13 KiB
13 KiB
Lofi-12 XT — Custom Firmware Tweakability Report
Based on: Lofi-12 XT.bin v1.1.156 (1,595,605 B), v1.2.179 (1,606,197 B),
v1.5.205 (1,707,049 B); structural notes in */reversed.md; cross-version
diff in rev-diff.md. All three images parse cleanly with the same tooling.
Date: 2026-09-26, updated 2026-09-30. Hardware dumped since (IC300/IC301 via CH341a); checksum solved; Ghidra lab built. Still static-only — no modded image has been boot-tested on-device yet.
TL;DR
| Question | Answer |
|---|---|
| Feasible to build a modded firmware? | Yes, plausibly — but only up to a ceiling. Container is fully parsed, code is identified (TI C674x DSP, unencrypted/unpacked), data sect is rich with symbols. Three versions give us a feature-diff oracle. |
| #1 blocker | Solved 2026-09-30: cmtd+0x08 = CRC32-IEEE init 0 over the image with bytes [8:12] replaced by 0xC27C6282 (proven 3/3; tools/ stamps it automatically). |
| Risk of bricking? | Low if careful. The .bin is a DDR snapshot loaded by the separate SPI-flash bootloader (MX25L12833F), and the updater lives in that bootloader - a bad image aborts SYSTEM UPDATE without killing recovery. Confirmed recovery: hold PAD while powering on with a stock SD. |
| How far can we go? | Text/UI swaps, asset swaps, constant tuning, small code patches: realistic. New DSP effects, new file formats, USB/stack changes: out of reach without a major RE campaign. |
| Recommended first mod | Same-length UI string swap on v1.5.205 (Threshold -> ThresholX), forged image already built and self-verified (462F735B) - awaiting the first on-device boot-test. |
1. What we actually have (and why it matters)
- Complete container format.
[cmtd 48B][mmtd 36B][sect ×6–7 → EOF, exact fit]verified on all three builds (chain mathnext_off = off + 12 + lentiles EOF exactly). Repacking is therefore a byte-shuffling problem, not a guessing problem. Section roles are stable across releases: asset blob (~85 kB) → float tables → 512 B record table → big code sect (1.39→1.49 MB) → 8 B zero slot (comes and goes) → strings/rodata (115→125 kB) → float ramp tail. - Code CPU identified: TI C674x DSP (C6000 family), little-endian. All three entry points disassemble under C64x-LE to the same reset prelude (
ZERO b0; mvc b0,ier; mvc b0,csr; …— interrupt disable + stack align); only stack immediates differ. ARM/Thumb disassembly of the same bytes yields ~nothing. The.binload addresses are all0xC2xxxxxx= DDR2 (SoC is TMS320C6748: ARM9 + C674x, 1 Gbit DDR2 at0xC0000000). So: DSP does the application; ARM9 is presumably bootloader-only in SPI flash. - No packing, no encryption, no obfuscation. Big-sect entropy is constant 6.887–6.890 with ~12.6% zeros across all builds; code/data mix with a low-entropy zero-padded tail. Nothing suggests compression or crypto was introduced between v1.1 and v1.5.
- Symbol-rich rodata. The strings sect contains full C++ RTTI/mangled names (libc++
NSt3__, i.e. TI clang-based CGT toolchain):FileUtil::…,App*Menu,N3HAL*Driver,N8SoundOSC…, plus UI strings, RIFF/WAVE/MIDI tags, project chunk tags (PJST/PTDT/SONG/…), and a full C674x crash dumper (Legacy NMI Exception,A0–A31/B0–B31,NTSR/ITSR/…). Code↔data cross-refs grow monotonically (5,854→5,909→6,362 code-ptrs; 2,932→2,967→3,141 self-ptrs) — i.e. vtables/function tables live in the strings sect and can anchor disassembly. - Three-point version oracle. v1.1→v1.2 is a +10 kB small delta (only user-visible string adds:
TRACK MUTE,AppPad::handleKeyPadOnSliceMode); v1.2→v1.5 is a +91 kB big delta (audio export family, MIDI Note Map, 4 new master FXMIsolator/MRackComp/SlipRoll/HoldDelay,REVERSE, precount rework withEibiEUliEoverloads,FileUtil::removeResourceFork+Rbparams,Fv→Fvi/Fvii/Fviiicallback migration, display-pipeline symbol turnover). This tells us exactly which subsystems are self-contained enough to backport/forward-port.
2. Risk assessment
| Risk | Level | Notes |
|---|---|---|
| Permanent brick (bootloader kill) | Low | Bootloader lives in SPI flash (MX25L12833F, dumped + mapped — see docs/bootloader.md), update .bin is a DDR image parsed by it. A corrupt .bin aborts SYSTEM UPDATE; recovery is the PAD-held boot with stock SD. |
| Soft-brick / failed boot loop | Medium | Wrong checksum, bad entry point, or misaligned sect will likely hang or drop back to updater. Mitigation: keep a known-good SD card with stock v1.5.205, document the hold-PAD-while-powering-on update flow, never ship without a revert image. |
| Silent data corruption (projects/SD) | Medium | FileUtil rework in v1.5 touches recursive FS ops + resource forks. Patches near file I/O deserve extra caution and SD-card backups. |
| Checksum | Solved, no longer a risk | cmtd+0x08 reproduced on all 3 builds; mmtd+0x38 flags need no handling (covered as-is). |
| Legal / warranty | Medium | Distributing full modded images contains Sonicware IP. Prefer distributing patches (xdelta/bps + script) against user-supplied stock .bin. Warranty implications unknown. |
| No debugger mapped | Medium | UART1_TX/RX + SPI + SD test points are named on the silkscreen (see docs/hardware.md), continuity untraced. Crash dumper strings suggest NMI output exists — worth hunting on-board before any code patch. |
Bottom line on risk: string/asset-only mods with a solved checksum are low-risk. Any code-segment edit without a debugger or recovery plan is medium-risk. Touching anything outside the .bin (SPI flash, AT32F421 USB MCU) is high-risk and out of scope.
3. Tweakability ladder (easiest → hardest)
Level 0 — Trivial (days, one person, no DSP expertise)
- Same-length UI string swaps. The strings sect is NUL-separated with padding (e.g.
Threshold\x00\x00\x00\x00ENCODER TEST). Any replacement of equal byte-length (pad with spaces/NULs) keeps every address stable — no pointer fixups, no checksum-of-lengths to worry about (only the one global checksum). Ideal for renaming menu items, translating UI, joke builds, ownership marks. - Asset/blob swaps. Sect #0 (~85 kB,
7e xxpatterned bitmap/font/waveform data) can be recolored or redrawn in place as long as length is preserved. Fonts/icons are the likely content. - Version-string spoofing.
cmtd/mmtdversion triples are plain LE u32s — trivial to bump for fork identification (checksum auto-recomputed, flags preserved as-is).
Level 1 — Easy (slack-space + constants, basic C6000 asm)
- Longer/shorter strings via slack. Top zero-runs in the v1.5 strings sect include 388, 256, 223, and many 196-byte gaps — enough to relocate a handful of lengthened strings and patch their pointers. Requires finding xrefs (the 6,362 code-ptrs give a starting map) but no code-cave engineering.
- Numeric constant tuning. Float tables (sects #1/#6) and the float ramp tail are plain LE float32 — filter/window coeffs, tempo/level defaults, threshold values. Tunable by hex-edit + listen. Same for the 512 B record table (preset/pattern-shaped fixed records).
- Behavior flags / timeouts / limits. Precount on/off defaults, LED timings, "1,024 files per folder" caps,
ARE YOU SURE?confirm gates are typically single immediates or branches — findable via string xref → code.
Level 2 — Medium (real code patches, needs Ghidra + C6000 skill)
- Feature NOPs / unlocks. Skipping a confirm dialog, forcing precount off, enabling hidden menus: overwrite a branch with NOP or invert a compare. Needs rebase-aware disassembly (load code sect at its DDR base, e.g.
C2B80C00for v1.5) and vtable-anchored function boundaries. - Branch-to-cave mini-features. The 8 B zero slot is not usable code space (too small, and it vanishes in v1.2 — it's alignment churn), but the low-entropy zero-padded tail of the code sect (
+0x140000…, entropy ~3.0) offers code-cave room for small injected routines (extra MIDI mapping, custom pad behavior) reached by patching a call site. - Backports between versions. Because v1.1→v1.2 is tiny, diffing those two isolates e.g.
TRACK MUTE/ slice-mode handling almost cleanly — the most realistic "port feature X to version Y" target. v1.2→v1.5 features are larger families and harder to lift.
Level 3 — Hard (weeks–months, DSP + toolchain mastery)
- New/changed DSP behavior (custom filter, altered timestretch/slice engine, new LFO shape). Requires understanding TI CGT calling conventions,
addkpc-relative addressing, fixed-vs-float pipelines — plus listening tests per iteration since there is no emulator. - Display-pipeline changes. v1.5 turned over all
RenderBufferIhLi128ELi128ELi1327Esymbols; the GUI stack is evidently custom and version-fragile. - FileUtil / project-format changes.
Rbparams,removeResourceFork,Fv→Fvicallback migration mean v1.5's FS layer differs structurally from v1.1/v1.2 — grafting across that boundary is genuinely hard.
Level 4 — Out of reach (even with current assets)
- Custom bootloader / SPI-flash layout changes. Dumps exist and the AIS
image is mapped (
docs/bootloader.md), but reflashing a bad bootloader can only be recovered via external programmer — out of scope for SD-only modding. - USB stack (AT32F421 MCU). Separate chip, separate firmware, not in scope of these images.
- New codecs / new file formats / USB audio / major new FX algorithms from scratch. No source, no SDK, no DSP build chain verified; would amount to writing C674x DSP code blind.
4. What we can tweak vs. what is beyond reach (concrete list)
Realistic mods:
- Rename/translate/reword any menu, dialog, error string (
AUDIO EXPORT,ARE YOU SURE?,MIXDOWN FILE NAME:, …). - Redraw/replace bitmap assets (fonts, icons, waveform glyphs) in sect #0.
- Retune float constants (filter coeffs, ramp, default tempo/thresholds) and fixed-record presets.
- NOP confirm dialogs, change defaults (precount, mute behavior), remap pad/MIDI-note handling, adjust LED update logic (
StepEditPageController::updateAppLEDexists as a named target). - Backport small v1.2 behaviors (track/pattern mute, slice-mode pad handling) across versions; cherry-pick single v1.5 strings/behaviors that are data-driven.
Beyond reach (without new inputs):
- New master FX on par with
MIsolator/MRackComp/SlipRoll/HoldDelay(these were ~90 kB of new code — a team-sized effort to replicate blind). - Audio export itself as a backport to v1.1/v1.2 (whole class family:
AppAudioExport*,DialogAudioExporting,MixDown~). - Reliable larger-than-slack additions (no dynamic allocator mapped; memory map beyond the tiled
C2B809E8…C2DBC698range has gaps marked only as descriptors/BSS). - USB behavior, SD-driver changes (
N3HAL*Driverfamily is named but unmapped), sample-rate/format support changes.
5. Toolchain & skills required
- Repacker: done —
tools/lofi_image.pyparses/packs the exact-fit chain and stamps the valid checksum (compute_checksum, proven 3/3). - Disassembly: working lab — Ghidra 12.1.3 + ghidra-c6000 + PyGhidra drivers
(
ghidra/headless/), proven on the bootloader CRC routine;analysis/gen_funcmap.pyfor vtable-anchored maps; capstoneCS_ARCH_TMS320C64Xfor quick preludes; rebase-aware diffing via function hashes (raw byte-diff is defeated by rebasingC2589800→C258D800→C2B80C00). - Patching: C674x assembly literacy, pointer/xref hunting from the strings sect, xdelta/bps distribution to avoid shipping Sonicware binaries.
- Hardware (before any code mod boots): revert flow is hold-PAD-while-powering-on
with stock SD; PCB photographed (
docs/photos/); UART1_TX/RX test points named on silkscreen (continuity untraced); SPI dumps banked locally; SD-card backups.
6. Suggested plan (lowest-risk order)
Crack— done (CRC32 +cmtd+0x080xC27C6282seed;tools/stamps it).- Prove the loop with a Level-0 mod. Same-length string swap on v1.5.205 → boot → revert to stock. If this fails, stop: no higher level is viable.
- Map xrefs for one Level-1 target (e.g. a confirm dialog or precount default) using string→code pointers; patch, test, revert.
- Only then attempt Ghidra full-disassembly + v1.1↔v1.2 micro-diff for a first Level-2 backport.
- Do not touch SPI flash, USB MCU, or FS-write paths until UART/crash logs are captured and a revert is drill-tested.
7. Verdict
- Feasibility: moderate-to-good for small mods, poor for big features. The format is open, packing is solved, the code is identified and unprotected, and three versions triangulate features well. The project no longer lives or dies on the checksum — it lives or dies on the first on-device boot test.
- Risk: contained if disciplined (DDR-image-only, stock revert on hand, string-first progression), and uncontained if the bootloader or USB MCU is touched.
- Ceiling with current assets: customized UI/assets, tuned constants, disabled annoyances, small behavior patches, possibly one backported mini-feature. New DSP engines, new I/O, and custom bootloaders remain out of reach.