diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20d69cc..079952b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v4 - name: Build and run the host tests run: | - cc -Wall -Wextra -Werror -I src tests/test_data.c src/data/kg_*.c -lm -o kg_test + cc -Wall -Wextra -Werror -I src tests/test_data.c src/data/kg_*.c src/third_party/tjpgd/tjpgd.c -lm -o kg_test ./kg_test tests/data/openmeteo-montreal.json simulator: @@ -40,7 +40,7 @@ jobs: KG_TIME=10:09:30 KG_FACE=$face sim/build/kronograf_sim --shot shots/face$face.png done for app in launcher chrono timer alarms steps weather settings notifs world sky level torch remote system \ - net net:wifi net:link net:ping net:trace net:dns net:ports net:lan net:speed net:ble net:cidr; do + radio video net net:wifi net:link net:ping net:trace net:dns net:ports net:lan net:speed net:ble net:cidr; do KG_TIME=10:09:30 KG_APP=$app sim/build/kronograf_sim --shot shots/${app/:/-}.png done - uses: actions/upload-artifact@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6de30b3..a45e43f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,8 @@ name: Release # Pushing a tag such as v0.5.0 builds the firmware on GitHub — where include/secrets.h does # not exist, so the binaries carry no Wi-Fi credentials or API key — and publishes it. +# A tag with a suffix (v0.7.0-beta.1, v0.7.0-rc.1) is published as a pre-release: never +# "Latest", for testing builds from a feature branch before they reach main. on: push: tags: ["v*"] @@ -40,7 +42,16 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - cat > notes.md < [!NOTE]" > notes.md + echo "> **Pre-release** for testing, built from a development branch: it may still contain bugs." >> notes.md + echo "" >> notes.md + else + : > notes.md + fi + cat >> notes.md <\`. EOF - gh release create "$GITHUB_REF_NAME" dist/* --title "Kronograf $GITHUB_REF_NAME" --notes-file notes.md + gh release create "$GITHUB_REF_NAME" dist/* --title "Kronograf $GITHUB_REF_NAME" --notes-file notes.md $PRE diff --git a/DECISIONS.md b/DECISIONS.md index a802213..70e6844 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -134,3 +134,11 @@ Each entry: decision — rationale. Most recent at the bottom. **D55. Offline astronomy and time zones.** The world clock evaluates POSIX TZ rules itself (`kg_tz.c`) instead of switching libc's TZ, which would disturb the watch's own local time; it matches the C library on twelve zones every 37 minutes over two years. Sunrise, sunset and civil twilight use NOAA's sunrise equation (±1–2 min); the Moon's phase uses the elongation with Meeus' main periodic terms, within 20 minutes of the 2024 and 2025 eclipses. All at the home location of `kg_weather.h`. **D56. Not yet verified on the watch** (built and checked in the simulator only, the watch being away): IR emission (the LED can be seen through a phone camera), accelerometer axes and signs in the Level app (`accel` console command), and the AXP202 charge-current reading (1 A shown while charging looks too high). **Verified on 2026-10-14** by OTA: boot of 0.6.0, ping (0 % loss), ARP sweep (23 devices), BLE scan (22 devices), speed test (D52), internal RAM 32 % free with Wi-Fi and BLE on. + +## Stage 9 — 2026-10-14: radio and video, development workflow + +**D57. Feature branches and pre-releases.** `main` only receives work that was tested on the watch. A feature lives on a branch with a pull request (CI builds and tests it); a tag with a suffix (`v0.7.0-beta.1`) makes `release.yml` publish a GitHub pre-release from that branch — still built by GitHub Actions, without secrets (D48), never marked "Latest". After validation the pull request is merged and a plain `vX.Y.Z` tag publishes the release. This stage is the first one to follow it (`feature/media`). + +**D58. Web radio: MP3 over plain HTTP, decoded on the watch.** minimp3 (CC0, vendored) decodes in a task on core 0 (priority 3), fed by a 96 KB ring buffer in PSRAM that a network task (priority 2) fills; playback starts after 32 KB (~2 s) and rebuffers after an underrun. The decoder's 16 KB scratch and its buffers come from PSRAM for the session only (patch `MINIMP3_SCRATCH`, D40's internal-RAM budget). Output: mono mix, quadratic volume, I²S at the stream's rate to the MAX98357A; tones are dropped while the radio owns the speaker. ICY metadata gives the song title (UTF-8 or Latin-1). The radio keeps Wi-Fi on with the screen off (`net_keep`), at the cost of light sleep. Stations were checked to serve MP3 over plain HTTP — HTTPS would cost two 16 KB TLS buffers and HLS/AAC are out of reach; redirects are followed (Radio Swiss Classic has two). The same loops run in the simulator (`KG_NET_REAL=1`), through SDL audio: Radio Paradise, BBC World Service, KEXP, FIP and Radio Swiss Classic play without underruns. **Measured on the watch (beta.1 → beta.2):** plays without underruns, Bluetooth on, titles shown (accents included). Stack high-water marks led to a 7 KB network task (it peaked at 5.9 of 6 KB with DNS and redirects; its 1.5 KB staging buffer moved into the session, in PSRAM) and a 3.5 KB decoder task (1.5 KB used); 6 I²S DMA buffers. Internal RAM while playing: ~24 % free (57 KB, low-water 36 KB) — below the 30 % target, the rest going to the Wi-Fi / lwIP buffers of a continuous stream; back to 31 % once stopped. OTA during playback fails for lack of memory: stop the radio first. + +**D59. Video: MJPEG only.** H.264 is beyond the ESP32 without a hardware decoder; MJPEG (or a JPEG snapshot URL, polled every second) is what small cameras serve. JPEG images are cut out of the byte stream by their SOI / EOI markers (multipart headers ignored), decoded by TJpgDec (vendored, RGB565, scale 1/1–1/8 so the image is at most 4/3 of the screen, then centred and cropped), into three 240 × 240 frames in PSRAM: the decoder never writes the frame shown or the one LVGL holds. The video only runs while its app is on screen, under the toolbox's Wi-Fi lease; the screen stays on for 10 minutes at most. A test source: `tools/mjpeg_test_server.py` (ffmpeg); 15 fps at 320 × 240 decoded in the simulator. **Measured on the watch:** 5–6 fps at 320 × 240 with Bluetooth on (~400 kbit/s, the network ceiling of D52), colours right, internal RAM 27 % free while playing. The test server is `tools/mjpeg_test_server.py` (multi-client; ffmpeg's built-in server only takes one client at a time and stayed stuck on a stale one). diff --git a/README.md b/README.md index 9f8e3f2..a854857 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![CI](https://github.com/storagebits/kronograf/actions/workflows/ci.yml/badge.svg)](https://github.com/storagebits/kronograf/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -**A from-scratch firmware for the LilyGO T-Watch 2020**: an aviation chronograph face, "time circuits" 7-segment displays straight out of *Back to the Future*, weather and real-time bus departures, iPhone notifications, a network engineer's toolbox on the wrist (Wi-Fi analyzer, ping, traceroute, LAN scan, speed test…), a TV remote, a world clock and a sun & moon almanac. ESP32, LVGL 8.3, and a macOS simulator that builds exactly the same interface. +**A from-scratch firmware for the LilyGO T-Watch 2020**: an aviation chronograph face, "time circuits" 7-segment displays straight out of *Back to the Future*, weather and real-time bus departures, iPhone notifications, a network engineer's toolbox on the wrist (Wi-Fi analyzer, ping, traceroute, LAN scan, speed test…), web radio, an MJPEG camera viewer, a TV remote, a world clock and a sun & moon almanac. ESP32, LVGL 8.3, and a macOS simulator that builds exactly the same interface.

