Skip to content

Repository files navigation

CMF Ringtone Tool — custom ringtones for the CMF Watch Pro 2

🎵 CMF Ringtone Tool

Your CMF Watch Pro 2 can play more than the five ringtones it shipped with — this project proves it.
A full reverse-engineering of the watch's .act ringtone format, plus the tools to decode it, encode it, splice it, and (soon) beam a custom tone straight onto your wrist over Bluetooth.

GitHub stars GitHub issues Python 3 CMF Watch Pro 2 License: GPL-3.0

Decode .act ringtones → WAV • Encode MP3/OGG/WAV → ACT v4 • Splice stock tones • Beam it to the watch over BLE

Decoder: working Encoder: working BLE install: untested on hardware

Tech stack: Python, Rust, JavaScript, HTML, CSS, Git, GitHub, Tauri
Python research tooling today — Rust + Tauri desktop app incoming

What is thisToolboxUsageBLE statusSafetyCredits


🎧 What is this?

The CMF Watch Pro 2 (Nothing's budget-friendly smartwatch) comes with a handful of built-in ringtones and no official way to add your own. The tones live on the watch's flash as .act files — a format nobody had documented, produced by an encoder nobody outside Actions Technology has.

So this project did the fun thing: it took the format apart, byte by byte, until it gave up its secrets. The result is a small, honest toolchain that can:

  • 🔓 Decode any .act ringtone back into a normal WAV you can listen to
  • 🎚️ Encode your own audio (WAV today, MP3/OGG/FLAC support in progress) into a real .act file
  • ✂️ Splice existing tones together into new combinations
  • 📦 Repack the watch's firmware image with a replaced ringtone
  • 📡 Install it over Bluetooth — no disassembly, no debug cables (currently being validated)

Everything here was figured out from the stock firmware and public SDKs. No NDAs were harmed.

⌚ Compatibility

Device / firmware Status
CMF Watch Pro 2 (Actions ATS3089C, Zephyr) ✅ Target device. All analysis and validation done against its stock firmware. All 12 stock tones decode cleanly.
Tuya ZS308B SDK .act samples ✅ All 10 decode cleanly (same codec, no XOR layer)
Other ATS308x-based watches 🧪 Very likely the same codec, but untested — don't assume
Other CMF / Nothing devices ❓ Unknown — no claims until tested

If you try this on different hardware or a different firmware version, please open an issue and share what you found — even "it did nothing" is useful data.

🧩 What on earth is .act?

.act is an Actions Technology "actii" audio bitstream — a proprietary, CELP-family parametric speech codec that CMF/Nothing (mis)uses for ringtones. A few things make it unusual:

  • It's obfuscated, but charmingly badly. Every byte on flash is XORed with a repeating 2-byte key (57 2a). Raw streams start with the magic e1 d3; on-flash files start with b6 f9. That's the whole "protection".
  • It's tiny. 16 kHz, mono, 16 kbps — 20-byte frames, each carrying 10 ms of audio (160 samples). A whole ringtone is about 15 KB.
  • Nobody publishes an encoder. The only public decoder is a compiled ARM library (a1_act_d.a) buried in the Actions SDK. The encoder is an internal Actions PC tool. So this repo runs the original decoder under CPU emulation, and builds an encoder by reverse-engineering what the decoder expects.

🔊 ACT v4 at a glance

Layer Bytes Notes
On-flash container Raw stream XOR 57 2a (repeating)
Magic e1 d3 v4, fixed 16 kHz mono (v1–v3 and a newer selectable-rate container also exist)
Frame 20 B → 160 samples (10 ms)
Sync frame starts b2 dd 03 42 Re-init marker; first frame of every file + periodic
EOF short final read Decoder treats it as clean end-of-stream

Each 160-bit frame packs 22 fields: LSP spectral-envelope indices (split-VQ), pitch lag/gain for two subframes, and a 2-pulses-per-track algebraic fixed codebook with log-domain gains. The full, evidence-linked field map is in act_emu/REPORT.md.

🛠️ The toolbox

Tool What it does Status
act_emu/act_decode.py .act → WAV, using the original Actions decoder running under Unicorn CPU emulation. Auto-detects the XOR layer. ✅ All 22 known files decode 100%
act_emu/act_encode.py Audio → ACT v4 encoder, built by inverting the decoder field by field, with an adaptive codebook (pitch) and closed-loop analysis-by-synthesis refinement driven by the emulated original decoder ✅ Works (see below)
act_emu/audio_in.py WAV/MP3/FLAC/OGG/AAC/M4A → canonical 16 kHz mono PCM (ffmpeg for the non-WAV formats), so the codec stays input-format independent ✅ All formats tested
act_emu/act_splice.py Builds valid custom .act files by splicing segments of stock tones ✅ Verified end-to-end
act_emu/fwmod.py Extract / replace / rebuild the sdfs_k partition and AOTA firmware container with valid CRC32s ✅ Round-trip byte-for-byte verified
act_emu/fw_registry.py Firmware compatibility registry — every version-dependent offset/entry/strategy lives here; unknown versions are refused ✅ 1.0.0.73 registered
act_emu/cmf_shell.py BLE debug-shell client for the watch 🧪 Pending on-device runs
act_emu/ble_ringtone.py Backup / safety-check / write / restore logic over the debug shell; the planner used by cmf_flash_plan.py and by the mocked transport tests ✅ Mock-tested
act_emu/cmf_flash_plan.py Offline planner: generates the exact mww/snandw command lists for a BLE-only ringtone swap ✅ Offline plan validated
act_emu/test_encoder.py Legacy encoder regression suite (kept for the stock-tone round-trip fixtures) ✅ All tests pass
tests/ Real pytest regression suite: signals, real content, edge cases, audio formats, offline firmware pipeline, mocked BLE transport ✅ 43 tests pass
act_emu/armlink.py + friends The archaeology kit: ARM/Thumb ELF linker, disassembler, tracer, oracle harness used to crack the format 🔬 Research tooling

🎚️ How the encoder works (and where it still struggles)

There's no official encoder, so act_encode.py is a genuine analysis-by-synthesis encoder built from scratch:

  1. Spectral envelope — 16th-order LPC (Levinson) → line spectral frequencies → quantized with the decoder's exact split-VQ tables and MA-prediction, tracking decoder state bit-for-bit.
  2. Pitch (adaptive codebook) — the 9-bit field→lag map and the 6-bit per-subframe offset are fully recovered from the locked-step oracle; candidate lags come from residual autocorrelation, and a pitch-continuity prior keeps the lag stable across frames. Tonal material is carried by the adaptive excitation, not just the spectral envelope.
  3. Excitation — the LPC residual is mapped onto the fixed codebook's 2-pulses-per-track structure using an empirically extracted pulse map (512 values per field, measured from the real decoder).
  4. Gains + refinement — initial gains come from the reversed log-domain interpolation table; with the locked-step OracleDecoder attached, every free field (fixed-codebook fields, pitch fields, gain fields) is tuned by coordinate descent against actual decoded waveform SNR, and the oracle state is snapshot/restored between the trials that advance decoder state.

The honest status: tones track their true pitch (zero-crossing and FFT-checked frequencies match), silence stays silent, sine SNR is positive (up to ~11 dB on 440 Hz in the focused loop), and melodies and ringtone-like content come through recognizably. Speech-quality evaluation hasn't been completed yet — treat music/tone reproduction as the validated class (see tests/ for exactly what is exercised).

✅ Testing & validation

Nothing here is "should work" — it's all executed and measured:

  • 🧪 Decoder: 22/22 known .act files (12 from the watch, 10 SDK samples) decode with 100% byte consumption and coherent PCM.
  • 🧪 Encoder (test_encoder.py, all passing): silence stays silent (RMS 8), tones come out at the tracked frequency, noise bursts stay stable, a 3-second clip encodes without drift, and a stock ringtone survives a full re-encode round-trip.
  • 🧪 pytest suite (43 tests, tests/): signal classes (silence, sines 20-3200 Hz, sweeps, noise), content classes (speech-like, melody, transients, ringtone sample), edge cases (sub-frame, exact frames, 60 s runs, stereo input), audio formats (WAV/FLAC/MP3/OGG plus robust failure paths), the offline SDFS/AOTA chain end-to-end, and deterministic mocked-transport BLE coverage (discovery, backup, staging, write, checksum repair, verification, restore, failure injection, unsupported-firmware refusal).
  • 🧪 Firmware packer: modified AOTA images re-parse, all CRC32s valid, round-trip byte-for-byte identical.
  • 🧪 BLE flash plan: the simulated post-write partition passes the real SDFS parser, all three checksum fields match, all untouched files stay byte-identical, and the new ringtone decodes error-free — the same plan is replayed word-for-word by the mocked transport tests.

📁 Repository structure

act_emu/          All the tools + the full technical report (act_emu/REPORT.md)
act_emu/wav/      Decoded reference WAVs of every known stock .act
artifacts/        Generated example: custom ring1.act + BLE staging command lists
docs/             README assets + docs/HARDWARE.md (manual on-device procedure)
tests/            pytest regression suite (codec, audio input, firmware, BLE mocks)
requirements.txt / requirements-dev.txt

📦 Getting started

Requirements: Python 3, plus:

pip install -r requirements.txt          # runtime (unicorn)
pip install -r requirements-dev.txt      # + tests (pytest, numpy, capstone)
# system: ffmpeg (only for MP3/FLAC/OGG/AAC input), bleak (only for BLE runs)
  • unicorn + capstone — CPU emulation and disassembly (decoder, encoder, research tools)
  • numpy, pytest — FFT-based quality metrics and the regression suite
  • ffmpeg — non-WAV audio input (optional; WAV needs nothing extra)
  • bleak — Bluetooth Low Energy (the on-watch tools only)

Two things are deliberately not in this repo, because they're proprietary to Actions/CMF/Nothing:

  1. The Actions decoder library (a1_act_d.a from the Actions SDK) — needed to reproduce the decoder-emulation linking step:
    cd /tmp/actdec && ar x <sdk>/framework/media/libal/a1_act_d.a
    python3 armlink.py /tmp/actdec/*.o linked.json
    (linked.json is already committed, so decoding works out of the box — you only need this to reproduce the linking step.)
  2. The stock firmware (original.bin) — needed for firmware repacking and BLE flash planning. Obtain it from your own watch or the community firmware dump linked below. It is proprietary to CMF/Nothing, and large, so it will never be committed here.

🚀 Usage

Decode a ringtone to WAV 🎧

python3 act_emu/act_decode.py ring1.act ring1.wav

Encode your own audio to ACT 🎚️

# WAV (16 kHz, mono, 16-bit PCM) goes straight in
python3 act_emu/act_encode.py mysong.wav mysong.act

# MP3 / OGG / FLAC / AAC / M4A: same API through act_emu/audio_in.py
python3 -c "from act_emu.audio_in import load_audio; \
  import act_emu.act_encode as E; \
  open('mysong.act','wb').write(E.encode_file(load_audio('mysong.mp3')))"

Splice stock tones into a custom ringtone ✂️

python3 act_emu/act_splice.py custom.act ring1.act ring4.act

Repack a firmware image with your ringtone 📦

python3 act_emu/fwmod.py original.bin --list                              # inspect
python3 act_emu/fwmod.py original.bin --replace ring1.act=custom.act --out modified_aota.bin

Run the test suites 🧪

python3 -m pytest tests/ -q          # full regression suite (codec + firmware + BLE mocks)
python3 act_emu/test_encoder.py      # legacy codec round-trip suite

BLE recon against a live watch (read-only) 📡

python3 act_emu/cmf_shell.py --address XX:XX:XX:XX:XX:XX recon

Generate a BLE flash plan 🗺️

python3 act_emu/cmf_flash_plan.py original.bin ring1_custom.act

🔬 Reverse-engineering notes

The full story — how the XOR layer was spotted, how the decoder library was statically linked and emulated, how every bit field of the frame was mapped, the codec tables, the BLE shell discovery, and the complete chain of evidence — lives in act_emu/REPORT.md. If you like that sort of thing, it's a fun read.

📡 Putting it on the watch (BLE status)

This is where honesty matters most. Here's exactly what is confirmed and what is not:

✅ Confirmed (static analysis + offline execution):

  • The production firmware exposes a full Zephyr debug shell over BLE — including mdw/mww (RAM read/write), snandr/snandw (raw NAND read/write through the FTL), and sdfs (dump any file by name — a built-in backup primitive).
  • The complete ringtone-swap procedure (recon → stage → write → patch checksums → verify) has been planned command-by-command, and the simulated result passes every offline check.

🧪 Experimental / not yet verified:

  • Nothing has been flashed to a physical watch. Not once. The on-device steps are pending a first, read-only recon run to confirm the debug commands are actually enabled on live hardware and to read out the real partition offset (PBASE).
  • The normal CMF BLE protocol (the one Gadgetbridge speaks) has no generic file-write command — the debug shell is the only known BLE write path, which is why it's experimental territory.

In short: the road is mapped and the car is built, but it hasn't left the garage yet. 🚗 The manual on-device procedure (read-only recon → harmless test target → real ringtone → restore) is documented step by step in docs/HARDWARE.md.

⚠️ Safety & backups

Playing with raw flash writes is inherently risky. If you go near the write path:

  • Back up first. sdfs ring1.act 15556 dumps the stock ringtone straight off the watch; keep it (and a stock firmware dump) somewhere safe before changing anything.
  • Writes go through the FTL (sector-aligned, ECC, bad-block management) — the same path the OS itself uses, so a correctly-placed write isn't inherently destructive. A misplaced one can be.
  • Biggest risks: a wrong partition offset (mitigated: read-back verification before every write) and power loss mid-write (mitigated: only two small regions are touched, and the stock tone can be restored the same way).
  • First on-device test should be harmless — e.g. change the sdfs.txt test string, not the ringtone.
  • You do this at your own risk. Bricking your watch is unlikely if you follow the procedure carefully, but "unlikely" is not "impossible".

🤝 Contributing

Contributions are very welcome — this is a research project and there's plenty left to do:

  • 🎚️ Calibrate the pitch field→lag map and enable the adaptive codebook (the encoder quality blocker)
  • ⌚ Test decoding/tools against other ATS308x devices and firmware versions
  • 📡 Carefully validate the BLE write path on real hardware (read-only recon first!)
  • 📝 Docs, tests, cleanups — all good

Open an issue before big changes so we don't duplicate effort. Please keep the tone of this repo: claims backed by execution, and clear labels on anything unverified.

🙏 Credits

This project stands on public third-party work — huge thanks to all of them (not vendored here, clone separately):

The ACT decoder binary (a1_act_d.a) is © Actions Technology; the CMF firmware is © CMF/Nothing. Both remain the property of their respective owners and are used here solely for interoperability research.

📄 License

GPL-3.0 — free software: use it, break it, improve it, share it. The full text lives in LICENSE.

Third-party components and upstream projects listed in Credits remain under their own licenses. The ACT decoder binary (a1_act_d.a) is © Actions Technology; the CMF firmware and trademarks are © CMF/Nothing.


Not affiliated with, endorsed by, or connected to Nothing Technology or CMF.
"CMF", "Nothing" and the watch itself belong to their respective owners.

Made with ☕, a disassembler, and an unhealthy amount of curiosity.

About

Custom ringtones for the CMF Watch Pro 2. Decode, tweak, and load your own tones — everything documented, everything open.

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages