sdlamp2/src/sdlamp2.c
Michael Smith e3a2bca794 Add --debug flag for diagnosing controller input on handheld
Logs joystick enumeration at startup (name, GameController status, GUID)
and all SDL input events in the main loop to help diagnose why the retro
handheld's d-pad/buttons aren't recognized by the GameController API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 16:08:13 +01:00

901 lines
28 KiB
C

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libavutil/version.h>
#include <libswresample/swresample.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include "SDL.h"
#include "SDL_image.h"
#define MAX_FILES 64
#define SEEK_SECONDS 10.0
/* --- Decoder context --- */
typedef struct {
AVFormatContext* format_ctx;
AVCodecContext* codec_ctx;
AVPacket* packet;
AVFrame* frame;
SwrContext* swr_ctx;
uint8_t* swr_buf;
int swr_buf_size;
int audio_stream_index;
int64_t duration;
int64_t current_pts;
AVRational time_base;
SDL_bool eof;
SDL_Texture* album_art;
int art_width;
int art_height;
} Decoder;
/* --- Globals --- */
static SDL_Window* window = NULL;
static SDL_Renderer* renderer = NULL;
static SDL_AudioDeviceID audio_device = 0;
static SDL_AudioStream* stream = NULL;
static SDL_bool paused = SDL_TRUE;
static Decoder decoder = {0};
static char audio_dir[512] = ".";
static char current_file[512] = "";
static char audio_files[MAX_FILES][256];
static int num_audio_files = 0;
static int current_file_index = 0;
/* --- Focus / navigation --- */
#define FOCUS_VOLUME 0
#define FOCUS_REWIND 1
#define FOCUS_STOP 2
#define FOCUS_PLAY 3
#define FOCUS_FF 4
#define FOCUS_NEXT 5
#define FOCUS_COUNT 6
static float volume = 0.5f;
static int focus_index = FOCUS_PLAY;
static SDL_GameController* controller = NULL;
static int debug_mode = 0;
/* --- Utility --- */
static void panic_and_abort(const char* title, const char* text) {
fprintf(stderr, "PANIC: %s ... %s\n", title, text);
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, text, window);
SDL_Quit();
exit(1);
}
static double get_current_seconds(void) {
return decoder.current_pts * av_q2d(decoder.time_base);
}
static double get_duration_seconds(void) {
if (decoder.duration <= 0) return 0.0;
return decoder.duration * av_q2d(decoder.time_base);
}
/* --- Position persistence --- */
static void save_position(const char* filename, double seconds) {
char path[1024];
snprintf(path, sizeof(path), "%s/positions.txt", audio_dir);
char lines[MAX_FILES][512];
int num_lines = 0;
SDL_bool found = SDL_FALSE;
FILE* f = fopen(path, "r");
if (f) {
char line[512];
while (fgets(line, sizeof(line), f) && num_lines < MAX_FILES) {
char* nl = strchr(line, '\n');
if (nl) *nl = '\0';
char* tab = strchr(line, '\t');
if (tab) {
size_t name_len = (size_t)(tab - line);
if (name_len == strlen(filename) && strncmp(line, filename, name_len) == 0) {
snprintf(lines[num_lines], sizeof(lines[0]), "%s\t%.2f", filename, seconds);
found = SDL_TRUE;
} else {
strncpy(lines[num_lines], line, sizeof(lines[0]) - 1);
lines[num_lines][sizeof(lines[0]) - 1] = '\0';
}
num_lines++;
}
}
fclose(f);
}
if (!found && num_lines < MAX_FILES) {
snprintf(lines[num_lines], sizeof(lines[0]), "%s\t%.2f", filename, seconds);
num_lines++;
}
f = fopen(path, "w");
if (f) {
for (int i = 0; i < num_lines; i++) {
fprintf(f, "%s\n", lines[i]);
}
fclose(f);
}
}
static double load_position(const char* filename) {
char path[1024];
snprintf(path, sizeof(path), "%s/positions.txt", audio_dir);
FILE* f = fopen(path, "r");
if (!f) return 0.0;
char line[512];
while (fgets(line, sizeof(line), f)) {
char* tab = strchr(line, '\t');
if (tab) {
size_t name_len = (size_t)(tab - line);
if (name_len == strlen(filename) && strncmp(line, filename, name_len) == 0) {
fclose(f);
return atof(tab + 1);
}
}
}
fclose(f);
return 0.0;
}
/* --- Volume persistence --- */
static void save_volume(void) {
char path[1024];
snprintf(path, sizeof(path), "%s/volume.txt", audio_dir);
FILE* f = fopen(path, "w");
if (f) {
fprintf(f, "%.2f\n", volume);
fclose(f);
}
}
static float load_volume(void) {
char path[1024];
snprintf(path, sizeof(path), "%s/volume.txt", audio_dir);
FILE* f = fopen(path, "r");
if (!f) return 0.5f;
float v = 0.5f;
if (fscanf(f, "%f", &v) != 1) v = 0.5f;
fclose(f);
if (v < 0.0f) v = 0.0f;
if (v > 1.0f) v = 1.0f;
return v;
}
static void adjust_volume(float delta) {
volume += delta;
if (volume < 0.0f) volume = 0.0f;
if (volume > 1.0f) volume = 1.0f;
save_volume();
}
/* --- File scanning --- */
static int has_audio_extension(const char* name) {
const char* ext = strrchr(name, '.');
if (!ext) return 0;
return (strcasecmp(ext, ".m4a") == 0 || strcasecmp(ext, ".mp3") == 0 ||
strcasecmp(ext, ".wav") == 0 || strcasecmp(ext, ".ogg") == 0);
}
static int compare_strings(const void* a, const void* b) {
return strcmp((const char*)a, (const char*)b);
}
static void scan_audio_files(const char* dir) {
num_audio_files = 0;
DIR* d = opendir(dir);
if (!d) return;
struct dirent* entry;
while ((entry = readdir(d)) != NULL && num_audio_files < MAX_FILES) {
if (entry->d_name[0] == '.') continue;
if (has_audio_extension(entry->d_name)) {
strncpy(audio_files[num_audio_files], entry->d_name, 255);
audio_files[num_audio_files][255] = '\0';
num_audio_files++;
}
}
closedir(d);
qsort(audio_files, num_audio_files, sizeof(audio_files[0]), compare_strings);
}
/* --- Decoder --- */
static void decoder_close(void) {
if (decoder.album_art) {
SDL_DestroyTexture(decoder.album_art);
}
if (decoder.swr_buf) {
av_free(decoder.swr_buf);
}
if (decoder.swr_ctx) {
swr_free(&decoder.swr_ctx);
}
if (decoder.frame) {
av_frame_free(&decoder.frame);
}
if (decoder.packet) {
av_packet_free(&decoder.packet);
}
if (decoder.codec_ctx) {
avcodec_free_context(&decoder.codec_ctx);
}
if (decoder.format_ctx) {
avformat_close_input(&decoder.format_ctx);
}
memset(&decoder, 0, sizeof(decoder));
}
static SDL_bool decoder_open(const char* path) {
decoder_close();
if (avformat_open_input(&decoder.format_ctx, path, NULL, NULL) != 0) {
fprintf(stderr, "Could not open file: %s\n", path);
return SDL_FALSE;
}
if (avformat_find_stream_info(decoder.format_ctx, NULL) < 0) {
fprintf(stderr, "Could not find stream info: %s\n", path);
decoder_close();
return SDL_FALSE;
}
/* Find first audio stream (not hardcoding 0 since album art may be stream 0) */
decoder.audio_stream_index = -1;
for (unsigned int i = 0; i < decoder.format_ctx->nb_streams; i++) {
if (decoder.format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
decoder.audio_stream_index = (int)i;
break;
}
}
if (decoder.audio_stream_index < 0) {
fprintf(stderr, "No audio stream found: %s\n", path);
decoder_close();
return SDL_FALSE;
}
AVStream* audio_stream = decoder.format_ctx->streams[decoder.audio_stream_index];
AVCodecParameters* codecpar = audio_stream->codecpar;
const AVCodec* codec = avcodec_find_decoder(codecpar->codec_id);
if (!codec) {
fprintf(stderr, "Unsupported codec in: %s\n", path);
decoder_close();
return SDL_FALSE;
}
decoder.codec_ctx = avcodec_alloc_context3(codec);
if (avcodec_parameters_to_context(decoder.codec_ctx, codecpar) < 0) {
fprintf(stderr, "Could not copy codec params: %s\n", path);
decoder_close();
return SDL_FALSE;
}
if (avcodec_open2(decoder.codec_ctx, codec, NULL) < 0) {
fprintf(stderr, "Could not open codec for: %s\n", path);
decoder_close();
return SDL_FALSE;
}
decoder.packet = av_packet_alloc();
decoder.frame = av_frame_alloc();
/* Set up SwrContext: convert from decoder's native format to interleaved float stereo 48kHz */
#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 28, 100)
AVChannelLayout out_ch_layout = AV_CHANNEL_LAYOUT_STEREO;
int swr_err = swr_alloc_set_opts2(&decoder.swr_ctx, &out_ch_layout, AV_SAMPLE_FMT_FLT, 48000,
&decoder.codec_ctx->ch_layout, decoder.codec_ctx->sample_fmt,
decoder.codec_ctx->sample_rate, 0, NULL);
if (swr_err < 0 || !decoder.swr_ctx || swr_init(decoder.swr_ctx) < 0) {
#else
int64_t in_ch_layout = decoder.codec_ctx->channel_layout;
if (!in_ch_layout) {
in_ch_layout = av_get_default_channel_layout(decoder.codec_ctx->channels);
}
decoder.swr_ctx =
swr_alloc_set_opts(NULL, AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_FLT, 48000, in_ch_layout,
decoder.codec_ctx->sample_fmt, decoder.codec_ctx->sample_rate, 0, NULL);
if (!decoder.swr_ctx || swr_init(decoder.swr_ctx) < 0) {
#endif
fprintf(stderr, "Could not initialize resampler for: %s\n", path);
decoder_close();
return SDL_FALSE;
}
/* Store timing info */
decoder.time_base = audio_stream->time_base;
if (audio_stream->duration != AV_NOPTS_VALUE) {
decoder.duration = audio_stream->duration;
} else if (decoder.format_ctx->duration != AV_NOPTS_VALUE) {
decoder.duration =
av_rescale_q(decoder.format_ctx->duration, AV_TIME_BASE_Q, decoder.time_base);
} else {
decoder.duration = 0;
}
decoder.current_pts = 0;
decoder.eof = SDL_FALSE;
/* Create SDL audio stream (FIFO — swr does all format conversion) */
SDL_FreeAudioStream(stream);
stream = NULL;
stream = SDL_NewAudioStream(AUDIO_F32, 2, 48000, AUDIO_F32, 2, 48000);
if (!stream) {
fprintf(stderr, "Could not create audio stream: %s\n", SDL_GetError());
decoder_close();
return SDL_FALSE;
}
/* Extract album art if present */
for (unsigned int i = 0; i < decoder.format_ctx->nb_streams; i++) {
if (decoder.format_ctx->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) {
AVPacket* pic = &decoder.format_ctx->streams[i]->attached_pic;
SDL_RWops* rw = SDL_RWFromConstMem(pic->data, pic->size);
if (rw) {
SDL_Surface* surface = IMG_Load_RW(rw, 1);
if (surface) {
decoder.album_art = SDL_CreateTextureFromSurface(renderer, surface);
decoder.art_width = surface->w;
decoder.art_height = surface->h;
SDL_FreeSurface(surface);
}
}
break;
}
}
#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 28, 100)
printf("Opened: %s (%s, %d ch, %d Hz, fmt=%s)\n", path, codec->name,
codecpar->ch_layout.nb_channels, codecpar->sample_rate,
av_get_sample_fmt_name(decoder.codec_ctx->sample_fmt));
#else
printf("Opened: %s (%s, %d ch, %d Hz, fmt=%s)\n", path, codec->name, codecpar->channels,
codecpar->sample_rate, av_get_sample_fmt_name(decoder.codec_ctx->sample_fmt));
#endif
return SDL_TRUE;
}
static SDL_bool decoder_pump(int max_bytes) {
if (decoder.eof || !decoder.format_ctx) return SDL_FALSE;
int bytes_fed = 0;
while (bytes_fed < max_bytes) {
int ret = av_read_frame(decoder.format_ctx, decoder.packet);
if (ret < 0) {
decoder.eof = SDL_TRUE;
return SDL_FALSE;
}
if (decoder.packet->stream_index != decoder.audio_stream_index) {
av_packet_unref(decoder.packet);
continue;
}
ret = avcodec_send_packet(decoder.codec_ctx, decoder.packet);
av_packet_unref(decoder.packet);
if (ret < 0) continue;
while (avcodec_receive_frame(decoder.codec_ctx, decoder.frame) == 0) {
if (decoder.frame->pts != AV_NOPTS_VALUE) {
decoder.current_pts = decoder.frame->pts;
}
int out_samples = swr_get_out_samples(decoder.swr_ctx, decoder.frame->nb_samples);
int out_size = out_samples * 2 * (int)sizeof(float);
if (out_size > decoder.swr_buf_size) {
av_free(decoder.swr_buf);
decoder.swr_buf = (uint8_t*)av_malloc(out_size);
if (!decoder.swr_buf) {
decoder.swr_buf_size = 0;
continue;
}
decoder.swr_buf_size = out_size;
}
uint8_t* out_planes[1] = {decoder.swr_buf};
int converted =
swr_convert(decoder.swr_ctx, out_planes, out_samples,
(const uint8_t**)decoder.frame->extended_data, decoder.frame->nb_samples);
if (converted > 0) {
int converted_bytes = converted * 2 * (int)sizeof(float);
if (SDL_AudioStreamPut(stream, decoder.swr_buf, converted_bytes) < 0) {
fprintf(stderr, "SDL_AudioStreamPut failed: %s\n", SDL_GetError());
}
bytes_fed += converted_bytes;
}
}
}
return SDL_TRUE;
}
static void decoder_seek(double target_seconds) {
if (!decoder.format_ctx) return;
double duration_secs = get_duration_seconds();
if (target_seconds < 0.0) target_seconds = 0.0;
if (duration_secs > 0.0 && target_seconds > duration_secs) target_seconds = duration_secs;
int64_t target_ts = (int64_t)(target_seconds / av_q2d(decoder.time_base));
av_seek_frame(decoder.format_ctx, decoder.audio_stream_index, target_ts, AVSEEK_FLAG_BACKWARD);
avcodec_flush_buffers(decoder.codec_ctx);
SDL_AudioStreamClear(stream);
SDL_ClearQueuedAudio(audio_device);
decoder.current_pts = target_ts;
decoder.eof = SDL_FALSE;
}
/* --- File switching --- */
static void switch_file(int index) {
/* Save position of current file before switching */
if (current_file[0] && decoder.format_ctx) {
save_position(current_file, get_current_seconds());
}
/* Try to open the requested file, skipping corrupt ones */
for (int attempts = 0; attempts < num_audio_files; attempts++) {
int try_index = (index + attempts) % num_audio_files;
current_file_index = try_index;
strncpy(current_file, audio_files[try_index], sizeof(current_file) - 1);
current_file[sizeof(current_file) - 1] = '\0';
char path[1024];
snprintf(path, sizeof(path), "%s/%s", audio_dir, audio_files[try_index]);
if (decoder_open(path)) {
double pos = load_position(current_file);
if (pos > 1.0) {
decoder_seek(pos);
}
char title[256];
snprintf(title, sizeof(title), "SDLamp2 - %s", current_file);
SDL_SetWindowTitle(window, title);
paused = SDL_FALSE;
SDL_PauseAudioDevice(audio_device, 0);
return;
}
fprintf(stderr, "Failed to open: %s, trying next\n", path);
}
/* All files failed */
current_file[0] = '\0';
SDL_SetWindowTitle(window, "SDLamp2 - No playable files");
}
/* --- Navigation --- */
static void activate_focused_button(void) {
switch (focus_index) {
case FOCUS_REWIND: {
double pos = get_current_seconds() - SEEK_SECONDS;
decoder_seek(pos < 0.0 ? 0.0 : pos);
break;
}
case FOCUS_STOP:
if (!paused) {
paused = SDL_TRUE;
SDL_PauseAudioDevice(audio_device, 1);
if (current_file[0]) {
save_position(current_file, get_current_seconds());
}
}
break;
case FOCUS_PLAY:
if (paused && num_audio_files > 0) {
paused = SDL_FALSE;
SDL_PauseAudioDevice(audio_device, 0);
}
break;
case FOCUS_FF: {
double pos = get_current_seconds() + SEEK_SECONDS;
double dur = get_duration_seconds();
decoder_seek((dur > 0.0 && pos > dur) ? dur : pos);
break;
}
case FOCUS_NEXT:
if (num_audio_files > 0) {
int next = (current_file_index + 1) % num_audio_files;
switch_file(next);
}
break;
default:
break;
}
}
/* --- Main --- */
int main(int argc, char** argv) {
/* Parse arguments: [--debug] [audio_directory] */
int argi = 1;
while (argi < argc && argv[argi][0] == '-') {
if (strcmp(argv[argi], "--debug") == 0) {
debug_mode = 1;
} else {
fprintf(stderr, "Unknown option: %s\n", argv[argi]);
}
argi++;
}
if (argi < argc) {
strncpy(audio_dir, argv[argi], sizeof(audio_dir) - 1);
audio_dir[sizeof(audio_dir) - 1] = '\0';
}
/* Volume slider (left of buttons) */
const SDL_Rect volume_bg = {25, 390, 30, 80};
/* Button positions (bottom of window, centered) */
const SDL_Rect rewind_btn = {80, 390, 80, 80};
const SDL_Rect stop_btn = {180, 390, 80, 80};
const SDL_Rect play_btn = {280, 390, 80, 80};
const SDL_Rect ff_btn = {380, 390, 80, 80};
const SDL_Rect next_btn = {480, 390, 80, 80};
/* Array of focusable rects indexed by FOCUS_* constants */
const SDL_Rect* focus_rects[FOCUS_COUNT] = {&volume_bg, &rewind_btn, &stop_btn,
&play_btn, &ff_btn, &next_btn};
/* Sprite sheet source rects */
const SDL_Rect rewind_sprite = {0, 0, 200, 200};
const SDL_Rect play_sprite = {220, 0, 200, 200};
const SDL_Rect ff_sprite = {440, 0, 200, 200};
const SDL_Rect stop_sprite = {0, 220, 200, 200};
const SDL_Rect next_sprite = {440, 220, 200, 200};
/* Progress bar area */
const SDL_Rect progress_bg = {20, 360, 600, 15};
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER) != 0) {
panic_and_abort("SDL_Init failed!", SDL_GetError());
}
window = SDL_CreateWindow("SDLamp2", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480,
0);
if (!window) {
panic_and_abort("SDL_CreateWindow failed!", SDL_GetError());
}
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_PRESENTVSYNC);
if (!renderer) {
panic_and_abort("SDL_CreateRenderer failed!", SDL_GetError());
}
SDL_AudioSpec desired;
SDL_zero(desired);
desired.freq = 48000;
desired.format = AUDIO_F32;
desired.channels = 2;
desired.samples = 4096;
desired.callback = NULL;
audio_device = SDL_OpenAudioDevice(NULL, 0, &desired, NULL, 0);
if (audio_device == 0) {
panic_and_abort("Could not open audio device!", SDL_GetError());
}
/* Enumerate and open game controllers */
int num_joy = SDL_NumJoysticks();
if (debug_mode) {
printf("[debug] Joysticks detected: %d\n", num_joy);
for (int i = 0; i < num_joy; i++) {
SDL_JoystickGUID guid = SDL_JoystickGetDeviceGUID(i);
char guid_str[64];
SDL_JoystickGetGUIDString(guid, guid_str, sizeof(guid_str));
printf("[debug] #%d: \"%s\" | GameController=%s | GUID=%s\n", i,
SDL_JoystickNameForIndex(i), SDL_IsGameController(i) ? "yes" : "no", guid_str);
}
fflush(stdout);
}
for (int i = 0; i < num_joy; i++) {
if (SDL_IsGameController(i)) {
controller = SDL_GameControllerOpen(i);
if (controller) {
printf("Controller: %s\n", SDL_GameControllerName(controller));
break;
}
}
}
SDL_Surface* controls_surface = IMG_Load("controls.png");
if (!controls_surface) {
panic_and_abort("Could not load controls asset!", SDL_GetError());
}
SDL_Texture* controls_texture = SDL_CreateTextureFromSurface(renderer, controls_surface);
SDL_FreeSurface(controls_surface);
if (!controls_texture) {
panic_and_abort("Could not create controls texture!", SDL_GetError());
}
/* Scan directory and open first file */
scan_audio_files(audio_dir);
volume = load_volume();
if (num_audio_files > 0) {
switch_file(0);
} else {
SDL_SetWindowTitle(window, "SDLamp2 - No audio files found");
}
SDL_bool running = SDL_TRUE;
SDL_Event e;
while (running) {
/* --- Event handling --- */
while (SDL_PollEvent(&e)) {
/* Debug: log all input-related events */
if (debug_mode) {
switch (e.type) {
case SDL_KEYDOWN:
printf("[debug] KEYDOWN: sym=%s (0x%x) scancode=%d\n",
SDL_GetKeyName(e.key.keysym.sym), e.key.keysym.sym, e.key.keysym.scancode);
break;
case SDL_KEYUP:
printf("[debug] KEYUP: sym=%s (0x%x) scancode=%d\n",
SDL_GetKeyName(e.key.keysym.sym), e.key.keysym.sym, e.key.keysym.scancode);
break;
case SDL_CONTROLLERBUTTONDOWN:
printf("[debug] CONTROLLERBUTTONDOWN: button=%d (%s)\n", e.cbutton.button,
SDL_GameControllerGetStringForButton(e.cbutton.button));
break;
case SDL_CONTROLLERBUTTONUP:
printf("[debug] CONTROLLERBUTTONUP: button=%d (%s)\n", e.cbutton.button,
SDL_GameControllerGetStringForButton(e.cbutton.button));
break;
case SDL_JOYBUTTONDOWN:
printf("[debug] JOYBUTTONDOWN: joy=%d button=%d\n", e.jbutton.which,
e.jbutton.button);
break;
case SDL_JOYBUTTONUP:
printf("[debug] JOYBUTTONUP: joy=%d button=%d\n", e.jbutton.which, e.jbutton.button);
break;
case SDL_JOYHATMOTION:
printf("[debug] JOYHATMOTION: joy=%d hat=%d value=%d\n", e.jhat.which, e.jhat.hat,
e.jhat.value);
break;
case SDL_JOYAXISMOTION:
if (e.jaxis.value > 8000 || e.jaxis.value < -8000) {
printf("[debug] JOYAXISMOTION: joy=%d axis=%d value=%d\n", e.jaxis.which,
e.jaxis.axis, e.jaxis.value);
}
break;
case SDL_CONTROLLERDEVICEADDED: {
SDL_JoystickGUID guid = SDL_JoystickGetDeviceGUID(e.cdevice.which);
char guid_str[64];
SDL_JoystickGetGUIDString(guid, guid_str, sizeof(guid_str));
printf("[debug] CONTROLLERDEVICEADDED: index=%d name=\"%s\" GUID=%s\n", e.cdevice.which,
SDL_JoystickNameForIndex(e.cdevice.which), guid_str);
break;
}
case SDL_CONTROLLERDEVICEREMOVED:
printf("[debug] CONTROLLERDEVICEREMOVED: id=%d\n", e.cdevice.which);
break;
case SDL_JOYDEVICEADDED:
printf("[debug] JOYDEVICEADDED: index=%d name=\"%s\"\n", e.jdevice.which,
SDL_JoystickNameForIndex(e.jdevice.which));
break;
case SDL_JOYDEVICEREMOVED:
printf("[debug] JOYDEVICEREMOVED: id=%d\n", e.jdevice.which);
break;
}
fflush(stdout);
}
switch (e.type) {
case SDL_QUIT:
running = SDL_FALSE;
break;
case SDL_KEYDOWN:
switch (e.key.keysym.sym) {
case SDLK_LEFT:
focus_index = (focus_index - 1 + FOCUS_COUNT) % FOCUS_COUNT;
break;
case SDLK_RIGHT:
focus_index = (focus_index + 1) % FOCUS_COUNT;
break;
case SDLK_UP:
if (focus_index == FOCUS_VOLUME) adjust_volume(0.05f);
break;
case SDLK_DOWN:
if (focus_index == FOCUS_VOLUME) adjust_volume(-0.05f);
break;
case SDLK_RETURN:
case SDLK_KP_ENTER:
activate_focused_button();
break;
}
break;
case SDL_CONTROLLERBUTTONDOWN:
switch (e.cbutton.button) {
case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
focus_index = (focus_index - 1 + FOCUS_COUNT) % FOCUS_COUNT;
break;
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
focus_index = (focus_index + 1) % FOCUS_COUNT;
break;
case SDL_CONTROLLER_BUTTON_DPAD_UP:
if (focus_index == FOCUS_VOLUME) adjust_volume(0.05f);
break;
case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
if (focus_index == FOCUS_VOLUME) adjust_volume(-0.05f);
break;
case SDL_CONTROLLER_BUTTON_A:
activate_focused_button();
break;
}
break;
case SDL_CONTROLLERDEVICEADDED:
if (!controller && SDL_IsGameController(e.cdevice.which)) {
controller = SDL_GameControllerOpen(e.cdevice.which);
if (controller) {
SDL_Joystick* joy = SDL_GameControllerGetJoystick(controller);
SDL_JoystickGUID guid = SDL_JoystickGetGUID(joy);
char guid_str[64];
SDL_JoystickGetGUIDString(guid, guid_str, sizeof(guid_str));
printf("Controller added: %s (GUID: %s)\n", SDL_GameControllerName(controller),
guid_str);
}
}
break;
case SDL_CONTROLLERDEVICEREMOVED:
if (controller && e.cdevice.which ==
SDL_JoystickInstanceID(
SDL_GameControllerGetJoystick(controller))) {
SDL_GameControllerClose(controller);
controller = NULL;
printf("Controller removed\n");
}
break;
}
}
/* --- Feed decoder into SDL audio stream when buffer is low --- */
if (!paused && !decoder.eof) {
int threshold = 48000 * 2 * (int)sizeof(float); /* ~1 second of audio */
if (SDL_AudioStreamAvailable(stream) < threshold) {
decoder_pump(threshold);
}
}
/* --- Drain audio stream into device queue --- */
if (SDL_GetQueuedAudioSize(audio_device) < 8192) {
int available = SDL_AudioStreamAvailable(stream);
if (available > 0) {
int to_get = available < 32768 ? available : 32768;
static Uint8 transfer_buf[32768];
int got = SDL_AudioStreamGet(stream, transfer_buf, to_get);
if (got > 0) {
float* samples = (float*)transfer_buf;
int num_samples = got / (int)sizeof(float);
for (int i = 0; i < num_samples; i++) {
samples[i] *= volume;
}
SDL_QueueAudio(audio_device, transfer_buf, got);
}
}
}
/* --- Handle EOF: let queue drain, then auto-stop and reset --- */
if (decoder.eof && !paused) {
if (SDL_AudioStreamAvailable(stream) == 0 && SDL_GetQueuedAudioSize(audio_device) == 0) {
paused = SDL_TRUE;
SDL_PauseAudioDevice(audio_device, 1);
decoder_seek(0.0);
}
}
/* --- Rendering --- */
SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF);
SDL_RenderClear(renderer);
/* Album art (centered, aspect-preserving, in upper area) */
if (decoder.album_art && decoder.art_width > 0 && decoder.art_height > 0) {
int max_w = 600, max_h = 340;
float scale_w = (float)max_w / decoder.art_width;
float scale_h = (float)max_h / decoder.art_height;
float scale = scale_w < scale_h ? scale_w : scale_h;
int draw_w = (int)(decoder.art_width * scale);
int draw_h = (int)(decoder.art_height * scale);
SDL_Rect art_rect = {(640 - draw_w) / 2, (350 - draw_h) / 2, draw_w, draw_h};
SDL_RenderCopy(renderer, decoder.album_art, NULL, &art_rect);
}
/* Progress bar */
SDL_SetRenderDrawColor(renderer, 0xC0, 0xC0, 0xC0, 0xFF);
SDL_RenderFillRect(renderer, &progress_bg);
double dur = get_duration_seconds();
if (dur > 0.0) {
double cur = get_current_seconds();
double frac = cur / dur;
if (frac > 1.0) frac = 1.0;
if (frac < 0.0) frac = 0.0;
SDL_Rect fill = {progress_bg.x, progress_bg.y, (int)(progress_bg.w * frac), progress_bg.h};
SDL_SetRenderDrawColor(renderer, 0x50, 0x50, 0x50, 0xFF);
SDL_RenderFillRect(renderer, &fill);
}
/* Volume slider */
SDL_SetRenderDrawColor(renderer, 0xC0, 0xC0, 0xC0, 0xFF);
SDL_RenderFillRect(renderer, &volume_bg);
{
int fill_h = (int)(volume_bg.h * volume);
SDL_Rect vol_fill = {volume_bg.x, volume_bg.y + volume_bg.h - fill_h, volume_bg.w, fill_h};
SDL_SetRenderDrawColor(renderer, 0x50, 0x50, 0x50, 0xFF);
SDL_RenderFillRect(renderer, &vol_fill);
}
/* Buttons */
SDL_RenderCopy(renderer, controls_texture, &rewind_sprite, &rewind_btn);
SDL_RenderCopy(renderer, controls_texture, &stop_sprite, &stop_btn);
SDL_RenderCopy(renderer, controls_texture, &play_sprite, &play_btn);
SDL_RenderCopy(renderer, controls_texture, &ff_sprite, &ff_btn);
SDL_RenderCopy(renderer, controls_texture, &next_sprite, &next_btn);
/* Focus highlight — 3px blue border around focused element */
{
const SDL_Rect r = *focus_rects[focus_index];
const int t = 3;
SDL_SetRenderDrawColor(renderer, 0x00, 0x80, 0xFF, 0xFF);
SDL_Rect top = {r.x - t, r.y - t, r.w + 2 * t, t};
SDL_Rect bot = {r.x - t, r.y + r.h, r.w + 2 * t, t};
SDL_Rect lft = {r.x - t, r.y, t, r.h};
SDL_Rect rgt = {r.x + r.w, r.y, t, r.h};
SDL_RenderFillRect(renderer, &top);
SDL_RenderFillRect(renderer, &bot);
SDL_RenderFillRect(renderer, &lft);
SDL_RenderFillRect(renderer, &rgt);
}
SDL_RenderPresent(renderer);
}
/* --- Cleanup --- */
if (current_file[0] && decoder.format_ctx) {
save_position(current_file, get_current_seconds());
}
save_volume();
decoder_close();
SDL_FreeAudioStream(stream);
SDL_DestroyTexture(controls_texture);
if (controller) SDL_GameControllerClose(controller);
SDL_CloseAudioDevice(audio_device);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}