From acdcc6e7fc789a39856cc29b7a37668bd1f95717 Mon Sep 17 00:00:00 2001 From: "Michael A. Cojocari" Date: Sun, 23 Aug 2026 20:32:26 -0400 Subject: [PATCH] Work --- AUDIO_PLAYBACK_TESTING.md | 156 +++ NOTICE | 7 + README.md | 4 + deploy/apps/README.md | 6 +- deploy/apps/apps.json | 4 +- deploy/apps/sdktest/1.7/sdktest.json | 1 + deploy/apps/sdktest/1.7/sdktest.lua | 293 ++++ deploy/site/sdk.html | 23 +- lib/minimp3/LICENSE | 117 ++ lib/minimp3/README.md | 16 + lib/minimp3/minimp3.h | 1872 ++++++++++++++++++++++++++ scripts/audio_test.lua | 104 ++ scripts/lua-harness/harness.lua | 122 +- scripts/sideload_app.py | 15 +- src/DataStore.cpp | 7 + src/DataStore.h | 1 + src/MyMesh.cpp | 59 +- src/MyMesh.h | 2 + src/ui-touch/LuaAppHost.cpp | 123 +- src/ui-touch/UITask.cpp | 882 +++++++++++- src/ui-touch/device_caps.h | 17 + src/ui-touch/lua_builtin.h | 19 +- test/test_minimp3_decoder.cpp | 144 ++ variants/tdisplay_p4/P4Audio.cpp | 38 +- variants/tdisplay_p4/P4Audio.h | 8 + 25 files changed, 3980 insertions(+), 60 deletions(-) create mode 100644 AUDIO_PLAYBACK_TESTING.md create mode 100644 deploy/apps/sdktest/1.7/sdktest.json create mode 100644 deploy/apps/sdktest/1.7/sdktest.lua create mode 100644 lib/minimp3/LICENSE create mode 100644 lib/minimp3/README.md create mode 100644 lib/minimp3/minimp3.h create mode 100644 scripts/audio_test.lua create mode 100644 test/test_minimp3_decoder.cpp diff --git a/AUDIO_PLAYBACK_TESTING.md b/AUDIO_PLAYBACK_TESTING.md new file mode 100644 index 0000000..cbbc67b --- /dev/null +++ b/AUDIO_PLAYBACK_TESTING.md @@ -0,0 +1,156 @@ +# Testing Lua audio playback + +This guide tests issue #315 with a real WAV or MP3. The examples use the +T-Deck environment and the provided `scripts/audio_test.lua` app. + +## Build and flash + +Build and upload the branch as usual: + +```sh +pio run -e LilyGo_TDeck_companion_radio_touch +pio run -e LilyGo_TDeck_companion_radio_touch -t upload +``` + +Keep the serial monitor available for `[LUAAPP]` or storage errors: + +```sh +pio device monitor -b 115200 +``` + +## Quick MP3 test from SD + +1. Use a FAT32 SD card and create these paths on it: + + ```text + /Music/test.mp3 + /meshcomod/apps/audio_test.lua + ``` + +2. Copy `scripts/audio_test.lua` from this repository to the second path. Rename + your MP3 to `test.mp3`, or edit `SD_PATH` near the top of the Lua file. +3. Insert the card before boot, then reboot the T-Deck. +4. Enable **Settings > Sound** and set volume above zero. +5. Open **Apps > audio_test**. The screen should report + `audio=true`, `wav=true`, and `mp3=true`. +6. Press **Play**. Expected status: + + ```text + State: playing + Source: sd Format: mp3 + ``` + +7. Exercise **Pause**, **Resume**, and **Stop**. Pressing **Play** while already + playing must restart/replace the current track without overlapping it. +8. Start playback and leave the app. Audio must stop immediately when the app + closes. + +The direct SD API used by the test is: + +```lua +local ok, err = wada.audio.play("sd:/Music/test.mp3") +``` + +`sd:` paths are absolute from the physical card root. Empty segments, +backslashes, trailing slashes, `.` and `..` are rejected. + +## Test app-local/internal storage + +A plain filename is resolved inside the current app's private directory. For +`audio_test.lua`, the logical files are: + +```text +/apps/audio_test.lua +/apps/audio_test.d/test.mp3 +``` + +On T-Deck and Pager, internal app storage is SPIFFS. The least destructive way +to populate it is the repository's serial uploader: + +1. Power off, remove the SD card, and reboot. This forces T-Deck app storage to + the internal SPIFFS backend. +2. Make a short internal-storage fixture. T-Deck has a 3.375 MB SPIFFS partition + shared with settings and history, so use the full song for SD testing and a + small clip here: + + ```sh + ffmpeg -y -i "/path/to/your/file.mp3" \ + -t 10 -ac 1 -ar 22050 -b:a 48k \ + /tmp/wadamesh-audio-test.mp3 + ``` + +3. Find the serial port with `pio device list`. +4. Install `pyserial` once if needed: + + ```sh + python3 -m pip install pyserial + ``` + +5. Upload the app and short MP3. Replace the port as needed: + + ```sh + python3 scripts/sideload_app.py \ + --port /dev/cu.usbmodem101 \ + --remote /apps/audio_test.lua \ + scripts/audio_test.lua + + python3 scripts/sideload_app.py \ + --port /dev/cu.usbmodem101 \ + --remote /apps/audio_test.d/test.mp3 \ + --reboot \ + /tmp/wadamesh-audio-test.mp3 + ``` + +6. Open **Apps > audio_test** after reboot. With no card inserted, the app + selects `test.mp3`; press **Play**. +7. Expected status is `Source: app Format: mp3`. + +The app-local API is simply: + +```lua +local ok, err = wada.audio.play("test.mp3") +``` + +Tanmatsu uses internal FFat and T-Display P4 uses internal LittleFS. The same +serial destination `/apps/audio_test.d/test.mp3` follows the active internal +backend, so no Lua code changes are needed. Direct `sd:` playback is shown only +when `wada.sys.caps().audio_sd` is true. + +Do not use `uploadfs` on a device with data you need to keep: a filesystem-image +upload replaces the internal storage partition, including settings and history. + +## Error and lifecycle checks + +- Disable Sound, then press **Play**: expect `Command: muted`. +- Rename the file or remove the card: expect `not found` or `no sd`. +- Try a non-WAV/MP3 extension: expect `unsupported format`. +- Remove the card during SD playback only after the baseline test. Playback must + enter `error` or stop without rebooting; reinserting the card should allow a + later play after the normal SD recovery delay. +- Receive a mesh notification during playback: it must not start a second audio + stream over the media track. +- Repeatedly open/play/close the app: each close must release the worker, codec, + amplifier, file handle, and storage lease. + +## Host decoder regression test + +The standalone test validates the pinned decoder and the same rolling 16 KB +input algorithm used by firmware: + +```sh +c++ -std=c++17 -O2 test/test_minimp3_decoder.cpp -o /tmp/test_minimp3_decoder +/tmp/test_minimp3_decoder /path/to/your/file.mp3 +``` + +A successful run prints decoded frames, samples per channel, sample rate, +channels, and bitrate changes. The implementation has also been checked with +CBR, VBR, leading ID3v2, trailing ID3v1, and trailing APEv2 metadata. + +## Supported formats + +- WAV: PCM, 16-bit, mono or stereo, 8-48 kHz. +- MP3: MPEG Layer III, mono or stereo, 8-48 kHz, CBR or VBR, with common ID3 and + APEv2 metadata. + +Stereo is downmixed to the device's mono speaker path. `status()` reports +`stopped`, `playing`, `paused`, `ended`, or `error`. diff --git a/NOTICE b/NOTICE index e5b4be9..711abdc 100644 --- a/NOTICE +++ b/NOTICE @@ -54,6 +54,13 @@ ed25519 (bundled in lib/ed25519) Orson Peters / orlp — public domain (Creative Commons Zero / zlib) https://github.com/orlp/ed25519 +minimp3 (bundled in lib/minimp3) + lieff/minimp3 contributors — CC0 1.0 Universal + https://github.com/lieff/minimp3 + Pinned at ea99364f61c14656440e8d77e9c233ccf3124633. The bundled header has + a small opt-in scratch-storage hook so the decoder workspace can live in + PSRAM; default upstream behavior is unchanged when that hook is not used. + AsyncElegantOTA (vendored in arch/esp32/AsyncElegantOTA) Copyright (c) Ayush Sharma — MIT License diff --git a/README.md b/README.md index 60ea0cd..2946c56 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,10 @@ the transports, …) are dropped from the lib via `-DMC_VENDORED_TOUCH_APP` so t aren't compiled twice. The build is byte-identical to the original in-tree meshcomod firmware. +Lua apps use the on-device SDK described in [LUA_APPS.md](LUA_APPS.md). For +WAV/MP3 playback, including a ready-to-sideload transport test app and exact SD +and internal-storage paths, see [AUDIO_PLAYBACK_TESTING.md](AUDIO_PLAYBACK_TESTING.md). + ## Build [PlatformIO](https://platformio.org/) pulls the core fork and all libraries diff --git a/deploy/apps/README.md b/deploy/apps/README.md index 6e9b046..751dcfb 100644 --- a/deploy/apps/README.md +++ b/deploy/apps/README.md @@ -16,8 +16,10 @@ One Lua file plus a small manifest. Apps talk to the firmware through the `wada. API — `wada.ui` (widgets, colours, a text prompt), `wada.sys`, `wada.store` (persistence), `wada.timer`, and on the larger boards `wada.fs`, `wada.net`, `wada.crypto` and the writable half of `wada.mesh`. Supported boards also expose -read-only SD directory metadata through `wada.sd`; file contents and writes stay -inaccessible. There is no general-purpose filesystem or network access; the API +asynchronous WAV/MP3 playback through `wada.audio`; plain filenames come from the +app sandbox on internal flash, SD, or SD_MMC. Read-only physical-SD directory +metadata is available through `wada.sd`, and direct card playback uses an explicit +`sd:/...` path. There is no general-purpose filesystem or network access; the API is the whole surface, which is what makes reviewing tractable. It is documented at [wadamesh.com/sdk.html](https://wadamesh.com/sdk.html). diff --git a/deploy/apps/apps.json b/deploy/apps/apps.json index ee2da03..62d8026 100644 --- a/deploy/apps/apps.json +++ b/deploy/apps/apps.json @@ -21,8 +21,8 @@ { "id": "sdktest", "name": "SDK Test", - "ver": "1.6", - "desc": "Developer tool. Checks the extended Lua SDK on this board: capabilities, clock, battery, GPS, private file access, read-only SD listing, crypto against published RFC vectors, channel discovery, and buttons that TRANSMIT test messages." + "ver": "1.7", + "desc": "Developer tool. Checks the extended Lua SDK on this board: capabilities, clock, battery, GPS, private file access, audio API, read-only SD listing, crypto against published RFC vectors, channel discovery, and buttons that TRANSMIT test messages." }, { "id": "2048", diff --git a/deploy/apps/sdktest/1.7/sdktest.json b/deploy/apps/sdktest/1.7/sdktest.json new file mode 100644 index 0000000..aa4e24e --- /dev/null +++ b/deploy/apps/sdktest/1.7/sdktest.json @@ -0,0 +1 @@ +{"id":"sdktest","name":"SDK Test","ver":"1.7","desc":"Developer tool. Checks the extended Lua SDK on this board: capabilities, clock, battery, GPS, private files, audio API, read-only SD listing, crypto vectors, packet identity, channels, and opt-in transmit calls."} diff --git a/deploy/apps/sdktest/1.7/sdktest.lua b/deploy/apps/sdktest/1.7/sdktest.lua new file mode 100644 index 0000000..94b64da --- /dev/null +++ b/deploy/apps/sdktest/1.7/sdktest.lua @@ -0,0 +1,293 @@ +-- SDK self-test. Exercises the extended SDK so the results can be read off the +-- screen instead of inferred from a build log. Published to the store as a +-- developer/bench tool. +-- +-- 1.2 adds wada.crypto (checked against published RFC vectors, so a PASS here is +-- real evidence and not just "it returned something"), wada.mesh.channels, and +-- wada.mesh.send_dm. +-- 1.3 adds the discovery surface (wada.mesh.discover / discovered), packet +-- identity in rx_log, exact micro-degree coordinates, wada.ui.input and the +-- windowed wada.fs.read. +-- 1.6 adds wada.sd.list: capability reporting, traversal rejection, root +-- metadata, and a nested directory listing when the card contains one. +-- 1.7 adds storage-neutral wada.audio capability and API-shape checks. Playback +-- stays manual so opening the bench test never emits sound unexpectedly. +-- 1.5 uses wada.ui.text_lines() to measure instead of estimating, where the +-- firmware has it. That call was added because of the 1.3 bug below: an app +-- could ask how tall a line is but not how wide, so laying out rows meant +-- guessing whether text would wrap. Falls back to the 1.4 estimate on older +-- firmware, so it still lays out correctly there. +-- 1.4 fixes rows drawing on top of one another. label:width() turns wrapping +-- on, so any line wider than the screen became two lines while the caller still +-- advanced by one, and the next row landed on top. Reported with a photo of the +-- channels line sitting across the contacts line. row() now owns the cursor and +-- advances by the height the text actually needs. +local ui, sys, store, timer = wada.ui, wada.sys, wada.store, wada.timer +local C = ui.colors + +local app = {} +local rows, keyline, sendline, dmline, msgline = {}, nil, nil, nil, nil +local discline, inputline = nil, nil +local W = 300 + +-- row() owns the vertical cursor. Every caller used to pass a y and then add a +-- hand-picked constant, which is only correct while the text fits on one line. +-- An app cannot measure rendered text, so the wrapped line count is estimated +-- from a deliberately pessimistic character width: over-estimating costs a +-- little whitespace, under-estimating overlaps the next row. +local LH, GAP = 14, 3 +local cy = 4 +local function row(text, color, extra_gap) + local l = ui.label(text, 6, cy, 12, color or C.text) + l:width(W - 12) + rows[#rows + 1] = l + local n + if ui.text_lines then + n = ui.text_lines(tostring(text), W - 12, 12) -- measured: exact + else + local cpl = math.max(16, (W - 12) // 7) -- older firmware: estimate + n = 0 + for seg in (tostring(text) .. "\n"):gmatch("(.-)\n") do + n = n + math.max(1, math.ceil(#seg / cpl)) + end + end + cy = cy + math.max(1, n or 1) * LH + GAP + (extra_gap or 0) + return l +end +local function yn(v) return v and "yes" or "NO" end + +function app.on_open(w, h) + W = w or 300 + ui.scroll(true) + cy = 4 + local y + + local c = sys.caps() + row("caps: sdk_ext=" .. yn(c.sdk_ext) .. " kbd=" .. yn(c.keyboard) .. + " touch=" .. yn(c.touch) .. " sd=" .. yn(c.sd), C.accent) + row("caps: sd_list=" .. yn(c.sd_list) .. " discover=" .. yn(c.discover) .. + " input=" .. yn(c.input) .. + " rx_identity=" .. yn(c.rx_identity), C.accent) + row("caps: audio=" .. yn(c.audio) .. " wav=" .. yn(c.audio_wav) .. + " mp3=" .. yn(c.audio_mp3) .. " audio_sd=" .. yn(c.audio_sd), C.accent) + if c.audio then + local api_ok = wada.audio and type(wada.audio.play) == "function" and + type(wada.audio.pause) == "function" and + type(wada.audio.resume) == "function" and + type(wada.audio.stop) == "function" and + type(wada.audio.status) == "function" + local status = api_ok and wada.audio.status() or nil + api_ok = api_ok and type(status) == "table" and type(status.state) == "string" + row("wada.audio API: " .. (api_ok and ("PASS (" .. status.state .. ")") or "FAIL"), + api_ok and C.good or C.bad) + else + row("wada.audio: unavailable on this board", C.sub) + end + row("layout: " .. (ui.text_lines and "measured (ui.text_lines)" or "estimated (older firmware)"), + ui.text_lines and C.good or C.sub) + -- crypto: published test vectors, so this is checkable rather than merely alive + if wada.crypto then + local hex = wada.crypto.hex + local k20 = string.rep(string.char(0x0b), 20) + local checks = { + { "sha256('abc')", hex(wada.crypto.sha256("abc")), + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" }, + { "sha1('abc')", hex(wada.crypto.sha1("abc")), + "a9993e364706816aba3e25717850c26c9cd0d89d" }, + { "hmac_sha1 RFC2202#1", hex(wada.crypto.hmac_sha1(k20, "Hi There")), + "b617318655057264e28bc0b6fb378c8ef146be00" }, + { "hmac_sha256 RFC4231#1", hex(wada.crypto.hmac_sha256(k20, "Hi There")), + "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7" }, + } + local allok = true + for _, t in ipairs(checks) do + local ok = (t[2] == t[3]) + if not ok then allok = false end + row("crypto " .. t[1] .. ": " .. (ok and "PASS" or ("FAIL got " .. tostring(t[2]):sub(1, 16))), + ok and C.good or C.bad) + end + -- The point of having it in C: this loop would blow the instruction budget in Lua. + local t0 = sys.millis() + for _ = 1, 200 do wada.crypto.hmac_sha1(k20, "Hi There") end + row(string.format("crypto: 200 x hmac_sha1 in %d ms%s", sys.millis() - t0, + allok and "" or " (VECTORS FAILED)"), allok and C.good or C.bad) + else + row("wada.crypto: MISSING", C.bad) + end + + -- channel discovery + local chans = wada.mesh.channels and wada.mesh.channels() or nil + if chans then + local names = table.concat(chans, ", ") + row("channels(): " .. #chans .. " " .. names:sub(1, 60), C.text) + else + row("wada.mesh.channels: MISSING", C.bad) + end + + if not c.sdk_ext then + row("extended SDK is OFF on this board - stopping here.", C.sub) + return + end + + -- contacts, so send_dm has a target to name. 1.3 also checks the pubkey field. + local cts = wada.mesh.contacts() + local first = cts[1] and cts[1].name or nil + row("contacts: " .. #cts .. (first and (" first=" .. first) or ""), C.text) + if cts[1] then + local pk = cts[1].pubkey + row("contacts[1].pubkey: " .. tostring(pk) .. + (type(pk) == "string" and #pk == 8 and " PASS" or " FAIL"), + (type(pk) == "string" and #pk == 8) and C.good or C.bad) + end + + local me = wada.mesh.self() + row("self: " .. tostring(me.name) .. " pubkey=" .. tostring(me.pubkey), C.text) + -- Exact coordinates. Lua's floats here are 32-bit, so an app that logs a track + -- must use the _e6 integers; this proves they are present and consistent. + local fix = sys.gps() + if fix then + local drift = math.abs(fix.lat_e6 / 1e6 - fix.lat) + row(string.format("gps: %d,%d e6 alt %dm %d sats drift %.6f %s", + fix.lat_e6, fix.lon_e6, fix.alt_m or 0, fix.sats, drift, + drift < 0.001 and "PASS" or "FAIL"), drift < 0.001 and C.good or C.bad) + else + row("gps: no fix (normal indoors)", C.sub) + end + + -- rx_log identity: adverts carry a real public key, addressed frames carry + -- one-byte hashes, everything else carries nothing. All three are correct. + local log = wada.mesh.rx_log() + local withpk, withsrc = 0, 0 + for _, r in ipairs(log) do + if r.pubkey then withpk = withpk + 1 end + if r.src then withsrc = withsrc + 1 end + end + row(string.format("rx_log: %d frames, %d with a pubkey (adverts), %d with src/dst", + #log, withpk, withsrc), #log > 0 and C.text or C.sub) + -- fs: windowed read. Writes once (the 1/sec limit means one write per open). + if wada.fs then + local probe = "0123456789abcdef" + wada.fs.write("sdktest.bin", probe) + local part, total = wada.fs.read("sdktest.bin", 4, 4) + local ok = (part == "4567" and total == #probe) + row("fs.read(name,4,4): " .. tostring(part) .. " total=" .. tostring(total) .. + (ok and " PASS" or " FAIL"), ok and C.good or C.bad) + end + + -- Physical SD listing is read-only and independently feature-detected. A + -- supported board with no inserted card is a normal bench state, not a test + -- failure; API shape and path rejection can still be checked separately. + if c.sd_list then + if not wada.sd or not wada.sd.list then + row("wada.sd.list: MISSING despite caps", C.bad) + else + local guard_ok, guard_failure = true, nil + local bad_paths = { "/..", "/.", "/a/../b", "/a//b", "/a/", "relative", "/a\\b" } + for _, path in ipairs(bad_paths) do + local rejected, baderr = wada.sd.list(path) + if rejected ~= nil or baderr ~= "bad path" then + guard_ok = false + guard_failure = path .. " -> " .. tostring(baderr) + break + end + end + row("sd traversal guard: " .. (guard_ok and "PASS" or ("FAIL " .. guard_failure)), + guard_ok and C.good or C.bad) + + local entries, err = wada.sd.list("/") + if not entries then + local note = err == "busy" and " (retry later)" or " (no readable card)" + row("sd.list('/'): " .. tostring(err) .. note, C.sub) + else + local metadata_ok, first_dir = true, nil + for _, entry in ipairs(entries) do + if type(entry.name) ~= "string" or + (entry.type ~= "file" and entry.type ~= "dir") or + type(entry.size) ~= "number" or type(entry.mtime) ~= "number" then + metadata_ok = false + end + if not first_dir and entry.type == "dir" then first_dir = entry.name end + end + row("sd.list('/'): " .. #entries .. " entries, metadata " .. + (metadata_ok and "PASS" or "FAIL") .. + (entries.truncated and " (truncated)" or ""), + metadata_ok and C.good or C.bad) + if first_dir then + local nested, nested_err = wada.sd.list("/" .. first_dir) + row("sd.list('/" .. first_dir .. "'): " .. + (nested and (#nested .. " entries PASS") or ("FAIL " .. tostring(nested_err))), + nested and C.good or C.bad) + else + row("sd nested listing: no directory on card to test", C.sub) + end + end + end + else + row("wada.sd.list: unavailable on this board", C.sub) + end + + sendline = row("mesh.send: not tried yet", C.sub) + dmline = row("mesh.send_dm: not tried yet", C.sub) + msgline = row("on_message: waiting (needs the read permissions)", C.sub) + y = cy + 4 + ui.button("Send to Public", 6, y, 120, 30, function() + local ok, err = wada.mesh.send("Public", "wadamesh SDK self-test") + sendline:set("mesh.send: " .. tostring(ok) .. " " .. tostring(err)) + sendline:color(ok and C.good or C.bad) + end) + ui.button("DM first contact", 132, y, 130, 30, function() + if not first then dmline:set("mesh.send_dm: no contacts to target"); dmline:color(C.bad); return end + local ok, err = wada.mesh.send_dm(first, "wadamesh SDK self-test (DM)") + dmline:set("mesh.send_dm -> " .. first .. ": " .. tostring(ok) .. " " .. tostring(err)) + dmline:color(ok and C.good or C.bad) + end) + cy = y + 38 + discline = row("mesh.discover: not tried yet", C.sub) + inputline = row("ui.input: not tried yet", C.sub) + -- Probing TRANSMITS and makes every neighbour reply, so it is a button, not + -- something this app does on open. + y = cy + 4 + ui.button("Probe", 6, y, 90, 30, function() + local tag, err = wada.mesh.discover() + if not tag then + discline:set("mesh.discover: " .. tostring(err)); discline:color(C.bad); return + end + discline:set("mesh.discover: sent, waiting for replies..."); discline:color(C.text) + end) + ui.button("Results", 102, y, 90, 30, function() + local hits = wada.mesh.discovered() + if #hits == 0 then + discline:set("discovered(): nothing yet - probe, then wait a few seconds") + discline:color(C.sub); return + end + local h = hits[1] + discline:set(string.format("discovered(): %d first %s snr %.1f/%.1f %s", + #hits, h.name or h.pubkey, h.snr, h.their_snr, h.direct and "direct" or (h.hops .. "h"))) + discline:color(C.good) + end) + y = y + 34 + ui.button("Beep", 6, y, 70, 30, function() sys.beep() end) + ui.button("Input", 82, y, 90, 30, function() + ui.input("Type anything", "hello", function(text) + inputline:set("ui.input -> " .. (text and ("'" .. text .. "'") or "cancelled")) + inputline:color(text and C.good or C.sub) + end) + end) +end + +function app.on_input(ev) + if ev.type == "key" and keyline then + keyline:set("keys: got '" .. tostring(ev.key) .. "'") + keyline:color(C.good) + end +end + +-- Proves the kind field and that DMs/rooms reach an app at all, not just channels. +function app.on_message(m) + if not msgline then return end + msgline:set("on_message: kind=" .. tostring(m.kind) .. " from=" .. tostring(m.sender) .. + " ch=" .. tostring(m.channel) .. " text=" .. tostring(m.text):sub(1, 20)) + msgline:color(C.good) +end + +return app diff --git a/deploy/site/sdk.html b/deploy/site/sdk.html index 6e66ce6..c0a72dc 100644 --- a/deploy/site/sdk.html +++ b/deploy/site/sdk.html @@ -141,6 +141,7 @@ wada.net wada.store wada.fs + wada.audio wada.sd wada.sys wada.timer @@ -441,6 +442,24 @@ end +
+

