Ring tones and wait tones¶
Custom audio on the head unit, and the tooling to prepare it.
What the unit plays¶
All plain PCM WAV. Two formats, depending on the slot:
| slot(s) | path | format |
|---|---|---|
ring1 … ring5 |
ring_tones/ringNRT.wav |
16-bit mono 44 100 Hz |
busy, error, ok, ko |
ring_tones/{busy,error,ok,ko}RT.wav |
16-bit mono 44 100 Hz |
wait:<LANG> |
wait_tones/MM_HoldOn_<LANG>_8kHz.wav |
16-bit stereo 8 000 Hz |
Wait-tone languages: CRC CZC DUN ENG FRF GED HRH ITI PLP PTP RUR SPE TRT.
The RT suffix in the ring-tone filenames is part of the real name.
Full inventory with durations is in Media partition.
The tool¶
tools/ringtones.py — no Python dependencies; uses ffmpeg when it needs to decode
anything other than an already-correct WAV (so mp3, ogg, flac, m4a all work). Without
ffmpeg it will still copy a WAV that is already in the target format.
uv run tools/ringtones.py list # slots and expected formats
uv run tools/ringtones.py probe song.mp3 # what is this file?
uv run tools/ringtones.py export --tree media/ -o stock/ # back up the stock tones
uv run tools/ringtones.py convert song.mp3 --slot ring1 -o ring1RT.wav
uv run tools/ringtones.py stage song.ogg --slot ring5 --tree media/
--tree is an extracted media partition — a directory containing ring_tones/
and wait_tones/. stage converts to the exact format and writes it under the correct
filename, ready to be packed back up.
GUI¶
tools/patch_studio.py (Qt / PySide6) wraps the same conversion, lets you preview
and export the stock tones, and builds patched packages from the bundled
patches/*.json definitions — including the always-enable-AUX and sticky-AUX variants.
Multiple definitions can be ticked at once; a conflicting address is reported rather
than silently applied.
It has three tabs: Ringtones, Splash screens (the brand artwork — see Media partition) and Pack & patch.
The studio builds and runs (verified on macOS with PySide6 6.11.2 — it also has headless
GUI tests in tests/test_studio.py, which skip when PySide6 is absent). It has not been
used to produce a package that was then flashed to a car, so treat the end-to-end result
as unverified; the pieces it drives are covered by tests. The CLI sequence below is the
verified path — walk it if the studio gives you trouble.
How a tone gets onto the unit¶
The tones live inside the media partition (system.bin), a gzip'd tar with its own
checksum cascade. tools/patch_media.py swaps a file inside the tar and rebuilds
system_ctrl.bin → system.bin → system.bin.inf → the module manifest → the root manifest.
A replacement tone almost never matches the original size, so the tar and every manifest above
it change; the SIZE: / SIZE_1..SIZE_32 fields are computed from the tar (see
Media partition).
Worked example: replacing a ring tone¶
The whole path, from an mp3 to a package that will flash, replacing ring1.
The one-command version is uv run tools/build_package.py --manifest builds/alien-piano-riff.json
(edit its paths first); in a manifest, media.tones maps a slot's file to the source audio and
an optional gain_db. The manual steps below are what it runs.
# 1. work on a copy — never edit your rollback package
cp -a SMEG_PLUS_UPG SMEG_PLUS_UPG_custom
# 2. application patches first (they rewrite ctrl.bin and NAV_ctrl.bin)
uv run tools/patch_smeg.py --src SMEG_PLUS_UPG --out overlay
rsync -a overlay/ SMEG_PLUS_UPG_custom/
# 3. extract the media partition, keeping the stock tones for restore
uv run tools/patch_media.py extract --package SMEG_PLUS_UPG_custom --module NAV \
--tree media --backup backup --backup-tones-only
# 4. convert and stage the replacement into the tree
uv run tools/ringtones.py convert my-tone.mp3 --slot ring1 -o media/ring_tones/ring1RT.wav
# 5. rebuild the partition and its checksum cascade
uv run tools/patch_media.py apply --package SMEG_PLUS_UPG_custom --module NAV \
--tree media --out overlay2
rsync -a overlay2/ SMEG_PLUS_UPG_custom/
# 6. re-seal — without this the unit refuses the media
uv run tools/patch_contract.py --package SMEG_PLUS_UPG_custom
Order matters
patch_smeg rewrites NAV_ctrl.bin and ctrl.bin for the application image;
patch_media then swaps the system.bin records inside those same manifests. Running
the media step against the already-patched package means it carries the application
change rather than reverting it. The contract step goes last, because it seals
whatever the package finally contains. This is the same app → media → contract rule laid
out in Running the tools.
apply --dry-run shows the diff and writes nothing:
To put the original back:
uv run tools/patch_media.py restore --backup backup --tree media \
--module NAV --only ring_tones/ring1RT.wav
Names¶
The name each slot shows in the phone's ringtone menu is a string literal in the
application image, not part of the audio file and not a database row (read, #190):
C_SRV_RING_TOUCH::SetRingFilePath pairs each ringN RT.wav with one of them and serves the
list over DBus (GetRingToneList), and the menu displays those strings.
| slot | stock name | address (NAV 5.43.A.R2) | max length |
|---|---|---|---|
| ring1 | Alien |
0x03063198 |
7 |
| ring2 | Blue_lemon |
0x030631ac |
11 |
| ring3 | Blue_tangerine |
0x030631c4 |
15 |
| ring4 | Green_apple |
0x030631e0 |
11 |
| ring5 | Green_lemon |
0x030631f8 |
11 |
The maximum is the string plus the NUL padding before the next literal; each string has
exactly one reference (read). build_package.py's media.names turns into an application
patch that rewrites the literal in place:
It refuses a name that is too long, non-ASCII, or for a module whose addresses are not mapped (only NAV 5.43.A.R2 is).
Built and verified offline (the patched image carries the new name and nothing else changes); not yet confirmed on a car.
ringtones.py names / rename inspect and edit a different list: phone/Ringing_List in the
seed up_common.sqlite. The unit does not display that list; the key appears nowhere in the
application image (read).
Level — the thing that will annoy you¶
The stock tones are mastered loud: peak around −1 dBFS. A normalised music track is typically 6–8 dB quieter, and in a car that difference is obvious — the new tone sounds muted next to the others.
ringtones.py convert deliberately does not touch level. It converts format only
(-ac <n> -ar <rate> -c:a pcm_s16le), so what you supply is exactly what plays. Check
your file against the stock tone before you flash:
ffmpeg -i old.wav -af volumedetect -f null - 2>&1 | grep -E "mean_volume|max_volume"
ffmpeg -i ring1RT.wav -af volumedetect -f null - 2>&1 | grep -E "mean_volume|max_volume"
If it is quiet, match the level yourself rather than letting the tool guess:
# simple gain, then convert
ffmpeg -i my-tone.mp3 -af "volume=7dB" -ar 44100 -ac 1 -c:a pcm_s16le ring1RT.wav
# or loudness-normalise with a true-peak ceiling, so nothing clips
ffmpeg -i my-tone.mp3 -af "loudnorm=I=-16:TP=-1.0" -ar 44100 -ac 1 -c:a pcm_s16le ring1RT.wav
As a reference, the stock ring1RT.wav measures mean_volume: -14.4 dB,
max_volume: -1.1 dB.