talking-f3

How this was done: making a closed device talk

This is the method behind the talking F3, written for anyone who wants to do the same for another recorder or gadget with a screen and a headphone output. HOW_TO_DECOMPILE.md covers the first step (getting readable code out of the firmware file). TECHNICAL.md describes the result. This file is the path in between: the order things were done in, the techniques that worked, and the traps.

It assumes some reverse-engineering background (reading Arm disassembly or Ghidra output). It was done by a blind developer, with an AI assistant doing most of the code reading. Every finding was confirmed on the real device by ear.


0. Before anything: a guaranteed way back

Do not change a single byte until you know how to undo any change. For the F3:

If a device has no safe recovery path like this, stop and find one (a debug port, a bootloader mode) before going further.

1. Read the firmware

Unpack and decompile (HOW_TO_DECOMPILE.md). The two things that made the F3 readable were undoing armlink’s compressed data (otherwise every table looks like random bytes) and loading each program at its real run-time address. Expect to spend real time here; nothing else works until the disassembly makes sense.

Then get your bearings:

2. Find room

The patch needs space for code, RAM and voice data that the firmware never uses:

3. One hook in the audio path

Speech has to be mixed into what the headphones receive. Find the audio output task: the DMA buffers feeding the audio interface (SAI on this chip), and the loop that fills them. Then choose one instruction to replace with a branch to your code:

The trampoline runs the instruction it replaced, saves registers, calls the module, then returns. Everything else in the firmware stays exactly as Zoom built it.

Check the facts you are tempted to assume, by measuring them:

4. First sound, then a talking debugger

The first build should do nothing but play a test tone. It proves the hook, the rate and the level, and that the device still records normally.

From then on, the device’s own voice is the debugger. There was no debug port, and the developer is blind, so a screen was no use either. Instead, diagnostic builds say numbers:

(SCR_PROBE, MON_PROBE, LANG_PROBE, OPUS_PROBE in arm/voice.c.) One flash of a probe build answers a question that would otherwise take hours of reading. For example, the Versions screen turned out to be screen 16, not the one the code reading suggested.

5. Test in an emulator before every flash

Each build runs first in an emulator (Unicorn): the real module, the real voice data and the firmware’s real data, with the emulated screen and button state set by the test script (tools/emu/; set-up and walk-through in EMULATION.md). It catches crashes, wrong output and timing before the device ever sees the file:

The emulator does not replace the device. It cannot show priority problems between tasks, or what happens during start-up. But it means a device test only checks behaviour, never “does it boot”.

6. Know what is on screen, one screen at a time

The firmware already knows what it displays. The work is finding where it keeps that:

Rules that saved a lot of trouble:

7. Speech that fits

A full speech synthesiser does not fit or run on such a device. Pre-recorded phrases do:

8. Timing and priorities on a real-time device

Most of the bugs that remained came from sharing the CPU:

9. Make it usable by others

A web page that builds the firmware in the browser means nobody needs a toolchain:

Only the patch is distributed; the manufacturer’s firmware never is.


A note on the law

This work reads the manufacturer’s firmware in order to make an independent program (the voice module) work with it, for accessibility. In the EU, Directive 2009/24/EC (article 6) allows decompiling for that kind of interoperability, under conditions. This repository publishes the following, all written for this project:

It does not publish the manufacturer’s firmware or decompiled code. This is not legal advice; check what applies where you live.