From 452634683ca58ef5fcca341f3dc3da8cd50148cc Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sat, 5 Sep 2026 23:06:57 +0200 Subject: [PATCH 1/4] packaging: cross-compile for the FP6's own cores The phone is one SoC, 4x Cortex-A520 + 4x Cortex-A720, and the package is built for nothing else, so build for it: -march=armv8.6-a+fp16fml+aes+sha3+sm4 -mtune=cortex-a720 instead of the generic Armv8 baseline. That is gcc's own -march=native expansion on the phone, re-based on armv8.6-a so clang can express the same set; 8.6 is the highest level whose mandatory set the phone exposes (8.7 would assume WFxT, 9.x SVE2 - neither is in its hwcaps, and SVE2 code would SIGILL). Same string as the fp6-img aports and kernel. --- README.md | 2 +- packaging/build-package.sh | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8db01bc..822474b 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ make install # DESTDIR/PREFIX staged install incl. the packaging/ files packaging/make-sysroot.sh # one-time: Alpine aarch64 sysroot from the CDN crafter-build -- --target=aarch64-alpine-linux-musl \ --sysroot=$HOME/.cache/imsd/sysroot-aarch64-alpine \ - --march=armv8-a --mtune=generic + --march=armv8.6-a+fp16fml+aes+sha3+sm4 --mtune=cortex-a720 # same flags after `crafter-build test --target=aarch64-alpine-linux-musl` # run the suite under qemu-aarch64 against the sysroot ``` diff --git a/packaging/build-package.sh b/packaging/build-package.sh index e035173..ae45bf6 100755 --- a/packaging/build-package.sh +++ b/packaging/build-package.sh @@ -75,7 +75,11 @@ retry "make sysroot" "$SRC/packaging/make-sysroot.sh" "$SYSROOT" # host arch — the aarch64 binaries are additionally exercised by fp6-img's # chroot `make check` whenever an image builds) cd "$SRC" -XTARGET="--target=aarch64-alpine-linux-musl --sysroot=$SYSROOT --march=armv8-a --mtune=generic" +# The phone is one SoC, 4x Cortex-A520 + 4x Cortex-A720, so build for it. +# armv8.6-a is the highest level whose mandatory set it exposes (8.7 would +# assume WFxT, 9.x SVE2 - neither is in its hwcaps); +fp16fml+aes+sha3+sm4 are +# the optional extensions it has. Same string as the fp6-img aports. +XTARGET="--target=aarch64-alpine-linux-musl --sysroot=$SYSROOT --march=armv8.6-a+fp16fml+aes+sha3+sm4 --mtune=cortex-a720" crafter-build -- $XTARGET crafter-build -- --product=media $XTARGET crafter-build -- --product=dialerd $XTARGET From 37ec8660b7c4025d34978142e34673806be0a436 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Tue, 8 Sep 2026 17:53:26 +0200 Subject: [PATCH 2/4] engine/sdp: CODECS override for the codecs we offer and accept MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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= (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. --- README.md | 1 + implementations/main.cpp | 14 +++- interfaces/Imsd-Engine.cppm | 23 ++++-- interfaces/Imsd-Sdp.cppm | 156 ++++++++++++++++++++++-------------- tests/Engine/main.cpp | 56 +++++++++++++ tests/Sdp/main.cpp | 87 ++++++++++++++++++++ 6 files changed, 270 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 822474b..2b631e6 100644 --- a/README.md +++ b/README.md @@ -210,3 +210,4 @@ GPL-3.0-only — see [LICENSE](LICENSE). Copyright (C) 2026 Catcrafts® catcrafts.net +| `CODECS` | *(empty — defaults)* | comma-separated codec preference list over `AMR-WB`, `AMR` (or `AMR-NB`), `PCMA`, `PCMU`: restricts and orders both the codecs offered on an outgoing call and those accepted from an inbound offer (default: offer AMR-WB + AMR, accept all four in that order). A bench knob — a network whose gateway transcodes every caller up to AMR-WB otherwise never lets the narrowband path run | diff --git a/implementations/main.cpp b/implementations/main.cpp index 43b77f1..559569a 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -571,6 +571,15 @@ public: precond_ = EnvOr("PRECOND", "0") == "1"; ealgOffer_ = EnvOr("EALG", "aes-cbc"); mediaBin_ = ResolveMediaBin(); + // CODECS: restrict + reorder the codecs we offer and accept (bench + // knob — a gateway that transcodes every caller up to AMR-WB never + // lets the narrowband path run otherwise). Empty = defaults. + codecs_ = imsd::sdp::ParseCodecList(EnvOr("CODECS", "")); + if (!codecs_.empty()) { + std::string list; + for (const auto& c : codecs_) list += (list.empty() ? "" : ",") + c; + Log(std::format("CODECS override active: {}", list)); + } // EMERGENCY_NUMBERS: comma-separated additions to the builtin // 112/911 (a lab core's advertised short code, carrier extras). std::string extra = EnvOr("EMERGENCY_NUMBERS", ""); @@ -688,6 +697,7 @@ private: int pcscfPort_ = 5060; int rtpPort_ = 50004; bool precond_ = false; + std::vector codecs_; // CODECS override; empty = defaults // registration imsd::msg::Context ctx_; @@ -1138,7 +1148,7 @@ private: Log(std::format("EMERGENCY dial ({}): urn:service:sos over the live registration, " "fallback plain INVITE — stage 1, no emergency registration/PDN; " "carrier-side behavior UNVERIFIED", c.number)); - call_.emplace(ctx_, rng_, c.uni, c.number, rtpPort_, precond_, emergency); + call_.emplace(ctx_, rng_, c.uni, c.number, rtpPort_, precond_, emergency, codecs_); CallInfo info{c.uni, c.number, "dialing", "outgoing", NowEpoch(), 0}; PostAdded(info); bool ok = Execute(call_->Start()); @@ -1198,7 +1208,7 @@ private: } std::string uni = std::format("ims-call-{}", ++callSeq_); DumpRaw("imsd-invite-in.raw", msg); - call_.emplace(ctx_, rng_, uni, imsd::engine::IncomingInvite{msg}, rtpPort_); + call_.emplace(ctx_, rng_, uni, imsd::engine::IncomingInvite{msg}, rtpPort_, codecs_); callReply_ = reply_; Log(std::format("incoming call {} from {}", uni, call_->Number())); // Validate the offer BEFORE the UI hears of the call. A refused diff --git a/interfaces/Imsd-Engine.cppm b/interfaces/Imsd-Engine.cppm index ffa46f2..b91eef1 100644 --- a/interfaces/Imsd-Engine.cppm +++ b/interfaces/Imsd-Engine.cppm @@ -99,15 +99,20 @@ export namespace imsd::engine { public: // `emergency`: the shell classified the dialled string as an // emergency number — the first attempt targets urn:service:sos. - CallMachine(const imsd::msg::Context& ctx, imsd::util::Rng& rng, std::string uni, std::string number, int rtpPort, bool precond, bool emergency = false) + // `codecs`: the daemon's CODECS override — the codecs offered on an + // outgoing call and accepted from an inbound offer, in preference + // order; empty = the sdp module's defaults. + CallMachine(const imsd::msg::Context& ctx, imsd::util::Rng& rng, std::string uni, std::string number, int rtpPort, bool precond, bool emergency = false, + std::vector codecs = {}) : ctx_(ctx), rng_(rng), uni_(std::move(uni)), number_(std::move(number)), - rtpPort_(rtpPort), precond_(precond), attemptSos_(emergency) { + rtpPort_(rtpPort), precond_(precond), codecs_(std::move(codecs)), attemptSos_(emergency) { ResetDialog(); } - CallMachine(const imsd::msg::Context& ctx, imsd::util::Rng& rng, std::string uni, IncomingInvite in, int rtpPort) + CallMachine(const imsd::msg::Context& ctx, imsd::util::Rng& rng, std::string uni, IncomingInvite in, int rtpPort, + std::vector codecs = {}) : ctx_(ctx), rng_(rng), uni_(std::move(uni)), rtpPort_(rtpPort), - precond_(false), direction_(Direction::Incoming), + precond_(false), codecs_(std::move(codecs)), direction_(Direction::Incoming), state_(CallState::Incoming), reason_("incoming"), invite_(in.invite) { number_ = imsd::sip::CallerId(invite_); @@ -148,6 +153,7 @@ export namespace imsd::engine { offer.rtpPort = rtpPort_; offer.sessionId = rng_.UInt(1000000, 9999999); offer.precond = precond_; + offer.codecs = codecs_; std::string sdp = imsd::sdp::BuildOffer(offer); std::vector a; a.push_back(Send(imsd::msg::BuildInvite(ctx_, d_, sdp, precond_))); @@ -158,8 +164,9 @@ export namespace imsd::engine { // UAS: answer the inbound INVITE with 100 Trying + 180 Ringing (made // reliable only when the caller Requires 100rel) and arm the ring // timeout. An offer we cannot serve — no audio line, or none of the - // codecs imsd-media plays (AMR-WB, AMR, PCMA, PCMU) — is refused - // with 488 up front, before the UI ever rings; the log line names + // codecs imsd-media plays (AMR-WB, AMR, PCMA, PCMU; the CODECS + // override narrows that set) — is refused with 488 up front, before + // the UI ever rings; the log line names // what WAS offered, so a field journal answers "which codec did the // gateway want" without a raw SIP dump. std::vector OnInvite() { @@ -318,6 +325,7 @@ export namespace imsd::engine { offer.sessionId = rng_.UInt(1000000, 9999999); offer.precond = precond_; offer.currRemote = "sendrecv"; + offer.codecs = codecs_; ans = imsd::sdp::BuildOffer(offer); } // RFC 4028 §9: a 2xx to a refresh MUST echo Session-Expires, @@ -429,6 +437,7 @@ export namespace imsd::engine { std::string uni_, number_; int rtpPort_; bool precond_; + std::vector codecs_; // CODECS override; empty = defaults Direction direction_ = Direction::Outgoing; CallState state_ = CallState::Dialing; std::string reason_ = "outgoing"; @@ -532,7 +541,7 @@ export namespace imsd::engine { } MediaLeg ParseAnswer() const { - imsd::sdp::Answer ans = imsd::sdp::ParseAnswer(answerSdp_); + imsd::sdp::Answer ans = imsd::sdp::ParseAnswer(answerSdp_, codecs_); MediaLeg leg; leg.remoteIp = ans.ip; leg.remotePort = ans.port < 0 ? 0 : ans.port; diff --git a/interfaces/Imsd-Sdp.cppm b/interfaces/Imsd-Sdp.cppm index 471e294..723720d 100644 --- a/interfaces/Imsd-Sdp.cppm +++ b/interfaces/Imsd-Sdp.cppm @@ -5,15 +5,18 @@ /* Imsd:Sdp — SDP offer/answer building + parsing for the voice media plane. -The offer is a fixed AMR-WB (octet-aligned) + AMR + telephone-event shape -with an optional GSMA IR.92 QoS-precondition block; it is what commercial IMS -cores expect from a VoLTE UE, and its exact bytes are pinned by tests/Sdp. The -parser extracts only what the media plane needs: remote address/port, the -negotiated payload type, the codec and octet-align mode — it reads answers -to our offers and inbound offers alike (same fields either way). The codecs -it knows are the ones imsd-media can play: AMR-WB, AMR (narrowband) and -G.711 (PCMA/PCMU) — the last two are what a PSTN gateway offers when a -landline calls. BuildAnswer is the terminating side: it answers an inbound +The offer is by default an AMR-WB (octet-aligned) + AMR + telephone-event +shape with an optional GSMA IR.92 QoS-precondition block; it is what +commercial IMS cores expect from a VoLTE UE, and its exact bytes are pinned by +tests/Sdp. The parser extracts only what the media plane needs: remote +address/port, the negotiated payload type, the codec and octet-align mode — it +reads answers to our offers and inbound offers alike (same fields either way). +The codecs it knows are the ones imsd-media can play: AMR-WB, AMR (narrowband) +and G.711 (PCMA/PCMU) — the last two are what a PSTN gateway offers when a +landline calls. An explicit codec list (the daemon's CODECS override) replaces +both the offered set and the acceptance preference: it is how the narrowband +path is exercised against a network whose gateway transcodes every caller up +to AMR-WB. BuildAnswer is the terminating side: it answers an inbound offer with the offer's own payload type numbers (RFC 3264) and mirrors its octet-align mode (RFC 4867 requires both directions to use one mode). Pure std C++. @@ -49,20 +52,52 @@ namespace imsd::sdp { export namespace imsd::sdp { + // The codec names imsd-media plays, in the default acceptance order: + // wideband beats narrowband, AMR beats G.711 (a mobile-to-mobile call + // must never land on a G.711 leg just because the gateway listed it). + constexpr std::array kCodecs{"AMR-WB", "AMR", "PCMA", "PCMU"}; + + // A codec preference list from its textual form ("PCMA, amr-nb"): + // case-insensitive, spaces ignored, AMR-NB accepted for AMR, unknown + // names and repeats dropped. Empty in = empty out = the defaults. + std::vector ParseCodecList(std::string_view spec) { + std::vector out; + for (auto part : std::views::split(spec, ',')) { + std::string n; + for (char c : std::string_view(part)) + if (c != ' ' && c != '\t') n += static_cast(std::toupper(static_cast(c))); + if (n == "AMR-NB") n = "AMR"; + if (std::ranges::find(kCodecs, n) == kCodecs.end()) continue; + if (std::ranges::find(out, n) == out.end()) out.push_back(n); + } + return out; + } + struct Offer { std::string local; // connection address (v4 or v6) int rtpPort = 0; std::uint64_t sessionId = 0; // o= sess-id/version; caller randomizes bool precond = false; // IR.92 QoS preconditions block std::string currRemote = "none"; // a=curr:qos remote + std::vector codecs; // offered codecs in order; empty = AMR-WB, AMR }; // 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 // transcoding up to wideband — each with telephone-event at its own - // clock. Byte layout is pinned by tests/Sdp. + // clock. Byte layout is pinned by tests/Sdp. An explicit `codecs` list + // offers exactly those, in that order, with fixed payload types (97 + // AMR-WB, 99 AMR, 8 PCMA, 0 PCMU) and telephone-event only at the clocks + // in use. G.711 is not offered by default: a VoLTE UE offering PCMA + // deviates from stock shapes for no gain. std::string BuildOffer(const Offer& o) { + static const std::vector kDefaultOffer{"AMR-WB", "AMR"}; + const std::vector& codecs = o.codecs.empty() ? kDefaultOffer : o.codecs; + bool wb = std::ranges::find(codecs, "AMR-WB") != codecs.end(); + bool amr = std::ranges::find(codecs, "AMR") != codecs.end(); + bool nb = codecs.size() > (wb ? 1u : 0u); // anything besides AMR-WB is 8 kHz + auto pt = [](const std::string& c) { return c == "AMR-WB" ? 97 : c == "AMR" ? 99 : c == "PCMA" ? 8 : 0; }; std::string ipver = Is6(o.local) ? "IP6" : "IP4"; std::string s; s += "v=0\r\n"; @@ -70,18 +105,33 @@ export namespace imsd::sdp { s += "s=-\r\n"; s += std::format("c=IN {} {}\r\n", ipver, o.local); s += "t=0 0\r\n"; - s += std::format("m=audio {} RTP/AVP 97 99 98 100\r\n", o.rtpPort); - s += "b=AS:41\r\n"; + std::string pts; + for (const auto& c : codecs) pts += std::format(" {}", pt(c)); + if (wb) pts += " 98"; + if (nb) pts += " 100"; + 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"; s += "b=RR:1536\r\n"; - s += "a=rtpmap:97 AMR-WB/16000/1\r\n"; - s += "a=fmtp:97 octet-align=1;mode-change-capability=2;max-red=0\r\n"; - s += "a=rtpmap:99 AMR/8000/1\r\n"; - s += "a=fmtp:99 octet-align=1;mode-change-capability=2;max-red=0\r\n"; - s += "a=rtpmap:98 telephone-event/16000\r\n"; - s += "a=fmtp:98 0-15\r\n"; - s += "a=rtpmap:100 telephone-event/8000\r\n"; - s += "a=fmtp:100 0-15\r\n"; + for (const auto& c : codecs) { + if (c == "AMR-WB") { + s += "a=rtpmap:97 AMR-WB/16000/1\r\n"; + s += "a=fmtp:97 octet-align=1;mode-change-capability=2;max-red=0\r\n"; + } else if (c == "AMR") { + s += "a=rtpmap:99 AMR/8000/1\r\n"; + s += "a=fmtp:99 octet-align=1;mode-change-capability=2;max-red=0\r\n"; + } else { + s += std::format("a=rtpmap:{} {}/8000/1\r\n", pt(c), c); + } + } + if (wb) { + s += "a=rtpmap:98 telephone-event/16000\r\n"; + s += "a=fmtp:98 0-15\r\n"; + } + if (nb) { + s += "a=rtpmap:100 telephone-event/8000\r\n"; + s += "a=fmtp:100 0-15\r\n"; + } s += "a=ptime:20\r\n"; s += "a=maxptime:240\r\n"; if (o.precond) { @@ -144,16 +194,16 @@ export namespace imsd::sdp { } // Payload-type preference: AMR-WB with octet-align=1 > AMR-WB > AMR with - // octet-align=1 > AMR > PCMA > PCMU. Octet-aligned wins within a codec - // because it is the media leg's native, call-proven format - // (bandwidth-efficient is supported but was the s56 static-audio + // octet-align=1 > AMR > PCMA > PCMU (kCodecs order). Octet-aligned wins + // within a codec because it is the media leg's native, call-proven + // format (bandwidth-efficient is supported but was the s56 static-audio // culprit) — when the peer offers both variants, taking the - // octet-aligned pt keeps real calls on the battle-tested path. Wideband - // beats narrowband, AMR beats G.711 (a mobile-to-mobile call must never - // land on a G.711 leg just because the gateway listed it). With no - // playable codec at all, the first pt is used and codec comes back EMPTY - // — pretending otherwise would defeat codec checks upstream. - Answer ParseAnswer(std::string_view body) { + // octet-aligned pt keeps real calls on the battle-tested path. A + // non-empty `prefs` replaces the codec order (and restricts acceptance + // to the codecs named) — the CODECS override. With no acceptable codec + // at all, the first pt is used and codec comes back EMPTY — pretending + // otherwise would defeat codec checks upstream. + Answer ParseAnswer(std::string_view body, std::span prefs = {}) { Answer a; if (auto at = Find(body, "c=IN IP6 ")) a.ip = TokenAt(body, *at + 9); else if (auto at4 = Find(body, "c=IN IP4 ")) a.ip = TokenAt(body, *at4 + 9); @@ -184,39 +234,29 @@ export namespace imsd::sdp { std::string_view line = body.substr(*fm, lineEnd == std::string_view::npos ? std::string_view::npos : lineEnd - *fm); return line.contains("octet-align=1"); }; - int wbOa = -1; - int wb = -1; - int nbOa = -1; - int nb = -1; - int pcma = -1; - int pcmu = -1; + struct Seen { int first = -1; int oa = -1; }; + std::map seen; // codec name -> first pt, first octet-aligned pt for (int p : pts) { std::string name = CodecName(body, p); - if (name == "AMR-WB") { - if (wb == -1) wb = p; - if (wbOa == -1 && hasOctetAlign(p)) wbOa = p; - } else if (name == "AMR") { - if (nb == -1) nb = p; - if (nbOa == -1 && hasOctetAlign(p)) nbOa = p; - } else if (name == "PCMA") { - if (pcma == -1) pcma = p; - } else if (name == "PCMU") { - if (pcmu == -1) pcmu = p; - } + if (std::ranges::find(kCodecs, name) == kCodecs.end()) continue; + Seen& s = seen[name]; + if (s.first == -1) s.first = p; + if (s.oa == -1 && name.starts_with("AMR") && hasOctetAlign(p)) s.oa = p; } - if (wbOa != -1 || wb != -1) { - a.payloadType = wbOa != -1 ? wbOa : wb; - a.codec = "AMR-WB"; - } else if (nbOa != -1 || nb != -1) { - a.payloadType = nbOa != -1 ? nbOa : nb; - a.codec = "AMR"; - } else if (pcma != -1) { - a.payloadType = pcma; - a.codec = "PCMA"; - } else if (pcmu != -1) { - a.payloadType = pcmu; - a.codec = "PCMU"; + auto pick = [&](std::string_view name) { + auto it = seen.find(std::string(name)); + if (it == seen.end()) return false; + a.payloadType = it->second.oa != -1 ? it->second.oa : it->second.first; + a.codec = std::string(name); + return true; + }; + bool found = false; + if (prefs.empty()) { + for (std::string_view n : kCodecs) if ((found = pick(n))) break; } else { + for (const std::string& n : prefs) if ((found = pick(n))) break; + } + if (!found) { a.payloadType = pts.front(); a.codec.clear(); } diff --git a/tests/Engine/main.cpp b/tests/Engine/main.cpp index b835679..bf19a76 100644 --- a/tests/Engine/main.cpp +++ b/tests/Engine/main.cpp @@ -772,6 +772,62 @@ int main() { Check(m.Terminated(), "abandoned machine is terminated"); } + // ---- CODECS override, terminating side: the transcoding gateway's + // mixed offer (KPN 2026-09-08) answered with G.711 A-law because the + // list says so — the on-air narrowband bench path + { + imsd::util::Rng rng(51); + std::string sdp = + "v=0\r\no=LucentPCSF 1 1 IN IP6 2001:db8::309\r\ns=-\r\n" + "c=IN IP6 2001:db8::309\r\nt=0 0\r\n" + "m=audio 41166 RTP/AVP 8 0 100 96 101 13 106\r\n" + "a=rtpmap:8 PCMA/8000\r\na=rtpmap:0 PCMU/8000\r\n" + "a=rtpmap:100 AMR/8000\r\na=fmtp:100 mode-change-period=2\r\n" + "a=rtpmap:96 AMR-WB/16000\r\na=fmtp:96 mode-change-period=2\r\n" + "a=rtpmap:101 telephone-event/8000\r\na=fmtp:101 0-15\r\n" + "a=rtpmap:106 telephone-event/16000\r\na=fmtp:106 0-15\r\n" + "a=sendrecv\r\na=ptime:20\r\na=maxptime:60\r\n"; + CallMachine m(c, rng, "ims-call-20", IncomingInvite{MtInviteSdp("mt-kpn-pcma", sdp)}, 50004, {"PCMA"}); + auto ai = m.OnInvite(); + Check(Responded(ai, "SIP/2.0 180 Ringing") != nullptr, "CODECS=PCMA: mixed offer rings"); + auto aa = m.OnAccept(); + const Action* ok = Responded(aa, "SIP/2.0 200 OK"); + Check(ok && ok->text.contains("m=audio 50004 RTP/AVP 8 101\r\n"), "CODECS=PCMA: answer is PCMA + narrowband DTMF at the offer's pts"); + Check(ok && ok->text.contains("a=rtpmap:8 PCMA/8000/1\r\n") && !ok->text.contains("AMR"), "CODECS=PCMA: no AMR in the answer"); + const Action* med = Find(aa, Action::Type::StartMedia); + Check(med && med->media.codec == "PCMA" && med->media.payloadType == 8 && !med->media.octetAlign, "CODECS=PCMA: media leg starts G.711 A-law"); + + // same offer, CODECS=AMR: narrowband AMR, bandwidth-efficient as offered + imsd::util::Rng rng2(52); + CallMachine m2(c, rng2, "ims-call-21", IncomingInvite{MtInviteSdp("mt-kpn-amr", sdp)}, 50004, {"AMR"}); + m2.OnInvite(); + auto aa2 = m2.OnAccept(); + const Action* ok2 = Responded(aa2, "SIP/2.0 200 OK"); + Check(ok2 && ok2->text.contains("m=audio 50004 RTP/AVP 100 101\r\n") && !ok2->text.contains("octet-align"), "CODECS=AMR: answer is AMR-NB BE + narrowband DTMF"); + const Action* med2 = Find(aa2, Action::Type::StartMedia); + Check(med2 && med2->media.codec == "AMR" && med2->media.payloadType == 100 && !med2->media.octetAlign, "CODECS=AMR: media leg starts AMR-NB BE"); + + // same offer, no override: unchanged — AMR-WB + imsd::util::Rng rng3(53); + CallMachine m3(c, rng3, "ims-call-22", IncomingInvite{MtInviteSdp("mt-kpn-default", sdp)}, 50004); + m3.OnInvite(); + auto aa3 = m3.OnAccept(); + const Action* med3 = Find(aa3, Action::Type::StartMedia); + Check(med3 && med3->media.codec == "AMR-WB" && med3->media.payloadType == 96, "no override: the same offer still lands on AMR-WB"); + } + + // ---- CODECS override, originating side: the INVITE offers only what the + // list names (G.711 = a landline-shaped offer toward the network) + { + imsd::util::Rng rng(54); + CallMachine m(c, rng, "ims-call-23", "0783690354", 50004, false, false, {"PCMA", "PCMU"}); + auto start = m.Start(); + const Action* inv = Sent(start, "INVITE "); + Check(inv && inv->text.contains("m=audio 50004 RTP/AVP 8 0 100\r\n"), "CODECS=PCMA,PCMU: INVITE offers G.711 only"); + Check(inv && !inv->text.contains("AMR"), "CODECS=PCMA,PCMU: no AMR in the offer"); + Check(inv && inv->text.contains("a=rtpmap:100 telephone-event/8000\r\n") && !inv->text.contains("telephone-event/16000"), "CODECS=PCMA,PCMU: narrowband DTMF only"); + } + if (Failures == 0) std::println("Engine: all tests passed"); return Failures; } diff --git a/tests/Sdp/main.cpp b/tests/Sdp/main.cpp index 5a70cc9..e7f68ca 100644 --- a/tests/Sdp/main.cpp +++ b/tests/Sdp/main.cpp @@ -294,6 +294,93 @@ int main() { Check(sdp.contains("a=rtpmap:0 PCMU/8000/1\r\n") && !sdp.contains("fmtp:0"), "PCMU answer has no fmtp"); } + // ---- CODECS override: the offer of a real interconnect gateway that + // transcodes up (KPN, 2026-09-08: a G.711-only caller arrives with + // AMR-WB BE added). Default picks AMR-WB; a preference list picks + // what it names, in its order, and nothing outside it. + { + std::string kpn = + "c=IN IP6 2001:db8::309\r\n" + "m=audio 41166 RTP/AVP 8 0 100 96 101 13 106\r\n" + "a=rtpmap:8 PCMA/8000\r\na=rtpmap:0 PCMU/8000\r\n" + "a=rtpmap:100 AMR/8000\r\na=fmtp:100 mode-change-period=2\r\n" + "a=rtpmap:96 AMR-WB/16000\r\na=fmtp:96 mode-change-period=2\r\n" + "a=rtpmap:101 telephone-event/8000\r\na=fmtp:101 0-15\r\n" + "a=rtpmap:106 telephone-event/16000\r\na=fmtp:106 0-15\r\n" + "a=sendrecv\r\na=ptime:20\r\na=maxptime:60\r\n"; + Answer d = ParseAnswer(kpn); + Check(d.payloadType == 96 && d.codec == "AMR-WB" && !d.octetAlign, "transcoding gateway: default takes AMR-WB BE"); + std::vector pcma{"PCMA"}; + Answer a = ParseAnswer(kpn, pcma); + Check(a.payloadType == 8 && a.codec == "PCMA" && !a.octetAlign, "CODECS=PCMA takes G.711 A-law out of a mixed offer"); + std::vector ulawFirst{"PCMU", "PCMA"}; + Answer u = ParseAnswer(kpn, ulawFirst); + Check(u.payloadType == 0 && u.codec == "PCMU", "CODECS order wins over the offer's order"); + std::vector nb{"AMR"}; + Answer n = ParseAnswer(kpn, nb); + Check(n.payloadType == 100 && n.codec == "AMR" && !n.octetAlign, "CODECS=AMR takes narrowband AMR, BE as offered"); + std::vector wb{"AMR-WB"}; + Answer w = ParseAnswer(kpn, wb); + Check(w.payloadType == 96 && w.codec == "AMR-WB", "CODECS=AMR-WB matches the default here"); + Answer g = ParseAnswer("c=IN IP4 192.0.2.1\r\n" "m=audio 5000 RTP/AVP 8 0 101\r\n" "a=rtpmap:101 telephone-event/8000\r\n", wb); + Check(g.codec.empty() && g.payloadType == 8, "CODECS=AMR-WB refuses a G.711-only offer (codec empty, first pt)"); + Check(g.port == 5000 && g.ip == "192.0.2.1", "address/port still parsed when no codec is acceptable"); + } + + // ---- ParseCodecList: case, spaces, AMR-NB alias, unknowns, repeats + { + auto l = ParseCodecList("pcma, amr-nb ,AMR-WB,g729,PCMA,,PCMU"); + Check(l.size() == 4 && l[0] == "PCMA" && l[1] == "AMR" && l[2] == "AMR-WB" && l[3] == "PCMU", "codec list normalised: PCMA,AMR,AMR-WB,PCMU"); + Check(ParseCodecList("").empty(), "empty spec = no override"); + Check(ParseCodecList("opus, g722").empty(), "only unknown names = no override"); + } + + // ---- BuildOffer with an explicit codec list: G.711 only (landline + // shape, for MO tests against a transcoding network) + { + std::string sdp = BuildOffer({ + .local = "192.0.2.1", + .rtpPort = 22222, + .sessionId = 1234567, + .codecs = {"PCMA", "PCMU"}, + }); + std::string expected = + "v=0\r\n" + "o=- 1234567 1234567 IN IP4 192.0.2.1\r\n" + "s=-\r\n" + "c=IN IP4 192.0.2.1\r\n" + "t=0 0\r\n" + "m=audio 22222 RTP/AVP 8 0 100\r\n" + "b=AS:80\r\n" + "b=RS:512\r\n" + "b=RR:1536\r\n" + "a=rtpmap:8 PCMA/8000/1\r\n" + "a=rtpmap:0 PCMU/8000/1\r\n" + "a=rtpmap:100 telephone-event/8000\r\n" + "a=fmtp:100 0-15\r\n" + "a=ptime:20\r\n" + "a=maxptime:240\r\n" + "a=sendrecv\r\n"; + Check(sdp == expected, "G.711-only offer byte-exact"); + } + + // ---- BuildOffer, AMR narrowband only: pt 99, narrowband DTMF only, AS 30 + { + std::string sdp = BuildOffer({.local = "2001:db8::1", .rtpPort = 50004, .sessionId = 7, .codecs = {"AMR"}}); + Check(sdp.contains("m=audio 50004 RTP/AVP 99 100\r\n"), "AMR-only offer m-line"); + Check(sdp.contains("b=AS:30\r\n"), "AMR-only offer bandwidth"); + Check(sdp.contains("a=rtpmap:99 AMR/8000/1\r\na=fmtp:99 octet-align=1;mode-change-capability=2;max-red=0\r\n"), "AMR-only offer rtpmap+fmtp"); + Check(!sdp.contains("16000"), "no wideband telephone-event without AMR-WB"); + } + + // ---- BuildOffer, explicit default order == the pinned default bytes + { + Offer o{.local = "2001:db8::1", .rtpPort = 50004, .sessionId = 7}; + Offer e = o; + e.codecs = {"AMR-WB", "AMR"}; + Check(BuildOffer(o) == BuildOffer(e), "explicit AMR-WB,AMR list is byte-identical to the default offer"); + } + if (Failures == 0) std::println("Sdp: all tests passed"); return Failures; } From c5cba2e7ff4f6aa7bddb271845047f959f7a0907 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Tue, 8 Sep 2026 19:51:24 +0200 Subject: [PATCH 3/4] tests: dial a documentation-style number in the CODECS offer scenario --- tests/Engine/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Engine/main.cpp b/tests/Engine/main.cpp index bf19a76..2ad2f25 100644 --- a/tests/Engine/main.cpp +++ b/tests/Engine/main.cpp @@ -820,7 +820,7 @@ int main() { // list names (G.711 = a landline-shaped offer toward the network) { imsd::util::Rng rng(54); - CallMachine m(c, rng, "ims-call-23", "0783690354", 50004, false, false, {"PCMA", "PCMU"}); + CallMachine m(c, rng, "ims-call-23", "0101234567", 50004, false, false, {"PCMA", "PCMU"}); auto start = m.Start(); const Action* inv = Sent(start, "INVITE "); Check(inv && inv->text.contains("m=audio 50004 RTP/AVP 8 0 100\r\n"), "CODECS=PCMA,PCMU: INVITE offers G.711 only"); From 69d8b2a716d054164b2c7d1ab5b75b469a3036ff Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Tue, 8 Sep 2026 19:52:02 +0200 Subject: [PATCH 4/4] imsd 0.3.2 Landline callers ring. The media leg plays AMR narrowband and G.711 (PCMA/PCMU) as well as AMR-WB, the engine accepts any of them from an inbound offer (case-insensitive names, static G.711 payload types) and names the offered codecs when it still has to refuse with 488. An inbound INVITE is validated before it is announced to the UI, so a refused call no longer flashes the dialer. Our own offer lists AMR narrowband after AMR-WB, so a call to a landline no longer depends on the network transcoding up. CODECS= restricts and orders the codecs offered and accepted, for bench work against networks that transcode every caller to AMR-WB. Bench-verified on KPN 2026-09-08: PCMA and AMR-NB in both directions, the default incoming path still AMR-WB, a mobile caller's offer still answered with octet-aligned AMR-WB. Packaging: cross-compiled for the FP6's Cortex-A520/A720 cores; the aport is gone, fp6-img installs this repo's published apk. --- implementations/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/implementations/main.cpp b/implementations/main.cpp index 559569a..8e4f6bb 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -41,7 +41,7 @@ import Imsd; namespace { // ---- static config (env-overridable, same knobs as imsd.py) --------------- -constexpr const char* Version = "0.3.1"; +constexpr const char* Version = "0.3.2"; constexpr const char* BusName = "net.catcrafts.IMS1"; constexpr const char* ObjPath = "/net/catcrafts/IMS1"; constexpr const char* Iface = "net.catcrafts.IMS1";