Demo: aviator face, time circuits with the lightning easter egg, night face, launcher, weather, an iPhone notification, then the network toolbox and its Wi-Fi analyzer @@ -16,7 +16,8 @@ | | | |---|---| | **Faces** | *Aviator chronograph* (smooth hands, battery / minutes / steps sub-dials, date window, weather complication, notification badge) · *Time circuits* (three 7-segment rows: next alarm, present time, last sync) · *Night* (time only, dark red, dimmed backlight). Swipe sideways between faces. | -| **Apps** | Stopwatch, timer, alarms, world clock, weather + buses, sun & moon, 7-day step history, level, torch, network toolbox, TV remote, system monitor, settings — a launcher of 3 × 3 pages drawn like sub-dials, swiped sideways. | +| **Apps** | Stopwatch, timer, alarms, world clock, weather + buses, sun & moon, 7-day step history, level, torch, network toolbox, radio, video, TV remote, system monitor, settings — a launcher of 3 × 3 pages drawn like sub-dials, swiped sideways. | +| **Radio & video** | *Radio*: MP3 web radio on the speaker (Radio Paradise, BBC World Service, KEXP, FIP, Radio Swiss Classic, or any http:// MP3 stream), song titles from the stream, volume, keeps playing with the screen off · *Video*: an MJPEG camera (ESP32-CAM, IP camera, 3D printer) or a JPEG snapshot URL, full screen at ~10–15 fps. Decoded on the watch (minimp3, TJpgDec). | | **Network toolbox** | *Wi-Fi analyzer* (2.4 GHz channel map, every network with channel, signal and security) · *Link* (SSID, BSSID, signal, address, gateway, DNS, MAC, public IP and provider) · *Ping* with an RTT history and loss · *Traceroute* (ICMP, TTL 1–24) · *DNS lookup* · *Port check* (16 common TCP services) · *LAN scan* (ARP sweep of the /24, private MACs spotted) · *Speed test* (10 s download) · *Bluetooth scan* (BLE devices and their makers) · *Subnet calculator* (offline). Wi-Fi only stays on while the toolbox is open. | | **World & sky** | Twelve cities from Honolulu to Auckland with summer time, day / night and day shift, plus Zulu time · sunrise, sunset, civil twilight, day length and its daily change, the Moon's phase drawn to scale and the next full moon — computed offline. | | **Hardware tools** | *TV remote* over the infrared LED (Samsung, LG, Sony, Philips; power, volume, channels, mute, input, and "all TVs") · *Level* from the accelerometer, zeroed on any surface · *Torch* in white or red at full backlight · *System*: battery voltage and current from the AXP202, its temperature, RAM and PSRAM, CPU, frame rate, uptime, reset cause. | @@ -47,6 +48,8 @@ cmake -S sim -B sim/build -DCMAKE_BUILD_TYPE=Release && cmake --build sim/build sim/build/kronograf_sim # ×3 window, the mouse is your finger KG_TIME=10:09:30 sim/build/kronograf_sim --shot shot.png # headless PNG screenshot KG_NET_REAL=1 KG_APP=net:trace sim/build/kronograf_sim # the toolbox, for real, from the Mac +KG_NET_REAL=1 KG_APP=radio sim/build/kronograf_sim # web radio through the Mac's speakers +tools/mjpeg_test_server.py # an MJPEG test stream for the Video app tools/make_screens.sh # screenshots + the overview sheet tools/make_demo_gif.sh # re-records the demo GIF (ffmpeg) ``` @@ -99,6 +102,7 @@ screenshot f LVGL capture → PNG lcdtest R | G | B test pat battlog battery-life log power current measured on USB, simulated battery weather [refresh] bus [refresh | stops | key ] ble [on | off | forget] net [ [target]] network toolbox: ping, trace, dns, ports, lan, speed, public, ble +radio [play | stop | vol | url ] video [play | stop | url ] sys system information accel accelerometer in g ir [ ] sends a TV code app net: opens a network tool sleep | wake | bl | timeout | face | app | timer | coredump | reboot @@ -109,7 +113,8 @@ sleep | wake | bl | timeout | face | app | timer | coredu ``` src/ui/ LVGL interface, identical on watch and simulator (faces, apps, theme, fonts) src/data/ plain C, host-tested: Open-Meteo, GTFS-RT (streamed protobuf), ANCS, network probes and - toolbox runners (BSD sockets), time zones, sun and moon, infrared codes + toolbox runners (BSD sockets), time zones, sun and moon, infrared codes, radio and video +src/third_party/ minimp3 (CC0) and TJpgDec (ChaN), vendored — see its README src/hal/hal.h the only hardware interface the UI sees src/hal/device/ T-Watch: display (esp_lcd, DMA), AXP202, BMA423, network, BLE, safeguards src/hal/sim/ simulated implementation @@ -138,14 +143,26 @@ Development went in stages, each one flashed and verified on the watch. - ✅ **5**: iPhone notifications. - 🧪 **6**: infrared TV remote — built and simulated, to be checked on the watch. - 🧪 **8**: network toolbox, world clock, sun & moon, level, torch, system monitor — built, simulated, network tools run for real from the simulator; on-watch check pending. +- 🧪 **9**: web radio and MJPEG video — on the `feature/media` branch, in pre-release. - 🔜 **7**: polish and a battery-life campaign. **Known limitations.** This board has no 32 kHz crystal, so light sleep is impossible while BLE is active: battery life with notifications is being measured (D41). The development unit's micro-USB connector is unreliable (B2), hence OTA updates and block flashing. +## Development workflow + +`main` only receives tested work. New features go on a branch (`feature/…`) with a pull request: CI builds and tests it, and a **pre-release** can be cut from the branch for testing on a watch — tag it `vX.Y.Z-beta.N` and GitHub Actions publishes it as a pre-release, never as "Latest". Once validated, the pull request is merged into `main` and a plain `vX.Y.Z` tag publishes the release. + +```sh +git switch -c feature/my-idea # work, commit +git push -u origin feature/my-idea # then open a pull request: CI runs on it +git tag -a v0.7.0-beta.1 -m "…" && git push origin v0.7.0-beta.1 # pre-release for testing +``` + ## Licence and credits Kronograf's code is released under the [MIT licence](LICENSE). Dependencies and assets: +- [minimp3](https://github.com/lieff/minimp3) (CC0) and [TJpgDec](http://elm-chan.org/fsw/tjpgd/) (ChaN's free licence), in `src/third_party/`. - [LVGL](https://lvgl.io) 8.3 (MIT), [NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) (Apache 2.0), [XPowersLib](https://github.com/lewisxhe/XPowersLib) and [SensorLib](https://github.com/lewisxhe/SensorLib) (MIT), [stb_image_write](https://github.com/nothings/stb) (public domain / MIT). - [Barlow Condensed](https://fonts.google.com/specimen/Barlow+Condensed) and [Allerta Stencil](https://fonts.google.com/specimen/Allerta+Stencil) fonts, under the SIL Open Font License (`assets/fonts/OFL-*.txt`). - [My-TTGO-Watch](https://github.com/sharandac/My-TTGO-Watch) and the LilyGO library served as references for the pinout and init sequences, without reusing their code (see `docs/AUDIT.md`). diff --git a/docs/screens/kronograf-overview.png b/docs/screens/kronograf-overview.png index cb2b10b..1bec136 100644 Binary files a/docs/screens/kronograf-overview.png and b/docs/screens/kronograf-overview.png differ diff --git a/docs/screens/sim/t2-face1.png b/docs/screens/sim/t2-face1.png index d631628..142b1cc 100644 Binary files a/docs/screens/sim/t2-face1.png and b/docs/screens/sim/t2-face1.png differ diff --git a/docs/screens/sim/t6-launcher-tools.png b/docs/screens/sim/t6-launcher-tools.png index 5a2ba96..f5dbb82 100644 Binary files a/docs/screens/sim/t6-launcher-tools.png and b/docs/screens/sim/t6-launcher-tools.png differ diff --git a/docs/screens/sim/t9-radio.png b/docs/screens/sim/t9-radio.png new file mode 100644 index 0000000..18b4364 Binary files /dev/null and b/docs/screens/sim/t9-radio.png differ diff --git a/docs/screens/sim/t9-video-setup.png b/docs/screens/sim/t9-video-setup.png new file mode 100644 index 0000000..b6ba503 Binary files /dev/null and b/docs/screens/sim/t9-video-setup.png differ diff --git a/docs/screens/sim/t9-video.png b/docs/screens/sim/t9-video.png new file mode 100644 index 0000000..f6a756f Binary files /dev/null and b/docs/screens/sim/t9-video.png differ diff --git a/include/kg_version.h b/include/kg_version.h index 0237492..d64a4f4 100644 --- a/include/kg_version.h +++ b/include/kg_version.h @@ -1,3 +1,3 @@ /* Firmware version, shown in System and on the console. Bumped with each release tag. */ #pragma once -#define KG_VERSION "0.6.1" +#define KG_VERSION "0.7.0-beta.2" diff --git a/sim/CMakeLists.txt b/sim/CMakeLists.txt index 1778b08..5b7e1ea 100644 --- a/sim/CMakeLists.txt +++ b/sim/CMakeLists.txt @@ -15,7 +15,8 @@ target_compile_options(lvgl PRIVATE -w) file(GLOB_RECURSE UI_SRC ${ROOT}/src/ui/*.c) file(GLOB DATA_SRC ${ROOT}/src/data/*.c) -add_executable(kronograf_sim main.c ${ROOT}/src/hal/sim/hal_sim.c ${ROOT}/src/hal/sim/hal_sim_net.c ${UI_SRC} ${DATA_SRC}) +add_executable(kronograf_sim main.c ${ROOT}/src/hal/sim/hal_sim.c ${ROOT}/src/hal/sim/hal_sim_net.c ${ROOT}/src/hal/sim/hal_sim_media.c + ${ROOT}/src/third_party/tjpgd/tjpgd.c ${UI_SRC} ${DATA_SRC}) target_compile_definitions(kronograf_sim PRIVATE KG_ROOT="${ROOT}") target_include_directories(kronograf_sim PRIVATE ${ROOT}/src ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(kronograf_sim PRIVATE ${ROOT}/include) @@ -25,4 +26,4 @@ if(UNIX AND NOT APPLE) endif() # Our code builds warning-free (generated fonts are excluded from -Werror). file(GLOB UI_OWN ${ROOT}/src/ui/*.c) -set_source_files_properties(main.c ${ROOT}/src/hal/sim/hal_sim.c ${ROOT}/src/hal/sim/hal_sim_net.c ${UI_OWN} ${DATA_SRC} PROPERTIES COMPILE_OPTIONS "-Wall;-Wextra;-Werror") +set_source_files_properties(main.c ${ROOT}/src/hal/sim/hal_sim.c ${ROOT}/src/hal/sim/hal_sim_net.c ${ROOT}/src/hal/sim/hal_sim_media.c ${UI_OWN} ${DATA_SRC} PROPERTIES COMPILE_OPTIONS "-Wall;-Wextra;-Werror") diff --git a/sim/demo-camera.jpg b/sim/demo-camera.jpg new file mode 100644 index 0000000..dc764e9 Binary files /dev/null and b/sim/demo-camera.jpg differ diff --git a/src/data/kg_media.c b/src/data/kg_media.c new file mode 100644 index 0000000..4cc74c3 --- /dev/null +++ b/src/data/kg_media.c @@ -0,0 +1,496 @@ +#include "kg_media.h" +#include "kg_ancs.h" +#include "kg_net.h" +#include "../third_party/tjpgd/tjpgd.h" +#include +#include +#include +#include + +/* minimp3: MP3 only, portable C, its 16 KB scratch from the platform allocator (the + * watch's task stacks live in internal RAM). One decoder at a time. */ +static void *s_mp3_scratch; +#define MINIMP3_SCRATCH s_mp3_scratch +#define MINIMP3_ONLY_MP3 +#define MINIMP3_NO_SIMD +#define MINIMP3_IMPLEMENTATION +#include "../third_party/minimp3/minimp3.h" + +/* ------------------------------------------------------------- ring buffer */ + +uint32_t kg_ring_used(const kg_ring_t *r) { return r->head - r->tail; } + +uint32_t kg_ring_write(kg_ring_t *r, const uint8_t *d, uint32_t n) +{ + uint32_t space = r->size - (r->head - r->tail); + if (n > space) n = space; + for (uint32_t i = 0; i < n;) { + uint32_t at = (r->head + i) % r->size, chunk = r->size - at < n - i ? r->size - at : n - i; + memcpy(r->buf + at, d + i, chunk); + i += chunk; + } + __sync_synchronize(); /* data before the index */ + r->head += n; + return n; +} + +uint32_t kg_ring_read(kg_ring_t *r, uint8_t *d, uint32_t n) +{ + uint32_t used = r->head - r->tail; + __sync_synchronize(); + if (n > used) n = used; + for (uint32_t i = 0; i < n;) { + uint32_t at = (r->tail + i) % r->size, chunk = r->size - at < n - i ? r->size - at : n - i; + memcpy(d + i, r->buf + at, chunk); + i += chunk; + } + __sync_synchronize(); + r->tail += n; + return n; +} + +/* --------------------------------------------------------- ICY metadata */ + +void kg_icy_init(kg_icy_t *icy, uint32_t metaint) +{ + memset(icy, 0, sizeof *icy); + icy->metaint = metaint; + icy->until_meta = metaint; + icy->meta_left = -1; +} + +/* "StreamTitle='Artist - Song';StreamUrl='';" → title. */ +static void icy_parse(kg_icy_t *icy) +{ + icy->meta[icy->meta_len < sizeof icy->meta ? icy->meta_len : sizeof icy->meta - 1] = 0; + const char *p = strstr(icy->meta, "StreamTitle='"); + if (!p) return; + p += 13; + const char *end = strstr(p, "';"); + if (!end) end = p + strlen(p); + /* Titles come in UTF-8 or, from older servers, Latin-1: invalid UTF-8 is read as Latin-1. + * Then only what our fonts can draw is kept. */ + char raw[2 * sizeof icy->title], t[sizeof icy->title]; + size_t len = (size_t)(end - p) < sizeof icy->title - 1 ? (size_t)(end - p) : sizeof icy->title - 1, n = 0; + bool utf8 = true; + for (size_t i = 0; i < len && utf8;) { + uint8_t c = (uint8_t)p[i]; + size_t follow = c < 0x80 ? 0 : (c & 0xE0) == 0xC0 ? 1 : (c & 0xF0) == 0xE0 ? 2 : (c & 0xF8) == 0xF0 ? 3 : 9; + utf8 = i + follow < len; + for (size_t k = 1; utf8 && k <= follow; k++) utf8 = ((uint8_t)p[i + k] & 0xC0) == 0x80; + i += follow + 1; + } + for (size_t i = 0; i < len && n + 2 < sizeof raw; i++) { + uint8_t c = (uint8_t)p[i]; + if (utf8 || c < 0x80) raw[n++] = (char)c; + else raw[n++] = (char)(0xC0 | c >> 6), raw[n++] = (char)(0x80 | (c & 0x3F)); + } + kg_text_sanitize(t, sizeof t, raw, n); + if (strcmp(t, icy->title)) { + memcpy(icy->title, t, sizeof t); + icy->title_changed = true; + } +} + +size_t kg_icy_feed(kg_icy_t *icy, uint8_t *d, size_t n) +{ + if (!icy->metaint) return n; + size_t out = 0; + for (size_t i = 0; i < n;) { + if (icy->meta_left < 0 && icy->until_meta == 0) { /* length byte: ×16 bytes of metadata */ + icy->meta_left = d[i++] * 16; + icy->meta_len = 0; + if (!icy->meta_left) { + icy->meta_left = -1; + icy->until_meta = icy->metaint; + } + continue; + } + if (icy->meta_left > 0) { + if (icy->meta_len < sizeof icy->meta) icy->meta[icy->meta_len++] = (char)d[i]; + i++; + if (--icy->meta_left == 0) { + icy_parse(icy); + icy->meta_left = -1; + icy->until_meta = icy->metaint; + } + continue; + } + size_t take = n - i < icy->until_meta ? n - i : icy->until_meta; /* audio */ + memmove(d + out, d + i, take); + out += take; + i += take; + icy->until_meta -= (uint32_t)take; + } + return out; +} + +/* ------------------------------------------------------------------ radio */ + +const kg_station_t KG_STATIONS[] = { + {"Radio Paradise", "Eclectic · California", "http://stream.radioparadise.com/mp3-128"}, + {"BBC World Service", "News · London", "http://stream.live.vc.bbcmedia.co.uk/bbc_world_service"}, + {"KEXP", "Indie · Seattle", "http://live-mp3-128.kexp.org/kexp128.mp3"}, + {"FIP", "Eclectic · Paris", "http://icecast.radiofrance.fr/fip-midfi.mp3"}, + {"Radio Swiss Classic", "Classical · Bern", "http://stream.srg-ssr.ch/m/rsc_de/mp3_128"}, +}; +const int KG_N_STATIONS = sizeof KG_STATIONS / sizeof KG_STATIONS[0]; + +#define RING_BYTES (96 * 1024) /* ~6 s at 128 kbit/s */ +#define PREBUFFER (32 * 1024) /* start after ~2 s */ +#define REBUFFER (16 * 1024) /* after an underrun */ + +static void radio_set(kg_radio_t *r, kg_radio_state_t s, const char *error) +{ + r->env->lock(); + r->st.state = s; + snprintf(r->st.error, sizeof r->st.error, "%s", error ? error : ""); + r->env->unlock(); +} + +bool kg_radio_init(kg_radio_t *r, const kg_media_env_t *env, const char *name, const char *url, uint8_t volume) +{ + memset(r, 0, sizeof *r); + r->env = env; + snprintf(r->url, sizeof r->url, "%s", url); + snprintf(r->st.station, sizeof r->st.station, "%s", name); + r->st.volume = volume > KG_RADIO_VOL_MAX ? KG_RADIO_VOL_MAX : volume; + r->st.state = KG_RADIO_CONNECTING; + r->ring.size = RING_BYTES; + r->ring.buf = env->alloc(RING_BYTES); + if (!s_mp3_scratch) s_mp3_scratch = env->alloc(sizeof(mp3dec_scratch_t)); + return r->ring.buf && s_mp3_scratch; +} + +void kg_radio_free(kg_radio_t *r) +{ + if (r->ring.buf) r->env->release(r->ring.buf); + r->ring.buf = NULL; +} + +static void radio_header(const char *name, const char *value, void *ctx) +{ + kg_radio_t *r = ctx; + if (!strcasecmp(name, "icy-metaint")) kg_icy_init(&r->icy, (uint32_t)strtoul(value, NULL, 10)); + else if (!strcasecmp(name, "icy-br")) r->st.kbps = (uint16_t)strtoul(value, NULL, 10); + else if (!strcasecmp(name, "content-type") && (strstr(value, "aac") || strstr(value, "ogg"))) { + radio_set(r, KG_RADIO_ERROR, "Only MP3 streams are supported"); + r->cancel = true; + } +} + +static bool radio_sink(const uint8_t *data, size_t len, void *ctx) +{ + kg_radio_t *r = ctx; + uint8_t *tmp = r->chunk; + while (len && !r->cancel) { + size_t n = len < sizeof r->chunk ? len : sizeof r->chunk; + memcpy(tmp, data, n); + data += n; + len -= n; + size_t audio = kg_icy_feed(&r->icy, tmp, n); + if (r->icy.title_changed) { + r->icy.title_changed = false; + r->env->lock(); + memcpy(r->st.title, r->icy.title, sizeof r->st.title); + r->env->unlock(); + } + for (size_t done = 0; done < audio && !r->cancel;) { /* ring full: the decoder catches up */ + done += kg_ring_write(&r->ring, tmp + done, (uint32_t)(audio - done)); + if (done < audio) r->env->sleep_ms(20); + } + } + return !r->cancel; +} + +void kg_radio_net_loop(kg_radio_t *r) +{ + int failures = 0; + while (!r->cancel) { + if (!r->env->link_up()) { + radio_set(r, KG_RADIO_CONNECTING, "Waiting for Wi-Fi"); + r->env->sleep_ms(500); + continue; + } + kg_icy_init(&r->icy, 0); + kg_http_req_t req = {.url = r->url, .headers = "Icy-MetaData: 1\r\n", .on_header = radio_header, + .sink = radio_sink, .ctx = r, .idle_ms = 8000, .cancel = &r->cancel}; + int st = kg_http_request(&req); + if (r->cancel) break; + failures++; + char e[48]; + if (st == -6) snprintf(e, sizeof e, "Only http:// streams"); + else if (st > 0 && st != 200) snprintf(e, sizeof e, "HTTP error %d", st); + else if (st == 200) snprintf(e, sizeof e, "Stream ended, reconnecting"); + else snprintf(e, sizeof e, "Cannot reach the station (%d)", st); + radio_set(r, failures >= 5 ? KG_RADIO_ERROR : KG_RADIO_CONNECTING, e); + if (failures >= 5 || st == -6) break; + r->env->sleep_ms(2000); + } + r->net_done = true; +} + +void kg_radio_play_loop(kg_radio_t *r) +{ + /* Decoder state and buffers (~15 KB) from the platform allocator, for the session only. */ + struct work { + mp3dec_t dec; + uint8_t in[4096]; + int16_t pcm[MINIMP3_MAX_SAMPLES_PER_FRAME]; + } *w = r->env->alloc(sizeof *w); + if (!w) { + radio_set(r, KG_RADIO_ERROR, "Out of memory"); + return; + } + mp3dec_t *dec = &w->dec; + uint8_t *in = w->in; + int16_t *pcm = w->pcm; + const size_t in_cap = sizeof w->in; + size_t in_len = 0; + uint32_t rate = 0; + bool playing = false; + mp3dec_init(dec); + while (!r->cancel) { + uint32_t used = kg_ring_used(&r->ring); + r->env->lock(); + r->st.buffer_pct = (uint8_t)(used * 100 / RING_BYTES); + r->env->unlock(); + if (!playing) { /* (re)fill before playing: Wi-Fi delivers in bursts */ + if (used < (rate ? REBUFFER : PREBUFFER) && !r->net_done) { + if (r->st.state != KG_RADIO_CONNECTING || used) radio_set(r, KG_RADIO_BUFFERING, NULL); + r->env->sleep_ms(50); + continue; + } + if (r->net_done && !used) break; + playing = true; + } + in_len += kg_ring_read(&r->ring, in + in_len, (uint32_t)(in_cap - in_len)); + mp3dec_frame_info_t info; + int samples = mp3dec_decode_frame(dec, in, (int)in_len, pcm, &info); + if (info.frame_bytes) { + memmove(in, in + info.frame_bytes, in_len - (size_t)info.frame_bytes); + in_len -= (size_t)info.frame_bytes; + } + if (samples > 0) { + if ((uint32_t)info.hz != rate) { + if (rate) r->env->pcm_close(); + rate = (uint32_t)info.hz; + if (!r->env->pcm_open(rate)) { + radio_set(r, KG_RADIO_ERROR, "No audio output"); + break; + } + r->env->lock(); + r->st.rate = rate; + if (!r->st.kbps) r->st.kbps = (uint16_t)info.bitrate_kbps; + r->env->unlock(); + } + if (r->st.state != KG_RADIO_PLAYING) radio_set(r, KG_RADIO_PLAYING, NULL); + /* Mono mix and volume: a quadratic curve feels even to the ear. */ + int32_t v = r->st.volume, gain = v * v * 32768 / (KG_RADIO_VOL_MAX * KG_RADIO_VOL_MAX); + for (int i = 0; i < samples; i++) { + int32_t s = info.channels == 2 ? ((int32_t)pcm[2 * i] + pcm[2 * i + 1]) / 2 : pcm[i]; + pcm[i] = (int16_t)(s * gain / 32768); + } + r->env->pcm_write(pcm, (size_t)samples); + } else if (!info.frame_bytes && kg_ring_used(&r->ring) == 0) { /* starved */ + if (r->net_done) break; + r->env->lock(); + r->st.underruns++; + r->env->unlock(); + playing = false; + } + } + if (rate) r->env->pcm_close(); + r->env->release(w); + if (!r->cancel && r->st.state != KG_RADIO_ERROR) radio_set(r, KG_RADIO_ERROR, r->st.error[0] ? r->st.error : "Stream ended"); +} + +/* ------------------------------------------------------------------ video */ + +bool kg_mjpeg_feed(kg_mjpeg_t *m, const uint8_t *d, size_t n, size_t *used) +{ + for (size_t i = 0; i < n; i++) { + uint8_t c = d[i]; + if (!m->in_frame) { + if (m->prev == 0xFF && c == 0xD8) { /* start of image */ + m->in_frame = true; + m->overflow = false; + m->buf[0] = 0xFF, m->buf[1] = 0xD8; + m->len = 2; + } + } else { + if (m->len < m->cap) m->buf[m->len++] = c; + else m->overflow = true; + if (m->prev == 0xFF && c == 0xD9) { /* end of image */ + m->in_frame = false; + m->prev = 0; + if (m->overflow) continue; /* too large: dropped */ + *used = i + 1; + return true; + } + } + m->prev = c; + } + *used = n; + return false; +} + +typedef struct { + const uint8_t *jpg; + size_t len, pos; + uint16_t *out; + int ox, oy; /* where the scaled image's (0, 0) lands in out (negative: cropped) */ + bool swap; +} jpeg_io_t; + +static size_t jpeg_in(JDEC *jd, uint8_t *buf, size_t n) +{ + jpeg_io_t *io = jd->device; + if (n > io->len - io->pos) n = io->len - io->pos; + if (buf) memcpy(buf, io->jpg + io->pos, n); + io->pos += n; + return n; +} + +static int jpeg_out(JDEC *jd, void *bitmap, JRECT *rect) +{ + jpeg_io_t *io = jd->device; + const uint16_t *px = bitmap; + int w = rect->right - rect->left + 1; + for (int y = rect->top; y <= rect->bottom; y++, px += w) { + int dy = y + io->oy; + if (dy < 0 || dy >= KG_VIDEO_H) continue; + for (int x = 0; x < w; x++) { + int dx = rect->left + x + io->ox; + if (dx < 0 || dx >= KG_VIDEO_W) continue; + uint16_t c = px[x]; + io->out[dy * KG_VIDEO_W + dx] = io->swap ? (uint16_t)(c >> 8 | c << 8) : c; + } + } + return 1; +} + +bool kg_jpeg_decode(const uint8_t *jpg, size_t len, uint16_t *out, bool swap, void *work, uint16_t *w, uint16_t *h) +{ + JDEC jd; + jpeg_io_t io = {jpg, len, 0, out, 0, 0, swap}; + if (jd_prepare(&jd, jpeg_in, work, KG_JPEG_WORK, &io) != JDR_OK) return false; + *w = jd.width, *h = jd.height; + /* Largest image that is at most 4/3 of the screen: a slight crop beats a thumbnail. */ + uint8_t scale = 0; + while (scale < 3 && ((jd.width >> scale) > KG_VIDEO_W * 4 / 3 || (jd.height >> scale) > KG_VIDEO_H * 4 / 3)) scale++; + int sw = jd.width >> scale, sh = jd.height >> scale; + io.ox = (KG_VIDEO_W - sw) / 2; + io.oy = (KG_VIDEO_H - sh) / 2; + if (sw < KG_VIDEO_W || sh < KG_VIDEO_H) memset(out, 0, KG_VIDEO_W * KG_VIDEO_H * 2); /* letterbox */ + return jd_decomp(&jd, jpeg_out, scale) == JDR_OK; +} + +static void video_set(kg_video_t *v, kg_video_state_t s, const char *error) +{ + v->env->lock(); + v->st.state = s; + snprintf(v->st.error, sizeof v->st.error, "%s", error ? error : ""); + v->env->unlock(); +} + +bool kg_video_init(kg_video_t *v, const kg_media_env_t *env, const char *url, bool swap) +{ + memset(v, 0, sizeof *v); + v->env = env; + v->swap = swap; + snprintf(v->url, sizeof v->url, "%s", url); + v->st.state = KG_VIDEO_CONNECTING; + v->mj.cap = 160 * 1024; + v->mj.buf = env->alloc(v->mj.cap); + v->work = env->alloc(KG_JPEG_WORK); + for (int i = 0; i < 3; i++) v->fb[i] = env->alloc(KG_VIDEO_W * KG_VIDEO_H * 2); + v->front = v->busy = -1; + return v->mj.buf && v->work && v->fb[0] && v->fb[1] && v->fb[2]; +} + +void kg_video_free(kg_video_t *v) +{ + v->env->release(v->mj.buf); + v->env->release(v->work); + for (int i = 0; i < 3; i++) v->env->release(v->fb[i]); + memset(v->fb, 0, sizeof v->fb); +} + +static bool video_sink(const uint8_t *d, size_t n, void *ctx) +{ + kg_video_t *v = ctx; + v->bytes_window += (uint32_t)n; + while (n && !v->cancel) { + size_t used; + if (kg_mjpeg_feed(&v->mj, d, n, &used)) { + /* Decode into the buffer that is neither shown nor held by the UI. */ + v->env->lock(); + int back = 0; + while (back == v->front || back == v->busy) back++; + v->env->unlock(); + uint16_t w, h; + if (kg_jpeg_decode(v->mj.buf, v->mj.len, v->fb[back], v->swap, v->work, &w, &h)) { + int64_t now = kg_net_us(); + v->frames_window++; + v->env->lock(); + v->front = back; + v->seq++; + v->st.frames++; + v->st.src_w = w, v->st.src_h = h; + v->st.state = KG_VIDEO_PLAYING; + if (now - v->t_window >= 1000000) { + float s = (float)(now - v->t_window) / 1e6f; + v->st.fps = v->frames_window / s; + v->st.kbps = (uint32_t)(v->bytes_window * 8 / s / 1000); + v->t_window = now; + v->frames_window = v->bytes_window = 0; + } + v->env->unlock(); + } + } + d += used; + n -= used; + } + return !v->cancel; +} + +void kg_video_loop(kg_video_t *v) +{ + int failures = 0; + v->t_window = kg_net_us(); + while (!v->cancel) { + if (!v->env->link_up()) { + video_set(v, KG_VIDEO_CONNECTING, "Waiting for Wi-Fi"); + v->env->sleep_ms(500); + continue; + } + uint32_t frames = v->st.frames; + kg_http_req_t req = {.url = v->url, .sink = video_sink, .ctx = v, .idle_ms = 8000, .cancel = &v->cancel}; + int st = kg_http_request(&req); + if (v->cancel) break; + if (st == 200 && v->st.frames > frames) { /* a single JPEG (snapshot URL): poll it */ + failures = 0; + v->env->sleep_ms(1000); + continue; + } + char e[48]; + if (st == -6) snprintf(e, sizeof e, "Only http:// streams"); + else if (st > 0 && st != 200) snprintf(e, sizeof e, "HTTP error %d", st); + else if (st == 200) snprintf(e, sizeof e, "No JPEG in the stream"); + else snprintf(e, sizeof e, "Cannot reach the camera (%d)", st); + video_set(v, ++failures >= 5 || st == -6 ? KG_VIDEO_ERROR : KG_VIDEO_CONNECTING, e); + if (failures >= 5 || st == -6) break; + v->env->sleep_ms(2000); + } +} + +const uint16_t *kg_video_take(kg_video_t *v, uint32_t *seq) +{ + v->env->lock(); + v->busy = v->front; + *seq = v->seq; + const uint16_t *f = v->front >= 0 ? v->fb[v->front] : NULL; + v->env->unlock(); + return f; +} diff --git a/src/data/kg_media.h b/src/data/kg_media.h new file mode 100644 index 0000000..f3ef1f4 --- /dev/null +++ b/src/data/kg_media.h @@ -0,0 +1,166 @@ +/* + * Streaming media, plain C shared by the watch and the simulator: + * - Radio: MP3 over HTTP (Icecast / SHOUTcast), ICY metadata for the song title, a + * compressed ring buffer between the network and the decoder, mono output with volume. + * - Video: MJPEG over HTTP (multipart stream or a single JPEG refreshed), decoded and + * fitted into 240 × 240 RGB565, triple-buffered for the UI. + * The platform runs the loops in its own threads and provides the sink and allocator. + */ +#pragma once +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + void (*lock)(void); + void (*unlock)(void); + void *(*alloc)(size_t bytes); /* large buffers (PSRAM on the watch), zeroed */ + void (*release)(void *p); + void (*sleep_ms)(uint32_t ms); + bool (*link_up)(void); + /* Audio sink: 16-bit mono. write blocks until the samples are queued (paces the decoder). */ + bool (*pcm_open)(uint32_t rate); + void (*pcm_write)(const int16_t *samples, size_t n); + void (*pcm_close)(void); +} kg_media_env_t; + +/* ------------------------------------------------------------- ring buffer */ + +/* Single producer, single consumer byte ring (one thread each side, no lock). */ +typedef struct { + uint8_t *buf; + uint32_t size; + volatile uint32_t head, tail; /* head: written, tail: read; both grow forever */ +} kg_ring_t; + +uint32_t kg_ring_used(const kg_ring_t *r); +uint32_t kg_ring_write(kg_ring_t *r, const uint8_t *d, uint32_t n); +uint32_t kg_ring_read(kg_ring_t *r, uint8_t *d, uint32_t n); + +/* --------------------------------------------------------- ICY metadata */ + +/* Removes the metadata blocks inserted every `metaint` bytes and keeps the StreamTitle. */ +typedef struct { + uint32_t metaint, until_meta; + int meta_left; /* bytes of the current metadata block still to read */ + uint16_t meta_len; + char meta[512]; + char title[96]; + bool title_changed; +} kg_icy_t; + +void kg_icy_init(kg_icy_t *icy, uint32_t metaint); +/* Strips metadata in place; returns the number of audio bytes left at the start of d. */ +size_t kg_icy_feed(kg_icy_t *icy, uint8_t *d, size_t n); + +/* ------------------------------------------------------------------- radio */ + +typedef struct { + const char *name; + const char *genre; + const char *url; +} kg_station_t; + +/* Built-in stations: international, MP3 over plain HTTP. */ +extern const kg_station_t KG_STATIONS[]; +extern const int KG_N_STATIONS; + +typedef enum { KG_RADIO_OFF, KG_RADIO_CONNECTING, KG_RADIO_BUFFERING, KG_RADIO_PLAYING, KG_RADIO_ERROR } kg_radio_state_t; + +typedef struct { + kg_radio_state_t state; + char station[32]; + char title[96]; /* ICY StreamTitle, "" if the station sends none */ + uint16_t kbps; + uint32_t rate; /* Hz */ + uint8_t buffer_pct; + uint8_t volume; /* 0..KG_RADIO_VOL_MAX */ + uint32_t underruns; + char error[48]; +} kg_radio_status_t; + +#define KG_RADIO_VOL_MAX 20 + +typedef struct { + kg_radio_status_t st; /* under env->lock */ + char url[256]; + volatile bool cancel; + volatile bool net_done; + kg_ring_t ring; + kg_icy_t icy; + uint8_t chunk[1460]; /* network → ring staging, off the network task's stack */ + const kg_media_env_t *env; +} kg_radio_t; + +/* Prepares a radio session (allocates the ring); false without memory. */ +bool kg_radio_init(kg_radio_t *r, const kg_media_env_t *env, const char *name, const char *url, uint8_t volume); +/* Network side: fetches the stream into the ring, reconnects on drops, until cancel. */ +void kg_radio_net_loop(kg_radio_t *r); +/* Decoder side: MP3 → mono PCM to the sink, until cancel. */ +void kg_radio_play_loop(kg_radio_t *r); +void kg_radio_free(kg_radio_t *r); + +/* ------------------------------------------------------------------- video */ + +#define KG_VIDEO_W 240 +#define KG_VIDEO_H 240 + +/* Pulls whole JPEG images out of a byte stream (multipart MJPEG or a bare JPEG). */ +typedef struct { + uint8_t *buf; + size_t cap, len; + bool in_frame, overflow; + uint8_t prev; +} kg_mjpeg_t; + +/* Consumes bytes up to the end of a frame; returns true when buf/len hold a whole JPEG + * (*used = bytes consumed). Call again with the rest. */ +bool kg_mjpeg_feed(kg_mjpeg_t *m, const uint8_t *d, size_t n, size_t *used); + +/* Decodes a JPEG into out (KG_VIDEO_W × KG_VIDEO_H RGB565), scaled by 1/1…1/8 to fit and + * centred (cropped or letterboxed). swap: byte-swapped RGB565 (LVGL on the watch). `work`: + * KG_JPEG_WORK bytes. Returns false on a bad image; *w, *h get the source size. */ +#define KG_JPEG_WORK 5200 +bool kg_jpeg_decode(const uint8_t *jpg, size_t len, uint16_t *out, bool swap, void *work, uint16_t *w, uint16_t *h); + +typedef enum { KG_VIDEO_OFF, KG_VIDEO_CONNECTING, KG_VIDEO_PLAYING, KG_VIDEO_ERROR } kg_video_state_t; + +typedef struct { + kg_video_state_t state; + float fps; + uint32_t frames, kbps; + uint16_t src_w, src_h; + char error[48]; +} kg_video_status_t; + +typedef struct { + kg_video_status_t st; /* under env->lock */ + char url[256]; + volatile bool cancel; + bool swap; + kg_mjpeg_t mj; + uint16_t *fb[3]; + int front, busy, back; + uint32_t seq; + void *work; + const kg_media_env_t *env; + /* statistics */ + int64_t t_window; + uint32_t frames_window, bytes_window; +} kg_video_t; + +bool kg_video_init(kg_video_t *v, const kg_media_env_t *env, const char *url, bool swap); +/* Fetches and decodes until cancel; reconnects, and refreshes single-JPEG URLs every second. */ +void kg_video_loop(kg_video_t *v); +/* Latest frame for the UI (stays valid until the next call), NULL if none; *seq changes + * with every new frame. */ +const uint16_t *kg_video_take(kg_video_t *v, uint32_t *seq); +void kg_video_free(kg_video_t *v); + +#ifdef __cplusplus +} +#endif diff --git a/src/data/kg_net.c b/src/data/kg_net.c index 384046d..49ca8b8 100644 --- a/src/data/kg_net.c +++ b/src/data/kg_net.c @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include @@ -285,69 +287,139 @@ int kg_resolve(const char *host, uint32_t *ips, int max, uint32_t *ms) /* ------------------------------------------------------------------ HTTP */ -int kg_http_get(const char *host, const char *path, kg_http_sink_t sink, void *ctx, uint32_t timeout_ms, - uint32_t *ttfb_us, volatile bool *cancel) +bool kg_url_parse(const char *url, kg_url_t *u) +{ + memset(u, 0, sizeof *u); + if (strncmp(url, "http://", 7)) return false; /* no TLS: plain HTTP only */ + const char *h = url + 7, *end = h + strcspn(h, ":/?"); + if (end == h || (size_t)(end - h) >= sizeof u->host) return false; + memcpy(u->host, h, (size_t)(end - h)); + u->port = 80; + if (*end == ':') { + unsigned port = (unsigned)strtoul(end + 1, (char **)&end, 10); + if (!port || port > 65535) return false; + u->port = (uint16_t)port; + } + snprintf(u->path, sizeof u->path, "%s", *end ? end : "/"); + if (u->path[0] != '/') { /* "http://host?x" */ + memmove(u->path + 1, u->path, sizeof u->path - 1); + u->path[0] = '/'; + u->path[sizeof u->path - 1] = 0; + } + return strlen(end) < sizeof u->path; +} + +/* One request, no redirect. Headers are handed to the callback line by line; the + * "Location" of a redirect is copied to `location`. */ +static int http_once(const kg_http_req_t *r, const kg_url_t *u, char *location, size_t loc_n) { uint32_t ip; - if (ttfb_us) *ttfb_us = 0; - if (kg_resolve(host, &ip, 1, NULL) != 1) return -1; + if (r->ttfb_us) *r->ttfb_us = 0; + if (kg_resolve(u->host, &ip, 1, NULL) != 1) return -1; + uint32_t idle = r->idle_ms ? r->idle_ms : 10000; int state; - int fd = tcp_connect(ip, 80, timeout_ms < 5000 ? timeout_ms : 5000, &state); + int fd = tcp_connect(ip, u->port, idle < 5000 ? idle : 5000, &state); if (fd < 0 || state != 1) { if (fd >= 0) close(fd); return -2; } - char req[256]; - int len = snprintf(req, sizeof req, "GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: Kronograf\r\nAccept: */*\r\n\r\n", - path, host); - int64_t t0 = kg_net_us(), deadline = t0 + (int64_t)timeout_ms * 1000; + char req[512]; + int len = snprintf(req, sizeof req, "GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: Kronograf\r\nAccept: */*\r\n%s\r\n", u->path, + u->host, r->headers ? r->headers : ""); + int64_t t0 = kg_net_us(), last = t0, deadline = r->timeout_ms ? t0 + (int64_t)r->timeout_ms * 1000 : 0; if (len <= 0 || len >= (int)sizeof req || send(fd, req, (size_t)len, 0) != len) { close(fd); return -2; } - char line[64]; /* status line */ + char line[256]; /* current header line */ size_t ll = 0; int status = -3; - uint32_t crlf = 0; /* last 4 header bytes, to spot the blank line */ - bool in_body = false; + bool in_body = false, first = true; uint8_t buf[1460]; for (;;) { - if (cancel && *cancel) break; - int64_t left = deadline - kg_net_us(); - if (left <= 0) break; + if (r->cancel && *r->cancel) break; + int64_t now = kg_net_us(); + if ((deadline && now >= deadline) || now - last >= (int64_t)idle * 1000) break; fd_set rd; FD_ZERO(&rd); FD_SET(fd, &rd); - struct timeval tv = {(long)(left / 1000000), (int)(left % 1000000)}; - if (left > 250000) tv.tv_sec = 0, tv.tv_usec = 250000; /* wakes up to check `cancel` */ + struct timeval tv = {0, 250000}; /* wakes up to check `cancel` and the timeouts */ int s = select(fd + 1, &rd, NULL, NULL, &tv); if (s < 0) break; if (s == 0) continue; ssize_t got = recv(fd, buf, sizeof buf, 0); if (got <= 0) break; /* end of the response (HTTP/1.0: connection closed) */ - if (ttfb_us && !*ttfb_us) *ttfb_us = (uint32_t)(kg_net_us() - t0); + last = kg_net_us(); + if (r->ttfb_us && !*r->ttfb_us) *r->ttfb_us = (uint32_t)(last - t0); const uint8_t *p = buf; size_t n = (size_t)got; - while (!in_body && n) { /* headers, of any length: only the status line is kept */ - uint8_t ch = *p++; + while (!in_body && n) { /* headers, line by line; overlong lines are truncated */ + char ch = (char)*p++; n--; - if (status == -3 && ll < sizeof line - 1 && ch != '\r' && ch != '\n') line[ll++] = (char)ch; - if (status == -3 && ch == '\n') { - line[ll] = 0; + if (ch == '\r') continue; + if (ch != '\n') { + if (ll < sizeof line - 1) line[ll++] = ch; + continue; + } + line[ll] = 0; + ll = 0; + if (first) { /* "HTTP/1.1 200 OK", or "ICY 200 OK" from SHOUTcast servers */ int code; - status = sscanf(line, "HTTP/%*d.%*d %d", &code) == 1 ? code : -4; + first = false; + if (sscanf(line, "HTTP/%*d.%*d %d", &code) == 1 || sscanf(line, "ICY %d", &code) == 1) status = code; + else status = -4; + if (status < 0) break; + continue; + } + if (!line[0]) { + in_body = true; + break; } - crlf = crlf << 8 | ch; - in_body = crlf == 0x0D0A0D0A; + char *colon = strchr(line, ':'); + if (!colon) continue; + *colon = 0; + const char *val = colon + 1; + while (*val == ' ') val++; + if (location && !strcasecmp(line, "location")) snprintf(location, loc_n, "%s", val); + if (r->on_header) r->on_header(line, val, r->ctx); } if (status == -4) break; - if (n && !sink(p, n, ctx)) break; + if (in_body && (status < 200 || status > 299)) break; /* redirect or error: no body wanted */ + if (n && !r->sink(p, n, r->ctx)) break; } close(fd); return status; } +int kg_http_request(const kg_http_req_t *r) +{ + char url[512], loc[256]; /* url: room for host, port and a relative Location */ + snprintf(url, sizeof url, "%s", r->url); + for (int hop = 0; hop < 4; hop++) { + kg_url_t u; + if (!kg_url_parse(url, &u)) return hop ? -5 : -6; + loc[0] = 0; + int st = http_once(r, &u, loc, sizeof loc); + if ((st == 301 || st == 302 || st == 303 || st == 307 || st == 308) && loc[0]) { + if (loc[0] == '/') snprintf(url, sizeof url, "http://%s:%u%s", u.host, u.port, loc); /* relative */ + else snprintf(url, sizeof url, "%s", loc); + continue; + } + return st; + } + return -5; +} + +int kg_http_get(const char *host, const char *path, kg_http_sink_t sink, void *ctx, uint32_t timeout_ms, + uint32_t *ttfb_us, volatile bool *cancel) +{ + char url[256]; + snprintf(url, sizeof url, "http://%s%s", host, path); + kg_http_req_t r = {.url = url, .sink = sink, .ctx = ctx, .timeout_ms = timeout_ms, .ttfb_us = ttfb_us, .cancel = cancel}; + return kg_http_request(&r); +} + bool kg_json_str(const char *json, const char *key, char *out, size_t n) { char pat[40]; diff --git a/src/data/kg_net.h b/src/data/kg_net.h index 409e0e8..7b0aa79 100644 --- a/src/data/kg_net.h +++ b/src/data/kg_net.h @@ -74,6 +74,14 @@ int kg_resolve(const char *host, uint32_t *ips, int max, uint32_t *ms); /* ------------------------------------------------------------------ HTTP */ +/* "http://host[:port]/path" → parts; false for anything else (https:// included). */ +typedef struct { + char host[64]; + uint16_t port; + char path[192]; +} kg_url_t; +bool kg_url_parse(const char *url, kg_url_t *u); + /* Body sink: return false to stop the transfer. */ typedef bool (*kg_http_sink_t)(const uint8_t *data, size_t len, void *ctx); /* Plain HTTP/1.0 GET (no TLS: speed test and public-IP services only). Returns the HTTP @@ -82,6 +90,23 @@ typedef bool (*kg_http_sink_t)(const uint8_t *data, size_t len, void *ctx); int kg_http_get(const char *host, const char *path, kg_http_sink_t sink, void *ctx, uint32_t timeout_ms, uint32_t *ttfb_us, volatile bool *cancel); +/* General HTTP/1.0 GET for streams: port, up to 3 redirects, "ICY 200" status lines, + * extra request headers ("Icy-MetaData: 1\r\n"), response headers through a callback, + * an idle timeout (no data for idle_ms ends it) and an optional total timeout (0: none). + * Returns the final HTTP status or a negative error as kg_http_get(); -5 too many + * redirects or a redirect to https, -6 unsupported URL. */ +typedef struct { + const char *url; + const char *headers; + void (*on_header)(const char *name, const char *value, void *ctx); + kg_http_sink_t sink; + void *ctx; + uint32_t timeout_ms, idle_ms; + uint32_t *ttfb_us; + volatile bool *cancel; +} kg_http_req_t; +int kg_http_request(const kg_http_req_t *r); + /* Copies the string value of "key" from a flat JSON object; false if absent. */ bool kg_json_str(const char *json, const char *key, char *out, size_t n); diff --git a/src/hal/device/console.cpp b/src/hal/device/console.cpp index a04758e..8075e52 100644 --- a/src/hal/device/console.cpp +++ b/src/hal/device/console.cpp @@ -19,6 +19,8 @@ * ble [on | off | forget] BLE state, ANCS counters, notifications * net [ [target]] network toolbox: ping, trace, dns, ports, lan, speed, public, ble * (starts it; "net" alone prints its progress and renews the Wi-Fi lease) + * radio [play | stop | vol <0-20> | url ] web radio (n: station, "custom" for the URL) + * video [play | stop | url ] MJPEG camera stream (plays while the Video app is open) * sys system information (as in the System app) * accel accelerometer reading in g * ir [ ] sends a TV code (brand 0-3, key 0 power … 6 input) @@ -341,6 +343,22 @@ static void run(char *cmd) } } else if (!strcmp(cmd, "net") || !strncmp(cmd, "net ", 4)) { cmd_net(cmd[3] ? cmd + 4 : ""); + } else if (!strcmp(cmd, "radio") || !strncmp(cmd, "radio ", 6)) { + int n; + if (!strncmp(cmd, "radio url ", 10)) hal_radio_set_custom_url(cmd + 10); + else if (!strcmp(cmd, "radio stop")) hal_radio_stop(); + else if (sscanf(cmd, "radio vol %d", &n) == 1) hal_radio_set_volume((uint8_t)(n < 0 ? 0 : n)); + else if (!strcmp(cmd, "radio play custom") && hal_radio_custom_url()[0]) hal_radio_play("Custom", hal_radio_custom_url()); + else if (sscanf(cmd, "radio play %d", &n) == 1 && n >= 0 && n < KG_N_STATIONS) + hal_radio_play(KG_STATIONS[n].name, KG_STATIONS[n].url); + for (int i = 0; i < KG_N_STATIONS && !strcmp(cmd, "radio"); i++) s_out->printf(" %d %s\n", i, KG_STATIONS[i].name); + media_print(*s_out); + } else if (!strcmp(cmd, "video") || !strncmp(cmd, "video ", 6)) { + if (!strncmp(cmd, "video url ", 10)) hal_video_set_url(cmd + 10); + else if (!strcmp(cmd, "video stop")) hal_video_stop(); + else if (!strcmp(cmd, "video play") && hal_video_url()[0]) hal_video_start(hal_video_url()); + s_out->printf("video url: %s\n", hal_video_url()[0] ? hal_video_url() : "(none)"); + media_print(*s_out); } else if (!strcmp(cmd, "sys")) { cmd_sys(); } else if (!strcmp(cmd, "accel")) { @@ -384,7 +402,7 @@ static void run(char *cmd) else if (!strncmp(cmd, "log ", 4)) { esp_log_level_set("*", (esp_log_level_t)atoi(cmd + 4)); s_out->println("ok"); - } else if (!strcmp(cmd, "help")) s_out->println("status | screenshot | sleep | wake | bl | timeout | touch | battlog | power [min] | face | egg | launcher | app | ring | timer | beep | weather [refresh] | bus [refresh | stops | key ] | ble [on|off|forget] | net [ [target]] | sys | accel | ir [ ] | coredump [erase] | reboot | diag | lcdtest | log <0-5> | help"); + } else if (!strcmp(cmd, "help")) s_out->println("status | screenshot | sleep | wake | bl | timeout | touch | battlog | power [min] | face | egg | launcher | app | ring | timer | beep | weather [refresh] | bus [refresh | stops | key ] | ble [on|off|forget] | net [ [target]] | radio [play |stop|vol |url ] | video [play|stop|url ] | sys | accel | ir [ ] | coredump [erase] | reboot | diag | lcdtest | log <0-5> | help"); else if (*cmd) s_out->printf("unknown command: %s\n", cmd); } diff --git a/src/hal/device/feedback.cpp b/src/hal/device/feedback.cpp index c5447c4..4fb7932 100644 --- a/src/hal/device/feedback.cpp +++ b/src/hal/device/feedback.cpp @@ -59,6 +59,8 @@ struct tone_job { }; static QueueHandle_t s_audio_q; static bool s_i2s_ready; +static SemaphoreHandle_t s_i2s_mx; /* one user of the speaker at a time: tones or a stream */ +static bool s_streaming; bool hal_sound_available(void) { @@ -70,16 +72,16 @@ bool hal_sound_available(void) } #ifdef PIN_I2S_DOUT -static void i2s_start(void) +static void i2s_start(uint32_t rate = SAMPLE_RATE, int dma_bufs = 4) { if (s_i2s_ready) return; i2s_config_t cfg = {}; cfg.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX); - cfg.sample_rate = SAMPLE_RATE; + cfg.sample_rate = rate; cfg.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT; cfg.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; cfg.communication_format = I2S_COMM_FORMAT_STAND_I2S; - cfg.dma_buf_count = 4; + cfg.dma_buf_count = dma_bufs; cfg.dma_buf_len = 256; cfg.tx_desc_auto_clear = true; /* silence if the task falls behind */ i2s_pin_config_t pins = {}; @@ -125,6 +127,7 @@ static void audio_task(void *) tone_job job; for (;;) { if (xQueueReceive(s_audio_q, &job, portMAX_DELAY) != pdTRUE) continue; + if (xSemaphoreTake(s_i2s_mx, 0) != pdTRUE) continue; /* the radio is playing: no beeps */ pmu_audio_power(true); vTaskDelay(pdMS_TO_TICKS(20)); /* let the amplifier settle */ i2s_start(); @@ -134,10 +137,57 @@ static void audio_task(void *) play_tone({0, 40}); /* flush the DMA buffers before powering down */ i2s_stop(); pmu_audio_power(false); + xSemaphoreGive(s_i2s_mx); } } #endif +/* Audio stream (radio): 16-bit mono at the stream's rate, 6 DMA buffers (~35 ms at 44.1 kHz) + * smoothing the decoder's bursts. Tones are dropped while a stream owns the speaker. */ +bool audio_stream_open(uint32_t rate) +{ +#ifdef PIN_I2S_DOUT + if (!hal_sound_available() || !s_i2s_mx) return false; + if (!s_streaming) { + if (xSemaphoreTake(s_i2s_mx, pdMS_TO_TICKS(2000)) != pdTRUE) return false; + pmu_audio_power(true); + vTaskDelay(pdMS_TO_TICKS(20)); + s_streaming = true; + } + i2s_stop(); + i2s_start(rate, 6); + return s_i2s_ready; +#else + (void)rate; + return false; +#endif +} + +void audio_stream_write(const int16_t *samples, size_t n) +{ +#ifdef PIN_I2S_DOUT + size_t written; + if (s_streaming && s_i2s_ready) i2s_write(I2S_NUM_0, samples, n * sizeof(int16_t), &written, portMAX_DELAY); +#else + (void)samples; + (void)n; +#endif +} + +void audio_stream_close(void) +{ +#ifdef PIN_I2S_DOUT + if (!s_streaming) return; + static const int16_t silence[256] = {}; + size_t written; + if (s_i2s_ready) i2s_write(I2S_NUM_0, silence, sizeof silence, &written, portMAX_DELAY); + i2s_stop(); + pmu_audio_power(false); + s_streaming = false; + xSemaphoreGive(s_i2s_mx); +#endif +} + void hal_tones(const kg_tone_t *tones, uint8_t n) { #ifdef PIN_I2S_DOUT @@ -164,6 +214,7 @@ void feedback_init(void) #endif #ifdef PIN_I2S_DOUT s_audio_q = xQueueCreate(2, sizeof(tone_job)); + s_i2s_mx = xSemaphoreCreateMutex(); /* Core 0, medium priority: sound must not stutter; the UI stays on core 1. */ xTaskCreatePinnedToCore(audio_task, "kg_audio", 3072, nullptr, 5, nullptr, 0); #endif diff --git a/src/hal/device/hal_device.h b/src/hal/device/hal_device.h index c40930a..7344f16 100644 --- a/src/hal/device/hal_device.h +++ b/src/hal/device/hal_device.h @@ -93,6 +93,17 @@ bool net_hold_active(void); void nettools_cancel(void); /* lease lapsed: stop the running tool */ void ble_tool_scan(kg_tool_state_t *st, const kg_tools_env_t *env); /* ble.cpp */ +/* Speaker stream for the radio (feedback.cpp): 16-bit mono. */ +bool audio_stream_open(uint32_t rate); +void audio_stream_write(const int16_t *samples, size_t n); +void audio_stream_close(void); + +/* Radio and video state for the console (media.cpp). */ +void media_print(Print &out); + +/* Wi-Fi kept on by the radio, screen off included (net.cpp). */ +void net_keep(bool on); + /* Infrared transmitter (ir.cpp). */ void ir_print(Print &out); diff --git a/src/hal/device/media.cpp b/src/hal/device/media.cpp new file mode 100644 index 0000000..b55fa0b --- /dev/null +++ b/src/hal/device/media.cpp @@ -0,0 +1,247 @@ +/* + * Radio and video on the watch: the portable loops of kg_media.c in FreeRTOS tasks, the + * speaker stream of feedback.cpp, buffers in PSRAM, URLs in NVS ("kgmedia"). + * Radio: network task (core 0, prio 2) → 96 KB ring → decoder task (core 0, prio 3) → I2S. + * Wi-Fi is kept on while it plays (net_keep), screen off included. + * Video: one task (core 0, prio 1) fetching and decoding into three 240 × 240 frames. + * Stopping cancels the loops; the last task out frees the session. + */ +#include "../hal.h" +#include "hal_device.h" +#include +#include +#include +#include + +static SemaphoreHandle_t s_mx; +static void lock(void) { xSemaphoreTake(s_mx, portMAX_DELAY); } +static void unlock(void) { xSemaphoreGive(s_mx); } +static void *ps_alloc(size_t n) { return heap_caps_calloc(1, n, MALLOC_CAP_SPIRAM); } +static void ps_free(void *p) { free(p); } +static void nap(uint32_t ms) { vTaskDelay(pdMS_TO_TICKS(ms)); } +static bool link_up(void) +{ + if (WiFi.status() == WL_CONNECTED) return true; + hal_net_hold(); /* (re)connects; the radio's keep or the video app's lease keeps it up */ + return false; +} +static const kg_media_env_t ENV = {lock, unlock, ps_alloc, ps_free, nap, link_up, + audio_stream_open, audio_stream_write, audio_stream_close}; + +static void ensure_mutex(void) +{ + if (!s_mx) s_mx = xSemaphoreCreateMutex(); +} + +/* ---------------------------------------------------------------- settings */ + +static char s_radio_url[256], s_video_url[256]; +static bool s_loaded; +static uint8_t s_volume = 10; + +static void load(void) +{ + if (s_loaded) return; + s_loaded = true; + Preferences p; + p.begin("kgmedia", true); + snprintf(s_radio_url, sizeof s_radio_url, "%s", p.getString("radio", "").c_str()); + snprintf(s_video_url, sizeof s_video_url, "%s", p.getString("video", "").c_str()); + s_volume = (uint8_t)p.getUChar("vol", 10); + p.end(); +} + +static void save_str(const char *key, const char *v) +{ + Preferences p; + p.begin("kgmedia", false); + if (v[0]) p.putString(key, v); + else p.remove(key); + p.end(); +} + +const char *hal_radio_custom_url(void) { return load(), s_radio_url; } +const char *hal_video_url(void) { return load(), s_video_url; } + +void hal_radio_set_custom_url(const char *url) +{ + load(); + snprintf(s_radio_url, sizeof s_radio_url, "%s", url); + save_str("radio", s_radio_url); +} + +void hal_video_set_url(const char *url) +{ + load(); + snprintf(s_video_url, sizeof s_video_url, "%s", url); + save_str("video", s_video_url); +} + +/* ------------------------------------------------------------------- radio */ + +static kg_radio_t *s_radio; /* session in PSRAM */ +static volatile int s_radio_tasks; /* running tasks of the session */ +static kg_radio_status_t s_radio_last; /* status once the session is gone */ +static TaskHandle_t s_radio_net_h, s_radio_mp3_h; + +static void radio_task_done(void) +{ + lock(); + bool last = --s_radio_tasks == 0; + if (last) { + s_radio_last = s_radio->st; + if (s_radio->cancel) s_radio_last.state = KG_RADIO_OFF; + } + unlock(); + if (last) { + kg_radio_free(s_radio); + net_keep(false); + } +} + +static void radio_net_task(void *) +{ + kg_radio_net_loop(s_radio); + radio_task_done(); + vTaskDelete(nullptr); +} + +static void radio_play_task(void *) +{ + kg_radio_play_loop(s_radio); + s_radio->cancel = s_radio->cancel || s_radio->st.state == KG_RADIO_ERROR; /* the network side stops too */ + radio_task_done(); + vTaskDelete(nullptr); +} + +static bool radio_wait_stopped(uint32_t ms) +{ + for (uint32_t t = 0; s_radio_tasks && t < ms; t += 20) vTaskDelay(pdMS_TO_TICKS(20)); + return !s_radio_tasks; +} + +void hal_radio_stop(void) +{ + if (s_radio && s_radio_tasks) s_radio->cancel = true; +} + +void hal_radio_play(const char *name, const char *url) +{ + ensure_mutex(); + load(); + hal_radio_stop(); + if (!radio_wait_stopped(2000)) return; /* still tearing down: the user can retry */ + if (!s_radio) s_radio = (kg_radio_t *)ps_alloc(sizeof *s_radio); + if (!s_radio || !hal_sound_available()) return; + if (!kg_radio_init(s_radio, &ENV, name, url, s_volume)) { + kg_radio_free(s_radio); + snprintf(s_radio_last.error, sizeof s_radio_last.error, "Out of memory"); + s_radio_last.state = KG_RADIO_ERROR; + return; + } + net_keep(true); + s_radio_tasks = 2; + /* ~6.2 KB measured at peak (DNS, redirects): 7 KB leaves a safe margin. */ + xTaskCreatePinnedToCore(radio_net_task, "kg_radio_net", 7168, nullptr, 2, &s_radio_net_h, 0); + /* Measured on the watch: the decoder peaks at ~1.5 KB of stack (its buffers are in PSRAM). */ + xTaskCreatePinnedToCore(radio_play_task, "kg_radio_mp3", 3584, nullptr, 3, &s_radio_mp3_h, 0); +} + +void hal_radio_status(kg_radio_status_t *out) +{ + ensure_mutex(); + load(); + lock(); + if (s_radio && s_radio_tasks) *out = s_radio->st; + else *out = s_radio_last; + unlock(); + out->volume = s_volume; +} + +void hal_radio_set_volume(uint8_t v) +{ + load(); + s_volume = v > KG_RADIO_VOL_MAX ? KG_RADIO_VOL_MAX : v; + if (s_radio && s_radio_tasks) s_radio->st.volume = s_volume; /* read by the decoder on each frame */ + Preferences p; + p.begin("kgmedia", false); + p.putUChar("vol", s_volume); + p.end(); +} + +uint8_t hal_radio_volume(void) { return load(), s_volume; } + +/* ------------------------------------------------------------------- video */ + +static kg_video_t *s_video; +static volatile bool s_video_task; +static kg_video_status_t s_video_last; + +static void video_task(void *) +{ + kg_video_loop(s_video); + lock(); + s_video_last = s_video->st; + if (s_video->cancel) s_video_last.state = KG_VIDEO_OFF; + s_video_task = false; + unlock(); + kg_video_free(s_video); + vTaskDelete(nullptr); +} + +void hal_video_stop(void) +{ + if (s_video && s_video_task) s_video->cancel = true; +} + +void hal_video_start(const char *url) +{ + ensure_mutex(); + hal_video_stop(); + for (int t = 0; s_video_task && t < 100; t++) vTaskDelay(pdMS_TO_TICKS(20)); + if (s_video_task) return; + if (!s_video) s_video = (kg_video_t *)ps_alloc(sizeof *s_video); + if (!s_video) return; + /* LVGL on the watch stores RGB565 byte-swapped (LV_COLOR_16_SWAP) for the SPI display. */ + if (!kg_video_init(s_video, &ENV, url, true)) { + kg_video_free(s_video); + snprintf(s_video_last.error, sizeof s_video_last.error, "Out of memory"); + s_video_last.state = KG_VIDEO_ERROR; + return; + } + s_video_task = true; + xTaskCreatePinnedToCore(video_task, "kg_video", 6144, nullptr, 1, nullptr, 0); +} + +void hal_video_status(kg_video_status_t *out) +{ + ensure_mutex(); + lock(); + *out = s_video && s_video_task ? s_video->st : s_video_last; + unlock(); +} + +const uint16_t *hal_video_frame(uint32_t *seq) +{ + if (!s_video || !s_video_task) return nullptr; + return kg_video_take(s_video, seq); +} + +/* Console: one line of radio / video state. */ +void media_print(Print &out) +{ + kg_radio_status_t r; + hal_radio_status(&r); + static const char *const RS[] = {"off", "connecting", "buffering", "playing", "error"}; + if (s_radio_tasks == 2) + out.printf("radio stacks free: net %u, mp3 %u bytes\n", (unsigned)uxTaskGetStackHighWaterMark(s_radio_net_h), + (unsigned)uxTaskGetStackHighWaterMark(s_radio_mp3_h)); + out.printf("radio: %s \"%s\" %u kbps %lu Hz buffer %u %% vol %u underruns %lu title \"%s\" %s\n", RS[r.state], + r.station, r.kbps, (unsigned long)r.rate, r.buffer_pct, r.volume, (unsigned long)r.underruns, r.title, + r.error); + kg_video_status_t v; + hal_video_status(&v); + static const char *const VS[] = {"off", "connecting", "playing", "error"}; + out.printf("video: %s %lu frames %.1f fps %lu kbit/s %ux%u %s\n", VS[v.state], (unsigned long)v.frames, v.fps, + (unsigned long)v.kbps, v.src_w, v.src_h, v.error); +} diff --git a/src/hal/device/net.cpp b/src/hal/device/net.cpp index 46a8a22..d2ebee3 100644 --- a/src/hal/device/net.cpp +++ b/src/hal/device/net.cpp @@ -65,7 +65,9 @@ static volatile uint32_t s_hold_until; static bool s_kept; static constexpr uint32_t HOLD_MS = 20000; -bool net_hold_active(void) { return s_hold_until && (int32_t)(s_hold_until - millis()) > 0; } +static volatile bool s_keep; /* radio playing */ + +bool net_hold_active(void) { return s_keep || (s_hold_until && (int32_t)(s_hold_until - millis()) > 0); } static constexpr uint32_t WIFI_TIMEOUT_MS = 15000; static constexpr uint32_t NTP_TIMEOUT_MS = 10000; @@ -463,6 +465,12 @@ void hal_net_hold(void) void hal_net_release(void) { s_hold_until = 0; } +void net_keep(bool on) +{ + s_keep = on; + if (on) hal_net_hold(); +} + /* Lease lapsed (toolbox closed, screen off): stop the tools, then Wi-Fi. */ static void hold_check(void) { diff --git a/src/hal/hal.h b/src/hal/hal.h index b381d9a..1da47cc 100644 --- a/src/hal/hal.h +++ b/src/hal/hal.h @@ -12,6 +12,7 @@ #include "../data/kg_ancs.h" #include "../data/kg_bus.h" #include "../data/kg_ir.h" +#include "../data/kg_media.h" #include "../data/kg_net.h" #include "../data/kg_tools.h" #include "../data/kg_weather.h" @@ -174,6 +175,26 @@ void hal_tool_start(kg_tool_t tool, const char *target); void hal_tool_stop(void); void hal_tool_get(kg_tool_state_t *out); +/* ------------------------------------------------------------------ media + * Radio: MP3 web radio on the speaker; keeps Wi-Fi on while it plays, screen off included. + * Video: MJPEG camera stream; runs while the app is on screen (the UI holds the Wi-Fi lease). + * URLs are http:// only (no TLS). */ +void hal_radio_play(const char *name, const char *url); +void hal_radio_stop(void); +void hal_radio_status(kg_radio_status_t *out); +void hal_radio_set_volume(uint8_t volume); /* 0..KG_RADIO_VOL_MAX, persistent */ +uint8_t hal_radio_volume(void); +const char *hal_radio_custom_url(void); /* "" if none */ +void hal_radio_set_custom_url(const char *url); /* persistent */ + +void hal_video_start(const char *url); +void hal_video_stop(void); +void hal_video_status(kg_video_status_t *out); +/* Latest frame (KG_VIDEO_W × KG_VIDEO_H, LVGL's RGB565), valid until the next call; NULL if none. */ +const uint16_t *hal_video_frame(uint32_t *seq); +const char *hal_video_url(void); /* "" if none */ +void hal_video_set_url(const char *url); /* persistent */ + /* ---------------------------------------------------------------- sensors */ /* Acceleration in g, watch frame: x towards 3 o'clock, y towards 12 o'clock, z out of the diff --git a/src/hal/sim/hal_sim_media.c b/src/hal/sim/hal_sim_media.c new file mode 100644 index 0000000..4f47605 --- /dev/null +++ b/src/hal/sim/hal_sim_media.c @@ -0,0 +1,196 @@ +/* + * Simulated radio and video. Demo data by default (screenshots): + * KG_RADIO=play the radio playing a station, with a song title + * KG_VIDEO_URL=… a camera URL; without KG_NET_REAL the frame is sim/demo-camera.jpg + * With KG_NET_REAL=1 both run for real from the Mac, as on the watch: the same loops + * (kg_media.c) in threads, the radio through SDL audio (try tools/mjpeg_test_server.py for + * the video). + */ +#include "../hal.h" +#include +#include +#include +#include +#include +#include + +static pthread_mutex_t s_mx = PTHREAD_MUTEX_INITIALIZER; +static void lock(void) { pthread_mutex_lock(&s_mx); } +static void unlock(void) { pthread_mutex_unlock(&s_mx); } +static void *zalloc(size_t n) { return calloc(1, n); } +static void nap(uint32_t ms) { usleep(ms * 1000); } +static bool up(void) { return true; } + +static SDL_AudioDeviceID s_dev; +static uint32_t s_rate; + +static bool pcm_open(uint32_t rate) +{ + if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) return false; + SDL_AudioSpec want = {0}, have; + want.freq = (int)rate; + want.format = AUDIO_S16SYS; + want.channels = 1; + want.samples = 1024; + s_dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0); + s_rate = rate; + if (s_dev) SDL_PauseAudioDevice(s_dev, 0); + return s_dev != 0; +} + +static void pcm_write(const int16_t *s, size_t n) +{ + if (!s_dev) return; + while (SDL_GetQueuedAudioSize(s_dev) > s_rate / 2 * 2) usleep(10000); /* ~0.5 s queued, as DMA would block */ + SDL_QueueAudio(s_dev, s, (Uint32)(n * 2)); +} + +static void pcm_close(void) +{ + if (s_dev) SDL_CloseAudioDevice(s_dev); + s_dev = 0; +} + +static const kg_media_env_t ENV = {lock, unlock, zalloc, free, nap, up, pcm_open, pcm_write, pcm_close}; +static bool real(void) { return getenv("KG_NET_REAL") != NULL; } + +/* ------------------------------------------------------------------ radio */ + +static kg_radio_t s_radio; +static pthread_t s_rnet, s_rplay; +static bool s_radio_on; +static uint8_t s_volume = 12; +static char s_radio_url[256]; +static kg_radio_status_t s_demo; + +static void *rnet(void *a) +{ + kg_radio_net_loop(a); + return NULL; +} +static void *rplay(void *a) +{ + kg_radio_play_loop(a); + return NULL; +} + +void hal_radio_stop(void) +{ + if (s_radio_on) { + s_radio.cancel = true; + pthread_join(s_rnet, NULL); + pthread_join(s_rplay, NULL); + kg_radio_free(&s_radio); + s_radio_on = false; + } + s_demo.state = KG_RADIO_OFF; +} + +void hal_radio_play(const char *name, const char *url) +{ + hal_radio_stop(); + if (!real()) { /* demo */ + memset(&s_demo, 0, sizeof s_demo); + s_demo.state = KG_RADIO_PLAYING; + snprintf(s_demo.station, sizeof s_demo.station, "%s", name); + snprintf(s_demo.title, sizeof s_demo.title, "The Clock Tower Ensemble - 88 Miles per Hour"); + s_demo.kbps = 128, s_demo.rate = 44100, s_demo.buffer_pct = 78; + return; + } + if (!kg_radio_init(&s_radio, &ENV, name, url, s_volume)) return; + s_radio_on = true; + pthread_create(&s_rnet, NULL, rnet, &s_radio); + pthread_create(&s_rplay, NULL, rplay, &s_radio); +} + +void hal_radio_status(kg_radio_status_t *out) +{ + static bool demo_started; + if (!demo_started && getenv("KG_RADIO")) { /* screenshot: already playing on arrival */ + demo_started = true; + hal_radio_play(KG_STATIONS[0].name, KG_STATIONS[0].url); + } + lock(); + *out = s_radio_on ? s_radio.st : s_demo; + unlock(); + out->volume = s_volume; +} + +void hal_radio_set_volume(uint8_t v) +{ + s_volume = v > KG_RADIO_VOL_MAX ? KG_RADIO_VOL_MAX : v; + s_radio.st.volume = s_volume; +} +uint8_t hal_radio_volume(void) { return s_volume; } +const char *hal_radio_custom_url(void) { return s_radio_url; } +void hal_radio_set_custom_url(const char *url) { snprintf(s_radio_url, sizeof s_radio_url, "%s", url); } + +/* ------------------------------------------------------------------ video */ + +static kg_video_t s_video; +static pthread_t s_vthread; +static bool s_video_on, s_demo_video; +static char s_video_url[256]; +static uint16_t *s_demo_frame; + +static void *vrun(void *a) +{ + kg_video_loop(a); + return NULL; +} + +const char *hal_video_url(void) +{ + if (!s_video_url[0] && getenv("KG_VIDEO_URL")) snprintf(s_video_url, sizeof s_video_url, "%s", getenv("KG_VIDEO_URL")); + return s_video_url; +} +void hal_video_set_url(const char *url) { snprintf(s_video_url, sizeof s_video_url, "%s", url); } + +void hal_video_stop(void) +{ + if (s_video_on) { + s_video.cancel = true; + pthread_join(s_vthread, NULL); + kg_video_free(&s_video); + s_video_on = false; + } + s_demo_video = false; +} + +void hal_video_start(const char *url) +{ + hal_video_stop(); + if (!real()) { /* demo: one decoded test card */ + static uint8_t jpg[64 * 1024]; + FILE *f = fopen(KG_ROOT "/sim/demo-camera.jpg", "rb"); + size_t len = f ? fread(jpg, 1, sizeof jpg, f) : 0; + if (f) fclose(f); + static uint8_t work[KG_JPEG_WORK]; + uint16_t w, h; + if (!s_demo_frame) s_demo_frame = malloc(KG_VIDEO_W * KG_VIDEO_H * 2); + s_demo_video = len && kg_jpeg_decode(jpg, len, s_demo_frame, false, work, &w, &h); + return; + } + if (!kg_video_init(&s_video, &ENV, url, false)) return; + s_video_on = true; + pthread_create(&s_vthread, NULL, vrun, &s_video); +} + +void hal_video_status(kg_video_status_t *out) +{ + if (s_video_on) { + lock(); + *out = s_video.st; + unlock(); + return; + } + memset(out, 0, sizeof *out); + if (s_demo_video) *out = (kg_video_status_t){KG_VIDEO_PLAYING, 12.4f, 372, 940, 320, 240, ""}; +} + +const uint16_t *hal_video_frame(uint32_t *seq) +{ + if (s_video_on) return kg_video_take(&s_video, seq); + *seq = 1; + return s_demo_video ? s_demo_frame : NULL; +} diff --git a/src/third_party/README.md b/src/third_party/README.md new file mode 100644 index 0000000..60a73e1 --- /dev/null +++ b/src/third_party/README.md @@ -0,0 +1,6 @@ +# Third-party code + +| Directory | Project | Version | Licence | Changes | +|---|---|---|---|---| +| `minimp3/` | [minimp3](https://github.com/lieff/minimp3) by lieff — MP3 decoder | commit `afb604c` | CC0 1.0 (public domain), see `minimp3/LICENSE` | `MINIMP3_SCRATCH`: the ~16 KB scratch structure can be provided by the caller instead of living on the stack (ESP32 task stacks are in internal RAM). Marked "Kronograf patch". | +| `tjpgd/` | [TJpgDec](http://elm-chan.org/fsw/tjpgd/) by ChaN — JPEG decoder | R0.03 | ChaN's free licence (no restriction on use; keep the notice), in `tjpgd.c` | `tjpgdcnf.h`: `JD_FORMAT` 1 (RGB565 output), `JD_FASTDECODE` 1 (32-bit CPUs). | diff --git a/src/third_party/minimp3/LICENSE b/src/third_party/minimp3/LICENSE new file mode 100644 index 0000000..2c4afab --- /dev/null +++ b/src/third_party/minimp3/LICENSE @@ -0,0 +1,117 @@ +CC0 1.0 Universal + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later +claims of infringement build upon, modify, incorporate in other works, reuse +and redistribute as freely as possible in any form whatsoever and for any +purposes, including without limitation commercial purposes. These owners may +contribute to the Commons to promote the ideal of a free culture and the +further production of creative, cultural and scientific works, or to gain +reputation or greater distribution for their Work in part through the use and +efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with a +Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not limited +to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer's heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer's express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, +non transferable, non sublicensable, non exclusive, irrevocable and +unconditional license to exercise Affirmer's Copyright and Related Rights in +the Work (i) in all territories worldwide, (ii) for the maximum duration +provided by applicable law or treaty (including future time extensions), (iii) +in any current or future medium and for any number of copies, and (iv) for any +purpose whatsoever, including without limitation commercial, advertising or +promotional purposes (the "License"). The License shall be deemed effective as +of the date CC0 was applied by Affirmer to the Work. Should any part of the +License for any reason be judged legally invalid or ineffective under +applicable law, such partial invalidity or ineffectiveness shall not +invalidate the remainder of the License, and in such case Affirmer hereby +affirms that he or she will not (i) exercise any of his or her remaining +Copyright and Related Rights in the Work or (ii) assert any associated claims +and causes of action with respect to the Work, in either case contrary to +Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see + + diff --git a/src/third_party/minimp3/minimp3.h b/src/third_party/minimp3/minimp3.h new file mode 100644 index 0000000..5341f93 --- /dev/null +++ b/src/third_party/minimp3/minimp3.h @@ -0,0 +1,1873 @@ +#ifndef MINIMP3_H +#define MINIMP3_H +/* + https://github.com/lieff/minimp3 + To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. + This software is distributed without any warranty. + See . +*/ +#include + +#define MINIMP3_MAX_SAMPLES_PER_FRAME (1152*2) + +typedef struct +{ + int frame_bytes, frame_offset, channels, hz, layer, bitrate_kbps; +} mp3dec_frame_info_t; + +typedef struct +{ + float mdct_overlap[2][9*32], qmf_state[15*2*32]; + int reserv, free_format_bytes; + unsigned char header[4], reserv_buf[511]; +} mp3dec_t; + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +void mp3dec_init(mp3dec_t *dec); +#ifndef MINIMP3_FLOAT_OUTPUT +typedef int16_t mp3d_sample_t; +#else /* MINIMP3_FLOAT_OUTPUT */ +typedef float mp3d_sample_t; +void mp3dec_f32_to_s16(const float *in, int16_t *out, int num_samples); +#endif /* MINIMP3_FLOAT_OUTPUT */ +int mp3dec_decode_frame(mp3dec_t *dec, const uint8_t *mp3, int mp3_bytes, mp3d_sample_t *pcm, mp3dec_frame_info_t *info); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* MINIMP3_H */ +#if defined(MINIMP3_IMPLEMENTATION) && !defined(_MINIMP3_IMPLEMENTATION_GUARD) +#define _MINIMP3_IMPLEMENTATION_GUARD + +#include +#include + +#define MAX_FREE_FORMAT_FRAME_SIZE 2304 /* more than ISO spec's */ +#ifndef MAX_FRAME_SYNC_MATCHES +#define MAX_FRAME_SYNC_MATCHES 10 +#endif /* MAX_FRAME_SYNC_MATCHES */ + +#define MAX_L3_FRAME_PAYLOAD_BYTES MAX_FREE_FORMAT_FRAME_SIZE /* MUST be >= 320000/8/32000*1152 = 1440 */ + +#define MAX_BITRESERVOIR_BYTES 511 +#define SHORT_BLOCK_TYPE 2 +#define STOP_BLOCK_TYPE 3 +#define MODE_MONO 3 +#define MODE_JOINT_STEREO 1 +#define HDR_SIZE 4 +#define HDR_IS_MONO(h) (((h[3]) & 0xC0) == 0xC0) +#define HDR_IS_MS_STEREO(h) (((h[3]) & 0xE0) == 0x60) +#define HDR_IS_FREE_FORMAT(h) (((h[2]) & 0xF0) == 0) +#define HDR_IS_CRC(h) (!((h[1]) & 1)) +#define HDR_TEST_PADDING(h) ((h[2]) & 0x2) +#define HDR_TEST_MPEG1(h) ((h[1]) & 0x8) +#define HDR_TEST_NOT_MPEG25(h) ((h[1]) & 0x10) +#define HDR_TEST_I_STEREO(h) ((h[3]) & 0x10) +#define HDR_TEST_MS_STEREO(h) ((h[3]) & 0x20) +#define HDR_GET_STEREO_MODE(h) (((h[3]) >> 6) & 3) +#define HDR_GET_STEREO_MODE_EXT(h) (((h[3]) >> 4) & 3) +#define HDR_GET_LAYER(h) (((h[1]) >> 1) & 3) +#define HDR_GET_BITRATE(h) ((h[2]) >> 4) +#define HDR_GET_SAMPLE_RATE(h) (((h[2]) >> 2) & 3) +#define HDR_GET_MY_SAMPLE_RATE(h) (HDR_GET_SAMPLE_RATE(h) + (((h[1] >> 3) & 1) + ((h[1] >> 4) & 1))*3) +#define HDR_IS_FRAME_576(h) ((h[1] & 14) == 2) +#define HDR_IS_LAYER_1(h) ((h[1] & 6) == 6) + +#define BITS_DEQUANTIZER_OUT -1 +#define MAX_SCF (255 + BITS_DEQUANTIZER_OUT*4 - 210) +#define MAX_SCFI ((MAX_SCF + 3) & ~3) + +#define MINIMP3_MIN(a, b) ((a) > (b) ? (b) : (a)) +#define MINIMP3_MAX(a, b) ((a) < (b) ? (b) : (a)) + +#if !defined(MINIMP3_NO_SIMD) + +#if !defined(MINIMP3_ONLY_SIMD) && (defined(_M_X64) || defined(__x86_64__) || defined(__aarch64__) || defined(_M_ARM64)) +/* x64 always have SSE2, arm64 always have neon, no need for generic code */ +#define MINIMP3_ONLY_SIMD +#endif /* SIMD checks... */ + +#if (defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64))) || ((defined(__i386__) || defined(__x86_64__)) && defined(__SSE2__)) +#if defined(_MSC_VER) +#include +#endif /* defined(_MSC_VER) */ +#include +#define HAVE_SSE 1 +#define HAVE_SIMD 1 +#define VSTORE _mm_storeu_ps +#define VLD _mm_loadu_ps +#define VSET _mm_set1_ps +#define VADD _mm_add_ps +#define VSUB _mm_sub_ps +#define VMUL _mm_mul_ps +#define VMAC(a, x, y) _mm_add_ps(a, _mm_mul_ps(x, y)) +#define VMSB(a, x, y) _mm_sub_ps(a, _mm_mul_ps(x, y)) +#define VMUL_S(x, s) _mm_mul_ps(x, _mm_set1_ps(s)) +#define VREV(x) _mm_shuffle_ps(x, x, _MM_SHUFFLE(0, 1, 2, 3)) +typedef __m128 f4; +#if defined(_MSC_VER) || defined(MINIMP3_ONLY_SIMD) +#define minimp3_cpuid __cpuid +#else /* defined(_MSC_VER) || defined(MINIMP3_ONLY_SIMD) */ +static __inline__ __attribute__((always_inline)) void minimp3_cpuid(int CPUInfo[], const int InfoType) +{ +#if defined(__PIC__) + __asm__ __volatile__( +#if defined(__x86_64__) + "push %%rbx\n" + "cpuid\n" + "xchgl %%ebx, %1\n" + "pop %%rbx\n" +#else /* defined(__x86_64__) */ + "xchgl %%ebx, %1\n" + "cpuid\n" + "xchgl %%ebx, %1\n" +#endif /* defined(__x86_64__) */ + : "=a" (CPUInfo[0]), "=r" (CPUInfo[1]), "=c" (CPUInfo[2]), "=d" (CPUInfo[3]) + : "a" (InfoType)); +#else /* defined(__PIC__) */ + __asm__ __volatile__( + "cpuid" + : "=a" (CPUInfo[0]), "=b" (CPUInfo[1]), "=c" (CPUInfo[2]), "=d" (CPUInfo[3]) + : "a" (InfoType)); +#endif /* defined(__PIC__)*/ +} +#endif /* defined(_MSC_VER) || defined(MINIMP3_ONLY_SIMD) */ +static int have_simd(void) +{ +#ifdef MINIMP3_ONLY_SIMD + return 1; +#else /* MINIMP3_ONLY_SIMD */ + static int g_have_simd; + int CPUInfo[4]; +#ifdef MINIMP3_TEST + static int g_counter; + if (g_counter++ > 100) + return 0; +#endif /* MINIMP3_TEST */ + if (g_have_simd) + goto end; + minimp3_cpuid(CPUInfo, 0); + g_have_simd = 1; + if (CPUInfo[0] > 0) + { + minimp3_cpuid(CPUInfo, 1); + g_have_simd = (CPUInfo[3] & (1 << 26)) + 1; /* SSE2 */ + } +end: + return g_have_simd - 1; +#endif /* MINIMP3_ONLY_SIMD */ +} +#elif defined(__ARM_NEON) || defined(__aarch64__) || defined(_M_ARM64) +#include +#define HAVE_SSE 0 +#define HAVE_SIMD 1 +#define VSTORE vst1q_f32 +#define VLD vld1q_f32 +#define VSET vmovq_n_f32 +#define VADD vaddq_f32 +#define VSUB vsubq_f32 +#define VMUL vmulq_f32 +#define VMAC(a, x, y) vmlaq_f32(a, x, y) +#define VMSB(a, x, y) vmlsq_f32(a, x, y) +#define VMUL_S(x, s) vmulq_f32(x, vmovq_n_f32(s)) +#define VREV(x) vcombine_f32(vget_high_f32(vrev64q_f32(x)), vget_low_f32(vrev64q_f32(x))) +typedef float32x4_t f4; +static int have_simd() +{ /* TODO: detect neon for !MINIMP3_ONLY_SIMD */ + return 1; +} +#else /* SIMD checks... */ +#define HAVE_SSE 0 +#define HAVE_SIMD 0 +#ifdef MINIMP3_ONLY_SIMD +#error MINIMP3_ONLY_SIMD used, but SSE/NEON not enabled +#endif /* MINIMP3_ONLY_SIMD */ +#endif /* SIMD checks... */ +#else /* !defined(MINIMP3_NO_SIMD) */ +#define HAVE_SIMD 0 +#endif /* !defined(MINIMP3_NO_SIMD) */ + +#if defined(__ARM_ARCH) && (__ARM_ARCH >= 6) && !defined(__aarch64__) && !defined(_M_ARM64) +#define HAVE_ARMV6 1 +static __inline__ __attribute__((always_inline)) int32_t minimp3_clip_int16_arm(int32_t a) +{ + int32_t x = 0; + __asm__ ("ssat %0, #16, %1" : "=r"(x) : "r"(a)); + return x; +} +#else +#define HAVE_ARMV6 0 +#endif + +typedef struct +{ + const uint8_t *buf; + int pos, limit; +} bs_t; + +typedef struct +{ + float scf[3*64]; + uint8_t total_bands, stereo_bands, bitalloc[64], scfcod[64]; +} L12_scale_info; + +typedef struct +{ + uint8_t tab_offset, code_tab_width, band_count; +} L12_subband_alloc_t; + +typedef struct +{ + const uint8_t *sfbtab; + uint16_t part_23_length, big_values, scalefac_compress; + uint8_t global_gain, block_type, mixed_block_flag, n_long_sfb, n_short_sfb; + uint8_t table_select[3], region_count[3], subblock_gain[3]; + uint8_t preflag, scalefac_scale, count1_table, scfsi; +} L3_gr_info_t; + +typedef struct +{ + bs_t bs; + uint8_t maindata[MAX_BITRESERVOIR_BYTES + MAX_L3_FRAME_PAYLOAD_BYTES]; + L3_gr_info_t gr_info[4]; + float grbuf[2][576], scf[40], syn[18 + 15][2*32]; + uint8_t ist_pos[2][39]; +} mp3dec_scratch_t; + +static void bs_init(bs_t *bs, const uint8_t *data, int bytes) +{ + bs->buf = data; + bs->pos = 0; + bs->limit = bytes*8; +} + +static uint32_t get_bits(bs_t *bs, int n) +{ + uint32_t next, cache = 0, s = bs->pos & 7; + int shl = n + s; + const uint8_t *p = bs->buf + (bs->pos >> 3); + if ((bs->pos += n) > bs->limit) + return 0; + next = *p++ & (255 >> s); + while ((shl -= 8) > 0) + { + cache |= next << shl; + next = *p++; + } + return cache | (next >> -shl); +} + +static int hdr_valid(const uint8_t *h) +{ + return h[0] == 0xff && + ((h[1] & 0xF0) == 0xf0 || (h[1] & 0xFE) == 0xe2) && + (HDR_GET_LAYER(h) != 0) && + (HDR_GET_BITRATE(h) != 15) && + (HDR_GET_SAMPLE_RATE(h) != 3); +} + +static int hdr_compare(const uint8_t *h1, const uint8_t *h2) +{ + return hdr_valid(h2) && + ((h1[1] ^ h2[1]) & 0xFE) == 0 && + ((h1[2] ^ h2[2]) & 0x0C) == 0 && + !(HDR_IS_FREE_FORMAT(h1) ^ HDR_IS_FREE_FORMAT(h2)); +} + +static unsigned hdr_bitrate_kbps(const uint8_t *h) +{ + static const uint8_t halfrate[2][3][15] = { + { { 0,4,8,12,16,20,24,28,32,40,48,56,64,72,80 }, { 0,4,8,12,16,20,24,28,32,40,48,56,64,72,80 }, { 0,16,24,28,32,40,48,56,64,72,80,88,96,112,128 } }, + { { 0,16,20,24,28,32,40,48,56,64,80,96,112,128,160 }, { 0,16,24,28,32,40,48,56,64,80,96,112,128,160,192 }, { 0,16,32,48,64,80,96,112,128,144,160,176,192,208,224 } }, + }; + return 2*halfrate[!!HDR_TEST_MPEG1(h)][HDR_GET_LAYER(h) - 1][HDR_GET_BITRATE(h)]; +} + +static unsigned hdr_sample_rate_hz(const uint8_t *h) +{ + static const unsigned g_hz[3] = { 44100, 48000, 32000 }; + return g_hz[HDR_GET_SAMPLE_RATE(h)] >> (int)!HDR_TEST_MPEG1(h) >> (int)!HDR_TEST_NOT_MPEG25(h); +} + +static unsigned hdr_frame_samples(const uint8_t *h) +{ + return HDR_IS_LAYER_1(h) ? 384 : (1152 >> (int)HDR_IS_FRAME_576(h)); +} + +static int hdr_frame_bytes(const uint8_t *h, int free_format_size) +{ + int frame_bytes = hdr_frame_samples(h)*hdr_bitrate_kbps(h)*125/hdr_sample_rate_hz(h); + if (HDR_IS_LAYER_1(h)) + { + frame_bytes &= ~3; /* slot align */ + } + return frame_bytes ? frame_bytes : free_format_size; +} + +static int hdr_padding(const uint8_t *h) +{ + return HDR_TEST_PADDING(h) ? (HDR_IS_LAYER_1(h) ? 4 : 1) : 0; +} + +#ifndef MINIMP3_ONLY_MP3 +static const L12_subband_alloc_t *L12_subband_alloc_table(const uint8_t *hdr, L12_scale_info *sci) +{ + const L12_subband_alloc_t *alloc; + int mode = HDR_GET_STEREO_MODE(hdr); + int nbands, stereo_bands = (mode == MODE_MONO) ? 0 : (mode == MODE_JOINT_STEREO) ? (HDR_GET_STEREO_MODE_EXT(hdr) << 2) + 4 : 32; + + if (HDR_IS_LAYER_1(hdr)) + { + static const L12_subband_alloc_t g_alloc_L1[] = { { 76, 4, 32 } }; + alloc = g_alloc_L1; + nbands = 32; + } else if (!HDR_TEST_MPEG1(hdr)) + { + static const L12_subband_alloc_t g_alloc_L2M2[] = { { 60, 4, 4 }, { 44, 3, 7 }, { 44, 2, 19 } }; + alloc = g_alloc_L2M2; + nbands = 30; + } else + { + static const L12_subband_alloc_t g_alloc_L2M1[] = { { 0, 4, 3 }, { 16, 4, 8 }, { 32, 3, 12 }, { 40, 2, 7 } }; + int sample_rate_idx = HDR_GET_SAMPLE_RATE(hdr); + unsigned kbps = hdr_bitrate_kbps(hdr) >> (int)(mode != MODE_MONO); + if (!kbps) /* free-format */ + { + kbps = 192; + } + + alloc = g_alloc_L2M1; + nbands = 27; + if (kbps < 56) + { + static const L12_subband_alloc_t g_alloc_L2M1_lowrate[] = { { 44, 4, 2 }, { 44, 3, 10 } }; + alloc = g_alloc_L2M1_lowrate; + nbands = sample_rate_idx == 2 ? 12 : 8; + } else if (kbps >= 96 && sample_rate_idx != 1) + { + nbands = 30; + } + } + + sci->total_bands = (uint8_t)nbands; + sci->stereo_bands = (uint8_t)MINIMP3_MIN(stereo_bands, nbands); + + return alloc; +} + +static void L12_read_scalefactors(bs_t *bs, uint8_t *pba, uint8_t *scfcod, int bands, float *scf) +{ + static const float g_deq_L12[18*3] = { +#define DQ(x) 9.53674316e-07f/x, 7.56931807e-07f/x, 6.00777173e-07f/x + DQ(3),DQ(7),DQ(15),DQ(31),DQ(63),DQ(127),DQ(255),DQ(511),DQ(1023),DQ(2047),DQ(4095),DQ(8191),DQ(16383),DQ(32767),DQ(65535),DQ(3),DQ(5),DQ(9) + }; + int i, m; + for (i = 0; i < bands; i++) + { + float s = 0; + int ba = *pba++; + int mask = ba ? 4 + ((19 >> scfcod[i]) & 3) : 0; + for (m = 4; m; m >>= 1) + { + if (mask & m) + { + int b = get_bits(bs, 6); + s = g_deq_L12[ba*3 - 6 + b % 3]*(1 << 21 >> b/3); + } + *scf++ = s; + } + } +} + +static void L12_read_scale_info(const uint8_t *hdr, bs_t *bs, L12_scale_info *sci) +{ + static const uint8_t g_bitalloc_code_tab[] = { + 0,17, 3, 4, 5,6,7, 8,9,10,11,12,13,14,15,16, + 0,17,18, 3,19,4,5, 6,7, 8, 9,10,11,12,13,16, + 0,17,18, 3,19,4,5,16, + 0,17,18,16, + 0,17,18,19, 4,5,6, 7,8, 9,10,11,12,13,14,15, + 0,17,18, 3,19,4,5, 6,7, 8, 9,10,11,12,13,14, + 0, 2, 3, 4, 5,6,7, 8,9,10,11,12,13,14,15,16 + }; + const L12_subband_alloc_t *subband_alloc = L12_subband_alloc_table(hdr, sci); + + int i, k = 0, ba_bits = 0; + const uint8_t *ba_code_tab = g_bitalloc_code_tab; + + for (i = 0; i < sci->total_bands; i++) + { + uint8_t ba; + if (i == k) + { + k += subband_alloc->band_count; + ba_bits = subband_alloc->code_tab_width; + ba_code_tab = g_bitalloc_code_tab + subband_alloc->tab_offset; + subband_alloc++; + } + ba = ba_code_tab[get_bits(bs, ba_bits)]; + sci->bitalloc[2*i] = ba; + if (i < sci->stereo_bands) + { + ba = ba_code_tab[get_bits(bs, ba_bits)]; + } + sci->bitalloc[2*i + 1] = sci->stereo_bands ? ba : 0; + } + + for (i = 0; i < 2*sci->total_bands; i++) + { + sci->scfcod[i] = sci->bitalloc[i] ? HDR_IS_LAYER_1(hdr) ? 2 : get_bits(bs, 2) : 6; + } + + L12_read_scalefactors(bs, sci->bitalloc, sci->scfcod, sci->total_bands*2, sci->scf); + + for (i = sci->stereo_bands; i < sci->total_bands; i++) + { + sci->bitalloc[2*i + 1] = 0; + } +} + +static int L12_dequantize_granule(float *grbuf, bs_t *bs, L12_scale_info *sci, int group_size) +{ + int i, j, k, choff = 576; + for (j = 0; j < 4; j++) + { + float *dst = grbuf + group_size*j; + for (i = 0; i < 2*sci->total_bands; i++) + { + int ba = sci->bitalloc[i]; + if (ba != 0) + { + if (ba < 17) + { + int half = (1 << (ba - 1)) - 1; + for (k = 0; k < group_size; k++) + { + dst[k] = (float)((int)get_bits(bs, ba) - half); + } + } else + { + unsigned mod = (2 << (ba - 17)) + 1; /* 3, 5, 9 */ + unsigned code = get_bits(bs, mod + 2 - (mod >> 3)); /* 5, 7, 10 */ + for (k = 0; k < group_size; k++, code /= mod) + { + dst[k] = (float)((int)(code % mod - mod/2)); + } + } + } + dst += choff; + choff = 18 - choff; + } + } + return group_size*4; +} + +static void L12_apply_scf_384(L12_scale_info *sci, const float *scf, float *dst) +{ + int i, k; + memcpy(dst + 576 + sci->stereo_bands*18, dst + sci->stereo_bands*18, (sci->total_bands - sci->stereo_bands)*18*sizeof(float)); + for (i = 0; i < sci->total_bands; i++, dst += 18, scf += 6) + { + for (k = 0; k < 12; k++) + { + dst[k + 0] *= scf[0]; + dst[k + 576] *= scf[3]; + } + } +} +#endif /* MINIMP3_ONLY_MP3 */ + +static int L3_read_side_info(bs_t *bs, L3_gr_info_t *gr, const uint8_t *hdr) +{ + static const uint8_t g_scf_long[8][23] = { + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 12,12,12,12,12,12,16,20,24,28,32,40,48,56,64,76,90,2,2,2,2,2,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,18,22,26,32,38,46,54,62,70,76,36,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 4,4,4,4,4,4,6,6,8,8,10,12,16,20,24,28,34,42,50,54,76,158,0 }, + { 4,4,4,4,4,4,6,6,6,8,10,12,16,18,22,28,34,40,46,54,54,192,0 }, + { 4,4,4,4,4,4,6,6,8,10,12,16,20,24,30,38,46,56,68,84,102,26,0 } + }; + static const uint8_t g_scf_short[8][40] = { + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 8,8,8,8,8,8,8,8,8,12,12,12,16,16,16,20,20,20,24,24,24,28,28,28,36,36,36,2,2,2,2,2,2,2,2,2,26,26,26,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,6,6,6,8,8,8,10,10,10,14,14,14,18,18,18,26,26,26,32,32,32,42,42,42,18,18,18,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,32,32,32,44,44,44,12,12,12,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,22,22,22,30,30,30,56,56,56,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,6,6,6,10,10,10,12,12,12,14,14,14,16,16,16,20,20,20,26,26,26,66,66,66,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,12,12,12,16,16,16,20,20,20,26,26,26,34,34,34,42,42,42,12,12,12,0 } + }; + static const uint8_t g_scf_mixed[8][40] = { + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 12,12,12,4,4,4,8,8,8,12,12,12,16,16,16,20,20,20,24,24,24,28,28,28,36,36,36,2,2,2,2,2,2,2,2,2,26,26,26,0 }, + { 6,6,6,6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,14,14,14,18,18,18,26,26,26,32,32,32,42,42,42,18,18,18,0 }, + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,32,32,32,44,44,44,12,12,12,0 }, + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,22,22,22,30,30,30,56,56,56,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,6,6,6,10,10,10,12,12,12,14,14,14,16,16,16,20,20,20,26,26,26,66,66,66,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,8,8,8,12,12,12,16,16,16,20,20,20,26,26,26,34,34,34,42,42,42,12,12,12,0 } + }; + + unsigned tables, scfsi = 0; + int main_data_begin, part_23_sum = 0; + int sr_idx = HDR_GET_MY_SAMPLE_RATE(hdr); sr_idx -= (sr_idx != 0); + int gr_count = HDR_IS_MONO(hdr) ? 1 : 2; + + if (HDR_TEST_MPEG1(hdr)) + { + gr_count *= 2; + main_data_begin = get_bits(bs, 9); + scfsi = get_bits(bs, 7 + gr_count); + } else + { + main_data_begin = get_bits(bs, 8 + gr_count) >> gr_count; + } + + do + { + if (HDR_IS_MONO(hdr)) + { + scfsi <<= 4; + } + gr->part_23_length = (uint16_t)get_bits(bs, 12); + part_23_sum += gr->part_23_length; + gr->big_values = (uint16_t)get_bits(bs, 9); + if (gr->big_values > 288) + { + return -1; + } + gr->global_gain = (uint8_t)get_bits(bs, 8); + gr->scalefac_compress = (uint16_t)get_bits(bs, HDR_TEST_MPEG1(hdr) ? 4 : 9); + gr->sfbtab = g_scf_long[sr_idx]; + gr->n_long_sfb = 22; + gr->n_short_sfb = 0; + if (get_bits(bs, 1)) + { + gr->block_type = (uint8_t)get_bits(bs, 2); + if (!gr->block_type) + { + return -1; + } + gr->mixed_block_flag = (uint8_t)get_bits(bs, 1); + gr->region_count[0] = 7; + gr->region_count[1] = 255; + if (gr->block_type == SHORT_BLOCK_TYPE) + { + scfsi &= 0x0F0F; + if (!gr->mixed_block_flag) + { + gr->region_count[0] = 8; + gr->sfbtab = g_scf_short[sr_idx]; + gr->n_long_sfb = 0; + gr->n_short_sfb = 39; + } else + { + gr->sfbtab = g_scf_mixed[sr_idx]; + gr->n_long_sfb = HDR_TEST_MPEG1(hdr) ? 8 : 6; + gr->n_short_sfb = 30; + } + } + tables = get_bits(bs, 10); + tables <<= 5; + gr->subblock_gain[0] = (uint8_t)get_bits(bs, 3); + gr->subblock_gain[1] = (uint8_t)get_bits(bs, 3); + gr->subblock_gain[2] = (uint8_t)get_bits(bs, 3); + } else + { + gr->block_type = 0; + gr->mixed_block_flag = 0; + tables = get_bits(bs, 15); + gr->region_count[0] = (uint8_t)get_bits(bs, 4); + gr->region_count[1] = (uint8_t)get_bits(bs, 3); + gr->region_count[2] = 255; + } + gr->table_select[0] = (uint8_t)(tables >> 10); + gr->table_select[1] = (uint8_t)((tables >> 5) & 31); + gr->table_select[2] = (uint8_t)((tables) & 31); + gr->preflag = HDR_TEST_MPEG1(hdr) ? get_bits(bs, 1) : (gr->scalefac_compress >= 500); + gr->scalefac_scale = (uint8_t)get_bits(bs, 1); + gr->count1_table = (uint8_t)get_bits(bs, 1); + gr->scfsi = (uint8_t)((scfsi >> 12) & 15); + scfsi <<= 4; + gr++; + } while(--gr_count); + + if (part_23_sum + bs->pos > bs->limit + main_data_begin*8) + { + return -1; + } + + return main_data_begin; +} + +static void L3_read_scalefactors(uint8_t *scf, uint8_t *ist_pos, const uint8_t *scf_size, const uint8_t *scf_count, bs_t *bitbuf, int scfsi) +{ + int i, k; + for (i = 0; i < 4 && scf_count[i]; i++, scfsi *= 2) + { + int cnt = scf_count[i]; + if (scfsi & 8) + { + memcpy(scf, ist_pos, cnt); + } else + { + int bits = scf_size[i]; + if (!bits) + { + memset(scf, 0, cnt); + memset(ist_pos, 0, cnt); + } else + { + int max_scf = (scfsi < 0) ? (1 << bits) - 1 : -1; + for (k = 0; k < cnt; k++) + { + int s = get_bits(bitbuf, bits); + ist_pos[k] = (s == max_scf ? -1 : s); + scf[k] = s; + } + } + } + ist_pos += cnt; + scf += cnt; + } + scf[0] = scf[1] = scf[2] = 0; +} + +static float L3_ldexp_q2(float y, int exp_q2) +{ + static const float g_expfrac[4] = { 9.31322575e-10f,7.83145814e-10f,6.58544508e-10f,5.53767716e-10f }; + int e; + do + { + e = MINIMP3_MIN(30*4, exp_q2); + y *= g_expfrac[e & 3]*(1 << 30 >> (e >> 2)); + } while ((exp_q2 -= e) > 0); + return y; +} + +static void L3_decode_scalefactors(const uint8_t *hdr, uint8_t *ist_pos, bs_t *bs, const L3_gr_info_t *gr, float *scf, int ch) +{ + static const uint8_t g_scf_partitions[3][28] = { + { 6,5,5, 5,6,5,5,5,6,5, 7,3,11,10,0,0, 7, 7, 7,0, 6, 6,6,3, 8, 8,5,0 }, + { 8,9,6,12,6,9,9,9,6,9,12,6,15,18,0,0, 6,15,12,0, 6,12,9,6, 6,18,9,0 }, + { 9,9,6,12,9,9,9,9,9,9,12,6,18,18,0,0,12,12,12,0,12, 9,9,6,15,12,9,0 } + }; + const uint8_t *scf_partition = g_scf_partitions[!!gr->n_short_sfb + !gr->n_long_sfb]; + uint8_t scf_size[4], iscf[40]; + int i, scf_shift = gr->scalefac_scale + 1, gain_exp, scfsi = gr->scfsi; + float gain; + + if (HDR_TEST_MPEG1(hdr)) + { + static const uint8_t g_scfc_decode[16] = { 0,1,2,3, 12,5,6,7, 9,10,11,13, 14,15,18,19 }; + int part = g_scfc_decode[gr->scalefac_compress]; + scf_size[1] = scf_size[0] = (uint8_t)(part >> 2); + scf_size[3] = scf_size[2] = (uint8_t)(part & 3); + } else + { + static const uint8_t g_mod[6*4] = { 5,5,4,4,5,5,4,1,4,3,1,1,5,6,6,1,4,4,4,1,4,3,1,1 }; + int k, modprod, sfc, ist = HDR_TEST_I_STEREO(hdr) && ch; + sfc = gr->scalefac_compress >> ist; + for (k = ist*3*4; sfc >= 0; sfc -= modprod, k += 4) + { + for (modprod = 1, i = 3; i >= 0; i--) + { + scf_size[i] = (uint8_t)(sfc / modprod % g_mod[k + i]); + modprod *= g_mod[k + i]; + } + } + scf_partition += k; + scfsi = -16; + } + L3_read_scalefactors(iscf, ist_pos, scf_size, scf_partition, bs, scfsi); + + if (gr->n_short_sfb) + { + int sh = 3 - scf_shift; + for (i = 0; i < gr->n_short_sfb; i += 3) + { + iscf[gr->n_long_sfb + i + 0] += gr->subblock_gain[0] << sh; + iscf[gr->n_long_sfb + i + 1] += gr->subblock_gain[1] << sh; + iscf[gr->n_long_sfb + i + 2] += gr->subblock_gain[2] << sh; + } + } else if (gr->preflag) + { + static const uint8_t g_preamp[10] = { 1,1,1,1,2,2,3,3,3,2 }; + for (i = 0; i < 10; i++) + { + iscf[11 + i] += g_preamp[i]; + } + } + + gain_exp = gr->global_gain + BITS_DEQUANTIZER_OUT*4 - 210 - (HDR_IS_MS_STEREO(hdr) ? 2 : 0); + gain = L3_ldexp_q2(1 << (MAX_SCFI/4), MAX_SCFI - gain_exp); + for (i = 0; i < (int)(gr->n_long_sfb + gr->n_short_sfb); i++) + { + scf[i] = L3_ldexp_q2(gain, iscf[i] << scf_shift); + } +} + +static const float g_pow43[129 + 16] = { + 0,-1,-2.519842f,-4.326749f,-6.349604f,-8.549880f,-10.902724f,-13.390518f,-16.000000f,-18.720754f,-21.544347f,-24.463781f,-27.473142f,-30.567351f,-33.741992f,-36.993181f, + 0,1,2.519842f,4.326749f,6.349604f,8.549880f,10.902724f,13.390518f,16.000000f,18.720754f,21.544347f,24.463781f,27.473142f,30.567351f,33.741992f,36.993181f,40.317474f,43.711787f,47.173345f,50.699631f,54.288352f,57.937408f,61.644865f,65.408941f,69.227979f,73.100443f,77.024898f,81.000000f,85.024491f,89.097188f,93.216975f,97.382800f,101.593667f,105.848633f,110.146801f,114.487321f,118.869381f,123.292209f,127.755065f,132.257246f,136.798076f,141.376907f,145.993119f,150.646117f,155.335327f,160.060199f,164.820202f,169.614826f,174.443577f,179.305980f,184.201575f,189.129918f,194.090580f,199.083145f,204.107210f,209.162385f,214.248292f,219.364564f,224.510845f,229.686789f,234.892058f,240.126328f,245.389280f,250.680604f,256.000000f,261.347174f,266.721841f,272.123723f,277.552547f,283.008049f,288.489971f,293.998060f,299.532071f,305.091761f,310.676898f,316.287249f,321.922592f,327.582707f,333.267377f,338.976394f,344.709550f,350.466646f,356.247482f,362.051866f,367.879608f,373.730522f,379.604427f,385.501143f,391.420496f,397.362314f,403.326427f,409.312672f,415.320884f,421.350905f,427.402579f,433.475750f,439.570269f,445.685987f,451.822757f,457.980436f,464.158883f,470.357960f,476.577530f,482.817459f,489.077615f,495.357868f,501.658090f,507.978156f,514.317941f,520.677324f,527.056184f,533.454404f,539.871867f,546.308458f,552.764065f,559.238575f,565.731879f,572.243870f,578.774440f,585.323483f,591.890898f,598.476581f,605.080431f,611.702349f,618.342238f,625.000000f,631.675540f,638.368763f,645.079578f +}; + +static float L3_pow_43(int x) +{ + float frac; + int sign, mult = 256; + + if (x < 129) + { + return g_pow43[16 + x]; + } + + if (x < 1024) + { + mult = 16; + x <<= 3; + } + + sign = 2*x & 64; + frac = (float)((x & 63) - sign) / ((x & ~63) + sign); + return g_pow43[16 + ((x + sign) >> 6)]*(1.f + frac*((4.f/3) + frac*(2.f/9)))*mult; +} + +static void L3_huffman(float *dst, bs_t *bs, const L3_gr_info_t *gr_info, const float *scf, int layer3gr_limit) +{ + static const int16_t tabs[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 785,785,785,785,784,784,784,784,513,513,513,513,513,513,513,513,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256, + -255,1313,1298,1282,785,785,785,785,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,290,288, + -255,1313,1298,1282,769,769,769,769,529,529,529,529,529,529,529,529,528,528,528,528,528,528,528,528,512,512,512,512,512,512,512,512,290,288, + -253,-318,-351,-367,785,785,785,785,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,819,818,547,547,275,275,275,275,561,560,515,546,289,274,288,258, + -254,-287,1329,1299,1314,1312,1057,1057,1042,1042,1026,1026,784,784,784,784,529,529,529,529,529,529,529,529,769,769,769,769,768,768,768,768,563,560,306,306,291,259, + -252,-413,-477,-542,1298,-575,1041,1041,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-383,-399,1107,1092,1106,1061,849,849,789,789,1104,1091,773,773,1076,1075,341,340,325,309,834,804,577,577,532,532,516,516,832,818,803,816,561,561,531,531,515,546,289,289,288,258, + -252,-429,-493,-559,1057,1057,1042,1042,529,529,529,529,529,529,529,529,784,784,784,784,769,769,769,769,512,512,512,512,512,512,512,512,-382,1077,-415,1106,1061,1104,849,849,789,789,1091,1076,1029,1075,834,834,597,581,340,340,339,324,804,833,532,532,832,772,818,803,817,787,816,771,290,290,290,290,288,258, + -253,-349,-414,-447,-463,1329,1299,-479,1314,1312,1057,1057,1042,1042,1026,1026,785,785,785,785,784,784,784,784,769,769,769,769,768,768,768,768,-319,851,821,-335,836,850,805,849,341,340,325,336,533,533,579,579,564,564,773,832,578,548,563,516,321,276,306,291,304,259, + -251,-572,-733,-830,-863,-879,1041,1041,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-511,-527,-543,1396,1351,1381,1366,1395,1335,1380,-559,1334,1138,1138,1063,1063,1350,1392,1031,1031,1062,1062,1364,1363,1120,1120,1333,1348,881,881,881,881,375,374,359,373,343,358,341,325,791,791,1123,1122,-703,1105,1045,-719,865,865,790,790,774,774,1104,1029,338,293,323,308,-799,-815,833,788,772,818,803,816,322,292,307,320,561,531,515,546,289,274,288,258, + -251,-525,-605,-685,-765,-831,-846,1298,1057,1057,1312,1282,785,785,785,785,784,784,784,784,769,769,769,769,512,512,512,512,512,512,512,512,1399,1398,1383,1367,1382,1396,1351,-511,1381,1366,1139,1139,1079,1079,1124,1124,1364,1349,1363,1333,882,882,882,882,807,807,807,807,1094,1094,1136,1136,373,341,535,535,881,775,867,822,774,-591,324,338,-671,849,550,550,866,864,609,609,293,336,534,534,789,835,773,-751,834,804,308,307,833,788,832,772,562,562,547,547,305,275,560,515,290,290, + -252,-397,-477,-557,-622,-653,-719,-735,-750,1329,1299,1314,1057,1057,1042,1042,1312,1282,1024,1024,785,785,785,785,784,784,784,784,769,769,769,769,-383,1127,1141,1111,1126,1140,1095,1110,869,869,883,883,1079,1109,882,882,375,374,807,868,838,881,791,-463,867,822,368,263,852,837,836,-543,610,610,550,550,352,336,534,534,865,774,851,821,850,805,593,533,579,564,773,832,578,578,548,548,577,577,307,276,306,291,516,560,259,259, + -250,-2107,-2507,-2764,-2909,-2974,-3007,-3023,1041,1041,1040,1040,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-767,-1052,-1213,-1277,-1358,-1405,-1469,-1535,-1550,-1582,-1614,-1647,-1662,-1694,-1726,-1759,-1774,-1807,-1822,-1854,-1886,1565,-1919,-1935,-1951,-1967,1731,1730,1580,1717,-1983,1729,1564,-1999,1548,-2015,-2031,1715,1595,-2047,1714,-2063,1610,-2079,1609,-2095,1323,1323,1457,1457,1307,1307,1712,1547,1641,1700,1699,1594,1685,1625,1442,1442,1322,1322,-780,-973,-910,1279,1278,1277,1262,1276,1261,1275,1215,1260,1229,-959,974,974,989,989,-943,735,478,478,495,463,506,414,-1039,1003,958,1017,927,942,987,957,431,476,1272,1167,1228,-1183,1256,-1199,895,895,941,941,1242,1227,1212,1135,1014,1014,490,489,503,487,910,1013,985,925,863,894,970,955,1012,847,-1343,831,755,755,984,909,428,366,754,559,-1391,752,486,457,924,997,698,698,983,893,740,740,908,877,739,739,667,667,953,938,497,287,271,271,683,606,590,712,726,574,302,302,738,736,481,286,526,725,605,711,636,724,696,651,589,681,666,710,364,467,573,695,466,466,301,465,379,379,709,604,665,679,316,316,634,633,436,436,464,269,424,394,452,332,438,363,347,408,393,448,331,422,362,407,392,421,346,406,391,376,375,359,1441,1306,-2367,1290,-2383,1337,-2399,-2415,1426,1321,-2431,1411,1336,-2447,-2463,-2479,1169,1169,1049,1049,1424,1289,1412,1352,1319,-2495,1154,1154,1064,1064,1153,1153,416,390,360,404,403,389,344,374,373,343,358,372,327,357,342,311,356,326,1395,1394,1137,1137,1047,1047,1365,1392,1287,1379,1334,1364,1349,1378,1318,1363,792,792,792,792,1152,1152,1032,1032,1121,1121,1046,1046,1120,1120,1030,1030,-2895,1106,1061,1104,849,849,789,789,1091,1076,1029,1090,1060,1075,833,833,309,324,532,532,832,772,818,803,561,561,531,560,515,546,289,274,288,258, + -250,-1179,-1579,-1836,-1996,-2124,-2253,-2333,-2413,-2477,-2542,-2574,-2607,-2622,-2655,1314,1313,1298,1312,1282,785,785,785,785,1040,1040,1025,1025,768,768,768,768,-766,-798,-830,-862,-895,-911,-927,-943,-959,-975,-991,-1007,-1023,-1039,-1055,-1070,1724,1647,-1103,-1119,1631,1767,1662,1738,1708,1723,-1135,1780,1615,1779,1599,1677,1646,1778,1583,-1151,1777,1567,1737,1692,1765,1722,1707,1630,1751,1661,1764,1614,1736,1676,1763,1750,1645,1598,1721,1691,1762,1706,1582,1761,1566,-1167,1749,1629,767,766,751,765,494,494,735,764,719,749,734,763,447,447,748,718,477,506,431,491,446,476,461,505,415,430,475,445,504,399,460,489,414,503,383,474,429,459,502,502,746,752,488,398,501,473,413,472,486,271,480,270,-1439,-1455,1357,-1471,-1487,-1503,1341,1325,-1519,1489,1463,1403,1309,-1535,1372,1448,1418,1476,1356,1462,1387,-1551,1475,1340,1447,1402,1386,-1567,1068,1068,1474,1461,455,380,468,440,395,425,410,454,364,467,466,464,453,269,409,448,268,432,1371,1473,1432,1417,1308,1460,1355,1446,1459,1431,1083,1083,1401,1416,1458,1445,1067,1067,1370,1457,1051,1051,1291,1430,1385,1444,1354,1415,1400,1443,1082,1082,1173,1113,1186,1066,1185,1050,-1967,1158,1128,1172,1097,1171,1081,-1983,1157,1112,416,266,375,400,1170,1142,1127,1065,793,793,1169,1033,1156,1096,1141,1111,1155,1080,1126,1140,898,898,808,808,897,897,792,792,1095,1152,1032,1125,1110,1139,1079,1124,882,807,838,881,853,791,-2319,867,368,263,822,852,837,866,806,865,-2399,851,352,262,534,534,821,836,594,594,549,549,593,593,533,533,848,773,579,579,564,578,548,563,276,276,577,576,306,291,516,560,305,305,275,259, + -251,-892,-2058,-2620,-2828,-2957,-3023,-3039,1041,1041,1040,1040,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-511,-527,-543,-559,1530,-575,-591,1528,1527,1407,1526,1391,1023,1023,1023,1023,1525,1375,1268,1268,1103,1103,1087,1087,1039,1039,1523,-604,815,815,815,815,510,495,509,479,508,463,507,447,431,505,415,399,-734,-782,1262,-815,1259,1244,-831,1258,1228,-847,-863,1196,-879,1253,987,987,748,-767,493,493,462,477,414,414,686,669,478,446,461,445,474,429,487,458,412,471,1266,1264,1009,1009,799,799,-1019,-1276,-1452,-1581,-1677,-1757,-1821,-1886,-1933,-1997,1257,1257,1483,1468,1512,1422,1497,1406,1467,1496,1421,1510,1134,1134,1225,1225,1466,1451,1374,1405,1252,1252,1358,1480,1164,1164,1251,1251,1238,1238,1389,1465,-1407,1054,1101,-1423,1207,-1439,830,830,1248,1038,1237,1117,1223,1148,1236,1208,411,426,395,410,379,269,1193,1222,1132,1235,1221,1116,976,976,1192,1162,1177,1220,1131,1191,963,963,-1647,961,780,-1663,558,558,994,993,437,408,393,407,829,978,813,797,947,-1743,721,721,377,392,844,950,828,890,706,706,812,859,796,960,948,843,934,874,571,571,-1919,690,555,689,421,346,539,539,944,779,918,873,932,842,903,888,570,570,931,917,674,674,-2575,1562,-2591,1609,-2607,1654,1322,1322,1441,1441,1696,1546,1683,1593,1669,1624,1426,1426,1321,1321,1639,1680,1425,1425,1305,1305,1545,1668,1608,1623,1667,1592,1638,1666,1320,1320,1652,1607,1409,1409,1304,1304,1288,1288,1664,1637,1395,1395,1335,1335,1622,1636,1394,1394,1319,1319,1606,1621,1392,1392,1137,1137,1137,1137,345,390,360,375,404,373,1047,-2751,-2767,-2783,1062,1121,1046,-2799,1077,-2815,1106,1061,789,789,1105,1104,263,355,310,340,325,354,352,262,339,324,1091,1076,1029,1090,1060,1075,833,833,788,788,1088,1028,818,818,803,803,561,561,531,531,816,771,546,546,289,274,288,258, + -253,-317,-381,-446,-478,-509,1279,1279,-811,-1179,-1451,-1756,-1900,-2028,-2189,-2253,-2333,-2414,-2445,-2511,-2526,1313,1298,-2559,1041,1041,1040,1040,1025,1025,1024,1024,1022,1007,1021,991,1020,975,1019,959,687,687,1018,1017,671,671,655,655,1016,1015,639,639,758,758,623,623,757,607,756,591,755,575,754,559,543,543,1009,783,-575,-621,-685,-749,496,-590,750,749,734,748,974,989,1003,958,988,973,1002,942,987,957,972,1001,926,986,941,971,956,1000,910,985,925,999,894,970,-1071,-1087,-1102,1390,-1135,1436,1509,1451,1374,-1151,1405,1358,1480,1420,-1167,1507,1494,1389,1342,1465,1435,1450,1326,1505,1310,1493,1373,1479,1404,1492,1464,1419,428,443,472,397,736,526,464,464,486,457,442,471,484,482,1357,1449,1434,1478,1388,1491,1341,1490,1325,1489,1463,1403,1309,1477,1372,1448,1418,1433,1476,1356,1462,1387,-1439,1475,1340,1447,1402,1474,1324,1461,1371,1473,269,448,1432,1417,1308,1460,-1711,1459,-1727,1441,1099,1099,1446,1386,1431,1401,-1743,1289,1083,1083,1160,1160,1458,1445,1067,1067,1370,1457,1307,1430,1129,1129,1098,1098,268,432,267,416,266,400,-1887,1144,1187,1082,1173,1113,1186,1066,1050,1158,1128,1143,1172,1097,1171,1081,420,391,1157,1112,1170,1142,1127,1065,1169,1049,1156,1096,1141,1111,1155,1080,1126,1154,1064,1153,1140,1095,1048,-2159,1125,1110,1137,-2175,823,823,1139,1138,807,807,384,264,368,263,868,838,853,791,867,822,852,837,866,806,865,790,-2319,851,821,836,352,262,850,805,849,-2399,533,533,835,820,336,261,578,548,563,577,532,532,832,772,562,562,547,547,305,275,560,515,290,290,288,258 }; + static const uint8_t tab32[] = { 130,162,193,209,44,28,76,140,9,9,9,9,9,9,9,9,190,254,222,238,126,94,157,157,109,61,173,205 }; + static const uint8_t tab33[] = { 252,236,220,204,188,172,156,140,124,108,92,76,60,44,28,12 }; + static const int16_t tabindex[2*16] = { 0,32,64,98,0,132,180,218,292,364,426,538,648,746,0,1126,1460,1460,1460,1460,1460,1460,1460,1460,1842,1842,1842,1842,1842,1842,1842,1842 }; + static const uint8_t g_linbits[] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,3,4,6,8,10,13,4,5,6,7,8,9,11,13 }; + +#define PEEK_BITS(n) (bs_cache >> (32 - n)) +#define FLUSH_BITS(n) { bs_cache <<= (n); bs_sh += (n); } +#define CHECK_BITS while (bs_sh >= 0) { bs_cache |= (uint32_t)*bs_next_ptr++ << bs_sh; bs_sh -= 8; } +#define BSPOS ((bs_next_ptr - bs->buf)*8 - 24 + bs_sh) + + float one = 0.0f; + int ireg = 0, big_val_cnt = gr_info->big_values; + const uint8_t *sfb = gr_info->sfbtab; + const uint8_t *bs_next_ptr = bs->buf + bs->pos/8; + uint32_t bs_cache = (((bs_next_ptr[0]*256u + bs_next_ptr[1])*256u + bs_next_ptr[2])*256u + bs_next_ptr[3]) << (bs->pos & 7); + int pairs_to_decode, np, bs_sh = (bs->pos & 7) - 8; + bs_next_ptr += 4; + + while (big_val_cnt > 0) + { + int tab_num = gr_info->table_select[ireg]; + int sfb_cnt = gr_info->region_count[ireg++]; + const int16_t *codebook = tabs + tabindex[tab_num]; + int linbits = g_linbits[tab_num]; + if (linbits) + { + do + { + np = *sfb++ / 2; + pairs_to_decode = MINIMP3_MIN(big_val_cnt, np); + one = *scf++; + do + { + int j, w = 5; + int leaf = codebook[PEEK_BITS(w)]; + while (leaf < 0) + { + FLUSH_BITS(w); + w = leaf & 7; + leaf = codebook[PEEK_BITS(w) - (leaf >> 3)]; + } + FLUSH_BITS(leaf >> 8); + + for (j = 0; j < 2; j++, dst++, leaf >>= 4) + { + int lsb = leaf & 0x0F; + if (lsb == 15) + { + lsb += PEEK_BITS(linbits); + FLUSH_BITS(linbits); + CHECK_BITS; + *dst = one*L3_pow_43(lsb)*((int32_t)bs_cache < 0 ? -1: 1); + } else + { + *dst = g_pow43[16 + lsb - 16*(bs_cache >> 31)]*one; + } + FLUSH_BITS(lsb ? 1 : 0); + } + CHECK_BITS; + } while (--pairs_to_decode); + } while ((big_val_cnt -= np) > 0 && --sfb_cnt >= 0); + } else + { + do + { + np = *sfb++ / 2; + pairs_to_decode = MINIMP3_MIN(big_val_cnt, np); + one = *scf++; + do + { + int j, w = 5; + int leaf = codebook[PEEK_BITS(w)]; + while (leaf < 0) + { + FLUSH_BITS(w); + w = leaf & 7; + leaf = codebook[PEEK_BITS(w) - (leaf >> 3)]; + } + FLUSH_BITS(leaf >> 8); + + for (j = 0; j < 2; j++, dst++, leaf >>= 4) + { + int lsb = leaf & 0x0F; + *dst = g_pow43[16 + lsb - 16*(bs_cache >> 31)]*one; + FLUSH_BITS(lsb ? 1 : 0); + } + CHECK_BITS; + } while (--pairs_to_decode); + } while ((big_val_cnt -= np) > 0 && --sfb_cnt >= 0); + } + } + + for (np = 1 - big_val_cnt;; dst += 4) + { + const uint8_t *codebook_count1 = (gr_info->count1_table) ? tab33 : tab32; + int leaf = codebook_count1[PEEK_BITS(4)]; + if (!(leaf & 8)) + { + leaf = codebook_count1[(leaf >> 3) + (bs_cache << 4 >> (32 - (leaf & 3)))]; + } + FLUSH_BITS(leaf & 7); + if (BSPOS > layer3gr_limit) + { + break; + } +#define RELOAD_SCALEFACTOR if (!--np) { np = *sfb++/2; if (!np) break; one = *scf++; } +#define DEQ_COUNT1(s) if (leaf & (128 >> s)) { dst[s] = ((int32_t)bs_cache < 0) ? -one : one; FLUSH_BITS(1) } + RELOAD_SCALEFACTOR; + DEQ_COUNT1(0); + DEQ_COUNT1(1); + RELOAD_SCALEFACTOR; + DEQ_COUNT1(2); + DEQ_COUNT1(3); + CHECK_BITS; + } + + bs->pos = layer3gr_limit; +} + +static void L3_midside_stereo(float *left, int n) +{ + int i = 0; + float *right = left + 576; +#if HAVE_SIMD + if (have_simd()) + { + for (; i < n - 3; i += 4) + { + f4 vl = VLD(left + i); + f4 vr = VLD(right + i); + VSTORE(left + i, VADD(vl, vr)); + VSTORE(right + i, VSUB(vl, vr)); + } +#ifdef __GNUC__ + /* Workaround for spurious -Waggressive-loop-optimizations warning from gcc. + * For more info see: https://github.com/lieff/minimp3/issues/88 + */ + if (__builtin_constant_p(n % 4 == 0) && n % 4 == 0) + return; +#endif + } +#endif /* HAVE_SIMD */ + for (; i < n; i++) + { + float a = left[i]; + float b = right[i]; + left[i] = a + b; + right[i] = a - b; + } +} + +static void L3_intensity_stereo_band(float *left, int n, float kl, float kr) +{ + int i; + for (i = 0; i < n; i++) + { + left[i + 576] = left[i]*kr; + left[i] = left[i]*kl; + } +} + +static void L3_stereo_top_band(const float *right, const uint8_t *sfb, int nbands, int max_band[3]) +{ + int i, k; + + max_band[0] = max_band[1] = max_band[2] = -1; + + for (i = 0; i < nbands; i++) + { + for (k = 0; k < sfb[i]; k += 2) + { + if (right[k] != 0 || right[k + 1] != 0) + { + max_band[i % 3] = i; + break; + } + } + right += sfb[i]; + } +} + +static void L3_stereo_process(float *left, const uint8_t *ist_pos, const uint8_t *sfb, const uint8_t *hdr, int max_band[3], int mpeg2_sh) +{ + static const float g_pan[7*2] = { 0,1,0.21132487f,0.78867513f,0.36602540f,0.63397460f,0.5f,0.5f,0.63397460f,0.36602540f,0.78867513f,0.21132487f,1,0 }; + unsigned i, max_pos = HDR_TEST_MPEG1(hdr) ? 7 : 64; + + for (i = 0; sfb[i]; i++) + { + unsigned ipos = ist_pos[i]; + if ((int)i > max_band[i % 3] && ipos < max_pos) + { + float kl, kr, s = HDR_TEST_MS_STEREO(hdr) ? 1.41421356f : 1; + if (HDR_TEST_MPEG1(hdr)) + { + kl = g_pan[2*ipos]; + kr = g_pan[2*ipos + 1]; + } else + { + kl = 1; + kr = L3_ldexp_q2(1, (ipos + 1) >> 1 << mpeg2_sh); + if (ipos & 1) + { + kl = kr; + kr = 1; + } + } + L3_intensity_stereo_band(left, sfb[i], kl*s, kr*s); + } else if (HDR_TEST_MS_STEREO(hdr)) + { + L3_midside_stereo(left, sfb[i]); + } + left += sfb[i]; + } +} + +static void L3_intensity_stereo(float *left, uint8_t *ist_pos, const L3_gr_info_t *gr, const uint8_t *hdr) +{ + int max_band[3], n_sfb = gr->n_long_sfb + gr->n_short_sfb; + int i, max_blocks = gr->n_short_sfb ? 3 : 1; + + L3_stereo_top_band(left + 576, gr->sfbtab, n_sfb, max_band); + if (gr->n_long_sfb) + { + max_band[0] = max_band[1] = max_band[2] = MINIMP3_MAX(MINIMP3_MAX(max_band[0], max_band[1]), max_band[2]); + } + for (i = 0; i < max_blocks; i++) + { + int default_pos = HDR_TEST_MPEG1(hdr) ? 3 : 0; + int itop = n_sfb - max_blocks + i; + int prev = itop - max_blocks; + ist_pos[itop] = max_band[i] >= prev ? default_pos : ist_pos[prev]; + } + L3_stereo_process(left, ist_pos, gr->sfbtab, hdr, max_band, gr[1].scalefac_compress & 1); +} + +static void L3_reorder(float *grbuf, float *scratch, const uint8_t *sfb) +{ + int i, len; + float *src = grbuf, *dst = scratch; + + for (;0 != (len = *sfb); sfb += 3, src += 2*len) + { + for (i = 0; i < len; i++, src++) + { + *dst++ = src[0*len]; + *dst++ = src[1*len]; + *dst++ = src[2*len]; + } + } + memcpy(grbuf, scratch, (dst - scratch)*sizeof(float)); +} + +static void L3_antialias(float *grbuf, int nbands) +{ + static const float g_aa[2][8] = { + {0.85749293f,0.88174200f,0.94962865f,0.98331459f,0.99551782f,0.99916056f,0.99989920f,0.99999316f}, + {0.51449576f,0.47173197f,0.31337745f,0.18191320f,0.09457419f,0.04096558f,0.01419856f,0.00369997f} + }; + + for (; nbands > 0; nbands--, grbuf += 18) + { + int i = 0; +#if HAVE_SIMD + if (have_simd()) for (; i < 8; i += 4) + { + f4 vu = VLD(grbuf + 18 + i); + f4 vd = VLD(grbuf + 14 - i); + f4 vc0 = VLD(g_aa[0] + i); + f4 vc1 = VLD(g_aa[1] + i); + vd = VREV(vd); + VSTORE(grbuf + 18 + i, VSUB(VMUL(vu, vc0), VMUL(vd, vc1))); + vd = VADD(VMUL(vu, vc1), VMUL(vd, vc0)); + VSTORE(grbuf + 14 - i, VREV(vd)); + } +#endif /* HAVE_SIMD */ +#ifndef MINIMP3_ONLY_SIMD + for(; i < 8; i++) + { + float u = grbuf[18 + i]; + float d = grbuf[17 - i]; + grbuf[18 + i] = u*g_aa[0][i] - d*g_aa[1][i]; + grbuf[17 - i] = u*g_aa[1][i] + d*g_aa[0][i]; + } +#endif /* MINIMP3_ONLY_SIMD */ + } +} + +static void L3_dct3_9(float *y) +{ + float s0, s1, s2, s3, s4, s5, s6, s7, s8, t0, t2, t4; + + s0 = y[0]; s2 = y[2]; s4 = y[4]; s6 = y[6]; s8 = y[8]; + t0 = s0 + s6*0.5f; + s0 -= s6; + t4 = (s4 + s2)*0.93969262f; + t2 = (s8 + s2)*0.76604444f; + s6 = (s4 - s8)*0.17364818f; + s4 += s8 - s2; + + s2 = s0 - s4*0.5f; + y[4] = s4 + s0; + s8 = t0 - t2 + s6; + s0 = t0 - t4 + t2; + s4 = t0 + t4 - s6; + + s1 = y[1]; s3 = y[3]; s5 = y[5]; s7 = y[7]; + + s3 *= 0.86602540f; + t0 = (s5 + s1)*0.98480775f; + t4 = (s5 - s7)*0.34202014f; + t2 = (s1 + s7)*0.64278761f; + s1 = (s1 - s5 - s7)*0.86602540f; + + s5 = t0 - s3 - t2; + s7 = t4 - s3 - t0; + s3 = t4 + s3 - t2; + + y[0] = s4 - s7; + y[1] = s2 + s1; + y[2] = s0 - s3; + y[3] = s8 + s5; + y[5] = s8 - s5; + y[6] = s0 + s3; + y[7] = s2 - s1; + y[8] = s4 + s7; +} + +static void L3_imdct36(float *grbuf, float *overlap, const float *window, int nbands) +{ + int i, j; + static const float g_twid9[18] = { + 0.73727734f,0.79335334f,0.84339145f,0.88701083f,0.92387953f,0.95371695f,0.97629601f,0.99144486f,0.99904822f,0.67559021f,0.60876143f,0.53729961f,0.46174861f,0.38268343f,0.30070580f,0.21643961f,0.13052619f,0.04361938f + }; + + for (j = 0; j < nbands; j++, grbuf += 18, overlap += 9) + { + float co[9], si[9]; + co[0] = -grbuf[0]; + si[0] = grbuf[17]; + for (i = 0; i < 4; i++) + { + si[8 - 2*i] = grbuf[4*i + 1] - grbuf[4*i + 2]; + co[1 + 2*i] = grbuf[4*i + 1] + grbuf[4*i + 2]; + si[7 - 2*i] = grbuf[4*i + 4] - grbuf[4*i + 3]; + co[2 + 2*i] = -(grbuf[4*i + 3] + grbuf[4*i + 4]); + } + L3_dct3_9(co); + L3_dct3_9(si); + + si[1] = -si[1]; + si[3] = -si[3]; + si[5] = -si[5]; + si[7] = -si[7]; + + i = 0; + +#if HAVE_SIMD + if (have_simd()) for (; i < 8; i += 4) + { + f4 vovl = VLD(overlap + i); + f4 vc = VLD(co + i); + f4 vs = VLD(si + i); + f4 vr0 = VLD(g_twid9 + i); + f4 vr1 = VLD(g_twid9 + 9 + i); + f4 vw0 = VLD(window + i); + f4 vw1 = VLD(window + 9 + i); + f4 vsum = VADD(VMUL(vc, vr1), VMUL(vs, vr0)); + VSTORE(overlap + i, VSUB(VMUL(vc, vr0), VMUL(vs, vr1))); + VSTORE(grbuf + i, VSUB(VMUL(vovl, vw0), VMUL(vsum, vw1))); + vsum = VADD(VMUL(vovl, vw1), VMUL(vsum, vw0)); + VSTORE(grbuf + 14 - i, VREV(vsum)); + } +#endif /* HAVE_SIMD */ + for (; i < 9; i++) + { + float ovl = overlap[i]; + float sum = co[i]*g_twid9[9 + i] + si[i]*g_twid9[0 + i]; + overlap[i] = co[i]*g_twid9[0 + i] - si[i]*g_twid9[9 + i]; + grbuf[i] = ovl*window[0 + i] - sum*window[9 + i]; + grbuf[17 - i] = ovl*window[9 + i] + sum*window[0 + i]; + } + } +} + +static void L3_idct3(float x0, float x1, float x2, float *dst) +{ + float m1 = x1*0.86602540f; + float a1 = x0 - x2*0.5f; + dst[1] = x0 + x2; + dst[0] = a1 + m1; + dst[2] = a1 - m1; +} + +static void L3_imdct12(float *x, float *dst, float *overlap) +{ + static const float g_twid3[6] = { 0.79335334f,0.92387953f,0.99144486f, 0.60876143f,0.38268343f,0.13052619f }; + float co[3], si[3]; + int i; + + L3_idct3(-x[0], x[6] + x[3], x[12] + x[9], co); + L3_idct3(x[15], x[12] - x[9], x[6] - x[3], si); + si[1] = -si[1]; + + for (i = 0; i < 3; i++) + { + float ovl = overlap[i]; + float sum = co[i]*g_twid3[3 + i] + si[i]*g_twid3[0 + i]; + overlap[i] = co[i]*g_twid3[0 + i] - si[i]*g_twid3[3 + i]; + dst[i] = ovl*g_twid3[2 - i] - sum*g_twid3[5 - i]; + dst[5 - i] = ovl*g_twid3[5 - i] + sum*g_twid3[2 - i]; + } +} + +static void L3_imdct_short(float *grbuf, float *overlap, int nbands) +{ + for (;nbands > 0; nbands--, overlap += 9, grbuf += 18) + { + float tmp[18]; + memcpy(tmp, grbuf, sizeof(tmp)); + memcpy(grbuf, overlap, 6*sizeof(float)); + L3_imdct12(tmp, grbuf + 6, overlap + 6); + L3_imdct12(tmp + 1, grbuf + 12, overlap + 6); + L3_imdct12(tmp + 2, overlap, overlap + 6); + } +} + +static void L3_change_sign(float *grbuf) +{ + int b, i; + for (b = 0, grbuf += 18; b < 32; b += 2, grbuf += 36) + for (i = 1; i < 18; i += 2) + grbuf[i] = -grbuf[i]; +} + +static void L3_imdct_gr(float *grbuf, float *overlap, unsigned block_type, unsigned n_long_bands) +{ + static const float g_mdct_window[2][18] = { + { 0.99904822f,0.99144486f,0.97629601f,0.95371695f,0.92387953f,0.88701083f,0.84339145f,0.79335334f,0.73727734f,0.04361938f,0.13052619f,0.21643961f,0.30070580f,0.38268343f,0.46174861f,0.53729961f,0.60876143f,0.67559021f }, + { 1,1,1,1,1,1,0.99144486f,0.92387953f,0.79335334f,0,0,0,0,0,0,0.13052619f,0.38268343f,0.60876143f } + }; + if (n_long_bands) + { + L3_imdct36(grbuf, overlap, g_mdct_window[0], n_long_bands); + grbuf += 18*n_long_bands; + overlap += 9*n_long_bands; + } + if (block_type == SHORT_BLOCK_TYPE) + L3_imdct_short(grbuf, overlap, 32 - n_long_bands); + else + L3_imdct36(grbuf, overlap, g_mdct_window[block_type == STOP_BLOCK_TYPE], 32 - n_long_bands); +} + +static void L3_save_reservoir(mp3dec_t *h, mp3dec_scratch_t *s) +{ + int pos = (s->bs.pos + 7)/8u; + int remains = s->bs.limit/8u - pos; + if (remains > MAX_BITRESERVOIR_BYTES) + { + pos += remains - MAX_BITRESERVOIR_BYTES; + remains = MAX_BITRESERVOIR_BYTES; + } + if (remains > 0) + { + memmove(h->reserv_buf, s->maindata + pos, remains); + } + h->reserv = remains; +} + +static int L3_restore_reservoir(mp3dec_t *h, bs_t *bs, mp3dec_scratch_t *s, int main_data_begin) +{ + int frame_bytes = (bs->limit - bs->pos)/8; + int bytes_have = MINIMP3_MIN(h->reserv, main_data_begin); + memcpy(s->maindata, h->reserv_buf + MINIMP3_MAX(0, h->reserv - main_data_begin), MINIMP3_MIN(h->reserv, main_data_begin)); + memcpy(s->maindata + bytes_have, bs->buf + bs->pos/8, frame_bytes); + bs_init(&s->bs, s->maindata, bytes_have + frame_bytes); + return h->reserv >= main_data_begin; +} + +static void L3_decode(mp3dec_t *h, mp3dec_scratch_t *s, L3_gr_info_t *gr_info, int nch) +{ + int ch; + + for (ch = 0; ch < nch; ch++) + { + int layer3gr_limit = s->bs.pos + gr_info[ch].part_23_length; + L3_decode_scalefactors(h->header, s->ist_pos[ch], &s->bs, gr_info + ch, s->scf, ch); + L3_huffman(s->grbuf[ch], &s->bs, gr_info + ch, s->scf, layer3gr_limit); + } + + if (HDR_TEST_I_STEREO(h->header)) + { + L3_intensity_stereo(s->grbuf[0], s->ist_pos[1], gr_info, h->header); + } else if (HDR_IS_MS_STEREO(h->header)) + { + L3_midside_stereo(s->grbuf[0], 576); + } + + for (ch = 0; ch < nch; ch++, gr_info++) + { + int aa_bands = 31; + int n_long_bands = (gr_info->mixed_block_flag ? 2 : 0) << (int)(HDR_GET_MY_SAMPLE_RATE(h->header) == 2); + + if (gr_info->n_short_sfb) + { + aa_bands = n_long_bands - 1; + L3_reorder(s->grbuf[ch] + n_long_bands*18, s->syn[0], gr_info->sfbtab + gr_info->n_long_sfb); + } + + L3_antialias(s->grbuf[ch], aa_bands); + L3_imdct_gr(s->grbuf[ch], h->mdct_overlap[ch], gr_info->block_type, n_long_bands); + L3_change_sign(s->grbuf[ch]); + } +} + +static void mp3d_DCT_II(float *grbuf, int n) +{ + static const float g_sec[24] = { + 10.19000816f,0.50060302f,0.50241929f,3.40760851f,0.50547093f,0.52249861f,2.05778098f,0.51544732f,0.56694406f,1.48416460f,0.53104258f,0.64682180f,1.16943991f,0.55310392f,0.78815460f,0.97256821f,0.58293498f,1.06067765f,0.83934963f,0.62250412f,1.72244716f,0.74453628f,0.67480832f,5.10114861f + }; + int i, k = 0; +#if HAVE_SIMD + if (have_simd()) for (; k < n; k += 4) + { + f4 t[4][8], *x; + float *y = grbuf + k; + + for (x = t[0], i = 0; i < 8; i++, x++) + { + f4 x0 = VLD(&y[i*18]); + f4 x1 = VLD(&y[(15 - i)*18]); + f4 x2 = VLD(&y[(16 + i)*18]); + f4 x3 = VLD(&y[(31 - i)*18]); + f4 t0 = VADD(x0, x3); + f4 t1 = VADD(x1, x2); + f4 t2 = VMUL_S(VSUB(x1, x2), g_sec[3*i + 0]); + f4 t3 = VMUL_S(VSUB(x0, x3), g_sec[3*i + 1]); + x[0] = VADD(t0, t1); + x[8] = VMUL_S(VSUB(t0, t1), g_sec[3*i + 2]); + x[16] = VADD(t3, t2); + x[24] = VMUL_S(VSUB(t3, t2), g_sec[3*i + 2]); + } + for (x = t[0], i = 0; i < 4; i++, x += 8) + { + f4 x0 = x[0], x1 = x[1], x2 = x[2], x3 = x[3], x4 = x[4], x5 = x[5], x6 = x[6], x7 = x[7], xt; + xt = VSUB(x0, x7); x0 = VADD(x0, x7); + x7 = VSUB(x1, x6); x1 = VADD(x1, x6); + x6 = VSUB(x2, x5); x2 = VADD(x2, x5); + x5 = VSUB(x3, x4); x3 = VADD(x3, x4); + x4 = VSUB(x0, x3); x0 = VADD(x0, x3); + x3 = VSUB(x1, x2); x1 = VADD(x1, x2); + x[0] = VADD(x0, x1); + x[4] = VMUL_S(VSUB(x0, x1), 0.70710677f); + x5 = VADD(x5, x6); + x6 = VMUL_S(VADD(x6, x7), 0.70710677f); + x7 = VADD(x7, xt); + x3 = VMUL_S(VADD(x3, x4), 0.70710677f); + x5 = VSUB(x5, VMUL_S(x7, 0.198912367f)); /* rotate by PI/8 */ + x7 = VADD(x7, VMUL_S(x5, 0.382683432f)); + x5 = VSUB(x5, VMUL_S(x7, 0.198912367f)); + x0 = VSUB(xt, x6); xt = VADD(xt, x6); + x[1] = VMUL_S(VADD(xt, x7), 0.50979561f); + x[2] = VMUL_S(VADD(x4, x3), 0.54119611f); + x[3] = VMUL_S(VSUB(x0, x5), 0.60134488f); + x[5] = VMUL_S(VADD(x0, x5), 0.89997619f); + x[6] = VMUL_S(VSUB(x4, x3), 1.30656302f); + x[7] = VMUL_S(VSUB(xt, x7), 2.56291556f); + } + + if (k > n - 3) + { +#if HAVE_SSE +#define VSAVE2(i, v) _mm_storel_pi((__m64 *)(void*)&y[i*18], v) +#else /* HAVE_SSE */ +#define VSAVE2(i, v) vst1_f32((float32_t *)&y[i*18], vget_low_f32(v)) +#endif /* HAVE_SSE */ + for (i = 0; i < 7; i++, y += 4*18) + { + f4 s = VADD(t[3][i], t[3][i + 1]); + VSAVE2(0, t[0][i]); + VSAVE2(1, VADD(t[2][i], s)); + VSAVE2(2, VADD(t[1][i], t[1][i + 1])); + VSAVE2(3, VADD(t[2][1 + i], s)); + } + VSAVE2(0, t[0][7]); + VSAVE2(1, VADD(t[2][7], t[3][7])); + VSAVE2(2, t[1][7]); + VSAVE2(3, t[3][7]); + } else + { +#define VSAVE4(i, v) VSTORE(&y[i*18], v) + for (i = 0; i < 7; i++, y += 4*18) + { + f4 s = VADD(t[3][i], t[3][i + 1]); + VSAVE4(0, t[0][i]); + VSAVE4(1, VADD(t[2][i], s)); + VSAVE4(2, VADD(t[1][i], t[1][i + 1])); + VSAVE4(3, VADD(t[2][1 + i], s)); + } + VSAVE4(0, t[0][7]); + VSAVE4(1, VADD(t[2][7], t[3][7])); + VSAVE4(2, t[1][7]); + VSAVE4(3, t[3][7]); + } + } else +#endif /* HAVE_SIMD */ +#ifdef MINIMP3_ONLY_SIMD + {} /* for HAVE_SIMD=1, MINIMP3_ONLY_SIMD=1 case we do not need non-intrinsic "else" branch */ +#else /* MINIMP3_ONLY_SIMD */ + for (; k < n; k++) + { + float t[4][8], *x, *y = grbuf + k; + + for (x = t[0], i = 0; i < 8; i++, x++) + { + float x0 = y[i*18]; + float x1 = y[(15 - i)*18]; + float x2 = y[(16 + i)*18]; + float x3 = y[(31 - i)*18]; + float t0 = x0 + x3; + float t1 = x1 + x2; + float t2 = (x1 - x2)*g_sec[3*i + 0]; + float t3 = (x0 - x3)*g_sec[3*i + 1]; + x[0] = t0 + t1; + x[8] = (t0 - t1)*g_sec[3*i + 2]; + x[16] = t3 + t2; + x[24] = (t3 - t2)*g_sec[3*i + 2]; + } + for (x = t[0], i = 0; i < 4; i++, x += 8) + { + float x0 = x[0], x1 = x[1], x2 = x[2], x3 = x[3], x4 = x[4], x5 = x[5], x6 = x[6], x7 = x[7], xt; + xt = x0 - x7; x0 += x7; + x7 = x1 - x6; x1 += x6; + x6 = x2 - x5; x2 += x5; + x5 = x3 - x4; x3 += x4; + x4 = x0 - x3; x0 += x3; + x3 = x1 - x2; x1 += x2; + x[0] = x0 + x1; + x[4] = (x0 - x1)*0.70710677f; + x5 = x5 + x6; + x6 = (x6 + x7)*0.70710677f; + x7 = x7 + xt; + x3 = (x3 + x4)*0.70710677f; + x5 -= x7*0.198912367f; /* rotate by PI/8 */ + x7 += x5*0.382683432f; + x5 -= x7*0.198912367f; + x0 = xt - x6; xt += x6; + x[1] = (xt + x7)*0.50979561f; + x[2] = (x4 + x3)*0.54119611f; + x[3] = (x0 - x5)*0.60134488f; + x[5] = (x0 + x5)*0.89997619f; + x[6] = (x4 - x3)*1.30656302f; + x[7] = (xt - x7)*2.56291556f; + + } + for (i = 0; i < 7; i++, y += 4*18) + { + y[0*18] = t[0][i]; + y[1*18] = t[2][i] + t[3][i] + t[3][i + 1]; + y[2*18] = t[1][i] + t[1][i + 1]; + y[3*18] = t[2][i + 1] + t[3][i] + t[3][i + 1]; + } + y[0*18] = t[0][7]; + y[1*18] = t[2][7] + t[3][7]; + y[2*18] = t[1][7]; + y[3*18] = t[3][7]; + } +#endif /* MINIMP3_ONLY_SIMD */ +} + +#ifndef MINIMP3_FLOAT_OUTPUT +static int16_t mp3d_scale_pcm(float sample) +{ +#if HAVE_ARMV6 + int32_t s32 = (int32_t)(sample + .5f); + s32 -= (s32 < 0); + int16_t s = (int16_t)minimp3_clip_int16_arm(s32); +#else + if (sample >= 32766.5) return (int16_t) 32767; + if (sample <= -32767.5) return (int16_t)-32768; + int16_t s = (int16_t)(sample + .5f); + s -= (s < 0); /* away from zero, to be compliant */ +#endif + return s; +} +#else /* MINIMP3_FLOAT_OUTPUT */ +static float mp3d_scale_pcm(float sample) +{ + return sample*(1.f/32768.f); +} +#endif /* MINIMP3_FLOAT_OUTPUT */ + +static void mp3d_synth_pair(mp3d_sample_t *pcm, int nch, const float *z) +{ + float a; + a = (z[14*64] - z[ 0]) * 29; + a += (z[ 1*64] + z[13*64]) * 213; + a += (z[12*64] - z[ 2*64]) * 459; + a += (z[ 3*64] + z[11*64]) * 2037; + a += (z[10*64] - z[ 4*64]) * 5153; + a += (z[ 5*64] + z[ 9*64]) * 6574; + a += (z[ 8*64] - z[ 6*64]) * 37489; + a += z[ 7*64] * 75038; + pcm[0] = mp3d_scale_pcm(a); + + z += 2; + a = z[14*64] * 104; + a += z[12*64] * 1567; + a += z[10*64] * 9727; + a += z[ 8*64] * 64019; + a += z[ 6*64] * -9975; + a += z[ 4*64] * -45; + a += z[ 2*64] * 146; + a += z[ 0*64] * -5; + pcm[16*nch] = mp3d_scale_pcm(a); +} + +static void mp3d_synth(float *xl, mp3d_sample_t *dstl, int nch, float *lins) +{ + int i; + float *xr = xl + 576*(nch - 1); + mp3d_sample_t *dstr = dstl + (nch - 1); + + static const float g_win[] = { + -1,26,-31,208,218,401,-519,2063,2000,4788,-5517,7134,5959,35640,-39336,74992, + -1,24,-35,202,222,347,-581,2080,1952,4425,-5879,7640,5288,33791,-41176,74856, + -1,21,-38,196,225,294,-645,2087,1893,4063,-6237,8092,4561,31947,-43006,74630, + -1,19,-41,190,227,244,-711,2085,1822,3705,-6589,8492,3776,30112,-44821,74313, + -1,17,-45,183,228,197,-779,2075,1739,3351,-6935,8840,2935,28289,-46617,73908, + -1,16,-49,176,228,153,-848,2057,1644,3004,-7271,9139,2037,26482,-48390,73415, + -2,14,-53,169,227,111,-919,2032,1535,2663,-7597,9389,1082,24694,-50137,72835, + -2,13,-58,161,224,72,-991,2001,1414,2330,-7910,9592,70,22929,-51853,72169, + -2,11,-63,154,221,36,-1064,1962,1280,2006,-8209,9750,-998,21189,-53534,71420, + -2,10,-68,147,215,2,-1137,1919,1131,1692,-8491,9863,-2122,19478,-55178,70590, + -3,9,-73,139,208,-29,-1210,1870,970,1388,-8755,9935,-3300,17799,-56778,69679, + -3,8,-79,132,200,-57,-1283,1817,794,1095,-8998,9966,-4533,16155,-58333,68692, + -4,7,-85,125,189,-83,-1356,1759,605,814,-9219,9959,-5818,14548,-59838,67629, + -4,7,-91,117,177,-106,-1428,1698,402,545,-9416,9916,-7154,12980,-61289,66494, + -5,6,-97,111,163,-127,-1498,1634,185,288,-9585,9838,-8540,11455,-62684,65290 + }; + float *zlin = lins + 15*64; + const float *w = g_win; + + zlin[4*15] = xl[18*16]; + zlin[4*15 + 1] = xr[18*16]; + zlin[4*15 + 2] = xl[0]; + zlin[4*15 + 3] = xr[0]; + + zlin[4*31] = xl[1 + 18*16]; + zlin[4*31 + 1] = xr[1 + 18*16]; + zlin[4*31 + 2] = xl[1]; + zlin[4*31 + 3] = xr[1]; + + mp3d_synth_pair(dstr, nch, lins + 4*15 + 1); + mp3d_synth_pair(dstr + 32*nch, nch, lins + 4*15 + 64 + 1); + mp3d_synth_pair(dstl, nch, lins + 4*15); + mp3d_synth_pair(dstl + 32*nch, nch, lins + 4*15 + 64); + +#if HAVE_SIMD + if (have_simd()) for (i = 14; i >= 0; i--) + { +#define VLOAD(k) f4 w0 = VSET(*w++); f4 w1 = VSET(*w++); f4 vz = VLD(&zlin[4*i - 64*k]); f4 vy = VLD(&zlin[4*i - 64*(15 - k)]); +#define V0(k) { VLOAD(k) b = VADD(VMUL(vz, w1), VMUL(vy, w0)) ; a = VSUB(VMUL(vz, w0), VMUL(vy, w1)); } +#define V1(k) { VLOAD(k) b = VADD(b, VADD(VMUL(vz, w1), VMUL(vy, w0))); a = VADD(a, VSUB(VMUL(vz, w0), VMUL(vy, w1))); } +#define V2(k) { VLOAD(k) b = VADD(b, VADD(VMUL(vz, w1), VMUL(vy, w0))); a = VADD(a, VSUB(VMUL(vy, w1), VMUL(vz, w0))); } + f4 a, b; + zlin[4*i] = xl[18*(31 - i)]; + zlin[4*i + 1] = xr[18*(31 - i)]; + zlin[4*i + 2] = xl[1 + 18*(31 - i)]; + zlin[4*i + 3] = xr[1 + 18*(31 - i)]; + zlin[4*i + 64] = xl[1 + 18*(1 + i)]; + zlin[4*i + 64 + 1] = xr[1 + 18*(1 + i)]; + zlin[4*i - 64 + 2] = xl[18*(1 + i)]; + zlin[4*i - 64 + 3] = xr[18*(1 + i)]; + + V0(0) V2(1) V1(2) V2(3) V1(4) V2(5) V1(6) V2(7) + + { +#ifndef MINIMP3_FLOAT_OUTPUT +#if HAVE_SSE + static const f4 g_max = { 32767.0f, 32767.0f, 32767.0f, 32767.0f }; + static const f4 g_min = { -32768.0f, -32768.0f, -32768.0f, -32768.0f }; + __m128i pcm8 = _mm_packs_epi32(_mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(a, g_max), g_min)), + _mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(b, g_max), g_min))); + dstr[(15 - i)*nch] = _mm_extract_epi16(pcm8, 1); + dstr[(17 + i)*nch] = _mm_extract_epi16(pcm8, 5); + dstl[(15 - i)*nch] = _mm_extract_epi16(pcm8, 0); + dstl[(17 + i)*nch] = _mm_extract_epi16(pcm8, 4); + dstr[(47 - i)*nch] = _mm_extract_epi16(pcm8, 3); + dstr[(49 + i)*nch] = _mm_extract_epi16(pcm8, 7); + dstl[(47 - i)*nch] = _mm_extract_epi16(pcm8, 2); + dstl[(49 + i)*nch] = _mm_extract_epi16(pcm8, 6); +#else /* HAVE_SSE */ + int16x4_t pcma, pcmb; + a = VADD(a, VSET(0.5f)); + b = VADD(b, VSET(0.5f)); + pcma = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(a), vreinterpretq_s32_u32(vcltq_f32(a, VSET(0))))); + pcmb = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(b), vreinterpretq_s32_u32(vcltq_f32(b, VSET(0))))); + vst1_lane_s16(dstr + (15 - i)*nch, pcma, 1); + vst1_lane_s16(dstr + (17 + i)*nch, pcmb, 1); + vst1_lane_s16(dstl + (15 - i)*nch, pcma, 0); + vst1_lane_s16(dstl + (17 + i)*nch, pcmb, 0); + vst1_lane_s16(dstr + (47 - i)*nch, pcma, 3); + vst1_lane_s16(dstr + (49 + i)*nch, pcmb, 3); + vst1_lane_s16(dstl + (47 - i)*nch, pcma, 2); + vst1_lane_s16(dstl + (49 + i)*nch, pcmb, 2); +#endif /* HAVE_SSE */ + +#else /* MINIMP3_FLOAT_OUTPUT */ + + static const f4 g_scale = { 1.0f/32768.0f, 1.0f/32768.0f, 1.0f/32768.0f, 1.0f/32768.0f }; + a = VMUL(a, g_scale); + b = VMUL(b, g_scale); +#if HAVE_SSE + _mm_store_ss(dstr + (15 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(1, 1, 1, 1))); + _mm_store_ss(dstr + (17 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(1, 1, 1, 1))); + _mm_store_ss(dstl + (15 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(0, 0, 0, 0))); + _mm_store_ss(dstl + (17 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(0, 0, 0, 0))); + _mm_store_ss(dstr + (47 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(3, 3, 3, 3))); + _mm_store_ss(dstr + (49 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(3, 3, 3, 3))); + _mm_store_ss(dstl + (47 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(2, 2, 2, 2))); + _mm_store_ss(dstl + (49 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(2, 2, 2, 2))); +#else /* HAVE_SSE */ + vst1q_lane_f32(dstr + (15 - i)*nch, a, 1); + vst1q_lane_f32(dstr + (17 + i)*nch, b, 1); + vst1q_lane_f32(dstl + (15 - i)*nch, a, 0); + vst1q_lane_f32(dstl + (17 + i)*nch, b, 0); + vst1q_lane_f32(dstr + (47 - i)*nch, a, 3); + vst1q_lane_f32(dstr + (49 + i)*nch, b, 3); + vst1q_lane_f32(dstl + (47 - i)*nch, a, 2); + vst1q_lane_f32(dstl + (49 + i)*nch, b, 2); +#endif /* HAVE_SSE */ +#endif /* MINIMP3_FLOAT_OUTPUT */ + } + } else +#endif /* HAVE_SIMD */ +#ifdef MINIMP3_ONLY_SIMD + {} /* for HAVE_SIMD=1, MINIMP3_ONLY_SIMD=1 case we do not need non-intrinsic "else" branch */ +#else /* MINIMP3_ONLY_SIMD */ + for (i = 14; i >= 0; i--) + { +#define LOAD(k) float w0 = *w++; float w1 = *w++; float *vz = &zlin[4*i - k*64]; float *vy = &zlin[4*i - (15 - k)*64]; +#define S0(k) { int j; LOAD(k); for (j = 0; j < 4; j++) b[j] = vz[j]*w1 + vy[j]*w0, a[j] = vz[j]*w0 - vy[j]*w1; } +#define S1(k) { int j; LOAD(k); for (j = 0; j < 4; j++) b[j] += vz[j]*w1 + vy[j]*w0, a[j] += vz[j]*w0 - vy[j]*w1; } +#define S2(k) { int j; LOAD(k); for (j = 0; j < 4; j++) b[j] += vz[j]*w1 + vy[j]*w0, a[j] += vy[j]*w1 - vz[j]*w0; } + float a[4], b[4]; + + zlin[4*i] = xl[18*(31 - i)]; + zlin[4*i + 1] = xr[18*(31 - i)]; + zlin[4*i + 2] = xl[1 + 18*(31 - i)]; + zlin[4*i + 3] = xr[1 + 18*(31 - i)]; + zlin[4*(i + 16)] = xl[1 + 18*(1 + i)]; + zlin[4*(i + 16) + 1] = xr[1 + 18*(1 + i)]; + zlin[4*(i - 16) + 2] = xl[18*(1 + i)]; + zlin[4*(i - 16) + 3] = xr[18*(1 + i)]; + + S0(0) S2(1) S1(2) S2(3) S1(4) S2(5) S1(6) S2(7) + + dstr[(15 - i)*nch] = mp3d_scale_pcm(a[1]); + dstr[(17 + i)*nch] = mp3d_scale_pcm(b[1]); + dstl[(15 - i)*nch] = mp3d_scale_pcm(a[0]); + dstl[(17 + i)*nch] = mp3d_scale_pcm(b[0]); + dstr[(47 - i)*nch] = mp3d_scale_pcm(a[3]); + dstr[(49 + i)*nch] = mp3d_scale_pcm(b[3]); + dstl[(47 - i)*nch] = mp3d_scale_pcm(a[2]); + dstl[(49 + i)*nch] = mp3d_scale_pcm(b[2]); + } +#endif /* MINIMP3_ONLY_SIMD */ +} + +static void mp3d_synth_granule(float *qmf_state, float *grbuf, int nbands, int nch, mp3d_sample_t *pcm, float *lins) +{ + int i; + for (i = 0; i < nch; i++) + { + mp3d_DCT_II(grbuf + 576*i, nbands); + } + + memcpy(lins, qmf_state, sizeof(float)*15*64); + + for (i = 0; i < nbands; i += 2) + { + mp3d_synth(grbuf + i, pcm + 32*nch*i, nch, lins + i*64); + } +#ifndef MINIMP3_NONSTANDARD_BUT_LOGICAL + if (nch == 1) + { + for (i = 0; i < 15*64; i += 2) + { + qmf_state[i] = lins[nbands*64 + i]; + } + } else +#endif /* MINIMP3_NONSTANDARD_BUT_LOGICAL */ + { + memcpy(qmf_state, lins + nbands*64, sizeof(float)*15*64); + } +} + +static int mp3d_match_frame(const uint8_t *hdr, int mp3_bytes, int frame_bytes) +{ + int i, nmatch; + for (i = 0, nmatch = 0; nmatch < MAX_FRAME_SYNC_MATCHES; nmatch++) + { + i += hdr_frame_bytes(hdr + i, frame_bytes) + hdr_padding(hdr + i); + if (i + HDR_SIZE > mp3_bytes) + return nmatch > 0; + if (!hdr_compare(hdr, hdr + i)) + return 0; + } + return 1; +} + +static int mp3d_find_frame(const uint8_t *mp3, int mp3_bytes, int *free_format_bytes, int *ptr_frame_bytes) +{ + int i, k; + for (i = 0; i < mp3_bytes - HDR_SIZE; i++, mp3++) + { + if (hdr_valid(mp3)) + { + int frame_bytes = hdr_frame_bytes(mp3, *free_format_bytes); + int frame_and_padding = frame_bytes + hdr_padding(mp3); + + for (k = HDR_SIZE; !frame_bytes && k < MAX_FREE_FORMAT_FRAME_SIZE && i + 2*k < mp3_bytes - HDR_SIZE; k++) + { + if (hdr_compare(mp3, mp3 + k)) + { + int fb = k - hdr_padding(mp3); + int nextfb = fb + hdr_padding(mp3 + k); + if (i + k + nextfb + HDR_SIZE > mp3_bytes || !hdr_compare(mp3, mp3 + k + nextfb)) + continue; + frame_and_padding = k; + frame_bytes = fb; + *free_format_bytes = fb; + } + } + if ((frame_bytes && i + frame_and_padding <= mp3_bytes && + mp3d_match_frame(mp3, mp3_bytes - i, frame_bytes)) || + (!i && frame_and_padding == mp3_bytes)) + { + *ptr_frame_bytes = frame_and_padding; + return i; + } + *free_format_bytes = 0; + } + } + *ptr_frame_bytes = 0; + return mp3_bytes; +} + +void mp3dec_init(mp3dec_t *dec) +{ + dec->header[0] = 0; +} + +int mp3dec_decode_frame(mp3dec_t *dec, const uint8_t *mp3, int mp3_bytes, mp3d_sample_t *pcm, mp3dec_frame_info_t *info) +{ + int i = 0, igr, frame_size = 0, success = 1; + const uint8_t *hdr; + bs_t bs_frame[1]; +#ifdef MINIMP3_SCRATCH /* Kronograf patch: the ~16 KB scratch lives outside the task stack */ + mp3dec_scratch_t *scratch_p = (mp3dec_scratch_t *)(MINIMP3_SCRATCH); +#define scratch (*scratch_p) +#else + mp3dec_scratch_t scratch; +#endif + + if (mp3_bytes > 4 && dec->header[0] == 0xff && hdr_compare(dec->header, mp3)) + { + frame_size = hdr_frame_bytes(mp3, dec->free_format_bytes) + hdr_padding(mp3); + if (frame_size != mp3_bytes && (frame_size + HDR_SIZE > mp3_bytes || !hdr_compare(mp3, mp3 + frame_size))) + { + frame_size = 0; + } + } + if (!frame_size) + { + memset(dec, 0, sizeof(mp3dec_t)); + i = mp3d_find_frame(mp3, mp3_bytes, &dec->free_format_bytes, &frame_size); + if (!frame_size || i + frame_size > mp3_bytes) + { + info->frame_bytes = i; + return 0; + } + } + + hdr = mp3 + i; + memcpy(dec->header, hdr, HDR_SIZE); + info->frame_bytes = i + frame_size; + info->frame_offset = i; + info->channels = HDR_IS_MONO(hdr) ? 1 : 2; + info->hz = hdr_sample_rate_hz(hdr); + info->layer = 4 - HDR_GET_LAYER(hdr); + info->bitrate_kbps = hdr_bitrate_kbps(hdr); + + if (!pcm) + { + return hdr_frame_samples(hdr); + } + + bs_init(bs_frame, hdr + HDR_SIZE, frame_size - HDR_SIZE); + if (HDR_IS_CRC(hdr)) + { + get_bits(bs_frame, 16); + } + + if (info->layer == 3) + { + int main_data_begin = L3_read_side_info(bs_frame, scratch.gr_info, hdr); + if (main_data_begin < 0 || bs_frame->pos > bs_frame->limit) + { + mp3dec_init(dec); + return 0; + } + success = L3_restore_reservoir(dec, bs_frame, &scratch, main_data_begin); + if (success) + { + for (igr = 0; igr < (HDR_TEST_MPEG1(hdr) ? 2 : 1); igr++, pcm += 576*info->channels) + { + memset(scratch.grbuf[0], 0, 576*2*sizeof(float)); + L3_decode(dec, &scratch, scratch.gr_info + igr*info->channels, info->channels); + mp3d_synth_granule(dec->qmf_state, scratch.grbuf[0], 18, info->channels, pcm, scratch.syn[0]); + } + } + L3_save_reservoir(dec, &scratch); + } else + { +#ifdef MINIMP3_ONLY_MP3 + return 0; +#else /* MINIMP3_ONLY_MP3 */ + L12_scale_info sci[1]; + L12_read_scale_info(hdr, bs_frame, sci); + + memset(scratch.grbuf[0], 0, 576*2*sizeof(float)); + for (i = 0, igr = 0; igr < 3; igr++) + { + if (12 == (i += L12_dequantize_granule(scratch.grbuf[0] + i, bs_frame, sci, info->layer | 1))) + { + i = 0; + L12_apply_scf_384(sci, sci->scf + igr, scratch.grbuf[0]); + mp3d_synth_granule(dec->qmf_state, scratch.grbuf[0], 12, info->channels, pcm, scratch.syn[0]); + memset(scratch.grbuf[0], 0, 576*2*sizeof(float)); + pcm += 384*info->channels; + } + if (bs_frame->pos > bs_frame->limit) + { + mp3dec_init(dec); + return 0; + } + } +#endif /* MINIMP3_ONLY_MP3 */ + } + return success*hdr_frame_samples(dec->header); +} +#ifdef MINIMP3_SCRATCH +#undef scratch +#endif + +#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/src/third_party/tjpgd/tjpgd.c b/src/third_party/tjpgd/tjpgd.c new file mode 100644 index 0000000..4b2c222 --- /dev/null +++ b/src/third_party/tjpgd/tjpgd.c @@ -0,0 +1,1153 @@ +/*----------------------------------------------------------------------------/ +/ TJpgDec - Tiny JPEG Decompressor R0.03 (C)ChaN, 2021 +/-----------------------------------------------------------------------------/ +/ The TJpgDec is a generic JPEG decompressor module for tiny embedded systems. +/ This is a free software that opened for education, research and commercial +/ developments under license policy of following terms. +/ +/ Copyright (C) 2021, ChaN, all right reserved. +/ +/ * The TJpgDec module is a free software and there is NO WARRANTY. +/ * No restriction on use. You can use, modify and redistribute it for +/ personal, non-profit or commercial products UNDER YOUR RESPONSIBILITY. +/ * Redistributions of source code must retain the above copyright notice. +/ +/-----------------------------------------------------------------------------/ +/ Oct 04, 2011 R0.01 First release. +/ Feb 19, 2012 R0.01a Fixed decompression fails when scan starts with an escape seq. +/ Sep 03, 2012 R0.01b Added JD_TBLCLIP option. +/ Mar 16, 2019 R0.01c Supprted stdint.h. +/ Jul 01, 2020 R0.01d Fixed wrong integer type usage. +/ May 08, 2021 R0.02 Supprted grayscale image. Separated configuration options. +/ Jun 11, 2021 R0.02a Some performance improvement. +/ Jul 01, 2021 R0.03 Added JD_FASTDECODE option. +/ Some performance improvement. +/----------------------------------------------------------------------------*/ + +#include "tjpgd.h" + + +#if JD_FASTDECODE == 2 +#define HUFF_BIT 10 /* Bit length to apply fast huffman decode */ +#define HUFF_LEN (1 << HUFF_BIT) +#define HUFF_MASK (HUFF_LEN - 1) +#endif + + +/*-----------------------------------------------*/ +/* Zigzag-order to raster-order conversion table */ +/*-----------------------------------------------*/ + +static const uint8_t Zig[64] = { /* Zigzag-order to raster-order conversion table */ + 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63 +}; + + + +/*-------------------------------------------------*/ +/* Input scale factor of Arai algorithm */ +/* (scaled up 16 bits for fixed point operations) */ +/*-------------------------------------------------*/ + +static const uint16_t Ipsf[64] = { /* See also aa_idct.png */ + (uint16_t)(1.00000*8192), (uint16_t)(1.38704*8192), (uint16_t)(1.30656*8192), (uint16_t)(1.17588*8192), (uint16_t)(1.00000*8192), (uint16_t)(0.78570*8192), (uint16_t)(0.54120*8192), (uint16_t)(0.27590*8192), + (uint16_t)(1.38704*8192), (uint16_t)(1.92388*8192), (uint16_t)(1.81226*8192), (uint16_t)(1.63099*8192), (uint16_t)(1.38704*8192), (uint16_t)(1.08979*8192), (uint16_t)(0.75066*8192), (uint16_t)(0.38268*8192), + (uint16_t)(1.30656*8192), (uint16_t)(1.81226*8192), (uint16_t)(1.70711*8192), (uint16_t)(1.53636*8192), (uint16_t)(1.30656*8192), (uint16_t)(1.02656*8192), (uint16_t)(0.70711*8192), (uint16_t)(0.36048*8192), + (uint16_t)(1.17588*8192), (uint16_t)(1.63099*8192), (uint16_t)(1.53636*8192), (uint16_t)(1.38268*8192), (uint16_t)(1.17588*8192), (uint16_t)(0.92388*8192), (uint16_t)(0.63638*8192), (uint16_t)(0.32442*8192), + (uint16_t)(1.00000*8192), (uint16_t)(1.38704*8192), (uint16_t)(1.30656*8192), (uint16_t)(1.17588*8192), (uint16_t)(1.00000*8192), (uint16_t)(0.78570*8192), (uint16_t)(0.54120*8192), (uint16_t)(0.27590*8192), + (uint16_t)(0.78570*8192), (uint16_t)(1.08979*8192), (uint16_t)(1.02656*8192), (uint16_t)(0.92388*8192), (uint16_t)(0.78570*8192), (uint16_t)(0.61732*8192), (uint16_t)(0.42522*8192), (uint16_t)(0.21677*8192), + (uint16_t)(0.54120*8192), (uint16_t)(0.75066*8192), (uint16_t)(0.70711*8192), (uint16_t)(0.63638*8192), (uint16_t)(0.54120*8192), (uint16_t)(0.42522*8192), (uint16_t)(0.29290*8192), (uint16_t)(0.14932*8192), + (uint16_t)(0.27590*8192), (uint16_t)(0.38268*8192), (uint16_t)(0.36048*8192), (uint16_t)(0.32442*8192), (uint16_t)(0.27590*8192), (uint16_t)(0.21678*8192), (uint16_t)(0.14932*8192), (uint16_t)(0.07612*8192) +}; + + + +/*---------------------------------------------*/ +/* Conversion table for fast clipping process */ +/*---------------------------------------------*/ + +#if JD_TBLCLIP + +#define BYTECLIP(v) Clip8[(unsigned int)(v) & 0x3FF] + +static const uint8_t Clip8[1024] = { + /* 0..255 */ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, + 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, + 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, + 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, + 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, + 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, + /* 256..511 */ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + /* -512..-257 */ + 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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, + /* -256..-1 */ + 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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 +}; + +#else /* JD_TBLCLIP */ + +static uint8_t BYTECLIP (int val) +{ + if (val < 0) return 0; + if (val > 255) return 255; + return (uint8_t)val; +} + +#endif + + + +/*-----------------------------------------------------------------------*/ +/* Allocate a memory block from memory pool */ +/*-----------------------------------------------------------------------*/ + +static void* alloc_pool ( /* Pointer to allocated memory block (NULL:no memory available) */ + JDEC* jd, /* Pointer to the decompressor object */ + size_t ndata /* Number of bytes to allocate */ +) +{ + char *rp = 0; + + + ndata = (ndata + 3) & ~3; /* Align block size to the word boundary */ + + if (jd->sz_pool >= ndata) { + jd->sz_pool -= ndata; + rp = (char*)jd->pool; /* Get start of available memory pool */ + jd->pool = (void*)(rp + ndata); /* Allocate requierd bytes */ + } + + return (void*)rp; /* Return allocated memory block (NULL:no memory to allocate) */ +} + + + + +/*-----------------------------------------------------------------------*/ +/* Create de-quantization and prescaling tables with a DQT segment */ +/*-----------------------------------------------------------------------*/ + +static JRESULT create_qt_tbl ( /* 0:OK, !0:Failed */ + JDEC* jd, /* Pointer to the decompressor object */ + const uint8_t* data, /* Pointer to the quantizer tables */ + size_t ndata /* Size of input data */ +) +{ + unsigned int i, zi; + uint8_t d; + int32_t *pb; + + + while (ndata) { /* Process all tables in the segment */ + if (ndata < 65) return JDR_FMT1; /* Err: table size is unaligned */ + ndata -= 65; + d = *data++; /* Get table property */ + if (d & 0xF0) return JDR_FMT1; /* Err: not 8-bit resolution */ + i = d & 3; /* Get table ID */ + pb = alloc_pool(jd, 64 * sizeof (int32_t));/* Allocate a memory block for the table */ + if (!pb) return JDR_MEM1; /* Err: not enough memory */ + jd->qttbl[i] = pb; /* Register the table */ + for (i = 0; i < 64; i++) { /* Load the table */ + zi = Zig[i]; /* Zigzag-order to raster-order conversion */ + pb[zi] = (int32_t)((uint32_t)*data++ * Ipsf[zi]); /* Apply scale factor of Arai algorithm to the de-quantizers */ + } + } + + return JDR_OK; +} + + + + +/*-----------------------------------------------------------------------*/ +/* Create huffman code tables with a DHT segment */ +/*-----------------------------------------------------------------------*/ + +static JRESULT create_huffman_tbl ( /* 0:OK, !0:Failed */ + JDEC* jd, /* Pointer to the decompressor object */ + const uint8_t* data, /* Pointer to the packed huffman tables */ + size_t ndata /* Size of input data */ +) +{ + unsigned int i, j, b, cls, num; + size_t np; + uint8_t d, *pb, *pd; + uint16_t hc, *ph; + + + while (ndata) { /* Process all tables in the segment */ + if (ndata < 17) return JDR_FMT1; /* Err: wrong data size */ + ndata -= 17; + d = *data++; /* Get table number and class */ + if (d & 0xEE) return JDR_FMT1; /* Err: invalid class/number */ + cls = d >> 4; num = d & 0x0F; /* class = dc(0)/ac(1), table number = 0/1 */ + pb = alloc_pool(jd, 16); /* Allocate a memory block for the bit distribution table */ + if (!pb) return JDR_MEM1; /* Err: not enough memory */ + jd->huffbits[num][cls] = pb; + for (np = i = 0; i < 16; i++) { /* Load number of patterns for 1 to 16-bit code */ + np += (pb[i] = *data++); /* Get sum of code words for each code */ + } + ph = alloc_pool(jd, np * sizeof (uint16_t));/* Allocate a memory block for the code word table */ + if (!ph) return JDR_MEM1; /* Err: not enough memory */ + jd->huffcode[num][cls] = ph; + hc = 0; + for (j = i = 0; i < 16; i++) { /* Re-build huffman code word table */ + b = pb[i]; + while (b--) ph[j++] = hc++; + hc <<= 1; + } + + if (ndata < np) return JDR_FMT1; /* Err: wrong data size */ + ndata -= np; + pd = alloc_pool(jd, np); /* Allocate a memory block for the decoded data */ + if (!pd) return JDR_MEM1; /* Err: not enough memory */ + jd->huffdata[num][cls] = pd; + for (i = 0; i < np; i++) { /* Load decoded data corresponds to each code word */ + d = *data++; + if (!cls && d > 11) return JDR_FMT1; + pd[i] = d; + } +#if JD_FASTDECODE == 2 + { /* Create fast huffman decode table */ + unsigned int span, td, ti; + uint16_t *tbl_ac = 0; + uint8_t *tbl_dc = 0; + + if (cls) { + tbl_ac = alloc_pool(jd, HUFF_LEN * sizeof (uint16_t)); /* LUT for AC elements */ + if (!tbl_ac) return JDR_MEM1; /* Err: not enough memory */ + jd->hufflut_ac[num] = tbl_ac; + memset(tbl_ac, 0xFF, HUFF_LEN * sizeof (uint16_t)); /* Default value (0xFFFF: may be long code) */ + } else { + tbl_dc = alloc_pool(jd, HUFF_LEN * sizeof (uint8_t)); /* LUT for AC elements */ + if (!tbl_dc) return JDR_MEM1; /* Err: not enough memory */ + jd->hufflut_dc[num] = tbl_dc; + memset(tbl_dc, 0xFF, HUFF_LEN * sizeof (uint8_t)); /* Default value (0xFF: may be long code) */ + } + for (i = b = 0; b < HUFF_BIT; b++) { /* Create LUT */ + for (j = pb[b]; j; j--) { + ti = ph[i] << (HUFF_BIT - 1 - b) & HUFF_MASK; /* Index of input pattern for the code */ + if (cls) { + td = pd[i++] | ((b + 1) << 8); /* b15..b8: code length, b7..b0: zero run and data length */ + for (span = 1 << (HUFF_BIT - 1 - b); span; span--, tbl_ac[ti++] = (uint16_t)td) ; + } else { + td = pd[i++] | ((b + 1) << 4); /* b7..b4: code length, b3..b0: data length */ + for (span = 1 << (HUFF_BIT - 1 - b); span; span--, tbl_dc[ti++] = (uint8_t)td) ; + } + } + } + jd->longofs[num][cls] = i; /* Code table offset for long code */ + } +#endif + } + + return JDR_OK; +} + + + + +/*-----------------------------------------------------------------------*/ +/* Extract a huffman decoded data from input stream */ +/*-----------------------------------------------------------------------*/ + +static int huffext ( /* >=0: decoded data, <0: error code */ + JDEC* jd, /* Pointer to the decompressor object */ + unsigned int id, /* Table ID (0:Y, 1:C) */ + unsigned int cls /* Table class (0:DC, 1:AC) */ +) +{ + size_t dc = jd->dctr; + uint8_t *dp = jd->dptr; + unsigned int d, flg = 0; + +#if JD_FASTDECODE == 0 + uint8_t bm, nd, bl; + const uint8_t *hb = jd->huffbits[id][cls]; /* Bit distribution table */ + const uint16_t *hc = jd->huffcode[id][cls]; /* Code word table */ + const uint8_t *hd = jd->huffdata[id][cls]; /* Data table */ + + + bm = jd->dbit; /* Bit mask to extract */ + d = 0; bl = 16; /* Max code length */ + do { + if (!bm) { /* Next byte? */ + if (!dc) { /* No input data is available, re-fill input buffer */ + dp = jd->inbuf; /* Top of input buffer */ + dc = jd->infunc(jd, dp, JD_SZBUF); + if (!dc) return 0 - (int)JDR_INP; /* Err: read error or wrong stream termination */ + } else { + dp++; /* Next data ptr */ + } + dc--; /* Decrement number of available bytes */ + if (flg) { /* In flag sequence? */ + flg = 0; /* Exit flag sequence */ + if (*dp != 0) return 0 - (int)JDR_FMT1; /* Err: unexpected flag is detected (may be collapted data) */ + *dp = 0xFF; /* The flag is a data 0xFF */ + } else { + if (*dp == 0xFF) { /* Is start of flag sequence? */ + flg = 1; continue; /* Enter flag sequence, get trailing byte */ + } + } + bm = 0x80; /* Read from MSB */ + } + d <<= 1; /* Get a bit */ + if (*dp & bm) d++; + bm >>= 1; + + for (nd = *hb++; nd; nd--) { /* Search the code word in this bit length */ + if (d == *hc++) { /* Matched? */ + jd->dbit = bm; jd->dctr = dc; jd->dptr = dp; + return *hd; /* Return the decoded data */ + } + hd++; + } + bl--; + } while (bl); + +#else + const uint8_t *hb, *hd; + const uint16_t *hc; + unsigned int nc, bl, wbit = jd->dbit % 32; + uint32_t w = jd->wreg & ((1UL << wbit) - 1); + + + while (wbit < 16) { /* Prepare 16 bits into the working register */ + if (jd->marker) { + d = 0xFF; /* Input stream has stalled for a marker. Generate stuff bits */ + } else { + if (!dc) { /* Buffer empty, re-fill input buffer */ + dp = jd->inbuf; /* Top of input buffer */ + dc = jd->infunc(jd, dp, JD_SZBUF); + if (!dc) return 0 - (int)JDR_INP; /* Err: read error or wrong stream termination */ + } + d = *dp++; dc--; + if (flg) { /* In flag sequence? */ + flg = 0; /* Exit flag sequence */ + if (d != 0) jd->marker = d; /* Not an escape of 0xFF but a marker */ + d = 0xFF; + } else { + if (d == 0xFF) { /* Is start of flag sequence? */ + flg = 1; continue; /* Enter flag sequence, get trailing byte */ + } + } + } + w = w << 8 | d; /* Shift 8 bits in the working register */ + wbit += 8; + } + jd->dctr = dc; jd->dptr = dp; + jd->wreg = w; + +#if JD_FASTDECODE == 2 + /* Table serch for the short codes */ + d = (unsigned int)(w >> (wbit - HUFF_BIT)); /* Short code as table index */ + if (cls) { /* AC element */ + d = jd->hufflut_ac[id][d]; /* Table decode */ + if (d != 0xFFFF) { /* It is done if hit in short code */ + jd->dbit = wbit - (d >> 8); /* Snip the code length */ + return d & 0xFF; /* b7..0: zero run and following data bits */ + } + } else { /* DC element */ + d = jd->hufflut_dc[id][d]; /* Table decode */ + if (d != 0xFF) { /* It is done if hit in short code */ + jd->dbit = wbit - (d >> 4); /* Snip the code length */ + return d & 0xF; /* b3..0: following data bits */ + } + } + + /* Incremental serch for the codes longer than HUFF_BIT */ + hb = jd->huffbits[id][cls] + HUFF_BIT; /* Bit distribution table */ + hc = jd->huffcode[id][cls] + jd->longofs[id][cls]; /* Code word table */ + hd = jd->huffdata[id][cls] + jd->longofs[id][cls]; /* Data table */ + bl = HUFF_BIT + 1; +#else + /* Incremental serch for all codes */ + hb = jd->huffbits[id][cls]; /* Bit distribution table */ + hc = jd->huffcode[id][cls]; /* Code word table */ + hd = jd->huffdata[id][cls]; /* Data table */ + bl = 1; +#endif + for ( ; bl <= 16; bl++) { /* Incremental search */ + nc = *hb++; + if (nc) { + d = w >> (wbit - bl); + do { /* Search the code word in this bit length */ + if (d == *hc++) { /* Matched? */ + jd->dbit = wbit - bl; /* Snip the huffman code */ + return *hd; /* Return the decoded data */ + } + hd++; + } while (--nc); + } + } +#endif + + return 0 - (int)JDR_FMT1; /* Err: code not found (may be collapted data) */ +} + + + + +/*-----------------------------------------------------------------------*/ +/* Extract N bits from input stream */ +/*-----------------------------------------------------------------------*/ + +static int bitext ( /* >=0: extracted data, <0: error code */ + JDEC* jd, /* Pointer to the decompressor object */ + unsigned int nbit /* Number of bits to extract (1 to 16) */ +) +{ + size_t dc = jd->dctr; + uint8_t *dp = jd->dptr; + unsigned int d, flg = 0; + +#if JD_FASTDECODE == 0 + uint8_t mbit = jd->dbit; + + d = 0; + do { + if (!mbit) { /* Next byte? */ + if (!dc) { /* No input data is available, re-fill input buffer */ + dp = jd->inbuf; /* Top of input buffer */ + dc = jd->infunc(jd, dp, JD_SZBUF); + if (!dc) return 0 - (int)JDR_INP; /* Err: read error or wrong stream termination */ + } else { + dp++; /* Next data ptr */ + } + dc--; /* Decrement number of available bytes */ + if (flg) { /* In flag sequence? */ + flg = 0; /* Exit flag sequence */ + if (*dp != 0) return 0 - (int)JDR_FMT1; /* Err: unexpected flag is detected (may be collapted data) */ + *dp = 0xFF; /* The flag is a data 0xFF */ + } else { + if (*dp == 0xFF) { /* Is start of flag sequence? */ + flg = 1; continue; /* Enter flag sequence */ + } + } + mbit = 0x80; /* Read from MSB */ + } + d <<= 1; /* Get a bit */ + if (*dp & mbit) d |= 1; + mbit >>= 1; + nbit--; + } while (nbit); + + jd->dbit = mbit; jd->dctr = dc; jd->dptr = dp; + return (int)d; + +#else + unsigned int wbit = jd->dbit % 32; + uint32_t w = jd->wreg & ((1UL << wbit) - 1); + + + while (wbit < nbit) { /* Prepare nbit bits into the working register */ + if (jd->marker) { + d = 0xFF; /* Input stream stalled, generate stuff bits */ + } else { + if (!dc) { /* Buffer empty, re-fill input buffer */ + dp = jd->inbuf; /* Top of input buffer */ + dc = jd->infunc(jd, dp, JD_SZBUF); + if (!dc) return 0 - (int)JDR_INP; /* Err: read error or wrong stream termination */ + } + d = *dp++; dc--; + if (flg) { /* In flag sequence? */ + flg = 0; /* Exit flag sequence */ + if (d != 0) jd->marker = d; /* Not an escape of 0xFF but a marker */ + d = 0xFF; + } else { + if (d == 0xFF) { /* Is start of flag sequence? */ + flg = 1; continue; /* Enter flag sequence, get trailing byte */ + } + } + } + w = w << 8 | d; /* Get 8 bits into the working register */ + wbit += 8; + } + jd->wreg = w; jd->dbit = wbit - nbit; + jd->dctr = dc; jd->dptr = dp; + + return (int)(w >> ((wbit - nbit) % 32)); +#endif +} + + + + +/*-----------------------------------------------------------------------*/ +/* Process restart interval */ +/*-----------------------------------------------------------------------*/ + +static JRESULT restart ( + JDEC* jd, /* Pointer to the decompressor object */ + uint16_t rstn /* Expected restert sequense number */ +) +{ + unsigned int i; + uint8_t *dp = jd->dptr; + size_t dc = jd->dctr; + +#if JD_FASTDECODE == 0 + uint16_t d = 0; + + /* Get two bytes from the input stream */ + for (i = 0; i < 2; i++) { + if (!dc) { /* No input data is available, re-fill input buffer */ + dp = jd->inbuf; + dc = jd->infunc(jd, dp, JD_SZBUF); + if (!dc) return JDR_INP; + } else { + dp++; + } + dc--; + d = d << 8 | *dp; /* Get a byte */ + } + jd->dptr = dp; jd->dctr = dc; jd->dbit = 0; + + /* Check the marker */ + if ((d & 0xFFD8) != 0xFFD0 || (d & 7) != (rstn & 7)) { + return JDR_FMT1; /* Err: expected RSTn marker is not detected (may be collapted data) */ + } + +#else + uint16_t marker; + + + if (jd->marker) { /* Generate a maker if it has been detected */ + marker = 0xFF00 | jd->marker; + jd->marker = 0; + } else { + marker = 0; + for (i = 0; i < 2; i++) { /* Get a restart marker */ + if (!dc) { /* No input data is available, re-fill input buffer */ + dp = jd->inbuf; + dc = jd->infunc(jd, dp, JD_SZBUF); + if (!dc) return JDR_INP; + } + marker = (marker << 8) | *dp++; /* Get a byte */ + dc--; + } + jd->dptr = dp; jd->dctr = dc; + } + + /* Check the marker */ + if ((marker & 0xFFD8) != 0xFFD0 || (marker & 7) != (rstn & 7)) { + return JDR_FMT1; /* Err: expected RSTn marker was not detected (may be collapted data) */ + } + + jd->dbit = 0; /* Discard stuff bits */ +#endif + + jd->dcv[2] = jd->dcv[1] = jd->dcv[0] = 0; /* Reset DC offset */ + return JDR_OK; +} + + + + +/*-----------------------------------------------------------------------*/ +/* Apply Inverse-DCT in Arai Algorithm (see also aa_idct.png) */ +/*-----------------------------------------------------------------------*/ + +static void block_idct ( + int32_t* src, /* Input block data (de-quantized and pre-scaled for Arai Algorithm) */ + jd_yuv_t* dst /* Pointer to the destination to store the block as byte array */ +) +{ + const int32_t M13 = (int32_t)(1.41421*4096), M2 = (int32_t)(1.08239*4096), M4 = (int32_t)(2.61313*4096), M5 = (int32_t)(1.84776*4096); + int32_t v0, v1, v2, v3, v4, v5, v6, v7; + int32_t t10, t11, t12, t13; + int i; + + /* Process columns */ + for (i = 0; i < 8; i++) { + v0 = src[8 * 0]; /* Get even elements */ + v1 = src[8 * 2]; + v2 = src[8 * 4]; + v3 = src[8 * 6]; + + t10 = v0 + v2; /* Process the even elements */ + t12 = v0 - v2; + t11 = (v1 - v3) * M13 >> 12; + v3 += v1; + t11 -= v3; + v0 = t10 + v3; + v3 = t10 - v3; + v1 = t11 + t12; + v2 = t12 - t11; + + v4 = src[8 * 7]; /* Get odd elements */ + v5 = src[8 * 1]; + v6 = src[8 * 5]; + v7 = src[8 * 3]; + + t10 = v5 - v4; /* Process the odd elements */ + t11 = v5 + v4; + t12 = v6 - v7; + v7 += v6; + v5 = (t11 - v7) * M13 >> 12; + v7 += t11; + t13 = (t10 + t12) * M5 >> 12; + v4 = t13 - (t10 * M2 >> 12); + v6 = t13 - (t12 * M4 >> 12) - v7; + v5 -= v6; + v4 -= v5; + + src[8 * 0] = v0 + v7; /* Write-back transformed values */ + src[8 * 7] = v0 - v7; + src[8 * 1] = v1 + v6; + src[8 * 6] = v1 - v6; + src[8 * 2] = v2 + v5; + src[8 * 5] = v2 - v5; + src[8 * 3] = v3 + v4; + src[8 * 4] = v3 - v4; + + src++; /* Next column */ + } + + /* Process rows */ + src -= 8; + for (i = 0; i < 8; i++) { + v0 = src[0] + (128L << 8); /* Get even elements (remove DC offset (-128) here) */ + v1 = src[2]; + v2 = src[4]; + v3 = src[6]; + + t10 = v0 + v2; /* Process the even elements */ + t12 = v0 - v2; + t11 = (v1 - v3) * M13 >> 12; + v3 += v1; + t11 -= v3; + v0 = t10 + v3; + v3 = t10 - v3; + v1 = t11 + t12; + v2 = t12 - t11; + + v4 = src[7]; /* Get odd elements */ + v5 = src[1]; + v6 = src[5]; + v7 = src[3]; + + t10 = v5 - v4; /* Process the odd elements */ + t11 = v5 + v4; + t12 = v6 - v7; + v7 += v6; + v5 = (t11 - v7) * M13 >> 12; + v7 += t11; + t13 = (t10 + t12) * M5 >> 12; + v4 = t13 - (t10 * M2 >> 12); + v6 = t13 - (t12 * M4 >> 12) - v7; + v5 -= v6; + v4 -= v5; + + /* Descale the transformed values 8 bits and output a row */ +#if JD_FASTDECODE >= 1 + dst[0] = (int16_t)((v0 + v7) >> 8); + dst[7] = (int16_t)((v0 - v7) >> 8); + dst[1] = (int16_t)((v1 + v6) >> 8); + dst[6] = (int16_t)((v1 - v6) >> 8); + dst[2] = (int16_t)((v2 + v5) >> 8); + dst[5] = (int16_t)((v2 - v5) >> 8); + dst[3] = (int16_t)((v3 + v4) >> 8); + dst[4] = (int16_t)((v3 - v4) >> 8); +#else + dst[0] = BYTECLIP((v0 + v7) >> 8); + dst[7] = BYTECLIP((v0 - v7) >> 8); + dst[1] = BYTECLIP((v1 + v6) >> 8); + dst[6] = BYTECLIP((v1 - v6) >> 8); + dst[2] = BYTECLIP((v2 + v5) >> 8); + dst[5] = BYTECLIP((v2 - v5) >> 8); + dst[3] = BYTECLIP((v3 + v4) >> 8); + dst[4] = BYTECLIP((v3 - v4) >> 8); +#endif + + dst += 8; src += 8; /* Next row */ + } +} + + + + +/*-----------------------------------------------------------------------*/ +/* Load all blocks in an MCU into working buffer */ +/*-----------------------------------------------------------------------*/ + +static JRESULT mcu_load ( + JDEC* jd /* Pointer to the decompressor object */ +) +{ + int32_t *tmp = (int32_t*)jd->workbuf; /* Block working buffer for de-quantize and IDCT */ + int d, e; + unsigned int blk, nby, i, bc, z, id, cmp; + jd_yuv_t *bp; + const int32_t *dqf; + + + nby = jd->msx * jd->msy; /* Number of Y blocks (1, 2 or 4) */ + bp = jd->mcubuf; /* Pointer to the first block of MCU */ + + for (blk = 0; blk < nby + 2; blk++) { /* Get nby Y blocks and two C blocks */ + cmp = (blk < nby) ? 0 : blk - nby + 1; /* Component number 0:Y, 1:Cb, 2:Cr */ + + if (cmp && jd->ncomp != 3) { /* Clear C blocks if not exist (monochrome image) */ + for (i = 0; i < 64; bp[i++] = 128) ; + + } else { /* Load Y/C blocks from input stream */ + id = cmp ? 1 : 0; /* Huffman table ID of this component */ + + /* Extract a DC element from input stream */ + d = huffext(jd, id, 0); /* Extract a huffman coded data (bit length) */ + if (d < 0) return (JRESULT)(0 - d); /* Err: invalid code or input */ + bc = (unsigned int)d; + d = jd->dcv[cmp]; /* DC value of previous block */ + if (bc) { /* If there is any difference from previous block */ + e = bitext(jd, bc); /* Extract data bits */ + if (e < 0) return (JRESULT)(0 - e); /* Err: input */ + bc = 1 << (bc - 1); /* MSB position */ + if (!(e & bc)) e -= (bc << 1) - 1; /* Restore negative value if needed */ + d += e; /* Get current value */ + jd->dcv[cmp] = (int16_t)d; /* Save current DC value for next block */ + } + dqf = jd->qttbl[jd->qtid[cmp]]; /* De-quantizer table ID for this component */ + tmp[0] = d * dqf[0] >> 8; /* De-quantize, apply scale factor of Arai algorithm and descale 8 bits */ + + /* Extract following 63 AC elements from input stream */ + memset(&tmp[1], 0, 63 * sizeof (int32_t)); /* Initialize all AC elements */ + z = 1; /* Top of the AC elements (in zigzag-order) */ + do { + d = huffext(jd, id, 1); /* Extract a huffman coded value (zero runs and bit length) */ + if (d == 0) break; /* EOB? */ + if (d < 0) return (JRESULT)(0 - d); /* Err: invalid code or input error */ + bc = (unsigned int)d; + z += bc >> 4; /* Skip leading zero run */ + if (z >= 64) return JDR_FMT1; /* Too long zero run */ + if (bc &= 0x0F) { /* Bit length? */ + d = bitext(jd, bc); /* Extract data bits */ + if (d < 0) return (JRESULT)(0 - d); /* Err: input device */ + bc = 1 << (bc - 1); /* MSB position */ + if (!(d & bc)) d -= (bc << 1) - 1; /* Restore negative value if needed */ + i = Zig[z]; /* Get raster-order index */ + tmp[i] = d * dqf[i] >> 8; /* De-quantize, apply scale factor of Arai algorithm and descale 8 bits */ + } + } while (++z < 64); /* Next AC element */ + + if (JD_FORMAT != 2 || !cmp) { /* C components may not be processed if in grayscale output */ + if (z == 1 || (JD_USE_SCALE && jd->scale == 3)) { /* If no AC element or scale ratio is 1/8, IDCT can be ommited and the block is filled with DC value */ + d = (jd_yuv_t)((*tmp / 256) + 128); + if (JD_FASTDECODE >= 1) { + for (i = 0; i < 64; bp[i++] = d) ; + } else { + memset(bp, d, 64); + } + } else { + block_idct(tmp, bp); /* Apply IDCT and store the block to the MCU buffer */ + } + } + } + + bp += 64; /* Next block */ + } + + return JDR_OK; /* All blocks have been loaded successfully */ +} + + + + +/*-----------------------------------------------------------------------*/ +/* Output an MCU: Convert YCrCb to RGB and output it in RGB form */ +/*-----------------------------------------------------------------------*/ + +static JRESULT mcu_output ( + JDEC* jd, /* Pointer to the decompressor object */ + int (*outfunc)(JDEC*, void*, JRECT*), /* RGB output function */ + unsigned int x, /* MCU location in the image */ + unsigned int y /* MCU location in the image */ +) +{ + const int CVACC = (sizeof (int) > 2) ? 1024 : 128; /* Adaptive accuracy for both 16-/32-bit systems */ + unsigned int ix, iy, mx, my, rx, ry; + int yy, cb, cr; + jd_yuv_t *py, *pc; + uint8_t *pix; + JRECT rect; + + + mx = jd->msx * 8; my = jd->msy * 8; /* MCU size (pixel) */ + rx = (x + mx <= jd->width) ? mx : jd->width - x; /* Output rectangular size (it may be clipped at right/bottom end of image) */ + ry = (y + my <= jd->height) ? my : jd->height - y; + if (JD_USE_SCALE) { + rx >>= jd->scale; ry >>= jd->scale; + if (!rx || !ry) return JDR_OK; /* Skip this MCU if all pixel is to be rounded off */ + x >>= jd->scale; y >>= jd->scale; + } + rect.left = x; rect.right = x + rx - 1; /* Rectangular area in the frame buffer */ + rect.top = y; rect.bottom = y + ry - 1; + + + if (!JD_USE_SCALE || jd->scale != 3) { /* Not for 1/8 scaling */ + pix = (uint8_t*)jd->workbuf; + + if (JD_FORMAT != 2) { /* RGB output (build an RGB MCU from Y/C component) */ + for (iy = 0; iy < my; iy++) { + pc = py = jd->mcubuf; + if (my == 16) { /* Double block height? */ + pc += 64 * 4 + (iy >> 1) * 8; + if (iy >= 8) py += 64; + } else { /* Single block height */ + pc += mx * 8 + iy * 8; + } + py += iy * 8; + for (ix = 0; ix < mx; ix++) { + cb = pc[0] - 128; /* Get Cb/Cr component and remove offset */ + cr = pc[64] - 128; + if (mx == 16) { /* Double block width? */ + if (ix == 8) py += 64 - 8; /* Jump to next block if double block heigt */ + pc += ix & 1; /* Step forward chroma pointer every two pixels */ + } else { /* Single block width */ + pc++; /* Step forward chroma pointer every pixel */ + } + yy = *py++; /* Get Y component */ + *pix++ = /*R*/ BYTECLIP(yy + ((int)(1.402 * CVACC) * cr) / CVACC); + *pix++ = /*G*/ BYTECLIP(yy - ((int)(0.344 * CVACC) * cb + (int)(0.714 * CVACC) * cr) / CVACC); + *pix++ = /*B*/ BYTECLIP(yy + ((int)(1.772 * CVACC) * cb) / CVACC); + } + } + } else { /* Monochrome output (build a grayscale MCU from Y comopnent) */ + for (iy = 0; iy < my; iy++) { + py = jd->mcubuf + iy * 8; + if (my == 16) { /* Double block height? */ + if (iy >= 8) py += 64; + } + for (ix = 0; ix < mx; ix++) { + if (mx == 16) { /* Double block width? */ + if (ix == 8) py += 64 - 8; /* Jump to next block if double block height */ + } + *pix++ = (uint8_t)*py++; /* Get and store a Y value as grayscale */ + } + } + } + + /* Descale the MCU rectangular if needed */ + if (JD_USE_SCALE && jd->scale) { + unsigned int x, y, r, g, b, s, w, a; + uint8_t *op; + + /* Get averaged RGB value of each square correcponds to a pixel */ + s = jd->scale * 2; /* Number of shifts for averaging */ + w = 1 << jd->scale; /* Width of square */ + a = (mx - w) * (JD_FORMAT != 2 ? 3 : 1); /* Bytes to skip for next line in the square */ + op = (uint8_t*)jd->workbuf; + for (iy = 0; iy < my; iy += w) { + for (ix = 0; ix < mx; ix += w) { + pix = (uint8_t*)jd->workbuf + (iy * mx + ix) * (JD_FORMAT != 2 ? 3 : 1); + r = g = b = 0; + for (y = 0; y < w; y++) { /* Accumulate RGB value in the square */ + for (x = 0; x < w; x++) { + r += *pix++; /* Accumulate R or Y (monochrome output) */ + if (JD_FORMAT != 2) { /* RGB output? */ + g += *pix++; /* Accumulate G */ + b += *pix++; /* Accumulate B */ + } + } + pix += a; + } /* Put the averaged pixel value */ + *op++ = (uint8_t)(r >> s); /* Put R or Y (monochrome output) */ + if (JD_FORMAT != 2) { /* RGB output? */ + *op++ = (uint8_t)(g >> s); /* Put G */ + *op++ = (uint8_t)(b >> s); /* Put B */ + } + } + } + } + + } else { /* For only 1/8 scaling (left-top pixel in each block are the DC value of the block) */ + + /* Build a 1/8 descaled RGB MCU from discrete comopnents */ + pix = (uint8_t*)jd->workbuf; + pc = jd->mcubuf + mx * my; + cb = pc[0] - 128; /* Get Cb/Cr component and restore right level */ + cr = pc[64] - 128; + for (iy = 0; iy < my; iy += 8) { + py = jd->mcubuf; + if (iy == 8) py += 64 * 2; + for (ix = 0; ix < mx; ix += 8) { + yy = *py; /* Get Y component */ + py += 64; + if (JD_FORMAT != 2) { + *pix++ = /*R*/ BYTECLIP(yy + ((int)(1.402 * CVACC) * cr / CVACC)); + *pix++ = /*G*/ BYTECLIP(yy - ((int)(0.344 * CVACC) * cb + (int)(0.714 * CVACC) * cr) / CVACC); + *pix++ = /*B*/ BYTECLIP(yy + ((int)(1.772 * CVACC) * cb / CVACC)); + } else { + *pix++ = yy; + } + } + } + } + + /* Squeeze up pixel table if a part of MCU is to be truncated */ + mx >>= jd->scale; + if (rx < mx) { /* Is the MCU spans rigit edge? */ + uint8_t *s, *d; + unsigned int x, y; + + s = d = (uint8_t*)jd->workbuf; + for (y = 0; y < ry; y++) { + for (x = 0; x < rx; x++) { /* Copy effective pixels */ + *d++ = *s++; + if (JD_FORMAT != 2) { + *d++ = *s++; + *d++ = *s++; + } + } + s += (mx - rx) * (JD_FORMAT != 2 ? 3 : 1); /* Skip truncated pixels */ + } + } + + /* Convert RGB888 to RGB565 if needed */ + if (JD_FORMAT == 1) { + uint8_t *s = (uint8_t*)jd->workbuf; + uint16_t w, *d = (uint16_t*)s; + unsigned int n = rx * ry; + + do { + w = (*s++ & 0xF8) << 8; /* RRRRR----------- */ + w |= (*s++ & 0xFC) << 3; /* -----GGGGGG----- */ + w |= *s++ >> 3; /* -----------BBBBB */ + *d++ = w; + } while (--n); + } + + /* Output the rectangular */ + return outfunc(jd, jd->workbuf, &rect) ? JDR_OK : JDR_INTR; +} + + + + +/*-----------------------------------------------------------------------*/ +/* Analyze the JPEG image and Initialize decompressor object */ +/*-----------------------------------------------------------------------*/ + +#define LDB_WORD(ptr) (uint16_t)(((uint16_t)*((uint8_t*)(ptr))<<8)|(uint16_t)*(uint8_t*)((ptr)+1)) + + +JRESULT jd_prepare ( + JDEC* jd, /* Blank decompressor object */ + size_t (*infunc)(JDEC*, uint8_t*, size_t), /* JPEG strem input function */ + void* pool, /* Working buffer for the decompression session */ + size_t sz_pool, /* Size of working buffer */ + void* dev /* I/O device identifier for the session */ +) +{ + uint8_t *seg, b; + uint16_t marker; + unsigned int n, i, ofs; + size_t len; + JRESULT rc; + + + memset(jd, 0, sizeof (JDEC)); /* Clear decompression object (this might be a problem if machine's null pointer is not all bits zero) */ + jd->pool = pool; /* Work memroy */ + jd->sz_pool = sz_pool; /* Size of given work memory */ + jd->infunc = infunc; /* Stream input function */ + jd->device = dev; /* I/O device identifier */ + + jd->inbuf = seg = alloc_pool(jd, JD_SZBUF); /* Allocate stream input buffer */ + if (!seg) return JDR_MEM1; + + ofs = marker = 0; /* Find SOI marker */ + do { + if (jd->infunc(jd, seg, 1) != 1) return JDR_INP; /* Err: SOI was not detected */ + ofs++; + marker = marker << 8 | seg[0]; + } while (marker != 0xFFD8); + + for (;;) { /* Parse JPEG segments */ + /* Get a JPEG marker */ + if (jd->infunc(jd, seg, 4) != 4) return JDR_INP; + marker = LDB_WORD(seg); /* Marker */ + len = LDB_WORD(seg + 2); /* Length field */ + if (len <= 2 || (marker >> 8) != 0xFF) return JDR_FMT1; + len -= 2; /* Segent content size */ + ofs += 4 + len; /* Number of bytes loaded */ + + switch (marker & 0xFF) { + case 0xC0: /* SOF0 (baseline JPEG) */ + if (len > JD_SZBUF) return JDR_MEM2; + if (jd->infunc(jd, seg, len) != len) return JDR_INP; /* Load segment data */ + + jd->width = LDB_WORD(&seg[3]); /* Image width in unit of pixel */ + jd->height = LDB_WORD(&seg[1]); /* Image height in unit of pixel */ + jd->ncomp = seg[5]; /* Number of color components */ + if (jd->ncomp != 3 && jd->ncomp != 1) return JDR_FMT3; /* Err: Supports only Grayscale and Y/Cb/Cr */ + + /* Check each image component */ + for (i = 0; i < jd->ncomp; i++) { + b = seg[7 + 3 * i]; /* Get sampling factor */ + if (i == 0) { /* Y component */ + if (b != 0x11 && b != 0x22 && b != 0x21) { /* Check sampling factor */ + return JDR_FMT3; /* Err: Supports only 4:4:4, 4:2:0 or 4:2:2 */ + } + jd->msx = b >> 4; jd->msy = b & 15; /* Size of MCU [blocks] */ + } else { /* Cb/Cr component */ + if (b != 0x11) return JDR_FMT3; /* Err: Sampling factor of Cb/Cr must be 1 */ + } + jd->qtid[i] = seg[8 + 3 * i]; /* Get dequantizer table ID for this component */ + if (jd->qtid[i] > 3) return JDR_FMT3; /* Err: Invalid ID */ + } + break; + + case 0xDD: /* DRI - Define Restart Interval */ + if (len > JD_SZBUF) return JDR_MEM2; + if (jd->infunc(jd, seg, len) != len) return JDR_INP; /* Load segment data */ + + jd->nrst = LDB_WORD(seg); /* Get restart interval (MCUs) */ + break; + + case 0xC4: /* DHT - Define Huffman Tables */ + if (len > JD_SZBUF) return JDR_MEM2; + if (jd->infunc(jd, seg, len) != len) return JDR_INP; /* Load segment data */ + + rc = create_huffman_tbl(jd, seg, len); /* Create huffman tables */ + if (rc) return rc; + break; + + case 0xDB: /* DQT - Define Quaitizer Tables */ + if (len > JD_SZBUF) return JDR_MEM2; + if (jd->infunc(jd, seg, len) != len) return JDR_INP; /* Load segment data */ + + rc = create_qt_tbl(jd, seg, len); /* Create de-quantizer tables */ + if (rc) return rc; + break; + + case 0xDA: /* SOS - Start of Scan */ + if (len > JD_SZBUF) return JDR_MEM2; + if (jd->infunc(jd, seg, len) != len) return JDR_INP; /* Load segment data */ + + if (!jd->width || !jd->height) return JDR_FMT1; /* Err: Invalid image size */ + if (seg[0] != jd->ncomp) return JDR_FMT3; /* Err: Wrong color components */ + + /* Check if all tables corresponding to each components have been loaded */ + for (i = 0; i < jd->ncomp; i++) { + b = seg[2 + 2 * i]; /* Get huffman table ID */ + if (b != 0x00 && b != 0x11) return JDR_FMT3; /* Err: Different table number for DC/AC element */ + n = i ? 1 : 0; /* Component class */ + if (!jd->huffbits[n][0] || !jd->huffbits[n][1]) { /* Check huffman table for this component */ + return JDR_FMT1; /* Err: Nnot loaded */ + } + if (!jd->qttbl[jd->qtid[i]]) { /* Check dequantizer table for this component */ + return JDR_FMT1; /* Err: Not loaded */ + } + } + + /* Allocate working buffer for MCU and pixel output */ + n = jd->msy * jd->msx; /* Number of Y blocks in the MCU */ + if (!n) return JDR_FMT1; /* Err: SOF0 has not been loaded */ + len = n * 64 * 2 + 64; /* Allocate buffer for IDCT and RGB output */ + if (len < 256) len = 256; /* but at least 256 byte is required for IDCT */ + jd->workbuf = alloc_pool(jd, len); /* and it may occupy a part of following MCU working buffer for RGB output */ + if (!jd->workbuf) return JDR_MEM1; /* Err: not enough memory */ + jd->mcubuf = alloc_pool(jd, (n + 2) * 64 * sizeof (jd_yuv_t)); /* Allocate MCU working buffer */ + if (!jd->mcubuf) return JDR_MEM1; /* Err: not enough memory */ + + /* Align stream read offset to JD_SZBUF */ + if (ofs %= JD_SZBUF) { + jd->dctr = jd->infunc(jd, seg + ofs, (size_t)(JD_SZBUF - ofs)); + } + jd->dptr = seg + ofs - (JD_FASTDECODE ? 0 : 1); + + return JDR_OK; /* Initialization succeeded. Ready to decompress the JPEG image. */ + + case 0xC1: /* SOF1 */ + case 0xC2: /* SOF2 */ + case 0xC3: /* SOF3 */ + case 0xC5: /* SOF5 */ + case 0xC6: /* SOF6 */ + case 0xC7: /* SOF7 */ + case 0xC9: /* SOF9 */ + case 0xCA: /* SOF10 */ + case 0xCB: /* SOF11 */ + case 0xCD: /* SOF13 */ + case 0xCE: /* SOF14 */ + case 0xCF: /* SOF15 */ + case 0xD9: /* EOI */ + return JDR_FMT3; /* Unsuppoted JPEG standard (may be progressive JPEG) */ + + default: /* Unknown segment (comment, exif or etc..) */ + /* Skip segment data (null pointer specifies to remove data from the stream) */ + if (jd->infunc(jd, 0, len) != len) return JDR_INP; + } + } +} + + + + +/*-----------------------------------------------------------------------*/ +/* Start to decompress the JPEG picture */ +/*-----------------------------------------------------------------------*/ + +JRESULT jd_decomp ( + JDEC* jd, /* Initialized decompression object */ + int (*outfunc)(JDEC*, void*, JRECT*), /* RGB output function */ + uint8_t scale /* Output de-scaling factor (0 to 3) */ +) +{ + unsigned int x, y, mx, my; + uint16_t rst, rsc; + JRESULT rc; + + + if (scale > (JD_USE_SCALE ? 3 : 0)) return JDR_PAR; + jd->scale = scale; + + mx = jd->msx * 8; my = jd->msy * 8; /* Size of the MCU (pixel) */ + + jd->dcv[2] = jd->dcv[1] = jd->dcv[0] = 0; /* Initialize DC values */ + rst = rsc = 0; + + rc = JDR_OK; + for (y = 0; y < jd->height; y += my) { /* Vertical loop of MCUs */ + for (x = 0; x < jd->width; x += mx) { /* Horizontal loop of MCUs */ + if (jd->nrst && rst++ == jd->nrst) { /* Process restart interval if enabled */ + rc = restart(jd, rsc++); + if (rc != JDR_OK) return rc; + rst = 1; + } + rc = mcu_load(jd); /* Load an MCU (decompress huffman coded stream, dequantize and apply IDCT) */ + if (rc != JDR_OK) return rc; + rc = mcu_output(jd, outfunc, x, y); /* Output the MCU (YCbCr to RGB, scaling and output) */ + if (rc != JDR_OK) return rc; + } + } + + return rc; +} diff --git a/src/third_party/tjpgd/tjpgd.h b/src/third_party/tjpgd/tjpgd.h new file mode 100644 index 0000000..2ce312c --- /dev/null +++ b/src/third_party/tjpgd/tjpgd.h @@ -0,0 +1,102 @@ +/*----------------------------------------------------------------------------/ +/ TJpgDec - Tiny JPEG Decompressor R0.03 include file (C)ChaN, 2021 +/----------------------------------------------------------------------------*/ +#ifndef DEF_TJPGDEC +#define DEF_TJPGDEC + +#ifdef __cplusplus +extern "C" { +#endif + +#include "tjpgdcnf.h" +#include + +#if defined(_WIN32) /* VC++ or some compiler without stdint.h */ +typedef unsigned char uint8_t; +typedef unsigned short uint16_t; +typedef short int16_t; +typedef unsigned long uint32_t; +typedef long int32_t; +#else /* Embedded platform */ +#include +#endif + +#if JD_FASTDECODE >= 1 +typedef int16_t jd_yuv_t; +#else +typedef uint8_t jd_yuv_t; +#endif + + +/* Error code */ +typedef enum { + JDR_OK = 0, /* 0: Succeeded */ + JDR_INTR, /* 1: Interrupted by output function */ + JDR_INP, /* 2: Device error or wrong termination of input stream */ + JDR_MEM1, /* 3: Insufficient memory pool for the image */ + JDR_MEM2, /* 4: Insufficient stream input buffer */ + JDR_PAR, /* 5: Parameter error */ + JDR_FMT1, /* 6: Data format error (may be broken data) */ + JDR_FMT2, /* 7: Right format but not supported */ + JDR_FMT3 /* 8: Not supported JPEG standard */ +} JRESULT; + + + +/* Rectangular region in the output image */ +typedef struct { + uint16_t left; /* Left end */ + uint16_t right; /* Right end */ + uint16_t top; /* Top end */ + uint16_t bottom; /* Bottom end */ +} JRECT; + + + +/* Decompressor object structure */ +typedef struct JDEC JDEC; +struct JDEC { + size_t dctr; /* Number of bytes available in the input buffer */ + uint8_t* dptr; /* Current data read ptr */ + uint8_t* inbuf; /* Bit stream input buffer */ + uint8_t dbit; /* Number of bits availavble in wreg or reading bit mask */ + uint8_t scale; /* Output scaling ratio */ + uint8_t msx, msy; /* MCU size in unit of block (width, height) */ + uint8_t qtid[3]; /* Quantization table ID of each component, Y, Cb, Cr */ + uint8_t ncomp; /* Number of color components 1:grayscale, 3:color */ + int16_t dcv[3]; /* Previous DC element of each component */ + uint16_t nrst; /* Restart inverval */ + uint16_t width, height; /* Size of the input image (pixel) */ + uint8_t* huffbits[2][2]; /* Huffman bit distribution tables [id][dcac] */ + uint16_t* huffcode[2][2]; /* Huffman code word tables [id][dcac] */ + uint8_t* huffdata[2][2]; /* Huffman decoded data tables [id][dcac] */ + int32_t* qttbl[4]; /* Dequantizer tables [id] */ +#if JD_FASTDECODE >= 1 + uint32_t wreg; /* Working shift register */ + uint8_t marker; /* Detected marker (0:None) */ +#if JD_FASTDECODE == 2 + uint8_t longofs[2][2]; /* Table offset of long code [id][dcac] */ + uint16_t* hufflut_ac[2]; /* Fast huffman decode tables for AC short code [id] */ + uint8_t* hufflut_dc[2]; /* Fast huffman decode tables for DC short code [id] */ +#endif +#endif + void* workbuf; /* Working buffer for IDCT and RGB output */ + jd_yuv_t* mcubuf; /* Working buffer for the MCU */ + void* pool; /* Pointer to available memory pool */ + size_t sz_pool; /* Size of momory pool (bytes available) */ + size_t (*infunc)(JDEC*, uint8_t*, size_t); /* Pointer to jpeg stream input function */ + void* device; /* Pointer to I/O device identifiler for the session */ +}; + + + +/* TJpgDec API functions */ +JRESULT jd_prepare (JDEC* jd, size_t (*infunc)(JDEC*,uint8_t*,size_t), void* pool, size_t sz_pool, void* dev); +JRESULT jd_decomp (JDEC* jd, int (*outfunc)(JDEC*,void*,JRECT*), uint8_t scale); + + +#ifdef __cplusplus +} +#endif + +#endif /* _TJPGDEC */ diff --git a/src/third_party/tjpgd/tjpgdcnf.h b/src/third_party/tjpgd/tjpgdcnf.h new file mode 100644 index 0000000..0ed0d03 --- /dev/null +++ b/src/third_party/tjpgd/tjpgdcnf.h @@ -0,0 +1,33 @@ +/*----------------------------------------------*/ +/* TJpgDec System Configurations R0.03 */ +/*----------------------------------------------*/ + +#define JD_SZBUF 512 +/* Specifies size of stream input buffer */ + +#define JD_FORMAT 1 /* Kronograf: RGB565, the display format */ +/* Specifies output pixel format. +/ 0: RGB888 (24-bit/pix) +/ 1: RGB565 (16-bit/pix) +/ 2: Grayscale (8-bit/pix) +*/ + +#define JD_USE_SCALE 1 +/* Switches output descaling feature. +/ 0: Disable +/ 1: Enable +*/ + +#define JD_TBLCLIP 1 +/* Use table conversion for saturation arithmetic. A bit faster, but increases 1 KB of code size. +/ 0: Disable +/ 1: Enable +*/ + +#define JD_FASTDECODE 1 /* Kronograf: 32-bit barrel shifter (ESP32, Mac) */ +/* Optimization level +/ 0: Basic optimization. Suitable for 8/16-bit MCUs. +/ 1: + 32-bit barrel shifter. Suitable for 32-bit MCUs. +/ 2: + Table conversion for huffman decoding (wants 6 << HUFF_BIT bytes of RAM) +*/ + diff --git a/src/ui/app_radio.c b/src/ui/app_radio.c new file mode 100644 index 0000000..846fc35 --- /dev/null +++ b/src/ui/app_radio.c @@ -0,0 +1,254 @@ +/* + * Radio: international web radios (MP3 over HTTP) on the speaker, plus one custom URL. + * Previous / next change station (and switch while playing); the song title comes from + * the stream's ICY metadata. Playback continues with the screen off; long-press the station + * name to type a custom URL. + */ +#include "nav.h" +#include "kit.h" +#include "theme.h" +#include "fonts/kg_fonts.h" +#include "../hal/hal.h" +#include +#include + +static lv_obj_t *s_scr, *s_name, *s_genre, *s_title, *s_info, *s_buf, *s_play, *s_vol, *s_kb_panel, *s_ta; +static lv_timer_t *s_timer; +static int s_station; +static bool s_drawn_playing; + +static int n_stations(void) { return KG_N_STATIONS + (hal_radio_custom_url()[0] ? 1 : 0); } + +static void station(int i, const char **name, const char **genre, const char **url) +{ + if (i < KG_N_STATIONS) { + *name = KG_STATIONS[i].name, *genre = KG_STATIONS[i].genre, *url = KG_STATIONS[i].url; + } else { + *name = "Custom", *genre = hal_radio_custom_url(), *url = hal_radio_custom_url(); + } +} + +static bool playing(const kg_radio_status_t *st) { return st->state != KG_RADIO_OFF && st->state != KG_RADIO_ERROR; } + +static void play_current(void) +{ + const char *name, *genre, *url; + station(s_station, &name, &genre, &url); + hal_radio_play(name, url); +} + +static void refresh(void) +{ + kg_radio_status_t st; + hal_radio_status(&st); + const char *name, *genre, *url; + station(s_station, &name, &genre, &url); + lv_label_set_text(s_name, name); + lv_label_set_text(s_genre, genre); + bool on = playing(&st) && !strcmp(st.station, name); + char b[64]; + switch (on ? st.state : st.state == KG_RADIO_ERROR ? KG_RADIO_ERROR : KG_RADIO_OFF) { + case KG_RADIO_CONNECTING: snprintf(b, sizeof b, "%s", st.error[0] ? st.error : "Connecting…"); break; + case KG_RADIO_BUFFERING: snprintf(b, sizeof b, "Buffering… %u %%", st.buffer_pct); break; + case KG_RADIO_PLAYING: + snprintf(b, sizeof b, "%u kbps · %.1f kHz · buffer %u %%", st.kbps, st.rate / 1000.0, st.buffer_pct); + break; + case KG_RADIO_ERROR: snprintf(b, sizeof b, "%s", st.error); break; + default: snprintf(b, sizeof b, hal_sound_available() ? "Tap play" : "No speaker on this watch"); break; + } + lv_label_set_text(s_info, b); + lv_obj_set_style_text_color(s_info, st.state == KG_RADIO_ERROR && !on ? KG_C_ALERT + : on && st.state != KG_RADIO_PLAYING ? KG_C_WARN : KG_C_TEXT_DIM, 0); + const char *title = on && st.title[0] ? st.title : on ? "Live" : ""; + if (strcmp(lv_label_get_text(s_title), title)) lv_label_set_text(s_title, title); /* keeps the scroll going */ + lv_bar_set_value(s_buf, on ? st.buffer_pct : 0, LV_ANIM_OFF); + lv_bar_set_value(s_vol, hal_radio_volume(), LV_ANIM_OFF); + if (on != s_drawn_playing) { + s_drawn_playing = on; + lv_obj_invalidate(s_play); + } +} + +static void tick(lv_timer_t *t) +{ + (void)t; + refresh(); +} + +static void play_cb(lv_event_t *e) +{ + (void)e; + kg_radio_status_t st; + hal_radio_status(&st); + if (s_drawn_playing) hal_radio_stop(); + else play_current(); + refresh(); +} + +static void step_cb(lv_event_t *e) +{ + int d = (int)(intptr_t)lv_event_get_user_data(e); + int n = n_stations(); + s_station = (s_station + d + n) % n; + hal_setting_set("radio_st", s_station); + if (s_drawn_playing) play_current(); /* switch stations while playing */ + lv_label_set_text(s_title, ""); + refresh(); +} + +static void vol_cb(lv_event_t *e) +{ + int d = (int)(intptr_t)lv_event_get_user_data(e); + int v = hal_radio_volume() + d; + if (v >= 0 && v <= KG_RADIO_VOL_MAX) hal_radio_set_volume((uint8_t)v); + refresh(); +} + +/* Play ▶ or stop ■ drawn on the main button. */ +static void play_draw(lv_event_t *e) +{ + lv_draw_ctx_t *ctx = lv_event_get_draw_ctx(e); + lv_area_t a; + lv_obj_get_coords(lv_event_get_target(e), &a); + lv_coord_t cx = (a.x1 + a.x2) / 2, cy = (a.y1 + a.y2) / 2; + lv_draw_rect_dsc_t d; + lv_draw_rect_dsc_init(&d); + d.bg_color = KG_C_BG; + if (s_drawn_playing) { + d.radius = 3; + lv_area_t sq = {cx - 11, cy - 11, cx + 11, cy + 11}; + lv_draw_rect(ctx, &d, &sq); + } else { + lv_point_t tri[3] = {{cx - 9, cy - 14}, {cx - 9, cy + 14}, {cx + 15, cy}}; + lv_draw_polygon(ctx, &d, tri, 3); + } +} + +static void kb_cb(lv_event_t *e) +{ + lv_obj_add_flag(s_kb_panel, LV_OBJ_FLAG_HIDDEN); + if (lv_event_get_code(e) != LV_EVENT_READY) return; + const char *url = lv_textarea_get_text(s_ta); + hal_radio_set_custom_url(!strcmp(url, "http://") ? "" : url); + s_station = hal_radio_custom_url()[0] ? KG_N_STATIONS : 0; + hal_setting_set("radio_st", s_station); + refresh(); +} + +static void custom_cb(lv_event_t *e) +{ + (void)e; + lv_textarea_set_text(s_ta, hal_radio_custom_url()[0] ? hal_radio_custom_url() : "http://"); + lv_obj_clear_flag(s_kb_panel, LV_OBJ_FLAG_HIDDEN); + kit_tick(); +} + +static lv_obj_t *round_btn(lv_obj_t *scr, const char *sym, lv_coord_t cx, lv_coord_t cy, lv_coord_t d, lv_event_cb_t cb, int arg) +{ + lv_obj_t *b = kit_button(scr, sym, false, d, d, cb, (void *)(intptr_t)arg); + lv_obj_set_style_text_font(lv_obj_get_child(b, 0), &lv_font_montserrat_14, 0); /* has the symbols */ + lv_obj_set_pos(b, cx - d / 2, cy - d / 2); + return b; +} + +static lv_obj_t *centered(lv_obj_t *scr, const lv_font_t *f, lv_color_t c, lv_coord_t y) +{ + lv_obj_t *l = kit_label(scr, f, c, ""); + lv_obj_set_width(l, 220); + lv_obj_set_style_text_align(l, LV_TEXT_ALIGN_CENTER, 0); + lv_label_set_long_mode(l, LV_LABEL_LONG_DOT); + lv_obj_set_pos(l, 10, y); + return l; +} + +static lv_obj_t *create(void) +{ + lv_obj_t *scr = s_scr = kit_screen(); + kit_title(scr, "RADIO"); + s_name = centered(scr, &kg_font_label_20, KG_C_TEXT, 24); + lv_obj_add_flag(s_name, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(s_name, custom_cb, LV_EVENT_LONG_PRESSED, NULL); + s_genre = centered(scr, &kg_font_text_16, KG_C_TEXT_DIM, 47); + s_title = centered(scr, &kg_font_text_16, KG_C_ACCENT, 74); + lv_label_set_long_mode(s_title, LV_LABEL_LONG_SCROLL_CIRCULAR); + s_info = centered(scr, &kg_font_text_16, KG_C_TEXT_DIM, 96); + + s_buf = lv_bar_create(scr); + lv_obj_set_size(s_buf, 140, 3); + lv_obj_set_pos(s_buf, 50, 120); + lv_bar_set_range(s_buf, 0, 100); + lv_obj_set_style_bg_color(s_buf, KG_C_BTN, LV_PART_MAIN); + lv_obj_set_style_bg_color(s_buf, KG_C_TRACK, LV_PART_INDICATOR); + + round_btn(scr, LV_SYMBOL_PREV, 46, 160, 46, step_cb, -1); + round_btn(scr, LV_SYMBOL_NEXT, 194, 160, 46, step_cb, 1); + s_play = kit_button(scr, "", true, 64, 64, play_cb, NULL); + lv_obj_set_pos(s_play, 120 - 32, 160 - 32); + lv_obj_add_event_cb(s_play, play_draw, LV_EVENT_DRAW_MAIN_END, NULL); + + round_btn(scr, LV_SYMBOL_VOLUME_MID, 30, 214, 34, vol_cb, -1); + round_btn(scr, LV_SYMBOL_VOLUME_MAX, 210, 214, 34, vol_cb, 1); + s_vol = lv_bar_create(scr); + lv_obj_set_size(s_vol, 120, 6); + lv_obj_set_pos(s_vol, 60, 211); + lv_bar_set_range(s_vol, 0, KG_RADIO_VOL_MAX); + lv_obj_set_style_bg_color(s_vol, KG_C_BTN, LV_PART_MAIN); + lv_obj_set_style_bg_color(s_vol, KG_C_ACCENT, LV_PART_INDICATOR); + + /* Custom URL: field + keyboard over the whole screen. */ + s_kb_panel = lv_obj_create(scr); + lv_obj_remove_style_all(s_kb_panel); + lv_obj_set_size(s_kb_panel, KG_W, KG_H); + lv_obj_set_style_bg_color(s_kb_panel, KG_C_BG, 0); + lv_obj_set_style_bg_opa(s_kb_panel, LV_OPA_COVER, 0); + lv_obj_add_flag(s_kb_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_t *hint = kit_label(s_kb_panel, &kg_font_text_16, KG_C_TEXT_DIM, "MP3 stream, http:// only"); + lv_obj_align(hint, LV_ALIGN_TOP_MID, 0, 2); + s_ta = lv_textarea_create(s_kb_panel); + lv_textarea_set_one_line(s_ta, true); + lv_textarea_set_max_length(s_ta, 255); + lv_obj_set_size(s_ta, 228, 38); + lv_obj_align(s_ta, LV_ALIGN_TOP_MID, 0, 22); + lv_obj_set_style_text_font(s_ta, &kg_font_text_16, 0); + lv_obj_set_style_bg_color(s_ta, KG_C_SURFACE, 0); + lv_obj_set_style_text_color(s_ta, KG_C_TEXT, 0); + lv_obj_set_style_border_color(s_ta, KG_C_ACCENT, 0); + lv_obj_t *kb = lv_keyboard_create(s_kb_panel); + lv_obj_set_size(kb, KG_W, 172); + lv_obj_align(kb, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_keyboard_set_textarea(kb, s_ta); + lv_obj_set_style_text_font(kb, &lv_font_montserrat_14, 0); + lv_obj_set_style_bg_color(kb, KG_C_BG, 0); + lv_obj_set_style_pad_all(kb, 2, 0); + lv_obj_set_style_pad_gap(kb, 2, 0); + lv_obj_set_style_bg_color(kb, KG_C_BTN, LV_PART_ITEMS); + lv_obj_set_style_text_color(kb, KG_C_TEXT, LV_PART_ITEMS); + lv_obj_set_style_border_width(kb, 0, LV_PART_ITEMS); + lv_obj_add_event_cb(kb, kb_cb, LV_EVENT_READY, NULL); + lv_obj_add_event_cb(kb, kb_cb, LV_EVENT_CANCEL, NULL); + + s_timer = lv_timer_create(tick, 500, NULL); + lv_timer_pause(s_timer); + return scr; +} + +static void on_show(void) +{ + s_station = (int)hal_setting_get("radio_st", 0); + if (s_station < 0 || s_station >= n_stations()) s_station = 0; + lv_obj_add_flag(s_kb_panel, LV_OBJ_FLAG_HIDDEN); + s_drawn_playing = false; + refresh(); + lv_timer_resume(s_timer); +} + +static void on_hide(void) { lv_timer_pause(s_timer); } /* the radio keeps playing */ + +const kg_app_t kg_app_radio = { + .name = "radio", + .label = "Radio", + .icon = KG_ICON_RADIO, + .create = create, + .on_show = on_show, + .on_hide = on_hide, +}; diff --git a/src/ui/app_video.c b/src/ui/app_video.c new file mode 100644 index 0000000..acb715b --- /dev/null +++ b/src/ui/app_video.c @@ -0,0 +1,211 @@ +/* + * Video: an MJPEG camera (ESP32-CAM, IP camera, 3D printer…) or a JPEG snapshot URL, + * full screen. Plays while the app is on screen; tap to show the overlay (state, frame + * rate, source size, "Change URL"). Wi-Fi is held by the app's lease. + */ +#include "nav.h" +#include "kit.h" +#include "theme.h" +#include "fonts/kg_fonts.h" +#include "../hal/hal.h" +#include +#include + +static lv_obj_t *s_img, *s_overlay, *s_status, *s_empty, *s_kb_panel, *s_ta; +static lv_timer_t *s_timer; +static lv_img_dsc_t s_dsc; +static uint32_t s_seq, s_ticks; +static bool s_overlay_on; +static uint32_t s_overlay_until; /* tick count: the overlay shows for the first seconds */ + +static void overlay(bool on) +{ + s_overlay_on = on; + if (on) lv_obj_clear_flag(s_overlay, LV_OBJ_FLAG_HIDDEN); + else lv_obj_add_flag(s_overlay, LV_OBJ_FLAG_HIDDEN); +} + +static void tick(lv_timer_t *t) +{ + (void)t; + if (s_ticks++ % 25 == 0) hal_net_hold(); /* renew the Wi-Fi lease about once a second */ + if (s_ticks < 10 * 60 * 25) lv_disp_trig_activity(NULL); /* watching: the screen stays on, 10 min at most */ + uint32_t seq; + const uint16_t *f = hal_video_frame(&seq); + if (f && seq != s_seq) { + s_seq = seq; + s_dsc.data = (const uint8_t *)f; + lv_img_cache_invalidate_src(&s_dsc); + lv_img_set_src(s_img, &s_dsc); + lv_obj_clear_flag(s_img, LV_OBJ_FLAG_HIDDEN); + } + if (s_ticks % 8) return; /* status: ~4 Hz */ + kg_video_status_t st; + hal_video_status(&st); + char b[80]; + switch (st.state) { + case KG_VIDEO_PLAYING: + snprintf(b, sizeof b, "LIVE %.1f fps · %ux%u · %lu kbit/s", (double)st.fps, st.src_w, st.src_h, (unsigned long)st.kbps); + break; + case KG_VIDEO_CONNECTING: snprintf(b, sizeof b, "%s", st.error[0] ? st.error : "Connecting…"); break; + case KG_VIDEO_ERROR: snprintf(b, sizeof b, "%s", st.error); break; + default: snprintf(b, sizeof b, "Stopped"); break; + } + lv_label_set_text(s_status, b); + lv_obj_set_style_text_color(s_status, st.state == KG_VIDEO_PLAYING ? KG_C_OK : st.state == KG_VIDEO_ERROR ? KG_C_ALERT : KG_C_WARN, 0); + if (st.state != KG_VIDEO_PLAYING && !s_seq && hal_video_url()[0]) overlay(true); /* nothing to see yet: say why */ + else if (s_overlay_until && s_ticks >= s_overlay_until) { + s_overlay_until = 0; + overlay(false); + } +} + +static void start(void) +{ + s_seq = 0; + s_overlay_until = s_ticks + 3 * 25; /* status visible for 3 s, then the picture alone */ + lv_obj_add_flag(s_img, LV_OBJ_FLAG_HIDDEN); + const char *url = hal_video_url(); + if (url[0]) { + lv_obj_add_flag(s_empty, LV_OBJ_FLAG_HIDDEN); + hal_net_hold(); + hal_video_start(url); + } else { + lv_obj_clear_flag(s_empty, LV_OBJ_FLAG_HIDDEN); + } +} + +static void tap_cb(lv_event_t *e) +{ + (void)e; + s_overlay_until = 0; + if (hal_video_url()[0]) overlay(!s_overlay_on); +} + +static void kb_cb(lv_event_t *e) +{ + lv_obj_add_flag(s_kb_panel, LV_OBJ_FLAG_HIDDEN); + if (lv_event_get_code(e) != LV_EVENT_READY) return; + const char *url = lv_textarea_get_text(s_ta); + hal_video_set_url(!strcmp(url, "http://") ? "" : url); + hal_video_stop(); + overlay(false); + start(); +} + +static void url_cb(lv_event_t *e) +{ + (void)e; + lv_textarea_set_text(s_ta, hal_video_url()[0] ? hal_video_url() : "http://"); + lv_obj_clear_flag(s_kb_panel, LV_OBJ_FLAG_HIDDEN); +} + +static lv_obj_t *create(void) +{ + lv_obj_t *scr = kit_screen(); + s_dsc.header.cf = LV_IMG_CF_TRUE_COLOR; + s_dsc.header.w = KG_VIDEO_W; + s_dsc.header.h = KG_VIDEO_H; + s_dsc.data_size = KG_VIDEO_W * KG_VIDEO_H * LV_COLOR_SIZE / 8; + s_img = lv_img_create(scr); + lv_obj_set_pos(s_img, 0, 0); + lv_obj_add_flag(s_img, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(scr, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(scr, tap_cb, LV_EVENT_SHORT_CLICKED, NULL); + + s_empty = lv_obj_create(scr); + lv_obj_remove_style_all(s_empty); + lv_obj_set_size(s_empty, KG_W, KG_H); + lv_obj_clear_flag(s_empty, LV_OBJ_FLAG_CLICKABLE); + kit_title(s_empty, "VIDEO"); + lv_obj_t *t = kit_label(s_empty, &kg_font_label_20, KG_C_TEXT, "No camera yet"); + lv_obj_align(t, LV_ALIGN_TOP_MID, 0, 60); + lv_obj_t *h = kit_label(s_empty, &kg_font_text_16, KG_C_TEXT_DIM, + "An MJPEG stream or a JPEG\nsnapshot URL (http://), from an\nESP32-CAM, an IP camera, a printer…"); + lv_obj_set_style_text_align(h, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(h, LV_ALIGN_TOP_MID, 0, 92); + lv_obj_t *set = kit_button(s_empty, "Set URL", true, 140, 40, url_cb, NULL); + lv_obj_align(set, LV_ALIGN_BOTTOM_MID, 0, -24); + + /* Overlay: a status pill at the top, "Change URL" at the bottom. */ + s_overlay = lv_obj_create(scr); + lv_obj_remove_style_all(s_overlay); + lv_obj_set_size(s_overlay, KG_W, KG_H); + lv_obj_clear_flag(s_overlay, LV_OBJ_FLAG_CLICKABLE); + lv_obj_t *pill = lv_obj_create(s_overlay); + lv_obj_remove_style_all(pill); + lv_obj_set_size(pill, 228, 26); + lv_obj_align(pill, LV_ALIGN_TOP_MID, 0, 6); + lv_obj_set_style_bg_color(pill, KG_C_BG, 0); + lv_obj_set_style_bg_opa(pill, LV_OPA_70, 0); + lv_obj_set_style_radius(pill, 13, 0); + lv_obj_clear_flag(pill, LV_OBJ_FLAG_CLICKABLE); + s_status = kit_label(pill, &kg_font_text_16, KG_C_OK, ""); + lv_obj_set_width(s_status, 216); + lv_label_set_long_mode(s_status, LV_LABEL_LONG_DOT); + lv_obj_set_style_text_align(s_status, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_center(s_status); + lv_obj_t *chg = kit_button(s_overlay, "Change URL", false, 140, 34, url_cb, NULL); + lv_obj_set_style_bg_opa(chg, LV_OPA_80, 0); + lv_obj_align(chg, LV_ALIGN_BOTTOM_MID, 0, -10); + lv_obj_add_flag(s_overlay, LV_OBJ_FLAG_HIDDEN); + + s_kb_panel = lv_obj_create(scr); + lv_obj_remove_style_all(s_kb_panel); + lv_obj_set_size(s_kb_panel, KG_W, KG_H); + lv_obj_set_style_bg_color(s_kb_panel, KG_C_BG, 0); + lv_obj_set_style_bg_opa(s_kb_panel, LV_OPA_COVER, 0); + lv_obj_add_flag(s_kb_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_t *hint = kit_label(s_kb_panel, &kg_font_text_16, KG_C_TEXT_DIM, "MJPEG or JPEG URL, http:// only"); + lv_obj_align(hint, LV_ALIGN_TOP_MID, 0, 2); + s_ta = lv_textarea_create(s_kb_panel); + lv_textarea_set_one_line(s_ta, true); + lv_textarea_set_max_length(s_ta, 255); + lv_obj_set_size(s_ta, 228, 38); + lv_obj_align(s_ta, LV_ALIGN_TOP_MID, 0, 22); + lv_obj_set_style_text_font(s_ta, &kg_font_text_16, 0); + lv_obj_set_style_bg_color(s_ta, KG_C_SURFACE, 0); + lv_obj_set_style_text_color(s_ta, KG_C_TEXT, 0); + lv_obj_set_style_border_color(s_ta, KG_C_ACCENT, 0); + lv_obj_t *kb = lv_keyboard_create(s_kb_panel); + lv_obj_set_size(kb, KG_W, 172); + lv_obj_align(kb, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_keyboard_set_textarea(kb, s_ta); + lv_obj_set_style_text_font(kb, &lv_font_montserrat_14, 0); + lv_obj_set_style_bg_color(kb, KG_C_BG, 0); + lv_obj_set_style_pad_all(kb, 2, 0); + lv_obj_set_style_pad_gap(kb, 2, 0); + lv_obj_set_style_bg_color(kb, KG_C_BTN, LV_PART_ITEMS); + lv_obj_set_style_text_color(kb, KG_C_TEXT, LV_PART_ITEMS); + lv_obj_set_style_border_width(kb, 0, LV_PART_ITEMS); + lv_obj_add_event_cb(kb, kb_cb, LV_EVENT_READY, NULL); + lv_obj_add_event_cb(kb, kb_cb, LV_EVENT_CANCEL, NULL); + + s_timer = lv_timer_create(tick, 40, NULL); + lv_timer_pause(s_timer); + return scr; +} + +static void on_show(void) +{ + lv_obj_add_flag(s_kb_panel, LV_OBJ_FLAG_HIDDEN); + s_ticks = 0; + start(); + overlay(hal_video_url()[0] != 0); + lv_timer_resume(s_timer); +} + +static void on_hide(void) +{ + lv_timer_pause(s_timer); + hal_video_stop(); +} + +const kg_app_t kg_app_video = { + .name = "video", + .label = "Video", + .icon = KG_ICON_VIDEO, + .create = create, + .on_show = on_show, + .on_hide = on_hide, +}; diff --git a/src/ui/nav.c b/src/ui/nav.c index 1149a2e..7e07b7b 100644 --- a/src/ui/nav.c +++ b/src/ui/nav.c @@ -16,8 +16,8 @@ static const kg_app_t *const APPS[] = { &kg_app_chrono, &kg_app_timer, &kg_app_alarms, &kg_app_world, &kg_app_weather, &kg_app_sky, &kg_app_steps, &kg_app_level, &kg_app_torch, - &kg_app_net, &kg_app_remote, &kg_app_system, - &kg_app_settings, + &kg_app_net, &kg_app_radio, &kg_app_video, + &kg_app_remote, &kg_app_system, &kg_app_settings, }; #define N_APPS ((int)(sizeof APPS / sizeof APPS[0])) @@ -167,6 +167,27 @@ void ui_nav_draw_icon(lv_draw_ctx_t *ctx, kg_icon_t icon, lv_coord_t cx, lv_coor line(ctx, &acc, cx + 8, cy - 2, cx + 10, cy - 4); line(ctx, &d, cx - 7, cy + 11, cx + 7, cy + 11); break; + case KG_ICON_RADIO: { /* transistor radio */ + line(ctx, &d, cx - 11, cy - 4, cx + 11, cy - 4); + line(ctx, &d, cx - 11, cy + 10, cx + 11, cy + 10); + line(ctx, &d, cx - 11, cy - 4, cx - 11, cy + 10); + line(ctx, &d, cx + 11, cy - 4, cx + 11, cy + 10); + line(ctx, &acc, cx - 6, cy - 4, cx + 7, cy - 12); + arc(ctx, 2, KG_C_ACCENT, cx - 4, cy + 3, 3, 0, 360); + line(ctx, &d, cx + 4, cy, cx + 7, cy); + line(ctx, &d, cx + 4, cy + 4, cx + 7, cy + 4); + break; + } + case KG_ICON_VIDEO: /* camera */ + line(ctx, &d, cx - 12, cy - 6, cx + 3, cy - 6); + line(ctx, &d, cx - 12, cy + 7, cx + 3, cy + 7); + line(ctx, &d, cx - 12, cy - 6, cx - 12, cy + 7); + line(ctx, &d, cx + 3, cy - 6, cx + 3, cy + 7); + line(ctx, &d, cx + 3, cy - 1, cx + 11, cy - 6); + line(ctx, &d, cx + 3, cy + 2, cx + 11, cy + 7); + line(ctx, &d, cx + 11, cy - 6, cx + 11, cy + 7); + arc(ctx, 2, KG_C_ACCENT, cx - 7, cy - 1, 1, 0, 360); + break; case KG_ICON_TORCH: /* flashlight */ line(ctx, &d, cx - 3, cy, cx - 3, cy + 12); line(ctx, &d, cx + 3, cy, cx + 3, cy + 12); diff --git a/src/ui/nav.h b/src/ui/nav.h index 63e0550..83780a8 100644 --- a/src/ui/nav.h +++ b/src/ui/nav.h @@ -12,6 +12,7 @@ extern "C" { typedef enum { KG_ICON_CHRONO, KG_ICON_TIMER, KG_ICON_ALARM, KG_ICON_STEPS, KG_ICON_SETTINGS, KG_ICON_WEATHER, KG_ICON_NET, KG_ICON_REMOTE, KG_ICON_SYSTEM, KG_ICON_LEVEL, KG_ICON_WORLD, KG_ICON_SKY, KG_ICON_TORCH, + KG_ICON_RADIO, KG_ICON_VIDEO, } kg_icon_t; typedef struct { @@ -24,7 +25,8 @@ typedef struct { } kg_app_t; extern const kg_app_t kg_app_chrono, kg_app_timer, kg_app_alarms, kg_app_steps, kg_app_weather, kg_app_settings, - kg_app_net, kg_app_remote, kg_app_system, kg_app_level, kg_app_world, kg_app_sky, kg_app_torch; + kg_app_net, kg_app_remote, kg_app_system, kg_app_level, kg_app_world, kg_app_sky, kg_app_torch, kg_app_radio, + kg_app_video; /* Opens a network tool directly ("ping", "wifi"…), with the toolbox already created. */ bool ui_net_open(const char *tool); diff --git a/tests/data/sine-440hz.mp3 b/tests/data/sine-440hz.mp3 new file mode 100644 index 0000000..3222cf0 Binary files /dev/null and b/tests/data/sine-440hz.mp3 differ diff --git a/tests/data/smpte-320x240.jpg b/tests/data/smpte-320x240.jpg new file mode 100644 index 0000000..0a23064 Binary files /dev/null and b/tests/data/smpte-320x240.jpg differ diff --git a/tests/net_probe.c b/tests/net_probe.c index d9f5b0c..687c5b5 100644 --- a/tests/net_probe.c +++ b/tests/net_probe.c @@ -1,9 +1,14 @@ /* * Manual check of the network primitives against the real network (macOS / Linux, no root): - * cc -Wall -Wextra -Werror -I src tests/net_probe.c src/data/kg_net.c -o /tmp/kg_net + * cc -Wall -Wextra -Werror -I src tests/net_probe.c src/data/kg_net.c src/data/kg_media.c src/third_party/tjpgd/tjpgd.c -o /tmp/kg_net * /tmp/kg_net trace 1.1.1.1 | ping 1.1.1.1 | ports 1.1.1.1 | dns example.com | speed | pubip + * /tmp/kg_net radio [ | url] decodes 8 s of a station (built-in n, default 0), no sound + * /tmp/kg_net video decodes 5 s of an MJPEG stream */ +#include "data/kg_media.h" #include "data/kg_net.h" +#include +#include #include #include #include @@ -25,10 +30,70 @@ static bool text_sink(const uint8_t *d, size_t n, void *ctx) return true; } +static pthread_mutex_t mx = PTHREAD_MUTEX_INITIALIZER; +static void lk(void) { pthread_mutex_lock(&mx); } +static void ulk(void) { pthread_mutex_unlock(&mx); } +static void *zalloc(size_t n) { return calloc(1, n); } +static void nap(uint32_t ms) { usleep(ms * 1000); } +static bool up(void) { return true; } +static size_t samples; +static bool p_open(uint32_t rate) +{ + printf(" audio open at %u Hz\n", rate); + return true; +} +static void p_write(const int16_t *s, size_t n) +{ + (void)s; + samples += n; + usleep((useconds_t)(n * 1000000 / 44100)); /* real-time pacing, as the I2S DMA does */ +} +static void p_close(void) {} +static const kg_media_env_t ENV = {lk, ulk, zalloc, free, nap, up, p_open, p_write, p_close}; + +static void *radio_net(void *r) { kg_radio_net_loop(r); return NULL; } +static void *radio_play(void *r) { kg_radio_play_loop(r); return NULL; } +static void *video_run(void *v) { kg_video_loop(v); return NULL; } + +static int media(const char *cmd, const char *arg) +{ + pthread_t a, b; + if (!strcmp(cmd, "radio")) { + static kg_radio_t r; + int n = arg && arg[0] >= '0' && arg[0] <= '9' ? atoi(arg) : 0; + const char *url = arg && !(arg[0] >= '0' && arg[0] <= '9') ? arg : KG_STATIONS[n].url; + if (!kg_radio_init(&r, &ENV, url == arg ? "custom" : KG_STATIONS[n].name, url, 10)) return 1; + pthread_create(&a, NULL, radio_net, &r); + pthread_create(&b, NULL, radio_play, &r); + for (int t = 0; t < 16; t++) { + usleep(500000); + lk(); + printf(" %.1f s state %d buffer %u %% %u kbps %u Hz underruns %u title \"%s\" %s\n", t * 0.5 + 0.5, r.st.state, + r.st.buffer_pct, r.st.kbps, r.st.rate, r.st.underruns, r.st.title, r.st.error); + ulk(); + } + r.cancel = true; + pthread_join(a, NULL); + pthread_join(b, NULL); + printf("%s: %zu samples decoded\n", r.st.station, samples); + return samples > 44100 ? 0 : 1; + } + static kg_video_t v; + if (!arg || !kg_video_init(&v, &ENV, arg, false)) return 1; + pthread_create(&a, NULL, video_run, &v); + sleep(5); + v.cancel = true; + pthread_join(a, NULL); + printf("video: state %d, %u frames, %.1f fps, %u kbit/s, %ux%u %s\n", v.st.state, v.st.frames, v.st.fps, v.st.kbps, + v.st.src_w, v.st.src_h, v.st.error); + return v.st.frames ? 0 : 1; +} + int main(int argc, char **argv) { if (argc < 2) return 2; const char *cmd = argv[1], *host = argc > 2 ? argv[2] : "1.1.1.1"; + if (!strcmp(cmd, "radio") || !strcmp(cmd, "video")) return media(cmd, argc > 2 ? argv[2] : NULL); char a[16], b[16]; uint32_t ip, ms; if (!strcmp(cmd, "dns")) { diff --git a/tests/test_data.c b/tests/test_data.c index 7d8f383..8fef0af 100644 --- a/tests/test_data.c +++ b/tests/test_data.c @@ -1,12 +1,13 @@ /* * Host tests for the data parsers (macOS): - * cc -Wall -Wextra -Werror -I src tests/test_data.c src/data/kg_*.c -lm -o /tmp/kg_test + * cc -Wall -Wextra -Werror -I src tests/test_data.c src/data/kg_*.c src/third_party/tjpgd/tjpgd.c -lm -o /tmp/kg_test * /tmp/kg_test tests/data/openmeteo-montreal.json [tripUpdates.pb stops] * The STM feed (~1 MB, downloaded with the API key) is not committed. */ #include "data/kg_ancs.h" #include "data/kg_bus.h" #include "data/kg_ir.h" +#include "data/kg_media.h" #include "data/kg_net.h" #include "data/kg_sky.h" #include "data/kg_tz.h" @@ -174,6 +175,119 @@ static void test_ir(void) printf("ir: NEC, Samsung, Sony and RC5 trains OK (%d brands)\n", KG_IR_N_BRANDS); } +/* Media: ICY metadata, ring buffer, the radio decoder end to end, MJPEG and JPEG. */ +static void media_nop(void) {} +static void *media_alloc(size_t n) { return calloc(1, n); } +static void media_sleep(uint32_t ms) { (void)ms; } +static bool media_up(void) { return true; } +static uint32_t pcm_rate; +static size_t pcm_n; +static int pcm_crossings; +static int pcm_sign; +static bool pcm_open(uint32_t rate) +{ + pcm_rate = rate; + return true; +} +static void pcm_write(const int16_t *s, size_t n) +{ + for (size_t i = 0; i < n; i++, pcm_n++) { + int sign = s[i] > 1000 ? 1 : s[i] < -1000 ? -1 : 0; /* hysteresis: the padding's noise is not a wave */ + if (sign && sign != pcm_sign) { + if (pcm_sign) pcm_crossings++; + pcm_sign = sign; + } + } +} +static const kg_media_env_t MEDIA_ENV = {media_nop, media_nop, media_alloc, free, media_sleep, media_up, + pcm_open, pcm_write, media_nop}; + +static void test_media(const char *dir) +{ + /* ICY: 8 audio bytes, metadata block, 8 audio bytes, empty block, cut at awkward places. */ + uint8_t s[64], meta[32] = "StreamTitle='Air - La femme';"; + size_t n = 0; + for (int i = 0; i < 8; i++) s[n++] = (uint8_t)('a' + i); + s[n++] = 2; + memcpy(s + n, meta, 32), n += 32; + for (int i = 0; i < 8; i++) s[n++] = (uint8_t)('A' + i); + s[n++] = 0; + s[n++] = 'z'; + kg_icy_t icy; + kg_icy_init(&icy, 8); + uint8_t out[64]; + size_t got = 0; + for (size_t off = 0; off < n; off += 5) { + size_t k = off + 5 > n ? n - off : 5; + memcpy(out + got, s + off, k); + got += kg_icy_feed(&icy, out + got, k); + } + assert(got == 17 && !memcmp(out, "abcdefghABCDEFGHz", 17) && !strcmp(icy.title, "Air - La femme")); + uint8_t one[40] = "x\x02StreamTitle='Caf\xe9';"; /* 1 audio byte, a 32-byte block in Latin-1 */ + kg_icy_init(&icy, 1); + assert(kg_icy_feed(&icy, one, 34) == 1 && !strcmp(icy.title, "Caf\xC3\xA9")); + + kg_ring_t ring = {calloc(1, 10), 10, 0, 0}; + uint8_t b[16]; + assert(kg_ring_write(&ring, (const uint8_t *)"0123456789AB", 12) == 10 && kg_ring_read(&ring, b, 4) == 4); + assert(kg_ring_write(&ring, (const uint8_t *)"CDEF", 4) == 4 && kg_ring_read(&ring, b, 16) == 10); + assert(!memcmp(b, "456789CDEF", 10)); + free(ring.buf); + + /* The radio's decoder on a 1 s, 440 Hz MP3: the second at 44.1 kHz (plus encoder padding), ~880 sign changes. */ + char path[256]; + snprintf(path, sizeof path, "%s/sine-440hz.mp3", dir); + size_t len; + char *mp3 = slurp(path, &len); + assert(mp3); + kg_radio_t r; + assert(kg_radio_init(&r, &MEDIA_ENV, "Test", "http://example.com/", KG_RADIO_VOL_MAX)); + assert(kg_ring_write(&r.ring, (const uint8_t *)mp3, (uint32_t)len) == len); + r.net_done = true; + kg_radio_play_loop(&r); + printf("media: MP3 → %zu samples at %u Hz, %d zero crossings\n", pcm_n, pcm_rate, pcm_crossings); + assert(pcm_rate == 44100 && pcm_n > 44100 && pcm_n < 49000 && abs(pcm_crossings - 880) < 10); + kg_radio_free(&r); + free(mp3); + + /* MJPEG: two copies of a JPEG between multipart headers, fed in small pieces. */ + snprintf(path, sizeof path, "%s/smpte-320x240.jpg", dir); + char *jpg = slurp(path, &len); + assert(jpg); + size_t total = 2 * (len + 64); + uint8_t *stream = malloc(total), *p = stream; + for (int k = 0; k < 2; k++) { + p += sprintf((char *)p, "--frame\r\nContent-Type: image/jpeg\r\n\r\n"); + memcpy(p, jpg, len), p += len; + p += sprintf((char *)p, "\r\n"); + } + kg_mjpeg_t mj = {malloc(64 * 1024), 64 * 1024, 0, false, false, 0}; + int frames = 0; + for (const uint8_t *q = stream; q < p;) { + size_t chunk = (size_t)(p - q) < 777 ? (size_t)(p - q) : 777, used; + while (chunk) { + if (kg_mjpeg_feed(&mj, q, chunk, &used)) { + assert(mj.len == len && !memcmp(mj.buf, jpg, len)); + frames++; + } + q += used, chunk -= used; + } + } + assert(frames == 2); + + /* 320 × 240 → 240 × 240: unscaled, cropped by 40 px each side. SMPTE bars: the first + * visible bar (x = 40 of the source) is white-ish grey, the second one yellow. */ + uint16_t *fb = malloc(KG_VIDEO_W * KG_VIDEO_H * 2), w, h; + void *work = malloc(KG_JPEG_WORK); + assert(kg_jpeg_decode(mj.buf, mj.len, fb, false, work, &w, &h) && w == 320 && h == 240); + uint16_t grey = fb[60 * KG_VIDEO_W + 5], yellow = fb[60 * KG_VIDEO_W + 30]; + int gr = grey >> 11 << 3, gg = (grey >> 5 & 63) << 2, gb = (grey & 31) << 3; + int yr = yellow >> 11 << 3, yg = (yellow >> 5 & 63) << 2, yb = (yellow & 31) << 3; + printf("media: JPEG %ux%u, pixels (%d,%d,%d) and (%d,%d,%d)\n", w, h, gr, gg, gb, yr, yg, yb); + assert(gr > 150 && gg > 150 && gb > 150 && yr > 150 && yg > 150 && yb < 80); + free(fb), free(work), free(mj.buf), free(stream), free(jpg); +} + int main(int argc, char **argv) { test_ancs(); @@ -181,6 +295,13 @@ int main(int argc, char **argv) test_tz(); test_sky(); test_ir(); + { + char dir[256]; + snprintf(dir, sizeof dir, "%s", argv[1]); + char *slash = strrchr(dir, '/'); + if (slash) *slash = 0; + test_media(dir); + } size_t len; char *json = slurp(argv[1], &len); assert(json); diff --git a/tools/make_screens.sh b/tools/make_screens.sh index 6790331..52c399a 100755 --- a/tools/make_screens.sh +++ b/tools/make_screens.sh @@ -33,9 +33,12 @@ shot t8-sky KG_APP=sky shot t8-level KG_APP=level shot t8-torch KG_APP=torch shot t8-system KG_APP=system +shot t9-radio KG_APP=radio KG_RADIO=play KG_SETTLE_MS=1200 +shot t9-video KG_APP=video KG_VIDEO_URL=http://192.168.2.50:81/stream KG_SETTLE_MS=600 +shot t9-video-setup KG_APP=video python3 tools/contact_sheet.py docs/screens/kronograf-overview.png --cols 4 \ $OUT/t5-aviator-badge.png $OUT/t2-face1.png $OUT/t2-face2.png $OUT/t4-launcher.png \ $OUT/t4-weather.png $OUT/t5-card.png $OUT/t3-timer.png $OUT/t3-alarms.png \ $OUT/t8-net-wifi.png $OUT/t8-net-ping.png $OUT/t8-net-trace.png $OUT/t8-net-speed.png \ - $OUT/t8-world.png $OUT/t8-sky.png $OUT/t6-remote.png $OUT/t8-level.png + $OUT/t8-world.png $OUT/t8-sky.png $OUT/t9-radio.png $OUT/t9-video.png diff --git a/tools/mjpeg_test_server.py b/tools/mjpeg_test_server.py new file mode 100755 index 0000000..fb3f346 --- /dev/null +++ b/tools/mjpeg_test_server.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""MJPEG test stream for the Video app, without a camera. + + tools/mjpeg_test_server.py [--size 320x240] [--fps 15] [--port 8090] + +Renders a looping test pattern once with ffmpeg, then serves it to any number of clients +at http://:/stream.mjpg (multipart MJPEG) and /snapshot.jpg (one JPEG). +""" +import argparse +import glob +import http.server +import os +import socket +import subprocess +import tempfile +import time + +ap = argparse.ArgumentParser() +ap.add_argument("--size", default="320x240") +ap.add_argument("--fps", type=int, default=15) +ap.add_argument("--port", type=int, default=8090) +a = ap.parse_args() + +tmp = tempfile.mkdtemp() +subprocess.run(["ffmpeg", "-loglevel", "error", "-f", "lavfi", "-i", f"testsrc2=size={a.size}:rate={a.fps}", + "-t", "4", "-q:v", "7", os.path.join(tmp, "f%03d.jpg")], check=True) +FRAMES = [open(p, "rb").read() for p in sorted(glob.glob(os.path.join(tmp, "*.jpg")))] + + +class Handler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + if self.path.startswith("/snapshot"): + f = FRAMES[int(time.time() * a.fps) % len(FRAMES)] + self.send_response(200) + self.send_header("Content-Type", "image/jpeg") + self.send_header("Content-Length", str(len(f))) + self.end_headers() + self.wfile.write(f) + return + self.send_response(200) + self.send_header("Content-Type", "multipart/x-mixed-replace; boundary=frame") + self.end_headers() + try: + i = 0 + while True: + f = FRAMES[i % len(FRAMES)] + self.wfile.write(b"--frame\r\nContent-Type: image/jpeg\r\nContent-Length: %d\r\n\r\n" % len(f) + f + b"\r\n") + i += 1 + time.sleep(1 / a.fps) + except (BrokenPipeError, ConnectionResetError): + pass + + def log_message(self, *args): + pass + + +ip = socket.gethostbyname(socket.gethostname()) if False else subprocess.run( + ["ipconfig", "getifaddr", "en0"], capture_output=True, text=True).stdout.strip() or "localhost" +print(f"{len(FRAMES)} frames, {a.size} at {a.fps} fps") +print(f" stream: http://{ip}:{a.port}/stream.mjpg") +print(f" snapshot: http://{ip}:{a.port}/snapshot.jpg (Ctrl-C to stop)") +http.server.ThreadingHTTPServer(("0.0.0.0", a.port), Handler).serve_forever()