From 9db8dfdd4891e2998723967545f2aaa36a804bda Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Tue, 10 Feb 2026 20:41:40 +0100 Subject: [PATCH] Implement streaming decoder, seeking, persistence, file switching, album art Replace fire-and-forget decode_audio() with a streaming Decoder that uses libswresample to convert planar float to interleaved stereo, fixing the sped-up audio bug and eliminating multi-GB memory usage for long files. Add 10-second rewind/fast-forward, stop (pause in place), position persistence per file via positions.txt, directory scanning with file switching, embedded album art display, and a progress bar. Handles both old and new FFmpeg channel layout APIs via version preprocessor check. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + docs/sdlamp2-fsd.md | 14 + src/sdlamp2.c | 691 ++++++++++++++++++++++++++++++++++---------- 3 files changed, 558 insertions(+), 148 deletions(-) diff --git a/.gitignore b/.gitignore index ed31b20..1decd85 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /build/sdlamp2.dSYM *.mp3 *.m4a +positions.txt diff --git a/docs/sdlamp2-fsd.md b/docs/sdlamp2-fsd.md index cb6532e..50d3f7a 100644 --- a/docs/sdlamp2-fsd.md +++ b/docs/sdlamp2-fsd.md @@ -37,3 +37,17 @@ This document specifies the functional requirements for an SDL2 based media play - Keep a changelog in this functional specification document ## 5. Changelog + +### 2026-02-10 — Full implementation of audio player features + +- **Streaming decoder**: Replaced fire-and-forget `decode_audio()` with a persistent `Decoder` struct that streams audio on demand via `decoder_pump()`. Uses `libswresample` (`swr_alloc_set_opts2`) to convert from the decoder's native format (e.g. planar float) to interleaved float stereo 48kHz. Fixes the sped-up/distorted audio bug and eliminates the multi-GB memory spike for long files. +- **Seeking**: Rewind (10s back) and fast-forward (10s ahead) via `av_seek_frame()` with codec buffer flush and audio pipeline clear. Clamped to file bounds. +- **Play/Stop separation**: Removed play/pause toggle. Play always resumes, stop always pauses in place and saves position. No icon toggling. +- **Position persistence**: Saves/loads playback position per file in `positions.txt` (tab-separated) in the audio directory. Position saved on stop, quit, and file switch. Restored on file open. +- **File selection**: Scans audio directory for `.m4a`, `.mp3`, `.wav`, `.ogg` files. Sorted alphabetically. 5th button ("next tape") cycles through files. Window title shows current filename. +- **Album art**: Extracts embedded cover art (`AV_DISPOSITION_ATTACHED_PIC`) and displays it scaled with preserved aspect ratio in the upper portion of the window. +- **Progress bar**: Gray bar between album art and controls showing playback position relative to duration. +- **Command-line argument**: First argument sets audio directory (defaults to current working directory). +- **Error handling**: Non-fatal errors (stream ops, corrupt files) use `fprintf(stderr)` and continue. Corrupt files are skipped when switching. Fatal errors (SDL init, window, audio device) still abort. Proper cleanup order on exit. +- **EOF handling**: When a file plays to the end, playback auto-pauses and resets to the start. +- **Removed dead code**: `load_audio_file()`, `wavbuf`/`wavlen`/`wavspec` globals. diff --git a/src/sdlamp2.c b/src/sdlamp2.c index 712f034..d523ccb 100644 --- a/src/sdlamp2.c +++ b/src/sdlamp2.c @@ -1,20 +1,58 @@ #include #include #include +#include +#include + +#include +#include +#include +#include +#include #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 Uint8* wavbuf = NULL; -static Uint32 wavlen = 0; -static SDL_AudioSpec wavspec; static SDL_AudioStream* stream = NULL; -// static SDL_bool paused = SDL_TRUE; -static SDL_bool paused = SDL_FALSE; +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; + +/* --- Utility --- */ static void panic_and_abort(const char* title, const char* text) { fprintf(stderr, "PANIC: %s ... %s\n", title, text); @@ -23,126 +61,419 @@ static void panic_and_abort(const char* title, const char* text) { exit(1); } -static SDL_bool load_audio_file(const char* fname) { - SDL_FreeAudioStream(stream); - stream = NULL; - SDL_FreeWAV(wavbuf); - wavbuf = NULL; - wavlen = 0; - - if (SDL_LoadWAV(fname, &wavspec, &wavbuf, &wavlen) == NULL) { - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Couldn't load wav file!", SDL_GetError(), - window); - return SDL_FALSE; - } - - stream = SDL_NewAudioStream(wavspec.format, wavspec.channels, wavspec.freq, AUDIO_F32, 2, 48000); - if (!stream) { - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Couldn't create audio stream!", SDL_GetError(), - window); - SDL_FreeWAV(wavbuf); - wavbuf = NULL; - wavlen = 0; - } - - if (SDL_AudioStreamPut(stream, wavbuf, wavlen) == -1) { // FIXME(m): graceful handling. - panic_and_abort("Audio stream put failed!", SDL_GetError()); - } - SDL_AudioStreamFlush(stream); // FIXME(m): error handling. - SDL_PauseAudioDevice(audio_device, paused); - - return SDL_TRUE; +static double get_current_seconds(void) { + return decoder.current_pts * av_q2d(decoder.time_base); } -static SDL_bool decode_audio() { - printf("libavutil version: %s\n", av_version_info()); +static double get_duration_seconds(void) { + if (decoder.duration <= 0) return 0.0; + return decoder.duration * av_q2d(decoder.time_base); +} - // Trying to open and decode AAC / ALAC (.m4a file) with ffmpeg libraries - AVFormatContext* pFormatContext = avformat_alloc_context(); - if (avformat_open_input(&pFormatContext, "./build/music.m4a", NULL, NULL) != 0) { - printf("ERROR: could not open the file!\n"); - return SDL_FALSE; - } - printf("Format: %s\n", pFormatContext->iformat->long_name); +/* --- Position persistence --- */ - avformat_find_stream_info(pFormatContext, NULL); - if (pFormatContext->nb_streams < 1) { - printf("ERROR: no valid streams found in file!\n"); - return SDL_FALSE; - } +static void save_position(const char* filename, double seconds) { + char path[1024]; + snprintf(path, sizeof(path), "%s/positions.txt", audio_dir); - AVCodecParameters* pLocalCodecParameters = pFormatContext->streams[0]->codecpar; - if (pLocalCodecParameters->codec_type != AVMEDIA_TYPE_AUDIO) { - printf("ERROR: no audio stream found in file!\n"); - return SDL_FALSE; - } + char lines[MAX_FILES][512]; + int num_lines = 0; + SDL_bool found = SDL_FALSE; - printf("Audio stream: %d channels, sample rate %d\n", - pLocalCodecParameters->channels, pLocalCodecParameters->sample_rate); - - const AVCodec* pLocalCodec = avcodec_find_decoder(pLocalCodecParameters->codec_id); - printf("Codec: %s ID %d bit_rate %lld\n", pLocalCodec->long_name, pLocalCodec->id, - pLocalCodecParameters->bit_rate); - - AVCodecContext* pCodecContext = avcodec_alloc_context3(pLocalCodec); - avcodec_parameters_to_context(pCodecContext, pLocalCodecParameters); - avcodec_open2(pCodecContext, pLocalCodec, NULL); - - AVPacket* pPacket = av_packet_alloc(); - AVFrame* pFrame = av_frame_alloc(); - - SDL_FreeAudioStream(stream); - stream = NULL; - wavlen = 0; - - stream = SDL_NewAudioStream(AUDIO_F32, pLocalCodecParameters->channels, - pLocalCodecParameters->sample_rate, AUDIO_F32, 2, 48000); - if (!stream) { - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Couldn't create audio stream!", SDL_GetError(), - window); - } - - while (av_read_frame(pFormatContext, pPacket) >= 0) { - if (!avcodec_send_packet(pCodecContext, pPacket)) { - avcodec_receive_frame(pCodecContext, pFrame); - if (pFrame->linesize[0] > 0) { - if (SDL_AudioStreamPut(stream, pFrame->data[0], pFrame->linesize[0]) == - -1) { // FIXME(m): graceful handling. - panic_and_abort("Audio stream put failed!", SDL_GetError()); + 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++; } } - - // printf("Frame %c (%lld) pts %lld dts %lld\n", av_get_picture_type_char(pFrame->pict_type), - // pCodecContext->frame_num, pFrame->pts, pFrame->pkt_dts); + fclose(f); } - SDL_AudioStreamFlush(stream); // FIXME(m): error handling. - SDL_PauseAudioDevice(audio_device, paused); + 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; +} + +/* --- 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; } -int main(int argc, char** argv) { - SDL_AudioSpec desired; - SDL_bool running = SDL_TRUE; - const SDL_Rect rewind_rect = {10, 10, 100, 100}; - const SDL_Rect stop_rect = {120, 10, 100, 100}; - const SDL_Rect play_pause_rect = {230, 10, 100, 100}; - const SDL_Rect fast_forward_rect = {340, 10, 100, 100}; +static SDL_bool decoder_pump(int max_bytes) { + if (decoder.eof || !decoder.format_ctx) return SDL_FALSE; - const SDL_Rect rewind_symbol_rect = {0, 0, 200, 200}; - const SDL_Rect play_symbol_rect = {220, 0, 200, 200}; - const SDL_Rect fast_forward_symbol_rect = {440, 0, 200, 200}; - const SDL_Rect stop_symbol_rect = {0, 220, 200, 200}; - const SDL_Rect pause_symbol_rect = {220, 220, 200, 200}; + 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"); +} + +/* --- Main --- */ + +int main(int argc, char** argv) { + if (argc > 1) { + strncpy(audio_dir, argv[1], sizeof(audio_dir) - 1); + audio_dir[sizeof(audio_dir) - 1] = '\0'; + } + + /* 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}; + + /* 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) != 0) { panic_and_abort("SDL_Init failed!", SDL_GetError()); } - window = - SDL_CreateWindow("SDLamp2", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, 0); + window = SDL_CreateWindow("SDLamp2", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, + 0); if (!window) { panic_and_abort("SDL_CreateWindow failed!", SDL_GetError()); } @@ -152,6 +483,7 @@ int main(int argc, char** argv) { panic_and_abort("SDL_CreateRenderer failed!", SDL_GetError()); } + SDL_AudioSpec desired; SDL_zero(desired); desired.freq = 48000; desired.format = AUDIO_F32; @@ -164,23 +496,30 @@ int main(int argc, char** argv) { panic_and_abort("Could not open audio device!", SDL_GetError()); } - // load_audio_file("music.wav"); - decode_audio(); - SDL_Surface* controls_surface = IMG_Load("controls.png"); - if (controls_surface == NULL) { + 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 == NULL) { - panic_and_abort("Could not load controls asset!", SDL_GetError()); + if (!controls_texture) { + panic_and_abort("Could not create controls texture!", SDL_GetError()); } + /* Scan directory and open first file */ + scan_audio_files(audio_dir); + 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 loop + /* --- Event handling --- */ while (SDL_PollEvent(&e)) { switch (e.type) { case SDL_QUIT: @@ -189,60 +528,116 @@ int main(int argc, char** argv) { case SDL_MOUSEBUTTONDOWN: { const SDL_Point pt = {e.button.x, e.button.y}; - if (SDL_PointInRect(&pt, - &rewind_rect)) { // Pressed the "rewind" button - SDL_ClearQueuedAudio(audio_device); - SDL_AudioStreamClear(stream); - if (SDL_AudioStreamPut(stream, wavbuf, wavlen) == -1) { // FIXME(m): graceful handling. - panic_and_abort("Audio stream put failed!", SDL_GetError()); + + if (SDL_PointInRect(&pt, &rewind_btn)) { + double pos = get_current_seconds() - SEEK_SECONDS; + decoder_seek(pos < 0.0 ? 0.0 : pos); + } else if (SDL_PointInRect(&pt, &stop_btn)) { + if (!paused) { + paused = SDL_TRUE; + SDL_PauseAudioDevice(audio_device, 1); + if (current_file[0]) { + save_position(current_file, get_current_seconds()); + } + } + } else if (SDL_PointInRect(&pt, &play_btn)) { + if (paused && num_audio_files > 0) { + paused = SDL_FALSE; + SDL_PauseAudioDevice(audio_device, 0); + } + } else if (SDL_PointInRect(&pt, &ff_btn)) { + double pos = get_current_seconds() + SEEK_SECONDS; + double dur = get_duration_seconds(); + decoder_seek((dur > 0.0 && pos > dur) ? dur : pos); + } else if (SDL_PointInRect(&pt, &next_btn)) { + if (num_audio_files > 0) { + int next = (current_file_index + 1) % num_audio_files; + switch_file(next); } - SDL_AudioStreamFlush(stream); // FIXME(m): error handling. - } else if (SDL_PointInRect(&pt, - &play_pause_rect)) { // Pressed the "pause" button - paused = paused ? SDL_FALSE : SDL_TRUE; - SDL_PauseAudioDevice(audio_device, paused); } break; } } } - // Deal with audio - if (SDL_GetQueuedAudioSize(audio_device) < 8192) { - const int bytes_remaining = SDL_AudioStreamAvailable(stream); - if (bytes_remaining > 0) { - const int new_bytes = SDL_min(bytes_remaining, 32 * 1024); - static Uint8 converted_buffer[32 * 1024]; - SDL_AudioStreamGet(stream, converted_buffer, - new_bytes); // FIXME(m): error handling. - SDL_QueueAudio(audio_device, converted_buffer, new_bytes); + /* --- 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); } } - // Deal with drawing to screen + /* --- 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) { + 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); - SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF); - SDL_RenderFillRect(renderer, &rewind_rect); - SDL_RenderFillRect(renderer, &stop_rect); - SDL_RenderFillRect(renderer, &play_pause_rect); - SDL_RenderFillRect(renderer, &fast_forward_rect); - - SDL_RenderCopy(renderer, controls_texture, &rewind_symbol_rect, &rewind_rect); - SDL_RenderCopy(renderer, controls_texture, &stop_symbol_rect, &stop_rect); - if (paused) { - SDL_RenderCopy(renderer, controls_texture, &play_symbol_rect, &play_pause_rect); - } else { - SDL_RenderCopy(renderer, controls_texture, &pause_symbol_rect, &play_pause_rect); + /* 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); } - SDL_RenderCopy(renderer, controls_texture, &fast_forward_symbol_rect, &fast_forward_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); + } + + /* 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); SDL_RenderPresent(renderer); } - // Clean up - SDL_FreeWAV(wavbuf); + /* --- Cleanup --- */ + if (current_file[0] && decoder.format_ctx) { + save_position(current_file, get_current_seconds()); + } + decoder_close(); + SDL_FreeAudioStream(stream); + SDL_DestroyTexture(controls_texture); SDL_CloseAudioDevice(audio_device); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window);