wada.audio audio

+
+

Asynchronous audio playback from the current app's private storage. A plain name such as track.wav resolves inside the same app folder as wada.fs, whether that folder lives on internal flash, SD, or SD_MMC. Check wada.sys.caps().audio and audio_wav before using it.

+ + + + + + + +
CallReturns / does
wada.audio.play(name)Starts or replaces playback and returns true, or nil, error. Plain names use app-private storage. On boards with caps().audio_sd, sd:/Music/track.wav reads directly from the physical card.
wada.audio.pause()Pauses an active track. Returns whether the command was accepted.
wada.audio.resume()Resumes a paused track. Returns whether the command was accepted.
wada.audio.stop()Stops the active track. Returns whether the command was accepted.
wada.audio.status(){state, path, source, format, error}. State is stopped, playing, paused, ended, or error; error is present only after a playback failure.
+

Supported formats are PCM WAV (16-bit mono or stereo at 8–48 kHz) and MPEG Layer III MP3, including CBR, VBR, and ID3-tagged files. Stereo is mixed to mono on the device. Check audio_wav or audio_mp3 rather than assuming a format exists on older firmware.

+

Playback follows the user's Sound switch and volume, never blocks the Lua callback, and stops automatically when the app closes. A second play() replaces the current track. Playlist ordering belongs to the app, so there are no ambiguous host-side next() or previous() calls.

+

Plain names follow the same sandbox rules as wada.fs. Explicit sd: paths follow wada.sd path validation. Expected errors include bad path, no storage, no sd, not found, busy, muted, and unsupported format.

+
+
+

wada.sd sd_list

@@ -466,7 +485,7 @@ end wada.sys.datetime(){year, month, day, hour, min, sec, wday} in local time. wday is 0 for Sunday. wada.sys.tr(s)Translate s through the device's active language, using the same table the firmware's own interface uses. Returns s unchanged when there is no translation, so it is always safe to wrap a string. Add your keys to deploy/apps/lang/<code>.lang alongside the firmware's. Older firmware has no sys.tr: write local tr = sys.tr or function(x) return x end and call tr(). wada.sys.beep()Short beep on boards with a buzzer; silent elsewhere, and silent when the user has sound off. - wada.sys.caps(){sdk_ext, keyboard, touch, sd, sd_list, compass, accel, discover, input, rx_identity, list, packets, sensors, map, measure}. Check sdk_ext before using anything marked ext below, and sd_list before using wada.sd. The rest are feature flags for calls added after the extended SDK first shipped, so an app can degrade on older firmware instead of erroring. + wada.sys.caps(){sdk_ext, keyboard, touch, sd, sd_list, audio, audio_wav, audio_mp3, audio_sd, compass, accel, discover, input, rx_identity, list, packets, sensors, map, measure}. Check sdk_ext before using anything marked ext, sd_list before using wada.sd, and audio plus the format flag before using wada.audio. audio_sd means direct sd: paths are available; plain audio names use app storage and do not require it. wada.sys.battery() ext{mv, pct, charging}. wada.sys.env() sensors{temp_c, humidity, pressure_hpa, alt_m}, or nil. A field is present only when the hardware actually reported it, so you can tell "no humidity sensor" from "0% humidity". Gated on caps().sensors — does this board have the sensor rail — and not on sdk_ext, which is a memory gate. Those are different questions: the plain Heltec V4 has the Expansion Kit but not the extended SDK, and most boards with the extended SDK have no sensors at all. wada.sys.gps() ext{lat, lon, lat_e6, lon_e6, sats, alt_m, time, speed_kmh, course}, or nil with no fix — which is the normal indoor case, so handle it. lat_e6/lon_e6 are exact micro-degrees; lat/lon are single-precision floats, so log the integers. alt_m is metres. time is satellite time, absent until the receiver has decoded the date. speed_kmh and course (degrees clockwise from north) appear only on boards whose GPS provider reports them, and course only while actually moving — a stationary receiver has no course, so it is absent rather than 0. Treat those last three as optional. @@ -587,7 +606,7 @@ end

Apps run in a restricted environment. These are removed: io, os, require, dofile, and loading new chunks at runtime. Available: math, string, table, and the usual pairs, ipairs, select, pcall, tostring, tonumber.

Memory comes from a capped pool in PSRAM, so an app that allocates without bound fails its own allocation rather than starving the radio or the UI. There is an instruction budget on every callback for the same reason.

-

The extended calls are not on every board. Anything marked ext above — wada.fs, wada.mesh.send, wada.mesh.send_dm, wada.mesh.discover, sys.battery, sys.gps — needs a board with the memory to carry it, so the Heltec V4 keeps its RAM for the mesh instead. Call wada.sys.caps().sdk_ext and degrade gracefully rather than assuming. wada.sd additionally needs a physical card interface, reported by caps().sd_list.

+

The extended calls are not on every board. Anything marked ext above — wada.fs, wada.mesh.send, wada.mesh.send_dm, wada.mesh.discover, sys.battery, sys.gps — needs a board with the memory to carry it, so the Heltec V4 keeps its RAM for the mesh instead. Call wada.sys.caps().sdk_ext and degrade gracefully rather than assuming. wada.sd additionally needs a physical card interface, reported by caps().sd_list. wada.audio instead follows caps().audio: storage may be internal, but the device still needs a stream-capable speaker path.

Rate limits are part of the contract, not a rainy-day guard: wada.fs writes are about one per second with a 32 KB file cap, and wada.mesh.send has a 5 second floor and a 180-character limit. They return false, "too fast" rather than throwing, and hitting them is expected — handle it.

None of this makes a hostile app safe, which is why the catalog is curated. It makes an honest app that has a bug survivable: it gets closed, and the device keeps carrying traffic.

