Files
diskos/docs/RE_CATALOGUE.md
b0hemia e0bc4785e9 diskOS installer: initial public beta
Flashes the diskOS custom UI onto the FiiO Snowsky Disc over Ingenic mask-ROM USB,
building the image from your own stock firmware. Runs from source via install.sh.
2026-08-26 15:26:14 +10:00

428 lines
33 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Snowsky Disc - mq_player / mq_ui Reverse-Engineering Catalogue
Teardown of the stock V2.09 firmware binaries to document everything the player
and UI are capable of, so diskOS (our LVGL UI) can drive every feature.
- Targets: stock `usr/bin/mq_player` (4126620 B) and `usr/bin/mq_ui` (4418300 B).
MIPS32 little-endian, **glibc dynamically linked**
(interp `/lib/ld-linux-mipsn8.so.1`; 28 DT_NEEDED incl libc.so.6/libsqlite3/FFmpeg - NOT
static musl; only *our* diskOS binary is static-musl).
- Disasm cache (regenerate with `mipsel-linux-gnu-objdump`):
disassembly, data, and string dumps of the two binaries.
- Verification: addresses below were spot-checked against the cache on 2026-06-25.
Items marked **(unverified)** have not yet been individually confirmed at the
instruction level.
---
## ⚑ CORRECTIONS - full verification pass (2026-07)
Handler-level RE of both binaries; corrections spot-checked against the binary and applied inline below.
- **Binaries are glibc dynamic, not static musl** (interp `/lib/ld-linux-mipsn8.so.1`, 28 DT_NEEDED). Affects any replacement-binary / preload work.
- **IPC frame = `TAG(4) + TOTAL_LEN(4hex) + VALUE1(4hex) + optional payload`** - LEN is *total*, not payload length; class-2 cmds add VALUE2. Builder `mq_ui 0x43f82c` (`%s%04X%04X%s`). Replies `%s%04X%04X%s` to `/ui`. Queue `/player` maxmsg=20 msgsize=8192.
- **`06b3` = BT CODEC SELECT** (0=SBC 1=AAC 2/3/4=LDAC; worker 0x40eaf4 → `set_out_dev_sample_array(2,44100,44100)` for SBC/AAC, 96000 for LDAC). **`06b4` = LDAC quality only** (mob/std/high via /usr/data/bt_pipe_recv). ← the two were swapped in our old notes.
- **✅ BT AUDIO OUTPUT NOW WORKS (2026-08-03, live-fixed).** The DAP CAN transmit to a BT speaker (a2dp-source) - it just wasn't exposed and the code is fragile. The crash we hit was NOT the 06b3 codec value and NOT unfinished code: it was **skipping the mandatory `0666000C0006` pre-stop** before `0666000C0002`. Full working route sequence + the `--sbc-quality=medium` (stereo, no stutter) fix → **COMMAND_MAP.md "BT AUDIO OUTPUT" section**. The earlier "SBC fix frame = 06b3000C0000" was wrong/incomplete: diskOS uses `06b3001D0000<MAC>` after the pre-stop + `06c1` init.
- **`06b1`/`06b2`** = BT sample-rate / bit-depth params (not list/query).
- **`0689`** = EQ preset *apply* (21-way engine 0x449544), not a DB-only/media-info op.
- **`0201`** = generic transport toggle (not Roon-specific). **`0657`/`0666`/`06b3` "close_player" = false positive** (shared teardown preamble). `0666`=route (2=BTSRC 4=SPDIF 6=DAC).
- **out_dev** enum has no BTSINK (that's an input/work-mode). 48k→44.1k resample is `libfiio_decoder` over FFmpeg `libswresample` - a stock path, so codec-feasible (CPU headroom still needs an on-device xrun benchmark).
- **IPC-hardening RISK:** stock parser trusts declared LEN and uses `strlen` over the real `mq_receive` byte count → diskOS must emit strictly-correct total lengths and never forward untrusted frames.
- Still UNVERIFIED / deep-RE TODO: exact `0642` receiver-mode values + AirPlay/DLNA/Roon/QPlay trigger sequences; `0715` runtime volume callback @0x822b14; full `SET_USB_MODE` wire encoding; `0722` OTA worker chain; CS43131 ioctl ABI; the remaining ~190 family-inferred tag meanings.
---
## 1. How mq_player is driven - the command dispatch
mq_player is a background command server. The UI never touches audio; it mails
text commands to a POSIX message queue **`/player`**, and the player mails status
frames back on **`/ui`**.
### Wire format
- Command frame: `TAG(4 ASCII) + LEN(4 ASCII hex) + DATA`.
- Response builder @ **0x488d64** uses format `%s%04X%04X%s`
(tag, then two 16-bit hex fields, then payload). **(verified: prologue @0x488d64)**
### Dispatch tables (verified structure)
Two arrays of 8-byte `{descriptor_ptr, handler_ptr}` entries:
- **Table A @ 0x7c9d30** - primary handlers. Entry 0 = `{0x64f6e0, 0x411790}`.
~131 entries. **(count unverified; structure verified)**
- **Table B @ 0x7ca150** - secondary/extended. Many entries have
`handler = 0x00000000` = **declared but unimplemented** (e.g. `{0x64fca8, NULL}`).
~76 entries. **(count unverified; NULL-handler structure verified)**
Note: 0x7b7870 / 0x7b7c90 (noted in older sessions) are **PLT stubs**, not the
real tables. ~194 tag-like ASCII strings exist in `.str` total; ~30 of the table
slots are NULL / reloc-only / unimplemented (NAS / qplay family). **(unverified)**
A command arrives → tag matched against table → handler thunk → real handler.
Most handlers are tiny getters/setters over an in-memory settings struct.
### Confirmed action commands (tag strings verified present in `.str`)
| Tag | Action |
|------|------------------------------------------|
| 0100 | play by list (list_type + index) |
| 0103 | seek |
| 0104 | favorite / unfavorite |
| 0201 | transport (play/pause/next/prev) |
| 0202 | request current state |
| 0622 | rescan SD / rebuild DB |
| 0657 | source/work-mode transition (NOT play-mode) |
| 0666 | set output route (2=BTSRC 4=SPDIF 6=local-DAC) |
| 0689 | select+APPLY EQ preset (21-way engine 0x449544; NOT DB-only) |
| 0715 | set volume |
(0100/0103/0104/0201/0202/0622/0657/0666/0689/0715 all confirmed as ASCII tag
strings in `mq_player.str`. Full 207-tag enumeration is in the raw table dump and
should be transcribed here in a follow-up pass.)
---
## 2. Audio pipeline
Decode → process → output.
### Decode
- **FFmpeg** stack for mp3/flac/wav/aac/aif/m4a/ape/ogg/wma. **(unverified)**
- **libfiio_decoder** for DSD / DFF / DSF / SACD-ISO; DTS. **(unverified)**
- libsndfile present. **(unverified)**
### DSD / DoP
- Modes: NATIVE / DOP / D2P. DoP carrier up to 768k. Roon `configure_*` files in
`usr/project/config/roon/` corroborate (configure_768_dop, _384_d2p, etc.).
**(config files verified on disk; mode enum unverified)**
### Output routing (`out_dev`, tag 0666)
LOCAL_ANALOG (CS43131 headphone DAC) / BTSRC / USB_HOST / SPDIF / I2S3.
(BTSINK is NOT an out_dev route - it is an input/work-mode; see §1 and the work-mode enum in §7.)
**(unverified)**
### Volume - set_volume @ 0x489558 **(verified: prologue @0x489558)**
- Applied by shelling out to an amixer control named **`ICODEC HPOUTL GAIN`**
(string verified @ 0x264dd4 in `.str`).
- Uses inotify to react to external volume changes. **(unverified)**
- On-board MCU over SPI also carries gain/filter/mute opcodes
(SET_EQ_PARAMETER 0x100B etc.). **(unverified)**
### Bluetooth
- bluealsa-based; source + sink; codecs sbc / aac / ldac. **(unverified)**
- **Stock BT stack = BLUEZ, decoded from `mq_player` strings** (NOT BSA - `bsa_server`/`bt_enable_bsa*.sh`
are dead code, wrong chip BCM4345C5, no caller; mq_player references bsa_server 0×). Full bring-up embedded
in mq_player: `brcm_patchram_plus --enable_lpm --enable_hci --no2bytes --tosleep 200000 --baudrate 3000000
--patchram /lib/firmware/bt_bcm/BCM4343A1_001.002.009.1026.1055.hcd /dev/ttyS0` (downloads the chip fw patch
over UART → creates working hci0; nothing does this at boot) → `/usr/project/bluetoothd --noplugin=sap
--plugin=a2dp,avrcp --mode=source` → `bluealsa -S --device=hci0 --profile=a2dp-source --ldac-abr
--ldac-quality=standard --codec=sbc --initial-volume=48` → `hciconfig hci0 up/piscan/class 0x200414` →
`bluetoothctl agent on/default-agent/pairable on`. Sink mode = no-LPM patchram + `--mode=sink -p hfp-ag
--codec=sbc/aac/ldac`. The missing patchram step is why a naive BT enable produces a
poor scan.
### Gapless
- `audio_track_update_play_gapless`. **(unverified)**
### ReplayGain
- Stored / read from DB. **(unverified)**
---
## 3. EQ - the apply path (the key unlock)
The EQ is a **software parametric biquad cascade inside mq_player**, NOT a
hardware DAC feature. **CORRECTION:** `0689` does NOT merely write
to SQLite - its handler (`0x4961bc`) invokes the **21-way preset engine `0x449544`**,
updates the rate caps and applies the DSP, then replies `a639`. Custom bands are set
separately via `0678` (`0x44a658`). Both are live apply paths, not DB-only.
### DSP engine (verified prologues exist)
- Coefficient calc @ **0x448144** - uses pow/sqrt/sincos to turn
{filterType, frequency, gain, qValue} into biquad coefficients.
- IIR cascade @ **0x447ed0** and @ **0x4483a4** - per-sample filter, 32-bit
saturation. **(verified: both are function prologues)**
### Storage - PEQ table (SQL strings verified in `.str`)
Columns: `STYLE_NAME, MASTER_GAIN (REAL), PARAMS_JSON (text), STYLE_PRESET (int)`.
- PARAMS_JSON = array of bands `{filterType, frequency, gain, qValue}` (up to 10)
+ a master gain.
- Confirmed SQL @ ~0x257cb8 region:
- `INSERT INTO PEQ (STYLE_NAME, MASTER_GAIN, PARAMS_JSON, STYLE_PRESET) VALUES ('%s', %.1f, '%s', %d)`
- `UPDATE PEQ SET STYLE_NAME = ?, MASTER_GAIN = %.1f, PARAMS_JSON = ? WHERE STYLE_PRESET = ?`
- `SELECT 1 FROM PEQ WHERE STYLE_PRESET = ? LIMIT 1`
### To apply a custom EQ from diskOS
1. Write/UPDATE the PEQ row (bands JSON + master gain) for a STYLE_PRESET.
2. Send **0689** to select that preset.
The player loads bands → computes coeffs @0x448144 → runs the cascade
@0x447ed0/0x4483a4 on every sample before output.
### ✅ WIRED + CONFIRMED in diskOS (2026-06-30)
Custom EQ is live: `eqcustom.c` (10-band UI, horizontal scroll) → `mdb_set_peq()` writes PEQ
slot 11 in the format below → `ui_apply_eq(11)` sends 0689 → **player applies, sound changes
on-device (user-verified).** `ui_apply_eq` clamp raised 0x0A→20 to reach user slots 11-20.
### CAPTURED PARAMS_JSON format (live, V2.09, 2026-06-30)
Saved a stock "User 1" custom EQ (+12 @ 32 Hz, −12 @ 16 kHz) and read it back. Ground truth:
- **10 fixed bands**: 32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000 Hz, each ±12.0 dB; plus a **master ±12 dB** = the `MASTER_GAIN` REAL column.
- PARAMS_JSON = JSON **array of 10 objects**, e.g. one band:
`{"filterType":0,"frequency":32,"position":0,"gain":"12.0","qValue":"0.7"}`
- `filterType` = 0 (peaking) - **int**; `frequency` Hz - **int**; `position` 0-9 band index - **int**.
- **`gain` and `qValue` are JSON STRINGS** (quoted: `"12.0"`, `"-12.0"`, `"0.7"`), NOT numbers. gain is one-decimal dB; default qValue `"0.7"`.
- **User slot → STYLE_PRESET**: User 1-10 = **11-20** (User 1 = 11). Built-in presets = 0-10 (off,jazz,rock,r&b,hip-hop,pop,dance,classical,retro,sibilance-atten-1,sibilance-atten-2). Rows 160-169 exist but are unused/placeholder (`PARAMS_JSON` = literal string `"(null)"`).
- Stock writes via `UPDATE PEQ SET ... PARAMS_JSON = ? WHERE STYLE_PRESET = ?` then selects with **0689000C<preset hex>** (User 1 = `0689000C000B`).
- Other stock audio settings seen in the same menu (candidates for diskOS, map to documented name-cmds): Gain H/L, BT codec, SPDIF on/off, DAC digital filter (Fast/Slow LL, Fast/Slow PC, NOS, Wideband FF), DRE on/off.
---
## 4. The `/ui` frames - player → UI
mq_ui opens the **`/ui`** mqueue (name built @0x41b388, recv thread @0x415584,
`mq_receive` wrapper @0x41b5c8). Each frame = `TAG(4) + %x extension + JSON "other"`
(parser @0x415674: `strncpy …,4` then `sscanf %x`). Dispatch is a
`strncmp(cmd,tag,4)` chain in `ui_ctrl_response` (@0x41bfd0-0x41c098). Recv log
`[UI RECV]: %s` @0x272960. **(IPC path + format instruction-verified)**
Player→UI frames use **`a`-prefixed** tags (replies/reports) + `06d*` (OTA). Tag
meaning is taken from the adjacent log string (string-inferred, high confidence).
| TAG | meaning | payload |
|------|---------|---------|
| a620 | version reply | version `%s` |
| a710 | max-volume reply | `maxVolume` |
| a715 | volume / UAC srate change | `currentVolume` |
| aa1b | UAC sample-rate change | rate; `usbAudio` |
| a704/a705 | wifi connect status | state; `wifi_ssid` |
| a706 | net status | `%d` |
| a615 | language config | `%d` |
| a609 | theme config | `%d` |
| aa27 | charge-protect | `charge_protect` |
| aa24 | sys config | `%d`; `sys_count` |
| a634 | memory-play | `memoryPlay`/`memoryType` |
| a639 | EQ type | preset + `filterType/frequency/gain/qValue` |
| a6b6 | BT paired devices | count + list |
| a6c5/a6c3/a6c2/a6c1/a6c4/a6c0 | BT disc/connect/open replies | state + address |
| aa1c | power-key event | `%d` |
| aa22 | TF-card (SD) insert/remove | event `%d` |
| aa0c/aa0f | screen status | `%X` |
| a60a | tip/toast popup | tip code |
| a644 | now-playing / UI state update | song JSON (below) |
| a622 | SD rescan / DB-rebuild status | scan state |
| 06d0/06d1/06d2 | OTA progress / success / file-count | `%d` |
Verified-present log strings: GET_VERSION_REPLY @0x27255c, GET_WIFI_CONNECT_STATUS
@0x272658, UAC_SRATE_CHANGE @0x27252c.
**Now-playing JSON payload** (fields verified in `.str`): `song_name`, `song_artist_name`
(@0x271b90), `song_album_name`, `song_style_name`, `song_track`, `song_channel`,
`song_duration_time` (@0x271b34), `duration`, `songposition` (@0x271c2c),
`song_sample_rate`, `song_encoding_rate`, `song_bit_rate`, `song_mimetype`,
`song_file_path`, `is_sacd/is_cue/is_dsd`, `love` (favorite), `state` (play state),
`playerflag` (@0x271c08), `curlistlength`, `pos_id`, `playing_num`,
`work_mode` (@0x271b… 0x272b34), `battery` (@0x272c58). Album art → width/height/
quality/rgb → `/usr/data/fiio/cover.png`.
→ For diskOS status indicators (battery/BT/wifi) we read tags a704/a706 (wifi/net),
the BT a6c* family, and `battery` inside a644. **This is the data we were missing.**
### mq_ui feature surface (subsystems)
IPC core (`/ui` in; `/player`,`/bt_control` out) · frame dispatch (cJSON) · local
browse/play (all-songs/album/artist/style/favorite/custom, `db_song_ctrl.c`) ·
now-playing (`class_play_screen.c`, album art `jpg_to_png.c`) · Roon endpoint ·
home/menus (app/system/audio/others/popup `.json`) · EQ (`equalizer.json`) ·
Bluetooth (`bt.json`) · WiFi/NAS (`wpa_supplicant`, `hostapd`, `wl rssi`) ·
system/version/OTA (`ota_update.c`, `set_local_time.c`) · FiiO Link (UDP
224.0.0.255 discovery + TCP control, device id "SNOWSKY DISC") · bundled zlog.
---
## 5. Config + database schema
SQLite 3.23.1, opened via `sqlite3_open_v2`. DB files (paths verified in `.str`):
| File | Holds |
|------|-------|
| `/usr/data/fiio/db/sysconfig.db` | SYSCONFIG (settings, single row ID=1), CUSTOM_THEME, NAS_CONFIG |
| `/usr/data/fiio/db/song.db` | SONG, MY_LOVE, MEMORY_PLAY, PLAY_LIST, LIST_SONG_0/1/2, CUSTOM_PLAYLIST, PLAYLIST_INFO, PEQ |
| `/usr/data/fiio/db/dic.db` | HAN_PINYIN (CODE INTEGER, PINYIN char(1)) - CJK pinyin sort (on-disk verified) |
| `/usr/data/fiio/db/theme.db` | theme assets |
| `/usr/data/fiio/db/ebook.db` | e-book |
| `/usr/data/bluetooth.db` | bt_devices |
Only `dic.db` ships in the rootfs; the rest are created at first boot under
`/usr/data/fiio/db/`. db→file binding is from co-located source names + init block
(high confidence for SONG-family→song.db, SYSCONFIG/theme/NAS→sysconfig.db).
### SONG (song.db) - literal CREATE for MY_LOVE verified verbatim; SONG = same + IS_LOVE
ID(PK) · PATH · NAME · TITLE · ALBUM · ARTIST · GENRE · DISC · TRACK · IS_CUE ·
IS_ISO · IS_DSD · OFFSET(BIGINT) · DURATION(BIGINT) · NAME_CODE · TITLE_CODE ·
ALBUM_CODE · ARTIST_CODE · GENRE_CODE(pinyin sort keys) · ADD_TIME(INT8) ·
SAMPLE_RATE · BIT_PER_SAMPLE · CHANNELS · BIT_RATE · SONG_MIMETYPE ·
SONG_PRODUCTION_YEAR · IS_SELECT · ALBUM_ARTIST · ALBUM_ARTIST_CODE ·
**IS_LOVE** (SONG only, added by upgrade ALTER - favorites flag).
MY_LOVE = same minus IS_LOVE, `UNIQUE(PATH,TRACK)`. **(CREATE verified @mq_ui.str:9793)**
### CUSTOM_PLAYLIST (song.db) - literal CREATE verified @mq_ui.str:9790
MY_LOVE columns + `PLAYLIST_ID` (after ID), `FOREIGN KEY(PLAYLIST_ID) REFERENCES
PLAYLIST_INFO(ID) ON DELETE CASCADE`, `UNIQUE(PLAYLIST_ID,PATH,TRACK)`.
### PLAYLIST_INFO (song.db) - parent of CUSTOM_PLAYLIST. ID(PK) firm; name/count inferred (LOW confidence).
### PLAY_LIST (song.db) - queue registry: ID(PK) · LIST_ID · LIST_NAME.
### LIST_SONG_0/1/2 (song.db) - active play queues, built by `INSERT INTO LIST_SONG_%d … SELECT … FROM SONG` (verified). Cols: ID · LIST_ID · POS_ID(=MUSIC_ID join key) · PATH · NAME · TITLE · ALBUM · ARTIST · GENRE · DISC · TRACK · IS_CUE · IS_ISO · OFFSET · DURATION · ADD_TIME · IS_SELECT · SONG_TYPE · ALBUM_ARTIST.
### MEMORY_PLAY (song.db) - resume state, single row. ID(=1) · MUSIC_ID · IS_PLAYING · POSITION · IS_CUE · IS_ISO · TRACK. **(UPDATE verified @mq_player.str:13412)**
### PEQ (song.db) - see §3. ID · STYLE_NAME · MASTER_GAIN(REAL) · PARAMS_JSON · STYLE_PRESET.
### CUSTOM_THEME (sysconfig.db) - ID · POS_ID · NAME · PATH · ALIAS · TYPE · IS_SYSTEM · ATTR · ALPHA · LOCK_TIME/DATE/BATTERY/ID3/MEM_TYPE · FRONT_COLOR.
### NAS_CONFIG (sysconfig.db) - ID · NAS_NAME · NAS_IP · USER · (password) · TYPE · AUTO_LOGIN.
### bt_devices (bluetooth.db) - ID · NAME · ADDR(MAC key) · CODEC · SAMPLING · VOLUME.
### SYSCONFIG (sysconfig.db) - single row, all INT, `UPDATE SYSCONFIG set %s = %d where ID = 1` (verified @0x25a910)
Column names verified from descriptor @mq_player.str:13091+. Runtime pak dump
(verified @0x25e7a4): `DSD_MODE,OUT_DEV,VOLUME,WORK_MODE,LIGHT_LEVEL,LIGTH_ON_TIME
(sic),RGB_LEVEL,MUTE,VOL_MODE,RGB_STATUS,MEM_PLAY`. Full key set (names verified;
**enum value mappings NOT proven** - inferred from name + cross-ref to command tags):
DSD_MODE · OUT_DEV/DEVICE_OUTPUT(→0666) · VOLUME · WORK_MODE · LIGHT_LEVEL ·
LIGTH_ON_TIME(backlight timeout, sic) · RGB_LEVEL · MUTE · VOL_MODE · RGB_STATUS ·
RGB_COLOUR · TRIGGER_IN · SYS_THEME · LANGUAGE · USB_MODE · NETWORK_MODE ·
EQ_TYPE(→0689, =PEQ.STYLE_PRESET) · MAX_VOL · BALANCE_VOL · POWER_SAVE ·
FILTER_TYPE(DAC digital filter) · PLAY_MODE(→0102) · MEMORY_PLAY(resume) ·
FOLDER_JUMP · PLAY_GAP(gapless) · INPUT_MODE · VOL_KNOB_MODE · OTA_CFG · BATTERY ·
BT_CODEC · SCREEN_ROT · PO_PRE_VOL/PO_VOL/PRE_VOL · THEME_MODE · CHARGE_PROTECT ·
LOCK_THEME · TREBLE · BASS · WIFI_STATUS · BT_STATUS · LO_DISABLE · OS_MODE ·
DSD_DECODE · SPDIF · AUTO_TIME · DRE_STATUS · LOCAL_IMG_ANIM · IMG_ANIM_BRIGHTNESS ·
ARM_VERSION · MCU_OTA_FAILED_TIME · KEY_SINGLE/DOUBLE/LONG_CLICK_SLE(gesture→action) ·
ARTIST_CLASS_TYPE · AUDIO_VOLUME_SET.
Config is committed to flash on write (`now try to save config to flash` @0x25a…).
---
## 7. Network streaming + USB modes + COMPLETENESS
**Major gap found:** the earlier sections missed the network-streaming receivers and several subsystems. The device is far more capable than §1-6 implied.
### Work-mode / OUT_DEV enum (the master "audio source" list) - names VERIFIED
mq_player has ONE work-mode enum (pointer-array @ ~0x7bb0a0 data; names in `.str` @0x25f9e0+). Switching mode is **name-driven**: a supervisor `@0x444604` does `strcmp(name,"AIRPLAY"/"DLNA"/"ROON"/...)` and starts/stops that receiver. **Names verified present; integer indices unverified:**
`0 NO_DEFINED · 1 I2S3_OUT · 2 USB_HOST_NULL · 3 NO_WORK_MODE · 4 LOCALPLAYER · 5 BTSINK · 6 ANALOG · 7 UAC(USB-DAC) · 8 DLNA · 9 AIRPLAY · 10 ROON · 11 SPDIF_OPT · 12 SPDIF_RX · 13 DMR · 14 DMC · 15 DMS · 16 MIX · 17 I2S_IN · 18 STREAM_AUDIO` (+ QPLAY). sysconfig keys NETWORK_MODE (cfg+0x5c) and OUT_DEV both feed this.
### AirPlay receiver - BUILT IN (was completely missed)
mq_player embeds a **full shairport-sync fork**: `fiio_airplay.c` + `src/shairport.c, rtsp.c, rtp.c, dacp.c, metadata.c, mdns_avahi.c, player.c` + `libshairplay.so`. Advertises **`_raop._tcp`/`_airplay._tcp`** as **"SNOWSKY DISC"**. Config: `/usr/project/config/airplay2/x2000_airplay2.conf` (Shairport-Sync style, AirPlay 2). Flow: supervisor sees mode "AIRPLAY" → sets `airplay_play=1` (@0x7efbe4) → `start_airplay@0x436008` → pthread `airplay_func@0x435fc0` → shairport loop. Needs **wlan0 up** (`start_switch_network_mode_thread@0x46e634` monitors `/sys/class/net/wlan0/operstate`=="up", then emits `/ui` tag **a706** = GET_NET_STATUS_REPLY - a706 is STATUS, NOT the trigger). Has `airplay_artwork_thread`, DACP remote. Audio → ALSA `snd_pcm_writei` to the DAC after out_dev switch. **TRIGGER COMMAND TAG: NOT yet pinned** (a `06xx` work-mode-switch carrying AIRPLAY=9, sent via `ui_send_cmd@0x43f82c` → mqueue `/player` idx 3, frame `TAG(4)+ext1(4hex)+len(4hex)+data`). Player also has a name-string command dispatch @0x482328 (e.g. `SET_USB_MODE`). **Next step to enable AirPlay from diskOS = read the mq_ui output/work-mode menu handler (callers of 0x43f82c) to capture the literal tag+payload for AIRPLAY/UAC/DLNA/ROON.**
### Also built-in network receivers (missed):
- **DLNA/UPnP renderer** - `dlna_player.c`, UPnP RenderingControl/AVTransport SCPD. (mode DLNA=8)
- **Roon Ready endpoint** - `roon_player.c` + `libroon.so` RAAT (`roon_transport_*`, volume/seek/shuffle). (mode ROON=10) UI screen `ui_roon_play.c`.
- **QPlay** (Tencent/QQ Music) receiver - `mq_player_server_qplay.c`, MD5(Seed+PSK) auth, SetNetwork/SetLyric.
- **BT SINK** (phone→player A2DP in) - `bt_sink_server.c/bt_sink_control.c` + AVRCP. (mode BTSINK=5)
### USB modes (partially missed)
USB_MODE sysconfig + `usb_mode_change_handler@0x48eb80` (named cmd `SET_USB_MODE`@dispatch 0x482328). Gadgets via configfs: **`uac_demo`** (USB-DAC, functions uac1.a/uac2.a, /dev/usb_dac, UAC_SRATE→/ui tag aa1b) = work-mode UAC(7); **`storage_demo`** (card-reader, mass_storage.0, lun.0/file=/dev/mmcblk0). `set_usb_host`@0x24da24. **USB_MODE integer values (charge/DAC/reader/host) NOT yet mapped.**
### CORRECTIONS to earlier sections
- **NAS is FULLY IMPLEMENTED** (`nas_control.c`: `mount -t cifs vers=3.0`, NFS, `smbclient -L`; NAS_CONFIG live) - §1's "NAS/qplay = NULL/unimplemented" was WRONG.
- **Tag counts:** ~**221** distinct `/player` `0xxx` command tags + ~**102** `axxx` reply tags (mq_ui emits ~161 cmds, handles ~102 replies). §1/§4 documented ~10 + ~30 → **~95% of the command surface is still undocumented** (entire a4xx reply family untouched).
- Other missed subsystems: **lyrics** (`lrc_paser.c`, `/usr/data/fiio/encoder.lrc`!), generic stream/I2S_IN/capture (`stream_audio.c`,`player_capture.c`), animated gif screensaver (LOCAL_IMG_ANIM), serial-number (`sn_nb.c`), image loaders (bmp/jpeg/png/yabmp), SACD/DST internals, MCU OTA (`ENTER_MCU_UPDATE_MODE`), full SPI/MCU handler family.
## 8. FULL command map - inventory complete, meanings mixed-confidence
**V2.09 dispatch tables** (entry = `{tag_str_ptr, handler_thunk_ptr}` 8B; thunks tail-jump via GOT to real handler):
- **Table A @0x7c9d30 = 131 entries** (terminator @0x7ca148). Tags 06xx/07xx/08xx/0a51.
- **Table B @0x7ca150 = 75 entries** (terminator @0x7ca3a8). Tags 00xx/01xx/02xx/04xx/05xx. ~36 in-table NULL (declared-unimplemented), ~11 thunk-but-GOT-null, ~28 live.
- **MCU/SPI name-commands** via hw_ctrl.c @0x48d0b8 + sysconfig-key dispatch @0x488000 (NOT 0x482328 = that's the tag↔enum resolver).
**⚠️ RELIABILITY:** tag list = reliable (mechanical table walk). Per-tag MEANINGS: ~36 string-VERIFIED, ~80 INFERRED-from-family, ~15 UNKNOWN(runtime-registered GOT=0). **Some conflict with the 1.95-era LIVE-tested set** (1.95 tables were @0x7b7870/0x7b7c90; V2.09 layout differs). **RESOLVED:** the `0657`/`0666`/`06b3` "close_player" reading was a false positive - all three call a shared teardown preamble at the start of a disruptive mode change; that teardown is NOT their meaning. `0657`=source/work-mode transition, `0666`=output route (2/4/6), `06b3`=BT codec select. **Treat remaining INFERRED meanings as needing verification; trust live tests + resolved-handler traces over family inference.**
**String-VERIFIED V2.09 commands (Table A):** 0802=song count, 0620=get WIFI_MAC(/usr/data/fiio/nb.txt), 0704=wifi scan list, 0705=wifi connect(psid/passwd), 0800=write wpa_supplicant.conf, 0701=wifi init, 0700=close network card, 0720/0722=OTA(wget ota_patch), 0639/0689/0690/0634/0641=media-info query→a639 reply, 0675/0630=EQ/PEQ get(gain/filterType/frequency), **0678=set PEQ band(filterType/frequency/gain/qValue)**, 0621=DROP DB tables, 0624=mount/storage, 0815=set MEMORY_PLAY position, 0820/0821/0822=key single/double/long action, 0647=gapless, 0648=artist_class_type, **06b1=BT sample-rate param(0x40d4d8), 06b2=BT bit-depth param(0x40d66c), 06b3=BT CODEC SELECT(0=SBC 1=AAC 2/3/4=LDAC; 0x40eaf4; SBC frame `06b3000C0000` pins 44100), 06b4=LDAC QUALITY only(0x40dbe8; mob/std/high)**, 06b7=BT paired, 06c0=BT trust/power, 06c1=BT alias, 06c4=BT disconnect/remove. (0657/0666/06b3 close_player = RESOLVED false positive, see above.)
**Table B verified:** **0100=main PLAY** (jumptable @0x650e2c by list_type), 0201=transport play/pause toggle (generic, →0x419ff4; NOT Roon-specific), 0112=playlist add song, 0115=add to custom list, 0113=love-list delete, 0114=custom-list delete, 0117=playlist reorder(src/dst), 0408=file/folder browse, 0413=album query, 0414=style/genre query, 02xx=NAS scan/browse(mq_player_server_nas.c, many GOT-null/unimplemented).
**MCU/SPI name-commands (hw_ctrl.c, SPI to MCU):** GET/SET_DEVICE_MAX_VOL, SET_DEVICE_VOL, GET/SET_INPUT_MODE, SET_OUTPUT_MODE, SET_GAIN, GET/SET_DAC_FILTER, **SET_USB_MODE**, GET/SET_EQ_PRE, GET/SET_EQ_PARAMETER, SET_EQ_RESET, SAVE_EQ, SET_AUDIO_FORMAT, SET_FACTORY, **ENTER_MCU_UPDATE_MODE**, SET_MCU_POWER, SET_MUTE, SET_POWER_DOWN_TO_MCU (shutdown path, 63 call sites), GET/SET_ZERO_DATA_DETECT_TIME, BT_REPORT_RATE/STATE/CODEC_TO_MCU. sysconfig-key setters: RGB_COLOUR/TRIGGER_IN/SYS_THEME/LANGUAGE/USB_MODE/NETWORK_MODE/EQ_TYPE/MAX_VOL/BALANCE_VOL/POWER_SAVE/FILTER_TYPE/PLAY_MODE/FOLDER_JUMP/PLAY_GAP/INPUT_MODE/VOL_KNOB_MODE/OTA_CFG/PO_PRE_VOL/PO_VOL/PRE_VOL/TREBLE/BASS/LO_DISABLE/OS_MODE/DSD_DECODE.
## 6. TODO (next passes)
- [x] ~~Live-verify 0657/0666~~ RESOLVED (handler-trace): 0657=source/work-mode transition, 0666=output route(2/4/6). "close_player" was a shared teardown preamble.
- [ ] **Pin the AirPlay trigger tag** (mq_ui work-mode menu → callers of ui_send_cmd@0x43f82c) - the one fact needed to enable AirPlay from diskOS.
- [x] **0622 is NOT a working rescan on V2.09:** diskOS "Rescan Library" sends 0622 → runs a long scan
(~13min, scan_songs_thread) but writes 0 rows to song.db (mtime unchanged). Clearing SONG + reboot also scanned but
wrote nothing. So song.db must be hand-built (laptop `tools/build_db.py` from sync_manifest.json → SONG +
PLAYLIST_INFO + CUSTOM_PLAYLIST; diskOS lib sorts by TEXT so *_CODE only affects play-queue order). The real scan
trigger (comm_scan_songs_thread) is still un-pinned. song.db schema is in this catalogue's table notes / dump from
song.db.bak. Stock leaves DURATION/SAMPLE_RATE/BIT_RATE/CHANNELS=0 and ALBUM=TITLE for tagless rips - match that.
- [x] **USB_MODE card-reader mode = VALUE 2 - LIVE-VERIFIED WORKING (transferred 32GB this way).** This is the clean
stock MSC path for file transfer, and it SIDESTEPS the dangerous 0666 entirely. Details:
- `usb_mode_change_handler` @ **0x48eb??** (log str "usb_mode_change_handler = %d" @vaddr 0x670358, ref'd
by `addiu v1,v1,856` @0x48eb84). Reads/writes current usb_mode global @ **0x822d20**. Branches on the
target mode value (s0): observed cases 2, 5, 6.
- On **usb_mode==2** (`bne s0,2` fall-through @0x48ebc4) it accesses
**`/sys/kernel/config/usb_gadget/storage_demo`** (lui 0x66 + 13620 = vaddr 0x663534) → builds a
`mass_storage.0` function, **lun.0/file = `/dev/mmcblk0`** (whole SD), toggles cdrom/nofua/removable/ro,
binds the UDC. (All configfs paths string-verified @ file 0x263534-0x263d8f.)
- **CLEAN handoff CONFIRMED:** before/while exporting it UNMOUNTS the SD - `unmount_udisk` @0x668ed4 (called
~7× around 0x470134-0x470388) runs `umount %s` with 5 retries + `umount -lf %s 2>/dev/null` lazy-force
fallback on `/tmp/sdcard` (strs @0x268be8/0x266e40/0x268e40). So it properly releases mq_player's SD hold
(the EBUSY blocker) - no corruption, unlike a raw composite-gadget export.
- Trigger: **`SET_USB_MODE`** name-command (str @vaddr 0x66fe40; dispatch refs @0x48232c/0x4863ec/0x48d154;
builders @0x408044/0x40fa94) and/or the `USB_MODE` sysconfig field. EXACT payload/value-encoding for
SET_USB_MODE still to pin (one more trace), but the value is **2**.
- ⚠ CAVEAT (untested): a UDC binds ONE gadget at a time, so binding `storage_demo` almost certainly UNBINDS
our `serial_demo` ACM → **the serial shell will drop while in reader mode**; exiting reader mode (mode
switch / replug) should restore it. TEST LIVE WITH USER PRESENT (power-cycle fallback). This is still far
safer than 0666 (no player crash / MCU-reboot path).
- ⇒ This unblocks **music-transfer-over-USB** via the stock mechanism: set USB_MODE=2 → laptop sees the SD as
a USB drive → copy 32GB at USB speed + fsck → exit reader mode → in-device rescan. Replaces the unsafe
wifi-push (watchdog reboots) and the blocked raw-MSC attempt.
- [ ] Transcribe the full 221-tag command table + 102 a-frame replies.
- [ ] Transcribe the full 207-tag command table (A+B) with handler addresses.
- [ ] Verify audio/decoder/BT/DSD claims (§2) at instruction level (currently unverified).
- [ ] Resolve SYSCONFIG enum value→option integer mappings.
- [ ] Pin PLAYLIST_INFO's full column list (currently only ID firm).
- [ ] diskOS wiring: status indicators from a644/a704/a706/a6c* + custom-EQ via PEQ write + 0689.
## 5b. SYSCONFIG - the master settings table (decoded 2026-06-30)
`/usr/data/fiio/db/sysconfig.db` → table **SYSCONFIG** (single row), one INT column per setting.
This is where every audio/system setting persists (the config-named commands write here).
Columns incl: DSD_MODE, OUT_DEV, VOLUME, WORK_MODE, LIGHT_LEVEL(brightness), LIGTH_ON_TIME,
MUTE, VOL_MODE, USB_MODE, NETWORK_MODE, EQ_TYPE(=0689 preset), MAX_VOL, BALANCE_VOL(channel
balance), POWER_SAVE, FILTER_TYPE(DAC filter), PLAY_MODE(=0102), MEMORY_PLAY, FOLDER_JUMP,
PLAY_GAP, INPUT_MODE, VOL_KNOB_MODE, BT_CODEC, SCREEN_ROT, PO_PRE_VOL/PO_VOL/PRE_VOL,
THEME_MODE, CHARGE_PROTECT, LOCK_THEME, TREBLE, BASS, WIFI_STATUS, BT_STATUS, LO_DISABLE,
OS_MODE, DSD_DECODE, SPDIF, AUTO_TIME, DRE_STATUS, DEVICE_OUTPUT, KEY_SINGLE/DOUBLE/LONG_CLICK_SLE,
ARTIST_CLASS_TYPE, AUDIO_VOLUME_SET.
Live sample (V2.09): FILTER_TYPE=1, DRE_STATUS=1, BALANCE_VOL=0, SPDIF=0, MAX_VOL=120,
BT_CODEC=3, MEMORY_PLAY=0, FOLDER_JUMP=0, PLAY_GAP=0, SCREEN_ROT=0, CHARGE_PROTECT=0,
ARTIST_CLASS_TYPE=0, TREBLE=0, BASS=0, OUT_DEV=6, OS_MODE=0, LO_DISABLE=0, DSD_MODE=1,
INPUT_MODE=1, VOL_MODE=1. (No explicit GAIN column - likely VOL_MODE or encoded in OUT_DEV;
confirm by toggling.) Filter enum (others.json 70-75): 0=FAST_LL,1=SLOW_LL,2=SLOW_PC,3=FAST_PC,
4=NON_OS,5=Wideband_FF. To wire a setting in diskOS: write its SYSCONFIG column + send its apply
command (verified: gapless 0647, BT codec 06b3 [0=SBC…4=LDAC-high], artist 0648, memory 0815, keys 0820-22; Gain/
Filter/DRE/Balance apply-cmds TBD via strace-correlate of stock mq_ui - strace on mq_ui is SAFE,
only strace-on-mq_player triggers the MCU reboot).
## 5c. Audio/DAC apply-commands - DECODED LIVE (2026-06-30, strace of stock mq_ui)
Captured by strace `mq_timedsend` on stock mq_ui while toggling each setting (SAFE - strace
on mq_ui, never mq_player). Frame format `<tag>000C<value 4hex>`. Note: SYSCONFIG column
writes are DEFERRED (didn't update live), so these were correlated by the FRAME, not the DB.
Background-noise tags to ignore: 0807 (recurring 0/1 status), 0704 (wifi scan).
- **DRE** = `0812` - `0812000C0000` = ON, `0812000C0001` = OFF (startup sent 0 when DRE on).
- **Gain** = `0645` - `0645000C0000`/`0001` (Low/High; startup sent 0).
- **Channel balance** = `0713` - `0713000C01<level>`; center = `0101`, nudging streamed 0105..010C
(one frame per step; 0x01 hi-byte = channel/side, lo-byte = level).
- **DAC Filter** = `0653` - enum = menu order (clean sweep): 0=FAST_LL, 1=SLOW_LL, 2=SLOW_PC, 3=FAST_PC, 4=NON_OS, 5=Wideband_FF.
- **SPDIF** = via output-route `0666` - SPDIF on = `0666000C0004`, analog = `0666000C0006`
(0666 = OUT_DEV/output route: LOCAL_ANALOG=6, SPDIF=4; mutually exclusive with headphone).
- **BT codec** = `06b3` (**CORRECTED**: mq_ui codec menu 0x464e90 sends 06b3, not 06b4):
codec-settings menu sends payload-less `06b3000C0000`=SBC `…0001`=AAC `…0002/3/4`=LDAC mob/std/high.
**But the ROUTE-to-speaker frame (live-captured 2026-08-03) is `06b3<len>000X<MAC>`** (X=codec, MAC payload
kept for stock frame-shape; worker ignores it). diskOS route uses `06b3001D0000<MAC>` (SBC). `06b4` is
LDAC-quality only. **Full working BT-transmit sequence → COMMAND_MAP.md "BT AUDIO OUTPUT" section.**
- Also re-confirmed: `0666`=output route, `0642`=network mode (from startup sync).
## 5d. More settings - DECODED LIVE 2026-06-30 (strace stock mq_ui, clean batch)
- **Channel balance** = `0713` - `0713000C<HHLL>`: center=`0000`; one side `00NN` (NN=step), other side `01NN`. (Same tag the audio-cluster capture saw.) Mixer-style, likely safe.
- **Gapless** = `0647` - `0647000C0001` on / `0000` off. (matches COMMAND_MAP 0647=set gapless)
- **Artist list grouping** = `0648` - `0648000C0000` Artist / `0001` Album-Artist. (ARTIST_CLASS_TYPE)
- **Memory playback (resume)** = `0684` - `0684000C00<0|1|2>` = Off / Position / Song. (0684 was "media getter" in COMMAND_MAP - now confirmed the MEMORY_PLAY setter)
- **Max volume** = `0711` - `0711000C00<NN>` where NN(hex)=cap, 0..0x78(120).
NOTE: only SPDIF (0666 route) wedges on a raw send; these are config/mixer commands, expected safe - but per the wedge lesson, apply on live user change only, don't blind-send at boot.
## 5e. Sibilance EQ presets wired (2026-07-01, code-only, staged not-yet-deployed)
Stock has 11 EQ presets (equalizer.json 0-10); diskOS had 9. Added preset 9="Sibilance 1",
10="Sibilance 2" to OPT_EQ (settings.c) + T_EQ (npmenus.c), nopts 9->11, clamps q>8->q>10.
Uses the existing 0689 path (ui_apply_eq, clamp already 0..20) so 0689000C0009/000A select them.