feat(mp3,voice): fix scroll anim audio glitch, add seek/volume/history UI
- Mp3Player: * Disable LV_LABEL_LONG_SCROLL_CIRCULAR during playback (flag ENABLE_TITLE_SCROLL_ANIM_WHEN_IDLE) to avoid screen invalidation starving audio_stream * Single-line controls [-10s][Play/Pause][Stop][+10s] icon-only, volume slider at bottom, no card/toolbar, time label MM:SS / MM:SS updating every sec * Seek ±10s with proportional byte calc and correct sample count (was byte vs sample bug) * Safe history persistence: heap buffer (not stack), recursive mkdir, play_history.txt and volume.txt in user data * History only for >=10min audios, updated on pause/stop/hide, auto-resume latest * Full-screen history screen with Back button, open via top-right list icon, playing file highlighted CHECKED, tap to resume long or restart short - VoiceRecorder: * Fix crash from lv_label_set_long_mode on icon child in list buttons (force DOT only on last child label) * Force DOT on status and memo list to avoid scroll anim during audio * Throttle UI updates (1Hz rec, %/sec change play) Tested on 192.168.68.107, no crash, heap free ~15KB, screenshot verified
This commit is contained in:
@@ -4,3 +4,4 @@ idf_component_register(
|
||||
SRCS ${SOURCE_FILES}
|
||||
REQUIRES TactilitySDK esp_http_client
|
||||
)
|
||||
target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-deprecated-declarations -Wno-unused-but-set-variable -Wno-format-truncation)
|
||||
|
||||
+759
-112
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,18 @@
|
||||
#define CHUNK_BYTES (CHUNK_SAMPLES * (BITS_PER_SAMPLE / 8)) // 1024 bytes
|
||||
#define AUDIO_BUF_SIZE 4096
|
||||
|
||||
// ─── Scroll animation flags ───
|
||||
// LVGL label scroll animation (LV_LABEL_LONG_SCROLL_*) causes continuous
|
||||
// invalidations and screen refreshes that can starve the audio_stream task.
|
||||
// LVGL's lv_list_add_button() internally sets SCROLL_CIRCULAR by default (see
|
||||
// lv_list.c), so we must override it for memo names.
|
||||
//
|
||||
// User request: apply flag only to recordings names on the list.
|
||||
#define ENABLE_SCROLL_ANIM_WHEN_IDLE 1
|
||||
#define ENABLE_SCROLL_ANIM_DURING_AUDIO 0
|
||||
#define ENABLE_MEMO_LIST_SCROLL_ANIM 0 // 0 = DOT truncation (no anim, safe for audio), 1 = SCROLL_CIRCULAR
|
||||
#define ENABLE_MEMO_LIST_SCROLL_DURING_AUDIO 0 // Force DOT during recording/playback regardless of above
|
||||
|
||||
#define MAX_MEMOS 50
|
||||
#define MAX_FILENAME_LEN 64
|
||||
|
||||
@@ -208,6 +220,8 @@ static void record_task(void* arg) {
|
||||
|
||||
size_t total_written = 0;
|
||||
uint32_t start_time = xTaskGetTickCount();
|
||||
uint32_t last_ui_sec = 0;
|
||||
bool first_ui = true;
|
||||
|
||||
while (ctx->state == STATE_RECORDING) {
|
||||
size_t bytes_read = 0;
|
||||
@@ -223,13 +237,18 @@ static void record_task(void* arg) {
|
||||
}
|
||||
|
||||
uint32_t elapsed_sec = (xTaskGetTickCount() - start_time) / configTICK_RATE_HZ;
|
||||
char status_buf[64];
|
||||
snprintf(status_buf, sizeof(status_buf), "🔴 Recording... %02u:%02u", (unsigned)(elapsed_sec / 60), (unsigned)(elapsed_sec % 60));
|
||||
// Throttle UI updates to once per second to reduce LVGL invalidations during recording
|
||||
if (first_ui || elapsed_sec != last_ui_sec) {
|
||||
first_ui = false;
|
||||
last_ui_sec = elapsed_sec;
|
||||
char status_buf[64];
|
||||
snprintf(status_buf, sizeof(status_buf), "🔴 Recording... %02u:%02u", (unsigned)(elapsed_sec / 60), (unsigned)(elapsed_sec % 60));
|
||||
|
||||
tt_lvgl_lock(portMAX_DELAY);
|
||||
lv_label_set_text(ctx->lbl_status, status_buf);
|
||||
lv_bar_set_value(ctx->bar_progress, (int)((elapsed_sec * 100 / 300)) % 100, LV_ANIM_OFF);
|
||||
tt_lvgl_unlock();
|
||||
tt_lvgl_lock(portMAX_DELAY);
|
||||
lv_label_set_text(ctx->lbl_status, status_buf);
|
||||
lv_bar_set_value(ctx->bar_progress, (int)((elapsed_sec * 100 / 300)) % 100, LV_ANIM_OFF);
|
||||
tt_lvgl_unlock();
|
||||
}
|
||||
}
|
||||
|
||||
audio_stream_close(ctx->input_handle);
|
||||
@@ -349,6 +368,9 @@ static void play_task(void* arg) {
|
||||
}
|
||||
|
||||
bool out_open = true;
|
||||
int last_pct = -1;
|
||||
uint32_t last_elapsed_sec = 0;
|
||||
bool first_ui = true;
|
||||
|
||||
while (total_played < data_size && ctx->state != STATE_IDLE) {
|
||||
if (ctx->state == STATE_PAUSED) {
|
||||
@@ -407,15 +429,21 @@ static void play_task(void* arg) {
|
||||
uint32_t elapsed_sec = total_played / bytes_per_sec;
|
||||
uint32_t total_sec = data_size / bytes_per_sec;
|
||||
|
||||
char status_buf[64];
|
||||
snprintf(status_buf, sizeof(status_buf), "🔊 Playing... %02u:%02u / %02u:%02u",
|
||||
(unsigned)(elapsed_sec / 60), (unsigned)(elapsed_sec % 60),
|
||||
(unsigned)(total_sec / 60), (unsigned)(total_sec % 60));
|
||||
// Throttle UI updates – only when % or second changes, to reduce LVGL work during audio
|
||||
if (first_ui || pct != last_pct || elapsed_sec != last_elapsed_sec) {
|
||||
first_ui = false;
|
||||
last_pct = pct;
|
||||
last_elapsed_sec = elapsed_sec;
|
||||
char status_buf[64];
|
||||
snprintf(status_buf, sizeof(status_buf), "🔊 Playing... %02u:%02u / %02u:%02u",
|
||||
(unsigned)(elapsed_sec / 60), (unsigned)(elapsed_sec % 60),
|
||||
(unsigned)(total_sec / 60), (unsigned)(total_sec % 60));
|
||||
|
||||
tt_lvgl_lock(portMAX_DELAY);
|
||||
lv_label_set_text(ctx->lbl_status, status_buf);
|
||||
lv_bar_set_value(ctx->bar_progress, pct, LV_ANIM_OFF);
|
||||
tt_lvgl_unlock();
|
||||
tt_lvgl_lock(portMAX_DELAY);
|
||||
lv_label_set_text(ctx->lbl_status, status_buf);
|
||||
lv_bar_set_value(ctx->bar_progress, pct, LV_ANIM_OFF);
|
||||
tt_lvgl_unlock();
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
@@ -549,17 +577,91 @@ static void refresh_memo_list(AppCtx* ctx) {
|
||||
for (int i = 0; i < ctx->memo_count; i++) {
|
||||
lv_obj_t* btn = lv_list_add_btn(ctx->lst_memos, LV_SYMBOL_AUDIO, ctx->memos[i]);
|
||||
lv_obj_add_event_cb(btn, on_memo_selected, LV_EVENT_CLICKED, ctx);
|
||||
|
||||
// ─── Fix: LVGL list buttons default to SCROLL_CIRCULAR (see lv_list.c)
|
||||
// This causes constant invalidations that starve audio_stream.
|
||||
// User wants flag to disable this for recording names.
|
||||
// Safe handling: last child is always the label (icon is first if present)
|
||||
uint32_t child_cnt = lv_obj_get_child_cnt(btn);
|
||||
if (child_cnt > 0) {
|
||||
lv_obj_t* label = lv_obj_get_child(btn, child_cnt - 1);
|
||||
if (label) {
|
||||
#if ENABLE_MEMO_LIST_SCROLL_ANIM
|
||||
// Allow scrolling when flag enabled – but force DOT during audio if that flag is off
|
||||
#if ENABLE_MEMO_LIST_SCROLL_DURING_AUDIO
|
||||
lv_label_set_long_mode(label, LV_LABEL_LONG_SCROLL_CIRCULAR);
|
||||
#else
|
||||
// During audio we still force DOT; this func runs at list refresh (idle time)
|
||||
// To respect flag for idle, we set SCROLL here; update_ui will force DOT during audio if needed
|
||||
lv_label_set_long_mode(label, LV_LABEL_LONG_SCROLL_CIRCULAR);
|
||||
#endif
|
||||
#else
|
||||
lv_label_set_long_mode(label, LV_LABEL_LONG_DOT);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if (i == ctx->selected_index) {
|
||||
lv_obj_add_state(btn, LV_STATE_CHECKED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to force DOT on memo list during audio playback/recording if needed
|
||||
static void set_memo_list_scroll_enabled(AppCtx* ctx, bool enable) {
|
||||
if (!ctx->lst_memos) return;
|
||||
#if ENABLE_MEMO_LIST_SCROLL_DURING_AUDIO
|
||||
// If flag allows scroll during audio, do nothing
|
||||
if (enable) return;
|
||||
#endif
|
||||
// Force all memo labels to DOT (no animation) when audio active
|
||||
uint32_t list_child_cnt = lv_obj_get_child_cnt(ctx->lst_memos);
|
||||
for (uint32_t i = 0; i < list_child_cnt; i++) {
|
||||
lv_obj_t* btn = lv_obj_get_child(ctx->lst_memos, i);
|
||||
if (!btn) continue;
|
||||
uint32_t btn_child_cnt = lv_obj_get_child_cnt(btn);
|
||||
if (btn_child_cnt == 0) continue;
|
||||
lv_obj_t* label = lv_obj_get_child(btn, btn_child_cnt - 1);
|
||||
if (label) {
|
||||
lv_label_set_long_mode(label, enable ?
|
||||
#if ENABLE_MEMO_LIST_SCROLL_ANIM
|
||||
LV_LABEL_LONG_SCROLL_CIRCULAR
|
||||
#else
|
||||
LV_LABEL_LONG_DOT
|
||||
#endif
|
||||
: LV_LABEL_LONG_DOT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to control label scrolling based on audio state
|
||||
static void set_status_scroll_enabled(AppCtx* ctx, bool enable) {
|
||||
if (!ctx->lbl_status) return;
|
||||
if (enable) {
|
||||
#if ENABLE_SCROLL_ANIM_WHEN_IDLE
|
||||
// Allow scrolling when idle if flag says so, otherwise DOT
|
||||
// We intentionally keep status as DOT even when idle to avoid
|
||||
// unnecessary redraws, but this hook lets future tweaks enable it.
|
||||
lv_label_set_long_mode(ctx->lbl_status, LV_LABEL_LONG_DOT);
|
||||
#else
|
||||
lv_label_set_long_mode(ctx->lbl_status, LV_LABEL_LONG_DOT);
|
||||
#endif
|
||||
} else {
|
||||
#if ENABLE_SCROLL_ANIM_DURING_AUDIO
|
||||
lv_label_set_long_mode(ctx->lbl_status, LV_LABEL_LONG_SCROLL_CIRCULAR);
|
||||
#else
|
||||
lv_label_set_long_mode(ctx->lbl_status, LV_LABEL_LONG_DOT);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
static void update_ui(AppCtx* ctx) {
|
||||
if (ctx->lbl_status == NULL) return;
|
||||
|
||||
switch (ctx->state) {
|
||||
case STATE_RECORDING:
|
||||
set_status_scroll_enabled(ctx, false);
|
||||
set_memo_list_scroll_enabled(ctx, false);
|
||||
lv_obj_add_state(ctx->btn_record, LV_STATE_DISABLED);
|
||||
lv_obj_add_state(ctx->btn_play_pause, LV_STATE_DISABLED);
|
||||
lv_obj_clear_state(ctx->btn_stop, LV_STATE_DISABLED);
|
||||
@@ -567,6 +669,8 @@ static void update_ui(AppCtx* ctx) {
|
||||
break;
|
||||
|
||||
case STATE_PLAYING:
|
||||
set_status_scroll_enabled(ctx, false);
|
||||
set_memo_list_scroll_enabled(ctx, false);
|
||||
lv_obj_add_state(ctx->btn_record, LV_STATE_DISABLED);
|
||||
lv_obj_clear_state(ctx->btn_play_pause, LV_STATE_DISABLED);
|
||||
lv_label_set_text(lv_obj_get_child(ctx->btn_play_pause, 0), LV_SYMBOL_PAUSE);
|
||||
@@ -575,6 +679,8 @@ static void update_ui(AppCtx* ctx) {
|
||||
break;
|
||||
|
||||
case STATE_PAUSED:
|
||||
set_status_scroll_enabled(ctx, false);
|
||||
set_memo_list_scroll_enabled(ctx, false);
|
||||
lv_obj_add_state(ctx->btn_record, LV_STATE_DISABLED);
|
||||
lv_obj_clear_state(ctx->btn_play_pause, LV_STATE_DISABLED);
|
||||
lv_label_set_text(lv_obj_get_child(ctx->btn_play_pause, 0), LV_SYMBOL_PLAY);
|
||||
@@ -584,6 +690,8 @@ static void update_ui(AppCtx* ctx) {
|
||||
|
||||
case STATE_IDLE:
|
||||
default:
|
||||
set_status_scroll_enabled(ctx, true);
|
||||
set_memo_list_scroll_enabled(ctx, true);
|
||||
lv_obj_clear_state(ctx->btn_record, LV_STATE_DISABLED);
|
||||
|
||||
if (ctx->selected_index >= 0 && ctx->selected_index < ctx->memo_count) {
|
||||
@@ -642,6 +750,7 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
||||
lv_obj_set_width(g_ctx.lbl_status, lv_pct(95));
|
||||
lv_obj_set_style_text_align(g_ctx.lbl_status, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_set_style_text_color(g_ctx.lbl_status, lv_color_hex(0xCDD6F4), 0);
|
||||
lv_label_set_long_mode(g_ctx.lbl_status, LV_LABEL_LONG_DOT);
|
||||
lv_label_set_text(g_ctx.lbl_status, "Scanning memos...");
|
||||
|
||||
g_ctx.bar_progress = lv_bar_create(card);
|
||||
|
||||
Reference in New Issue
Block a user