sdp, engine, imsd, media: the far end's telephone-event packets are played as the tones they stand for

Keys pressed on the other phone reached imsd-media as RFC 4733 events and
were dropped with every packet that is not the audio's. The playout now
takes them: the gap before an event's start (a stock sender's DTX silence
often precedes a key press) is filled as before an audio frame, each
packet reports the event's duration so far from its start stamp, and what
the clock has not covered yet is synthesized as the event's Q.23
frequency pair at the packet's volume, so a retransmitted end packet adds
nothing, a lost update is caught up by the next, and a far end whose
audio resumes past the event (the stock phone's does) meets the clock
with the usual fill. An answer may renumber telephone-event from what we
offered (RFC 3264: the answerer sends with the offer's numbers, §6.1,
and the offerer with the answer's, §5.1), so the leg gets both: DTMF_PT
to send with, DTMF_RX_PT to take events at as well — our own SDP's
number: the offer's 98 or 100 on a call we placed, the offer's number we
echoed on one we answered; when the answer gave the audio our offer's
number, that number is the leg's audio and DTMF_RX_PT is the answer's
telephone-event number instead — and takes events at either number,
never at the audio's. --selftest pins the frequency table; the stats
gain rx_dtmf, tones and dtmf_rx_pt.

Tested: --selftest, tests/Sdp and tests/Engine (the eight suites pass);
the loopback's --dtmf-rx arm on a host build of this commit's tree
(journal/ims/captures/2026-10-15-dtmf-loopback-c3-rx.txt; the tx, both,
plain and collision runs beside it); the dev phone's dtmf benches ran in
builds of this series.
This commit is contained in:
Jorijn van der Graaf 2026-10-15 21:18:28 +02:00
commit e31887bf21
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
7 changed files with 244 additions and 38 deletions

View file

@ -283,7 +283,15 @@ export namespace imsd::engine {
// Parsed SDP answer needed to bring up the media leg. `codec` is one of
// imsd-media's: "AMR-WB", "AMR", "PCMA", "PCMU" (empty = none we play).
// `dtmfPt` is the telephone-event payload type at the codec's clock
// (RFC 4733); nullopt = none negotiated, no DTMF on this call.
// (RFC 4733) the leg sends with — the peer's SDP's; nullopt = none
// negotiated, no DTMF on this call. `dtmfRxPt` is a second number the
// leg takes events at: our own SDP's at that clock — our offer's
// number on a call we placed (an answerer sends with the offer's
// numbers, RFC 3264 §6.1, and may renumber ours in the answer), the
// offer's we echoed on a call we answered; the answer's telephone-event
// number instead when the answer gave the audio our offer's number,
// which is then the leg's audio number. The leg takes events at
// either number, never at the audio's.
struct MediaLeg {
std::string remoteIp;
int remotePort = 0;
@ -291,6 +299,7 @@ export namespace imsd::engine {
bool octetAlign = false;
std::string codec = "AMR-WB";
std::optional<int> dtmfPt;
std::optional<int> dtmfRxPt;
bool Valid() const { return !remoteIp.empty() && remotePort > 0 && payloadType >= 0; }
};
@ -831,6 +840,12 @@ export namespace imsd::engine {
leg.octetAlign = ans.octetAlign;
leg.codec = ans.codec;
leg.dtmfPt = ans.dtmfPt;
// the second number the leg takes events at: our offer's on a call
// we placed — the answer's instead when the answer gave the audio
// that very number, which is then the leg's audio number
const int offered = imsd::sdp::OfferedTelephoneEventPt(imsd::sdp::CodecRate(leg.codec));
leg.dtmfRxPt = !leg.dtmfPt ? std::nullopt
: std::optional<int>(direction_ == Direction::Outgoing && offered != leg.payloadType ? offered : *leg.dtmfPt);
return leg;
}

View file

@ -84,6 +84,11 @@ export namespace imsd::sdp {
std::vector<std::string> codecs; // offered codecs in order; empty = AMR-WB, AMR
};
// The payload type our offer gives telephone-event at a clock rate: the
// number an answerer sends with (RFC 3264 §6.1), whatever the answer
// renumbered ours to.
int OfferedTelephoneEventPt(int clockRate) { return clockRate == 16000 ? 98 : 100; }
// Our offer: AMR-WB (octet-aligned) first, AMR narrowband second — the
// 3GPP-mandatory codec every IMS core and PSTN gateway can answer, so a
// call we place TO a landline no longer depends on the far side
@ -109,8 +114,8 @@ export namespace imsd::sdp {
s += "t=0 0\r\n";
std::string pts;
for (const auto& c : codecs) pts += std::format(" {}", pt(c));
if (wb) pts += " 98";
if (nb) pts += " 100";
if (wb) pts += std::format(" {}", OfferedTelephoneEventPt(16000));
if (nb) pts += std::format(" {}", OfferedTelephoneEventPt(8000));
s += std::format("m=audio {} RTP/AVP{}\r\n", o.rtpPort, pts);
s += std::format("b=AS:{}\r\n", wb ? 41 : amr ? 30 : 80);
s += "b=RS:512\r\n";
@ -127,12 +132,12 @@ export namespace imsd::sdp {
}
}
if (wb) {
s += "a=rtpmap:98 telephone-event/16000\r\n";
s += "a=fmtp:98 0-15\r\n";
s += std::format("a=rtpmap:{} telephone-event/16000\r\n", OfferedTelephoneEventPt(16000));
s += std::format("a=fmtp:{} 0-15\r\n", OfferedTelephoneEventPt(16000));
}
if (nb) {
s += "a=rtpmap:100 telephone-event/8000\r\n";
s += "a=fmtp:100 0-15\r\n";
s += std::format("a=rtpmap:{} telephone-event/8000\r\n", OfferedTelephoneEventPt(8000));
s += std::format("a=fmtp:{} 0-15\r\n", OfferedTelephoneEventPt(8000));
}
s += "a=ptime:20\r\n";
s += "a=maxptime:240\r\n";