SendDtmf was a stub: GNOME Calls' SendTones and Plasma Dialer's sendDtmf
reached imsd and went nowhere. The SDP parser reads the telephone-event
payload type at the chosen codec's clock into the answer, among the
m-line's formats only — an rtpmap the line does not list is not
negotiated, and nothing is sent with a format the answer does not carry
(RFC 3264 §8.3.2); an event's rate is its rtpmap clock (RFC 4733
§2.4.1), an offer with two codec clocks lists telephone-event at each,
and the name is matched as every encoding name is, case-insensitively,
with the clock whole. The media leg carries it, and imsd-media gets it
as DTMF_PT with its stdin on a pipe from imsd, the control pipe. The
SDP answer imsd sends to an inbound offer carries telephone-event by the
same rule, where it echoed any telephone-event rtpmap with a lower-case
name and a clock prefix before: an rtpmap off the offer's m-line is no
longer echoed, an upper-case name now is. The decision is the engine's:
OnDtmf lets the tones through on an active call that negotiated
telephone-event (the answer's number on a call we placed, the offer's on
one we answered) and drops them otherwise with a line that says why:
before the answer, after the call, no telephone-event at the codec's
clock in the SDP, a character that is no tone or more than 128 tones
(then the whole string, never part of what was pressed); letters are
upper-cased. The shell writes one `dtmf <tones>` line per SendDtmf,
whole or not at all: the bound keeps the line within one atomic pipe
write, the write end is non-blocking so that a leg that stopped reading
fails the write instead of holding the engine thread, and a longer line
is refused; SIGPIPE is ignored so that a write after the leg died fails
with EPIPE instead of ending the daemon. The command queue's one
argument is `arg` now — the number to dial, or the tones. The `media …`
start line gains the telephone-event pt at its end.
Tested: tests/Sdp and tests/Engine (the eight suites pass); the dev
phone's dtmf benches ran in builds of this series
(journal/ims/captures/2026-10-14-dtmf-bench-1/ onward).
KPN's interconnect gateway transcodes every caller up: a G.711-only
fixed-line INVITE reached the phone offering PCMA, PCMU, AMR and AMR-WB
(bench call 2026-09-08), so the AMR-WB-only build rang and the narrowband
path — the one the Telia field report hit with 488 — cannot be reached on
the air through KPN by any caller.
CODECS=<list> (comma-separated over AMR-WB, AMR/AMR-NB, PCMA, PCMU) makes
the list both the codecs offered on an outgoing call and the acceptance
preference for an inbound offer, in that order: CODECS=PCMA takes G.711
A-law out of the mixed offer above, CODECS=PCMA,PCMU sends a G.711-only
offer toward the network. Unset, nothing changes — the default offer
bytes and the AMR-WB > AMR > PCMA > PCMU preference stay pinned. The
daemon logs an active override at startup.
Our offer listed AMR-WB alone. A call we place TO a landline then depends
on the far side transcoding up to wideband; a gateway that will not is
entitled to refuse. AMR narrowband is the 3GPP-mandatory codec every IMS
core and PSTN gateway can answer, so it goes second, with telephone-event
at the 8 kHz clock beside the existing 16 kHz one. AMR-WB stays first, so
a mobile-to-mobile call negotiates exactly what it did before; if the
network does answer AMR, the engine already starts the media leg on the
answered codec.
Byte pins in tests/Sdp and tests/Messages re-pinned (INVITE body 323 ->
466 bytes).
NOTE FOR RELEASE: this changes the MO INVITE on the path the arranged 112
calls used. One bench MO call on KPN before it ships.
An incoming INVITE whose offer carried no AMR-WB was refused with 488
before ringing. A landline caller arrives through the PSTN gateway, which
offers narrowband — AMR (NB) and/or G.711 — so every landline call was
refused, deterministically. Field report 2026-09-01 (Telia Norge): a
doctor's office called twice, both 488; a mobile caller rang fine.
The parser now recognises PCMA/PCMU by rtpmap name or by static payload
type (a G.711 offer may carry no rtpmap line at all, RFC 3551 §6), matches
encoding names case-insensitively (RFC 4566 §6), and prefers
AMR-WB > AMR > PCMA > PCMU with octet-aligned first within AMR — a mobile
caller offering everything still lands on AMR-WB. The engine accepts any
codec the media leg plays and answers at the offer's own payload type with
the DTMF clock matching the codec (8 kHz for narrowband). A refused offer
now logs what WAS offered ('offered: 18 G729, 101 TELEPHONE-EVENT'), so a
field journal answers 'which codec did the gateway want' without a raw SIP
dump.
The media leg gains the matching codecs in the next commit.
Userspace VoLTE/IMS daemon for mainline Linux phones (developed on the
Fairphone 6): a GLib-free core (SIP, SDP, USIM AKA, IPsec SA setup, RTP
media, call engine) behind a GDBus control daemon, a standalone AMR-WB
media leg, and a Plasma Dialer backend. C++26 modules built with Crafter
Build; the tree is clean under the project's house-style linter
(crafter-build lint) across all three build products.
Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>