fix(bookplayer): remove fades from page playback
This commit is contained in:
@@ -32,10 +32,6 @@
|
|||||||
#define MAX_TITLE 128
|
#define MAX_TITLE 128
|
||||||
#define MAX_AUTHOR 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 {
|
typedef enum {
|
||||||
STATE_IDLE,
|
STATE_IDLE,
|
||||||
STATE_PLAYING,
|
STATE_PLAYING,
|
||||||
@@ -93,6 +89,10 @@ typedef struct {
|
|||||||
uint8_t stream_channels;
|
uint8_t stream_channels;
|
||||||
uint8_t stream_bits;
|
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;
|
TaskHandle_t playback_task_handle;
|
||||||
} AppCtx;
|
} AppCtx;
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ static AppCtx g_ctx;
|
|||||||
|
|
||||||
/* ─── Forward Declarations ─── */
|
/* ─── Forward Declarations ─── */
|
||||||
static void update_ui(AppCtx* ctx);
|
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 load_page(AppCtx* ctx, int page_index, bool start_audio);
|
||||||
static void return_to_picker(AppCtx* ctx);
|
static void return_to_picker(AppCtx* ctx);
|
||||||
static void audio_playback_task(void* arg);
|
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 ─── */
|
/* ─── 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) {
|
if (ctx->playback_task_handle != NULL) {
|
||||||
|
ctx->keep_stream_on_stop = keep_stream_open;
|
||||||
ctx->state = STATE_IDLE;
|
ctx->state = STATE_IDLE;
|
||||||
while (ctx->playback_task_handle != NULL) {
|
while (ctx->playback_task_handle != NULL) {
|
||||||
tt_lvgl_unlock();
|
tt_lvgl_unlock();
|
||||||
vTaskDelay(pdMS_TO_TICKS(10));
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
tt_lvgl_lock(portMAX_DELAY);
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
}
|
}
|
||||||
|
ctx->keep_stream_on_stop = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Load Page Content (Image, Caption, Audio path) ─── */
|
/* ─── Load Page Content (Image, Caption, Audio path) ─── */
|
||||||
static void load_page(AppCtx* ctx, int page_index, bool start_audio) {
|
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;
|
if (page_index < 0 || page_index >= ctx->page_count) return;
|
||||||
ctx->current_page = page_index;
|
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 ─── */
|
/* ─── Return to Book Picker Screen ─── */
|
||||||
static void return_to_picker(AppCtx* ctx) {
|
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);
|
close_stream_if_open(ctx);
|
||||||
|
|
||||||
if (ctx->manifest_root) {
|
if (ctx->manifest_root) {
|
||||||
@@ -419,7 +421,6 @@ static void play_mp3(AppCtx* ctx) {
|
|||||||
bool eof = false;
|
bool eof = false;
|
||||||
int sample_rate = 0;
|
int sample_rate = 0;
|
||||||
int channels = 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);
|
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;
|
channels = info.channels;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust Volume and apply a fade-in/out at the start/end of the page
|
// Adjust volume without altering the start or end of the narration.
|
||||||
// so the transition to the next page doesn't click.
|
|
||||||
int vol = ctx->volume;
|
int vol = ctx->volume;
|
||||||
int16_t* samples_ptr = (int16_t*)ctx->pcm_buf;
|
int16_t* samples_ptr = (int16_t*)ctx->pcm_buf;
|
||||||
size_t sample_count = (size_t)samples * info.channels;
|
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) {
|
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;
|
int32_t scaled = (int32_t)samples_ptr[i] * vol / 100;
|
||||||
scaled = (int32_t)((float)scaled * gain);
|
|
||||||
samples_ptr[i] = (int16_t)scaled;
|
samples_ptr[i] = (int16_t)scaled;
|
||||||
}
|
}
|
||||||
page_frames_written += frames_in_chunk;
|
|
||||||
|
|
||||||
// Write via audio_stream (resampled to native 44100 internally)
|
// Write via audio_stream (resampled to native 44100 internally)
|
||||||
size_t offset = 0;
|
size_t offset = 0;
|
||||||
@@ -536,12 +521,10 @@ static void play_mp3(AppCtx* ctx) {
|
|||||||
|
|
||||||
bool stopped_externally = (ctx->state == STATE_IDLE);
|
bool stopped_externally = (ctx->state == STATE_IDLE);
|
||||||
|
|
||||||
if (stopped_externally) {
|
if (stopped_externally && !ctx->keep_stream_on_stop) {
|
||||||
// User-initiated stop (prev/next, back, app close): tear the stream down.
|
// App exit and returning to the picker release the output device.
|
||||||
close_stream_if_open(ctx);
|
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) {
|
if (!stopped_externally) {
|
||||||
tt_lvgl_lock(portMAX_DELAY);
|
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);
|
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 total_played = 0;
|
||||||
size_t page_frames_written = 0;
|
|
||||||
bool stream_open = true;
|
bool stream_open = true;
|
||||||
|
|
||||||
while (total_played < data_size && ctx->state != STATE_IDLE) {
|
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);
|
size_t read_bytes = fread(ctx->audio_buf, 1, to_read, file);
|
||||||
if (read_bytes == 0) break;
|
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;
|
int vol = ctx->volume;
|
||||||
int16_t* samples_ptr = (int16_t*)ctx->audio_buf;
|
int16_t* samples_ptr = (int16_t*)ctx->audio_buf;
|
||||||
size_t sample_count = read_bytes / sizeof(int16_t);
|
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) {
|
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;
|
int32_t scaled = (int32_t)samples_ptr[i] * vol / 100;
|
||||||
scaled = (int32_t)((float)scaled * gain);
|
|
||||||
samples_ptr[i] = (int16_t)scaled;
|
samples_ptr[i] = (int16_t)scaled;
|
||||||
}
|
}
|
||||||
page_frames_written += frames_in_chunk;
|
|
||||||
|
|
||||||
// Write via audio_stream
|
// Write via audio_stream
|
||||||
size_t offset = 0;
|
size_t offset = 0;
|
||||||
@@ -702,12 +669,10 @@ static void play_wav(AppCtx* ctx) {
|
|||||||
|
|
||||||
bool stopped_externally = (ctx->state == STATE_IDLE);
|
bool stopped_externally = (ctx->state == STATE_IDLE);
|
||||||
|
|
||||||
if (stopped_externally) {
|
if (stopped_externally && !ctx->keep_stream_on_stop) {
|
||||||
// User-initiated stop (prev/next, back, app close): tear the stream down.
|
// App exit and returning to the picker release the output device.
|
||||||
close_stream_if_open(ctx);
|
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) {
|
if (!stopped_externally) {
|
||||||
tt_lvgl_lock(portMAX_DELAY);
|
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) {
|
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);
|
close_stream_if_open(&g_ctx);
|
||||||
|
|
||||||
if (g_ctx.manifest_root) {
|
if (g_ctx.manifest_root) {
|
||||||
|
|||||||
Reference in New Issue
Block a user