diff --git a/lib/minimp3/LICENSE b/lib/minimp3/LICENSE new file mode 100644 index 0000000..2c4afab --- /dev/null +++ b/lib/minimp3/LICENSE @@ -0,0 +1,117 @@ +CC0 1.0 Universal + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later +claims of infringement build upon, modify, incorporate in other works, reuse +and redistribute as freely as possible in any form whatsoever and for any +purposes, including without limitation commercial purposes. These owners may +contribute to the Commons to promote the ideal of a free culture and the +further production of creative, cultural and scientific works, or to gain +reputation or greater distribution for their Work in part through the use and +efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with a +Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not limited +to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer's heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer's express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, +non transferable, non sublicensable, non exclusive, irrevocable and +unconditional license to exercise Affirmer's Copyright and Related Rights in +the Work (i) in all territories worldwide, (ii) for the maximum duration +provided by applicable law or treaty (including future time extensions), (iii) +in any current or future medium and for any number of copies, and (iv) for any +purpose whatsoever, including without limitation commercial, advertising or +promotional purposes (the "License"). The License shall be deemed effective as +of the date CC0 was applied by Affirmer to the Work. Should any part of the +License for any reason be judged legally invalid or ineffective under +applicable law, such partial invalidity or ineffectiveness shall not +invalidate the remainder of the License, and in such case Affirmer hereby +affirms that he or she will not (i) exercise any of his or her remaining +Copyright and Related Rights in the Work or (ii) assert any associated claims +and causes of action with respect to the Work, in either case contrary to +Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see + + diff --git a/lib/minimp3/README.md b/lib/minimp3/README.md new file mode 100644 index 0000000..7fbdc38 --- /dev/null +++ b/lib/minimp3/README.md @@ -0,0 +1,16 @@ +# minimp3 + +This directory vendors `minimp3.h` from +[lieff/minimp3](https://github.com/lieff/minimp3) at commit +`ea99364f61c14656440e8d77e9c233ccf3124633` under CC0-1.0. + +Pinned upstream and local file hashes: + +- upstream `minimp3.h`: `57e437c5c1f0e8b243885d3929c8973b5e6c778451e0100ab4251d19915cb3ad` +- local `minimp3.h` with the hook below: `3f59a7f29de636ca0db0aab8a5b86739c43ac8bf9ac902010e7220f783c0ae0a` +- `LICENSE`: `6a1ee543e5282cd9061881edf462e6fdab181f328da71fc2c9a6950a80e94d01` + +Wadamesh adds one opt-in `MINIMP3_SCRATCH` hook around the decoder's local +scratch object. The Lua audio worker uses it to place the roughly 16 KB decoder +workspace in PSRAM instead of its FreeRTOS task stack. Builds that do not define +the hook retain upstream behavior. diff --git a/lib/minimp3/minimp3.h b/lib/minimp3/minimp3.h new file mode 100644 index 0000000..ffab423 --- /dev/null +++ b/lib/minimp3/minimp3.h @@ -0,0 +1,1872 @@ +#ifndef MINIMP3_H +#define MINIMP3_H +/* + https://github.com/lieff/minimp3 + To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. + This software is distributed without any warranty. + See . +*/ +#include + +#define MINIMP3_MAX_SAMPLES_PER_FRAME (1152*2) + +typedef struct +{ + int frame_bytes, frame_offset, channels, hz, layer, bitrate_kbps; +} mp3dec_frame_info_t; + +typedef struct +{ + float mdct_overlap[2][9*32], qmf_state[15*2*32]; + int reserv, free_format_bytes; + unsigned char header[4], reserv_buf[511]; +} mp3dec_t; + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +void mp3dec_init(mp3dec_t *dec); +#ifndef MINIMP3_FLOAT_OUTPUT +typedef int16_t mp3d_sample_t; +#else /* MINIMP3_FLOAT_OUTPUT */ +typedef float mp3d_sample_t; +void mp3dec_f32_to_s16(const float *in, int16_t *out, int num_samples); +#endif /* MINIMP3_FLOAT_OUTPUT */ +int mp3dec_decode_frame(mp3dec_t *dec, const uint8_t *mp3, int mp3_bytes, mp3d_sample_t *pcm, mp3dec_frame_info_t *info); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* MINIMP3_H */ +#if defined(MINIMP3_IMPLEMENTATION) && !defined(_MINIMP3_IMPLEMENTATION_GUARD) +#define _MINIMP3_IMPLEMENTATION_GUARD + +#include +#include + +#define MAX_FREE_FORMAT_FRAME_SIZE 2304 /* more than ISO spec's */ +#ifndef MAX_FRAME_SYNC_MATCHES +#define MAX_FRAME_SYNC_MATCHES 10 +#endif /* MAX_FRAME_SYNC_MATCHES */ + +#define MAX_L3_FRAME_PAYLOAD_BYTES MAX_FREE_FORMAT_FRAME_SIZE /* MUST be >= 320000/8/32000*1152 = 1440 */ + +#define MAX_BITRESERVOIR_BYTES 511 +#define SHORT_BLOCK_TYPE 2 +#define STOP_BLOCK_TYPE 3 +#define MODE_MONO 3 +#define MODE_JOINT_STEREO 1 +#define HDR_SIZE 4 +#define HDR_IS_MONO(h) (((h[3]) & 0xC0) == 0xC0) +#define HDR_IS_MS_STEREO(h) (((h[3]) & 0xE0) == 0x60) +#define HDR_IS_FREE_FORMAT(h) (((h[2]) & 0xF0) == 0) +#define HDR_IS_CRC(h) (!((h[1]) & 1)) +#define HDR_TEST_PADDING(h) ((h[2]) & 0x2) +#define HDR_TEST_MPEG1(h) ((h[1]) & 0x8) +#define HDR_TEST_NOT_MPEG25(h) ((h[1]) & 0x10) +#define HDR_TEST_I_STEREO(h) ((h[3]) & 0x10) +#define HDR_TEST_MS_STEREO(h) ((h[3]) & 0x20) +#define HDR_GET_STEREO_MODE(h) (((h[3]) >> 6) & 3) +#define HDR_GET_STEREO_MODE_EXT(h) (((h[3]) >> 4) & 3) +#define HDR_GET_LAYER(h) (((h[1]) >> 1) & 3) +#define HDR_GET_BITRATE(h) ((h[2]) >> 4) +#define HDR_GET_SAMPLE_RATE(h) (((h[2]) >> 2) & 3) +#define HDR_GET_MY_SAMPLE_RATE(h) (HDR_GET_SAMPLE_RATE(h) + (((h[1] >> 3) & 1) + ((h[1] >> 4) & 1))*3) +#define HDR_IS_FRAME_576(h) ((h[1] & 14) == 2) +#define HDR_IS_LAYER_1(h) ((h[1] & 6) == 6) + +#define BITS_DEQUANTIZER_OUT -1 +#define MAX_SCF (255 + BITS_DEQUANTIZER_OUT*4 - 210) +#define MAX_SCFI ((MAX_SCF + 3) & ~3) + +#define MINIMP3_MIN(a, b) ((a) > (b) ? (b) : (a)) +#define MINIMP3_MAX(a, b) ((a) < (b) ? (b) : (a)) + +#if !defined(MINIMP3_NO_SIMD) + +#if !defined(MINIMP3_ONLY_SIMD) && (defined(_M_X64) || defined(__x86_64__) || defined(__aarch64__) || defined(_M_ARM64)) +/* x64 always have SSE2, arm64 always have neon, no need for generic code */ +#define MINIMP3_ONLY_SIMD +#endif /* SIMD checks... */ + +#if (defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64))) || ((defined(__i386__) || defined(__x86_64__)) && defined(__SSE2__)) +#if defined(_MSC_VER) +#include +#endif /* defined(_MSC_VER) */ +#include +#define HAVE_SSE 1 +#define HAVE_SIMD 1 +#define VSTORE _mm_storeu_ps +#define VLD _mm_loadu_ps +#define VSET _mm_set1_ps +#define VADD _mm_add_ps +#define VSUB _mm_sub_ps +#define VMUL _mm_mul_ps +#define VMAC(a, x, y) _mm_add_ps(a, _mm_mul_ps(x, y)) +#define VMSB(a, x, y) _mm_sub_ps(a, _mm_mul_ps(x, y)) +#define VMUL_S(x, s) _mm_mul_ps(x, _mm_set1_ps(s)) +#define VREV(x) _mm_shuffle_ps(x, x, _MM_SHUFFLE(0, 1, 2, 3)) +typedef __m128 f4; +#if defined(_MSC_VER) || defined(MINIMP3_ONLY_SIMD) +#define minimp3_cpuid __cpuid +#else /* defined(_MSC_VER) || defined(MINIMP3_ONLY_SIMD) */ +static __inline__ __attribute__((always_inline)) void minimp3_cpuid(int CPUInfo[], const int InfoType) +{ +#if defined(__PIC__) + __asm__ __volatile__( +#if defined(__x86_64__) + "push %%rbx\n" + "cpuid\n" + "xchgl %%ebx, %1\n" + "pop %%rbx\n" +#else /* defined(__x86_64__) */ + "xchgl %%ebx, %1\n" + "cpuid\n" + "xchgl %%ebx, %1\n" +#endif /* defined(__x86_64__) */ + : "=a" (CPUInfo[0]), "=r" (CPUInfo[1]), "=c" (CPUInfo[2]), "=d" (CPUInfo[3]) + : "a" (InfoType)); +#else /* defined(__PIC__) */ + __asm__ __volatile__( + "cpuid" + : "=a" (CPUInfo[0]), "=b" (CPUInfo[1]), "=c" (CPUInfo[2]), "=d" (CPUInfo[3]) + : "a" (InfoType)); +#endif /* defined(__PIC__)*/ +} +#endif /* defined(_MSC_VER) || defined(MINIMP3_ONLY_SIMD) */ +static int have_simd(void) +{ +#ifdef MINIMP3_ONLY_SIMD + return 1; +#else /* MINIMP3_ONLY_SIMD */ + static int g_have_simd; + int CPUInfo[4]; +#ifdef MINIMP3_TEST + static int g_counter; + if (g_counter++ > 100) + return 0; +#endif /* MINIMP3_TEST */ + if (g_have_simd) + goto end; + minimp3_cpuid(CPUInfo, 0); + g_have_simd = 1; + if (CPUInfo[0] > 0) + { + minimp3_cpuid(CPUInfo, 1); + g_have_simd = (CPUInfo[3] & (1 << 26)) + 1; /* SSE2 */ + } +end: + return g_have_simd - 1; +#endif /* MINIMP3_ONLY_SIMD */ +} +#elif defined(__ARM_NEON) || defined(__aarch64__) || defined(_M_ARM64) +#include +#define HAVE_SSE 0 +#define HAVE_SIMD 1 +#define VSTORE vst1q_f32 +#define VLD vld1q_f32 +#define VSET vmovq_n_f32 +#define VADD vaddq_f32 +#define VSUB vsubq_f32 +#define VMUL vmulq_f32 +#define VMAC(a, x, y) vmlaq_f32(a, x, y) +#define VMSB(a, x, y) vmlsq_f32(a, x, y) +#define VMUL_S(x, s) vmulq_f32(x, vmovq_n_f32(s)) +#define VREV(x) vcombine_f32(vget_high_f32(vrev64q_f32(x)), vget_low_f32(vrev64q_f32(x))) +typedef float32x4_t f4; +static int have_simd() +{ /* TODO: detect neon for !MINIMP3_ONLY_SIMD */ + return 1; +} +#else /* SIMD checks... */ +#define HAVE_SSE 0 +#define HAVE_SIMD 0 +#ifdef MINIMP3_ONLY_SIMD +#error MINIMP3_ONLY_SIMD used, but SSE/NEON not enabled +#endif /* MINIMP3_ONLY_SIMD */ +#endif /* SIMD checks... */ +#else /* !defined(MINIMP3_NO_SIMD) */ +#define HAVE_SIMD 0 +#endif /* !defined(MINIMP3_NO_SIMD) */ + +#if defined(__ARM_ARCH) && (__ARM_ARCH >= 6) && !defined(__aarch64__) && !defined(_M_ARM64) && !defined(__ARM_ARCH_6M__) +#define HAVE_ARMV6 1 +static __inline__ __attribute__((always_inline)) int32_t minimp3_clip_int16_arm(int32_t a) +{ + int32_t x = 0; + __asm__ ("ssat %0, #16, %1" : "=r"(x) : "r"(a)); + return x; +} +#else +#define HAVE_ARMV6 0 +#endif + +typedef struct +{ + const uint8_t *buf; + int pos, limit; +} bs_t; + +typedef struct +{ + float scf[3*64]; + uint8_t total_bands, stereo_bands, bitalloc[64], scfcod[64]; +} L12_scale_info; + +typedef struct +{ + uint8_t tab_offset, code_tab_width, band_count; +} L12_subband_alloc_t; + +typedef struct +{ + const uint8_t *sfbtab; + uint16_t part_23_length, big_values, scalefac_compress; + uint8_t global_gain, block_type, mixed_block_flag, n_long_sfb, n_short_sfb; + uint8_t table_select[3], region_count[3], subblock_gain[3]; + uint8_t preflag, scalefac_scale, count1_table, scfsi; +} L3_gr_info_t; + +typedef struct +{ + bs_t bs; + uint8_t maindata[MAX_BITRESERVOIR_BYTES + MAX_L3_FRAME_PAYLOAD_BYTES]; + L3_gr_info_t gr_info[4]; + float grbuf[2][576], scf[40], syn[18 + 15][2*32]; + uint8_t ist_pos[2][39]; +} mp3dec_scratch_t; + +static void bs_init(bs_t *bs, const uint8_t *data, int bytes) +{ + bs->buf = data; + bs->pos = 0; + bs->limit = bytes*8; +} + +static uint32_t get_bits(bs_t *bs, int n) +{ + uint32_t next, cache = 0, s = bs->pos & 7; + int shl = n + s; + const uint8_t *p = bs->buf + (bs->pos >> 3); + if ((bs->pos += n) > bs->limit) + return 0; + next = *p++ & (255 >> s); + while ((shl -= 8) > 0) + { + cache |= next << shl; + next = *p++; + } + return cache | (next >> -shl); +} + +static int hdr_valid(const uint8_t *h) +{ + return h[0] == 0xff && + ((h[1] & 0xF0) == 0xf0 || (h[1] & 0xFE) == 0xe2) && + (HDR_GET_LAYER(h) != 0) && + (HDR_GET_BITRATE(h) != 15) && + (HDR_GET_SAMPLE_RATE(h) != 3); +} + +static int hdr_compare(const uint8_t *h1, const uint8_t *h2) +{ + return hdr_valid(h2) && + ((h1[1] ^ h2[1]) & 0xFE) == 0 && + ((h1[2] ^ h2[2]) & 0x0C) == 0 && + !(HDR_IS_FREE_FORMAT(h1) ^ HDR_IS_FREE_FORMAT(h2)); +} + +static unsigned hdr_bitrate_kbps(const uint8_t *h) +{ + static const uint8_t halfrate[2][3][15] = { + { { 0,4,8,12,16,20,24,28,32,40,48,56,64,72,80 }, { 0,4,8,12,16,20,24,28,32,40,48,56,64,72,80 }, { 0,16,24,28,32,40,48,56,64,72,80,88,96,112,128 } }, + { { 0,16,20,24,28,32,40,48,56,64,80,96,112,128,160 }, { 0,16,24,28,32,40,48,56,64,80,96,112,128,160,192 }, { 0,16,32,48,64,80,96,112,128,144,160,176,192,208,224 } }, + }; + return 2*halfrate[!!HDR_TEST_MPEG1(h)][HDR_GET_LAYER(h) - 1][HDR_GET_BITRATE(h)]; +} + +static unsigned hdr_sample_rate_hz(const uint8_t *h) +{ + static const unsigned g_hz[3] = { 44100, 48000, 32000 }; + return g_hz[HDR_GET_SAMPLE_RATE(h)] >> (int)!HDR_TEST_MPEG1(h) >> (int)!HDR_TEST_NOT_MPEG25(h); +} + +static unsigned hdr_frame_samples(const uint8_t *h) +{ + return HDR_IS_LAYER_1(h) ? 384 : (1152 >> (int)HDR_IS_FRAME_576(h)); +} + +static int hdr_frame_bytes(const uint8_t *h, int free_format_size) +{ + int frame_bytes = hdr_frame_samples(h)*hdr_bitrate_kbps(h)*125/hdr_sample_rate_hz(h); + if (HDR_IS_LAYER_1(h)) + { + frame_bytes &= ~3; /* slot align */ + } + return frame_bytes ? frame_bytes : free_format_size; +} + +static int hdr_padding(const uint8_t *h) +{ + return HDR_TEST_PADDING(h) ? (HDR_IS_LAYER_1(h) ? 4 : 1) : 0; +} + +#ifndef MINIMP3_ONLY_MP3 +static const L12_subband_alloc_t *L12_subband_alloc_table(const uint8_t *hdr, L12_scale_info *sci) +{ + const L12_subband_alloc_t *alloc; + int mode = HDR_GET_STEREO_MODE(hdr); + int nbands, stereo_bands = (mode == MODE_MONO) ? 0 : (mode == MODE_JOINT_STEREO) ? (HDR_GET_STEREO_MODE_EXT(hdr) << 2) + 4 : 32; + + if (HDR_IS_LAYER_1(hdr)) + { + static const L12_subband_alloc_t g_alloc_L1[] = { { 76, 4, 32 } }; + alloc = g_alloc_L1; + nbands = 32; + } else if (!HDR_TEST_MPEG1(hdr)) + { + static const L12_subband_alloc_t g_alloc_L2M2[] = { { 60, 4, 4 }, { 44, 3, 7 }, { 44, 2, 19 } }; + alloc = g_alloc_L2M2; + nbands = 30; + } else + { + static const L12_subband_alloc_t g_alloc_L2M1[] = { { 0, 4, 3 }, { 16, 4, 8 }, { 32, 3, 12 }, { 40, 2, 7 } }; + int sample_rate_idx = HDR_GET_SAMPLE_RATE(hdr); + unsigned kbps = hdr_bitrate_kbps(hdr) >> (int)(mode != MODE_MONO); + if (!kbps) /* free-format */ + { + kbps = 192; + } + + alloc = g_alloc_L2M1; + nbands = 27; + if (kbps < 56) + { + static const L12_subband_alloc_t g_alloc_L2M1_lowrate[] = { { 44, 4, 2 }, { 44, 3, 10 } }; + alloc = g_alloc_L2M1_lowrate; + nbands = sample_rate_idx == 2 ? 12 : 8; + } else if (kbps >= 96 && sample_rate_idx != 1) + { + nbands = 30; + } + } + + sci->total_bands = (uint8_t)nbands; + sci->stereo_bands = (uint8_t)MINIMP3_MIN(stereo_bands, nbands); + + return alloc; +} + +static void L12_read_scalefactors(bs_t *bs, uint8_t *pba, uint8_t *scfcod, int bands, float *scf) +{ + static const float g_deq_L12[18*3] = { +#define DQ(x) 9.53674316e-07f/x, 7.56931807e-07f/x, 6.00777173e-07f/x + DQ(3),DQ(7),DQ(15),DQ(31),DQ(63),DQ(127),DQ(255),DQ(511),DQ(1023),DQ(2047),DQ(4095),DQ(8191),DQ(16383),DQ(32767),DQ(65535),DQ(3),DQ(5),DQ(9) + }; + int i, m; + for (i = 0; i < bands; i++) + { + float s = 0; + int ba = *pba++; + int mask = ba ? 4 + ((19 >> scfcod[i]) & 3) : 0; + for (m = 4; m; m >>= 1) + { + if (mask & m) + { + int b = get_bits(bs, 6); + s = g_deq_L12[ba*3 - 6 + b % 3]*(1 << 21 >> b/3); + } + *scf++ = s; + } + } +} + +static void L12_read_scale_info(const uint8_t *hdr, bs_t *bs, L12_scale_info *sci) +{ + static const uint8_t g_bitalloc_code_tab[] = { + 0,17, 3, 4, 5,6,7, 8,9,10,11,12,13,14,15,16, + 0,17,18, 3,19,4,5, 6,7, 8, 9,10,11,12,13,16, + 0,17,18, 3,19,4,5,16, + 0,17,18,16, + 0,17,18,19, 4,5,6, 7,8, 9,10,11,12,13,14,15, + 0,17,18, 3,19,4,5, 6,7, 8, 9,10,11,12,13,14, + 0, 2, 3, 4, 5,6,7, 8,9,10,11,12,13,14,15,16 + }; + const L12_subband_alloc_t *subband_alloc = L12_subband_alloc_table(hdr, sci); + + int i, k = 0, ba_bits = 0; + const uint8_t *ba_code_tab = g_bitalloc_code_tab; + + for (i = 0; i < sci->total_bands; i++) + { + uint8_t ba; + if (i == k) + { + k += subband_alloc->band_count; + ba_bits = subband_alloc->code_tab_width; + ba_code_tab = g_bitalloc_code_tab + subband_alloc->tab_offset; + subband_alloc++; + } + ba = ba_code_tab[get_bits(bs, ba_bits)]; + sci->bitalloc[2*i] = ba; + if (i < sci->stereo_bands) + { + ba = ba_code_tab[get_bits(bs, ba_bits)]; + } + sci->bitalloc[2*i + 1] = sci->stereo_bands ? ba : 0; + } + + for (i = 0; i < 2*sci->total_bands; i++) + { + sci->scfcod[i] = sci->bitalloc[i] ? HDR_IS_LAYER_1(hdr) ? 2 : get_bits(bs, 2) : 6; + } + + L12_read_scalefactors(bs, sci->bitalloc, sci->scfcod, sci->total_bands*2, sci->scf); + + for (i = sci->stereo_bands; i < sci->total_bands; i++) + { + sci->bitalloc[2*i + 1] = 0; + } +} + +static int L12_dequantize_granule(float *grbuf, bs_t *bs, L12_scale_info *sci, int group_size) +{ + int i, j, k, choff = 576; + for (j = 0; j < 4; j++) + { + float *dst = grbuf + group_size*j; + for (i = 0; i < 2*sci->total_bands; i++) + { + int ba = sci->bitalloc[i]; + if (ba != 0) + { + if (ba < 17) + { + int half = (1 << (ba - 1)) - 1; + for (k = 0; k < group_size; k++) + { + dst[k] = (float)((int)get_bits(bs, ba) - half); + } + } else + { + unsigned mod = (2 << (ba - 17)) + 1; /* 3, 5, 9 */ + unsigned code = get_bits(bs, mod + 2 - (mod >> 3)); /* 5, 7, 10 */ + for (k = 0; k < group_size; k++, code /= mod) + { + dst[k] = (float)((int)(code % mod - mod/2)); + } + } + } + dst += choff; + choff = 18 - choff; + } + } + return group_size*4; +} + +static void L12_apply_scf_384(L12_scale_info *sci, const float *scf, float *dst) +{ + int i, k; + memcpy(dst + 576 + sci->stereo_bands*18, dst + sci->stereo_bands*18, (sci->total_bands - sci->stereo_bands)*18*sizeof(float)); + for (i = 0; i < sci->total_bands; i++, dst += 18, scf += 6) + { + for (k = 0; k < 12; k++) + { + dst[k + 0] *= scf[0]; + dst[k + 576] *= scf[3]; + } + } +} +#endif /* MINIMP3_ONLY_MP3 */ + +static int L3_read_side_info(bs_t *bs, L3_gr_info_t *gr, const uint8_t *hdr) +{ + static const uint8_t g_scf_long[8][23] = { + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 12,12,12,12,12,12,16,20,24,28,32,40,48,56,64,76,90,2,2,2,2,2,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,18,22,26,32,38,46,54,62,70,76,36,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 4,4,4,4,4,4,6,6,8,8,10,12,16,20,24,28,34,42,50,54,76,158,0 }, + { 4,4,4,4,4,4,6,6,6,8,10,12,16,18,22,28,34,40,46,54,54,192,0 }, + { 4,4,4,4,4,4,6,6,8,10,12,16,20,24,30,38,46,56,68,84,102,26,0 } + }; + static const uint8_t g_scf_short[8][40] = { + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 8,8,8,8,8,8,8,8,8,12,12,12,16,16,16,20,20,20,24,24,24,28,28,28,36,36,36,2,2,2,2,2,2,2,2,2,26,26,26,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,6,6,6,8,8,8,10,10,10,14,14,14,18,18,18,26,26,26,32,32,32,42,42,42,18,18,18,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,32,32,32,44,44,44,12,12,12,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,22,22,22,30,30,30,56,56,56,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,6,6,6,10,10,10,12,12,12,14,14,14,16,16,16,20,20,20,26,26,26,66,66,66,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,12,12,12,16,16,16,20,20,20,26,26,26,34,34,34,42,42,42,12,12,12,0 } + }; + static const uint8_t g_scf_mixed[8][40] = { + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 12,12,12,4,4,4,8,8,8,12,12,12,16,16,16,20,20,20,24,24,24,28,28,28,36,36,36,2,2,2,2,2,2,2,2,2,26,26,26,0 }, + { 6,6,6,6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,14,14,14,18,18,18,26,26,26,32,32,32,42,42,42,18,18,18,0 }, + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,32,32,32,44,44,44,12,12,12,0 }, + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,22,22,22,30,30,30,56,56,56,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,6,6,6,10,10,10,12,12,12,14,14,14,16,16,16,20,20,20,26,26,26,66,66,66,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,8,8,8,12,12,12,16,16,16,20,20,20,26,26,26,34,34,34,42,42,42,12,12,12,0 } + }; + + unsigned tables, scfsi = 0; + int main_data_begin, part_23_sum = 0; + int sr_idx = HDR_GET_MY_SAMPLE_RATE(hdr); sr_idx -= (sr_idx != 0); + int gr_count = HDR_IS_MONO(hdr) ? 1 : 2; + + if (HDR_TEST_MPEG1(hdr)) + { + gr_count *= 2; + main_data_begin = get_bits(bs, 9); + scfsi = get_bits(bs, 7 + gr_count); + } else + { + main_data_begin = get_bits(bs, 8 + gr_count) >> gr_count; + } + + do + { + if (HDR_IS_MONO(hdr)) + { + scfsi <<= 4; + } + gr->part_23_length = (uint16_t)get_bits(bs, 12); + part_23_sum += gr->part_23_length; + gr->big_values = (uint16_t)get_bits(bs, 9); + if (gr->big_values > 288) + { + return -1; + } + gr->global_gain = (uint8_t)get_bits(bs, 8); + gr->scalefac_compress = (uint16_t)get_bits(bs, HDR_TEST_MPEG1(hdr) ? 4 : 9); + gr->sfbtab = g_scf_long[sr_idx]; + gr->n_long_sfb = 22; + gr->n_short_sfb = 0; + if (get_bits(bs, 1)) + { + gr->block_type = (uint8_t)get_bits(bs, 2); + if (!gr->block_type) + { + return -1; + } + gr->mixed_block_flag = (uint8_t)get_bits(bs, 1); + gr->region_count[0] = 7; + gr->region_count[1] = 255; + if (gr->block_type == SHORT_BLOCK_TYPE) + { + scfsi &= 0x0F0F; + if (!gr->mixed_block_flag) + { + gr->region_count[0] = 8; + gr->sfbtab = g_scf_short[sr_idx]; + gr->n_long_sfb = 0; + gr->n_short_sfb = 39; + } else + { + gr->sfbtab = g_scf_mixed[sr_idx]; + gr->n_long_sfb = HDR_TEST_MPEG1(hdr) ? 8 : 6; + gr->n_short_sfb = 30; + } + } + tables = get_bits(bs, 10); + tables <<= 5; + gr->subblock_gain[0] = (uint8_t)get_bits(bs, 3); + gr->subblock_gain[1] = (uint8_t)get_bits(bs, 3); + gr->subblock_gain[2] = (uint8_t)get_bits(bs, 3); + } else + { + gr->block_type = 0; + gr->mixed_block_flag = 0; + tables = get_bits(bs, 15); + gr->region_count[0] = (uint8_t)get_bits(bs, 4); + gr->region_count[1] = (uint8_t)get_bits(bs, 3); + gr->region_count[2] = 255; + } + gr->table_select[0] = (uint8_t)(tables >> 10); + gr->table_select[1] = (uint8_t)((tables >> 5) & 31); + gr->table_select[2] = (uint8_t)((tables) & 31); + gr->preflag = HDR_TEST_MPEG1(hdr) ? get_bits(bs, 1) : (gr->scalefac_compress >= 500); + gr->scalefac_scale = (uint8_t)get_bits(bs, 1); + gr->count1_table = (uint8_t)get_bits(bs, 1); + gr->scfsi = (uint8_t)((scfsi >> 12) & 15); + scfsi <<= 4; + gr++; + } while(--gr_count); + + if (part_23_sum + bs->pos > bs->limit + main_data_begin*8) + { + return -1; + } + + return main_data_begin; +} + +static void L3_read_scalefactors(uint8_t *scf, uint8_t *ist_pos, const uint8_t *scf_size, const uint8_t *scf_count, bs_t *bitbuf, int scfsi) +{ + int i, k; + for (i = 0; i < 4 && scf_count[i]; i++, scfsi *= 2) + { + int cnt = scf_count[i]; + if (scfsi & 8) + { + memcpy(scf, ist_pos, cnt); + } else + { + int bits = scf_size[i]; + if (!bits) + { + memset(scf, 0, cnt); + memset(ist_pos, 0, cnt); + } else + { + int max_scf = (scfsi < 0) ? (1 << bits) - 1 : -1; + for (k = 0; k < cnt; k++) + { + int s = get_bits(bitbuf, bits); + ist_pos[k] = (s == max_scf ? -1 : s); + scf[k] = s; + } + } + } + ist_pos += cnt; + scf += cnt; + } + scf[0] = scf[1] = scf[2] = 0; +} + +static float L3_ldexp_q2(float y, int exp_q2) +{ + static const float g_expfrac[4] = { 9.31322575e-10f,7.83145814e-10f,6.58544508e-10f,5.53767716e-10f }; + int e; + do + { + e = MINIMP3_MIN(30*4, exp_q2); + y *= g_expfrac[e & 3]*(1 << 30 >> (e >> 2)); + } while ((exp_q2 -= e) > 0); + return y; +} + +static void L3_decode_scalefactors(const uint8_t *hdr, uint8_t *ist_pos, bs_t *bs, const L3_gr_info_t *gr, float *scf, int ch) +{ + static const uint8_t g_scf_partitions[3][28] = { + { 6,5,5, 5,6,5,5,5,6,5, 7,3,11,10,0,0, 7, 7, 7,0, 6, 6,6,3, 8, 8,5,0 }, + { 8,9,6,12,6,9,9,9,6,9,12,6,15,18,0,0, 6,15,12,0, 6,12,9,6, 6,18,9,0 }, + { 9,9,6,12,9,9,9,9,9,9,12,6,18,18,0,0,12,12,12,0,12, 9,9,6,15,12,9,0 } + }; + const uint8_t *scf_partition = g_scf_partitions[!!gr->n_short_sfb + !gr->n_long_sfb]; + uint8_t scf_size[4], iscf[40]; + int i, scf_shift = gr->scalefac_scale + 1, gain_exp, scfsi = gr->scfsi; + float gain; + + if (HDR_TEST_MPEG1(hdr)) + { + static const uint8_t g_scfc_decode[16] = { 0,1,2,3, 12,5,6,7, 9,10,11,13, 14,15,18,19 }; + int part = g_scfc_decode[gr->scalefac_compress]; + scf_size[1] = scf_size[0] = (uint8_t)(part >> 2); + scf_size[3] = scf_size[2] = (uint8_t)(part & 3); + } else + { + static const uint8_t g_mod[6*4] = { 5,5,4,4,5,5,4,1,4,3,1,1,5,6,6,1,4,4,4,1,4,3,1,1 }; + int k, modprod, sfc, ist = HDR_TEST_I_STEREO(hdr) && ch; + sfc = gr->scalefac_compress >> ist; + for (k = ist*3*4; sfc >= 0; sfc -= modprod, k += 4) + { + for (modprod = 1, i = 3; i >= 0; i--) + { + scf_size[i] = (uint8_t)(sfc / modprod % g_mod[k + i]); + modprod *= g_mod[k + i]; + } + } + scf_partition += k; + scfsi = -16; + } + L3_read_scalefactors(iscf, ist_pos, scf_size, scf_partition, bs, scfsi); + + if (gr->n_short_sfb) + { + int sh = 3 - scf_shift; + for (i = 0; i < gr->n_short_sfb; i += 3) + { + iscf[gr->n_long_sfb + i + 0] += gr->subblock_gain[0] << sh; + iscf[gr->n_long_sfb + i + 1] += gr->subblock_gain[1] << sh; + iscf[gr->n_long_sfb + i + 2] += gr->subblock_gain[2] << sh; + } + } else if (gr->preflag) + { + static const uint8_t g_preamp[10] = { 1,1,1,1,2,2,3,3,3,2 }; + for (i = 0; i < 10; i++) + { + iscf[11 + i] += g_preamp[i]; + } + } + + gain_exp = gr->global_gain + BITS_DEQUANTIZER_OUT*4 - 210 - (HDR_IS_MS_STEREO(hdr) ? 2 : 0); + gain = L3_ldexp_q2(1 << (MAX_SCFI/4), MAX_SCFI - gain_exp); + for (i = 0; i < (int)(gr->n_long_sfb + gr->n_short_sfb); i++) + { + scf[i] = L3_ldexp_q2(gain, iscf[i] << scf_shift); + } +} + +static const float g_pow43[129 + 16] = { + 0,-1,-2.519842f,-4.326749f,-6.349604f,-8.549880f,-10.902724f,-13.390518f,-16.000000f,-18.720754f,-21.544347f,-24.463781f,-27.473142f,-30.567351f,-33.741992f,-36.993181f, + 0,1,2.519842f,4.326749f,6.349604f,8.549880f,10.902724f,13.390518f,16.000000f,18.720754f,21.544347f,24.463781f,27.473142f,30.567351f,33.741992f,36.993181f,40.317474f,43.711787f,47.173345f,50.699631f,54.288352f,57.937408f,61.644865f,65.408941f,69.227979f,73.100443f,77.024898f,81.000000f,85.024491f,89.097188f,93.216975f,97.382800f,101.593667f,105.848633f,110.146801f,114.487321f,118.869381f,123.292209f,127.755065f,132.257246f,136.798076f,141.376907f,145.993119f,150.646117f,155.335327f,160.060199f,164.820202f,169.614826f,174.443577f,179.305980f,184.201575f,189.129918f,194.090580f,199.083145f,204.107210f,209.162385f,214.248292f,219.364564f,224.510845f,229.686789f,234.892058f,240.126328f,245.389280f,250.680604f,256.000000f,261.347174f,266.721841f,272.123723f,277.552547f,283.008049f,288.489971f,293.998060f,299.532071f,305.091761f,310.676898f,316.287249f,321.922592f,327.582707f,333.267377f,338.976394f,344.709550f,350.466646f,356.247482f,362.051866f,367.879608f,373.730522f,379.604427f,385.501143f,391.420496f,397.362314f,403.326427f,409.312672f,415.320884f,421.350905f,427.402579f,433.475750f,439.570269f,445.685987f,451.822757f,457.980436f,464.158883f,470.357960f,476.577530f,482.817459f,489.077615f,495.357868f,501.658090f,507.978156f,514.317941f,520.677324f,527.056184f,533.454404f,539.871867f,546.308458f,552.764065f,559.238575f,565.731879f,572.243870f,578.774440f,585.323483f,591.890898f,598.476581f,605.080431f,611.702349f,618.342238f,625.000000f,631.675540f,638.368763f,645.079578f +}; + +static float L3_pow_43(int x) +{ + float frac; + int sign, mult = 256; + + if (x < 129) + { + return g_pow43[16 + x]; + } + + if (x < 1024) + { + mult = 16; + x <<= 3; + } + + sign = 2*x & 64; + frac = (float)((x & 63) - sign) / ((x & ~63) + sign); + return g_pow43[16 + ((x + sign) >> 6)]*(1.f + frac*((4.f/3) + frac*(2.f/9)))*mult; +} + +static void L3_huffman(float *dst, bs_t *bs, const L3_gr_info_t *gr_info, const float *scf, int layer3gr_limit) +{ + static const int16_t tabs[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 785,785,785,785,784,784,784,784,513,513,513,513,513,513,513,513,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256, + -255,1313,1298,1282,785,785,785,785,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,290,288, + -255,1313,1298,1282,769,769,769,769,529,529,529,529,529,529,529,529,528,528,528,528,528,528,528,528,512,512,512,512,512,512,512,512,290,288, + -253,-318,-351,-367,785,785,785,785,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,819,818,547,547,275,275,275,275,561,560,515,546,289,274,288,258, + -254,-287,1329,1299,1314,1312,1057,1057,1042,1042,1026,1026,784,784,784,784,529,529,529,529,529,529,529,529,769,769,769,769,768,768,768,768,563,560,306,306,291,259, + -252,-413,-477,-542,1298,-575,1041,1041,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-383,-399,1107,1092,1106,1061,849,849,789,789,1104,1091,773,773,1076,1075,341,340,325,309,834,804,577,577,532,532,516,516,832,818,803,816,561,561,531,531,515,546,289,289,288,258, + -252,-429,-493,-559,1057,1057,1042,1042,529,529,529,529,529,529,529,529,784,784,784,784,769,769,769,769,512,512,512,512,512,512,512,512,-382,1077,-415,1106,1061,1104,849,849,789,789,1091,1076,1029,1075,834,834,597,581,340,340,339,324,804,833,532,532,832,772,818,803,817,787,816,771,290,290,290,290,288,258, + -253,-349,-414,-447,-463,1329,1299,-479,1314,1312,1057,1057,1042,1042,1026,1026,785,785,785,785,784,784,784,784,769,769,769,769,768,768,768,768,-319,851,821,-335,836,850,805,849,341,340,325,336,533,533,579,579,564,564,773,832,578,548,563,516,321,276,306,291,304,259, + -251,-572,-733,-830,-863,-879,1041,1041,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-511,-527,-543,1396,1351,1381,1366,1395,1335,1380,-559,1334,1138,1138,1063,1063,1350,1392,1031,1031,1062,1062,1364,1363,1120,1120,1333,1348,881,881,881,881,375,374,359,373,343,358,341,325,791,791,1123,1122,-703,1105,1045,-719,865,865,790,790,774,774,1104,1029,338,293,323,308,-799,-815,833,788,772,818,803,816,322,292,307,320,561,531,515,546,289,274,288,258, + -251,-525,-605,-685,-765,-831,-846,1298,1057,1057,1312,1282,785,785,785,785,784,784,784,784,769,769,769,769,512,512,512,512,512,512,512,512,1399,1398,1383,1367,1382,1396,1351,-511,1381,1366,1139,1139,1079,1079,1124,1124,1364,1349,1363,1333,882,882,882,882,807,807,807,807,1094,1094,1136,1136,373,341,535,535,881,775,867,822,774,-591,324,338,-671,849,550,550,866,864,609,609,293,336,534,534,789,835,773,-751,834,804,308,307,833,788,832,772,562,562,547,547,305,275,560,515,290,290, + -252,-397,-477,-557,-622,-653,-719,-735,-750,1329,1299,1314,1057,1057,1042,1042,1312,1282,1024,1024,785,785,785,785,784,784,784,784,769,769,769,769,-383,1127,1141,1111,1126,1140,1095,1110,869,869,883,883,1079,1109,882,882,375,374,807,868,838,881,791,-463,867,822,368,263,852,837,836,-543,610,610,550,550,352,336,534,534,865,774,851,821,850,805,593,533,579,564,773,832,578,578,548,548,577,577,307,276,306,291,516,560,259,259, + -250,-2107,-2507,-2764,-2909,-2974,-3007,-3023,1041,1041,1040,1040,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-767,-1052,-1213,-1277,-1358,-1405,-1469,-1535,-1550,-1582,-1614,-1647,-1662,-1694,-1726,-1759,-1774,-1807,-1822,-1854,-1886,1565,-1919,-1935,-1951,-1967,1731,1730,1580,1717,-1983,1729,1564,-1999,1548,-2015,-2031,1715,1595,-2047,1714,-2063,1610,-2079,1609,-2095,1323,1323,1457,1457,1307,1307,1712,1547,1641,1700,1699,1594,1685,1625,1442,1442,1322,1322,-780,-973,-910,1279,1278,1277,1262,1276,1261,1275,1215,1260,1229,-959,974,974,989,989,-943,735,478,478,495,463,506,414,-1039,1003,958,1017,927,942,987,957,431,476,1272,1167,1228,-1183,1256,-1199,895,895,941,941,1242,1227,1212,1135,1014,1014,490,489,503,487,910,1013,985,925,863,894,970,955,1012,847,-1343,831,755,755,984,909,428,366,754,559,-1391,752,486,457,924,997,698,698,983,893,740,740,908,877,739,739,667,667,953,938,497,287,271,271,683,606,590,712,726,574,302,302,738,736,481,286,526,725,605,711,636,724,696,651,589,681,666,710,364,467,573,695,466,466,301,465,379,379,709,604,665,679,316,316,634,633,436,436,464,269,424,394,452,332,438,363,347,408,393,448,331,422,362,407,392,421,346,406,391,376,375,359,1441,1306,-2367,1290,-2383,1337,-2399,-2415,1426,1321,-2431,1411,1336,-2447,-2463,-2479,1169,1169,1049,1049,1424,1289,1412,1352,1319,-2495,1154,1154,1064,1064,1153,1153,416,390,360,404,403,389,344,374,373,343,358,372,327,357,342,311,356,326,1395,1394,1137,1137,1047,1047,1365,1392,1287,1379,1334,1364,1349,1378,1318,1363,792,792,792,792,1152,1152,1032,1032,1121,1121,1046,1046,1120,1120,1030,1030,-2895,1106,1061,1104,849,849,789,789,1091,1076,1029,1090,1060,1075,833,833,309,324,532,532,832,772,818,803,561,561,531,560,515,546,289,274,288,258, + -250,-1179,-1579,-1836,-1996,-2124,-2253,-2333,-2413,-2477,-2542,-2574,-2607,-2622,-2655,1314,1313,1298,1312,1282,785,785,785,785,1040,1040,1025,1025,768,768,768,768,-766,-798,-830,-862,-895,-911,-927,-943,-959,-975,-991,-1007,-1023,-1039,-1055,-1070,1724,1647,-1103,-1119,1631,1767,1662,1738,1708,1723,-1135,1780,1615,1779,1599,1677,1646,1778,1583,-1151,1777,1567,1737,1692,1765,1722,1707,1630,1751,1661,1764,1614,1736,1676,1763,1750,1645,1598,1721,1691,1762,1706,1582,1761,1566,-1167,1749,1629,767,766,751,765,494,494,735,764,719,749,734,763,447,447,748,718,477,506,431,491,446,476,461,505,415,430,475,445,504,399,460,489,414,503,383,474,429,459,502,502,746,752,488,398,501,473,413,472,486,271,480,270,-1439,-1455,1357,-1471,-1487,-1503,1341,1325,-1519,1489,1463,1403,1309,-1535,1372,1448,1418,1476,1356,1462,1387,-1551,1475,1340,1447,1402,1386,-1567,1068,1068,1474,1461,455,380,468,440,395,425,410,454,364,467,466,464,453,269,409,448,268,432,1371,1473,1432,1417,1308,1460,1355,1446,1459,1431,1083,1083,1401,1416,1458,1445,1067,1067,1370,1457,1051,1051,1291,1430,1385,1444,1354,1415,1400,1443,1082,1082,1173,1113,1186,1066,1185,1050,-1967,1158,1128,1172,1097,1171,1081,-1983,1157,1112,416,266,375,400,1170,1142,1127,1065,793,793,1169,1033,1156,1096,1141,1111,1155,1080,1126,1140,898,898,808,808,897,897,792,792,1095,1152,1032,1125,1110,1139,1079,1124,882,807,838,881,853,791,-2319,867,368,263,822,852,837,866,806,865,-2399,851,352,262,534,534,821,836,594,594,549,549,593,593,533,533,848,773,579,579,564,578,548,563,276,276,577,576,306,291,516,560,305,305,275,259, + -251,-892,-2058,-2620,-2828,-2957,-3023,-3039,1041,1041,1040,1040,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-511,-527,-543,-559,1530,-575,-591,1528,1527,1407,1526,1391,1023,1023,1023,1023,1525,1375,1268,1268,1103,1103,1087,1087,1039,1039,1523,-604,815,815,815,815,510,495,509,479,508,463,507,447,431,505,415,399,-734,-782,1262,-815,1259,1244,-831,1258,1228,-847,-863,1196,-879,1253,987,987,748,-767,493,493,462,477,414,414,686,669,478,446,461,445,474,429,487,458,412,471,1266,1264,1009,1009,799,799,-1019,-1276,-1452,-1581,-1677,-1757,-1821,-1886,-1933,-1997,1257,1257,1483,1468,1512,1422,1497,1406,1467,1496,1421,1510,1134,1134,1225,1225,1466,1451,1374,1405,1252,1252,1358,1480,1164,1164,1251,1251,1238,1238,1389,1465,-1407,1054,1101,-1423,1207,-1439,830,830,1248,1038,1237,1117,1223,1148,1236,1208,411,426,395,410,379,269,1193,1222,1132,1235,1221,1116,976,976,1192,1162,1177,1220,1131,1191,963,963,-1647,961,780,-1663,558,558,994,993,437,408,393,407,829,978,813,797,947,-1743,721,721,377,392,844,950,828,890,706,706,812,859,796,960,948,843,934,874,571,571,-1919,690,555,689,421,346,539,539,944,779,918,873,932,842,903,888,570,570,931,917,674,674,-2575,1562,-2591,1609,-2607,1654,1322,1322,1441,1441,1696,1546,1683,1593,1669,1624,1426,1426,1321,1321,1639,1680,1425,1425,1305,1305,1545,1668,1608,1623,1667,1592,1638,1666,1320,1320,1652,1607,1409,1409,1304,1304,1288,1288,1664,1637,1395,1395,1335,1335,1622,1636,1394,1394,1319,1319,1606,1621,1392,1392,1137,1137,1137,1137,345,390,360,375,404,373,1047,-2751,-2767,-2783,1062,1121,1046,-2799,1077,-2815,1106,1061,789,789,1105,1104,263,355,310,340,325,354,352,262,339,324,1091,1076,1029,1090,1060,1075,833,833,788,788,1088,1028,818,818,803,803,561,561,531,531,816,771,546,546,289,274,288,258, + -253,-317,-381,-446,-478,-509,1279,1279,-811,-1179,-1451,-1756,-1900,-2028,-2189,-2253,-2333,-2414,-2445,-2511,-2526,1313,1298,-2559,1041,1041,1040,1040,1025,1025,1024,1024,1022,1007,1021,991,1020,975,1019,959,687,687,1018,1017,671,671,655,655,1016,1015,639,639,758,758,623,623,757,607,756,591,755,575,754,559,543,543,1009,783,-575,-621,-685,-749,496,-590,750,749,734,748,974,989,1003,958,988,973,1002,942,987,957,972,1001,926,986,941,971,956,1000,910,985,925,999,894,970,-1071,-1087,-1102,1390,-1135,1436,1509,1451,1374,-1151,1405,1358,1480,1420,-1167,1507,1494,1389,1342,1465,1435,1450,1326,1505,1310,1493,1373,1479,1404,1492,1464,1419,428,443,472,397,736,526,464,464,486,457,442,471,484,482,1357,1449,1434,1478,1388,1491,1341,1490,1325,1489,1463,1403,1309,1477,1372,1448,1418,1433,1476,1356,1462,1387,-1439,1475,1340,1447,1402,1474,1324,1461,1371,1473,269,448,1432,1417,1308,1460,-1711,1459,-1727,1441,1099,1099,1446,1386,1431,1401,-1743,1289,1083,1083,1160,1160,1458,1445,1067,1067,1370,1457,1307,1430,1129,1129,1098,1098,268,432,267,416,266,400,-1887,1144,1187,1082,1173,1113,1186,1066,1050,1158,1128,1143,1172,1097,1171,1081,420,391,1157,1112,1170,1142,1127,1065,1169,1049,1156,1096,1141,1111,1155,1080,1126,1154,1064,1153,1140,1095,1048,-2159,1125,1110,1137,-2175,823,823,1139,1138,807,807,384,264,368,263,868,838,853,791,867,822,852,837,866,806,865,790,-2319,851,821,836,352,262,850,805,849,-2399,533,533,835,820,336,261,578,548,563,577,532,532,832,772,562,562,547,547,305,275,560,515,290,290,288,258 }; + static const uint8_t tab32[] = { 130,162,193,209,44,28,76,140,9,9,9,9,9,9,9,9,190,254,222,238,126,94,157,157,109,61,173,205 }; + static const uint8_t tab33[] = { 252,236,220,204,188,172,156,140,124,108,92,76,60,44,28,12 }; + static const int16_t tabindex[2*16] = { 0,32,64,98,0,132,180,218,292,364,426,538,648,746,0,1126,1460,1460,1460,1460,1460,1460,1460,1460,1842,1842,1842,1842,1842,1842,1842,1842 }; + static const uint8_t g_linbits[] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,3,4,6,8,10,13,4,5,6,7,8,9,11,13 }; + +#define PEEK_BITS(n) (bs_cache >> (32 - n)) +#define FLUSH_BITS(n) { bs_cache <<= (n); bs_sh += (n); } +#define CHECK_BITS while (bs_sh >= 0) { bs_cache |= (uint32_t)*bs_next_ptr++ << bs_sh; bs_sh -= 8; } +#define BSPOS ((bs_next_ptr - bs->buf)*8 - 24 + bs_sh) + + float one = 0.0f; + int ireg = 0, big_val_cnt = gr_info->big_values; + const uint8_t *sfb = gr_info->sfbtab; + const uint8_t *bs_next_ptr = bs->buf + bs->pos/8; + uint32_t bs_cache = (((bs_next_ptr[0]*256u + bs_next_ptr[1])*256u + bs_next_ptr[2])*256u + bs_next_ptr[3]) << (bs->pos & 7); + int pairs_to_decode, np, bs_sh = (bs->pos & 7) - 8; + bs_next_ptr += 4; + + while (big_val_cnt > 0) + { + int tab_num = gr_info->table_select[ireg]; + int sfb_cnt = gr_info->region_count[ireg++]; + const int16_t *codebook = tabs + tabindex[tab_num]; + int linbits = g_linbits[tab_num]; + if (linbits) + { + do + { + np = *sfb++ / 2; + pairs_to_decode = MINIMP3_MIN(big_val_cnt, np); + one = *scf++; + do + { + int j, w = 5; + int leaf = codebook[PEEK_BITS(w)]; + while (leaf < 0) + { + FLUSH_BITS(w); + w = leaf & 7; + leaf = codebook[PEEK_BITS(w) - (leaf >> 3)]; + } + FLUSH_BITS(leaf >> 8); + + for (j = 0; j < 2; j++, dst++, leaf >>= 4) + { + int lsb = leaf & 0x0F; + if (lsb == 15) + { + lsb += PEEK_BITS(linbits); + FLUSH_BITS(linbits); + CHECK_BITS; + *dst = one*L3_pow_43(lsb)*((int32_t)bs_cache < 0 ? -1: 1); + } else + { + *dst = g_pow43[16 + lsb - 16*(bs_cache >> 31)]*one; + } + FLUSH_BITS(lsb ? 1 : 0); + } + CHECK_BITS; + } while (--pairs_to_decode); + } while ((big_val_cnt -= np) > 0 && --sfb_cnt >= 0); + } else + { + do + { + np = *sfb++ / 2; + pairs_to_decode = MINIMP3_MIN(big_val_cnt, np); + one = *scf++; + do + { + int j, w = 5; + int leaf = codebook[PEEK_BITS(w)]; + while (leaf < 0) + { + FLUSH_BITS(w); + w = leaf & 7; + leaf = codebook[PEEK_BITS(w) - (leaf >> 3)]; + } + FLUSH_BITS(leaf >> 8); + + for (j = 0; j < 2; j++, dst++, leaf >>= 4) + { + int lsb = leaf & 0x0F; + *dst = g_pow43[16 + lsb - 16*(bs_cache >> 31)]*one; + FLUSH_BITS(lsb ? 1 : 0); + } + CHECK_BITS; + } while (--pairs_to_decode); + } while ((big_val_cnt -= np) > 0 && --sfb_cnt >= 0); + } + } + + for (np = 1 - big_val_cnt;; dst += 4) + { + const uint8_t *codebook_count1 = (gr_info->count1_table) ? tab33 : tab32; + int leaf = codebook_count1[PEEK_BITS(4)]; + if (!(leaf & 8)) + { + leaf = codebook_count1[(leaf >> 3) + (bs_cache << 4 >> (32 - (leaf & 3)))]; + } + FLUSH_BITS(leaf & 7); + if (BSPOS > layer3gr_limit) + { + break; + } +#define RELOAD_SCALEFACTOR if (!--np) { np = *sfb++/2; if (!np) break; one = *scf++; } +#define DEQ_COUNT1(s) if (leaf & (128 >> s)) { dst[s] = ((int32_t)bs_cache < 0) ? -one : one; FLUSH_BITS(1) } + RELOAD_SCALEFACTOR; + DEQ_COUNT1(0); + DEQ_COUNT1(1); + RELOAD_SCALEFACTOR; + DEQ_COUNT1(2); + DEQ_COUNT1(3); + CHECK_BITS; + } + + bs->pos = layer3gr_limit; +} + +static void L3_midside_stereo(float *left, int n) +{ + int i = 0; + float *right = left + 576; +#if HAVE_SIMD + if (have_simd()) + { + for (; i < n - 3; i += 4) + { + f4 vl = VLD(left + i); + f4 vr = VLD(right + i); + VSTORE(left + i, VADD(vl, vr)); + VSTORE(right + i, VSUB(vl, vr)); + } +#ifdef __GNUC__ + /* Workaround for spurious -Waggressive-loop-optimizations warning from gcc. + * For more info see: https://github.com/lieff/minimp3/issues/88 + */ + if (__builtin_constant_p(n % 4 == 0) && n % 4 == 0) + return; +#endif + } +#endif /* HAVE_SIMD */ + for (; i < n; i++) + { + float a = left[i]; + float b = right[i]; + left[i] = a + b; + right[i] = a - b; + } +} + +static void L3_intensity_stereo_band(float *left, int n, float kl, float kr) +{ + int i; + for (i = 0; i < n; i++) + { + left[i + 576] = left[i]*kr; + left[i] = left[i]*kl; + } +} + +static void L3_stereo_top_band(const float *right, const uint8_t *sfb, int nbands, int max_band[3]) +{ + int i, k; + + max_band[0] = max_band[1] = max_band[2] = -1; + + for (i = 0; i < nbands; i++) + { + for (k = 0; k < sfb[i]; k += 2) + { + if (right[k] != 0 || right[k + 1] != 0) + { + max_band[i % 3] = i; + break; + } + } + right += sfb[i]; + } +} + +static void L3_stereo_process(float *left, const uint8_t *ist_pos, const uint8_t *sfb, const uint8_t *hdr, int max_band[3], int mpeg2_sh) +{ + static const float g_pan[7*2] = { 0,1,0.21132487f,0.78867513f,0.36602540f,0.63397460f,0.5f,0.5f,0.63397460f,0.36602540f,0.78867513f,0.21132487f,1,0 }; + unsigned i, max_pos = HDR_TEST_MPEG1(hdr) ? 7 : 64; + + for (i = 0; sfb[i]; i++) + { + unsigned ipos = ist_pos[i]; + if ((int)i > max_band[i % 3] && ipos < max_pos) + { + float kl, kr, s = HDR_TEST_MS_STEREO(hdr) ? 1.41421356f : 1; + if (HDR_TEST_MPEG1(hdr)) + { + kl = g_pan[2*ipos]; + kr = g_pan[2*ipos + 1]; + } else + { + kl = 1; + kr = L3_ldexp_q2(1, (ipos + 1) >> 1 << mpeg2_sh); + if (ipos & 1) + { + kl = kr; + kr = 1; + } + } + L3_intensity_stereo_band(left, sfb[i], kl*s, kr*s); + } else if (HDR_TEST_MS_STEREO(hdr)) + { + L3_midside_stereo(left, sfb[i]); + } + left += sfb[i]; + } +} + +static void L3_intensity_stereo(float *left, uint8_t *ist_pos, const L3_gr_info_t *gr, const uint8_t *hdr) +{ + int max_band[3], n_sfb = gr->n_long_sfb + gr->n_short_sfb; + int i, max_blocks = gr->n_short_sfb ? 3 : 1; + + L3_stereo_top_band(left + 576, gr->sfbtab, n_sfb, max_band); + if (gr->n_long_sfb) + { + max_band[0] = max_band[1] = max_band[2] = MINIMP3_MAX(MINIMP3_MAX(max_band[0], max_band[1]), max_band[2]); + } + for (i = 0; i < max_blocks; i++) + { + int default_pos = HDR_TEST_MPEG1(hdr) ? 3 : 0; + int itop = n_sfb - max_blocks + i; + int prev = itop - max_blocks; + ist_pos[itop] = max_band[i] >= prev ? default_pos : ist_pos[prev]; + } + L3_stereo_process(left, ist_pos, gr->sfbtab, hdr, max_band, gr[1].scalefac_compress & 1); +} + +static void L3_reorder(float *grbuf, float *scratch, const uint8_t *sfb) +{ + int i, len; + float *src = grbuf, *dst = scratch; + + for (;0 != (len = *sfb); sfb += 3, src += 2*len) + { + for (i = 0; i < len; i++, src++) + { + *dst++ = src[0*len]; + *dst++ = src[1*len]; + *dst++ = src[2*len]; + } + } + memcpy(grbuf, scratch, (dst - scratch)*sizeof(float)); +} + +static void L3_antialias(float *grbuf, int nbands) +{ + static const float g_aa[2][8] = { + {0.85749293f,0.88174200f,0.94962865f,0.98331459f,0.99551782f,0.99916056f,0.99989920f,0.99999316f}, + {0.51449576f,0.47173197f,0.31337745f,0.18191320f,0.09457419f,0.04096558f,0.01419856f,0.00369997f} + }; + + for (; nbands > 0; nbands--, grbuf += 18) + { + int i = 0; +#if HAVE_SIMD + if (have_simd()) for (; i < 8; i += 4) + { + f4 vu = VLD(grbuf + 18 + i); + f4 vd = VLD(grbuf + 14 - i); + f4 vc0 = VLD(g_aa[0] + i); + f4 vc1 = VLD(g_aa[1] + i); + vd = VREV(vd); + VSTORE(grbuf + 18 + i, VSUB(VMUL(vu, vc0), VMUL(vd, vc1))); + vd = VADD(VMUL(vu, vc1), VMUL(vd, vc0)); + VSTORE(grbuf + 14 - i, VREV(vd)); + } +#endif /* HAVE_SIMD */ +#ifndef MINIMP3_ONLY_SIMD + for(; i < 8; i++) + { + float u = grbuf[18 + i]; + float d = grbuf[17 - i]; + grbuf[18 + i] = u*g_aa[0][i] - d*g_aa[1][i]; + grbuf[17 - i] = u*g_aa[1][i] + d*g_aa[0][i]; + } +#endif /* MINIMP3_ONLY_SIMD */ + } +} + +static void L3_dct3_9(float *y) +{ + float s0, s1, s2, s3, s4, s5, s6, s7, s8, t0, t2, t4; + + s0 = y[0]; s2 = y[2]; s4 = y[4]; s6 = y[6]; s8 = y[8]; + t0 = s0 + s6*0.5f; + s0 -= s6; + t4 = (s4 + s2)*0.93969262f; + t2 = (s8 + s2)*0.76604444f; + s6 = (s4 - s8)*0.17364818f; + s4 += s8 - s2; + + s2 = s0 - s4*0.5f; + y[4] = s4 + s0; + s8 = t0 - t2 + s6; + s0 = t0 - t4 + t2; + s4 = t0 + t4 - s6; + + s1 = y[1]; s3 = y[3]; s5 = y[5]; s7 = y[7]; + + s3 *= 0.86602540f; + t0 = (s5 + s1)*0.98480775f; + t4 = (s5 - s7)*0.34202014f; + t2 = (s1 + s7)*0.64278761f; + s1 = (s1 - s5 - s7)*0.86602540f; + + s5 = t0 - s3 - t2; + s7 = t4 - s3 - t0; + s3 = t4 + s3 - t2; + + y[0] = s4 - s7; + y[1] = s2 + s1; + y[2] = s0 - s3; + y[3] = s8 + s5; + y[5] = s8 - s5; + y[6] = s0 + s3; + y[7] = s2 - s1; + y[8] = s4 + s7; +} + +static void L3_imdct36(float *grbuf, float *overlap, const float *window, int nbands) +{ + int i, j; + static const float g_twid9[18] = { + 0.73727734f,0.79335334f,0.84339145f,0.88701083f,0.92387953f,0.95371695f,0.97629601f,0.99144486f,0.99904822f,0.67559021f,0.60876143f,0.53729961f,0.46174861f,0.38268343f,0.30070580f,0.21643961f,0.13052619f,0.04361938f + }; + + for (j = 0; j < nbands; j++, grbuf += 18, overlap += 9) + { + float co[9], si[9]; + co[0] = -grbuf[0]; + si[0] = grbuf[17]; + for (i = 0; i < 4; i++) + { + si[8 - 2*i] = grbuf[4*i + 1] - grbuf[4*i + 2]; + co[1 + 2*i] = grbuf[4*i + 1] + grbuf[4*i + 2]; + si[7 - 2*i] = grbuf[4*i + 4] - grbuf[4*i + 3]; + co[2 + 2*i] = -(grbuf[4*i + 3] + grbuf[4*i + 4]); + } + L3_dct3_9(co); + L3_dct3_9(si); + + si[1] = -si[1]; + si[3] = -si[3]; + si[5] = -si[5]; + si[7] = -si[7]; + + i = 0; + +#if HAVE_SIMD + if (have_simd()) for (; i < 8; i += 4) + { + f4 vovl = VLD(overlap + i); + f4 vc = VLD(co + i); + f4 vs = VLD(si + i); + f4 vr0 = VLD(g_twid9 + i); + f4 vr1 = VLD(g_twid9 + 9 + i); + f4 vw0 = VLD(window + i); + f4 vw1 = VLD(window + 9 + i); + f4 vsum = VADD(VMUL(vc, vr1), VMUL(vs, vr0)); + VSTORE(overlap + i, VSUB(VMUL(vc, vr0), VMUL(vs, vr1))); + VSTORE(grbuf + i, VSUB(VMUL(vovl, vw0), VMUL(vsum, vw1))); + vsum = VADD(VMUL(vovl, vw1), VMUL(vsum, vw0)); + VSTORE(grbuf + 14 - i, VREV(vsum)); + } +#endif /* HAVE_SIMD */ + for (; i < 9; i++) + { + float ovl = overlap[i]; + float sum = co[i]*g_twid9[9 + i] + si[i]*g_twid9[0 + i]; + overlap[i] = co[i]*g_twid9[0 + i] - si[i]*g_twid9[9 + i]; + grbuf[i] = ovl*window[0 + i] - sum*window[9 + i]; + grbuf[17 - i] = ovl*window[9 + i] + sum*window[0 + i]; + } + } +} + +static void L3_idct3(float x0, float x1, float x2, float *dst) +{ + float m1 = x1*0.86602540f; + float a1 = x0 - x2*0.5f; + dst[1] = x0 + x2; + dst[0] = a1 + m1; + dst[2] = a1 - m1; +} + +static void L3_imdct12(float *x, float *dst, float *overlap) +{ + static const float g_twid3[6] = { 0.79335334f,0.92387953f,0.99144486f, 0.60876143f,0.38268343f,0.13052619f }; + float co[3], si[3]; + int i; + + L3_idct3(-x[0], x[6] + x[3], x[12] + x[9], co); + L3_idct3(x[15], x[12] - x[9], x[6] - x[3], si); + si[1] = -si[1]; + + for (i = 0; i < 3; i++) + { + float ovl = overlap[i]; + float sum = co[i]*g_twid3[3 + i] + si[i]*g_twid3[0 + i]; + overlap[i] = co[i]*g_twid3[0 + i] - si[i]*g_twid3[3 + i]; + dst[i] = ovl*g_twid3[2 - i] - sum*g_twid3[5 - i]; + dst[5 - i] = ovl*g_twid3[5 - i] + sum*g_twid3[2 - i]; + } +} + +static void L3_imdct_short(float *grbuf, float *overlap, int nbands) +{ + for (;nbands > 0; nbands--, overlap += 9, grbuf += 18) + { + float tmp[18]; + memcpy(tmp, grbuf, sizeof(tmp)); + memcpy(grbuf, overlap, 6*sizeof(float)); + L3_imdct12(tmp, grbuf + 6, overlap + 6); + L3_imdct12(tmp + 1, grbuf + 12, overlap + 6); + L3_imdct12(tmp + 2, overlap, overlap + 6); + } +} + +static void L3_change_sign(float *grbuf) +{ + int b, i; + for (b = 0, grbuf += 18; b < 32; b += 2, grbuf += 36) + for (i = 1; i < 18; i += 2) + grbuf[i] = -grbuf[i]; +} + +static void L3_imdct_gr(float *grbuf, float *overlap, unsigned block_type, unsigned n_long_bands) +{ + static const float g_mdct_window[2][18] = { + { 0.99904822f,0.99144486f,0.97629601f,0.95371695f,0.92387953f,0.88701083f,0.84339145f,0.79335334f,0.73727734f,0.04361938f,0.13052619f,0.21643961f,0.30070580f,0.38268343f,0.46174861f,0.53729961f,0.60876143f,0.67559021f }, + { 1,1,1,1,1,1,0.99144486f,0.92387953f,0.79335334f,0,0,0,0,0,0,0.13052619f,0.38268343f,0.60876143f } + }; + if (n_long_bands) + { + L3_imdct36(grbuf, overlap, g_mdct_window[0], n_long_bands); + grbuf += 18*n_long_bands; + overlap += 9*n_long_bands; + } + if (block_type == SHORT_BLOCK_TYPE) + L3_imdct_short(grbuf, overlap, 32 - n_long_bands); + else + L3_imdct36(grbuf, overlap, g_mdct_window[block_type == STOP_BLOCK_TYPE], 32 - n_long_bands); +} + +static void L3_save_reservoir(mp3dec_t *h, mp3dec_scratch_t *s) +{ + int pos = (s->bs.pos + 7)/8u; + int remains = s->bs.limit/8u - pos; + if (remains > MAX_BITRESERVOIR_BYTES) + { + pos += remains - MAX_BITRESERVOIR_BYTES; + remains = MAX_BITRESERVOIR_BYTES; + } + if (remains > 0) + { + memmove(h->reserv_buf, s->maindata + pos, remains); + } + h->reserv = remains; +} + +static int L3_restore_reservoir(mp3dec_t *h, bs_t *bs, mp3dec_scratch_t *s, int main_data_begin) +{ + int frame_bytes = (bs->limit - bs->pos)/8; + int bytes_have = MINIMP3_MIN(h->reserv, main_data_begin); + memcpy(s->maindata, h->reserv_buf + MINIMP3_MAX(0, h->reserv - main_data_begin), MINIMP3_MIN(h->reserv, main_data_begin)); + memcpy(s->maindata + bytes_have, bs->buf + bs->pos/8, frame_bytes); + bs_init(&s->bs, s->maindata, bytes_have + frame_bytes); + return h->reserv >= main_data_begin; +} + +static void L3_decode(mp3dec_t *h, mp3dec_scratch_t *s, L3_gr_info_t *gr_info, int nch) +{ + int ch; + + for (ch = 0; ch < nch; ch++) + { + int layer3gr_limit = s->bs.pos + gr_info[ch].part_23_length; + L3_decode_scalefactors(h->header, s->ist_pos[ch], &s->bs, gr_info + ch, s->scf, ch); + L3_huffman(s->grbuf[ch], &s->bs, gr_info + ch, s->scf, layer3gr_limit); + } + + if (HDR_TEST_I_STEREO(h->header)) + { + L3_intensity_stereo(s->grbuf[0], s->ist_pos[1], gr_info, h->header); + } else if (HDR_IS_MS_STEREO(h->header)) + { + L3_midside_stereo(s->grbuf[0], 576); + } + + for (ch = 0; ch < nch; ch++, gr_info++) + { + int aa_bands = 31; + int n_long_bands = (gr_info->mixed_block_flag ? 2 : 0) << (int)(HDR_GET_MY_SAMPLE_RATE(h->header) == 2); + + if (gr_info->n_short_sfb) + { + aa_bands = n_long_bands - 1; + L3_reorder(s->grbuf[ch] + n_long_bands*18, s->syn[0], gr_info->sfbtab + gr_info->n_long_sfb); + } + + L3_antialias(s->grbuf[ch], aa_bands); + L3_imdct_gr(s->grbuf[ch], h->mdct_overlap[ch], gr_info->block_type, n_long_bands); + L3_change_sign(s->grbuf[ch]); + } +} + +static void mp3d_DCT_II(float *grbuf, int n) +{ + static const float g_sec[24] = { + 10.19000816f,0.50060302f,0.50241929f,3.40760851f,0.50547093f,0.52249861f,2.05778098f,0.51544732f,0.56694406f,1.48416460f,0.53104258f,0.64682180f,1.16943991f,0.55310392f,0.78815460f,0.97256821f,0.58293498f,1.06067765f,0.83934963f,0.62250412f,1.72244716f,0.74453628f,0.67480832f,5.10114861f + }; + int i, k = 0; +#if HAVE_SIMD + if (have_simd()) for (; k < n; k += 4) + { + f4 t[4][8], *x; + float *y = grbuf + k; + + for (x = t[0], i = 0; i < 8; i++, x++) + { + f4 x0 = VLD(&y[i*18]); + f4 x1 = VLD(&y[(15 - i)*18]); + f4 x2 = VLD(&y[(16 + i)*18]); + f4 x3 = VLD(&y[(31 - i)*18]); + f4 t0 = VADD(x0, x3); + f4 t1 = VADD(x1, x2); + f4 t2 = VMUL_S(VSUB(x1, x2), g_sec[3*i + 0]); + f4 t3 = VMUL_S(VSUB(x0, x3), g_sec[3*i + 1]); + x[0] = VADD(t0, t1); + x[8] = VMUL_S(VSUB(t0, t1), g_sec[3*i + 2]); + x[16] = VADD(t3, t2); + x[24] = VMUL_S(VSUB(t3, t2), g_sec[3*i + 2]); + } + for (x = t[0], i = 0; i < 4; i++, x += 8) + { + f4 x0 = x[0], x1 = x[1], x2 = x[2], x3 = x[3], x4 = x[4], x5 = x[5], x6 = x[6], x7 = x[7], xt; + xt = VSUB(x0, x7); x0 = VADD(x0, x7); + x7 = VSUB(x1, x6); x1 = VADD(x1, x6); + x6 = VSUB(x2, x5); x2 = VADD(x2, x5); + x5 = VSUB(x3, x4); x3 = VADD(x3, x4); + x4 = VSUB(x0, x3); x0 = VADD(x0, x3); + x3 = VSUB(x1, x2); x1 = VADD(x1, x2); + x[0] = VADD(x0, x1); + x[4] = VMUL_S(VSUB(x0, x1), 0.70710677f); + x5 = VADD(x5, x6); + x6 = VMUL_S(VADD(x6, x7), 0.70710677f); + x7 = VADD(x7, xt); + x3 = VMUL_S(VADD(x3, x4), 0.70710677f); + x5 = VSUB(x5, VMUL_S(x7, 0.198912367f)); /* rotate by PI/8 */ + x7 = VADD(x7, VMUL_S(x5, 0.382683432f)); + x5 = VSUB(x5, VMUL_S(x7, 0.198912367f)); + x0 = VSUB(xt, x6); xt = VADD(xt, x6); + x[1] = VMUL_S(VADD(xt, x7), 0.50979561f); + x[2] = VMUL_S(VADD(x4, x3), 0.54119611f); + x[3] = VMUL_S(VSUB(x0, x5), 0.60134488f); + x[5] = VMUL_S(VADD(x0, x5), 0.89997619f); + x[6] = VMUL_S(VSUB(x4, x3), 1.30656302f); + x[7] = VMUL_S(VSUB(xt, x7), 2.56291556f); + } + + if (k > n - 3) + { +#if HAVE_SSE +#define VSAVE2(i, v) _mm_storel_pi((__m64 *)(void*)&y[i*18], v) +#else /* HAVE_SSE */ +#define VSAVE2(i, v) vst1_f32((float32_t *)&y[i*18], vget_low_f32(v)) +#endif /* HAVE_SSE */ + for (i = 0; i < 7; i++, y += 4*18) + { + f4 s = VADD(t[3][i], t[3][i + 1]); + VSAVE2(0, t[0][i]); + VSAVE2(1, VADD(t[2][i], s)); + VSAVE2(2, VADD(t[1][i], t[1][i + 1])); + VSAVE2(3, VADD(t[2][1 + i], s)); + } + VSAVE2(0, t[0][7]); + VSAVE2(1, VADD(t[2][7], t[3][7])); + VSAVE2(2, t[1][7]); + VSAVE2(3, t[3][7]); + } else + { +#define VSAVE4(i, v) VSTORE(&y[i*18], v) + for (i = 0; i < 7; i++, y += 4*18) + { + f4 s = VADD(t[3][i], t[3][i + 1]); + VSAVE4(0, t[0][i]); + VSAVE4(1, VADD(t[2][i], s)); + VSAVE4(2, VADD(t[1][i], t[1][i + 1])); + VSAVE4(3, VADD(t[2][1 + i], s)); + } + VSAVE4(0, t[0][7]); + VSAVE4(1, VADD(t[2][7], t[3][7])); + VSAVE4(2, t[1][7]); + VSAVE4(3, t[3][7]); + } + } else +#endif /* HAVE_SIMD */ +#ifdef MINIMP3_ONLY_SIMD + {} /* for HAVE_SIMD=1, MINIMP3_ONLY_SIMD=1 case we do not need non-intrinsic "else" branch */ +#else /* MINIMP3_ONLY_SIMD */ + for (; k < n; k++) + { + float t[4][8], *x, *y = grbuf + k; + + for (x = t[0], i = 0; i < 8; i++, x++) + { + float x0 = y[i*18]; + float x1 = y[(15 - i)*18]; + float x2 = y[(16 + i)*18]; + float x3 = y[(31 - i)*18]; + float t0 = x0 + x3; + float t1 = x1 + x2; + float t2 = (x1 - x2)*g_sec[3*i + 0]; + float t3 = (x0 - x3)*g_sec[3*i + 1]; + x[0] = t0 + t1; + x[8] = (t0 - t1)*g_sec[3*i + 2]; + x[16] = t3 + t2; + x[24] = (t3 - t2)*g_sec[3*i + 2]; + } + for (x = t[0], i = 0; i < 4; i++, x += 8) + { + float x0 = x[0], x1 = x[1], x2 = x[2], x3 = x[3], x4 = x[4], x5 = x[5], x6 = x[6], x7 = x[7], xt; + xt = x0 - x7; x0 += x7; + x7 = x1 - x6; x1 += x6; + x6 = x2 - x5; x2 += x5; + x5 = x3 - x4; x3 += x4; + x4 = x0 - x3; x0 += x3; + x3 = x1 - x2; x1 += x2; + x[0] = x0 + x1; + x[4] = (x0 - x1)*0.70710677f; + x5 = x5 + x6; + x6 = (x6 + x7)*0.70710677f; + x7 = x7 + xt; + x3 = (x3 + x4)*0.70710677f; + x5 -= x7*0.198912367f; /* rotate by PI/8 */ + x7 += x5*0.382683432f; + x5 -= x7*0.198912367f; + x0 = xt - x6; xt += x6; + x[1] = (xt + x7)*0.50979561f; + x[2] = (x4 + x3)*0.54119611f; + x[3] = (x0 - x5)*0.60134488f; + x[5] = (x0 + x5)*0.89997619f; + x[6] = (x4 - x3)*1.30656302f; + x[7] = (xt - x7)*2.56291556f; + + } + for (i = 0; i < 7; i++, y += 4*18) + { + y[0*18] = t[0][i]; + y[1*18] = t[2][i] + t[3][i] + t[3][i + 1]; + y[2*18] = t[1][i] + t[1][i + 1]; + y[3*18] = t[2][i + 1] + t[3][i] + t[3][i + 1]; + } + y[0*18] = t[0][7]; + y[1*18] = t[2][7] + t[3][7]; + y[2*18] = t[1][7]; + y[3*18] = t[3][7]; + } +#endif /* MINIMP3_ONLY_SIMD */ +} + +#ifndef MINIMP3_FLOAT_OUTPUT +static int16_t mp3d_scale_pcm(float sample) +{ +#if HAVE_ARMV6 + int32_t s32 = (int32_t)(sample + .5f); + s32 -= (s32 < 0); + int16_t s = (int16_t)minimp3_clip_int16_arm(s32); +#else + if (sample >= 32766.5) return (int16_t) 32767; + if (sample <= -32767.5) return (int16_t)-32768; + int16_t s = (int16_t)(sample + .5f); + s -= (s < 0); /* away from zero, to be compliant */ +#endif + return s; +} +#else /* MINIMP3_FLOAT_OUTPUT */ +static float mp3d_scale_pcm(float sample) +{ + return sample*(1.f/32768.f); +} +#endif /* MINIMP3_FLOAT_OUTPUT */ + +static void mp3d_synth_pair(mp3d_sample_t *pcm, int nch, const float *z) +{ + float a; + a = (z[14*64] - z[ 0]) * 29; + a += (z[ 1*64] + z[13*64]) * 213; + a += (z[12*64] - z[ 2*64]) * 459; + a += (z[ 3*64] + z[11*64]) * 2037; + a += (z[10*64] - z[ 4*64]) * 5153; + a += (z[ 5*64] + z[ 9*64]) * 6574; + a += (z[ 8*64] - z[ 6*64]) * 37489; + a += z[ 7*64] * 75038; + pcm[0] = mp3d_scale_pcm(a); + + z += 2; + a = z[14*64] * 104; + a += z[12*64] * 1567; + a += z[10*64] * 9727; + a += z[ 8*64] * 64019; + a += z[ 6*64] * -9975; + a += z[ 4*64] * -45; + a += z[ 2*64] * 146; + a += z[ 0*64] * -5; + pcm[16*nch] = mp3d_scale_pcm(a); +} + +static void mp3d_synth(float *xl, mp3d_sample_t *dstl, int nch, float *lins) +{ + int i; + float *xr = xl + 576*(nch - 1); + mp3d_sample_t *dstr = dstl + (nch - 1); + + static const float g_win[] = { + -1,26,-31,208,218,401,-519,2063,2000,4788,-5517,7134,5959,35640,-39336,74992, + -1,24,-35,202,222,347,-581,2080,1952,4425,-5879,7640,5288,33791,-41176,74856, + -1,21,-38,196,225,294,-645,2087,1893,4063,-6237,8092,4561,31947,-43006,74630, + -1,19,-41,190,227,244,-711,2085,1822,3705,-6589,8492,3776,30112,-44821,74313, + -1,17,-45,183,228,197,-779,2075,1739,3351,-6935,8840,2935,28289,-46617,73908, + -1,16,-49,176,228,153,-848,2057,1644,3004,-7271,9139,2037,26482,-48390,73415, + -2,14,-53,169,227,111,-919,2032,1535,2663,-7597,9389,1082,24694,-50137,72835, + -2,13,-58,161,224,72,-991,2001,1414,2330,-7910,9592,70,22929,-51853,72169, + -2,11,-63,154,221,36,-1064,1962,1280,2006,-8209,9750,-998,21189,-53534,71420, + -2,10,-68,147,215,2,-1137,1919,1131,1692,-8491,9863,-2122,19478,-55178,70590, + -3,9,-73,139,208,-29,-1210,1870,970,1388,-8755,9935,-3300,17799,-56778,69679, + -3,8,-79,132,200,-57,-1283,1817,794,1095,-8998,9966,-4533,16155,-58333,68692, + -4,7,-85,125,189,-83,-1356,1759,605,814,-9219,9959,-5818,14548,-59838,67629, + -4,7,-91,117,177,-106,-1428,1698,402,545,-9416,9916,-7154,12980,-61289,66494, + -5,6,-97,111,163,-127,-1498,1634,185,288,-9585,9838,-8540,11455,-62684,65290 + }; + float *zlin = lins + 15*64; + const float *w = g_win; + + zlin[4*15] = xl[18*16]; + zlin[4*15 + 1] = xr[18*16]; + zlin[4*15 + 2] = xl[0]; + zlin[4*15 + 3] = xr[0]; + + zlin[4*31] = xl[1 + 18*16]; + zlin[4*31 + 1] = xr[1 + 18*16]; + zlin[4*31 + 2] = xl[1]; + zlin[4*31 + 3] = xr[1]; + + mp3d_synth_pair(dstr, nch, lins + 4*15 + 1); + mp3d_synth_pair(dstr + 32*nch, nch, lins + 4*15 + 64 + 1); + mp3d_synth_pair(dstl, nch, lins + 4*15); + mp3d_synth_pair(dstl + 32*nch, nch, lins + 4*15 + 64); + +#if HAVE_SIMD + if (have_simd()) for (i = 14; i >= 0; i--) + { +#define VLOAD(k) f4 w0 = VSET(*w++); f4 w1 = VSET(*w++); f4 vz = VLD(&zlin[4*i - 64*k]); f4 vy = VLD(&zlin[4*i - 64*(15 - k)]); +#define V0(k) { VLOAD(k) b = VADD(VMUL(vz, w1), VMUL(vy, w0)) ; a = VSUB(VMUL(vz, w0), VMUL(vy, w1)); } +#define V1(k) { VLOAD(k) b = VADD(b, VADD(VMUL(vz, w1), VMUL(vy, w0))); a = VADD(a, VSUB(VMUL(vz, w0), VMUL(vy, w1))); } +#define V2(k) { VLOAD(k) b = VADD(b, VADD(VMUL(vz, w1), VMUL(vy, w0))); a = VADD(a, VSUB(VMUL(vy, w1), VMUL(vz, w0))); } + f4 a, b; + zlin[4*i] = xl[18*(31 - i)]; + zlin[4*i + 1] = xr[18*(31 - i)]; + zlin[4*i + 2] = xl[1 + 18*(31 - i)]; + zlin[4*i + 3] = xr[1 + 18*(31 - i)]; + zlin[4*i + 64] = xl[1 + 18*(1 + i)]; + zlin[4*i + 64 + 1] = xr[1 + 18*(1 + i)]; + zlin[4*i - 64 + 2] = xl[18*(1 + i)]; + zlin[4*i - 64 + 3] = xr[18*(1 + i)]; + + V0(0) V2(1) V1(2) V2(3) V1(4) V2(5) V1(6) V2(7) + + { +#ifndef MINIMP3_FLOAT_OUTPUT +#if HAVE_SSE + static const f4 g_max = { 32767.0f, 32767.0f, 32767.0f, 32767.0f }; + static const f4 g_min = { -32768.0f, -32768.0f, -32768.0f, -32768.0f }; + __m128i pcm8 = _mm_packs_epi32(_mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(a, g_max), g_min)), + _mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(b, g_max), g_min))); + dstr[(15 - i)*nch] = _mm_extract_epi16(pcm8, 1); + dstr[(17 + i)*nch] = _mm_extract_epi16(pcm8, 5); + dstl[(15 - i)*nch] = _mm_extract_epi16(pcm8, 0); + dstl[(17 + i)*nch] = _mm_extract_epi16(pcm8, 4); + dstr[(47 - i)*nch] = _mm_extract_epi16(pcm8, 3); + dstr[(49 + i)*nch] = _mm_extract_epi16(pcm8, 7); + dstl[(47 - i)*nch] = _mm_extract_epi16(pcm8, 2); + dstl[(49 + i)*nch] = _mm_extract_epi16(pcm8, 6); +#else /* HAVE_SSE */ + int16x4_t pcma, pcmb; + a = VADD(a, VSET(0.5f)); + b = VADD(b, VSET(0.5f)); + pcma = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(a), vreinterpretq_s32_u32(vcltq_f32(a, VSET(0))))); + pcmb = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(b), vreinterpretq_s32_u32(vcltq_f32(b, VSET(0))))); + vst1_lane_s16(dstr + (15 - i)*nch, pcma, 1); + vst1_lane_s16(dstr + (17 + i)*nch, pcmb, 1); + vst1_lane_s16(dstl + (15 - i)*nch, pcma, 0); + vst1_lane_s16(dstl + (17 + i)*nch, pcmb, 0); + vst1_lane_s16(dstr + (47 - i)*nch, pcma, 3); + vst1_lane_s16(dstr + (49 + i)*nch, pcmb, 3); + vst1_lane_s16(dstl + (47 - i)*nch, pcma, 2); + vst1_lane_s16(dstl + (49 + i)*nch, pcmb, 2); +#endif /* HAVE_SSE */ + +#else /* MINIMP3_FLOAT_OUTPUT */ + + static const f4 g_scale = { 1.0f/32768.0f, 1.0f/32768.0f, 1.0f/32768.0f, 1.0f/32768.0f }; + a = VMUL(a, g_scale); + b = VMUL(b, g_scale); +#if HAVE_SSE + _mm_store_ss(dstr + (15 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(1, 1, 1, 1))); + _mm_store_ss(dstr + (17 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(1, 1, 1, 1))); + _mm_store_ss(dstl + (15 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(0, 0, 0, 0))); + _mm_store_ss(dstl + (17 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(0, 0, 0, 0))); + _mm_store_ss(dstr + (47 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(3, 3, 3, 3))); + _mm_store_ss(dstr + (49 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(3, 3, 3, 3))); + _mm_store_ss(dstl + (47 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(2, 2, 2, 2))); + _mm_store_ss(dstl + (49 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(2, 2, 2, 2))); +#else /* HAVE_SSE */ + vst1q_lane_f32(dstr + (15 - i)*nch, a, 1); + vst1q_lane_f32(dstr + (17 + i)*nch, b, 1); + vst1q_lane_f32(dstl + (15 - i)*nch, a, 0); + vst1q_lane_f32(dstl + (17 + i)*nch, b, 0); + vst1q_lane_f32(dstr + (47 - i)*nch, a, 3); + vst1q_lane_f32(dstr + (49 + i)*nch, b, 3); + vst1q_lane_f32(dstl + (47 - i)*nch, a, 2); + vst1q_lane_f32(dstl + (49 + i)*nch, b, 2); +#endif /* HAVE_SSE */ +#endif /* MINIMP3_FLOAT_OUTPUT */ + } + } else +#endif /* HAVE_SIMD */ +#ifdef MINIMP3_ONLY_SIMD + {} /* for HAVE_SIMD=1, MINIMP3_ONLY_SIMD=1 case we do not need non-intrinsic "else" branch */ +#else /* MINIMP3_ONLY_SIMD */ + for (i = 14; i >= 0; i--) + { +#define LOAD(k) float w0 = *w++; float w1 = *w++; float *vz = &zlin[4*i - k*64]; float *vy = &zlin[4*i - (15 - k)*64]; +#define S0(k) { int j; LOAD(k); for (j = 0; j < 4; j++) b[j] = vz[j]*w1 + vy[j]*w0, a[j] = vz[j]*w0 - vy[j]*w1; } +#define S1(k) { int j; LOAD(k); for (j = 0; j < 4; j++) b[j] += vz[j]*w1 + vy[j]*w0, a[j] += vz[j]*w0 - vy[j]*w1; } +#define S2(k) { int j; LOAD(k); for (j = 0; j < 4; j++) b[j] += vz[j]*w1 + vy[j]*w0, a[j] += vy[j]*w1 - vz[j]*w0; } + float a[4], b[4]; + + zlin[4*i] = xl[18*(31 - i)]; + zlin[4*i + 1] = xr[18*(31 - i)]; + zlin[4*i + 2] = xl[1 + 18*(31 - i)]; + zlin[4*i + 3] = xr[1 + 18*(31 - i)]; + zlin[4*(i + 16)] = xl[1 + 18*(1 + i)]; + zlin[4*(i + 16) + 1] = xr[1 + 18*(1 + i)]; + zlin[4*(i - 16) + 2] = xl[18*(1 + i)]; + zlin[4*(i - 16) + 3] = xr[18*(1 + i)]; + + S0(0) S2(1) S1(2) S2(3) S1(4) S2(5) S1(6) S2(7) + + dstr[(15 - i)*nch] = mp3d_scale_pcm(a[1]); + dstr[(17 + i)*nch] = mp3d_scale_pcm(b[1]); + dstl[(15 - i)*nch] = mp3d_scale_pcm(a[0]); + dstl[(17 + i)*nch] = mp3d_scale_pcm(b[0]); + dstr[(47 - i)*nch] = mp3d_scale_pcm(a[3]); + dstr[(49 + i)*nch] = mp3d_scale_pcm(b[3]); + dstl[(47 - i)*nch] = mp3d_scale_pcm(a[2]); + dstl[(49 + i)*nch] = mp3d_scale_pcm(b[2]); + } +#endif /* MINIMP3_ONLY_SIMD */ +} + +static void mp3d_synth_granule(float *qmf_state, float *grbuf, int nbands, int nch, mp3d_sample_t *pcm, float *lins) +{ + int i; + for (i = 0; i < nch; i++) + { + mp3d_DCT_II(grbuf + 576*i, nbands); + } + + memcpy(lins, qmf_state, sizeof(float)*15*64); + + for (i = 0; i < nbands; i += 2) + { + mp3d_synth(grbuf + i, pcm + 32*nch*i, nch, lins + i*64); + } +#ifndef MINIMP3_NONSTANDARD_BUT_LOGICAL + if (nch == 1) + { + for (i = 0; i < 15*64; i += 2) + { + qmf_state[i] = lins[nbands*64 + i]; + } + } else +#endif /* MINIMP3_NONSTANDARD_BUT_LOGICAL */ + { + memcpy(qmf_state, lins + nbands*64, sizeof(float)*15*64); + } +} + +static int mp3d_match_frame(const uint8_t *hdr, int mp3_bytes, int frame_bytes) +{ + int i, nmatch; + for (i = 0, nmatch = 0; nmatch < MAX_FRAME_SYNC_MATCHES; nmatch++) + { + i += hdr_frame_bytes(hdr + i, frame_bytes) + hdr_padding(hdr + i); + if (i + HDR_SIZE > mp3_bytes) + return nmatch > 0; + if (!hdr_compare(hdr, hdr + i)) + return 0; + } + return 1; +} + +static int mp3d_find_frame(const uint8_t *mp3, int mp3_bytes, int *free_format_bytes, int *ptr_frame_bytes) +{ + int i, k; + for (i = 0; i < mp3_bytes - HDR_SIZE; i++, mp3++) + { + if (hdr_valid(mp3)) + { + int frame_bytes = hdr_frame_bytes(mp3, *free_format_bytes); + int frame_and_padding = frame_bytes + hdr_padding(mp3); + + for (k = HDR_SIZE; !frame_bytes && k < MAX_FREE_FORMAT_FRAME_SIZE && i + 2*k < mp3_bytes - HDR_SIZE; k++) + { + if (hdr_compare(mp3, mp3 + k)) + { + int fb = k - hdr_padding(mp3); + int nextfb = fb + hdr_padding(mp3 + k); + if (i + k + nextfb + HDR_SIZE > mp3_bytes || !hdr_compare(mp3, mp3 + k + nextfb)) + continue; + frame_and_padding = k; + frame_bytes = fb; + *free_format_bytes = fb; + } + } + if ((frame_bytes && i + frame_and_padding <= mp3_bytes && + mp3d_match_frame(mp3, mp3_bytes - i, frame_bytes)) || + (!i && frame_and_padding == mp3_bytes)) + { + *ptr_frame_bytes = frame_and_padding; + return i; + } + *free_format_bytes = 0; + } + } + *ptr_frame_bytes = 0; + return mp3_bytes; +} + +void mp3dec_init(mp3dec_t *dec) +{ + dec->header[0] = 0; +} + +int mp3dec_decode_frame(mp3dec_t *dec, const uint8_t *mp3, int mp3_bytes, mp3d_sample_t *pcm, mp3dec_frame_info_t *info) +{ + int i = 0, igr, frame_size = 0, success = 1; + const uint8_t *hdr; + bs_t bs_frame[1]; +#ifdef MINIMP3_SCRATCH +#define scratch (*MINIMP3_SCRATCH) +#else + mp3dec_scratch_t scratch; +#endif + + if (mp3_bytes > 4 && dec->header[0] == 0xff && hdr_compare(dec->header, mp3)) + { + frame_size = hdr_frame_bytes(mp3, dec->free_format_bytes) + hdr_padding(mp3); + if (frame_size != mp3_bytes && (frame_size + HDR_SIZE > mp3_bytes || !hdr_compare(mp3, mp3 + frame_size))) + { + frame_size = 0; + } + } + if (!frame_size) + { + memset(dec, 0, sizeof(mp3dec_t)); + i = mp3d_find_frame(mp3, mp3_bytes, &dec->free_format_bytes, &frame_size); + if (!frame_size || i + frame_size > mp3_bytes) + { + info->frame_bytes = i; + return 0; + } + } + + hdr = mp3 + i; + memcpy(dec->header, hdr, HDR_SIZE); + info->frame_bytes = i + frame_size; + info->frame_offset = i; + info->channels = HDR_IS_MONO(hdr) ? 1 : 2; + info->hz = hdr_sample_rate_hz(hdr); + info->layer = 4 - HDR_GET_LAYER(hdr); + info->bitrate_kbps = hdr_bitrate_kbps(hdr); + + if (!pcm) + { + return hdr_frame_samples(hdr); + } + + bs_init(bs_frame, hdr + HDR_SIZE, frame_size - HDR_SIZE); + if (HDR_IS_CRC(hdr)) + { + get_bits(bs_frame, 16); + } + + if (info->layer == 3) + { + int main_data_begin = L3_read_side_info(bs_frame, scratch.gr_info, hdr); + if (main_data_begin < 0 || bs_frame->pos > bs_frame->limit) + { + mp3dec_init(dec); + return 0; + } + success = L3_restore_reservoir(dec, bs_frame, &scratch, main_data_begin); + if (success) + { + for (igr = 0; igr < (HDR_TEST_MPEG1(hdr) ? 2 : 1); igr++, pcm += 576*info->channels) + { + memset(scratch.grbuf[0], 0, 576*2*sizeof(float)); + L3_decode(dec, &scratch, scratch.gr_info + igr*info->channels, info->channels); + mp3d_synth_granule(dec->qmf_state, scratch.grbuf[0], 18, info->channels, pcm, scratch.syn[0]); + } + } + L3_save_reservoir(dec, &scratch); + } else + { +#ifdef MINIMP3_ONLY_MP3 + return 0; +#else /* MINIMP3_ONLY_MP3 */ + L12_scale_info sci[1]; + L12_read_scale_info(hdr, bs_frame, sci); + + memset(scratch.grbuf[0], 0, 576*2*sizeof(float)); + for (i = 0, igr = 0; igr < 3; igr++) + { + if (12 == (i += L12_dequantize_granule(scratch.grbuf[0] + i, bs_frame, sci, info->layer | 1))) + { + i = 0; + L12_apply_scf_384(sci, sci->scf + igr, scratch.grbuf[0]); + mp3d_synth_granule(dec->qmf_state, scratch.grbuf[0], 12, info->channels, pcm, scratch.syn[0]); + memset(scratch.grbuf[0], 0, 576*2*sizeof(float)); + pcm += 384*info->channels; + } + if (bs_frame->pos > bs_frame->limit) + { + mp3dec_init(dec); + return 0; + } + } +#endif /* MINIMP3_ONLY_MP3 */ + } +#ifdef MINIMP3_SCRATCH +#undef scratch +#endif + return success*hdr_frame_samples(dec->header); +} + +#ifdef MINIMP3_FLOAT_OUTPUT +void mp3dec_f32_to_s16(const float *in, int16_t *out, int num_samples) +{ + int i = 0; +#if HAVE_SIMD + int aligned_count = num_samples & ~7; + for(; i < aligned_count; i += 8) + { + static const f4 g_scale = { 32768.0f, 32768.0f, 32768.0f, 32768.0f }; + f4 a = VMUL(VLD(&in[i ]), g_scale); + f4 b = VMUL(VLD(&in[i+4]), g_scale); +#if HAVE_SSE + static const f4 g_max = { 32767.0f, 32767.0f, 32767.0f, 32767.0f }; + static const f4 g_min = { -32768.0f, -32768.0f, -32768.0f, -32768.0f }; + __m128i pcm8 = _mm_packs_epi32(_mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(a, g_max), g_min)), + _mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(b, g_max), g_min))); + out[i ] = _mm_extract_epi16(pcm8, 0); + out[i+1] = _mm_extract_epi16(pcm8, 1); + out[i+2] = _mm_extract_epi16(pcm8, 2); + out[i+3] = _mm_extract_epi16(pcm8, 3); + out[i+4] = _mm_extract_epi16(pcm8, 4); + out[i+5] = _mm_extract_epi16(pcm8, 5); + out[i+6] = _mm_extract_epi16(pcm8, 6); + out[i+7] = _mm_extract_epi16(pcm8, 7); +#else /* HAVE_SSE */ + int16x4_t pcma, pcmb; + a = VADD(a, VSET(0.5f)); + b = VADD(b, VSET(0.5f)); + pcma = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(a), vreinterpretq_s32_u32(vcltq_f32(a, VSET(0))))); + pcmb = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(b), vreinterpretq_s32_u32(vcltq_f32(b, VSET(0))))); + vst1_lane_s16(out+i , pcma, 0); + vst1_lane_s16(out+i+1, pcma, 1); + vst1_lane_s16(out+i+2, pcma, 2); + vst1_lane_s16(out+i+3, pcma, 3); + vst1_lane_s16(out+i+4, pcmb, 0); + vst1_lane_s16(out+i+5, pcmb, 1); + vst1_lane_s16(out+i+6, pcmb, 2); + vst1_lane_s16(out+i+7, pcmb, 3); +#endif /* HAVE_SSE */ + } +#endif /* HAVE_SIMD */ + for(; i < num_samples; i++) + { + float sample = in[i] * 32768.0f; + if (sample >= 32766.5) + out[i] = (int16_t) 32767; + else if (sample <= -32767.5) + out[i] = (int16_t)-32768; + else + { + int16_t s = (int16_t)(sample + .5f); + s -= (s < 0); /* away from zero, to be compliant */ + out[i] = s; + } + } +} +#endif /* MINIMP3_FLOAT_OUTPUT */ +#endif /* MINIMP3_IMPLEMENTATION && !_MINIMP3_IMPLEMENTATION_GUARD */ diff --git a/scripts/audio_test.lua b/scripts/audio_test.lua new file mode 100644 index 0000000..c539d3a --- /dev/null +++ b/scripts/audio_test.lua @@ -0,0 +1,104 @@ +-- Manual WAV/MP3 transport test for wada.audio. +-- SD test file: /Music/test.mp3 +-- App-local file: /apps/audio_test.d/test.mp3 on the active app storage. +local ui, sys, timer = wada.ui, wada.sys, wada.timer +local audio = wada.audio +local C = ui.colors + +local SD_PATH = "sd:/Music/test.mp3" +local APP_PATH = "test.mp3" + +local app = {} +local source = APP_PATH +local source_line, state_line, detail_line, command_line + +local function show_command(ok, err) + command_line:set(ok and "Command: accepted" or ("Command: " .. tostring(err or "rejected"))) + command_line:color(ok and C.good or C.bad) +end + +local function refresh() + if not audio then return end + local status = audio.status() + state_line:set("State: " .. tostring(status.state)) + state_line:color(status.state == "error" and C.bad or + status.state == "playing" and C.good or C.text) + local detail = "Source: " .. tostring(status.source or "-") .. + " Format: " .. tostring(status.format or "-") + if status.error then detail = detail .. " Error: " .. tostring(status.error) end + detail_line:set(detail) + detail_line:color(status.error and C.bad or C.sub) +end + +function app.on_open(w, h) + local caps = sys.caps() + source = APP_PATH + if caps.audio_sd and wada.sd and wada.sd.list then + local entries = wada.sd.list("/") + if entries then source = SD_PATH end + end + + ui.label("Audio playback test", 6, 4, 14, C.accent) + ui.label("audio=" .. tostring(caps.audio) .. + " wav=" .. tostring(caps.audio_wav) .. + " mp3=" .. tostring(caps.audio_mp3), 6, 24, 12, C.sub) + source_line = ui.label("File: " .. source, 6, 44, 12, C.text) + source_line:width(w - 12) + state_line = ui.label("State: stopped", 6, 64, 12, C.text) + detail_line = ui.label("Source: - Format: -", 6, 82, 12, C.sub) + detail_line:width(w - 12) + command_line = ui.label("Command: ready", 6, 100, 12, C.sub) + command_line:width(w - 12) + + if not caps.audio or not audio then + command_line:set("Command: wada.audio unavailable") + command_line:color(C.bad) + return + end + + local gap = 5 + local button_w = math.max(54, (w - 12 - gap * 3) // 4) + local x = 6 + ui.button("Play", x, 116, button_w, 30, function() + local ok, err = audio.play(source) + show_command(ok, err) + refresh() + end) + x = x + button_w + gap + ui.button("Pause", x, 116, button_w, 30, function() + show_command(audio.pause()) + refresh() + end) + x = x + button_w + gap + ui.button("Resume", x, 116, button_w, 30, function() + show_command(audio.resume()) + refresh() + end) + x = x + button_w + gap + ui.button("Stop", x, 116, button_w, 30, function() + show_command(audio.stop()) + refresh() + end) + + if caps.audio_sd then + ui.button("Switch SD / app storage", 6, 150, math.min(w - 12, 190), 26, function() + source = source == SD_PATH and APP_PATH or SD_PATH + source_line:set("File: " .. source) + command_line:set("Command: source changed") + command_line:color(C.sub) + end) + end + + timer.every(200) + refresh() +end + +function app.on_tick() + refresh() +end + +function app.on_close() + if audio then audio.stop() end +end + +return app diff --git a/scripts/lua-harness/harness.lua b/scripts/lua-harness/harness.lua index dc7adc2..de97db2 100644 --- a/scripts/lua-harness/harness.lua +++ b/scripts/lua-harness/harness.lua @@ -26,6 +26,13 @@ local cfg = {} -- per-scenario device config local widgets = { canvases = 0, labels = 0, buttons = 0, scroll = false, timer_ms = nil } local drawlog = { text = {}, circles = {}, ops = 0 } local storekv = {} +local audio_state = { state = "stopped", path = "", source = "", format = "", error = nil } +local audio_log = {} + +local function audio_host_close() + audio_state.state = "stopped" + audio_log[#audio_log + 1] = "release" +end local function mkcanvas(w, h) checkint(w, "canvas w"); checkint(h, "canvas h") @@ -102,7 +109,11 @@ local function build_wada() wada.sys.beep = function() return false end wada.sys.caps = function() return { sdk_ext = cfg.caps.sdk_ext, keyboard = cfg.caps.keyboard, touch = cfg.caps.touch, sd = true, compass = cfg.caps.compass, - accel = cfg.caps.accel } end + accel = cfg.caps.accel, sd_list = cfg.caps.sd_list or false, + audio = cfg.caps.audio or false, + audio_wav = cfg.caps.audio or false, + audio_mp3 = cfg.caps.audio or false, + audio_sd = cfg.caps.audio_sd or false } end if cfg.caps.sdk_ext then wada.sys.battery = function() return { mv = 3900, pct = 70, charging = false } end wada.sys.gps = function() return cfg.gps and cfg.gps() or nil end @@ -126,6 +137,56 @@ local function build_wada() wada.timer.every = function(ms) checkint(ms, "timer.every"); if ms < 33 then ms = 33 end; widgets.timer_ms = ms end wada.timer.stop = function() widgets.timer_ms = nil end + + if cfg.caps.audio then + wada.audio = {} + wada.audio.play = function(path) + assert(type(path) == "string", "audio.play path must be a string") + local source = "app" + if path:sub(1, 3) == "sd:" then + if not cfg.caps.audio_sd then return nil, "no sd" end + local card_path = path:sub(4) + if card_path:sub(1, 1) ~= "/" or card_path:find("//", 1, true) or + card_path:find("/../", 1, true) or card_path:sub(-3) == "/.." or + card_path:sub(-2) == "/." or card_path:sub(-1) == "/" then + return nil, "bad path" + end + source = "sd" + elseif #path == 0 or #path > 32 or path:sub(1, 1) == "." or + not path:match("^[A-Za-z0-9._-]+$") then + return nil, "bad path" + end + local format = path:lower():sub(-4) + if format ~= ".wav" and format ~= ".mp3" then return nil, "unsupported format" end + audio_state = { state = "playing", path = path, source = source, + format = format:sub(2), error = nil } + audio_log[#audio_log + 1] = "play:" .. path + return true + end + wada.audio.pause = function() + if audio_state.state ~= "playing" then return false end + audio_state.state = "paused"; audio_log[#audio_log + 1] = "pause"; return true + end + wada.audio.resume = function() + if audio_state.state ~= "paused" then return false end + audio_state.state = "playing"; audio_log[#audio_log + 1] = "resume"; return true + end + wada.audio.stop = function() + if audio_state.state ~= "playing" and audio_state.state ~= "paused" then return false end + audio_state.state = "stopped"; audio_log[#audio_log + 1] = "stop"; return true + end + wada.audio.status = function() + local out = {} + for key, value in pairs(audio_state) do out[key] = value end + return out + end + end + if cfg.caps.sd_list then + wada.sd = { list = function(path) + assert(path == "/", "mock SD only exposes the root") + return {} + end } + end return wada end @@ -159,6 +220,8 @@ local function reset_world() widgets = { canvases = 0, labels = 0, buttons = 0, scroll = false, timer_ms = nil } labels, buttons, toasts, drawlog = {}, {}, {}, { text = {}, circles = {}, ops = 0 } clock_ms = 1000 + audio_state = { state = "stopped", path = "", source = "", format = "", error = nil } + audio_log = {} end -- simulated magnetometer: Earth field 0.45 G at true heading `deg` (device frame == @@ -683,6 +746,61 @@ scenarios.tanmatsu = function() if app.on_close then guarded(BUDGET, app.on_close) end end +scenarios.audio_api = function() + cfg = { w = 320, h = 196, + caps = { sdk_ext = true, keyboard = true, touch = false, + audio = true, audio_sd = true, sd_list = true } } + wada = build_wada() + local caps = wada.sys.caps() + assert(caps.audio and caps.audio_wav and caps.audio_mp3 and caps.audio_sd, + "audio capability flags do not match the WAV/MP3 contract") + assert(wada.audio and wada.audio.play and wada.audio.pause and wada.audio.resume and + wada.audio.stop and wada.audio.status, "wada.audio API is incomplete") + + assert(wada.audio.play("track.wav")) + local status = wada.audio.status() + assert(status.state == "playing" and status.path == "track.wav" and + status.source == "app" and status.format == "wav", "app-storage play status is wrong") + assert(wada.audio.pause() and wada.audio.status().state == "paused", "pause failed") + assert(wada.audio.resume() and wada.audio.status().state == "playing", "resume failed") + assert(wada.audio.play("next.wav") and wada.audio.status().path == "next.wav", + "a second play must replace the active track") + assert(wada.audio.play("sd:/Music/card.wav")) + assert(wada.audio.status().source == "sd", "sd: source was not reported") + + local value, err = wada.audio.play("../escape.wav") + assert(value == nil and err == "bad path", "app sandbox traversal was accepted") + value, err = wada.audio.play("sd:/Music/../escape.wav") + assert(value == nil and err == "bad path", "SD traversal was accepted") + assert(wada.audio.play("track.mp3") and wada.audio.status().format == "mp3", + "MP3 playback was not accepted or reported") + value, err = wada.audio.play("track.flac") + assert(value == nil and err == "unsupported format", "unknown audio format was accepted") + + assert(wada.audio.stop() and wada.audio.status().state == "stopped", "stop failed") + assert(wada.audio.play("close.wav")) + audio_host_close() + assert(wada.audio.status().state == "stopped" and audio_log[#audio_log] == "release", + "host close did not release playback") + print(" transport + storage sandbox: PASS (" .. #audio_log .. " host calls)") + + if APP_PATH:match("audio_test%.lua$") then + local app = load_app() + assert(guarded(BUDGET, app.on_open, cfg.w, cfg.h)) + assert(#buttons == 5, "audio test app did not create all transport/source buttons") + buttons[1].fn(); assert(wada.audio.status().state == "playing", "Play button failed") + buttons[2].fn(); assert(wada.audio.status().state == "paused", "Pause button failed") + buttons[3].fn(); assert(wada.audio.status().state == "playing", "Resume button failed") + buttons[4].fn(); assert(wada.audio.status().state == "stopped", "Stop button failed") + buttons[5].fn(); buttons[1].fn() + assert(wada.audio.status().source == "app", "source switch did not select app storage") + guarded(BUDGET, app.on_tick) + guarded(BUDGET, app.on_close) + assert(wada.audio.status().state == "stopped", "test app close did not stop playback") + print(" scripts/audio_test.lua UI: PASS") + end +end + -- instruction cost of one heavy tick (redraw forced every tick by spinning the heading) scenarios.cost = function() cfg = { w = 320, h = 196, caps = { sdk_ext = true, keyboard = true, touch = false, compass = true, accel = true }, @@ -711,7 +829,7 @@ scenarios.cost = function() assert(worst < BUDGET / 4, "tick too expensive") end -local order = { "declination", "align_nofix", "bearings_absolute", "m9", "r8", "v4", "pager", "pager_portrait_jumbo", "tanmatsu", "cost" } +local order = { "declination", "align_nofix", "bearings_absolute", "m9", "r8", "v4", "pager", "pager_portrait_jumbo", "tanmatsu", "audio_api", "cost" } for _, name in ipairs(order) do if SCENARIO == "all" or SCENARIO == name then print("== " .. name) diff --git a/scripts/sideload_app.py b/scripts/sideload_app.py index c74525e..3b29497 100644 --- a/scripts/sideload_app.py +++ b/scripts/sideload_app.py @@ -22,6 +22,7 @@ Usage: scripts/sideload_app.py --port /dev/cu.wchusbserial10 deploy/apps/gpscompass/1.0 scripts/sideload_app.py --port ... --reboot deploy/apps/gpscompass/1.0/gpscompass.lua scripts/sideload_app.py --port ... --dest /lang deploy/apps/lang/11/de.lang + scripts/sideload_app.py --port ... --remote /apps/audio_test.d/test.mp3 ~/Music/test.mp3 Pass an app VERSION directory to send its .lua + .json, or individual files. --reboot restarts the device afterwards so the drawer/Store rescan @@ -152,6 +153,7 @@ def main(): ap.add_argument("--port", required=True) ap.add_argument("--baud", type=int, default=115200) ap.add_argument("--dest", default="/apps", help="/apps (default) or /lang") + ap.add_argument("--remote", help="exact remote path for one file, including /apps/.d/") ap.add_argument("--reboot", action="store_true", help="reboot the device afterwards") args = ap.parse_args() @@ -170,10 +172,19 @@ def main(): else: sys.exit("not found: " + p) + if args.remote: + if len(files) != 1: + sys.exit("--remote requires exactly one input file") + if not args.remote.startswith(("/apps/", "/lang/")): + sys.exit("--remote must begin with /apps/ or /lang/") + s = open_port(args.port, args.baud) try: - for f in files: - push(s, f, "%s/%s" % (args.dest.rstrip("/"), os.path.basename(f))) + if args.remote: + push(s, files[0], args.remote) + else: + for f in files: + push(s, f, "%s/%s" % (args.dest.rstrip("/"), os.path.basename(f))) if args.reboot: s.write(b"reboot\n") s.flush() diff --git a/src/DataStore.cpp b/src/DataStore.cpp index 4919f10..cd698b8 100644 --- a/src/DataStore.cpp +++ b/src/DataStore.cpp @@ -230,6 +230,13 @@ bool DataStore::mkdirRooted(FILESYSTEM* fs, const char* dir) { return fs->exists(p) || fs->mkdir(p); } +File DataStore::openWriteRootedFlatSafe(FILESYSTEM* fs, const char* filename) { + // Do not pass create=true here. Arduino's VFS implementation interprets it + // as "mkdir every parent first", but SPIFFS is flat and returns ENOTSUP for + // mkdir while still accepting slash-containing file keys. + return fs->open(_rp(filename), "w"); +} + bool DataStore::removeRooted(FILESYSTEM* fs, const char* filename) { return fs->remove(_rp(filename)); } diff --git a/src/DataStore.h b/src/DataStore.h index 0a547c3..539f8e9 100644 --- a/src/DataStore.h +++ b/src/DataStore.h @@ -109,6 +109,7 @@ public: File openAppend(FILESYSTEM* fs, const char* filename); // create if missing bool fileExists(FILESYSTEM* fs, const char* filename); bool mkdirRooted(FILESYSTEM* fs, const char* dir); // true if it exists afterwards + File openWriteRootedFlatSafe(FILESYSTEM* fs, const char* filename); // SPIFFS accepts slash keys but no mkdir bool removeRooted(FILESYSTEM* fs, const char* filename); bool renameFile(FILESYSTEM* fs, const char* from, const char* to); #endif diff --git a/src/MyMesh.cpp b/src/MyMesh.cpp index 8f482cc..b769e62 100644 --- a/src/MyMesh.cpp +++ b/src/MyMesh.cpp @@ -5445,7 +5445,7 @@ void MyMesh::checkCLIRescueCmd() { // opens the file, "fadd " lines append to it // (self-checking, see cliPutChunk), "fend" closes it. // Same physical-access trust level as "rm" and "erase" above, with a - // NARROWER scope: only the two sideload directories the Store writes. + // NARROWER scope: Store files plus one app's private .d directory. cliPutBegin(&cli_command[5]); } else if (memcmp(cli_command, "fadd ", 5) == 0) { cliPutChunk(&cli_command[5]); @@ -5470,24 +5470,51 @@ void MyMesh::checkCLIRescueCmd() { // "Error: ...". void MyMesh::cliPutBegin(const char* path) { if (_cli_put) { _cli_put.close(); _cli_put_len = 0; } + _cli_put_ended = false; const char* dir = nullptr; if (memcmp(path, "/apps/", 6) == 0) dir = "/apps"; else if (memcmp(path, "/lang/", 6) == 0) dir = "/lang"; if (!dir) { Serial.println("Error: path must be /apps/ or /lang/"); return; } const char* name = path + 6; - const size_t nlen = strlen(name); - if (nlen == 0 || nlen > 40) { Serial.println("Error: bad file name length"); return; } - for (size_t i = 0; i < nlen; i++) { - const char c = name[i]; - const bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || - c == '.' || c == '_' || c == '-'; + const char* slash = strchr(name, '/'); + const char* leaf = slash ? slash + 1 : name; + const size_t dir_len = slash ? (size_t)(slash - name) : 0; + const size_t leaf_len = strlen(leaf); + if (!leaf_len || leaf_len > 40 || (slash && leaf_len > 32) || + leaf[0] == '.' || strchr(leaf, '/')) { + Serial.println("Error: bad file name"); + return; + } + for (size_t i = 0; i < leaf_len; i++) { + const char c = leaf[i]; + const bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-'; if (!ok) { Serial.println("Error: file name may only use A-Z a-z 0-9 . _ -"); return; } } - if (name[0] == '.') { Serial.println("Error: file name may not start with '.'"); return; } + + char app_data_dir[48] = ""; + if (slash) { + // The only nested destination is /apps/.d/, matching wada.fs. + if (strcmp(dir, "/apps") != 0 || dir_len < 3 || dir_len > 25 || + name[dir_len - 2] != '.' || name[dir_len - 1] != 'd' || name[0] == '.') { + Serial.println("Error: nested path must be /apps/.d/"); + return; + } + for (size_t i = 0; i + 2 < dir_len; i++) { + const char c = name[i]; + const bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' || c == '-'; + if (!ok) { Serial.println("Error: bad app id"); return; } + } + snprintf(app_data_dir, sizeof app_data_dir, "/apps/%.*s", (int)dir_len, name); + } FILESYSTEM* fs = _store->getHotDataFS(); if (!fs) { Serial.println("Error: storage not ready"); return; } - if (!_store->mkdirRooted(fs, dir)) { Serial.printf("Error: cannot create %s\n", dir); return; } - _cli_put = _store->openWrite(fs, path); + // Hierarchical filesystems create these directories. SPIFFS returns ENOTSUP + // but accepts the complete slash-containing path as a flat file key. + _store->mkdirRooted(fs, dir); + if (app_data_dir[0]) _store->mkdirRooted(fs, app_data_dir); + _cli_put = _store->openWriteRootedFlatSafe(fs, path); if (!_cli_put) { Serial.printf("Error: cannot open %s for writing\n", path); return; } _cli_put_len = 0; Serial.printf("ok fput %s\n", path); @@ -5518,7 +5545,7 @@ void MyMesh::cliPutChunk(const char* args) { if ((s & 0xFFFF) != (sum & 0xFFFF)) { Serial.printf("Error: checksum at %u\n", (unsigned)_cli_put_len); return; } if (olen && _cli_put.write(buf, olen) != olen) { Serial.println("Error: write failed (card full?)"); - _cli_put.close(); _cli_put_len = 0; + _cli_put.close(); _cli_put_len = 0; _cli_put_ended = false; return; } _cli_put_len += olen; @@ -5526,9 +5553,15 @@ void MyMesh::cliPutChunk(const char* args) { } void MyMesh::cliPutEnd() { - if (!_cli_put) { Serial.println("Error: no file open (fput first)"); return; } + if (!_cli_put) { + if (_cli_put_ended) Serial.printf("ok fend %u bytes\n", (unsigned)_cli_put_last_len); + else Serial.println("Error: no file open (fput first)"); + return; + } _cli_put.close(); - Serial.printf("ok fend %u bytes\n", (unsigned)_cli_put_len); + _cli_put_last_len = _cli_put_len; + _cli_put_ended = true; + Serial.printf("ok fend %u bytes\n", (unsigned)_cli_put_last_len); _cli_put_len = 0; } #endif // ESP32 diff --git a/src/MyMesh.h b/src/MyMesh.h index 4fb8a51..ad2118b 100644 --- a/src/MyMesh.h +++ b/src/MyMesh.h @@ -1296,6 +1296,8 @@ private: // Serial sideload state ("fput" / "fadd" / "fend"): the file being written. File _cli_put; uint32_t _cli_put_len = 0; + uint32_t _cli_put_last_len = 0; + bool _cli_put_ended = false; void cliPutBegin(const char* path); void cliPutChunk(const char* args); void cliPutEnd(); diff --git a/src/ui-touch/LuaAppHost.cpp b/src/ui-touch/LuaAppHost.cpp index dd51781..f30c20a 100644 --- a/src/ui-touch/LuaAppHost.cpp +++ b/src/ui-touch/LuaAppHost.cpp @@ -35,6 +35,18 @@ extern bool luaHostScreenOn(); // false = display asleep; app tick extern void luaHostKeepAwake(bool on); // hold the screen + ticks for a measuring app // notification chime; false = no sounder / muted extern fs::FS* luaHostAppFs(); // /apps storage root FS (may be null) extern void luaHostAppPath(char* out, size_t cap, const char* rel); // prefixes the store root +#if CAP_LUA_AUDIO +extern bool luaHostAudioPlay(fs::FS* fs, const char* path, const char* shown, + const char* source, uint32_t owner, + char* error, size_t error_cap); +extern bool luaHostAudioPause(uint32_t owner, bool pause); +extern bool luaHostAudioStop(uint32_t owner, bool release); +extern 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); +#endif #if CAP_LUA_SD_LIST extern fs::FS* luaHostSdFs(bool* busy); // mounted physical SD, or null extern bool luaHostSdReadFailed(); // failed open was a dead card @@ -184,10 +196,12 @@ struct Host { AppTimer timers[kMaxTimers]; bool in_lua = false; // re-entrancy guard (dismiss from inside a callback) bool want_close = false; + uint32_t generation = 0; // resource ownership across close/reopen char id[24] = ""; char title[32] = ""; }; Host* s_h = nullptr; +uint32_t s_host_generation = 0; char s_bar_title[40]; // appPageBegin keeps the pointer — must outlive the page @@ -764,12 +778,17 @@ int sysBeep(lua_State* L) { lua_pushboolean(L, luaHostBeep()); return 1; } // rather than assume: the extended SDK is absent on low-resource boards (see // CAP_LUA_SDK_EXT in device_caps.h), and a store app runs on all of them. int sysCaps(lua_State* L) { - lua_createtable(L, 0, 14); + lua_createtable(L, 0, 20); lua_pushboolean(L, CAP_LUA_SDK_EXT); lua_setfield(L, -2, "sdk_ext"); lua_pushboolean(L, CAP_KEYBOARD); lua_setfield(L, -2, "keyboard"); lua_pushboolean(L, CAP_TOUCH); lua_setfield(L, -2, "touch"); lua_pushboolean(L, CAP_SD); lua_setfield(L, -2, "sd"); lua_pushboolean(L, CAP_LUA_SD_LIST); lua_setfield(L, -2, "sd_list"); + lua_pushboolean(L, CAP_LUA_AUDIO); lua_setfield(L, -2, "audio"); + lua_pushboolean(L, CAP_LUA_AUDIO); lua_setfield(L, -2, "audio_wav"); + lua_pushboolean(L, CAP_LUA_AUDIO); lua_setfield(L, -2, "audio_mp3"); + lua_pushboolean(L, CAP_LUA_AUDIO && CAP_LUA_SD_LIST); + lua_setfield(L, -2, "audio_sd"); // Feature flags for the calls added after the first extended SDK shipped, so // an app can degrade instead of erroring on firmware that predates them. lua_pushboolean(L, CAP_LUA_SDK_EXT); lua_setfield(L, -2, "discover"); // wada.mesh.discover @@ -1563,6 +1582,93 @@ int sdList(lua_State* L) { #endif #endif // CAP_LUA_SDK_EXT +#if CAP_LUA_AUDIO +static bool audioSafeName(const char* name, size_t len) { + if (!name || len == 0 || len > 32 || name[0] == '.') return false; + for (size_t i = 0; i < len; ++i) { + const char c = name[i]; + const bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-'; + if (!ok) return false; + } + return true; +} + +static int audioError(lua_State* L, const char* error) { + lua_pushnil(L); + lua_pushstring(L, error); + return 2; +} + +// wada.audio.play(name) resolves inside /apps/.d on the board's active +// storage backend. An explicit sd:/... path is available only with sd_list. +int audioPlay(lua_State* L) { + if (!s_h) return audioError(L, "closed"); + size_t requested_len = 0; + const char* requested = luaL_checklstring(L, 1, &requested_len); + fs::FS* fs = nullptr; + char path[224] = ""; + const char* source = "app"; + + if (requested_len >= 3 && !memcmp(requested, "sd:", 3)) { +#if CAP_LUA_SD_LIST + const char* card_path = requested + 3; + const size_t card_len = requested_len - 3; + if (!sdSafePath(card_path, card_len, path, sizeof path)) return audioError(L, "bad path"); + bool busy = false; + fs = luaHostSdFs(&busy); + if (!fs) return audioError(L, busy ? "busy" : "no sd"); + source = "sd"; +#else + return audioError(L, "no sd"); +#endif + } else { + if (!audioSafeName(requested, requested_len)) return audioError(L, "bad path"); + fs = luaHostAppFs(); + if (!fs) return audioError(L, "no storage"); + char rel[96]; + snprintf(rel, sizeof rel, "/apps/%s.d/%s", s_h->id, requested); + luaHostAppPath(path, sizeof path, rel); + } + + char error[40] = ""; + if (!luaHostAudioPlay(fs, path, requested, source, s_h->generation, error, sizeof error)) + return audioError(L, error[0] ? error : "playback failed"); + lua_pushboolean(L, 1); + return 1; +} + +int audioPause(lua_State* L) { + lua_pushboolean(L, s_h && luaHostAudioPause(s_h->generation, true)); + return 1; +} + +int audioResume(lua_State* L) { + lua_pushboolean(L, s_h && luaHostAudioPause(s_h->generation, false)); + return 1; +} + +int audioStop(lua_State* L) { + lua_pushboolean(L, s_h && luaHostAudioStop(s_h->generation, false)); + return 1; +} + +int audioStatus(lua_State* L) { + char state[12], path[192], source[8], format[8], error[40]; + luaHostAudioStatus(s_h ? s_h->generation : 0, + state, sizeof state, path, sizeof path, + source, sizeof source, format, sizeof format, + error, sizeof error); + lua_createtable(L, 0, 5); + lua_pushstring(L, state); lua_setfield(L, -2, "state"); + lua_pushstring(L, path); lua_setfield(L, -2, "path"); + lua_pushstring(L, source); lua_setfield(L, -2, "source"); + lua_pushstring(L, format); lua_setfield(L, -2, "format"); + if (error[0]) { lua_pushstring(L, error); lua_setfield(L, -2, "error"); } + return 1; +} +#endif + void storePath(char* out, size_t cap) { char rel[40]; snprintf(rel, sizeof rel, "/apps/%s.sav", s_h->id); @@ -2172,6 +2278,16 @@ void openWada(lua_State* L) { lua_setfield(L, -2, "sd"); #endif +#if CAP_LUA_AUDIO + lua_newtable(L); // wada.audio (app storage + optional SD) + lua_pushcfunction(L, audioPlay); lua_setfield(L, -2, "play"); + lua_pushcfunction(L, audioPause); lua_setfield(L, -2, "pause"); + lua_pushcfunction(L, audioResume); lua_setfield(L, -2, "resume"); + lua_pushcfunction(L, audioStop); lua_setfield(L, -2, "stop"); + lua_pushcfunction(L, audioStatus); lua_setfield(L, -2, "status"); + lua_setfield(L, -2, "audio"); +#endif + lua_newtable(L); // wada.mesh (read-only) lua_pushcfunction(L, meshContacts); lua_setfield(L, -2, "contacts"); lua_pushcfunction(L, meshRxLog); lua_setfield(L, -2, "rx_log"); @@ -2397,6 +2513,9 @@ void hostTeardown() { Host* h = s_h; if (!h) return; s_h = nullptr; // bindings see "closed" from here on +#if CAP_LUA_AUDIO + luaHostAudioStop(h->generation, true); +#endif // A wada.ui.input dialog lives on lv_layer_top, so it would outlive the app // that opened it. Drop it before the state goes, not after. luaHostTextPromptDismiss(); @@ -2465,6 +2584,8 @@ bool luaAppLaunch(const char* id, const char* title, const char* src, size_t len Host* h = new Host(); h->heap.cap = kHeapCap; + h->generation = ++s_host_generation; + if (!h->generation) h->generation = ++s_host_generation; snprintf(h->id, sizeof h->id, "%s", id); snprintf(h->title, sizeof h->title, "%s", title ? title : id); diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index dcdfcd9..de0c913 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -19,6 +19,20 @@ #include #include #include // 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 #include @@ -52,6 +66,7 @@ #include // detect/read/erase the panic coredump for the crash-report export #include #include // xTaskGetCurrentTaskHandleForCPU / pcTaskGetName — Task-WDT crash self-record + #include #else // Tanmatsu's 16M.csv has no coredump partition yet — stub so the crash-export compiles + links. #include @@ -832,6 +847,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 @@ -988,29 +1053,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; @@ -1302,6 +1344,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) @@ -1321,6 +1366,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) @@ -20033,8 +20081,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 @@ -21552,6 +21602,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, @@ -39611,6 +39664,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) { @@ -39627,11 +39685,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 @@ -39650,6 +39710,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 @@ -47071,18 +47132,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"); @@ -47209,6 +47274,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 @@ -52546,6 +53357,9 @@ static bool sdRuntimeLifecycleBusy() { bool busy = s_hist_flush_busy || s_hist_flush_req || s_sdinfo_request || s_sdinfo_busy || touchPrefsIoBusy(); +#if CAP_LUA_AUDIO + busy = busy || luaAudioStorageBusy(); +#endif #if defined(HAS_TDECK_GT911) || defined(TLORA_PAGER) busy = busy || s_notify_playing; #endif diff --git a/src/ui-touch/device_caps.h b/src/ui-touch/device_caps.h index 879d43f..b005ffc 100644 --- a/src/ui-touch/device_caps.h +++ b/src/ui-touch/device_caps.h @@ -273,6 +273,15 @@ #define CAP_SOUND_FILES 0 #endif +// Sustained PCM output for media playback. This is deliberately independent +// of storage: Lua app files may live on internal flash, SD, or SD_MMC. +#if defined(HAS_TDECK_GT911) || defined(TLORA_PAGER) || \ + defined(HAS_TDISPLAY_P4) || defined(HAS_TANMATSU) + #define CAP_AUDIO_STREAM 1 +#else + #define CAP_AUDIO_STREAM 0 +#endif + // ---- On-device web browser (the "Web" reader app) --------------------------- // The reader fetches pages over on-device HTTPS, and a TLS handshake needs ~30 KB of // free INTERNAL heap. Only the 8 MB-PSRAM boards (T-Deck, Tanmatsu, ThinkNode M9, RAK @@ -294,6 +303,14 @@ #define CAP_LUA_APPS 1 #endif +#ifndef CAP_LUA_AUDIO + #if CAP_LUA_APPS && CAP_AUDIO_STREAM + #define CAP_LUA_AUDIO 1 + #else + #define CAP_LUA_AUDIO 0 + #endif +#endif + // ---- Extended Lua SDK ------------------------------------------------------ // The BASE SDK (drawing, timers, key/value store, read-only mesh + radio stats, // http_get) is board-agnostic and ships everywhere CAP_LUA_APPS is on. diff --git a/src/ui-touch/lua_builtin.h b/src/ui-touch/lua_builtin.h index ecbcc82..ec20023 100644 --- a/src/ui-touch/lua_builtin.h +++ b/src/ui-touch/lua_builtin.h @@ -340,6 +340,8 @@ static const char kLuaSrc_sdktest[] = R"WADALUA(-- SDK self-test. Exercises the -- windowed wada.fs.read. -- 1.6 adds wada.sd.list: capability reporting, traversal rejection, root -- metadata, and a nested directory listing when the card contains one. +-- 1.7 adds storage-neutral wada.audio capability and API-shape checks. Playback +-- stays manual so opening the bench test never emits sound unexpectedly. -- 1.5 uses wada.ui.text_lines() to measure instead of estimating, where the -- firmware has it. That call was added because of the 1.3 bug below: an app -- could ask how tall a line is but not how wide, so laying out rows meant @@ -396,6 +398,21 @@ function app.on_open(w, h) row("caps: sd_list=" .. yn(c.sd_list) .. " discover=" .. yn(c.discover) .. " input=" .. yn(c.input) .. " rx_identity=" .. yn(c.rx_identity), C.accent) + row("caps: audio=" .. yn(c.audio) .. " wav=" .. yn(c.audio_wav) .. + " mp3=" .. yn(c.audio_mp3) .. " audio_sd=" .. yn(c.audio_sd), C.accent) + if c.audio then + local api_ok = wada.audio and type(wada.audio.play) == "function" and + type(wada.audio.pause) == "function" and + type(wada.audio.resume) == "function" and + type(wada.audio.stop) == "function" and + type(wada.audio.status) == "function" + local status = api_ok and wada.audio.status() or nil + api_ok = api_ok and type(status) == "table" and type(status.state) == "string" + row("wada.audio API: " .. (api_ok and ("PASS (" .. status.state .. ")") or "FAIL"), + api_ok and C.good or C.bad) + else + row("wada.audio: unavailable on this board", C.sub) + end row("layout: " .. (ui.text_lines and "measured (ui.text_lines)" or "estimated (older firmware)"), ui.text_lines and C.good or C.sub) -- crypto: published test vectors, so this is checkable rather than merely alive @@ -1775,7 +1792,7 @@ static const LuaBuiltinApp kLuaBuiltin[] = { { "monitor", "RF Monitor", "1.3", kLuaSrc_monitor }, { "airtime", "Airtime", "1.4", kLuaSrc_airtime }, { "snake", "Snake", "1.0", kLuaSrc_snake }, - { "sdktest", "SDK Test", "1.6", kLuaSrc_sdktest }, + { "sdktest", "SDK Test", "1.7", kLuaSrc_sdktest }, { "2048", "2048", "1.2", kLuaSrc_2048 }, { "wardrive", "Wardrive", "1.0", kLuaSrc_wardrive }, { "nearby", "Nearby", "1.0", kLuaSrc_nearby }, diff --git a/test/test_minimp3_decoder.cpp b/test/test_minimp3_decoder.cpp new file mode 100644 index 0000000..93f4faf --- /dev/null +++ b/test/test_minimp3_decoder.cpp @@ -0,0 +1,144 @@ +#include +#include +#include +#include + +static void* testMp3Scratch(); +#define MINIMP3_ONLY_MP3 +#define MINIMP3_NO_SIMD +#define MINIMP3_SCRATCH ((mp3dec_scratch_t*)testMp3Scratch()) +#define MINIMP3_IMPLEMENTATION +#include "../lib/minimp3/minimp3.h" + +static mp3dec_scratch_t g_scratch; +static void* testMp3Scratch() { return &g_scratch; } + +struct DecodeBuffer { + uint8_t input[16 * 1024]; + mp3d_sample_t pcm[MINIMP3_MAX_SAMPLES_PER_FRAME]; +}; + +static uint32_t readLe32(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 long mp3DataEnd(FILE* file) { + assert(fseek(file, 0, SEEK_END) == 0); + long end = ftell(file); + assert(end >= 0); + uint8_t footer[128]; + if (end >= 128 && fseek(file, end - 128, SEEK_SET) == 0 && + fread(footer, 1, 3, file) == 3 && !memcmp(footer, "TAG", 3)) { + end -= 128; + } + if (end >= 32 && fseek(file, end - 32, SEEK_SET) == 0 && + fread(footer, 1, 32, file) == 32 && !memcmp(footer, "APETAGEX", 8)) { + const uint32_t version = readLe32(footer + 8); + const uint32_t tag_size = readLe32(footer + 12); + const uint32_t flags = readLe32(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 <= (uint64_t)end) + end -= (long)total_size; + } + rewind(file); + return end; +} + +static bool decodeFile(const char* path) { + FILE* file = fopen(path, "rb"); + if (!file) { + fprintf(stderr, "%s: could not open\n", path); + return false; + } + const long data_end = mp3DataEnd(file); + if (data_end <= 0) { fclose(file); return false; } + + DecodeBuffer buffer; + mp3dec_t decoder; + mp3dec_init(&decoder); + size_t input_start = 0, input_count = 0; + uint64_t samples_total = 0; + int frames = 0, sample_rate = 0, channels = 0, bitrate_changes = 0; + int last_bitrate = 0; + bool eof = false, ok = true; + + while (ok) { + if (!eof && input_count < 4096) { + if (input_start && input_start + input_count + 4096 > sizeof buffer.input) { + memmove(buffer.input, buffer.input + input_start, input_count); + input_start = 0; + } + size_t room = sizeof buffer.input - (input_start + input_count); + const long position = ftell(file); + const size_t remaining = position < data_end ? (size_t)(data_end - position) : 0; + if (room > remaining) room = remaining; + const size_t got = fread(buffer.input + input_start + input_count, 1, room, file); + if (got > 0) { + input_count += got; + eof = ftell(file) >= data_end; + } + else if (ferror(file)) ok = false; + else eof = true; + } + + if (!ok || !input_count) break; + + mp3dec_frame_info_t info = {}; + const int samples = mp3dec_decode_frame(&decoder, buffer.input + input_start, + (int)input_count, buffer.pcm, &info); + if (info.frame_bytes < 0 || (size_t)info.frame_bytes > input_count) { + ok = false; + 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) { + break; + } else if (input_start + input_count == sizeof buffer.input) { + ok = false; + break; + } else { + continue; + } + + if (samples <= 0) continue; + if (info.layer != 3 || (info.channels != 1 && info.channels != 2) || + info.hz < 8000 || info.hz > 48000) { + ok = false; + break; + } + if (!sample_rate) { + sample_rate = info.hz; + channels = info.channels; + } else if (sample_rate != info.hz) { + ok = false; + break; + } + if (last_bitrate && last_bitrate != info.bitrate_kbps) ++bitrate_changes; + last_bitrate = info.bitrate_kbps; + samples_total += (uint64_t)samples; + ++frames; + } + + fclose(file); + if (!ok || frames == 0 || samples_total == 0) { + fprintf(stderr, "%s: decode failed after %d frames\n", path, frames); + return false; + } + printf("%s: %d frames, %llu samples/channel, %d Hz, %d channel(s), %d bitrate changes\n", + path, frames, (unsigned long long)samples_total, sample_rate, channels, + bitrate_changes); + return true; +} + +int main(int argc, char** argv) { + if (argc < 2) { + fprintf(stderr, "usage: %s file.mp3 [file.mp3 ...]\n", argv[0]); + return 2; + } + for (int i = 1; i < argc; ++i) assert(decodeFile(argv[i])); + return 0; +} diff --git a/variants/tdisplay_p4/P4Audio.cpp b/variants/tdisplay_p4/P4Audio.cpp index c0a5a9e..a2658b8 100644 --- a/variants/tdisplay_p4/P4Audio.cpp +++ b/variants/tdisplay_p4/P4Audio.cpp @@ -89,6 +89,7 @@ bool p4AudioReady() { if (s_ready) return true; if (s_failed) return false; if (!s_mtx) s_mtx = xSemaphoreCreateMutex(); + if (!s_mtx) return false; xSemaphoreTake(s_mtx, portMAX_DELAY); if (s_ready || s_failed) { xSemaphoreGive(s_mtx); return s_ready; } @@ -125,6 +126,9 @@ bool p4AudioReady() { if (i2s_channel_init_std_mode(s_tx, &std_cfg) != ESP_OK || i2s_channel_enable(s_tx) != ESP_OK) { printf("[P4AUDIO] I2S init failed\n"); + i2s_channel_disable(s_tx); + i2s_del_channel(s_tx); + s_tx = nullptr; s_failed = true; xSemaphoreGive(s_mtx); return false; @@ -139,7 +143,7 @@ void p4AudioTone(int freq_hz, int duration_ms, int amplitude) { if (amplitude <= 0 || freq_hz <= 0 || duration_ms <= 0) return; if (!p4AudioReady()) return; if (amplitude > 30000) amplitude = 30000; - xSemaphoreTake(s_mtx, portMAX_DELAY); + if (xSemaphoreTake(s_mtx, 0) != pdTRUE) return; const int total = P4A_RATE * duration_ms / 1000; const int fade = P4A_RATE * 4 / 1000; // 4 ms in/out fade kills the click static int16_t buf[256 * 2]; // stereo frames, chunked @@ -163,4 +167,36 @@ void p4AudioTone(int freq_hz, int duration_ms, int amplitude) { xSemaphoreGive(s_mtx); } +uint32_t p4AudioStreamRate() { return P4A_RATE; } + +bool p4AudioStreamBegin() { + if (!p4AudioReady()) return false; + return xSemaphoreTake(s_mtx, portMAX_DELAY) == pdTRUE; +} + +bool p4AudioStreamWrite(const int16_t* samples, size_t frames) { + if (!samples || !frames || !s_tx) return false; + static int16_t stereo[256 * 2]; + while (frames) { + const size_t count = frames > 256 ? 256 : frames; + for (size_t i = 0; i < count; ++i) + stereo[2 * i] = stereo[2 * i + 1] = samples[i]; + size_t written = 0; + const size_t bytes = count * 2 * sizeof(int16_t); + if (i2s_channel_write(s_tx, stereo, bytes, &written, pdMS_TO_TICKS(300)) != ESP_OK || + written != bytes) return false; + samples += count; + frames -= count; + } + return true; +} + +void p4AudioStreamEnd() { + if (!s_mtx) return; + static const int16_t silence[256 * 2] = {}; + size_t written = 0; + i2s_channel_write(s_tx, silence, sizeof silence, &written, pdMS_TO_TICKS(300)); + xSemaphoreGive(s_mtx); +} + #endif // HAS_TDISPLAY_P4 diff --git a/variants/tdisplay_p4/P4Audio.h b/variants/tdisplay_p4/P4Audio.h index 46d9681..e1d1418 100644 --- a/variants/tdisplay_p4/P4Audio.h +++ b/variants/tdisplay_p4/P4Audio.h @@ -5,6 +5,7 @@ // DOUT=10. Fixed 16 kHz / 16-bit stereo — plenty for notification chimes, tiny buffers. // All calls are safe from any task; init is lazy on first use (~ms, I²C + I2S bring-up). #include +#include // Bring the codec + I2S up (idempotent). Returns false if the codec never answered. bool p4AudioReady(); @@ -12,3 +13,10 @@ bool p4AudioReady(); // Blocking sine tone: `amplitude` is the 16-bit peak (0..~30000; the UI uses pct*130). // No-op (fast) when amplitude <= 0 or the codec is absent. void p4AudioTone(int freq_hz, int duration_ms, int amplitude); + +// Exclusive fixed-rate PCM stream used by media playback. Samples are mono; +// the backend duplicates them to the codec's stereo I2S slots. +uint32_t p4AudioStreamRate(); +bool p4AudioStreamBegin(); +bool p4AudioStreamWrite(const int16_t* samples, size_t frames); +void p4AudioStreamEnd();