diff --git a/Apps/BookPlayer/main/Source/main.c b/Apps/BookPlayer/main/Source/main.c index 47133e4..957fccd 100644 --- a/Apps/BookPlayer/main/Source/main.c +++ b/Apps/BookPlayer/main/Source/main.c @@ -32,10 +32,6 @@ #define MAX_TITLE 128 #define MAX_AUTHOR 128 -// Fade-in/out applied at the start/end of each page's narration to avoid an -// amplitude step (audible "pop") at page boundaries. In frames per channel. -#define FADE_FRAMES 4096 - typedef enum { STATE_IDLE, STATE_PLAYING, @@ -93,6 +89,10 @@ typedef struct { uint8_t stream_channels; uint8_t stream_bits; + // Page navigation stops the decoder task but keeps a compatible output + // stream open for the next page. + bool keep_stream_on_stop; + TaskHandle_t playback_task_handle; } AppCtx; @@ -116,7 +116,7 @@ static AppCtx g_ctx; /* ─── Forward Declarations ─── */ static void update_ui(AppCtx* ctx); -static void wait_for_playback_task_to_exit(AppCtx* ctx); +static void wait_for_playback_task_to_exit(AppCtx* ctx, bool keep_stream_open); static void load_page(AppCtx* ctx, int page_index, bool start_audio); static void return_to_picker(AppCtx* ctx); static void audio_playback_task(void* arg); @@ -266,20 +266,22 @@ static void handle_audio_finished(AppCtx* ctx) { } /* ─── Helper to wait for playback thread to terminate safely ─── */ -static void wait_for_playback_task_to_exit(AppCtx* ctx) { +static void wait_for_playback_task_to_exit(AppCtx* ctx, bool keep_stream_open) { if (ctx->playback_task_handle != NULL) { + ctx->keep_stream_on_stop = keep_stream_open; ctx->state = STATE_IDLE; while (ctx->playback_task_handle != NULL) { tt_lvgl_unlock(); vTaskDelay(pdMS_TO_TICKS(10)); tt_lvgl_lock(portMAX_DELAY); } + ctx->keep_stream_on_stop = false; } } /* ─── Load Page Content (Image, Caption, Audio path) ─── */ static void load_page(AppCtx* ctx, int page_index, bool start_audio) { - wait_for_playback_task_to_exit(ctx); + wait_for_playback_task_to_exit(ctx, true); if (page_index < 0 || page_index >= ctx->page_count) return; ctx->current_page = page_index; @@ -351,7 +353,7 @@ static void load_page(AppCtx* ctx, int page_index, bool start_audio) { /* ─── Return to Book Picker Screen ─── */ static void return_to_picker(AppCtx* ctx) { - wait_for_playback_task_to_exit(ctx); + wait_for_playback_task_to_exit(ctx, false); close_stream_if_open(ctx); if (ctx->manifest_root) { @@ -419,7 +421,6 @@ static void play_mp3(AppCtx* ctx) { bool eof = false; int sample_rate = 0; int channels = 0; - size_t page_frames_written = 0; ESP_LOGI(TAG, "Starting MP3 playback via audio-stream: %s (%d bytes)", ctx->current_audio_path, file_size); @@ -474,30 +475,14 @@ static void play_mp3(AppCtx* ctx) { channels = info.channels; } - // Adjust Volume and apply a fade-in/out at the start/end of the page - // so the transition to the next page doesn't click. + // Adjust volume without altering the start or end of the narration. int vol = ctx->volume; int16_t* samples_ptr = (int16_t*)ctx->pcm_buf; size_t sample_count = (size_t)samples * info.channels; - uint8_t ch = (uint8_t)info.channels; - size_t bytes_per_frame = (size_t)ch * sizeof(int16_t); - size_t frames_in_chunk = (size_t)samples; - float remaining_frames = (file_size > bytes_read_total) - ? (float)(file_size - bytes_read_total) / (float)bytes_per_frame : 0.0f; for (size_t i = 0; i < sample_count; ++i) { - size_t frame = page_frames_written + i / ch; - float fade_in = (frame < FADE_FRAMES) ? (float)frame / (float)FADE_FRAMES : 1.0f; - float fade_out = 1.0f; - float remaining = remaining_frames - (float)(i / ch); - if (remaining < (float)FADE_FRAMES) { - fade_out = (remaining > 0.0f) ? remaining / (float)FADE_FRAMES : 0.0f; - } - float gain = fade_in * fade_out; int32_t scaled = (int32_t)samples_ptr[i] * vol / 100; - scaled = (int32_t)((float)scaled * gain); samples_ptr[i] = (int16_t)scaled; } - page_frames_written += frames_in_chunk; // Write via audio_stream (resampled to native 44100 internally) size_t offset = 0; @@ -536,12 +521,10 @@ static void play_mp3(AppCtx* ctx) { bool stopped_externally = (ctx->state == STATE_IDLE); - if (stopped_externally) { - // User-initiated stop (prev/next, back, app close): tear the stream down. + if (stopped_externally && !ctx->keep_stream_on_stop) { + // App exit and returning to the picker release the output device. close_stream_if_open(ctx); } - // On a natural page finish we deliberately LEAVE the stream open so the next - // page reuses it, instead of closing+recreating the codec (which pops). if (!stopped_externally) { tt_lvgl_lock(portMAX_DELAY); @@ -621,7 +604,6 @@ static void play_wav(AppCtx* ctx) { ESP_LOGI(TAG, "Starting WAV via audio-stream: %s (%u Hz, %u ch)", ctx->current_audio_path, (unsigned int)header.sample_rate, (unsigned int)header.channels); size_t total_played = 0; - size_t page_frames_written = 0; bool stream_open = true; while (total_played < data_size && ctx->state != STATE_IDLE) { @@ -643,29 +625,14 @@ static void play_wav(AppCtx* ctx) { size_t read_bytes = fread(ctx->audio_buf, 1, to_read, file); if (read_bytes == 0) break; - // Scaling Volume + fade-in/out at page boundaries to avoid a click. + // Adjust volume without altering the start or end of the narration. int vol = ctx->volume; int16_t* samples_ptr = (int16_t*)ctx->audio_buf; size_t sample_count = read_bytes / sizeof(int16_t); - uint8_t ch = (header.channels > 0) ? header.channels : 1; - size_t bytes_per_frame = (size_t)ch * sizeof(int16_t); - size_t frames_in_chunk = read_bytes / bytes_per_frame; - float remaining_frames = (data_size > total_played) - ? (float)(data_size - total_played) / (float)bytes_per_frame : 0.0f; for (size_t i = 0; i < sample_count; ++i) { - size_t frame = page_frames_written + i / ch; - float fade_in = (frame < FADE_FRAMES) ? (float)frame / (float)FADE_FRAMES : 1.0f; - float fade_out = 1.0f; - float remaining = remaining_frames - (float)(i / ch); - if (remaining < (float)FADE_FRAMES) { - fade_out = (remaining > 0.0f) ? remaining / (float)FADE_FRAMES : 0.0f; - } - float gain = fade_in * fade_out; int32_t scaled = (int32_t)samples_ptr[i] * vol / 100; - scaled = (int32_t)((float)scaled * gain); samples_ptr[i] = (int16_t)scaled; } - page_frames_written += frames_in_chunk; // Write via audio_stream size_t offset = 0; @@ -702,12 +669,10 @@ static void play_wav(AppCtx* ctx) { bool stopped_externally = (ctx->state == STATE_IDLE); - if (stopped_externally) { - // User-initiated stop (prev/next, back, app close): tear the stream down. + if (stopped_externally && !ctx->keep_stream_on_stop) { + // App exit and returning to the picker release the output device. close_stream_if_open(ctx); } - // On a natural page finish we deliberately LEAVE the stream open so the next - // page reuses it, instead of closing+recreating the codec (which pops). if (!stopped_externally) { tt_lvgl_lock(portMAX_DELAY); @@ -1063,7 +1028,7 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) { } static void onHideApp(AppHandle app, void* data) { - wait_for_playback_task_to_exit(&g_ctx); + wait_for_playback_task_to_exit(&g_ctx, false); close_stream_if_open(&g_ctx); if (g_ctx.manifest_root) {