From eebe01dd592b5e094113112660b1b4ec8c642d81 Mon Sep 17 00:00:00 2001 From: Francois <48120561+storagebits@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:05:42 -0400 Subject: [PATCH 1/7] =?UTF-8?q?Data:=20HTTP=20client=20for=20streams=20?= =?UTF-8?q?=E2=80=94=20ports,=20redirects,=20ICY=20status,=20headers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kg_http_request() generalises the toolbox's GET: http://host:port/path URLs, up to three redirects (relative ones included), SHOUTcast's "ICY 200" status line, extra request headers, response headers through a callback, an idle timeout for endless streams and an optional total one. kg_http_get() is now a thin wrapper. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BHBYox5ENEx9Hdd9LiBx8a --- src/data/kg_net.c | 126 ++++++++++++++++++++++++++++++++++++---------- src/data/kg_net.h | 25 +++++++++ 2 files changed, 124 insertions(+), 27 deletions(-) diff --git a/src/data/kg_net.c b/src/data/kg_net.c index 384046d..566fd64 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[256], loc[256]; + 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); From dbf54a0ef4ca46b56ba608b396721f9d64f3db54 Mon Sep 17 00:00:00 2001 From: Francois <48120561+storagebits@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:05:42 -0400 Subject: [PATCH 2/7] Data: web radio and MJPEG video pipelines, with minimp3 and TJpgDec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kg_media: portable loops shared by the watch and the simulator. - Radio: MP3 over HTTP into a single-producer / single-consumer ring buffer, ICY metadata stripped for the song title (UTF-8 or Latin-1), minimp3 decoding to mono with a quadratic volume; prebuffering, rebuffering after an underrun, reconnection. - Video: JPEG images cut out of an MJPEG stream (or a snapshot URL polled every second), decoded by TJpgDec, scaled and centred into 240 × 240 RGB565, triple-buffered. - Vendored in src/third_party with their licences: minimp3 (CC0; its 16 KB scratch can come from the caller, "Kronograf patch") and TJpgDec R0.03 (RGB565, 32-bit fast decode). - Tests: ICY stripping across chunk boundaries, ring wrap-around, a 1 s 440 Hz MP3 decoded end to end (879 zero crossings for 880), two JPEGs extracted from a multipart stream and decoded with the right SMPTE colours. Fixtures generated with ffmpeg. - tests/net_probe.c plays real stations and MJPEG streams without sound: the five built-in stations decode without underruns, redirects included. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BHBYox5ENEx9Hdd9LiBx8a --- .github/workflows/ci.yml | 4 +- sim/CMakeLists.txt | 5 +- src/data/kg_media.c | 496 ++++++++ src/data/kg_media.h | 165 +++ src/third_party/README.md | 6 + src/third_party/minimp3/LICENSE | 117 ++ src/third_party/minimp3/minimp3.h | 1873 +++++++++++++++++++++++++++++ src/third_party/tjpgd/tjpgd.c | 1153 ++++++++++++++++++ src/third_party/tjpgd/tjpgd.h | 102 ++ src/third_party/tjpgd/tjpgdcnf.h | 33 + tests/data/sine-440hz.mp3 | Bin 0 -> 8611 bytes tests/data/smpte-320x240.jpg | Bin 0 -> 3641 bytes tests/net_probe.c | 67 +- tests/test_data.c | 123 +- 14 files changed, 4138 insertions(+), 6 deletions(-) create mode 100644 src/data/kg_media.c create mode 100644 src/data/kg_media.h create mode 100644 src/third_party/README.md create mode 100644 src/third_party/minimp3/LICENSE create mode 100644 src/third_party/minimp3/minimp3.h create mode 100644 src/third_party/tjpgd/tjpgd.c create mode 100644 src/third_party/tjpgd/tjpgd.h create mode 100644 src/third_party/tjpgd/tjpgdcnf.h create mode 100644 tests/data/sine-440hz.mp3 create mode 100644 tests/data/smpte-320x240.jpg 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/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/src/data/kg_media.c b/src/data/kg_media.c new file mode 100644 index 0000000..4634503 --- /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[1460]; + while (len && !r->cancel) { + size_t n = len < sizeof tmp ? len : sizeof tmp; + 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..275314f --- /dev/null +++ b/src/data/kg_media.h @@ -0,0 +1,165 @@ +/* + * 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; + 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/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/tests/data/sine-440hz.mp3 b/tests/data/sine-440hz.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..3222cf0dfb8b96b95ff60a8e930fe898c9e84f30 GIT binary patch literal 8611 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

yuO<9|yI+83AgZ^=E1{dbIo3;7K+ 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 M|Id5=kN*9C0q0?gbpQYW literal 0 HcmV?d00001 diff --git a/tests/data/smpte-320x240.jpg b/tests/data/smpte-320x240.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0a2306450d8aad377a977c8482c7bd669c612dcd GIT binary patch literal 3641 zcmeHJc|6ox8$UC{3?YUjk*rs7m2FI^v1BV1g(yVIo^|Y*VTvr#btTsl4R@48C1Xi; z)9r>vvSgW=Y}v|gY>9b))cf8)-hbae-amSNpU-*Dc|PBB&i6UbbDs0#^l%1%kg4$r zV*rB#0E0fj*#JVPoPw`tsU1)|d_YZA4R9F10Pt+v#cY`#pLU3##E_KBM!C*;rGr{KX)fGDQGXf? zpB0t4BG;&ip_Vww%la!A>Z2hC;oK+`9~VSmYkL@+3xSEEmLSoZ-0HM|>v;d?XAOll zjB@`JZI`(bt!AnJeus!fgY2=!Sx!Ab^MF~H9s(u{;AntA15O{{N5G%~)Yk?5^$^eI zWeW}K&m-Xl;)RyC>9QdjKKt1$=doj<{6*COoGx!mO+KN;IA(1aC1_FHRc?OhT5w`q zVVRRAvfkwq;98qJPPe#0T#|P!ka#6d!O~wW^jTLR;&{Z}0qDNR=it7}q}<9|zyQ8# zFtU_`BzXcL`K)*tu`%RcRjZj*qel7Ps}b2*GD?{ZIaW}dH*Yhu6F@H-D% z=5}dmmmGj_@KOoOagypiTZ-Z58xc(FBkhJg0svo2OYF+u*YEV&v*Y)p`q@-GV@Aca zYX}&>y99VYY*H?;b2ojH*`vBBDq!cipxjZKWpptG@WN;SEFR}4jBk1$B!}^NYV$HV zv+NMFT`@pwhBGFT+ z5(a1LYtO6_AHi~Vlb|3^9-`agZ!bx@J~@ndCCaH= z#w+BxiV|I_QFWl zuF9+=QLQ;(0E`RG0)g-%-DYoH%=$&-9@hiK9I(IiTlZq%uAr8LIgvZrO6IB??&}Lr zRl&m(0H`lxiS5kV-n8`Cd$kijj$Y7v19|h7U{^j9)~dn?sh%l4g>w>H#_bGOM6Eh@ zj~WaOu`0q_{e2iUW5nDqJav`h)ZV?#8+Gb?#dc0reGC~hZ7SoT`HrvlhprI3PQ5<- zTZDC`=u@*VmP$&_jud>-$h8PU(^uh0!F6-Wd)bx9P#d!_vEC5 zhx=9kZ2r}yfX_5y9uC|2%;eW2lxAQ1UGb-`plvg7t2%|9HJ%PJ_tdX+Nmg1`rHyBM zbjD!DQ~Grj8e8LW3q$rQh5XE z2yj5lbkN=a+Y|;XW8+OiT46s;W=@CD6GL&Ws*bC~I~y*qbXdJVQ!z;au3SLTD}{Rs6bdU$*(h!FS&O@}yKRvOJPg>8*q>O>RQX+lWBiMC^t<(z^c?fIyGNFW!l_inqJ#Mt&?k-3FuZ% zJ2=b%C1Qhvd5zC6DAc@uyqv0DNnAziPMnM6iJ)fmC(mBc_4_uoK3O?FQ$_0gTJvGA z&7cPdoPK>c;^dbwmHXztZ1%z!g9F5=TCK^%-eLPa@ud}6z;*l9KypHibsy8vLx81g z8!fxt_6x{!3=Zu^In5#m;Cd#|O#%Ppri1e}-vWo<&WD)X{aEnVibu2A%>xZ92~7@n zv3io!A_$8V8Z)RoLhVtF!Y<@8vwJ6Z=_^Fhd*>W1DT*{RoHRGB6M)sJUAC4ssJ0(E zkpqx!GOTR`-E+*AHe=Y1!=%k^p`YC^CPWs^gvdmVoeXQ}KN(iPY4ri}Tz-&7bP!G1 zc-qUO4;M#G9)3+7ABhbNJN-G!F(xZ-++uQXTy^S%K_IL}dvli4_P`@4Rk$b~jY#c1 zO|1J3(Zc7CvdE$%+Uf+&;dWq;gX+!w_ctG4{D?nv=(|~0{GPUa+Tz8xHC&g_2Imp|gbK|Mj#YIlcb^8KP=N literal 0 HcmV?d00001 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); From e8ca93c089156f1f7e093be0b729133303ca1526 Mon Sep 17 00:00:00 2001 From: Francois <48120561+storagebits@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:05:42 -0400 Subject: [PATCH 3/7] HAL: radio and video on the watch and in the simulator - Watch (media.cpp): radio in two tasks on core 0 (network, decoder) with buffers in PSRAM, Wi-Fi kept on while it plays (net_keep), screen off included; video in one task under the app's Wi-Fi lease. URLs and volume in NVS ("kgmedia"). - Speaker (feedback.cpp): a 16-bit mono stream at the station's rate; one user at a time, tones are dropped while the radio plays. - Console: radio [play | stop | vol | url ], video [play | stop | url ]. - Simulator: demo data for screenshots (KG_RADIO, KG_VIDEO_URL, a test card decoded by TJpgDec); with KG_NET_REAL=1 the real loops, the radio through SDL audio. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BHBYox5ENEx9Hdd9LiBx8a --- sim/demo-camera.jpg | Bin 0 -> 4319 bytes src/hal/device/console.cpp | 20 ++- src/hal/device/feedback.cpp | 57 ++++++++- src/hal/device/hal_device.h | 11 ++ src/hal/device/media.cpp | 241 ++++++++++++++++++++++++++++++++++++ src/hal/device/net.cpp | 10 +- src/hal/hal.h | 21 ++++ src/hal/sim/hal_sim_media.c | 196 +++++++++++++++++++++++++++++ 8 files changed, 551 insertions(+), 5 deletions(-) create mode 100644 sim/demo-camera.jpg create mode 100644 src/hal/device/media.cpp create mode 100644 src/hal/sim/hal_sim_media.c diff --git a/sim/demo-camera.jpg b/sim/demo-camera.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dc764e90147c9b390e360fdcc170711d5460691b GIT binary patch literal 4319 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 Hoc{g4ng#aQ literal 0 HcmV?d00001 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..1cf6ff0 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, 8 DMA buffers (~45 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, 8); + 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..3b8c705 --- /dev/null +++ b/src/hal/device/media.cpp @@ -0,0 +1,241 @@ +/* + * 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 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; + xTaskCreatePinnedToCore(radio_net_task, "kg_radio_net", 6144, nullptr, 2, nullptr, 0); + xTaskCreatePinnedToCore(radio_play_task, "kg_radio_mp3", 6144, nullptr, 3, nullptr, 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"}; + 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..5c915be --- /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.sh 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; +} From 48636572cc939227497f9289773f55721d25e27c Mon Sep 17 00:00:00 2001 From: Francois <48120561+storagebits@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:05:42 -0400 Subject: [PATCH 4/7] UI: Radio and Video apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Radio: station and genre, scrolling song title, stream state, previous / play-stop / next (switches station while playing), volume; long-press the station for a custom URL. - Video: full-screen camera picture, a status overlay for the first seconds or on tap (frame rate, source size, bit rate), URL entry on the watch. - The launcher's second page: Network, Radio, Video, Remote, System, Settings. - README (features, console, simulator, development workflow), DECISIONS D57–D59, and the overview's last row: world clock, sun & moon, radio, video. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BHBYox5ENEx9Hdd9LiBx8a --- DECISIONS.md | 8 + README.md | 23 ++- docs/screens/kronograf-overview.png | Bin 127303 -> 124362 bytes docs/screens/sim/t2-face1.png | Bin 15196 -> 15346 bytes docs/screens/sim/t6-launcher-tools.png | Bin 8263 -> 9414 bytes docs/screens/sim/t9-radio.png | Bin 0 -> 10983 bytes docs/screens/sim/t9-video-setup.png | Bin 0 -> 8409 bytes docs/screens/sim/t9-video.png | Bin 0 -> 9245 bytes src/ui/app_radio.c | 254 +++++++++++++++++++++++++ src/ui/app_video.c | 211 ++++++++++++++++++++ src/ui/nav.c | 25 ++- src/ui/nav.h | 4 +- tools/make_screens.sh | 5 +- tools/mjpeg_test_server.sh | 12 ++ 14 files changed, 535 insertions(+), 7 deletions(-) create mode 100644 docs/screens/sim/t9-radio.png create mode 100644 docs/screens/sim/t9-video-setup.png create mode 100644 docs/screens/sim/t9-video.png create mode 100644 src/ui/app_radio.c create mode 100644 src/ui/app_video.c create mode 100755 tools/mjpeg_test_server.sh diff --git a/DECISIONS.md b/DECISIONS.md index a802213..492521a 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. + +**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.sh` (ffmpeg); 15 fps at 320 × 240 decoded in the simulator. Expected on the watch: ~10–15 fps with Bluetooth off, fewer with it on (D52). diff --git a/README.md b/README.md index 9f8e3f2..2b1e99c 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.sh # 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 cb2b10b54912df53edfacc810b3fb0b114ed78f8..1bec136b5ffff1da15cb3b810411ddeb9ba0373e 100644 GIT binary patch delta 93289 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ⅅ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

I3%nH{Hqjd+xZwtT%4}`Bs`I0_->Q5t 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 TRB~#MKtB@$bNxzP=lK5uCC%>~ delta 96241 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
#=V$NyY&lF;V^t^f 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 SrHUpKXkZd6~t9ra(*+OInR diff --git a/docs/screens/sim/t2-face1.png b/docs/screens/sim/t2-face1.png index d631628fec9ee2224a285a39707ac8580a871d5e..142b1cc6200d78e9fad097df23049a4a59e40c40 100644 GIT binary patch delta 3854 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 delta 3680 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 RltJ{%P}l6z3vFWL{{WH-$3g%A delta 6839 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 diff --git a/docs/screens/sim/t9-radio.png b/docs/screens/sim/t9-radio.png new file mode 100644 index 0000000000000000000000000000000000000000..18b4364898cdc5ac0fd1a919978c70408265a953 GIT binary patch literal 10983 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

a-RBSk~|DP7O>6jp(`)ZwdPYke3v?ctu@hvdHOa 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 K=YH9h&iP;G)&vXy literal 0 HcmV?d00001 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/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.sh b/tools/mjpeg_test_server.sh new file mode 100755 index 0000000..6db1fa9 --- /dev/null +++ b/tools/mjpeg_test_server.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Serves an animated MJPEG test pattern on http://:8090/stream.mjpg, to try the +# Video app without a camera (simulator or watch on the same network). Needs ffmpeg. +# tools/mjpeg_test_server.sh [size] [fps] defaults: 320x240, 15 +set -uo pipefail +SIZE=${1:-320x240} +FPS=${2:-15} +echo "MJPEG test pattern: http://$(ipconfig getifaddr en0 2>/dev/null || hostname -I | cut -d' ' -f1):8090/stream.mjpg (Ctrl-C to stop)" +while true; do # ffmpeg's built-in server takes one client at a time: serve the next one + ffmpeg -loglevel error -re -f lavfi -i "testsrc2=size=$SIZE:rate=$FPS" -f mpjpeg -q:v 7 \ + -listen 1 http://0.0.0.0:8090/stream.mjpg || sleep 1 +done From 155b2d05ae6946efa7fb14e27925377cfd097356 Mon Sep 17 00:00:00 2001 From: Francois <48120561+storagebits@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:05:43 -0400 Subject: [PATCH 5/7] Release: pre-releases from tags with a suffix; version 0.7.0-beta.1 A tag such as v0.7.0-beta.1 or v0.7.0-rc.1 is published as a GitHub pre-release with a note that it is a testing build, never as "Latest". Plain vX.Y.Z tags are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BHBYox5ENEx9Hdd9LiBx8a --- .github/workflows/release.yml | 15 +++++++++++++-- include/kg_version.h | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) 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/include/kg_version.h b/include/kg_version.h index 0237492..482e35c 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.1" From 0ff2f601dca9107106adfb49173458b240a6d35b Mon Sep 17 00:00:00 2001 From: Francois <48120561+storagebits@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:07:53 -0400 Subject: [PATCH 6/7] Data: room for a rebuilt redirect URL (GCC format-truncation) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BHBYox5ENEx9Hdd9LiBx8a --- src/data/kg_net.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/kg_net.c b/src/data/kg_net.c index 566fd64..49ca8b8 100644 --- a/src/data/kg_net.c +++ b/src/data/kg_net.c @@ -394,7 +394,7 @@ static int http_once(const kg_http_req_t *r, const kg_url_t *u, char *location, int kg_http_request(const kg_http_req_t *r) { - char url[256], loc[256]; + 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; From 8826d66bd89e38a5c2f2ec83ea1ee99651109308 Mon Sep 17 00:00:00 2001 From: Francois <48120561+storagebits@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:09:21 -0400 Subject: [PATCH 7/7] Radio and video: tuned on the watch; multi-client MJPEG test server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured on the watch with beta.1: - The radio's network task peaked at 5.9 of its 6 KB of stack (DNS, redirects): it gets 7 KB, and its 1.5 KB staging buffer moves into the session (PSRAM). The decoder used 1.5 of 6 KB: 3.5 KB now. Six I²S DMA buffers instead of eight. The console's "radio" shows the stacks' free space while playing. - Results: no underruns with Bluetooth on, ~24 % internal RAM free while playing (31 % once stopped); video at 5–6 fps, 27 % free. - tools/mjpeg_test_server.py replaces the ffmpeg-only script, whose built-in server takes a single client and stayed stuck on a stale connection; it also serves /snapshot.jpg. Version 0.7.0-beta.2. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BHBYox5ENEx9Hdd9LiBx8a --- DECISIONS.md | 4 +-- README.md | 2 +- include/kg_version.h | 2 +- src/data/kg_media.c | 4 +-- src/data/kg_media.h | 1 + src/hal/device/feedback.cpp | 4 +-- src/hal/device/media.cpp | 10 ++++-- src/hal/sim/hal_sim_media.c | 2 +- tools/mjpeg_test_server.py | 62 +++++++++++++++++++++++++++++++++++++ tools/mjpeg_test_server.sh | 12 ------- 10 files changed, 80 insertions(+), 23 deletions(-) create mode 100755 tools/mjpeg_test_server.py delete mode 100755 tools/mjpeg_test_server.sh diff --git a/DECISIONS.md b/DECISIONS.md index 492521a..70e6844 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -139,6 +139,6 @@ Each entry: decision — rationale. Most recent at the bottom. **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. +**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.sh` (ffmpeg); 15 fps at 320 × 240 decoded in the simulator. Expected on the watch: ~10–15 fps with Bluetooth off, fewer with it on (D52). +**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 2b1e99c..a854857 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ sim/build/kronograf_sim # ×3 window, the 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.sh # an MJPEG test stream for the Video app +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) ``` diff --git a/include/kg_version.h b/include/kg_version.h index 482e35c..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.7.0-beta.1" +#define KG_VERSION "0.7.0-beta.2" diff --git a/src/data/kg_media.c b/src/data/kg_media.c index 4634503..4cc74c3 100644 --- a/src/data/kg_media.c +++ b/src/data/kg_media.c @@ -182,9 +182,9 @@ static void radio_header(const char *name, const char *value, void *ctx) static bool radio_sink(const uint8_t *data, size_t len, void *ctx) { kg_radio_t *r = ctx; - uint8_t tmp[1460]; + uint8_t *tmp = r->chunk; while (len && !r->cancel) { - size_t n = len < sizeof tmp ? len : sizeof tmp; + size_t n = len < sizeof r->chunk ? len : sizeof r->chunk; memcpy(tmp, data, n); data += n; len -= n; diff --git a/src/data/kg_media.h b/src/data/kg_media.h index 275314f..f3ef1f4 100644 --- a/src/data/kg_media.h +++ b/src/data/kg_media.h @@ -92,6 +92,7 @@ typedef struct { 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; diff --git a/src/hal/device/feedback.cpp b/src/hal/device/feedback.cpp index 1cf6ff0..4fb7932 100644 --- a/src/hal/device/feedback.cpp +++ b/src/hal/device/feedback.cpp @@ -142,7 +142,7 @@ static void audio_task(void *) } #endif -/* Audio stream (radio): 16-bit mono at the stream's rate, 8 DMA buffers (~45 ms at 44.1 kHz) +/* 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) { @@ -155,7 +155,7 @@ bool audio_stream_open(uint32_t rate) s_streaming = true; } i2s_stop(); - i2s_start(rate, 8); + i2s_start(rate, 6); return s_i2s_ready; #else (void)rate; diff --git a/src/hal/device/media.cpp b/src/hal/device/media.cpp index 3b8c705..b55fa0b 100644 --- a/src/hal/device/media.cpp +++ b/src/hal/device/media.cpp @@ -82,6 +82,7 @@ void hal_video_set_url(const char *url) 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) { @@ -140,8 +141,10 @@ void hal_radio_play(const char *name, const char *url) } net_keep(true); s_radio_tasks = 2; - xTaskCreatePinnedToCore(radio_net_task, "kg_radio_net", 6144, nullptr, 2, nullptr, 0); - xTaskCreatePinnedToCore(radio_play_task, "kg_radio_mp3", 6144, nullptr, 3, nullptr, 0); + /* ~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) @@ -230,6 +233,9 @@ 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); diff --git a/src/hal/sim/hal_sim_media.c b/src/hal/sim/hal_sim_media.c index 5c915be..4f47605 100644 --- a/src/hal/sim/hal_sim_media.c +++ b/src/hal/sim/hal_sim_media.c @@ -3,7 +3,7 @@ * 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.sh for + * (kg_media.c) in threads, the radio through SDL audio (try tools/mjpeg_test_server.py for * the video). */ #include "../hal.h" 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() diff --git a/tools/mjpeg_test_server.sh b/tools/mjpeg_test_server.sh deleted file mode 100755 index 6db1fa9..0000000 --- a/tools/mjpeg_test_server.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -# Serves an animated MJPEG test pattern on http://:8090/stream.mjpg, to try the -# Video app without a camera (simulator or watch on the same network). Needs ffmpeg. -# tools/mjpeg_test_server.sh [size] [fps] defaults: 320x240, 15 -set -uo pipefail -SIZE=${1:-320x240} -FPS=${2:-15} -echo "MJPEG test pattern: http://$(ipconfig getifaddr en0 2>/dev/null || hostname -I | cut -d' ' -f1):8090/stream.mjpg (Ctrl-C to stop)" -while true; do # ffmpeg's built-in server takes one client at a time: serve the next one - ffmpeg -loglevel error -re -f lavfi -i "testsrc2=size=$SIZE:rate=$FPS" -f mpjpeg -q:v 7 \ - -listen 1 http://0.0.0.0:8090/stream.mjpg || sleep 1 -done