mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-10-16 21:10:11 +00:00
Merge PR #316: WAV/MP3 playback for Lua apps
oumike. Closes #315 (pisti87's request). Two conflicts, both resolved by keeping BOTH sides rather than choosing: - sdRuntimeLifecycleBusy() gained an audio-playback source here and a web reader source in #317. They are independent consumers of the same card and both have to gate the mount lifecycle. The reader's self-exclusion is kept: it calls this from its own task while holding the card and would otherwise deadlock against itself. - The Lua harness caps table needed sd_list from #312 as well as the audio flags, and the test order needed the wardrive suite from #324 as well as audio_api. Built on all eight S3 envs and both ESP32-P4 targets.
This commit is contained in:
+851
-34
@@ -19,6 +19,20 @@
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
#include <cerrno> // chat-store write diagnostics surface errno (ENFILE vs ENOSPC vs EIO)
|
||||
#if CAP_LUA_AUDIO
|
||||
static void* wadaMp3Scratch();
|
||||
#define MINIMP3_ONLY_MP3
|
||||
#define MINIMP3_NO_SIMD
|
||||
#define MINIMP3_SCRATCH ((mp3dec_scratch_t*)wadaMp3Scratch())
|
||||
#define MINIMP3_IMPLEMENTATION
|
||||
#include "../../lib/minimp3/minimp3.h"
|
||||
#undef MINIMP3_IMPLEMENTATION
|
||||
#undef MINIMP3_SCRATCH
|
||||
#undef MINIMP3_NO_SIMD
|
||||
#undef MINIMP3_ONLY_MP3
|
||||
static void* s_wada_mp3_scratch = nullptr;
|
||||
static void* wadaMp3Scratch() { return s_wada_mp3_scratch; }
|
||||
#endif
|
||||
#if defined(ESP32)
|
||||
#include <time.h>
|
||||
#include <SPIFFS.h>
|
||||
@@ -52,6 +66,7 @@
|
||||
#include <esp_core_dump.h> // detect/read/erase the panic coredump for the crash-report export
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h> // xTaskGetCurrentTaskHandleForCPU / pcTaskGetName — Task-WDT crash self-record
|
||||
#include <freertos/queue.h>
|
||||
#else
|
||||
// Tanmatsu's 16M.csv has no coredump partition yet — stub so the crash-export compiles + links.
|
||||
#include <esp_err.h>
|
||||
@@ -843,6 +858,56 @@ static inline void tileFetchPendingDec() {
|
||||
// below are shared; T-Deck and the pager each get their own I2S install/
|
||||
// tone/WAV functions, since the pager additionally drives an ES8311 codec
|
||||
// over I2C that the T-Deck's plain MAX98357A DAC doesn't have.
|
||||
#if CAP_AUDIO_STREAM
|
||||
static uint32_t wavRd32(File& f){ uint8_t b[4]; if(f.read(b,4)!=4) return 0; return (uint32_t)b[0]|((uint32_t)b[1]<<8)|((uint32_t)b[2]<<16)|((uint32_t)b[3]<<24); }
|
||||
static uint16_t wavRd16(File& f){ uint8_t b[2]; if(f.read(b,2)!=2) return 0; return (uint16_t)(b[0]|(b[1]<<8)); }
|
||||
static bool wavParse(File& f, uint16_t* pch, uint32_t* prate, uint32_t* pdata) {
|
||||
char tag[4];
|
||||
if (f.read((uint8_t*)tag,4)!=4 || memcmp(tag,"RIFF",4)) return false;
|
||||
const uint32_t riff_size = wavRd32(f);
|
||||
const uint64_t riff_end = 8u + (uint64_t)riff_size;
|
||||
if (riff_size < 4 || riff_end > f.size() || riff_end > 0xFFFFFFFFu) return false;
|
||||
if (f.read((uint8_t*)tag,4)!=4 || memcmp(tag,"WAVE",4)) return false;
|
||||
uint16_t fmt=0, ch=0, bits=0; uint32_t rate=0, dlen=0; bool hf=false, hd=false;
|
||||
while ((uint64_t)f.position() + 8u <= riff_end) {
|
||||
if (f.read((uint8_t*)tag,4)!=4) break;
|
||||
uint32_t csz = wavRd32(f);
|
||||
if (!memcmp(tag,"fmt ",4)) {
|
||||
if (csz < 16) return false;
|
||||
fmt = wavRd16(f); ch = wavRd16(f); rate = wavRd32(f); wavRd32(f); wavRd16(f); bits = wavRd16(f);
|
||||
const uint64_t skip = (uint64_t)(csz - 16) + (csz & 1u);
|
||||
const uint64_t next = (uint64_t)f.position() + skip;
|
||||
if (next > riff_end || (skip && !f.seek((uint32_t)next))) return false;
|
||||
hf = true;
|
||||
} else if (!memcmp(tag,"data",4)) {
|
||||
if ((uint64_t)f.position() + csz > riff_end) return false;
|
||||
dlen = csz; hd = true; break;
|
||||
} else {
|
||||
const uint64_t skip = (uint64_t)csz + (csz & 1u);
|
||||
const uint64_t next = (uint64_t)f.position() + skip;
|
||||
if (next > riff_end || (skip && !f.seek((uint32_t)next))) return false;
|
||||
}
|
||||
}
|
||||
if (!hf || !hd || fmt != 1 || bits != 16 || (ch != 1 && ch != 2) || rate < 8000 || rate > 48000)
|
||||
return false;
|
||||
if (pch) *pch = ch; if (prate) *prate = rate; if (pdata) *pdata = dlen;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if CAP_LUA_AUDIO
|
||||
static volatile bool s_lua_audio_active = false;
|
||||
static volatile uint32_t s_lua_audio_storage_pending = 0;
|
||||
static volatile bool s_lua_audio_storage_active = false;
|
||||
static inline bool luaAudioActive() {
|
||||
return __atomic_load_n(&s_lua_audio_active, __ATOMIC_ACQUIRE);
|
||||
}
|
||||
static inline bool luaAudioStorageBusy() {
|
||||
return __atomic_load_n(&s_lua_audio_storage_pending, __ATOMIC_ACQUIRE) != 0 ||
|
||||
__atomic_load_n(&s_lua_audio_storage_active, __ATOMIC_ACQUIRE);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(HELTEC_LORA_V4_R8) || defined(HAS_THINKNODE_M9)
|
||||
static bool fmSdTryMount(); // V4-R8/M9 microSD — fwd decl (defined in the mount-helper block below; sdRestoreRun needs it)
|
||||
#endif
|
||||
@@ -999,29 +1064,6 @@ static void pagerPlayToneRaw(int freq, int ms) {
|
||||
// amp. Supports PCM 16-bit, mono or stereo (downmixed to the mono amp), sample
|
||||
// rate read from the header; capped to ~6 s so a stray big file can't hold the
|
||||
// notify task. Any problem -> false, and the caller falls back to the chime.
|
||||
static uint32_t wavRd32(File& f){ uint8_t b[4]; if(f.read(b,4)!=4) return 0; return (uint32_t)b[0]|((uint32_t)b[1]<<8)|((uint32_t)b[2]<<16)|((uint32_t)b[3]<<24); }
|
||||
static uint16_t wavRd16(File& f){ uint8_t b[2]; if(f.read(b,2)!=2) return 0; return (uint16_t)(b[0]|(b[1]<<8)); }
|
||||
static bool wavParse(File& f, uint16_t* pch, uint32_t* prate, uint32_t* pdata) {
|
||||
char tag[4];
|
||||
if (f.read((uint8_t*)tag,4)!=4 || memcmp(tag,"RIFF",4)) return false;
|
||||
wavRd32(f);
|
||||
if (f.read((uint8_t*)tag,4)!=4 || memcmp(tag,"WAVE",4)) return false;
|
||||
uint16_t fmt=0, ch=0, bits=0; uint32_t rate=0, dlen=0; bool hf=false, hd=false;
|
||||
while (f.available() >= 8) {
|
||||
if (f.read((uint8_t*)tag,4)!=4) break;
|
||||
uint32_t csz = wavRd32(f);
|
||||
if (!memcmp(tag,"fmt ",4)) {
|
||||
fmt = wavRd16(f); ch = wavRd16(f); rate = wavRd32(f); wavRd32(f); wavRd16(f); bits = wavRd16(f);
|
||||
if (csz > 16) f.seek(f.position() + (csz - 16));
|
||||
hf = true;
|
||||
} else if (!memcmp(tag,"data",4)) { dlen = csz; hd = true; break; }
|
||||
else f.seek(f.position() + csz + (csz & 1));
|
||||
}
|
||||
if (!hf || !hd || fmt != 1 || bits != 16 || (ch != 1 && ch != 2) || rate < 8000 || rate > 48000)
|
||||
return false;
|
||||
if (pch) *pch = ch; if (prate) *prate = rate; if (pdata) *pdata = dlen;
|
||||
return true;
|
||||
}
|
||||
static bool wavOpen(const char* prefpath, File& f) {
|
||||
if (!prefpath || !prefpath[0]) return false;
|
||||
fs::FS* fsp = &SPIFFS; const char* fp = prefpath;
|
||||
@@ -1313,6 +1355,9 @@ static void tanBeep(); // I2S notification tick; defined far below (with the C
|
||||
#endif
|
||||
// Play the platform's notification chime. Caller checks the buzzer/sound pref.
|
||||
static inline void uiPlaySlot(int slot) {
|
||||
#if CAP_LUA_AUDIO
|
||||
if (luaAudioActive()) return;
|
||||
#endif
|
||||
#if defined(HAS_TDECK_GT911)
|
||||
tdeckPlayNotifySlot(slot);
|
||||
#elif defined(HAS_TDISPLAY_P4)
|
||||
@@ -1332,6 +1377,9 @@ static inline void uiPlayMention() { uiPlaySlot(TOUCH_SND_MEN); }
|
||||
// Preview an arbitrary WAV file (not yet saved to a slot) -- used by the
|
||||
// sound picker's "play" button before the user commits to a choice.
|
||||
static inline void uiPreviewWavFile(const char* path) {
|
||||
#if CAP_LUA_AUDIO
|
||||
if (luaAudioActive()) return;
|
||||
#endif
|
||||
#if defined(HAS_TDECK_GT911)
|
||||
tdeckPreviewWavFile(path);
|
||||
#elif defined(TLORA_PAGER)
|
||||
@@ -20152,8 +20200,10 @@ static void fmOpenStorage(fs::FS* fs, const char* store, const char* path) {
|
||||
}
|
||||
static void fmInternalClickCb(lv_event_t* e) {
|
||||
if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;
|
||||
#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4)
|
||||
fmOpenStorage(&FFat, "Internal", "/"); // the internal FAT data partition (locfd / 'storage')
|
||||
#if defined(HAS_TANMATSU)
|
||||
fmOpenStorage(&FFat, "Internal", "/"); // internal FAT data partition (locfd)
|
||||
#elif defined(HAS_TDISPLAY_P4)
|
||||
fmOpenStorage(&LittleFS, "Internal", "/"); // internal LittleFS 'storage' partition
|
||||
#else
|
||||
fmOpenStorage(&SPIFFS, "Internal", "/");
|
||||
#endif
|
||||
@@ -21671,6 +21721,9 @@ static uint64_t s_tan_sd_size = 0;
|
||||
static uint32_t s_tan_sd_retry_after = 0; // backoff so an absent/cold card isn't re-probed every render
|
||||
static bool tanSdTryMount() {
|
||||
if (s_tan_sd_mounted) return true;
|
||||
#if CAP_LUA_AUDIO
|
||||
if (luaAudioStorageBusy()) return false;
|
||||
#endif
|
||||
if (millis() < s_tan_sd_retry_after) return false;
|
||||
#if defined(HAS_TDISPLAY_P4)
|
||||
// Hot-insert path (no-op when main.cpp already mounted at boot): 20 MHz like the boot ladder,
|
||||
@@ -39793,6 +39846,11 @@ static void tanKbBacklightTick(bool off = false) {
|
||||
if (v != s_last) { s_last = v; bsp_input_set_backlight_brightness(v); }
|
||||
}
|
||||
static uint8_t s_volume_pct = 70;
|
||||
static SemaphoreHandle_t s_tan_audio_mutex = nullptr;
|
||||
static SemaphoreHandle_t tanAudioMutex() {
|
||||
if (!s_tan_audio_mutex) s_tan_audio_mutex = xSemaphoreCreateMutex();
|
||||
return s_tan_audio_mutex;
|
||||
}
|
||||
// The audio subsystem (ES8156 codec + I2S) is brought up by bsp_device_initialize()
|
||||
// at boot — so here we only set the codec volume and toggle the speaker amplifier.
|
||||
static void applyVolume(uint8_t pct) {
|
||||
@@ -39809,11 +39867,13 @@ static void applyVolume(uint8_t pct) {
|
||||
// which leaves every descriptor zeroed once played out, and the tone stops cleanly.
|
||||
static void tanBeep() {
|
||||
if (s_volume_pct == 0) return;
|
||||
SemaphoreHandle_t mutex = tanAudioMutex();
|
||||
if (!mutex || xSemaphoreTake(mutex, 0) != pdTRUE) return;
|
||||
static uint32_t s_last_beep = 0; // throttle: holding the slider ramps fast, but
|
||||
if (millis() - s_last_beep < 200) return; // don't machine-gun a tone on every repeat step
|
||||
if (millis() - s_last_beep < 200) { xSemaphoreGive(mutex); return; } // don't machine-gun a tone on every repeat step
|
||||
s_last_beep = millis();
|
||||
i2s_chan_handle_t h = nullptr;
|
||||
if (bsp_audio_get_i2s_handle(&h) != ESP_OK || !h) return;
|
||||
if (bsp_audio_get_i2s_handle(&h) != ESP_OK || !h) { xSemaphoreGive(mutex); return; }
|
||||
const int rate = 44100, freq = 880;
|
||||
const int tone = rate * 30 / 1000; // ~30 ms tone …
|
||||
const int total = tone + rate * 50 / 1000; // … then ~50 ms silence (> the DMA ring) flushes the loop
|
||||
@@ -39832,6 +39892,7 @@ static void tanBeep() {
|
||||
}
|
||||
size_t wr = 0;
|
||||
i2s_channel_write(h, buf, (size_t)n * 2 * sizeof(int16_t), &wr, 200 / portTICK_PERIOD_MS);
|
||||
xSemaphoreGive(mutex);
|
||||
}
|
||||
#elif defined(HAS_TDISPLAY_P4)
|
||||
// T-Display P4: the RM69A10 AMOLED has no backlight — brightness is the panel's own DCS
|
||||
@@ -47303,18 +47364,22 @@ static bool uiDataFsReady() {
|
||||
}
|
||||
#endif
|
||||
#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4)
|
||||
// Tanmatsu + T-Display P4: prefer the microSD card. On the Tanmatsu the internal FFat 'locfd'
|
||||
// loses frequently-rewritten data (broken FAT metadata; see the tile-cache notes); on the
|
||||
// T-Display P4 SD-first keeps history on the same medium the DataStore adopts. Mirror the
|
||||
// T-Deck's /meshcomod root; fall back to FFat only when no card is present. Both are mounted at
|
||||
// boot in main.cpp (g_sd_ok / g_fs_ok). (The P4 used to fall into the #else SPIFFS branch below —
|
||||
// no SPIFFS partition exists there, so history was never persisted and vanished on every reboot.)
|
||||
// Tanmatsu + T-Display P4: use each board's mounted internal data partition
|
||||
// (Tanmatsu FFat 'locfd', P4 LittleFS 'storage'), with SD_MMC as fallback.
|
||||
// #167: hot UI data (chat history) lives on the INTERNAL LittleFS -- SD write bursts
|
||||
// electrically disturb this board's AMOLED, and the P4's internal FAT layer is broken
|
||||
// (see the storage note in tdisplay_p4/main/main.cpp). SD = degraded fallback only;
|
||||
// tiles keep using the SD via their own selector.
|
||||
extern bool g_fs_ok;
|
||||
if (g_fs_ok) { s_ui_data_fs = &LittleFS; s_ui_data_root[0] = '\0'; return true; }
|
||||
if (g_fs_ok) {
|
||||
#if defined(HAS_TANMATSU)
|
||||
s_ui_data_fs = &FFat;
|
||||
#else
|
||||
s_ui_data_fs = &LittleFS;
|
||||
#endif
|
||||
s_ui_data_root[0] = '\0';
|
||||
return true;
|
||||
}
|
||||
extern bool g_sd_ok;
|
||||
if (g_sd_ok) {
|
||||
SD_MMC.mkdir("/meshcomod");
|
||||
@@ -47441,6 +47506,752 @@ fs::FS* luaHostAppFs() { return uiDataFsReady() ? s_ui_data_fs : nullptr; }
|
||||
void luaHostAppPath(char* out, size_t cap, const char* rel) {
|
||||
snprintf(out, cap, "%s%s", s_ui_data_root, rel); // SD-rooted stores prefix /meshcomod
|
||||
}
|
||||
|
||||
#if CAP_LUA_AUDIO
|
||||
namespace {
|
||||
enum class LuaAudioCommandKind : uint8_t { Play, Pause, Resume, Stop, Release };
|
||||
enum class LuaAudioState : uint8_t { Stopped, Playing, Paused, Ended, Error };
|
||||
enum class LuaAudioRunResult : uint8_t { Ended, Stopped, Replaced, Released, Error };
|
||||
enum class LuaAudioTaskState : uint8_t { Stopped, Starting, Running, Stopping };
|
||||
enum class LuaAudioFormat : uint8_t { Wav, Mp3 };
|
||||
|
||||
struct LuaAudioCommand {
|
||||
LuaAudioCommandKind kind = LuaAudioCommandKind::Stop;
|
||||
fs::FS* fs = nullptr;
|
||||
uint32_t owner = 0;
|
||||
LuaAudioFormat format = LuaAudioFormat::Wav;
|
||||
char path[224] = "";
|
||||
char shown[192] = "";
|
||||
char source[8] = "";
|
||||
};
|
||||
|
||||
struct LuaAudioStatus {
|
||||
uint32_t owner = 0;
|
||||
LuaAudioState state = LuaAudioState::Stopped;
|
||||
char path[192] = "";
|
||||
char source[8] = "";
|
||||
char format[8] = "";
|
||||
char error[40] = "";
|
||||
};
|
||||
|
||||
struct LuaAudioSinkSession {
|
||||
uint32_t output_rate = 0;
|
||||
float gain = 1.0f;
|
||||
bool open = false;
|
||||
#if defined(HAS_TANMATSU)
|
||||
i2s_chan_handle_t handle = nullptr;
|
||||
SemaphoreHandle_t mutex = nullptr;
|
||||
#endif
|
||||
};
|
||||
|
||||
class LuaAudioStorageLease {
|
||||
public:
|
||||
LuaAudioStorageLease() {
|
||||
__atomic_store_n(&s_lua_audio_storage_active, true, __ATOMIC_RELEASE);
|
||||
if (__atomic_load_n(&s_lua_audio_storage_pending, __ATOMIC_ACQUIRE) != 0)
|
||||
__atomic_fetch_sub(&s_lua_audio_storage_pending, 1u, __ATOMIC_ACQ_REL);
|
||||
}
|
||||
~LuaAudioStorageLease() {
|
||||
__atomic_store_n(&s_lua_audio_storage_active, false, __ATOMIC_RELEASE);
|
||||
}
|
||||
};
|
||||
|
||||
static QueueHandle_t s_lua_audio_queue = nullptr;
|
||||
static TaskHandle_t s_lua_audio_task = nullptr;
|
||||
static LuaAudioTaskState s_lua_audio_task_state = LuaAudioTaskState::Stopped;
|
||||
static portMUX_TYPE s_lua_audio_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||
static LuaAudioStatus s_lua_audio_status;
|
||||
|
||||
static void luaAudioCopy(char* out, size_t cap, const char* value) {
|
||||
if (!cap) return;
|
||||
snprintf(out, cap, "%s", value ? value : "");
|
||||
}
|
||||
|
||||
static void luaAudioSetTrackStatus(const LuaAudioCommand& command, LuaAudioState state,
|
||||
const char* error = nullptr) {
|
||||
portENTER_CRITICAL(&s_lua_audio_mux);
|
||||
s_lua_audio_status.owner = command.owner;
|
||||
s_lua_audio_status.state = state;
|
||||
luaAudioCopy(s_lua_audio_status.path, sizeof s_lua_audio_status.path, command.shown);
|
||||
luaAudioCopy(s_lua_audio_status.source, sizeof s_lua_audio_status.source, command.source);
|
||||
luaAudioCopy(s_lua_audio_status.format, sizeof s_lua_audio_status.format,
|
||||
command.format == LuaAudioFormat::Mp3 ? "mp3" : "wav");
|
||||
luaAudioCopy(s_lua_audio_status.error, sizeof s_lua_audio_status.error, error);
|
||||
portEXIT_CRITICAL(&s_lua_audio_mux);
|
||||
}
|
||||
|
||||
static void luaAudioSetState(uint32_t owner, LuaAudioState state, const char* error = nullptr) {
|
||||
portENTER_CRITICAL(&s_lua_audio_mux);
|
||||
if (s_lua_audio_status.owner == owner) {
|
||||
s_lua_audio_status.state = state;
|
||||
luaAudioCopy(s_lua_audio_status.error, sizeof s_lua_audio_status.error, error);
|
||||
}
|
||||
portEXIT_CRITICAL(&s_lua_audio_mux);
|
||||
}
|
||||
|
||||
static bool luaAudioSinkBegin(uint32_t source_rate, LuaAudioSinkSession* sink,
|
||||
const char** error) {
|
||||
if (!sink) return false;
|
||||
const int volume = (int)touchPrefsGetSoundVolume();
|
||||
if (volume <= 0) { if (error) *error = "muted"; return false; }
|
||||
|
||||
#if defined(HAS_TDECK_GT911)
|
||||
if (s_notify_playing) { if (error) *error = "busy"; return false; }
|
||||
if (!tdeckAudioInstallRate((int)source_rate)) {
|
||||
if (error) *error = "audio unavailable";
|
||||
return false;
|
||||
}
|
||||
sink->output_rate = source_rate;
|
||||
sink->gain = (float)volume / 100.0f;
|
||||
#elif defined(TLORA_PAGER)
|
||||
if (s_notify_playing) { if (error) *error = "busy"; return false; }
|
||||
board.setAmpEnabled(true);
|
||||
if (!pagerAudioInstallRate((int)source_rate)) {
|
||||
board.setAmpEnabled(false);
|
||||
if (error) *error = "audio unavailable";
|
||||
return false;
|
||||
}
|
||||
s_pager_codec.setVolumePercent((uint8_t)volume);
|
||||
s_pager_codec.setMute(false);
|
||||
sink->output_rate = source_rate;
|
||||
#elif defined(HAS_TDISPLAY_P4)
|
||||
if (!p4AudioStreamBegin()) {
|
||||
if (error) *error = "audio unavailable";
|
||||
return false;
|
||||
}
|
||||
sink->output_rate = p4AudioStreamRate();
|
||||
sink->gain = (float)volume / 100.0f;
|
||||
#elif defined(HAS_TANMATSU)
|
||||
sink->mutex = tanAudioMutex();
|
||||
if (!sink->mutex || xSemaphoreTake(sink->mutex, pdMS_TO_TICKS(300)) != pdTRUE) {
|
||||
if (error) *error = "busy";
|
||||
return false;
|
||||
}
|
||||
if (bsp_audio_get_i2s_handle(&sink->handle) != ESP_OK || !sink->handle) {
|
||||
xSemaphoreGive(sink->mutex);
|
||||
sink->mutex = nullptr;
|
||||
if (error) *error = "audio unavailable";
|
||||
return false;
|
||||
}
|
||||
applyVolume((uint8_t)volume);
|
||||
sink->output_rate = 44100;
|
||||
#endif
|
||||
|
||||
sink->open = sink->output_rate != 0;
|
||||
return sink->open;
|
||||
}
|
||||
|
||||
static bool luaAudioSinkWrite(LuaAudioSinkSession* sink, const int16_t* samples, size_t frames) {
|
||||
if (!sink || !sink->open || !samples || !frames) return false;
|
||||
#if defined(HAS_TDECK_GT911) || defined(TLORA_PAGER)
|
||||
size_t written = 0;
|
||||
const size_t bytes = frames * sizeof(int16_t);
|
||||
return i2s_write(kI2sPort, samples, bytes, &written, pdMS_TO_TICKS(300)) == ESP_OK &&
|
||||
written == bytes;
|
||||
#elif defined(HAS_TDISPLAY_P4)
|
||||
return p4AudioStreamWrite(samples, frames);
|
||||
#elif defined(HAS_TANMATSU)
|
||||
static int16_t stereo[256 * 2];
|
||||
if (frames > 256) return false;
|
||||
for (size_t i = 0; i < frames; ++i)
|
||||
stereo[2 * i] = stereo[2 * i + 1] = samples[i];
|
||||
size_t written = 0;
|
||||
const size_t bytes = frames * 2 * sizeof(int16_t);
|
||||
return i2s_channel_write(sink->handle, stereo, bytes, &written, pdMS_TO_TICKS(300)) == ESP_OK &&
|
||||
written == bytes;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void luaAudioSinkEnd(LuaAudioSinkSession* sink) {
|
||||
if (!sink || !sink->open) return;
|
||||
#if defined(HAS_TDECK_GT911)
|
||||
i2s_zero_dma_buffer(kI2sPort);
|
||||
i2s_driver_uninstall(kI2sPort);
|
||||
#elif defined(TLORA_PAGER)
|
||||
pagerAudioUninstall();
|
||||
board.setAmpEnabled(false);
|
||||
#elif defined(HAS_TDISPLAY_P4)
|
||||
p4AudioStreamEnd();
|
||||
#elif defined(HAS_TANMATSU)
|
||||
static const int16_t silence[256 * 2] = {};
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
size_t written = 0;
|
||||
i2s_channel_write(sink->handle, silence, sizeof silence, &written, pdMS_TO_TICKS(300));
|
||||
}
|
||||
if (sink->mutex) xSemaphoreGive(sink->mutex);
|
||||
sink->handle = nullptr;
|
||||
sink->mutex = nullptr;
|
||||
#endif
|
||||
sink->open = false;
|
||||
}
|
||||
|
||||
static bool luaAudioControlMatches(const LuaAudioCommand& control, uint32_t owner) {
|
||||
return control.kind == LuaAudioCommandKind::Play || control.owner == owner;
|
||||
}
|
||||
|
||||
static bool luaAudioPollControl(const LuaAudioCommand& command,
|
||||
LuaAudioCommand* replacement,
|
||||
LuaAudioRunResult* result) {
|
||||
LuaAudioCommand control;
|
||||
if (xQueueReceive(s_lua_audio_queue, &control, 0) != pdTRUE ||
|
||||
!luaAudioControlMatches(control, command.owner)) return false;
|
||||
|
||||
if (control.kind == LuaAudioCommandKind::Play) {
|
||||
if (replacement) *replacement = control;
|
||||
if (result) *result = LuaAudioRunResult::Replaced;
|
||||
return true;
|
||||
}
|
||||
if (control.kind == LuaAudioCommandKind::Stop) {
|
||||
if (result) *result = LuaAudioRunResult::Stopped;
|
||||
return true;
|
||||
}
|
||||
if (control.kind == LuaAudioCommandKind::Release) {
|
||||
if (result) *result = LuaAudioRunResult::Released;
|
||||
return true;
|
||||
}
|
||||
if (control.kind != LuaAudioCommandKind::Pause) return false;
|
||||
|
||||
luaAudioSetState(command.owner, LuaAudioState::Paused);
|
||||
for (;;) {
|
||||
if (xQueueReceive(s_lua_audio_queue, &control, portMAX_DELAY) != pdTRUE) continue;
|
||||
if (!luaAudioControlMatches(control, command.owner)) continue;
|
||||
if (control.kind == LuaAudioCommandKind::Resume) {
|
||||
luaAudioSetState(command.owner, LuaAudioState::Playing);
|
||||
return false;
|
||||
}
|
||||
if (control.kind == LuaAudioCommandKind::Play) {
|
||||
if (replacement) *replacement = control;
|
||||
if (result) *result = LuaAudioRunResult::Replaced;
|
||||
return true;
|
||||
}
|
||||
if (control.kind == LuaAudioCommandKind::Release) {
|
||||
if (result) *result = LuaAudioRunResult::Released;
|
||||
return true;
|
||||
}
|
||||
if (control.kind == LuaAudioCommandKind::Stop) {
|
||||
if (result) *result = LuaAudioRunResult::Stopped;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static LuaAudioRunResult luaAudioRunTrack(const LuaAudioCommand& command,
|
||||
LuaAudioCommand* replacement) {
|
||||
LuaAudioStorageLease storage_lease;
|
||||
File file = command.fs ? command.fs->open(command.path, "r") : File();
|
||||
if (!file || file.isDirectory()) {
|
||||
if (file) file.close();
|
||||
luaAudioSetTrackStatus(command, LuaAudioState::Error, "not found");
|
||||
return LuaAudioRunResult::Error;
|
||||
}
|
||||
|
||||
uint16_t channels = 0;
|
||||
uint32_t source_rate = 0, data_len = 0;
|
||||
if (!wavParse(file, &channels, &source_rate, &data_len) ||
|
||||
data_len < (uint32_t)channels * sizeof(int16_t)) {
|
||||
file.close();
|
||||
luaAudioSetTrackStatus(command, LuaAudioState::Error, "unsupported format");
|
||||
return LuaAudioRunResult::Error;
|
||||
}
|
||||
|
||||
LuaAudioSinkSession sink;
|
||||
const char* sink_error = nullptr;
|
||||
__atomic_store_n(&s_lua_audio_active, true, __ATOMIC_RELEASE);
|
||||
if (!luaAudioSinkBegin(source_rate, &sink, &sink_error)) {
|
||||
__atomic_store_n(&s_lua_audio_active, false, __ATOMIC_RELEASE);
|
||||
file.close();
|
||||
luaAudioSetTrackStatus(command, LuaAudioState::Error,
|
||||
sink_error ? sink_error : "audio unavailable");
|
||||
return LuaAudioRunResult::Error;
|
||||
}
|
||||
|
||||
luaAudioSetTrackStatus(command, LuaAudioState::Playing);
|
||||
LuaAudioRunResult result = LuaAudioRunResult::Ended;
|
||||
const char* run_error = nullptr;
|
||||
bool interrupted = false;
|
||||
uint32_t remaining = data_len;
|
||||
uint64_t resample_phase = 0;
|
||||
int16_t input[256];
|
||||
int16_t output[256];
|
||||
size_t output_count = 0;
|
||||
const uint32_t frame_bytes = (uint32_t)channels * sizeof(int16_t);
|
||||
|
||||
while (remaining >= frame_bytes && !interrupted) {
|
||||
if (luaAudioPollControl(command, replacement, &result)) break;
|
||||
|
||||
size_t want = sizeof input;
|
||||
if (want > remaining) want = remaining;
|
||||
want -= want % frame_bytes;
|
||||
const int got = file.read((uint8_t*)input, want);
|
||||
if (got <= 0) {
|
||||
result = LuaAudioRunResult::Error;
|
||||
run_error = "read failed";
|
||||
break;
|
||||
}
|
||||
const size_t frames = (size_t)got / frame_bytes;
|
||||
for (size_t i = 0; i < frames; ++i) {
|
||||
int32_t sample = channels == 2
|
||||
? ((int32_t)input[2 * i] + input[2 * i + 1]) / 2
|
||||
: input[i];
|
||||
sample = (int32_t)((float)sample * sink.gain);
|
||||
if (sample > 32767) sample = 32767;
|
||||
else if (sample < -32768) sample = -32768;
|
||||
|
||||
resample_phase += sink.output_rate;
|
||||
while (resample_phase >= source_rate) {
|
||||
output[output_count++] = (int16_t)sample;
|
||||
resample_phase -= source_rate;
|
||||
if (output_count == sizeof output / sizeof output[0]) {
|
||||
if (!luaAudioSinkWrite(&sink, output, output_count)) {
|
||||
result = LuaAudioRunResult::Error;
|
||||
run_error = "output failed";
|
||||
interrupted = true;
|
||||
break;
|
||||
}
|
||||
output_count = 0;
|
||||
}
|
||||
}
|
||||
if (interrupted) break;
|
||||
}
|
||||
remaining -= (uint32_t)got;
|
||||
}
|
||||
|
||||
if (!interrupted && result == LuaAudioRunResult::Ended && output_count &&
|
||||
!luaAudioSinkWrite(&sink, output, output_count)) {
|
||||
result = LuaAudioRunResult::Error;
|
||||
run_error = "output failed";
|
||||
}
|
||||
|
||||
luaAudioSinkEnd(&sink);
|
||||
file.close();
|
||||
__atomic_store_n(&s_lua_audio_active, false, __ATOMIC_RELEASE);
|
||||
|
||||
if (result == LuaAudioRunResult::Ended)
|
||||
luaAudioSetState(command.owner, LuaAudioState::Ended);
|
||||
else if (result == LuaAudioRunResult::Stopped || result == LuaAudioRunResult::Released)
|
||||
luaAudioSetState(command.owner, LuaAudioState::Stopped);
|
||||
else if (result == LuaAudioRunResult::Error)
|
||||
luaAudioSetState(command.owner, LuaAudioState::Error, run_error ? run_error : "playback failed");
|
||||
return result;
|
||||
}
|
||||
|
||||
struct LuaMp3Work {
|
||||
mp3dec_t decoder;
|
||||
mp3dec_scratch_t scratch;
|
||||
uint8_t input[16 * 1024];
|
||||
mp3d_sample_t pcm[MINIMP3_MAX_SAMPLES_PER_FRAME];
|
||||
};
|
||||
|
||||
static uint32_t luaAudioReadLe32(const uint8_t* value) {
|
||||
return (uint32_t)value[0] | ((uint32_t)value[1] << 8) |
|
||||
((uint32_t)value[2] << 16) | ((uint32_t)value[3] << 24);
|
||||
}
|
||||
|
||||
static uint32_t luaAudioMp3DataEnd(File& file) {
|
||||
uint32_t end = (uint32_t)file.size();
|
||||
uint8_t footer[128];
|
||||
if (end >= 128 && file.seek(end - 128) && file.read(footer, 3) == 3 &&
|
||||
!memcmp(footer, "TAG", 3)) {
|
||||
end -= 128;
|
||||
}
|
||||
if (end >= 32 && file.seek(end - 32) && file.read(footer, 32) == 32 &&
|
||||
!memcmp(footer, "APETAGEX", 8)) {
|
||||
const uint32_t version = luaAudioReadLe32(footer + 8);
|
||||
const uint32_t tag_size = luaAudioReadLe32(footer + 12);
|
||||
const uint32_t flags = luaAudioReadLe32(footer + 20);
|
||||
const uint64_t total_size = (uint64_t)tag_size + ((flags & 0x80000000u) ? 32u : 0u);
|
||||
if ((version == 1000 || version == 2000) && tag_size >= 32 && total_size <= end)
|
||||
end -= (uint32_t)total_size;
|
||||
}
|
||||
file.seek(0);
|
||||
return end;
|
||||
}
|
||||
|
||||
static LuaAudioRunResult luaAudioRunMp3Track(const LuaAudioCommand& command,
|
||||
LuaAudioCommand* replacement) {
|
||||
LuaAudioStorageLease storage_lease;
|
||||
File file = command.fs ? command.fs->open(command.path, "r") : File();
|
||||
if (!file || file.isDirectory()) {
|
||||
if (file) file.close();
|
||||
luaAudioSetTrackStatus(command, LuaAudioState::Error, "not found");
|
||||
return LuaAudioRunResult::Error;
|
||||
}
|
||||
|
||||
LuaMp3Work* work = (LuaMp3Work*)heap_caps_malloc(
|
||||
sizeof(LuaMp3Work), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (!work) {
|
||||
file.close();
|
||||
luaAudioSetTrackStatus(command, LuaAudioState::Error, "out of memory");
|
||||
return LuaAudioRunResult::Error;
|
||||
}
|
||||
s_wada_mp3_scratch = &work->scratch;
|
||||
mp3dec_init(&work->decoder);
|
||||
const uint32_t data_end = luaAudioMp3DataEnd(file);
|
||||
if (!data_end) {
|
||||
s_wada_mp3_scratch = nullptr;
|
||||
heap_caps_free(work);
|
||||
file.close();
|
||||
luaAudioSetTrackStatus(command, LuaAudioState::Error, "unsupported format");
|
||||
return LuaAudioRunResult::Error;
|
||||
}
|
||||
|
||||
__atomic_store_n(&s_lua_audio_active, true, __ATOMIC_RELEASE);
|
||||
luaAudioSetTrackStatus(command, LuaAudioState::Playing);
|
||||
LuaAudioRunResult result = LuaAudioRunResult::Ended;
|
||||
const char* run_error = nullptr;
|
||||
LuaAudioSinkSession sink;
|
||||
uint32_t source_rate = 0;
|
||||
uint64_t resample_phase = 0;
|
||||
int16_t output[256];
|
||||
size_t output_count = 0;
|
||||
size_t input_start = 0, input_count = 0;
|
||||
bool eof = false, decoded_any = false, interrupted = false;
|
||||
|
||||
while (!interrupted) {
|
||||
if (luaAudioPollControl(command, replacement, &result)) break;
|
||||
|
||||
if (!eof && input_count < 4096) {
|
||||
if (input_start && input_start + input_count + 4096 > sizeof work->input) {
|
||||
memmove(work->input, work->input + input_start, input_count);
|
||||
input_start = 0;
|
||||
}
|
||||
size_t room = sizeof work->input - (input_start + input_count);
|
||||
const uint32_t position = (uint32_t)file.position();
|
||||
const uint32_t remaining = position < data_end ? data_end - position : 0;
|
||||
if (room > remaining) room = remaining;
|
||||
const int got = room ? file.read(work->input + input_start + input_count, room) : 0;
|
||||
if (got > 0) {
|
||||
input_count += (size_t)got;
|
||||
eof = file.position() >= data_end;
|
||||
} else if (file.position() < data_end) {
|
||||
result = LuaAudioRunResult::Error;
|
||||
run_error = "read failed";
|
||||
break;
|
||||
} else {
|
||||
eof = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!input_count) {
|
||||
if (!decoded_any) {
|
||||
result = LuaAudioRunResult::Error;
|
||||
run_error = "unsupported format";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
mp3dec_frame_info_t info = {};
|
||||
const int samples = mp3dec_decode_frame(&work->decoder,
|
||||
work->input + input_start, (int)input_count, work->pcm, &info);
|
||||
if (info.frame_bytes < 0 || (size_t)info.frame_bytes > input_count) {
|
||||
result = LuaAudioRunResult::Error;
|
||||
run_error = "decode failed";
|
||||
break;
|
||||
}
|
||||
if (info.frame_bytes > 0) {
|
||||
input_start += (size_t)info.frame_bytes;
|
||||
input_count -= (size_t)info.frame_bytes;
|
||||
if (!input_count) input_start = 0;
|
||||
} else if (eof) {
|
||||
if (!decoded_any) {
|
||||
result = LuaAudioRunResult::Error;
|
||||
run_error = "unsupported format";
|
||||
}
|
||||
break;
|
||||
} else if (input_start + input_count == sizeof work->input) {
|
||||
result = LuaAudioRunResult::Error;
|
||||
run_error = "decode failed";
|
||||
break;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (samples <= 0) continue;
|
||||
if (info.layer != 3 || (info.channels != 1 && info.channels != 2) ||
|
||||
info.hz < 8000 || info.hz > 48000) {
|
||||
result = LuaAudioRunResult::Error;
|
||||
run_error = "unsupported format";
|
||||
break;
|
||||
}
|
||||
if (!sink.open) {
|
||||
const char* sink_error = nullptr;
|
||||
if (!luaAudioSinkBegin((uint32_t)info.hz, &sink, &sink_error)) {
|
||||
result = LuaAudioRunResult::Error;
|
||||
run_error = sink_error ? sink_error : "audio unavailable";
|
||||
break;
|
||||
}
|
||||
source_rate = (uint32_t)info.hz;
|
||||
} else if (source_rate != (uint32_t)info.hz) {
|
||||
result = LuaAudioRunResult::Error;
|
||||
run_error = "sample rate changed";
|
||||
break;
|
||||
}
|
||||
|
||||
decoded_any = true;
|
||||
for (int i = 0; i < samples; ++i) {
|
||||
int32_t sample = info.channels == 2
|
||||
? ((int32_t)work->pcm[2 * i] + work->pcm[2 * i + 1]) / 2
|
||||
: work->pcm[i];
|
||||
sample = (int32_t)((float)sample * sink.gain);
|
||||
if (sample > 32767) sample = 32767;
|
||||
else if (sample < -32768) sample = -32768;
|
||||
|
||||
resample_phase += sink.output_rate;
|
||||
while (resample_phase >= source_rate) {
|
||||
output[output_count++] = (int16_t)sample;
|
||||
resample_phase -= source_rate;
|
||||
if (output_count == sizeof output / sizeof output[0]) {
|
||||
if (!luaAudioSinkWrite(&sink, output, output_count)) {
|
||||
result = LuaAudioRunResult::Error;
|
||||
run_error = "output failed";
|
||||
interrupted = true;
|
||||
break;
|
||||
}
|
||||
output_count = 0;
|
||||
}
|
||||
}
|
||||
if (interrupted) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!interrupted && result == LuaAudioRunResult::Ended && output_count &&
|
||||
!luaAudioSinkWrite(&sink, output, output_count)) {
|
||||
result = LuaAudioRunResult::Error;
|
||||
run_error = "output failed";
|
||||
}
|
||||
|
||||
luaAudioSinkEnd(&sink);
|
||||
s_wada_mp3_scratch = nullptr;
|
||||
heap_caps_free(work);
|
||||
file.close();
|
||||
__atomic_store_n(&s_lua_audio_active, false, __ATOMIC_RELEASE);
|
||||
|
||||
if (result == LuaAudioRunResult::Ended)
|
||||
luaAudioSetState(command.owner, LuaAudioState::Ended);
|
||||
else if (result == LuaAudioRunResult::Stopped || result == LuaAudioRunResult::Released)
|
||||
luaAudioSetState(command.owner, LuaAudioState::Stopped);
|
||||
else if (result == LuaAudioRunResult::Error)
|
||||
luaAudioSetState(command.owner, LuaAudioState::Error,
|
||||
run_error ? run_error : "playback failed");
|
||||
return result;
|
||||
}
|
||||
|
||||
static void luaAudioWorker(void*) {
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
bool release = false;
|
||||
while (!release) {
|
||||
LuaAudioCommand command;
|
||||
if (xQueueReceive(s_lua_audio_queue, &command, portMAX_DELAY) != pdTRUE) continue;
|
||||
if (command.kind == LuaAudioCommandKind::Release) {
|
||||
luaAudioSetState(command.owner, LuaAudioState::Stopped);
|
||||
break;
|
||||
}
|
||||
if (command.kind == LuaAudioCommandKind::Stop) {
|
||||
luaAudioSetState(command.owner, LuaAudioState::Stopped);
|
||||
continue;
|
||||
}
|
||||
if (command.kind != LuaAudioCommandKind::Play) continue;
|
||||
|
||||
for (;;) {
|
||||
LuaAudioCommand replacement;
|
||||
const LuaAudioRunResult result = command.format == LuaAudioFormat::Mp3
|
||||
? luaAudioRunMp3Track(command, &replacement)
|
||||
: luaAudioRunTrack(command, &replacement);
|
||||
if (result == LuaAudioRunResult::Replaced) {
|
||||
command = replacement;
|
||||
continue;
|
||||
}
|
||||
release = result == LuaAudioRunResult::Released;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
__atomic_store_n(&s_lua_audio_active, false, __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&s_lua_audio_storage_pending, 0u, __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&s_lua_audio_storage_active, false, __ATOMIC_RELEASE);
|
||||
xQueueReset(s_lua_audio_queue);
|
||||
portENTER_CRITICAL(&s_lua_audio_mux);
|
||||
s_lua_audio_task = nullptr;
|
||||
s_lua_audio_task_state = LuaAudioTaskState::Stopped;
|
||||
portEXIT_CRITICAL(&s_lua_audio_mux);
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
static bool luaAudioReturnError(char* error, size_t cap, const char* message) {
|
||||
luaAudioCopy(error, cap, message);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool luaAudioPathEndsWith(const char* path, const char* suffix) {
|
||||
if (!path || !suffix) return false;
|
||||
const size_t path_len = strlen(path), suffix_len = strlen(suffix);
|
||||
if (path_len < suffix_len) return false;
|
||||
path += path_len - suffix_len;
|
||||
for (size_t i = 0; i < suffix_len; ++i) {
|
||||
char a = path[i], b = suffix[i];
|
||||
if (a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a');
|
||||
if (b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a');
|
||||
if (a != b) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool luaHostAudioPlay(fs::FS* fs, const char* path, const char* shown, const char* source,
|
||||
uint32_t owner, char* error, size_t error_cap) {
|
||||
if (!fs) return luaAudioReturnError(error, error_cap, "no storage");
|
||||
if (!path || !path[0] || strlen(path) >= 224)
|
||||
return luaAudioReturnError(error, error_cap, "bad path");
|
||||
if (!g_lv.task || g_lv.task->isBuzzerQuiet() || touchPrefsGetSoundVolume() == 0)
|
||||
return luaAudioReturnError(error, error_cap, "muted");
|
||||
|
||||
File probe = fs->open(path, "r");
|
||||
if (!probe || probe.isDirectory()) {
|
||||
if (probe) probe.close();
|
||||
return luaAudioReturnError(error, error_cap, "not found");
|
||||
}
|
||||
uint16_t channels = 0;
|
||||
uint32_t rate = 0, data_len = 0;
|
||||
LuaAudioFormat format;
|
||||
bool supported = false;
|
||||
if (luaAudioPathEndsWith(shown, ".wav")) {
|
||||
format = LuaAudioFormat::Wav;
|
||||
supported = wavParse(probe, &channels, &rate, &data_len) && data_len > 0;
|
||||
} else if (luaAudioPathEndsWith(shown, ".mp3")) {
|
||||
format = LuaAudioFormat::Mp3;
|
||||
supported = probe.size() > 0;
|
||||
}
|
||||
probe.close();
|
||||
if (!supported) return luaAudioReturnError(error, error_cap, "unsupported format");
|
||||
|
||||
if (!s_lua_audio_queue) s_lua_audio_queue = xQueueCreate(4, sizeof(LuaAudioCommand));
|
||||
if (!s_lua_audio_queue) return luaAudioReturnError(error, error_cap, "out of memory");
|
||||
|
||||
bool start_task = false;
|
||||
portENTER_CRITICAL(&s_lua_audio_mux);
|
||||
if (s_lua_audio_task_state == LuaAudioTaskState::Stopped) {
|
||||
s_lua_audio_task_state = LuaAudioTaskState::Starting;
|
||||
start_task = true;
|
||||
} else if (s_lua_audio_task_state != LuaAudioTaskState::Running) {
|
||||
portEXIT_CRITICAL(&s_lua_audio_mux);
|
||||
return luaAudioReturnError(error, error_cap, "busy");
|
||||
}
|
||||
portEXIT_CRITICAL(&s_lua_audio_mux);
|
||||
|
||||
TaskHandle_t task = nullptr;
|
||||
if (start_task) {
|
||||
if (xTaskCreate(luaAudioWorker, "lua-audio", 8192, nullptr, 3, &task) != pdPASS) {
|
||||
portENTER_CRITICAL(&s_lua_audio_mux);
|
||||
s_lua_audio_task_state = LuaAudioTaskState::Stopped;
|
||||
portEXIT_CRITICAL(&s_lua_audio_mux);
|
||||
return luaAudioReturnError(error, error_cap, "out of memory");
|
||||
}
|
||||
portENTER_CRITICAL(&s_lua_audio_mux);
|
||||
s_lua_audio_task = task;
|
||||
s_lua_audio_task_state = LuaAudioTaskState::Running;
|
||||
portEXIT_CRITICAL(&s_lua_audio_mux);
|
||||
}
|
||||
|
||||
LuaAudioCommand command;
|
||||
command.kind = LuaAudioCommandKind::Play;
|
||||
command.fs = fs;
|
||||
command.owner = owner;
|
||||
command.format = format;
|
||||
luaAudioCopy(command.path, sizeof command.path, path);
|
||||
luaAudioCopy(command.shown, sizeof command.shown, shown);
|
||||
luaAudioCopy(command.source, sizeof command.source, source);
|
||||
__atomic_fetch_add(&s_lua_audio_storage_pending, 1u, __ATOMIC_ACQ_REL);
|
||||
if (xQueueSend(s_lua_audio_queue, &command, 0) != pdPASS) {
|
||||
__atomic_fetch_sub(&s_lua_audio_storage_pending, 1u, __ATOMIC_ACQ_REL);
|
||||
if (start_task) xTaskNotifyGive(task);
|
||||
return luaAudioReturnError(error, error_cap, "busy");
|
||||
}
|
||||
luaAudioSetTrackStatus(command, LuaAudioState::Playing);
|
||||
if (start_task) xTaskNotifyGive(task);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool luaHostAudioPause(uint32_t owner, bool pause) {
|
||||
LuaAudioState state;
|
||||
TaskHandle_t task;
|
||||
LuaAudioTaskState task_state;
|
||||
portENTER_CRITICAL(&s_lua_audio_mux);
|
||||
state = s_lua_audio_status.owner == owner ? s_lua_audio_status.state : LuaAudioState::Stopped;
|
||||
task = s_lua_audio_task;
|
||||
task_state = s_lua_audio_task_state;
|
||||
portEXIT_CRITICAL(&s_lua_audio_mux);
|
||||
if (!task || task_state != LuaAudioTaskState::Running ||
|
||||
(pause ? state != LuaAudioState::Playing : state != LuaAudioState::Paused)) return false;
|
||||
LuaAudioCommand command;
|
||||
command.kind = pause ? LuaAudioCommandKind::Pause : LuaAudioCommandKind::Resume;
|
||||
command.owner = owner;
|
||||
return xQueueSend(s_lua_audio_queue, &command, 0) == pdPASS;
|
||||
}
|
||||
|
||||
bool luaHostAudioStop(uint32_t owner, bool release) {
|
||||
TaskHandle_t task;
|
||||
LuaAudioTaskState task_state;
|
||||
portENTER_CRITICAL(&s_lua_audio_mux);
|
||||
task = s_lua_audio_task;
|
||||
task_state = s_lua_audio_task_state;
|
||||
if (release && task && task_state == LuaAudioTaskState::Running)
|
||||
s_lua_audio_task_state = LuaAudioTaskState::Stopping;
|
||||
portEXIT_CRITICAL(&s_lua_audio_mux);
|
||||
if (!task || task_state == LuaAudioTaskState::Stopped) {
|
||||
luaAudioSetState(owner, LuaAudioState::Stopped);
|
||||
return true;
|
||||
}
|
||||
if (task_state != LuaAudioTaskState::Running) return false;
|
||||
|
||||
LuaAudioCommand command;
|
||||
command.kind = release ? LuaAudioCommandKind::Release : LuaAudioCommandKind::Stop;
|
||||
command.owner = owner;
|
||||
const BaseType_t queued = release
|
||||
? xQueueSendToFront(s_lua_audio_queue, &command, pdMS_TO_TICKS(100))
|
||||
: xQueueSend(s_lua_audio_queue, &command, 0);
|
||||
if (queued != pdPASS) {
|
||||
if (release) {
|
||||
portENTER_CRITICAL(&s_lua_audio_mux);
|
||||
if (s_lua_audio_task_state == LuaAudioTaskState::Stopping)
|
||||
s_lua_audio_task_state = LuaAudioTaskState::Running;
|
||||
portEXIT_CRITICAL(&s_lua_audio_mux);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!release) return true;
|
||||
|
||||
for (int i = 0; i < 160; ++i) {
|
||||
vTaskDelay(pdMS_TO_TICKS(5));
|
||||
portENTER_CRITICAL(&s_lua_audio_mux);
|
||||
task = s_lua_audio_task;
|
||||
portEXIT_CRITICAL(&s_lua_audio_mux);
|
||||
if (!task) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void luaHostAudioStatus(uint32_t owner, char* state, size_t state_cap,
|
||||
char* path, size_t path_cap, char* source, size_t source_cap,
|
||||
char* format, size_t format_cap, char* error, size_t error_cap) {
|
||||
LuaAudioStatus status;
|
||||
portENTER_CRITICAL(&s_lua_audio_mux);
|
||||
status = s_lua_audio_status;
|
||||
portEXIT_CRITICAL(&s_lua_audio_mux);
|
||||
if (status.owner != owner) status = LuaAudioStatus();
|
||||
|
||||
const char* state_name = "stopped";
|
||||
if (status.state == LuaAudioState::Playing) state_name = "playing";
|
||||
else if (status.state == LuaAudioState::Paused) state_name = "paused";
|
||||
else if (status.state == LuaAudioState::Ended) state_name = "ended";
|
||||
else if (status.state == LuaAudioState::Error) state_name = "error";
|
||||
luaAudioCopy(state, state_cap, state_name);
|
||||
luaAudioCopy(path, path_cap, status.path);
|
||||
luaAudioCopy(source, source_cap, status.source);
|
||||
luaAudioCopy(format, format_cap, status.format);
|
||||
luaAudioCopy(error, error_cap, status.error);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if CAP_LUA_SD_LIST
|
||||
// Return the physical removable card, never the app-data filesystem. Mounting
|
||||
// stays in UITask so Lua cannot bypass shared-bus coordination or create a
|
||||
@@ -52997,10 +53808,16 @@ static bool sdRuntimeLifecycleBusy() {
|
||||
s_sdinfo_busy ||
|
||||
touchPrefsIoBusy();
|
||||
#if defined(MULTI_TRANSPORT_COMPANION)
|
||||
// The web reader and the Lua audio player are independent SD consumers, so
|
||||
// both gate the mount lifecycle. The reader excludes ITSELF: it calls this
|
||||
// from its own task while holding the card, and would otherwise deadlock.
|
||||
const bool reader_is_caller = s_reader_sd_busy &&
|
||||
s_reader_sd_owner == xTaskGetCurrentTaskHandle();
|
||||
busy = busy || (s_reader_sd_busy && !reader_is_caller);
|
||||
#endif
|
||||
#if CAP_LUA_AUDIO
|
||||
busy = busy || luaAudioStorageBusy();
|
||||
#endif
|
||||
#if defined(HAS_TDECK_GT911) || defined(TLORA_PAGER)
|
||||
busy = busy || s_notify_playing;
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user