From 3778cb339963cf123a474ec8a4489a1e8cbdb29d Mon Sep 17 00:00:00 2001 From: Adolfo Date: Sat, 25 Jul 2026 22:06:43 -0400 Subject: [PATCH] feat(DM): library UI with episode list, SD status, cache, close button, stable single-file DL - DiscoveryMountain: new UI optimized for episode list (not player anymore) - Shows episodes for current season, indicates if on SD card (green=On SD, gray=Not DL) - Per-episode action: download single via SdDownloader or play via Mp3Player - DL Missing button for missing eps in season (currently single first missing, batch reverted) - Season selector screen with cached/total counts - Top-right X close button - Seasons/episodes cached to cache.json to avoid network fetch every launch - Fixes: overlay deletion race (deferred timer), ui_timer leak causing crash on close, season selection overwritten by fetch task - Fixes: unicode ellipsis squares - SdDownloader: revert to stable single-file mode (batch array caused crashes) - Remove batch fields and array allocation, keep single url/path download - Result bundle simple success/path/bytes - Mp3Player: add resume position support (pos_sec from bundle, total_sec estimate, total_decoded_samples), return position on exit via bundle for DM to save Tested on 192.168.68.133: opens, shows S10 2/6, Play/DL buttons, close works, no crash on open/close cycles, cache shows 'Loaded from cache, refreshing...' --- Apps/DiscoveryMountain/CMakeLists.txt | 12 + Apps/DiscoveryMountain/CRASH_ANALYSIS.md | 263 +++ Apps/DiscoveryMountain/CRASH_REPORT.md | 259 +++ Apps/DiscoveryMountain/main/CMakeLists.txt | 8 + .../main/Source/app_context.c | 3 + .../main/Source/app_context.h | 99 + Apps/DiscoveryMountain/main/Source/download.c | 49 + Apps/DiscoveryMountain/main/Source/download.h | 24 + Apps/DiscoveryMountain/main/Source/main.c | 383 ++++ Apps/DiscoveryMountain/main/Source/minimp3.h | 1869 +++++++++++++++++ Apps/DiscoveryMountain/main/Source/network.c | 330 +++ Apps/DiscoveryMountain/main/Source/network.h | 23 + Apps/DiscoveryMountain/main/Source/player.c | 235 +++ Apps/DiscoveryMountain/main/Source/player.h | 23 + Apps/DiscoveryMountain/main/Source/storage.c | 321 +++ Apps/DiscoveryMountain/main/Source/storage.h | 42 + Apps/DiscoveryMountain/main/Source/ui.c | 554 +++++ Apps/DiscoveryMountain/main/Source/ui.h | 28 + Apps/DiscoveryMountain/manifest.properties | 7 + Apps/Mp3Player/main/CMakeLists.txt | 2 +- Apps/Mp3Player/main/Source/main.c | 185 +- Apps/SdDownloader/CMakeLists.txt | 12 + Apps/SdDownloader/main/CMakeLists.txt | 6 + Apps/SdDownloader/main/Source/main.c | 451 ++++ Apps/SdDownloader/manifest.properties | 7 + 25 files changed, 5178 insertions(+), 17 deletions(-) create mode 100644 Apps/DiscoveryMountain/CMakeLists.txt create mode 100644 Apps/DiscoveryMountain/CRASH_ANALYSIS.md create mode 100644 Apps/DiscoveryMountain/CRASH_REPORT.md create mode 100644 Apps/DiscoveryMountain/main/CMakeLists.txt create mode 100644 Apps/DiscoveryMountain/main/Source/app_context.c create mode 100644 Apps/DiscoveryMountain/main/Source/app_context.h create mode 100644 Apps/DiscoveryMountain/main/Source/download.c create mode 100644 Apps/DiscoveryMountain/main/Source/download.h create mode 100644 Apps/DiscoveryMountain/main/Source/main.c create mode 100644 Apps/DiscoveryMountain/main/Source/minimp3.h create mode 100644 Apps/DiscoveryMountain/main/Source/network.c create mode 100644 Apps/DiscoveryMountain/main/Source/network.h create mode 100644 Apps/DiscoveryMountain/main/Source/player.c create mode 100644 Apps/DiscoveryMountain/main/Source/player.h create mode 100644 Apps/DiscoveryMountain/main/Source/storage.c create mode 100644 Apps/DiscoveryMountain/main/Source/storage.h create mode 100644 Apps/DiscoveryMountain/main/Source/ui.c create mode 100644 Apps/DiscoveryMountain/main/Source/ui.h create mode 100644 Apps/DiscoveryMountain/manifest.properties create mode 100644 Apps/SdDownloader/CMakeLists.txt create mode 100644 Apps/SdDownloader/main/CMakeLists.txt create mode 100644 Apps/SdDownloader/main/Source/main.c create mode 100644 Apps/SdDownloader/manifest.properties diff --git a/Apps/DiscoveryMountain/CMakeLists.txt b/Apps/DiscoveryMountain/CMakeLists.txt new file mode 100644 index 0000000..5a28cac --- /dev/null +++ b/Apps/DiscoveryMountain/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.20) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +if (DEFINED ENV{TACTILITY_SDK_PATH}) + set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH}) +else() + set(TACTILITY_SDK_PATH "../../release/TactilitySDK") + message(WARNING "TACTILITY_SDK_PATH not set, defaulting to ${TACTILITY_SDK_PATH}") +endif() +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") +set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH}) +project(DiscoveryMountain) +tactility_project(DiscoveryMountain) diff --git a/Apps/DiscoveryMountain/CRASH_ANALYSIS.md b/Apps/DiscoveryMountain/CRASH_ANALYSIS.md new file mode 100644 index 0000000..806102f --- /dev/null +++ b/Apps/DiscoveryMountain/CRASH_ANALYSIS.md @@ -0,0 +1,263 @@ +# DiscoveryMountain – Crash After Install – Root Cause & Fix + +**Board:** CYD ES3C28P (es3c28p) `192.168.68.132` +**Firmware:** 0.8.0-dev / IDF 5.5.2 +**App ID:** `one.tactility.discoverymountain` +**Branch:** `personal/discovery-mountain-player` → merged into `main` +**Date:** 2026-07-21 + +--- + +## Symptoms + +- After `tactility.py install 192.168.68.132`, device reboots or shows blue dialog `Application failed to start: missing symbol` +- Serial logs: `E ELF: Can't find symbol ...`, then `Guru Meditation` (LoadProhibited, StoreProhibited, IllegalInstruction, Cache error) +- Even when it did start, fetch fell back to 6 dummy episodes (`S01E01`…) with empty `audio_url`, so download always failed +- When forced to download 4.3 MB MP3 from `http://192.168.68.110:8098/.../esp32_24k.mp3`, device hard-faulted during `play_task` + +--- + +## 1. Missing Symbol Loader Failures + +### Detected via `xtensa-esp32s3-elf-nm -D …elf | grep " U "` + +``` +U lv_font_montserrat_14 +U lv_roller_create +U lv_roller_set_options +U lv_roller_get_selected +U lv_roller_set_selected +U qsort +U cJSON_* (via json component) +``` + +**Why:** +- Firmware exports defined in `firmware/Modules/lvgl-module/source/symbols.c` – roller not exported +- `lv_font_montserrat_14` is a data symbol, not exported (AGENTS.md known missing) +- `qsort` not in `g_esp_libc_elfsyms` nor `main_symbols` +- `cJSON` not exported – apps must vendor `cJSON.c` (see `BookPlayer`, `McpScreen`) + +**Fix:** +- `manifest.properties` V1 INI → V2 flat: + ``` + manifest.version=0.2 + target.sdk=0.8.0-dev + target.platforms=esp32s3 + app.id=one.tactility.discoverymountain + app.version.name=0.1.0 + app.version.code=1 + ``` +- `main/CMakeLists.txt`: + ```cmake + set(CJSON_SOURCE "$ENV{IDF_PATH}/components/json/cJSON/cJSON.c") + idf_component_register(SRCS ${SOURCE_FILES} ${CJSON_SOURCE} + INCLUDE_DIRS Source "$ENV{IDF_PATH}/components/json/cJSON" + REQUIRES TactilitySDK esp_http_client lwip) + ``` +- Replace roller with `lv_dropdown` (exported, see `symbols.c:272-291`) +- Replace `qsort` with bubble sort +- Replace `lv_font_montserrat_14` with `lvgl_get_text_font(FONT_SIZE_DEFAULT)` from `tactility/lvgl_fonts.h` + +After fix: `nm -D` shows no missing roller/qsort/font, verify script passes. + +--- + +## 2. Fatal `memset` Bug + +```c +memset(&G,0,0); // size 0! +``` +Static `G` is zero-initialized first boot, but second `onShow` leaves dangling `lv_obj_t*`, `play_handle`, etc. + +**Fix:** `memset(&G,0,sizeof(G))` + +--- + +## 3. Blocking LVGL Thread → Watchdog Reboot + +Original `onShow`: +```c +load_state(); fetch_data(); build_ui(); +``` +`fetch_data()` did 2× `esp_http_client_perform` with 8s timeout + `cJSON_Parse` of 91KB JSON on LVGL thread → 16s block → TWDT. + +**Fix:** +- Build UI immediately with "Fetching..." +- `xTaskCreate(fetch_task_fn,"dm_fetch",16384, ...)` +- `fetch_task_fn` does `fetch_data()` in background, then `tt_lvgl_lock` to update labels +- Same for download: `dl_task` 12288 stack + +--- + +## 4. `esp_http_client` vs Raw Sockets + +- `esp_http_client` failed on device for PB `http://192.168.68.110:8095` (returned chunked `1634\r\n{...}`), while `curl --http1.0` returned plain JSON. + +Raw socket version via `lwip_socket`, `ipaddr_addr`, `my_htons` (like `RobotArm`) is more reliable and allows PSRAM buffers. + +PocketBase: +- `HTTP/1.1` → `Transfer-Encoding: chunked` → `1634\r\n{...}\r\n0\r\n\r\n` +- `HTTP/1.0` → `Content-Length` absent, body until close → 5684 bytes seasons, 91293 bytes episodes + +**Fix:** Implement `http_get_raw` with: +- PSRAM `heap_caps_malloc(65536, MALLOC_CAP_SPIRAM|8BIT)` growing to 600KB +- Manual realloc via malloc+memcpy+free (avoid `heap_caps_realloc` not exported) +- `strstr(buf,"\r\n\r\n")` to find header end +- Detect `chunked`, decode hex chunk sizes via `strtol(hex, NULL, 16)` +- Return de-chunked body + +Result: `DM: seasons items count 37`, `episodes items 232` (was fallback 37/6) + +--- + +## 5. Download Buffer & PSRAM + +- `dl_ep` used `esp_http_client` + `FILE*` with 2KB stack buf, but also `rename` with static `sd_path()` race +- 4.3MB MP3 download needs streaming, not full RAM +- `malloc(600000)` in internal heap (120KB free) → OOM → cache error + +**Fix:** +- `make_sd_path(out,len,slug)` caller-provided buffer +- `ensure_dir()` does NOT `mkdir("/sdcard")` +- `dl_ep_raw` uses PSRAM `hdr 8192` + `recv_buf 4096`, streams directly to `FILE*` +- Progress via `lv_label_set_text` under `tt_lvgl_lock` +- LRU keeps 8 files, skips current playing + +Result: `DL done total 4329701 expected 4329701` – file correctly saved to `/sdcard/dm/` + +--- + +## 6. Audio: I2S Direct vs audio-stream + PSRAM + +Original used `i2s_controller_*` directly: +```c +device_lock(i2s_dev); i2s_controller_set_config(...); i2s_controller_write(...); +``` +Conflicts with Audio service, no resampling, `vTaskDelete` while holding lock → deadlock, `samples*2` overflow for mono. + +**Fix (from BookPlayer):** +- `find_audio_device()` via `device_find_by_name("audio-stream")` +- `audio_stream_open_output(dev, cfg, &handle)` +- `audio_stream_write(handle, pcm+off, ...)` +- `close_stream()` via `audio_stream_close` +- `wait_play_exit()` uses `tt_lvgl_unlock(); vTaskDelay(10); tt_lvgl_lock()` pattern +- Buffers: `inbuf 16384` + `pcm 1152*2*2` from PSRAM +- Volume scaling with proper clipping +- Stack 12288 for play/dl/fetch (was 6144/8192 → overflow on cJSON 91KB) +- Rate-limit `ui_timer` 500ms, only update bar if `pct != last_pct` + +**Playback crash – stack overflow:** +``` +***ERROR*** A stack overflow in task dm_play has been detected. +... +Guru Meditation: BREAK instr at vTaskGenericNotifyGiveFromISR +... +PC : 0x40385c6e : vPortYieldFromInt +``` +Root: `mp3dec_t` (~2KB) + large locals on 8192 stack + `malloc` for buffers + `fread` + `memmove` caused overflow. + +**Fix:** +- Increase `dm_play` stack 8192 → 16384 +- Allocate `mp3dec_t* dec` from PSRAM via `heap_caps_malloc(..., SPIRAM|8BIT)` instead of stack +- Allocate `inbuf` / `pcm` via `malloc` (internal) first, fallback PSRAM +- Add ID3v2 skip: + ```c + uint8_t id3hdr[10]; + if(fread(id3hdr,1,10,file)==10 && memcmp(id3hdr,"ID3",3)==0){ + int sz = (id3hdr[6]&0x7F)<<21 | ...; + fseek(file, sz+10, SEEK_SET); + } + ``` +- Add logs `open_stream sr=%d ch=%d`, `open_stream ok`, `decoding frame buffered=%d` + +Result after fix: +``` +DM: play task start /sdcard/dm/463_...mp3 size 4329701 +DM: open_stream sr=16000 ch=1 +esp32_i2s: Configuring I2S pins... +Adev_Codec: Open codec device OK +DM: open_stream ok +``` +Stays alive 50s+ (previously crashed <1s). + +### 7. UI Progress Not Showing (user report: "It is playing now, the UI does not show the progress, maybe due to the autoplay.") + +Root: +```c +G.pos_sec += samples / info.hz; // integer division 1152/16000 = 0! +``` +`pos_sec` never increments, so `pct = pos_sec*100/total_sec` stays 0, bar never moves. Also `last_pct` not reset on new episode. + +**Fix:** +- Track `total_decoded_samples += samples; G.pos_sec = total_decoded_samples / hz;` +- Reset `last_pct=-1` in `select_ep` and `start_idx_internal` +- Keep `ui_timer` 500ms but always update time label, only bar when pct changes +- Remove auto-play for final stable (user presses Play manually) – auto-play from `fetch_task` caused race with LVGL lock and hid progress issue + +After fix, progress bar moves and time label updates `0:01 / 4:30` etc. +No immediate crash, stays alive 50s+ (previously crashed <1s). + +Remaining TODO: test actual audio output via speaker, try 44.1k file if 16k resampling still issues, add `taskYIELD()` and volume ramp to avoid pop. + +--- + +## Build & Deploy (correct env) + +Per `AGENTS.md`: + +```bash +cd /Users/adolforeyna/Projects/Tactility/apps +unset PYTHONPATH; unset PYTHONHOME +export IDF_PYTHON_ENV_PATH=/Users/adolforeyna/.espressif/python_env/idf5.5_py3.10_env +source /Users/adolforeyna/esp/esp-idf-v5.5.2/export.sh +export TACTILITY_SDK_PATH=/Users/adolforeyna/Projects/Tactility/firmware/release/TactilitySDK + +$IDF_PYTHON_ENV_PATH/bin/python tactility.py Apps/DiscoveryMountain build esp32s3 --local-sdk +curl -X PUT http://192.168.68.132/api/apps/install -F "file=@Apps/DiscoveryMountain/build/DiscoveryMountain.app" +curl -X POST "http://192.168.68.132/api/apps/run?id=one.tactility.discoverymountain" +# serial +python3 -c "import serial; s=serial.Serial('/dev/cu.usbmodem101',115200); ..." +``` + +Verify symbols: +```bash +xtensa-esp32s3-elf-nm -D Apps/DiscoveryMountain/build/cmake-build-esp32s3/DiscoveryMountain.app.elf | grep " U " | grep -E "roller|qsort|montserrat" +# should be empty +``` + +--- + +## Final State (2026-07-21 post-fix) + +- **Stable install:** No `missing symbol`, no `memset` garbage, no watchdog. Boots to launcher, shows UI, stays alive. +- **Fetch:** `37 seasons / 232 episodes` via raw LWIP sockets + PSRAM 200KB buffer, `HTTP/1.0` to avoid chunked. Previously fallback 6 dummy. +- **Download:** `DL done total 4329701 expected 4329701` via simple streaming `lwip_recv` → `FILE*` with PSRAM hdr/recv buffers, LRU keeps 8 files. Previously OOM and crash. +- **Playback:** Fixed stack overflow in `dm_play` (8192 → 16384) + `mp3dec_t` heap allocated + ID3 skip. Now: + ``` + DM: play task start ... size 4329701 + DM: open_stream sr=16000 ch=1 + DM: open_stream ok + ``` + No immediate crash, stays alive 50s+. Audio output via `audio_stream` (ES8311) should work, but needs manual play test (auto-play removed for stability). +- **UI:** Roller → dropdown, font → `lvgl_get_text_font`, no `HideStatusBar` per user request. +- **Build:** Verified `nm -D` clean, `tactility.py build esp32s3 --local-sdk` ok. + +## Verified Logs (final) + +``` +DM: fetch task start +DM: GET .../dm_seasons... → GET ok 5684 +DM: seasons items count 37 +DM: GET .../dm_episodes... → GET ok 91293 +DM: episodes items 232 +DM: fetch done seasons=37 eps=232 +``` + +--- + +## TODO + +- [ ] Fix playback: skip ID3v2 before decode, try internal RAM for `mp3dec_t`, test with 44.1k mp3 from other episode (not `esp32_24k`) +- [ ] Use `device_get_by_name` / `device_put` instead of deprecated `find` +- [ ] Use `tt_app_get_user_data_path` for state file, not hardcoded `/sdcard/apps/...` +- [ ] Rate-limit screenshot API (2s gap) to avoid heap spike diff --git a/Apps/DiscoveryMountain/CRASH_REPORT.md b/Apps/DiscoveryMountain/CRASH_REPORT.md new file mode 100644 index 0000000..36ec981 --- /dev/null +++ b/Apps/DiscoveryMountain/CRASH_REPORT.md @@ -0,0 +1,259 @@ +# Discovery Mountain – Download & Playback Crash Report + +**Date:** 2026-07-25 +**Device:** 192.168.68.133 (es3c35p, /dev/cu.usbmodem1101) +**App ID:** `one.tactility.discoverymountain` (DM) + `one.tactility.sddownloader` (SDDL) + `one.tactility.mp3player` (MP3) +**Firmware:** 0.8.0-dev / IDF 5.5.2 / Tactility SDK 0.8.0-dev + +--- + +## Summary – What is failing: DM or Downloader? + +**Both, same root cause: low internal heap (3–7 KB) + unsafe task handling.** + +- **DM fetch:** 1st launch OK (37/232), 2nd launch crashes with StoreProhibited. +- **Downloader (SDDL):** S01E01 dummy (empty URL) causes gui stack overflow, S01E04 34 MB file causes gui overflow + phy_wakeup crash. + +--- + +## 1. Original Download Crash (from your logs) + +**Log:** `2026-07-24/cu.usbmodem101_21-42-13_s002.log` + +``` +[21:43:39] DM: DL start 396_Wherever_You_Go_S07E01 url ... 36 MB +[21:43:42] dl header done cl=36147383, wrote initial 1143, streaming +[21:43:57] W DM: dl timeout 1/12 total=21303/36147383 +[21:43:51] wifi:bcn_timeout,ap_probe_send_start +[21:44:05] Guru Meditation Error: Core 0 panic'ed (StoreProhibited) PC 0x4038fc5d EXCVADDR 0x05 +Backtrace: 0x4038fc5a 0x400398b5 0x4038bd9d 0x4038d7c9 0x421fdab6 +[21:44:05] WebServerService: GET /api/sysinfo +``` + +- Download stalls at 21 KB / 36 MB, 12×15 s timeouts = 3 min blocked in `lwip_recv` +- `onHide` did `vTaskDelay(100)` then `vTaskDelete(dl_handle)` while still blocked in recv → fd/socket/PSRAM leak → next WebServer request crashes. +- Also `sdmmc_read_sectors: not enough mem 0x101` seen in many logs – DMA pool 32 KB exhausted. + +**Fix applied:** +- Timeout 15 s → 5 s, max 12 → 5 (25 s max stall) +- `onHide` graceful wait 2 s fetch / 3 s DL, only force delete after, no leak +- `recv_buf`/`hdr` 8192 → 4096 PSRAM, `fflush` every 8 chunks, `vTaskDelay(10)` to let SDMMC recover +- `sin_len = sizeof(sa)`, proper `my_htons` masking + +--- + +## 2. S01E01 – Empty URL + GUI Stack Overflow + +**Log:** `cu.usbmodem1101_20-33-06_s008.log` + +``` +[20:33:14] DM: connect fail 192.168.68.110:8095 → seasons fallback, episodes fallback → done 37/6 +[20:33:17] DM: Starting external SdDownloader for S01E01 -> /sdcard/dm/S01E01.mp3 +[20:33:17] SDDL: Should autostart has_url=1 has_path=1 url= path=/sdcard/dm/S01E01.mp3 +[20:33:17] xTaskCreate download_task +***ERROR*** A stack overflow in task gui has been detected. +Backtrace: 0x40385b29:0x3fcdb9d0 ... +``` + +- When PB fetch fails (connect fail), DM falls back to 6 dummy episodes `S01E01`…`S01E06` with **empty `audio_url`**. +- User presses S01E01 → DM launches SdDownloader with empty URL → `download_one_file` checks `strlen(url)<8` → `Invalid URL` → calls `update_ui_status` + `set_result_and_close(false)` which did `vTaskDelay(1500)` + `tt_app_stop()` **from download_task** → deadlock: onHide waits for download_task, download_task waits in delay, gui task overflows. +- Also `S01E01.mp3` path exists? No, so it tries download. + +**Fix:** +- `try_external_downloader` now checks `strlen(audio_url)<8` → shows "No URL – fetch failed?" and returns false, no launch +- `btn_dl_season_cb` filters `strlen>=8` +- `SdDownloader` `set_result_and_close` no longer calls `tt_app_stop()` from download_task (just sets result, leaves app to be closed via Cancel button) → no deadlock +- `SdDownloader` download_task stack 16384 → 8192 to save heap + +**User note:** "That episode should be on sd card already" – real S01E01 is `463_Discovering_Discovery_Mountain_S01E01.mp3` (4.3 MB), not dummy `S01E01.mp3`. Real file exists, dummy does not. + +--- + +## 3. S01E04 – 34 MB File, GUI Stack Overflow on DL Start + +**Log:** `cu.usbmodem1101_20-26-11_s006.log` + +``` +[20:28:43] DM: Starting external SdDownloader for 460_The_Lookout...S01E04 -> /sdcard/dm/460_...mp3 +[20:28:43] SDDL: Should autostart url=http://.../460_...mp3 path=/sdcard/dm/460_...mp3 +[20:28:43] xTaskCreate download_task res=1 handle=0x3fcc3f08 + +***ERROR*** A stack overflow in task gui +``` + +- S01E04 is 34 MB (`file_size 34460594`). Download task created, but gui task overflows immediately, before any DL log. +- Root: `lru_check` had `FE files[200]` → 200*304=60 KB on stack of download_task (12 KB stack) → overflow. Also `sort_eps` had `EpInfo t` 500 bytes on stack inside double loop. +- `ensure_parent_dir` had 512+512 stack, `req[512]`, etc. + +**Fix:** +- `lru_check`: `files` allocated from PSRAM via `heap_caps_malloc(200*sizeof(FE))` +- `sort_eps`: temp `EpInfo t` allocated from PSRAM +- `ensure_parent_dir`: rewritten without `strtok_r` (not exported) and without large stack, using manual parsing and `strncat` +- Download task stack 16K → 8K, fetch task stays 12K, heap check before socket. + +--- + +## 4. DM Second Launch – StoreProhibited 0x8208775e + +**Log:** `..._17-37-00_s015.log` (latest after clean build) + +``` +[20:54:50] fetch done seasons=37 eps=232 +[20:55:39] onShow heap before fetch: internal=50455 psram=4567656 +[20:55:39] xTaskCreate fetch_task res=1 handle=0x3fcc5110 +[20:55:39] fetch task start → seasons 723 ok +[20:55:39] GET ...page=1 → 9875 ok → got 50 +[20:55:39] Guru StoreProhibited PC 0x4238a8c0 EXCVADDR 0x8208775e +Backtrace: 0x4238a8bd(app) 0x420874f3(lv_timer.c:107) 0x42069754(esp_lvgl_port.c:242) 0x421fdaba(port.c:139) +``` + +- 1st fetch after boot OK, 2nd fetch (app reopened) crashes 11 ms after first page GET, before header_end. +- Backtrace shows crash in **LVGL timer task**, not fetch task: `lv_timer.c:107` → `ui_timer` +- `ui_timer` (200 ms) was doing heavy work: `save_current_ep_pos()` + `save_state()` (cJSON create, print, fopen, fwrite) every 5 sec when playing. Called from GUI task with limited stack → StoreProhibited when trying to write to flash 0x82... (string literal in flash?). +- Also `G` global is 240 KB (400*492) BSS in DRAM, plus `saved_pos` 40 KB = 280 KB, leaving free heap 50 KB → after first fetch cJSON 10 KB nodes, heap fragments to 3-7 KB → `MemoryChecker: Internal memory low: 3607/7279/7435` + +**Fix:** +- `ui_timer`: early return if `G.fetching`, remove periodic `save_state()` (saving is done explicitly in `select_ep`, `go_next`, `onHide`, `onResult`) +- `AppCtx.MAX_EPS` 400→250, `title` 128→64, `audio_url` 256→128, `MP3_BUF` 16384→4096 → size 240 KB → ~100 KB +- `fetch_task` stack 16384→12288, `onShow` heap log, `xTaskCreate` result log +- Pagination 100/page → 50/page (now 25/page) with 300-400 ms delay between pages to let heap recover, `fields=season_num` and `fields=title,slug,season_num,episode_num,audio_url` to cut JSON 91 KB → 42 KB → 9 KB/page + +--- + +## 5. DL Season Button – IntegerDivideByZero in Bundle + +**Log:** `cu.usbmodem1101_16-57-59_s014.log` + +``` +[17:34:06] episodes page 1 got 50 items +[17:34:06] Guru IntegerDivideByZero PC 0x42014cf3 +Backtrace: 0x42014cf0(hashtable_policy.h:534) 0x42014dac(unordered_map.h:988) 0x42014345(tt_bundle.cpp:46 putInt32) 0x4237a4d3(app) +``` + +- Pressing **DL Season** during fetch → `G.ep_cnt` only 50 (partial), `G.fetching` true, but button didn't check fetching → `btn_dl_season_cb` called `tt_bundle_alloc` + 20× `putString` (url+path) while internal heap low from cJSON → `unordered_map` bucket_count=0 → `% bucket_count` → divide by zero. + +**Fix:** +- `btn_seasons_cb` and `btn_dl_season_cb` now check `if(G.fetching) return;` +- `btn_dl_season_cb`: `missing_idx[50]` → `[10]` to avoid LVGL stack overflow, heap check `>15 KB` before alloc, and per-put heap check `>8 KB` else abort with "Low memory, try 1 by 1" +- Also filters `strlen(audio_url)>=8` + +--- + +## 6. Play Button – Now Opens Mp3Player, Returns Timestamp + +**Previous:** DM had internal `play_task` using `audio_stream` directly. User reported "pres play nothing happened" because audio-stream device not found when launched from DM (DM still held it). + +**Fix in `player.c`:** +```c +stop_playback_sync(); wait_play_exit(); close_overlay(); +BundleHandle b = tt_bundle_alloc(); +putString("file", fpath); putInt32("pos_sec", saved); +tt_app_start_with_bundle("one.tactility.mp3player", b); +``` +- `onResult` in DM now handles Mp3Player return: + - `optInt32("pos_sec"/"timestamp")` → `set_saved_pos_for_slug` + - Updates UI `S01 E02 * • 0:07`, `Resume – tap Play` + - No autoplay after DL or after Mp3Player (per request) + +**Mp3Player (`one.tactility.mp3player`) fixes:** +- Retry finding audio device 5×200 ms (was failing when launched from DM) +- Tracks `pos_sec = total_decoded_samples / hz`, `total_sec` estimate +- `onHide` returns bundle `pos_sec`, `timestamp`, `file`, `finished`, `success` +- `onHide` now closes audio stream **before** alloc result to free heap, frees buffers before bundle, only puts 2 ints to avoid OOM, no SD file write in low heap + +**Tested on 133:** +``` +[20:03:30] DM: Launching Mp3Player for /sdcard/dm/463_...mp3 pos 4 +[20:03:31] Mp3Player: Found audio-stream device 0x3fca480c +[20:03:31] Starting MP3 playback size 4329701 resume=4 +[20:03:31] Audio stream opened 16000 Hz, 1 ch +[20:03:56] onHide start pos_sec=15 +[20:03:59] Set result bundle pos_sec=15 +[20:03:59] onHide returning pos_sec=15 total_sec=269 +[20:03:59] DM: onResult result=1 pending_idx=0 cur_idx=0 → saved pos 15 +``` + +--- + +## 7. SdDownloader – Now Batch + Headless (per your latest spec) + +**Manifest:** `one.tactility.sddownloader`, 30 KB, 0 missing symbols + +**UI:** Only bar, `% (3/10)`, detail `x / y KB`, URL/path labels read-only, status, Cancel. No input fields. + +**Args via bundle (no manual input):** +- Single: `url`, `path`, `override` bool (default false → fail if exists) +- Batch (full season): `count` int, `url_0`/`path_0` … `url_n`/`path_n`, `override` bool +- Example from DM `btn_dl_season_cb`: +```c +bundle.putInt32("count", 7); +putString("url_0", "http://.../S02E01.mp3"); putString("path_0", "/sdcard/dm/..."); +... +tt_app_start_with_bundle("one.tactility.sddownloader", b); +``` + +**Returns bool:** +```c +bundle.putBool("success", true/false); +putInt32("success_count"), "fail_count", "total_count", "bytes", "path", "error" +setResult(OK/ERROR, bundle) +``` +DM `onResult` shows `DL: X ok, Y fail` and refreshes `seen` flags. + +**Robustness:** +- 8 KB stack (was 16 KB), 4 KB PSRAM buf, 15 s timeout, 5 retries +- `ensure_parent_dir` mkdir -p without `strtok` (not exported) +- `stat` check for exists → fail if not override +- No `tt_app_stop()` from download task (caused deadlock + gui stack overflow for S01E01 empty URL) + +--- + +## Current Builds on 133 + +- `DiscoveryMountain.app` 80 KB (was 70 KB) – entry 0x4237543c, 118 undef, 0 missing – 37/232 fetch, seasons dropdown works, no autoplay, DL Season 10 max, external Mp3Player/SdDownloader, no fullscreen covering (root now child of parent, overlays closed before launch) +- `SdDownloader.app` 30 KB – 74 undef, 0 missing – batch 10, file-exists check +- `Mp3Player.app` 40 KB – 0 missing after adding `esp_http_client` to REQUIRES – returns timestamp + +--- + +## Still Open / Next + +- Second fetch after app restart still StoreProhibited at 0x4238a8c0 (PC in app) during first page GET – likely `cJSON_Parse` + `has_slug` stat + low heap after first fetch. Mitigation: reduce `MAX_EPS` further or move `G` to PSRAM with `EXT_RAM_ATTR` (tried, caused other crash), or free `G.eps` between fetches. +- S01E01 dummy fallback (empty URL) now shows "No URL – fetch failed?" instead of crash, but ideally should never appear if fetch succeeds. Ensure PB at `192.168.68.110:8095` is reachable (seen `connect fail` when WiFi bcn_timeout). +- Mp3Player close takes 2.5 s (force delete after 50 tries) – should be optimized to not need force delete. + +--- + +## Reproduction Steps for S01E04 Crash (you reported) + +1. Clean fetch fails → fallback 37/6 dummy episodes `S01E01`…`S01E06` with empty URL +2. Press S01E01 → DM tries external DL with empty URL → SDDL `url=` empty → `Invalid URL` → `set_result_and_close` did `tt_app_stop()` from download task → deadlock → gui stack overflow + +Fixed by: empty URL check in `try_external_downloader` + `btn_dl_season_cb` + removing `tt_app_stop()` from download task. + +For real S01E04 (34 MB, `460_...`), previous crash was gui stack overflow right after `xTaskCreate` – fixed by reducing download_task stack 16K→8K and moving `files[200]` 60KB array from stack to PSRAM in `lru_check`. + +--- + +## Files Changed + +- `Apps/DiscoveryMountain/main/Source/app_context.c` – added `pending_dl_idx`, `root` +- `app_context.h` – reduced `MAX_EPS` 400→250, `MP3_BUF` 16K→4K, `title` 128→64, `audio_url` 256→128, added `root`, `pending_dl_idx`, batch fields in SDDL +- `network.c` – 50/page pagination (was 400), fields filter, 8K internal buf, detailed logs for connect +- `storage.c` – `sort_eps` temp from PSRAM, `lru_check` files from PSRAM (was 60KB stack) +- `download.c` – stubbed to only overlays (internal DL removed) +- `player.c` – simplified to only external launchers, no internal `play_task`, added `download_season()` +- `ui.c` – `build_ui(parent)` uses parent, not `scr_act()`, root stored, 2 buttons 48% (Seasons + DL Season), `ui_timer` early return if fetching, `btn_dl_season_cb` with heap checks +- `main.c` – onShow heap log, xTaskCreate 12288, onResult handles both Mp3Player timestamp and SdDownloader batch, onHide does NOT clear `pending_dl_idx`, deletes root +- `SdDownloader/main/Source/main.c` – new app, batch support, file-exists + override logic, returns bool +- `Mp3Player/main/Source/main.c` – returns `pos_sec`/`timestamp`, retry audio device, auto-finds test file if no bundle + +--- + +## How to Test Full Season Download + +1. Open DM → wait `fetch done 37/232` +2. Tap `Seasons` → S01 → tap `DL Season` +3. Should show `Starting DL 6 eps S01` → launches SDDL → bar shows `3/6: 45%` etc. +4. On finish, DM shows `DL: 6 ok, 0 fail` and refreshes `*` markers +5. Tap episode with `*` → Play → launches Mp3Player → plays from saved pos → close → DM shows `Resume • 0:12` diff --git a/Apps/DiscoveryMountain/main/CMakeLists.txt b/Apps/DiscoveryMountain/main/CMakeLists.txt new file mode 100644 index 0000000..644d376 --- /dev/null +++ b/Apps/DiscoveryMountain/main/CMakeLists.txt @@ -0,0 +1,8 @@ +file(GLOB_RECURSE SOURCE_FILES Source/*.c) +set(CJSON_SOURCE "$ENV{IDF_PATH}/components/json/cJSON/cJSON.c") + +idf_component_register( + SRCS ${SOURCE_FILES} ${CJSON_SOURCE} + INCLUDE_DIRS Source "$ENV{IDF_PATH}/components/json/cJSON" + REQUIRES TactilitySDK esp_http_client lwip +) diff --git a/Apps/DiscoveryMountain/main/Source/app_context.c b/Apps/DiscoveryMountain/main/Source/app_context.c new file mode 100644 index 0000000..5fdadde --- /dev/null +++ b/Apps/DiscoveryMountain/main/Source/app_context.c @@ -0,0 +1,3 @@ +#include "app_context.h" + +AppCtx G = {0}; diff --git a/Apps/DiscoveryMountain/main/Source/app_context.h b/Apps/DiscoveryMountain/main/Source/app_context.h new file mode 100644 index 0000000..2e5b5b2 --- /dev/null +++ b/Apps/DiscoveryMountain/main/Source/app_context.h @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define TAG "DM" +#define PB "http://192.168.68.110:8095" +#define DM_DIR "/sdcard/dm" +#define STATE_PATH "/sdcard/apps/one.tactility.discoverymountain/userdata/state.json" +#define CACHE_PATH "/sdcard/apps/one.tactility.discoverymountain/userdata/cache.json" +#define MAX_SEASONS 40 +#define MAX_EPS 250 +#define MP3_BUF 4096 + +typedef struct { int num; } SeasonInfo; +typedef struct { + char title[64]; + char slug[96]; + int season; + int ep_num; + char audio_url[128]; + bool seen; +} EpInfo; + +typedef struct { char slug[96]; int pos; } SavedPos; + +typedef struct { + SeasonInfo seasons[MAX_SEASONS]; + int season_cnt; + EpInfo eps[MAX_EPS]; + int ep_cnt; + int cur_season; + int cur_ep_idx; + struct Device* stream_dev; + AudioStreamHandle stream_handle; + bool is_playing; + bool is_paused; + bool need_stop; + TaskHandle_t play_handle; + TaskHandle_t fetch_handle; + TaskHandle_t dl_handle; + int pos_sec; + int total_sec; + int volume; + int last_pct; + lv_obj_t *root; + lv_obj_t *lbl_title, *lbl_meta, *bar, *lbl_time, *lbl_status, *btn_play; + lv_obj_t *lbl_season; + lv_obj_t *ep_list_cont; + lv_obj_t *btn_seasons; + lv_obj_t *btn_dl_missing; + lv_obj_t *overlay, *dropdown; + lv_obj_t *season_list_cont; + lv_timer_t *ui_timer_handle; + // Download UI + lv_obj_t *dl_overlay; + lv_obj_t *dl_bar; + lv_obj_t *dl_lbl_pct; + lv_obj_t *dl_lbl_detail; + lv_obj_t *dl_lbl_title; + lv_obj_t *dl_lbl_sub; + char cur_title[128]; + char last_slug[96]; + bool fetching; + bool downloading; + volatile int dl_total; + int dl_expected; + int dl_last_ui_update; + volatile bool dl_cancel_req; + volatile bool fetch_cancel_req; + char dl_ep_title[128]; + char dl_slug[96]; + int dl_season; + int dl_ep_num; + int dl_last_pct; + // Per-episode resume + SavedPos saved_pos[MAX_EPS]; + int saved_pos_cnt; + int last_save_tick; + + // External downloader integration (SdDownloader) + int pending_dl_idx; +} AppCtx; + +extern AppCtx G; + +#ifdef __cplusplus +} +#endif diff --git a/Apps/DiscoveryMountain/main/Source/download.c b/Apps/DiscoveryMountain/main/Source/download.c new file mode 100644 index 0000000..6b42297 --- /dev/null +++ b/Apps/DiscoveryMountain/main/Source/download.c @@ -0,0 +1,49 @@ +#include "download.h" +#include "storage.h" +#include +#include +#include "esp_log.h" + +// Simplified: internal download removed, we use external SdDownloader app +// Keep overlay functions as stubs for compatibility, but they do minimal + +void hide_dl_overlay(){ + if(G.dl_overlay){ + lv_obj_delete(G.dl_overlay); + G.dl_overlay=NULL; + G.dl_bar=NULL; + G.dl_lbl_pct=NULL; + G.dl_lbl_detail=NULL; + G.dl_lbl_title=NULL; + G.dl_lbl_sub=NULL; + } + G.downloading=false; +} + +void show_dl_overlay_locked(const char* ep_title, const char* slug, int season, int ep_num){ + // No longer used - external downloader has its own UI + // Keep as stub that shows simple status + (void)ep_title; (void)slug; (void)season; (void)ep_num; + G.downloading=true; +} + +void update_dl_overlay_ui(){ + // No internal download progress anymore +} + +bool dl_ep_raw(EpInfo* ep){ + (void)ep; + return false; +} + +bool dl_ep(EpInfo* ep){ + (void)ep; + return false; +} + +void dl_task(void* arg){ + (void)arg; + G.downloading=false; + G.dl_handle=NULL; + vTaskDelete(NULL); +} diff --git a/Apps/DiscoveryMountain/main/Source/download.h b/Apps/DiscoveryMountain/main/Source/download.h new file mode 100644 index 0000000..be44218 --- /dev/null +++ b/Apps/DiscoveryMountain/main/Source/download.h @@ -0,0 +1,24 @@ +#pragma once + +#include "app_context.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Raw download to file with progress tracking +bool dl_ep_raw(EpInfo* ep); +bool dl_ep(EpInfo* ep); + +// Download task +void dl_task(void* arg); + +// Download overlay UI +void hide_dl_overlay(void); +void show_dl_overlay_locked(const char* ep_title, const char* slug, int season, int ep_num); +void update_dl_overlay_ui(void); + +#ifdef __cplusplus +} +#endif diff --git a/Apps/DiscoveryMountain/main/Source/main.c b/Apps/DiscoveryMountain/main/Source/main.c new file mode 100644 index 0000000..4951480 --- /dev/null +++ b/Apps/DiscoveryMountain/main/Source/main.c @@ -0,0 +1,383 @@ +#include +#include +#include +#include "app_context.h" +#include "storage.h" +#include "network.h" +#include "download.h" +#include "player.h" +#include "ui.h" + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "esp_heap_caps.h" + +static void fetch_task_fn(void* arg){ + (void)arg; + G.fetching=true; + G.fetch_cancel_req=false; + ESP_LOGI(TAG,"fetch task start"); + + // Try load cached seasons/episodes – instant offline support + bool cache_loaded = load_cache(); + if(cache_loaded){ + ESP_LOGI(TAG,"Cache loaded: %d seasons, %d eps", G.season_cnt, G.ep_cnt); + // Restore cur idx from last_slug + if(strlen(G.last_slug)>0){ + for(int i=0;i0){ + s1=0; + G.cur_season=G.eps[0].season; + } + G.cur_ep_idx=s1; + } + // Show cached data immediately + G.fetching=false; + ui_rebuild_episode_list(); + tt_lvgl_lock(portMAX_DELAY); + if(G.lbl_status) lv_label_set_text(G.lbl_status,"Loaded from cache, refreshing..."); + tt_lvgl_unlock(); + G.fetching=true; + vTaskDelay(pdMS_TO_TICKS(200)); + } else { + ESP_LOGI(TAG,"No cache found, will fetch from network"); + } + + // Network fetch – updates if possible, otherwise keeps cache + bool fetch_ok = fetch_data(); + + if(G.fetch_cancel_req){ + ESP_LOGI(TAG,"fetch task canceled, exiting"); + G.fetching=false; + G.fetch_handle=NULL; + vTaskDelete(NULL); + return; + } + + if(fetch_ok){ + ESP_LOGI(TAG,"Network fetch ok: %d seasons, %d eps – saving cache", G.season_cnt, G.ep_cnt); + save_cache(); + } else { + if(cache_loaded){ + ESP_LOGW(TAG,"Network fetch failed, restoring cache"); + // fetch_data overwrote with fallback, restore cache + load_cache(); + } else { + ESP_LOGW(TAG,"Network fetch failed and no cache – using fallback"); + } + } + + ESP_LOGI(TAG,"fetch done seasons=%d eps=%d cache=%d fetch_ok=%d",G.season_cnt,G.ep_cnt,cache_loaded,fetch_ok); + vTaskDelay(pdMS_TO_TICKS(300)); + + // Resolve current episode index – preserve user season selection if cache was already shown + if(cache_loaded){ + // User may have changed season while network fetch was in progress – keep their selection if it still exists + int cur_s = G.cur_season; + bool season_exists=false; + for(int i=0;i0){ + for(int i=0;i0){ + for(int i=0;i0){ + G.cur_ep_idx=0; + G.cur_season=G.eps[0].season; + } + } + } else { + // First launch – use last_slug to restore position + if(strlen(G.last_slug)>0){ + for(int i=0;i0){ + s1=0; + G.cur_season=G.eps[0].season; + } + G.cur_ep_idx=s1; + } + } + + G.fetching=false; + + tt_lvgl_lock(portMAX_DELAY); + if(G.fetch_cancel_req){ + tt_lvgl_unlock(); + G.fetch_handle=NULL; + vTaskDelete(NULL); + return; + } + tt_lvgl_unlock(); + + ui_rebuild_episode_list(); + + if(G.ep_cnt>0 && G.cur_ep_idx>=0){ + EpInfo* ep=&G.eps[G.cur_ep_idx]; + int saved = get_saved_pos_for_slug(ep->slug); + if(saved==0) saved = G.pos_sec; + if(saved>0) G.pos_sec=saved; + } + + tt_lvgl_lock(portMAX_DELAY); + if(G.lbl_status){ + if(G.ep_cnt>0){ + if(fetch_ok) lv_label_set_text(G.lbl_status, "Select episode to play / download"); + else if(cache_loaded) lv_label_set_text(G.lbl_status, "Offline, using cached data"); + else lv_label_set_text(G.lbl_status,"No network – limited data"); + } else { + lv_label_set_text(G.lbl_status,"No data – check network"); + } + } + tt_lvgl_unlock(); + + G.fetch_handle=NULL; + save_state(); + vTaskDelete(NULL); +} + +static void onShow(AppHandle app, void* data, lv_obj_t* parent){ + (void)app; (void)data; (void)parent; + memset(&G,0,sizeof(G)); + G.pos_sec=0; + G.total_sec=0; + G.cur_season=1; + G.cur_ep_idx=-1; + G.pending_dl_idx=-1; + G.volume=80; + G.last_pct=-1; + G.dl_last_pct=-1; + G.dl_expected=-1; + G.fetch_cancel_req=false; + G.dl_cancel_req=false; + ensure_dir(); + load_state(); + build_ui(parent); + size_t free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); + size_t free_psram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); + ESP_LOGI(TAG, "onShow heap before fetch task: internal=%d psram=%d", free_internal, free_psram); + BaseType_t res = xTaskCreate(fetch_task_fn,"dm_fetch",12288,NULL,5,&G.fetch_handle); + ESP_LOGI(TAG, "xTaskCreate fetch_task res=%d handle=%p", res, G.fetch_handle); + if (res != pdPASS) { + tt_lvgl_lock(portMAX_DELAY); + if (G.lbl_status) lv_label_set_text(G.lbl_status, "Failed to create fetch task"); + tt_lvgl_unlock(); + } +} + +static void onResult(AppHandle app, void* data, AppLaunchId launchId, AppResult result, BundleHandle resultData){ + (void)app; (void)data; (void)launchId; + ESP_LOGI(TAG, "onResult: result=%d pending_idx=%d cur_idx=%d", result, G.pending_dl_idx, G.cur_ep_idx); + + // Check if result contains timestamp from Mp3Player + if (resultData) { + int32_t pos = 0; + bool has_pos = false; + if (tt_bundle_opt_int32(resultData, "pos_sec", &pos) || + tt_bundle_opt_int32(resultData, "timestamp", &pos) || + tt_bundle_opt_int32(resultData, "position", &pos)) { + has_pos = true; + } + + char file_path[512]={0}; + bool has_file = tt_bundle_opt_string(resultData, "file", file_path, sizeof(file_path)) || + tt_bundle_opt_string(resultData, "path", file_path, sizeof(file_path)); + + if (has_pos) { + ESP_LOGI(TAG, "Mp3Player returned pos %d sec for file %s", pos, has_file?file_path:"unknown"); + int target_idx = -1; + if (has_file && strlen(file_path)>0) { + for(int i=0;i=0 && G.pending_dl_idx < G.ep_cnt) target_idx = G.pending_dl_idx; + if (target_idx<0 && G.cur_ep_idx>=0) target_idx = G.cur_ep_idx; + + if (target_idx>=0) { + bool finished = false; + tt_bundle_opt_bool(resultData, "finished", &finished); + int32_t total_sec = 0; + tt_bundle_opt_int32(resultData, "total_sec", &total_sec); + if (finished || (total_sec>0 && pos >= total_sec-10)) { + set_saved_pos_for_slug(G.eps[target_idx].slug, 0); + G.pos_sec = 0; + ESP_LOGI(TAG, "Episode %s finished, clearing pos", G.eps[target_idx].slug); + } else { + set_saved_pos_for_slug(G.eps[target_idx].slug, pos); + G.pos_sec = pos; + ESP_LOGI(TAG, "Saved pos %d for %s", pos, G.eps[target_idx].slug); + } + save_state(); + ui_rebuild_episode_list(); + tt_lvgl_lock(portMAX_DELAY); + if (G.lbl_status) { + if (pos>5) lv_label_set_text(G.lbl_status,"Progress saved – tap episode to resume"); + else lv_label_set_text(G.lbl_status,"Select episode to play / download"); + } + tt_lvgl_unlock(); + } + G.pending_dl_idx = -1; + return; + } + } + + // SdDownloader batch result handling + if (G.pending_dl_idx <0 || G.pending_dl_idx >= G.ep_cnt) { + if (resultData) { + int32_t succ=0, fail=0, total=0; + if (tt_bundle_opt_int32(resultData, "success_count", &succ) || + tt_bundle_opt_int32(resultData, "total_count", &total)) { + tt_bundle_opt_int32(resultData, "fail_count", &fail); + ESP_LOGI(TAG, "Batch DL result: success %ld fail %ld total %ld", (long)succ, (long)fail, (long)total); + for(int i=0;i=0){ + ESP_LOGI(TAG, "Single DL success idx %d", idx); + } else { + ESP_LOGW(TAG, "External DL failed for idx %d", idx); + } +} + +static void onHide(AppHandle app, void* data){ + (void)app; (void)data; + save_current_ep_pos(); + G.fetch_cancel_req=true; + G.dl_cancel_req=true; + + if(G.fetch_handle){ + ESP_LOGI(TAG,"onHide: waiting fetch to exit"); + tt_lvgl_lock(portMAX_DELAY); + for(int i=0;i<20 && G.fetch_handle!=NULL; i++){ + tt_lvgl_unlock(); + vTaskDelay(pdMS_TO_TICKS(100)); + tt_lvgl_lock(portMAX_DELAY); + } + if(G.fetch_handle){ + ESP_LOGW(TAG,"onHide: force deleting fetch task (leak risk)"); + vTaskDelete(G.fetch_handle); + G.fetch_handle=NULL; + } + G.fetching=false; + tt_lvgl_unlock(); + } + + if(G.dl_handle){ + ESP_LOGI(TAG,"onHide: waiting dl to exit"); + tt_lvgl_lock(portMAX_DELAY); + for(int i=0;i<30 && G.dl_handle!=NULL; i++){ + tt_lvgl_unlock(); + vTaskDelay(pdMS_TO_TICKS(100)); + tt_lvgl_lock(portMAX_DELAY); + } + if(G.dl_handle){ + ESP_LOGW(TAG,"onHide: force deleting dl task after timeout"); + vTaskDelete(G.dl_handle); + G.dl_handle=NULL; + G.downloading=false; + if(G.dl_overlay) hide_dl_overlay(); + } + tt_lvgl_unlock(); + } + + tt_lvgl_lock(portMAX_DELAY); + wait_play_exit(); + ui_deinit(); + close_overlay(); + if(G.dl_overlay) hide_dl_overlay(); + if(G.root){ + lv_obj_delete(G.root); + G.root = NULL; + G.lbl_title = NULL; + G.lbl_meta = NULL; + G.lbl_season = NULL; + G.ep_list_cont = NULL; + G.lbl_time = NULL; + G.lbl_status = NULL; + G.btn_play = NULL; + G.btn_seasons = NULL; + G.btn_dl_missing = NULL; + G.season_list_cont = NULL; + G.bar = NULL; + } + tt_lvgl_unlock(); + save_state(); +} + +int main(int argc, char* argv[]){ + (void)argc; (void)argv; + tt_app_register((AppRegistration){ .onShow=onShow, .onHide=onHide, .onResult=onResult }); + return 0; +} diff --git a/Apps/DiscoveryMountain/main/Source/minimp3.h b/Apps/DiscoveryMountain/main/Source/minimp3.h new file mode 100644 index 0000000..9002b68 --- /dev/null +++ b/Apps/DiscoveryMountain/main/Source/minimp3.h @@ -0,0 +1,1869 @@ +#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]; + float scratch_padding[4100]; +} 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) +#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]; + _Static_assert(sizeof(mp3dec_scratch_t) <= sizeof(dec->scratch_padding), "scratch_padding is too small"); +#define scratch (*(mp3dec_scratch_t*)dec->scratch_padding) + + 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 */ + } + int result = success*hdr_frame_samples(dec->header); +#undef scratch + return result; +} + +#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/Apps/DiscoveryMountain/main/Source/network.c b/Apps/DiscoveryMountain/main/Source/network.c new file mode 100644 index 0000000..ade8bec --- /dev/null +++ b/Apps/DiscoveryMountain/main/Source/network.c @@ -0,0 +1,330 @@ +#include "network.h" +#include "storage.h" +#include "cJSON.h" +#include +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "esp_heap_caps.h" +#include + +uint16_t my_htons(uint16_t v){ + // Proper byte swap with masking to avoid overflow + return (uint16_t)(((v & 0x00FF) << 8) | ((v & 0xFF00) >> 8)); +} + +bool resolve_host(const char* host, uint32_t* out_ip){ + if(!host || !out_ip) return false; + uint32_t ip = ipaddr_addr(host); + if(ip!=0 && ip!=0xFFFFFFFF){ + *out_ip=ip; + return true; + } + ESP_LOGE(TAG,"resolve fail (DNS not supported) %s",host); + return false; +} + +void normalize_audio_url(const char* in, char* out, size_t out_len){ + if(!in || !out) return; + if(strncmp(in,"http://",7)==0){ + strncpy(out,in,out_len-1); + out[out_len-1]=0; + return; + } + if(in[0]=='/'){ + snprintf(out,out_len,"%s%s",PB,in); + return; + } + if(strstr(in,"://")==NULL){ + if(in[0]=='/') snprintf(out,out_len,"%s%s",PB,in); + else snprintf(out,out_len,"%s/%s",PB,in); + return; + } + strncpy(out,in,out_len-1); + out[out_len-1]=0; +} + +int http_get_raw(const char* url, char** out_body){ + if(!url || strncmp(url,"http://",7)!=0){ ESP_LOGE(TAG,"http_get url not http %s",url); return -1; } + const char* p = url+7; + const char* slash = strchr(p,'/'); + if(!slash){ ESP_LOGE(TAG,"no slash %s",url); return -1; } + char hostport[128]={0}; + size_t hlen=slash-p; + if(hlen>=sizeof(hostport)) return -1; + memcpy(hostport,p,hlen); + hostport[hlen]=0; + const char* path=slash; + char host[96]={0}; + int port=80; + char* colon=strchr(hostport,':'); + if(colon){ *colon=0; strncpy(host,hostport,sizeof(host)-1); port=atoi(colon+1); if(port<=0) port=80; } else { strncpy(host,hostport,sizeof(host)-1); } + ESP_LOGI(TAG,"GET %s host=%s port=%d",url,host,port); + uint32_t ip=0; + ESP_LOGI(TAG,"Resolving host %s",host); + if(!resolve_host(host,&ip)){ ESP_LOGE(TAG,"ip fail %s",host); return -1; } + ESP_LOGI(TAG,"Resolved %s -> ip 0x%08x", host, ip); + int fd=lwip_socket(AF_INET,SOCK_STREAM,0); + ESP_LOGI(TAG,"Socket fd=%d", fd); + if(fd<0){ ESP_LOGE(TAG,"socket fail"); return -1; } + struct sockaddr_in sa; + ESP_LOGI(TAG,"Memset sa"); + memset(&sa,0,sizeof(sa)); + ESP_LOGI(TAG,"Set sa len/family/port/addr"); + sa.sin_len = sizeof(sa); + sa.sin_family=AF_INET; + sa.sin_port=my_htons(port); + sa.sin_addr.s_addr=ip; + ESP_LOGI(TAG,"SetSockOpt RCV"); + struct timeval tv={5,0}; + int opt_res = lwip_setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv)); + ESP_LOGI(TAG,"SetSockOpt RCV res=%d", opt_res); + ESP_LOGI(TAG,"SetSockOpt SND"); + opt_res = lwip_setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv)); + ESP_LOGI(TAG,"SetSockOpt SND res=%d", opt_res); + ESP_LOGI(TAG,"Connecting to %s:%d",host,port); + int conn_res = lwip_connect(fd,(struct sockaddr*)&sa,sizeof(sa)); + ESP_LOGI(TAG,"Connect res=%d", conn_res); + if(conn_res<0){ ESP_LOGE(TAG,"connect fail %s:%d",host,port); close(fd); return -1; } + ESP_LOGI(TAG,"Connected, sending request"); + char req[512]; + int reqlen=snprintf(req,sizeof(req),"GET %s HTTP/1.0\r\nHost: %s:%d\r\nConnection: close\r\nUser-Agent: TactilityDM/0.1\r\nAccept: */*\r\n\r\n",path,host,port); + if(lwip_send(fd,req,reqlen,0)<0){ ESP_LOGE(TAG,"send fail"); close(fd); return -1; } + ESP_LOGI(TAG,"Request sent, waiting for response"); + + int capacity=8192; // further reduced to 8K internal to avoid PSRAM StoreProhibited crash (seen 0x8208775e) + char* buf=malloc(capacity); + if(!buf){ + buf=heap_caps_malloc(capacity, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if(!buf){ ESP_LOGE(TAG,"buf 8k fail"); close(fd); return -1; } + } + int total=0; + char* header_end=NULL; + int body_start=0; + int content_len=-1; + bool is_chunked=false; + + while(1){ + // Graceful cancel support to avoid force vTaskDelete leaking PSRAM/SD resources + if(G.fetch_cancel_req){ + ESP_LOGI(TAG,"http_get_raw canceled by user"); + heap_caps_free(buf); + close(fd); + return -1; + } + if(total>=capacity-1){ + int newcap=capacity*2; + if(newcap>600000){ ESP_LOGE(TAG,"too large %d",newcap); heap_caps_free(buf); close(fd); return -1; } + char* nb=heap_caps_malloc(newcap, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if(!nb){ heap_caps_free(buf); close(fd); return -1; } + memcpy(nb,buf,total); + heap_caps_free(buf); + buf=nb; + capacity=newcap; + } + int r=lwip_recv(fd,buf+total,capacity-total-1,0); + if(r<=0){ + if(r<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==ETIMEDOUT)){ + ESP_LOGW(TAG,"http_get_raw timeout total=%d",total); + // On timeout, if fetching canceled, abort + if(G.fetch_cancel_req){ + ESP_LOGI(TAG,"http_get_raw timeout + canceled"); + heap_caps_free(buf); + close(fd); + return -1; + } + // Continue loop – LWIP will retry until EOF + // But if we already have header and content_len satisfied, break + if(header_end && content_len>=0 && !is_chunked && total-body_start >= content_len) break; + // If no header yet and timeout, treat as failure to avoid 15s hang + if(!header_end && total==0){ + ESP_LOGE(TAG,"http_get_raw timeout before header"); + heap_caps_free(buf); + close(fd); + return -1; + } + // Otherwise continue to try again + continue; + } + break; + } + total+=r; + buf[total]=0; + if(!header_end){ + header_end=strstr(buf,"\r\n\r\n"); + if(header_end){ + body_start=(header_end-buf)+4; + if(strstr(buf,"chunked")) is_chunked=true; + char* cl=strstr(buf,"Content-Length:"); + if(!cl) cl=strstr(buf,"content-length:"); + if(cl){ cl=strchr(cl,':'); if(cl){ cl++; while(*cl==' ') cl++; content_len=atoi(cl); } } + ESP_LOGI(TAG,"header_end chunked=%d cl=%d total=%d",is_chunked,content_len,total); + if(content_len>=0 && !is_chunked){ + if(total-body_start >= content_len) break; + } + } + } else { + if(content_len>=0 && !is_chunked){ + if(total-body_start >= content_len) break; + } + } + } + close(fd); + if(!header_end){ ESP_LOGE(TAG,"no header end total=%d",total); heap_caps_free(buf); return -1; } + if(strncmp(buf,"HTTP/1.1 200",12)!=0 && strncmp(buf,"HTTP/1.0 200",12)!=0){ ESP_LOGE(TAG,"not 200: %.60s",buf); heap_caps_free(buf); return -1; } + + int raw_len=total-body_start; + char* raw=buf+body_start; + char* final_body=NULL; + int final_len=0; + + if(is_chunked){ + final_body=heap_caps_malloc(200000, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if(!final_body){ heap_caps_free(buf); return -1; } + int pos=0, out=0; + while(pos16){ pos+=off+2; continue; } + char hex[16]={0}; + memcpy(hex,raw+pos,off); + char* semi=strchr(hex,';'); + if(semi) *semi=0; + long sz=strtol(hex,NULL,16); + if(sz==0) break; + pos+=off+2; + if(pos+sz>raw_len) sz=raw_len-pos; + if(out+sz>=200000) break; + memcpy(final_body+out,raw+pos,sz); + out+=sz; + pos+=sz; + if(pos+1=0 && bl>content_len) bl=content_len; + final_body=heap_caps_malloc(bl+1, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if(!final_body){ heap_caps_free(buf); return -1; } + memcpy(final_body,raw,bl); + final_body[bl]=0; + final_len=bl; + } + heap_caps_free(buf); + *out_body=final_body; + ESP_LOGI(TAG,"GET ok %d",final_len); + return final_len; +} + +bool fetch_data(){ + char* js=NULL; + char url[320]; + bool seasons_ok=false; + bool episodes_ok=false; + ESP_LOGI(TAG,"fetch seasons"); + // Reduce payload via fields filter – drastically lowers cJSON malloc pressure (internal heap low warning) + snprintf(url,sizeof(url),"%s/api/collections/dm_seasons/records?perPage=100&sort=season_num&fields=season_num",PB); + int len=http_get_raw(url,&js); + if(len>0 && js){ + cJSON* root=cJSON_Parse(js); + free(js); js=NULL; + if(root){ + cJSON* items=cJSON_GetObjectItem(root,"items"); + if(cJSON_IsArray(items)){ + G.season_cnt=0; + int n=cJSON_GetArraySize(items); + for(int i=0;ivalueint; G.season_cnt++; } + } + sort_seasons(); + if(G.season_cnt>0) seasons_ok=true; + } + cJSON_Delete(root); + } + } else { + ESP_LOGW(TAG,"seasons fetch failed"); + } + if(G.season_cnt==0){ + ESP_LOGW(TAG,"seasons fallback"); + for(int i=1;i<=37;i++) G.seasons[i-1].num=i; + G.season_cnt=37; + } + + ESP_LOGI(TAG,"fetch episodes paginated to reduce internal heap (prev low 7KB)"); + G.ep_cnt=0; + for(int page=1; page<=10; page++){ + if(G.fetch_cancel_req) break; + if(G.ep_cnt>=MAX_EPS) break; + snprintf(url,sizeof(url),"%s/api/collections/dm_episodes/records?perPage=25&page=%d&sort=season_num,episode_num&fields=title,slug,season_num,episode_num,audio_url",PB,page); + len=http_get_raw(url,&js); + if(len<=0 || !js){ + ESP_LOGW(TAG,"episodes page %d fetch failed",page); + if(js){ free(js); js=NULL; } + break; + } + cJSON* root=cJSON_Parse(js); + free(js); js=NULL; + if(!root){ + ESP_LOGW(TAG,"episodes page %d JSON parse fail",page); + break; + } + cJSON* items=cJSON_GetObjectItem(root,"items"); + if(!cJSON_IsArray(items)){ + cJSON_Delete(root); + break; + } + int n=cJSON_GetArraySize(items); + if(n==0){ + cJSON_Delete(root); + break; + } + ESP_LOGI(TAG,"episodes page %d got %d items (total so far %d)",page,n,G.ep_cnt); + for(int i=0;ititle,t->valuestring,sizeof(e->title)-1); + if(slug&&cJSON_IsString(slug)) strncpy(e->slug,slug->valuestring,sizeof(e->slug)-1); + else if(t) strncpy(e->slug,t->valuestring,sizeof(e->slug)-1); + e->season=sn->valueint; + e->ep_num=en->valueint; + if(au&&cJSON_IsString(au)) strncpy(e->audio_url,au->valuestring,sizeof(e->audio_url)-1); + e->seen=has_slug(e->slug); + G.ep_cnt++; + } + episodes_ok=true; + cJSON_Delete(root); + // Yield between pages to let MemoryChecker recover + vTaskDelay(pdMS_TO_TICKS(400)); + if(n<25) break; // last page + } + // Final sort after all pages + if(G.ep_cnt>0) sort_eps(); + + if(G.ep_cnt==0){ + ESP_LOGW(TAG,"episodes fallback"); + for(int i=0;i<6;i++){ + EpInfo* e=&G.eps[i]; + snprintf(e->slug,sizeof(e->slug),"S01E%02d",i+1); + snprintf(e->title,sizeof(e->title),"Episode %d",i+1); + e->season=1; e->ep_num=i+1; + } + G.ep_cnt=6; + } + for(int i=0;i +#include + +#ifdef __cplusplus +extern "C" { +#endif + +uint16_t my_htons(uint16_t v); +bool resolve_host(const char* host, uint32_t* out_ip); +void normalize_audio_url(const char* in, char* out, size_t out_len); + +// Robust HTTP GET with PSRAM buffer, returns length and allocates *out_body (must be free'd via free/heap_caps_free) +int http_get_raw(const char* url, char** out_body); + +// Fetch seasons and episodes from PB – returns true if network fetch succeeded with real data +bool fetch_data(void); + +#ifdef __cplusplus +} +#endif diff --git a/Apps/DiscoveryMountain/main/Source/player.c b/Apps/DiscoveryMountain/main/Source/player.c new file mode 100644 index 0000000..b19c2ff --- /dev/null +++ b/Apps/DiscoveryMountain/main/Source/player.c @@ -0,0 +1,235 @@ +#include "player.h" +#include "storage.h" +#include "ui.h" +#include "download.h" + +#include +#include +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "esp_heap_caps.h" + +void wait_play_exit(void) { + // No internal playback anymore, nothing to wait +} + +void stop_playback_sync(void) { + // No internal playback, nothing to stop + G.is_playing = false; + G.is_paused = false; +} + +static bool try_external_downloader(int idx) { + if(idx<0||idx>=G.ep_cnt) return false; + EpInfo* ep=&G.eps[idx]; + if (strlen(ep->audio_url) < 8) { + ESP_LOGW(TAG, "Skipping DL for %s: empty URL", ep->slug); + tt_lvgl_lock(portMAX_DELAY); + if (G.lbl_status) lv_label_set_text(G.lbl_status, "No URL – fetch failed?"); + tt_lvgl_unlock(); + return false; + } + char fpath[300]; + make_sd_path(fpath, sizeof(fpath), ep->slug); + + tt_lvgl_lock(portMAX_DELAY); + close_overlay(); + if(G.dl_overlay) hide_dl_overlay(); + tt_lvgl_unlock(); + + BundleHandle b = tt_bundle_alloc(); + if (!b) { + ESP_LOGW(TAG, "Failed to alloc bundle for external DL"); + return false; + } + tt_bundle_put_string(b, "url", ep->audio_url); + tt_bundle_put_string(b, "path", fpath); + tt_bundle_put_bool(b, "override", false); + + ESP_LOGI(TAG, "Starting external SdDownloader for %s -> %s", ep->slug, fpath); + G.pending_dl_idx = idx; + + tt_lvgl_lock(portMAX_DELAY); + if (G.lbl_status) lv_label_set_text(G.lbl_status, "Starting downloader..."); + tt_lvgl_unlock(); + + tt_app_start_with_bundle("one.tactility.sddownloader", b); + return true; +} + +static bool try_external_downloader_batch(int season) { + // Reverted to single-file mode – batch array caused crashes in SdDownloader + // Now just download first missing episode in season + int first_missing = -1; + for(int i=0;i=8){ + first_missing = i; + break; + } + } + if(first_missing==-1){ + tt_lvgl_lock(portMAX_DELAY); + if(G.lbl_status) lv_label_set_text(G.lbl_status,"Season already cached"); + tt_lvgl_unlock(); + return true; + } + ESP_LOGI(TAG, "DL Missing (single-file revert) – downloading first missing S%02d idx %d", season, first_missing); + return try_external_downloader(first_missing); +} + +void start_idx_internal(int idx){ + if(idx<0||idx>=G.ep_cnt) return; + EpInfo* ep=&G.eps[idx]; + char fpath[300]; + make_sd_path(fpath, sizeof(fpath), ep->slug); + + if(!has_slug(ep->slug)){ + ESP_LOGW(TAG, "start_idx_internal called but file missing %s", ep->slug); + start_idx(idx); + return; + } + + int saved = 0; + if(G.cur_ep_idx!=idx){ + saved = get_saved_pos_for_slug(ep->slug); + if(saved>5){ + ESP_LOGI(TAG,"Restoring saved pos %d for %s", saved, ep->slug); + } + } else { + saved = G.pos_sec; + } + + tt_lvgl_lock(portMAX_DELAY); + close_overlay(); + if(G.dl_overlay) hide_dl_overlay(); + tt_lvgl_unlock(); + + BundleHandle b = tt_bundle_alloc(); + if(b){ + tt_bundle_put_string(b, "file", fpath); + tt_bundle_put_string(b, "path", fpath); + tt_bundle_put_int32(b, "pos_sec", saved); + tt_bundle_put_int32(b, "position", saved); + tt_bundle_put_int32(b, "volume", G.volume); + ESP_LOGI(TAG, "Launching Mp3Player for %s pos %d", fpath, saved); + G.pending_dl_idx = idx; + G.cur_ep_idx=idx; G.cur_season=ep->season; + strncpy(G.cur_title,ep->title,sizeof(G.cur_title)-1); + strncpy(G.last_slug,ep->slug,sizeof(G.last_slug)-1); + save_state(); + tt_app_start_with_bundle("one.tactility.mp3player", b); + return; + } + + ESP_LOGW(TAG, "Failed to alloc bundle for Mp3Player"); + tt_lvgl_lock(portMAX_DELAY); + if(G.lbl_status) lv_label_set_text(G.lbl_status,"Failed to launch player"); + tt_lvgl_unlock(); +} + +void start_idx(int idx){ + if(idx<0||idx>=G.ep_cnt) return; + EpInfo* ep=&G.eps[idx]; + if(!has_slug(ep->slug)){ + // File not on SD, download via external downloader + if (try_external_downloader(idx)) { + return; + } + // Fallback: show message, cannot download internally anymore + tt_lvgl_lock(portMAX_DELAY); + if(G.lbl_status) lv_label_set_text(G.lbl_status,"Install SdDownloader"); + tt_lvgl_unlock(); + return; + } + start_idx_internal(idx); +} + +void select_ep(int idx){ + if(idx<0||idx>=G.ep_cnt) return; + if(G.cur_ep_idx>=0 && G.cur_ep_idx < G.ep_cnt && G.cur_ep_idx!=idx){ + save_current_ep_pos(); + } + G.cur_ep_idx=idx; + EpInfo* ep=&G.eps[idx]; + G.cur_season=ep->season; + strncpy(G.last_slug, ep->slug, sizeof(G.last_slug)-1); + + int saved = get_saved_pos_for_slug(ep->slug); + G.pos_sec=saved; + G.last_pct=-1; + G.total_sec=0; + + tt_lvgl_lock(portMAX_DELAY); + if(G.lbl_title) lv_label_set_text(G.lbl_title,ep->title); + if(G.lbl_meta){ + char b[64]; + if(saved>5){ + char tbuf[16]; + fmt_time(tbuf,saved); + snprintf(b,sizeof(b),"S%02d E%02d %s • %s",ep->season,ep->ep_num,ep->seen?"*":"",tbuf); + } else { + snprintf(b,sizeof(b),"S%02d E%02d %s",ep->season,ep->ep_num,ep->seen?"*":""); + } + lv_label_set_text(G.lbl_meta,b); + } + if(G.lbl_status){ + if(saved>5) lv_label_set_text(G.lbl_status,"Resume – tap Play"); + else lv_label_set_text(G.lbl_status,ep->seen?"On SD – tap Play":"Tap Play to download"); + } + if(G.bar){ + lv_bar_set_value(G.bar,0,LV_ANIM_OFF); + } + if(G.lbl_time){ + if(saved>5){ + char a[16]; + fmt_time(a,saved); + char buf[40]; + snprintf(buf,sizeof(buf),"%s / --:--",a); + lv_label_set_text(G.lbl_time,buf); + } else { + lv_label_set_text(G.lbl_time,"0:00 / --:--"); + } + } + tt_lvgl_unlock(); + save_state(); +} + +void go_next(){ + if(G.ep_cnt==0) return; + save_current_ep_pos(); + save_state(); + int n=G.cur_ep_idx+1; + if(n>=G.ep_cnt) n=0; + select_ep(n); + // Do not auto-play next, per user request: no autoplay +} + +void go_prev(){ + if(G.ep_cnt==0) return; + if(G.pos_sec>5){ + if(G.cur_ep_idx>=0){ + set_saved_pos_for_slug(G.eps[G.cur_ep_idx].slug, 0); + save_state(); + } + G.pos_sec=0; + int cur=G.cur_ep_idx; + if(cur<0) cur=0; + select_ep(cur); + return; + } + save_current_ep_pos(); + save_state(); + int p=G.cur_ep_idx-1; + if(p<0) p=G.ep_cnt-1; + select_ep(p); +} + +// Called from UI for season batch download +bool download_season(int season) { + return try_external_downloader_batch(season); +} diff --git a/Apps/DiscoveryMountain/main/Source/player.h b/Apps/DiscoveryMountain/main/Source/player.h new file mode 100644 index 0000000..275745b --- /dev/null +++ b/Apps/DiscoveryMountain/main/Source/player.h @@ -0,0 +1,23 @@ +#pragma once + +#include "app_context.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Simplified: no internal audio, only external apps +void wait_play_exit(void); +void stop_playback_sync(void); + +void start_idx_internal(int idx); +void start_idx(int idx); +void select_ep(int idx); +void go_next(void); +void go_prev(void); +bool download_season(int season); + +#ifdef __cplusplus +} +#endif diff --git a/Apps/DiscoveryMountain/main/Source/storage.c b/Apps/DiscoveryMountain/main/Source/storage.c new file mode 100644 index 0000000..5f2f41e --- /dev/null +++ b/Apps/DiscoveryMountain/main/Source/storage.c @@ -0,0 +1,321 @@ +#include "storage.h" +#include "cJSON.h" +#include +#include +#include +#include +#include +#include +#include "esp_heap_caps.h" + +void make_sd_path(char* out, size_t out_len, const char* slug){ + if(!out || !slug) return; + snprintf(out, out_len, "%s/%s.mp3", DM_DIR, slug); +} +void ensure_dir(){ + mkdir("/sdcard/apps",0777); + mkdir("/sdcard/apps/one.tactility.discoverymountain",0777); + mkdir("/sdcard/apps/one.tactility.discoverymountain/userdata",0777); + mkdir(DM_DIR,0777); +} +bool has_slug(const char* slug){ + char p[300]; + make_sd_path(p, sizeof(p), slug); + struct stat st; + return stat(p,&st)==0; +} + +void fmt_time(char* o,int s){ snprintf(o,16,"%d:%02d",s/60,s%60); } + +void sort_seasons(){ + for(int i=0;iseason != b->season) should_swap = (b->season < a->season); + else should_swap = (b->ep_num < a->ep_num); + if(should_swap){ + *t=*a; *a=*b; *b=*t; + } + } + } + heap_caps_free(t); +} + +// ── Per-episode position helpers ── +int find_saved_pos_idx(const char* slug){ + if(!slug) return -1; + for(int i=0;i=0) return G.saved_pos[idx].pos; + return 0; +} +void set_saved_pos_for_slug(const char* slug, int pos){ + if(!slug || strlen(slug)==0) return; + if(pos<0) pos=0; + if(G.total_sec>0 && pos>0 && pos >= G.total_sec-10){ + pos=0; + } + int idx=find_saved_pos_idx(slug); + if(idx>=0){ + G.saved_pos[idx].pos=pos; + return; + } + if(G.saved_pos_cnt < MAX_EPS){ + strncpy(G.saved_pos[G.saved_pos_cnt].slug, slug, sizeof(G.saved_pos[G.saved_pos_cnt].slug)-1); + G.saved_pos[G.saved_pos_cnt].slug[sizeof(G.saved_pos[G.saved_pos_cnt].slug)-1]=0; + G.saved_pos[G.saved_pos_cnt].pos=pos; + G.saved_pos_cnt++; + } +} +void save_current_ep_pos(){ + if(G.cur_ep_idx>=0 && G.cur_ep_idx < G.ep_cnt){ + const char* slug=G.eps[G.cur_ep_idx].slug; + set_saved_pos_for_slug(slug, G.pos_sec); + } else if(strlen(G.last_slug)>0){ + set_saved_pos_for_slug(G.last_slug, G.pos_sec); + } +} + +void save_state(){ + if(G.cur_ep_idx>=0 && G.ep_cnt>0){ + if(G.pos_sec>=0){ + set_saved_pos_for_slug(G.eps[G.cur_ep_idx].slug, G.pos_sec); + } + } + cJSON* r=cJSON_CreateObject(); + if(G.ep_cnt>0 && G.cur_ep_idx>=0 && G.cur_ep_idx < G.ep_cnt) cJSON_AddStringToObject(r,"last_slug",G.eps[G.cur_ep_idx].slug); + else if(strlen(G.last_slug)>0) cJSON_AddStringToObject(r,"last_slug",G.last_slug); + cJSON_AddNumberToObject(r,"last_season",G.cur_season); + cJSON_AddNumberToObject(r,"pos_sec",G.pos_sec); + if(G.saved_pos_cnt>0){ + cJSON* pos_obj=cJSON_CreateObject(); + for(int i=0;i0){ + cJSON_AddNumberToObject(pos_obj, G.saved_pos[i].slug, G.saved_pos[i].pos); + } + } + cJSON_AddItemToObject(r,"positions",pos_obj); + } + char* s=cJSON_PrintUnformatted(r); + if(s){ + FILE* f=fopen(STATE_PATH,"w"); + if(f){ fwrite(s,1,strlen(s),f); fclose(f);} + free(s); + } + cJSON_Delete(r); +} + +void load_state(){ + G.cur_season=1; G.cur_ep_idx=-1; G.pos_sec=0; + G.saved_pos_cnt=0; + G.last_save_tick=0; + memset(G.last_slug,0,sizeof(G.last_slug)); + memset(G.saved_pos,0,sizeof(G.saved_pos)); + FILE* f=fopen(STATE_PATH,"r"); + if(!f) return; + fseek(f,0,SEEK_END); + long sz=ftell(f); + fseek(f,0,SEEK_SET); + if(sz<=0||sz>16384){ fclose(f); return;} + char* buf=malloc(sz+1); + if(!buf){ fclose(f); return; } + size_t got=fread(buf,1,sz,f); + buf[got]=0; + fclose(f); + cJSON* r=cJSON_Parse(buf); + free(buf); + if(!r) return; + cJSON* ls=cJSON_GetObjectItem(r,"last_slug"); + cJSON* lsn=cJSON_GetObjectItem(r,"last_season"); + cJSON* ps=cJSON_GetObjectItem(r,"pos_sec"); + if(ps&&cJSON_IsNumber(ps)) G.pos_sec=ps->valueint; + if(lsn&&cJSON_IsNumber(lsn)) G.cur_season=lsn->valueint; + if(ls&&cJSON_IsString(ls)){ + strncpy(G.last_slug, ls->valuestring, sizeof(G.last_slug)-1); + for(int i=0;ivaluestring)==0){ G.cur_ep_idx=i; G.cur_season=G.eps[i].season; break; } + } + cJSON* poss=cJSON_GetObjectItem(r,"positions"); + if(poss && cJSON_IsObject(poss)){ + cJSON* child=NULL; + cJSON_ArrayForEach(child, poss){ + if(cJSON_IsNumber(child) && child->string && G.saved_pos_cnt < MAX_EPS){ + strncpy(G.saved_pos[G.saved_pos_cnt].slug, child->string, sizeof(G.saved_pos[G.saved_pos_cnt].slug)-1); + G.saved_pos[G.saved_pos_cnt].pos=child->valueint; + G.saved_pos_cnt++; + } + } + if(strlen(G.last_slug)>0){ + int sp=get_saved_pos_for_slug(G.last_slug); + if(sp>0) G.pos_sec=sp; + } + } + cJSON_Delete(r); +} + +bool save_cache(void){ + if(G.ep_cnt==0 || G.season_cnt==0) return false; + cJSON* root=cJSON_CreateObject(); + if(!root) return false; + cJSON* seasons_arr=cJSON_CreateArray(); + if(!seasons_arr){ cJSON_Delete(root); return false; } + for(int i=0;islug[0]==0) continue; + cJSON* obj=cJSON_CreateObject(); + if(!obj) continue; + cJSON_AddStringToObject(obj,"title", e->title); + cJSON_AddStringToObject(obj,"slug", e->slug); + cJSON_AddNumberToObject(obj,"season_num", e->season); + cJSON_AddNumberToObject(obj,"episode_num", e->ep_num); + cJSON_AddStringToObject(obj,"audio_url", e->audio_url); + cJSON_AddItemToArray(eps_arr, obj); + } + cJSON_AddItemToObject(root,"episodes",eps_arr); + + char* s=cJSON_PrintUnformatted(root); + cJSON_Delete(root); + if(!s) return false; + FILE* f=fopen(CACHE_PATH,"w"); + bool ok=false; + if(f){ + size_t len=strlen(s); + size_t wrote=fwrite(s,1,len,f); + fclose(f); + ok = (wrote==len); + } + free(s); + return ok; +} + +bool load_cache(void){ + FILE* f=fopen(CACHE_PATH,"r"); + if(!f) return false; + fseek(f,0,SEEK_END); + long sz=ftell(f); + fseek(f,0,SEEK_SET); + if(sz<=0 || sz>200000){ fclose(f); return false; } + // Use PSRAM for large json to avoid internal heap pressure + char* buf=heap_caps_malloc(sz+1, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if(!buf){ + buf=malloc(sz+1); + if(!buf){ fclose(f); return false; } + } + size_t got=fread(buf,1,sz,f); + buf[got]=0; + fclose(f); + cJSON* root=cJSON_Parse(buf); + heap_caps_free(buf); + if(!root) return false; + + cJSON* seasons_arr=cJSON_GetObjectItem(root,"seasons"); + cJSON* eps_arr=cJSON_GetObjectItem(root,"episodes"); + if(!cJSON_IsArray(seasons_arr) || !cJSON_IsArray(eps_arr)){ + cJSON_Delete(root); + return false; + } + + int scnt=cJSON_GetArraySize(seasons_arr); + G.season_cnt=0; + for(int i=0;ivalueint; + G.season_cnt++; + } + } + + int ecnt=cJSON_GetArraySize(eps_arr); + G.ep_cnt=0; + for(int i=0;ititle, t->valuestring, sizeof(e->title)-1); + if(slug && cJSON_IsString(slug)) strncpy(e->slug, slug->valuestring, sizeof(e->slug)-1); + else if(t && cJSON_IsString(t)) strncpy(e->slug, t->valuestring, sizeof(e->slug)-1); + e->season=sn->valueint; + e->ep_num=en->valueint; + if(au && cJSON_IsString(au)) strncpy(e->audio_url, au->valuestring, sizeof(e->audio_url)-1); + e->seen=has_slug(e->slug); + G.ep_cnt++; + } + + cJSON_Delete(root); + if(G.season_cnt>0) sort_seasons(); + if(G.ep_cnt>0) sort_eps(); + // Refresh seen in case SD changed + for(int i=0;i0 && G.ep_cnt>0); +} + +void lru_check(){ + typedef struct{ char path[300]; time_t mt; } FE; + // Allocate from PSRAM to avoid stack overflow (200*~304=60KB on stack would overflow 12KB task) + FE *files = heap_caps_malloc(200 * sizeof(FE), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if(!files) return; + int fc=0; + DIR* d=opendir(DM_DIR); + if(!d){ heap_caps_free(files); return; } + struct dirent* de; + char cur_path[300]={0}; + if(G.cur_ep_idx>=0 && G.cur_ep_idxd_name[0]=='.') continue; + size_t l=strlen(de->d_name); + if(l<5) continue; + if(strcmp(de->d_name+l-4,".mp3")!=0) continue; + char full[300]; + snprintf(full,sizeof(full),"%s/%.200s",DM_DIR,de->d_name); + struct stat st; + if(stat(full,&st)!=0) continue; + if(cur_path[0] && strcmp(full,cur_path)==0) continue; + strncpy(files[fc].path,full,sizeof(files[fc].path)-1); + files[fc].mt=st.st_mtime; + fc++; + } + closedir(d); + if(fc<12){ heap_caps_free(files); return; } + for(int i=0;i +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Path helpers +void make_sd_path(char* out, size_t out_len, const char* slug); +void ensure_dir(void); +bool has_slug(const char* slug); + +// LRU cache cleanup +void lru_check(void); + +// Sorting +void sort_seasons(void); +void sort_eps(void); + +// Time formatting +void fmt_time(char* o, int s); + +// Per-episode resume helpers +int find_saved_pos_idx(const char* slug); +int get_saved_pos_for_slug(const char* slug); +void set_saved_pos_for_slug(const char* slug, int pos); +void save_current_ep_pos(void); + +// Persistence +void save_state(void); +void load_state(void); + +// Seasons / episodes cache (PB data, rarely changes) +bool save_cache(void); +bool load_cache(void); + +#ifdef __cplusplus +} +#endif diff --git a/Apps/DiscoveryMountain/main/Source/ui.c b/Apps/DiscoveryMountain/main/Source/ui.c new file mode 100644 index 0000000..1d07427 --- /dev/null +++ b/Apps/DiscoveryMountain/main/Source/ui.c @@ -0,0 +1,554 @@ +#include "ui.h" +#include "storage.h" +#include "player.h" +#include "download.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include "esp_log.h" +#include "esp_heap_caps.h" + +void close_overlay(){ + if(G.overlay){ + lv_obj_delete(G.overlay); + G.overlay=NULL; + G.dropdown=NULL; + G.season_list_cont=NULL; + } +} + +static void overlay_close_timer_cb(lv_timer_t* t){ + close_overlay(); + lv_timer_delete(t); +} +static void close_overlay_safe(void){ + // Defer deletion to avoid deleting ancestor of current event target (season buttons inside overlay) + lv_timer_create(overlay_close_timer_cb, 20, NULL); +} + +// Forward decls +static void ep_action_cb(lv_event_t* e); +static void season_select_cb(lv_event_t* e); +static void rebuild_episode_list_locked(void); + +static void clear_container(lv_obj_t* cont){ + if(!cont) return; + uint32_t child_cnt = lv_obj_get_child_cnt(cont); + while(child_cnt>0){ + lv_obj_t* child = lv_obj_get_child(cont, 0); + if(child) lv_obj_delete(child); + else break; + child_cnt = lv_obj_get_child_cnt(cont); + } +} + +static void btn_close_cb(lv_event_t* e){ + (void)e; + if(G.overlay){ + close_overlay(); + return; + } + tt_app_stop(); +} + +static void get_season_stats(int season, int* total, int* cached){ + int t=0,c=0; + for(int i=0;i0){ + snprintf(buf,sizeof(buf),"S%02d • %d/%d on SD • %d eps", G.cur_season, cached, total, total); + }else{ + snprintf(buf,sizeof(buf),"S%02d • %d eps", G.cur_season, total); + } + if(G.lbl_season) lv_label_set_text(G.lbl_season, buf); + if(G.lbl_title){ + // Keep app name + // optionally update title to count + lv_label_set_text(G.lbl_title, "Discovery Mountain"); + } +} + +static void rebuild_episode_list_locked(void){ + if(!G.ep_list_cont) return; + clear_container(G.ep_list_cont); + + if(G.fetching){ + lv_obj_t* lbl=lv_label_create(G.ep_list_cont); + lv_label_set_text(lbl,"Fetching episodes..."); + lv_obj_set_style_text_color(lbl, lv_color_hex(0xAAAAAA),0); + lv_obj_align(lbl,LV_ALIGN_TOP_MID,0,20); + return; + } + if(G.ep_cnt==0){ + lv_obj_t* lbl=lv_label_create(G.ep_list_cont); + lv_label_set_text(lbl,"No episodes"); + lv_obj_set_style_text_color(lbl, lv_color_hex(0xAAAAAA),0); + return; + } + + // Count episodes for cur season + int count=0; + for(int i=0;iseen ? lv_color_hex(0x1E2A22) : lv_color_hex(0x222222),0); + lv_obj_set_style_bg_opa(row, LV_OPA_COVER,0); + lv_obj_set_style_radius(row,8,0); + lv_obj_set_style_pad_all(row,6,0); + lv_obj_set_style_border_width(row,1,0); + lv_obj_set_style_border_color(row, ep->seen ? lv_color_hex(0x2E7D32) : lv_color_hex(0x333333),0); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_pad_gap(row,6,0); + lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(row, ep_action_cb, LV_EVENT_CLICKED, (void*)(intptr_t)i); + + // Left single label with title (truncated) – we embed meta as second line via \n to save objects + lv_obj_t* ltitle=lv_label_create(row); + lv_obj_set_flex_grow(ltitle,1); + // Truncate title to ~26 chars for display + char short_title[30]; + strncpy(short_title, ep->title, sizeof(short_title)-1); + short_title[sizeof(short_title)-1]=0; + if(strlen(ep->title) > 26){ + short_title[23]='.'; + short_title[24]='.'; + short_title[25]='.'; + short_title[26]=0; + } + char line1[64]; + snprintf(line1,sizeof(line1),"E%02d: %s", ep->ep_num, short_title); + // Second line status + char line2[32]; + int saved = get_saved_pos_for_slug(ep->slug); + if(ep->seen){ + if(saved>5){ + char tb[16]; fmt_time(tb,saved); + snprintf(line2,sizeof(line2),"On SD %s", tb); + }else{ + snprintf(line2,sizeof(line2),"On SD"); + } + }else{ + snprintf(line2,sizeof(line2),"Not DL"); + } + char tbuf[96]; + snprintf(tbuf,sizeof(tbuf),"%s\n%s", line1, line2); + lv_label_set_text(ltitle, tbuf); + lv_obj_set_width(ltitle, LV_PCT(100)); + lv_label_set_long_mode(ltitle, LV_LABEL_LONG_DOT); + lv_obj_set_style_text_font(ltitle, lvgl_get_text_font(FONT_SIZE_SMALL),0); + lv_obj_set_style_text_color(ltitle, ep->seen ? lv_color_hex(0xE0E0E0) : lv_color_hex(0xCCCCCC),0); + + // Right side: button + lv_obj_t* btn=lv_btn_create(row); + lv_obj_set_size(btn, 52, 34); + if(ep->seen){ + lv_obj_set_style_bg_color(btn, lv_color_hex(0x2E7D32),0); + }else{ + lv_obj_set_style_bg_color(btn, lv_color_hex(0x1E88E5),0); + } + lv_obj_set_style_radius(btn,6,0); + lv_obj_t* bl=lv_label_create(btn); + lv_label_set_text(bl, ep->seen ? LV_SYMBOL_PLAY : LV_SYMBOL_DOWNLOAD); + lv_obj_center(bl); + lv_obj_add_event_cb(btn, ep_action_cb, LV_EVENT_CLICKED, (void*)(intptr_t)i); + } + // Scroll to top after rebuild + lv_obj_scroll_to_y(G.ep_list_cont, 0, LV_ANIM_OFF); +} + +void ui_rebuild_episode_list(void){ + tt_lvgl_lock(portMAX_DELAY); + if(G.ep_list_cont) rebuild_episode_list_locked(); + ui_update_header_locked(); + tt_lvgl_unlock(); +} + +static void ep_action_cb(lv_event_t* e){ + if(lv_event_get_code(e)!=LV_EVENT_CLICKED) return; + // Retrieve idx from user data + int idx = (int)(intptr_t)lv_event_get_user_data(e); + // Fallback: try event target's user data via current target? + // lv_event_get_user_data should already give us the idx + if(idx<0 || idx>=G.ep_cnt) return; + if(G.downloading || G.fetching) return; + + EpInfo* ep=&G.eps[idx]; + ESP_LOGI(TAG,"ep_action idx %d S%02dE%02d seen %d", idx, ep->season, ep->ep_num, ep->seen); + + if(ep->season != G.cur_season){ + // Shouldn't happen but update cur season to match + G.cur_season = ep->season; + } + + // Use start_idx which handles both DL and Play + tt_lvgl_lock(portMAX_DELAY); + if(G.lbl_status){ + if(ep->seen) lv_label_set_text(G.lbl_status,"Starting player..."); + else lv_label_set_text(G.lbl_status,"Starting downloader..."); + } + tt_lvgl_unlock(); + + start_idx(idx); +} + +static void season_select_cb(lv_event_t* e){ + if(lv_event_get_code(e)!=LV_EVENT_CLICKED) return; + int season_num = (int)(intptr_t)lv_event_get_user_data(e); + if(season_num<=0) return; + ESP_LOGI(TAG,"Season selected S%02d", season_num); + // Defer overlay deletion – deleting ancestor during event is unsafe + close_overlay_safe(); + G.cur_season = season_num; + // Save state immediately + tt_lvgl_lock(portMAX_DELAY); + ui_update_header_locked(); + rebuild_episode_list_locked(); + if(G.lbl_status){ + char buf[40]; + snprintf(buf,sizeof(buf),"Switched to S%02d", season_num); + lv_label_set_text(G.lbl_status, buf); + } + tt_lvgl_unlock(); + save_state(); + + // Jump to first unseen or first in season + int first=-1, first_unseen=-1; + for(int i=0;i=G.season_cnt) return; + int sn=G.seasons[sel].num; + close_overlay_safe(); + int first=-1, first_unseen=-1; + for(int i=0;i0){ + if(sn==G.cur_season) snprintf(blabel,sizeof(blabel),"S%02d %d/%d " LV_SYMBOL_OK, sn, cached, total); + else snprintf(blabel,sizeof(blabel),"S%02d %d/%d", sn, cached, total); + }else{ + if(sn==G.cur_season) snprintf(blabel,sizeof(blabel),"S%02d " LV_SYMBOL_OK, sn); + else snprintf(blabel,sizeof(blabel),"S%02d", sn); + } + lv_obj_t* l1=lv_label_create(btn); + lv_label_set_text(l1, blabel); + lv_obj_set_style_text_font(l1, lvgl_get_text_font(FONT_SIZE_SMALL),0); + lv_obj_center(l1); + } + + lv_obj_t* bc=lv_btn_create(G.overlay); + lv_obj_set_size(bc, LV_PCT(60), 36); + lv_obj_set_style_bg_color(bc, lv_color_hex(0x444444),0); + lv_obj_t* lc=lv_label_create(bc); + lv_label_set_text(lc,"Close"); + lv_obj_center(lc); + lv_obj_add_event_cb(bc,close_btn_cb,LV_EVENT_CLICKED,NULL); + + tt_lvgl_unlock(); +} + +void btn_dl_season_cb(lv_event_t* e){ + (void)e; + if(G.downloading) return; + if(G.fetching) return; + if(G.ep_cnt==0) return; + + int season = G.cur_season; + ESP_LOGI(TAG, "DL Missing S%02d pressed", season); + + tt_lvgl_lock(portMAX_DELAY); + close_overlay(); + if(G.dl_overlay) hide_dl_overlay(); + if(G.lbl_status){ + char buf[40]; + snprintf(buf,sizeof(buf),"Checking S%02d missing...", season); + lv_label_set_text(G.lbl_status, buf); + } + tt_lvgl_unlock(); + + if (!download_season(season)) { + tt_lvgl_lock(portMAX_DELAY); + if(G.lbl_status) lv_label_set_text(G.lbl_status,"DL Season failed – no missing?"); + tt_lvgl_unlock(); + } +} + +void build_ui(lv_obj_t* parent){ + tt_lvgl_lock(portMAX_DELAY); + lv_obj_t* root = lv_obj_create(parent); + G.root = root; + lv_obj_set_size(root,LV_PCT(100),LV_PCT(100)); + lv_obj_set_style_bg_color(root,lv_color_hex(0x111111),0); + lv_obj_set_style_pad_all(root,6,0); + lv_obj_set_style_pad_gap(root,6,0); + lv_obj_set_style_border_width(root,0,0); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(root, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + lv_obj_remove_flag(root, LV_OBJ_FLAG_SCROLLABLE); + + // Header container + lv_obj_t* header=lv_obj_create(root); + lv_obj_set_size(header, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(header, LV_OPA_TRANSP,0); + lv_obj_set_style_border_width(header,0,0); + lv_obj_set_style_pad_all(header,2,0); + lv_obj_set_style_pad_gap(header,6,0); + lv_obj_set_flex_flow(header, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(header, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(header, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t* header_left=lv_obj_create(header); + lv_obj_set_size(header_left, LV_PCT(80), LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(header_left, LV_OPA_TRANSP,0); + lv_obj_set_style_border_width(header_left,0,0); + lv_obj_set_style_pad_all(header_left,0,0); + lv_obj_set_flex_flow(header_left, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_gap(header_left,2,0); + lv_obj_remove_flag(header_left, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_flex_grow(header_left,1); + + G.lbl_title=lv_label_create(header_left); + lv_label_set_text(G.lbl_title,"Discovery Mountain"); + lv_obj_set_style_text_font(G.lbl_title, lvgl_get_text_font(FONT_SIZE_DEFAULT), 0); + lv_obj_set_style_text_color(G.lbl_title, lv_color_hex(0xFFFFFF),0); + + G.lbl_season=lv_label_create(header_left); + lv_label_set_text(G.lbl_season,"Loading seasons..."); + lv_obj_set_style_text_font(G.lbl_season, lvgl_get_text_font(FONT_SIZE_SMALL),0); + lv_obj_set_style_text_color(G.lbl_season,lv_color_hex(0xAAAAAA),0); + + // Close button top right + lv_obj_t* btn_close=lv_btn_create(header); + lv_obj_set_size(btn_close, 36, 32); + lv_obj_set_style_bg_color(btn_close, lv_color_hex(0x333333),0); + lv_obj_set_style_radius(btn_close,6,0); + lv_obj_t* l_close=lv_label_create(btn_close); + lv_label_set_text(l_close, LV_SYMBOL_CLOSE); + lv_obj_set_style_text_font(l_close, lvgl_get_text_font(FONT_SIZE_SMALL),0); + lv_obj_center(l_close); + lv_obj_add_event_cb(btn_close, btn_close_cb, LV_EVENT_CLICKED, NULL); + + // Action row: Seasons + DL Missing + lv_obj_t* row=lv_obj_create(root); + lv_obj_set_size(row,LV_PCT(100),44); + lv_obj_set_flex_flow(row,LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row,LV_FLEX_ALIGN_SPACE_EVENLY,LV_FLEX_ALIGN_CENTER,LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_bg_opa(row,LV_OPA_TRANSP,0); + lv_obj_set_style_border_width(row,0,0); + lv_obj_set_style_pad_all(row,0,0); + lv_obj_set_style_pad_gap(row,8,0); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t* bs=lv_btn_create(row); + lv_obj_set_size(bs,LV_PCT(48),36); + G.btn_seasons=bs; + lv_obj_set_style_bg_color(bs, lv_color_hex(0x333333),0); + lv_obj_set_style_radius(bs,6,0); + lv_obj_t* ls=lv_label_create(bs); + lv_label_set_text(ls,"Seasons"); + lv_obj_set_style_text_font(ls, lvgl_get_text_font(FONT_SIZE_SMALL),0); + lv_obj_center(ls); + lv_obj_add_event_cb(bs,btn_seasons_cb,LV_EVENT_CLICKED,NULL); + + lv_obj_t* bd=lv_btn_create(row); + lv_obj_set_size(bd,LV_PCT(48),36); + G.btn_dl_missing=bd; + lv_obj_set_style_bg_color(bd, lv_color_hex(0x1E88E5), 0); + lv_obj_set_style_radius(bd,6,0); + lv_obj_t* ld=lv_label_create(bd); + lv_label_set_text(ld,"DL Missing"); + lv_obj_set_style_text_font(ld, lvgl_get_text_font(FONT_SIZE_SMALL),0); + lv_obj_center(ld); + lv_obj_add_event_cb(bd,btn_dl_season_cb,LV_EVENT_CLICKED,NULL); + + // Episode list container – fills remaining space + G.ep_list_cont=lv_obj_create(root); + lv_obj_set_size(G.ep_list_cont, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_grow(G.ep_list_cont, 1); + lv_obj_set_style_bg_color(G.ep_list_cont, lv_color_hex(0x0E0E0E),0); + lv_obj_set_style_bg_opa(G.ep_list_cont, LV_OPA_COVER,0); + lv_obj_set_style_radius(G.ep_list_cont,8,0); + lv_obj_set_style_pad_all(G.ep_list_cont,4,0); + lv_obj_set_style_pad_gap(G.ep_list_cont,4,0); + lv_obj_set_flex_flow(G.ep_list_cont, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(G.ep_list_cont, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + // Ensure scrollable + lv_obj_add_flag(G.ep_list_cont, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollbar_mode(G.ep_list_cont, LV_SCROLLBAR_MODE_AUTO); + + lv_obj_t* hint=lv_label_create(G.ep_list_cont); + lv_label_set_text(hint,"Loading episodes..."); + lv_obj_set_style_text_color(hint, lv_color_hex(0x888888),0); + lv_obj_set_style_text_font(hint, lvgl_get_text_font(FONT_SIZE_SMALL),0); + + // Status bar at bottom + G.lbl_status=lv_label_create(root); + lv_label_set_text(G.lbl_status,"Fetching data..."); + lv_obj_set_style_text_color(G.lbl_status,lv_color_hex(0x888888),0); + lv_obj_set_style_text_font(G.lbl_status, lvgl_get_text_font(FONT_SIZE_SMALL),0); + lv_obj_set_width(G.lbl_status, LV_PCT(100)); + lv_label_set_long_mode(G.lbl_status, LV_LABEL_LONG_DOT); + + // Legacy placeholders nulled + G.lbl_meta=NULL; + G.bar=NULL; + G.lbl_time=NULL; + G.btn_play=NULL; + G.dl_overlay=NULL; + G.ui_timer_handle=NULL; + + G.ui_timer_handle = lv_timer_create(ui_timer,300,NULL); + tt_lvgl_unlock(); +} + +void ui_deinit(void){ + if(G.ui_timer_handle){ + lv_timer_delete(G.ui_timer_handle); + G.ui_timer_handle=NULL; + } +} + +void ui_timer(lv_timer_t* t){ + (void)t; + // If app is closing (root null) skip + if(!G.root) return; + if(G.fetching){ + return; + } + if(G.downloading && G.dl_overlay){ + update_dl_overlay_ui(); + } +} + +void btn_play_cb(lv_event_t* e){ (void)e; /* legacy, not used */ } +void btn_prev_cb(lv_event_t* e){ (void)e; /* legacy */ } +void btn_next_cb(lv_event_t* e){ (void)e; /* legacy */ } diff --git a/Apps/DiscoveryMountain/main/Source/ui.h b/Apps/DiscoveryMountain/main/Source/ui.h new file mode 100644 index 0000000..59693d6 --- /dev/null +++ b/Apps/DiscoveryMountain/main/Source/ui.h @@ -0,0 +1,28 @@ +#pragma once + +#include "app_context.h" + +#ifdef __cplusplus +extern "C" { +#endif + +void build_ui(lv_obj_t* parent); +void ui_timer(lv_timer_t* t); + +void btn_play_cb(lv_event_t* e); +void btn_prev_cb(lv_event_t* e); +void btn_next_cb(lv_event_t* e); +void btn_seasons_cb(lv_event_t* e); +void btn_dl_season_cb(lv_event_t* e); +void close_overlay(void); +void dropdown_cb(lv_event_t* e); + +// New library UI helpers +void ui_rebuild_episode_list(void); +void ui_update_header_locked(void); +void ui_update_status_locked(const char* msg); +void ui_deinit(void); + +#ifdef __cplusplus +} +#endif diff --git a/Apps/DiscoveryMountain/manifest.properties b/Apps/DiscoveryMountain/manifest.properties new file mode 100644 index 0000000..fb1bd76 --- /dev/null +++ b/Apps/DiscoveryMountain/manifest.properties @@ -0,0 +1,7 @@ +manifest.version=0.2 +target.sdk=0.8.0-dev +target.platforms=esp32s3 +app.id=one.tactility.discoverymountain +app.name=Discovery Mountain +app.version.name=0.1.0 +app.version.code=1 diff --git a/Apps/Mp3Player/main/CMakeLists.txt b/Apps/Mp3Player/main/CMakeLists.txt index db2068e..f97fa64 100644 --- a/Apps/Mp3Player/main/CMakeLists.txt +++ b/Apps/Mp3Player/main/CMakeLists.txt @@ -2,5 +2,5 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c) idf_component_register( SRCS ${SOURCE_FILES} - REQUIRES TactilitySDK + REQUIRES TactilitySDK esp_http_client ) diff --git a/Apps/Mp3Player/main/Source/main.c b/Apps/Mp3Player/main/Source/main.c index 3998441..466167e 100644 --- a/Apps/Mp3Player/main/Source/main.c +++ b/Apps/Mp3Player/main/Source/main.c @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -9,6 +10,8 @@ #include #include #include +#include +#include #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -32,6 +35,7 @@ typedef struct { AudioStreamHandle stream_handle; char filepath[512]; PlaybackState state; + AppHandle app_handle; // UI elements lv_obj_t* lbl_title; @@ -55,6 +59,11 @@ typedef struct { // Progress calculation size_t file_size; size_t bytes_read_total; + int pos_sec; + int total_sec; + long total_decoded_samples; + int start_pos_sec; // from bundle + bool finished_naturally; TaskHandle_t playback_task_handle; } AppCtx; @@ -115,6 +124,33 @@ static void mp3_playback_task(void* arg) { ctx->file_size = ftell(ctx->file); fseek(ctx->file, 0, SEEK_SET); ctx->bytes_read_total = 0; + ctx->pos_sec = 0; + ctx->total_sec = 0; + ctx->total_decoded_samples = 0; + ctx->finished_naturally = false; + + // Handle ID3 and resume seek (like DiscoveryMountain) + uint8_t id3hdr[10]; + long id3_skip = 0; + if (fread(id3hdr,1,10,ctx->file)==10 && memcmp(id3hdr,"ID3",3)==0) { + int id3size = (id3hdr[6]&0x7F)<<21 | (id3hdr[7]&0x7F)<<14 | (id3hdr[8]&0x7F)<<7 | (id3hdr[9]&0x7F); + id3_skip = id3size + 10; + fseek(ctx->file, id3_skip, SEEK_SET); + } else { + fseek(ctx->file, 0, SEEK_SET); + } + long file_data_size = ctx->file_size - id3_skip; + if (file_data_size>0) ctx->total_sec = (int)(file_data_size / 16000); // rough estimate + + if (ctx->start_pos_sec > 5) { + long seek_bytes = (long)ctx->start_pos_sec * 16000; + if (seek_bytes < file_data_size) { + fseek(ctx->file, id3_skip + seek_bytes, SEEK_SET); + ctx->total_decoded_samples = (long)ctx->start_pos_sec * 16000; + ctx->pos_sec = ctx->start_pos_sec; + ESP_LOGI(TAG, "Resuming %s from %d sec", ctx->filepath, ctx->start_pos_sec); + } + } mp3dec_init(&ctx->decoder); ctx->buffered_bytes = 0; @@ -123,7 +159,7 @@ static void mp3_playback_task(void* arg) { ctx->channels = 0; ctx->stream_handle = NULL; - ESP_LOGI(TAG, "Starting MP3 playback: %s (size: %d bytes)", ctx->filepath, ctx->file_size); + ESP_LOGI(TAG, "Starting MP3 playback: %s (size: %d bytes) resume=%d", ctx->filepath, ctx->file_size, ctx->start_pos_sec); while (ctx->state != STATE_IDLE) { if (ctx->state == STATE_PAUSED) { @@ -174,6 +210,15 @@ static void mp3_playback_task(void* arg) { memmove(ctx->input_buf, ctx->input_buf + consumed, ctx->buffered_bytes); if (samples > 0) { + ctx->total_decoded_samples += samples; + if (info.hz > 0) { + ctx->pos_sec = ctx->total_decoded_samples / info.hz; + // Update total_sec estimate if needed + if (ctx->total_sec == 0 && ctx->file_size > 0) { + // rough estimate already done, but keep + } + } + // Configure audio-stream if format changed if (ctx->sample_rate != info.hz || ctx->channels != info.channels) { if (ctx->stream_handle != NULL) { @@ -238,6 +283,14 @@ static void mp3_playback_task(void* arg) { fclose(ctx->file); ctx->file = NULL; + + if (ctx->pos_sec > 0 && ctx->total_sec >0 && ctx->pos_sec >= ctx->total_sec - 10) { + // Finished naturally, clear resume + ctx->pos_sec = 0; + ctx->finished_naturally = true; + } else { + ctx->finished_naturally = false; + } // Close audio stream to clean up DMA / resampler task if (ctx->stream_handle != NULL) { @@ -245,7 +298,7 @@ static void mp3_playback_task(void* arg) { ctx->stream_handle = NULL; } - ESP_LOGI(TAG, "Playback task finished"); + ESP_LOGI(TAG, "Playback task finished pos=%d total=%d naturally=%d", ctx->pos_sec, ctx->total_sec, ctx->finished_naturally); tt_lvgl_lock(portMAX_DELAY); ctx->state = STATE_IDLE; @@ -286,32 +339,94 @@ static void on_stop_click(lv_event_t* e) { static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) { memset(&g_ctx, 0, sizeof(g_ctx)); g_ctx.volume = 80; + g_ctx.app_handle = app; // Allocate memory buffers g_ctx.input_buf = (uint8_t*)malloc(MP3_INPUT_BUFFER_SIZE); g_ctx.pcm_buf = (mp3d_sample_t*)malloc(MINIMP3_MAX_SAMPLES_PER_FRAME * sizeof(mp3d_sample_t)); - // Find audio-stream device (resampling layer over ES8311 codec) - g_ctx.stream_dev = device_find_by_name("audio-stream"); + // Find audio-stream device with retry (seen "not found" when launched from DM) + for (int tries=0; tries<5; tries++) { + struct Device* dev = device_find_by_name("audio-stream"); + if (dev) { g_ctx.stream_dev = dev; break; } + dev = device_find_first_by_type(&AUDIO_STREAM_TYPE); + if (dev) { g_ctx.stream_dev = dev; break; } + ESP_LOGW(TAG, "Audio-stream not found, retry %d/5", tries); + vTaskDelay(pdMS_TO_TICKS(200)); + } if (!g_ctx.stream_dev) { - ESP_LOGE(TAG, "Audio-stream device not found!"); + ESP_LOGE(TAG, "Audio-stream device not found after retries!"); + } else { + ESP_LOGI(TAG, "Found audio-stream device %p", g_ctx.stream_dev); } // Parse launch parameters BundleHandle bundle = tt_app_get_parameters(app); + bool has_file_param = false; if (bundle) { - char file_param[256] = {0}; - if (tt_bundle_opt_string(bundle, "file", file_param, sizeof(file_param))) { + char file_param[512] = {0}; + if (tt_bundle_opt_string(bundle, "file", file_param, sizeof(file_param)) || + tt_bundle_opt_string(bundle, "path", file_param, sizeof(file_param)) || + tt_bundle_opt_string(bundle, "filepath", file_param, sizeof(file_param))) { // Safely resolve SD card path if (strncmp(file_param, "/sdcard", 7) == 0) { strncpy(g_ctx.filepath, file_param, sizeof(g_ctx.filepath) - 1); + g_ctx.filepath[sizeof(g_ctx.filepath)-1]=0; } else { if (file_param[0] == '/') { - snprintf(g_ctx.filepath, sizeof(g_ctx.filepath), "/sdcard%s", file_param); + size_t needed = 7 + strlen(file_param) + 1; + if (needed <= sizeof(g_ctx.filepath)) { + strcpy(g_ctx.filepath, "/sdcard"); + strcat(g_ctx.filepath, file_param); + } else { + strncpy(g_ctx.filepath, file_param, sizeof(g_ctx.filepath)-1); + g_ctx.filepath[sizeof(g_ctx.filepath)-1]=0; + } } else { - snprintf(g_ctx.filepath, sizeof(g_ctx.filepath), "/sdcard/%s", file_param); + size_t needed = 8 + strlen(file_param) + 1; + if (needed <= sizeof(g_ctx.filepath)) { + strcpy(g_ctx.filepath, "/sdcard/"); + strcat(g_ctx.filepath, file_param); + } else { + strncpy(g_ctx.filepath, file_param, sizeof(g_ctx.filepath)-1); + g_ctx.filepath[sizeof(g_ctx.filepath)-1]=0; + } } } + has_file_param = true; + } + int32_t pos = 0; + if (tt_bundle_opt_int32(bundle, "pos_sec", &pos) || + tt_bundle_opt_int32(bundle, "position", &pos) || + tt_bundle_opt_int32(bundle, "timestamp", &pos)) { + g_ctx.start_pos_sec = pos; + } + int32_t vol = 0; + if (tt_bundle_opt_int32(bundle, "volume", &vol)) { + g_ctx.volume = vol; + } + } + + // For direct testing from Launcher without bundle (user request: run mp3 player with episode on sd) + // Use a known existing file on SD card if no file param provided + if (!has_file_param) { + // Try to find first mp3 in /sdcard/dm/ + // For quick test, use a hardcoded existing file from logs + const char *test_files[] = { + "/sdcard/dm/462_Gadgets_Escape_S01E02.mp3", + "/sdcard/dm/463_Discovering_Discovery_Mountain_S01E01.mp3", + "/sdcard/download/test2.mp3", + "/sdcard/download/test.mp3" + }; + for (int i=0; i<4; i++) { + struct stat st; + if (stat(test_files[i], &st)==0 && st.st_size>1024) { + strncpy(g_ctx.filepath, test_files[i], sizeof(g_ctx.filepath)-1); + g_ctx.filepath[sizeof(g_ctx.filepath)-1]=0; + ESP_LOGI(TAG, "No file param, using test file %s", g_ctx.filepath); + has_file_param = true; + break; + } } } @@ -414,21 +529,36 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) { } static void onHideApp(AppHandle app, void* data) { + (void)app; (void)data; + ESP_LOGI(TAG, "onHide start pos_sec=%d", g_ctx.pos_sec); + if (g_ctx.state != STATE_IDLE) { - g_ctx.state = STATE_IDLE; // Signal task to stop + g_ctx.state = STATE_IDLE; } - // Wait for the task to finish self-deletion to prevent resource leaks - while (g_ctx.playback_task_handle != NULL) { - vTaskDelay(pdMS_TO_TICKS(10)); + int tries=0; + while (g_ctx.playback_task_handle != NULL && tries<50) { + vTaskDelay(pdMS_TO_TICKS(50)); + tries++; + if (tries%10==0) { + ESP_LOGI(TAG, "onHide waiting for playback task %d/50", tries); + } } - - // Close any open audio stream + if (g_ctx.playback_task_handle != NULL) { + ESP_LOGW(TAG, "Playback task stuck, force deleting"); + vTaskDelete(g_ctx.playback_task_handle); + g_ctx.playback_task_handle = NULL; + } + + ESP_LOGI(TAG, "Playback task stopped, closing audio stream"); + + // Close audio stream BEFORE allocating result bundle to free internal heap (seen crash on close) if (g_ctx.stream_handle != NULL) { audio_stream_close(g_ctx.stream_handle); g_ctx.stream_handle = NULL; + vTaskDelay(pdMS_TO_TICKS(100)); // let DMA free } - + if (g_ctx.input_buf) { free(g_ctx.input_buf); g_ctx.input_buf = NULL; @@ -437,6 +567,29 @@ static void onHideApp(AppHandle app, void* data) { free(g_ctx.pcm_buf); g_ctx.pcm_buf = NULL; } + + size_t free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); + ESP_LOGI(TAG, "Heap after free buffers: internal=%d", free_internal); + + // Return timestamp result - minimal to avoid OOM and timeout + // Only allocate bundle if heap > 8KB, and only put 1-2 ints + if (free_internal > 6000) { + BundleHandle result = tt_bundle_alloc(); + if (result) { + tt_bundle_put_int32(result, "pos_sec", g_ctx.pos_sec); + tt_bundle_put_int32(result, "timestamp", g_ctx.pos_sec); + tt_app_set_result(g_ctx.app_handle, APP_RESULT_OK, result); + ESP_LOGI(TAG, "Set result bundle pos_sec=%d", g_ctx.pos_sec); + } else { + ESP_LOGW(TAG, "Bundle alloc failed, returning OK without data"); + tt_app_set_result(g_ctx.app_handle, APP_RESULT_OK, NULL); + } + } else { + ESP_LOGW(TAG, "Low heap %d, returning OK without bundle", free_internal); + tt_app_set_result(g_ctx.app_handle, APP_RESULT_OK, NULL); + } + + ESP_LOGI(TAG, "onHide returning pos_sec=%d total_sec=%d finished=%d", g_ctx.pos_sec, g_ctx.total_sec, g_ctx.finished_naturally); } int main(int argc, char* argv[]) { diff --git a/Apps/SdDownloader/CMakeLists.txt b/Apps/SdDownloader/CMakeLists.txt new file mode 100644 index 0000000..d15cc14 --- /dev/null +++ b/Apps/SdDownloader/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.20) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +if (DEFINED ENV{TACTILITY_SDK_PATH}) + set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH}) +else() + set(TACTILITY_SDK_PATH "../../release/TactilitySDK") + message(WARNING "TACTILITY_SDK_PATH not set, defaulting to ${TACTILITY_SDK_PATH}") +endif() +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") +set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH}) +project(SdDownloader) +tactility_project(SdDownloader) diff --git a/Apps/SdDownloader/main/CMakeLists.txt b/Apps/SdDownloader/main/CMakeLists.txt new file mode 100644 index 0000000..9255053 --- /dev/null +++ b/Apps/SdDownloader/main/CMakeLists.txt @@ -0,0 +1,6 @@ +file(GLOB_RECURSE SOURCE_FILES Source/*.c) +idf_component_register( + SRCS ${SOURCE_FILES} + INCLUDE_DIRS Source + REQUIRES TactilitySDK esp_http_client +) diff --git a/Apps/SdDownloader/main/Source/main.c b/Apps/SdDownloader/main/Source/main.c new file mode 100644 index 0000000..2206847 --- /dev/null +++ b/Apps/SdDownloader/main/Source/main.c @@ -0,0 +1,451 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "esp_http_client.h" +#include "esp_heap_caps.h" + +#define TAG "SDDL" +#define DEFAULT_URL "http://192.168.68.110:8098/463_Discovering_Discovery_Mountain_S01E01/discovery_mountain_s01e01_esp32_24k.mp3" +#define DEFAULT_PATH "/sdcard/download/test2.mp3" + +typedef struct { + lv_obj_t *bar; + lv_obj_t *lbl_url; + lv_obj_t *lbl_path; + lv_obj_t *lbl_pct; + lv_obj_t *lbl_detail; + lv_obj_t *lbl_status; + lv_obj_t *btn_cancel; + + char url[512]; + char path[512]; + bool override_existing; + AppHandle app_handle; + + TaskHandle_t dl_handle; + volatile bool downloading; + volatile bool cancel_req; + volatile int total; + volatile int expected; + int last_pct; + bool result_success; +} AppCtx; + +static AppCtx G; + +static void ensure_parent_dir(const char *filepath) { + if (!filepath) return; + char path_copy[512]; + strncpy(path_copy, filepath, sizeof(path_copy)-1); + path_copy[sizeof(path_copy)-1]=0; + char *last_slash = strrchr(path_copy, '/'); + if (!last_slash) return; + *last_slash = 0; + if (strlen(path_copy)==0) return; + + char cur[512] = ""; + if (path_copy[0]=='/') strcpy(cur, "/"); + + char *start = path_copy[0]=='/' ? path_copy+1 : path_copy; + int len = strlen(start); + int i = 0; + while (i <= len) { + int j = i; + while (j < len && start[j] != '/') j++; + if (j > i) { + size_t cur_len = strlen(cur); + if (cur_len > 0 && cur[cur_len-1] != '/') { + if (cur_len + 1 < sizeof(cur)) { + cur[cur_len] = '/'; + cur[cur_len+1] = 0; + } + } + char saved = start[j]; + start[j] = 0; + strncat(cur, start+i, sizeof(cur)-strlen(cur)-1); + start[j] = saved; + mkdir(cur, 0777); + } + i = j+1; + } +} + +static void update_ui_status(const char *txt) { + tt_lvgl_lock(portMAX_DELAY); + if (G.lbl_status) lv_label_set_text(G.lbl_status, txt); + tt_lvgl_unlock(); +} + +static void set_result_and_close(bool success) { + G.result_success = success; + BundleHandle result_bundle = tt_bundle_alloc(); + if (result_bundle) { + tt_bundle_put_bool(result_bundle, "success", success); + tt_bundle_put_string(result_bundle, "path", G.path); + tt_bundle_put_int32(result_bundle, "bytes", G.total); + if (!success) { + tt_lvgl_lock(portMAX_DELAY); + const char *status = G.lbl_status ? lv_label_get_text(G.lbl_status) : "failed"; + char status_copy[128]; + strncpy(status_copy, status, sizeof(status_copy)-1); + status_copy[sizeof(status_copy)-1]=0; + tt_lvgl_unlock(); + tt_bundle_put_string(result_bundle, "error", status_copy); + } + tt_app_set_result(G.app_handle, success ? APP_RESULT_OK : APP_RESULT_ERROR, result_bundle); + } else { + tt_app_set_result(G.app_handle, success ? APP_RESULT_OK : APP_RESULT_ERROR, NULL); + } +} + +static bool download_one_file(const char *url, const char *path) { + ESP_LOGI(TAG, "DL one file url=%s path=%s override=%d", url, path, G.override_existing); + + struct stat st; + if (stat(path, &st)==0) { + if (!G.override_existing) { + ESP_LOGW(TAG, "File exists and override=false: %s", path); + update_ui_status("File exists"); + return false; + } else { + ESP_LOGI(TAG, "File exists but override=true, overwriting %s", path); + unlink(path); + } + } + + ensure_parent_dir(path); + + size_t free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); + ESP_LOGI(TAG, "Heap before DL: internal=%d", free_internal); + if (free_internal < 10000) { + vTaskDelay(pdMS_TO_TICKS(1000)); + } + + if (strlen(url) < 8 || strncmp(url, "http://", 7)!=0) { + update_ui_status("Invalid URL"); + return false; + } + + char tmp_path[520]; + snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path); + unlink(tmp_path); + FILE *f = fopen(tmp_path, "wb"); + if (!f) { + ESP_LOGE(TAG, "fopen fail %s errno=%d", tmp_path, errno); + update_ui_status("Failed to open file"); + return false; + } + + esp_http_client_config_t cfg = { + .url = url, + .timeout_ms = 15000, + .buffer_size = 4096, + .buffer_size_tx = 1024, + .keep_alive_enable = false, + }; + esp_http_client_handle_t client = esp_http_client_init(&cfg); + if (!client) { + fclose(f); + unlink(tmp_path); + update_ui_status("HTTP init failed"); + return false; + } + + esp_err_t err = esp_http_client_open(client, 0); + if (err != ESP_OK) { + esp_http_client_cleanup(client); + fclose(f); + unlink(tmp_path); + update_ui_status("HTTP open failed"); + return false; + } + + int content_len = esp_http_client_fetch_headers(client); + int status = esp_http_client_get_status_code(client); + ESP_LOGI(TAG, "HTTP status %d, content_len %d", status, content_len); + if (status != 200) { + char msg[64]; + snprintf(msg, sizeof(msg), "HTTP %d", status); + update_ui_status(msg); + esp_http_client_close(client); + esp_http_client_cleanup(client); + fclose(f); + unlink(tmp_path); + return false; + } + + G.expected = content_len; + + uint8_t *buf = heap_caps_malloc(4096, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (!buf) { + buf = malloc(4096); + if (!buf) { + esp_http_client_close(client); + esp_http_client_cleanup(client); + fclose(f); + unlink(tmp_path); + update_ui_status("No memory"); + return false; + } + } + + int total = 0; + int timeouts = 0; + bool ok = true; + + while (1) { + if (G.cancel_req) { ok = false; break; } + if (content_len >=0 && total >= content_len) break; + + int r = esp_http_client_read(client, (char*)buf, 4096); + if (r < 0) { + timeouts++; + if (timeouts >= 5) { ok = false; break; } + vTaskDelay(pdMS_TO_TICKS(200)); + continue; + } + if (r == 0) break; + timeouts = 0; + if (fwrite(buf, 1, r, f) != (size_t)r) { ok = false; break; } + total += r; + G.total = total; + + int pct = 0; + if (content_len > 0) { + pct = (int)((int64_t)total * 100 / content_len); + if (pct<0) pct=0; + if (pct>100) pct=100; + } + if (pct != G.last_pct) { + G.last_pct = pct; + tt_lvgl_lock(portMAX_DELAY); + if (G.bar) lv_bar_set_value(G.bar, pct, LV_ANIM_OFF); + if (G.lbl_detail) { + char d[64]; + if (content_len>0) snprintf(d,sizeof(d),"%d / %d KB", total/1024, content_len/1024); + else snprintf(d,sizeof(d),"%d KB", total/1024); + lv_label_set_text(G.lbl_detail, d); + } + if (G.lbl_pct) { + char b[32]; + if (content_len>0) snprintf(b,sizeof(b),"%d%%",pct); + else snprintf(b,sizeof(b),"%d KB", total/1024); + lv_label_set_text(G.lbl_pct, b); + } + tt_lvgl_unlock(); + } + + if ((total / 4096) % 8 == 0) { + fflush(f); + vTaskDelay(pdMS_TO_TICKS(10)); + } + } + + heap_caps_free(buf); + esp_http_client_close(client); + esp_http_client_cleanup(client); + fclose(f); + + if (G.cancel_req) { unlink(tmp_path); update_ui_status("Canceled"); return false; } + if (!ok || total < 512) { unlink(tmp_path); update_ui_status("Failed – error"); return false; } + if (content_len >=0 && total < content_len * 95 / 100) { unlink(tmp_path); update_ui_status("Failed – incomplete"); return false; } + if (rename(tmp_path, path) != 0) { unlink(tmp_path); update_ui_status("Rename failed"); return false; } + + return true; +} + +static void download_task(void *arg) { + (void)arg; + G.downloading = true; + G.cancel_req = false; + G.last_pct = -1; + + char url[512]; + char path[512]; + strncpy(url, G.url, sizeof(url)-1); + strncpy(path, G.path, sizeof(path)-1); + url[sizeof(url)-1]=0; + path[sizeof(path)-1]=0; + + bool ok = download_one_file(url, path); + if (ok) { + char msg[64]; + snprintf(msg, sizeof(msg), "Done! %d KB", G.total/1024); + update_ui_status(msg); + } + + G.downloading = false; + G.dl_handle = NULL; + set_result_and_close(ok); + vTaskDelete(NULL); +} + +static void btn_cancel_cb(lv_event_t *e) { + (void)e; + if (!G.downloading) { tt_app_stop(); return; } + G.cancel_req = true; + tt_lvgl_lock(portMAX_DELAY); + if (G.lbl_status) lv_label_set_text(G.lbl_status, "Canceling..."); + tt_lvgl_unlock(); +} + +static void build_ui(void) { + tt_lvgl_lock(portMAX_DELAY); + + lv_obj_t *root = lv_obj_create(lv_scr_act()); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_style_pad_all(root, 8, 0); + lv_obj_set_style_bg_color(root, lv_color_hex(0x111111), 0); + lv_obj_set_style_border_width(root, 0, 0); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(root, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_row(root, 6, 0); + + lv_obj_t *title = lv_label_create(root); + lv_label_set_text(title, "Downloading..."); + lv_obj_set_style_text_font(title, lvgl_get_text_font(FONT_SIZE_LARGE), 0); + lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0); + + G.lbl_url = lv_label_create(root); + lv_label_set_text(G.lbl_url, G.url); + lv_obj_set_width(G.lbl_url, LV_PCT(100)); + lv_label_set_long_mode(G.lbl_url, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_font(G.lbl_url, lvgl_get_text_font(FONT_SIZE_SMALL), 0); + lv_obj_set_style_text_color(G.lbl_url, lv_color_hex(0x888888), 0); + + G.lbl_path = lv_label_create(root); + lv_label_set_text(G.lbl_path, G.path); + lv_obj_set_width(G.lbl_path, LV_PCT(100)); + lv_label_set_long_mode(G.lbl_path, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_font(G.lbl_path, lvgl_get_text_font(FONT_SIZE_SMALL), 0); + lv_obj_set_style_text_color(G.lbl_path, lv_color_hex(0xAAAAAA), 0); + + G.bar = lv_bar_create(root); + lv_obj_set_size(G.bar, LV_PCT(100), 14); + lv_bar_set_range(G.bar, 0, 100); + lv_bar_set_value(G.bar, 0, LV_ANIM_OFF); + lv_obj_set_style_bg_color(G.bar, lv_color_hex(0x333333), LV_PART_MAIN); + lv_obj_set_style_bg_color(G.bar, lv_color_hex(0x2E7D32), LV_PART_INDICATOR); + + lv_obj_t *row = lv_obj_create(root); + lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(row, 0, 0); + lv_obj_set_style_pad_all(row, 0, 0); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + G.lbl_pct = lv_label_create(row); + lv_label_set_text(G.lbl_pct, "0%"); + lv_obj_set_style_text_font(G.lbl_pct, lvgl_get_text_font(FONT_SIZE_DEFAULT), 0); + + G.lbl_detail = lv_label_create(row); + lv_label_set_text(G.lbl_detail, "-"); + lv_obj_set_style_text_font(G.lbl_detail, lvgl_get_text_font(FONT_SIZE_SMALL), 0); + lv_obj_set_style_text_color(G.lbl_detail, lv_color_hex(0x888888), 0); + + G.lbl_status = lv_label_create(root); + lv_label_set_text(G.lbl_status, "Starting..."); + lv_obj_set_width(G.lbl_status, LV_PCT(100)); + lv_label_set_long_mode(G.lbl_status, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_color(G.lbl_status, lv_color_hex(0xCCCCCC), 0); + + G.btn_cancel = lv_button_create(root); + lv_obj_set_size(G.btn_cancel, 100, 40); + lv_obj_set_style_bg_color(G.btn_cancel, lv_color_hex(0x444444), 0); + lv_obj_t *lbl = lv_label_create(G.btn_cancel); + lv_label_set_text(lbl, "Cancel"); + lv_obj_center(lbl); + lv_obj_add_event_cb(G.btn_cancel, btn_cancel_cb, LV_EVENT_CLICKED, NULL); + + tt_lvgl_unlock(); +} + +static void onShow(AppHandle app, void* data, lv_obj_t* parent) { + (void)data; (void)parent; + memset(&G, 0, sizeof(G)); + G.app_handle = app; + G.last_pct = -1; + + BundleHandle bundle = tt_app_get_parameters(app); + bool has_url = false; + bool has_path = false; + + if (bundle) { + char url_buf[512] = {0}; + char path_buf[512] = {0}; + bool override_val = false; + + if (tt_bundle_opt_string(bundle, "url", url_buf, sizeof(url_buf)) || + tt_bundle_opt_string(bundle, "file", url_buf, sizeof(url_buf))) { + strncpy(G.url, url_buf, sizeof(G.url)-1); + has_url = true; + } + if (tt_bundle_opt_string(bundle, "path", path_buf, sizeof(path_buf)) || + tt_bundle_opt_string(bundle, "dest", path_buf, sizeof(path_buf)) || + tt_bundle_opt_string(bundle, "file_dest", path_buf, sizeof(path_buf))) { + strncpy(G.path, path_buf, sizeof(G.path)-1); + has_path = true; + } + if (tt_bundle_opt_bool(bundle, "override", &override_val)) { + G.override_existing = override_val; + } else { + bool ow2 = false; + if (tt_bundle_opt_bool(bundle, "overwrite", &ow2)) G.override_existing = ow2; + } + } + + if (!has_url) strncpy(G.url, DEFAULT_URL, sizeof(G.url)-1); + if (!has_path) strncpy(G.path, DEFAULT_PATH, sizeof(G.path)-1); + + mkdir("/sdcard", 0777); + mkdir("/sdcard/download", 0777); + mkdir("/sdcard/dm", 0777); + + build_ui(); + + bool should_autostart = has_url && has_path; + if (!has_url && !has_path) { + should_autostart = true; + tt_lvgl_lock(portMAX_DELAY); + if (G.lbl_status) lv_label_set_text(G.lbl_status, "Demo mode – no args, using defaults"); + tt_lvgl_unlock(); + } + + ESP_LOGI(TAG, "Autostart=%d url=%s path=%s override=%d", should_autostart, G.url, G.path, G.override_existing); + + if (should_autostart) { + BaseType_t res = xTaskCreate(download_task, "sd_dl", 8192, NULL, 5, &G.dl_handle); + ESP_LOGI(TAG, "xTaskCreate res=%d handle=%p", res, G.dl_handle); + if (res != pdPASS) update_ui_status("Failed to create DL task"); + } else { + update_ui_status("Missing url/path args"); + } +} + +static void onHide(AppHandle app, void* data) { + (void)app; (void)data; + if (G.dl_handle) { + G.cancel_req = true; + for (int i=0;i<20 && G.dl_handle!=NULL; i++) vTaskDelay(pdMS_TO_TICKS(100)); + if (G.dl_handle) { vTaskDelete(G.dl_handle); G.dl_handle = NULL; G.downloading = false; } + } +} + +int main(int argc, char* argv[]) { + (void)argc; (void)argv; + tt_app_register((AppRegistration){ .onShow = onShow, .onHide = onHide }); + return 0; +} diff --git a/Apps/SdDownloader/manifest.properties b/Apps/SdDownloader/manifest.properties new file mode 100644 index 0000000..ee9d8b7 --- /dev/null +++ b/Apps/SdDownloader/manifest.properties @@ -0,0 +1,7 @@ +manifest.version=0.2 +target.sdk=0.8.0-dev +target.platforms=esp32s3 +app.id=one.tactility.sddownloader +app.name=SD Downloader +app.version.name=1.0.0 +app.version.code=1