Compare commits
1 Commits
5519ed2f72
...
fd7af95bcf
| Author | SHA1 | Date | |
|---|---|---|---|
| fd7af95bcf |
@@ -488,6 +488,74 @@ static void play_task(void *arg)
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
// ─── Gamebook narration player (async, streamed, interruptible) ──────────────
|
||||
// One persistent task plays one WAV at a time, streaming straight from the SD
|
||||
// in small chunks (no full-file malloc → any length, RAM-safe). A new request
|
||||
// or a stop flag breaks the stream so tapping a choice cuts the narration.
|
||||
|
||||
static TaskHandle_t s_gb_task = NULL;
|
||||
static char s_gb_path[160];
|
||||
static volatile bool s_gb_new = false;
|
||||
static volatile bool s_gb_stop = false;
|
||||
|
||||
static void gb_play_task(void *arg)
|
||||
{
|
||||
(void) arg;
|
||||
uint8_t hdr[64];
|
||||
uint8_t buf[2048];
|
||||
for (;;) {
|
||||
if (!s_gb_new) { vTaskDelay(pdMS_TO_TICKS(20)); continue; }
|
||||
char path[160];
|
||||
strncpy(path, s_gb_path, sizeof(path) - 1);
|
||||
path[sizeof(path) - 1] = '\0';
|
||||
s_gb_new = false;
|
||||
s_gb_stop = false;
|
||||
|
||||
ensure_sd_mounted();
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) { ESP_LOGW(TAG, "gb: open %s failed", path); continue; }
|
||||
size_t hn = fread(hdr, 1, sizeof(hdr), f);
|
||||
wav_info_t wi;
|
||||
if (parse_wav_header(hdr, hn, &wi) != ESP_OK || wi.bits_per_sample != 16) {
|
||||
ESP_LOGW(TAG, "gb: bad/usupported WAV %s", path);
|
||||
fclose(f);
|
||||
continue;
|
||||
}
|
||||
fseek(f, wi.data_offset, SEEK_SET);
|
||||
size_t remaining = wi.data_size;
|
||||
while (remaining > 0 && !s_gb_stop && !s_gb_new) {
|
||||
size_t want = remaining < sizeof(buf) ? remaining : sizeof(buf);
|
||||
size_t got = fread(buf, 1, want, f);
|
||||
if (got == 0) break;
|
||||
size_t written = 0;
|
||||
if (s_spk_handle_local) {
|
||||
i2s_channel_write(s_spk_handle_local, buf, got, &written,
|
||||
pdMS_TO_TICKS(500));
|
||||
}
|
||||
remaining -= got;
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
void cmd_exec_play_file_async(const char *path)
|
||||
{
|
||||
if (!path || !path[0]) return;
|
||||
strncpy(s_gb_path, path, sizeof(s_gb_path) - 1);
|
||||
s_gb_path[sizeof(s_gb_path) - 1] = '\0';
|
||||
s_gb_stop = true; // interrupt whatever is playing
|
||||
s_gb_new = true; // … and pick up the new clip
|
||||
if (!s_gb_task) {
|
||||
xTaskCreate(gb_play_task, "gb_play", 4096, NULL, 4, &s_gb_task);
|
||||
}
|
||||
}
|
||||
|
||||
void cmd_exec_stop_play(void)
|
||||
{
|
||||
s_gb_stop = true;
|
||||
s_gb_new = false;
|
||||
}
|
||||
|
||||
// ─── op=play ──────────────────────────────────────────────────────────────────
|
||||
|
||||
static esp_err_t exec_play(const cJSON *a)
|
||||
|
||||
@@ -34,6 +34,14 @@ esp_err_t cmd_exec_handle(const char *data, size_t len);
|
||||
// Must be called from main.c after speaker_init(), before any CMD arrives.
|
||||
void cmd_exec_set_spk_handle(i2s_chan_handle_t h);
|
||||
|
||||
// Play a 16 kHz mono 16-bit WAV from the SD asynchronously, streamed in chunks
|
||||
// (RAM-safe for long narration) and INTERRUPTIBLE: a new call stops the current
|
||||
// clip and starts the new one. Used by the touch gamebook for passage audio.
|
||||
void cmd_exec_play_file_async(const char *path);
|
||||
|
||||
// Stop the async narration (no-op if nothing is playing).
|
||||
void cmd_exec_stop_play(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include "bsp/esp-bsp.h"
|
||||
#include "lvgl.h"
|
||||
#include "cmd_exec.h" // async WAV narration player
|
||||
|
||||
static const char *TAG = "gamebook";
|
||||
|
||||
@@ -124,6 +125,7 @@ static void menu_cb(lv_event_t *e);
|
||||
// ── Library menu ────────────────────────────────────────────────────────────
|
||||
static void show_library(void)
|
||||
{
|
||||
cmd_exec_stop_play(); // silence any passage narration
|
||||
if (s_book) { cJSON_Delete(s_book); s_book = NULL; s_passages = NULL; }
|
||||
ensure_root();
|
||||
add_title("Choisis ton histoire");
|
||||
@@ -147,6 +149,15 @@ static void enter_passage(const char *pid)
|
||||
ensure_root();
|
||||
add_title(cJSON_IsString(screen) ? screen->valuestring : "");
|
||||
add_text(cJSON_IsString(text) ? text->valuestring : "");
|
||||
|
||||
// Narration: play this passage's WAV from the SD if the pack has audio.
|
||||
// Interruptible — tapping a choice cuts it and plays the next passage.
|
||||
const cJSON *wav = cJSON_GetObjectItem(p, "wav");
|
||||
if (cJSON_IsString(wav) && wav->valuestring[0]) {
|
||||
char path[160];
|
||||
snprintf(path, sizeof(path), "%s/%s", GB_DIR, wav->valuestring);
|
||||
cmd_exec_play_file_async(path);
|
||||
}
|
||||
int n = cJSON_IsArray(choices) ? cJSON_GetArraySize(choices) : 0;
|
||||
if (n == 0) {
|
||||
add_button("Revenir au menu", menu_cb, NULL);
|
||||
|
||||
Reference in New Issue
Block a user