Add slmodem V.32bis/V.34 modem engine (2400 → 33.6k) - #27
Merged
Conversation
`modem_connect_tone` in spandsp's `v8_parms_t` is a `MODEM_CONNECT_TONES_*` enum, not a boolean. The worker set it to `true` (== 1 == MODEM_CONNECT_TONES_FAX_CNG), so as the V.8 answerer it emitted the fax calling tone (1100 Hz, 0.5s on / 3s off) instead of a modem answer tone. Calling modems never received a valid answer tone, so V.8 always failed and the worker fell back to V.21, holding a 1650 Hz mark the caller could not train against. Captured RTP confirmed the 1100 Hz CNG cadence outbound and near-total silence inbound (the modem never engaged). Set it to MODEM_CONNECT_TONES_ANSAM_PR (2100 Hz ANSam with phase reversals), the standard V.8 answer tone. Verified offline (worker now emits continuous 2100 Hz ANSam) and on a live Windows dial-up call: V.8 now succeeds and selects V.22bis at 2400 baud (v8-v22bis-selected) instead of failing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
V.22bis is a synchronous modem, but Windows dial-up carries asynchronous PPP (8N1). The worker fed the raw V.22bis bit stream straight into an 8-bit accumulator with no start/stop handling, so characters never aligned to PPP HDLC flags and `decodedBytes` stayed 0 (PPP could never come up). The V.21 path already handled this via spandsp's FSK async framing; V.22bis did not. Insert spandsp async_rx/async_tx (use_v14=TRUE) between the V.22bis bits and the existing HDLC byte framing: rx: v22bis bits -> async_rx (strip start/stop) -> hdlc_rx_byte -> pty tx: pty -> hdlc bytes -> async_tx (add start/stop) -> v22bis bits Also fix v22bis_status: spandsp SIG_STATUS_* codes are all negative, so the old `status < 0` test could never report carrier-up or training-succeeded. Decode the specific codes instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Several fixes needed to carry real async PPP and to negotiate cleanly: - Transparent data path: pppd runs async HDLC itself, so stop re-framing. async_put_byte() now writes the recovered octet stream (0x7E flags, byte-stuffing, FCS intact) straight to the pty, and poll_pty() queues the raw pty octets for V.14 transmission instead of HDLC-framing them. Without this, pppd saw de-framed payloads with no frame delimiters and LCP never ran. - V.8 handoff use-after-free: v8_result() runs synchronously inside v8_rx(), which keeps using the v8 context afterwards. Freeing v8 and allocating the v22bis/async state there corrupted that context (the new state often reused the freed block). Now the callback only records the decision; main() does the v8_free()/data-modem start after v8_rx/v8_tx return (per spandsp's tests/v8_tests.c pattern). Verified with a spandsp loopback harness. - V.8 modulation choice now masks the caller's offer with what we advertised, so we don't get pulled into V.22bis when only V.21 was offered. - Forced-mode start (SIPFAX_MODEM_START_MODE) defers entry to the first input frame, so the operator side is wired before pty-opened fires (start-up race that intermittently prevented pppd from starting).
pppd has no command-line option to select a secrets file; it always reads
/etc/ppp/{chap,pap}-secrets. The supervisor wrapped pppd in a shell that
appended a bogus `chap-secrets <tmpfile>` option, which pppd rejects
("unrecognized option 'chap-secrets'"), so it exited before LCP and the
caller saw error 721.
Render the per-call credentials to the standard path before launch (single
active call), spawn pppd directly (no wrapper, no bogus option), and clear
the file in place on teardown -- we own the file but not the root-owned
/etc/ppp directory, so truncate rather than unlink.
Standalone spandsp loopback (caller <-> answerer) that runs full V.8 negotiation and the V.22bis handoff, in both forced and V.8 modes, with an optional echo model. Used to prove the handoff logic is correct independent of the real modem/channel. cc -O2 -o v8handoff_test tests/v8handoff_test.c -lspandsp -lm ./v8handoff_test forced # PASS ./v8handoff_test v8 # PASS (handoff trains s2s) ECHO_GAIN=0.3 ECHO_DELAY=64 ./v8handoff_test v8 # PASS with echo
PPP never worked on the first VM because the runtime was never set up: the ppp package was absent, the cloud kernel has no PPP modules, and the hardened systemd unit blocks setuid pppd. Add the systemd drop-in that grants pppd the privileges it needs (without privileging the node process) and a runbook section covering the ppp package, generic kernel, the drop-in, and the pre-created /etc/ppp secrets files. Also document why the live service forces V.22bis instead of relying on V.8 negotiation.
Adds an optional higher-speed modem engine that drives the free SmartLink slmodem datapump, reaching V.34 33,600 bps on real calls (vs V.22bis 2400 on the spandsp engine) over the existing G.711/RTP -> pppd pipeline. - vendor/slmodem-bridge/bridge.c: native bridge converting SIPfax G.711 stdio <-> slmodemd's 9600 Hz S16 audio, driving the AT answer/originate sequence and emitting pty-opened for pppd. Includes a polyphase FIR resampler (fc=3950 Hz, 64 taps) flat to +/-0 dB through 3700 Hz; a naive linear resampler drooped ~6 dB across the upper band and capped the link at 9600. test_resamp.c is the offline SNR/frequency-response harness used to choose the filter. - vendor/slmodem/: vendoring of slmodemd + the gratis dsplibs.o datapump (pinned SHA-256) and 0001-...patch, which switches slmodemd's audio transport to a direct AF_UNIX connect, adds SIPFAX_TTY_LINK, and only drops privileges when started as root (so it runs as the unprivileged service user). - src/index.js: SIPFAX_MODEM_ENGINE=slmodem selects the bridge; default stays spandsp so the existing V.21/V.22bis path is unchanged. - deploy/README.md: multilib build notes (the datapump is 32-bit x86). Validated end-to-end: real vintage-machine calls at 14400 (V.32bis) and 33600 (V.34); two-modem loopback passes data both ways. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
renderChapSecrets wrote to a hardcoded /etc/ppp path, so the pppd-supervisor
tests failed with EACCES on CI runners. Add a secretsDir option to
PppdSupervisor (default /etc/ppp) and have the tests inject a writable temp
dir.
Unblocking the secrets write surfaced two stale assertions in the same test:
it expected a /bin/sh-wrapper spawn with a session-dir arg and per-pid
secrets in the session dir. The supervisor spawns pppd directly (the working
production path) and writes <secretsDir>/{chap,pap}-secrets, so the test is
updated to assert the actual behavior. All 38 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an optional higher-speed modem engine (
SIPFAX_MODEM_ENGINE=slmodem) that drives the free SmartLink slmodem datapump, reaching V.34 33,600 bps on real vintage-machine calls — up from V.22bis 2400 on the spandsp engine — over SIPfax's existing G.711/RTP →pppdpipeline. Default engine staysspandsp, so the V.21/V.22bis path is unchanged.What's added
vendor/slmodem-bridge/bridge.c— native bridge: SIPfax G.711 stdio ↔ slmodemd 9600 Hz S16 audio; drives the AT answer/originate sequence; emitspty-openedforpppd. Includes a polyphase FIR resampler (fc=3950 Hz, 64 taps). The linear resampler it replaced drooped ~6 dB across the upper voiceband and capped the link at 9600.vendor/slmodem-bridge/test_resamp.c— offline resampler SNR / frequency-response harness used to choose the filter (objective, no live-call guessing).vendor/slmodem/— vendoring of slmodemd + the gratisdsplibs.odatapump (pinned SHA-256) and0001-…patch: direct AF_UNIX audio transport,SIPFAX_TTY_LINK, and run-unprivileged.src/index.js—SIPFAX_MODEM_ENGINEselection.deploy/README.md— multilib build notes (the datapump is 32-bit x86).Validation
Enable
Build (needs
gcc-multilib libc6-dev-i386):./vendor/slmodem/fetch.sh && make -C vendor/slmodem/slmodemd && make -C vendor/slmodem-bridgeLicensing
dsplibs.ois gratis + redistributable but closed-source (32-bit x86); provenance pinned invendor/slmodem/README.md. Bridge/patch derive from D-Modem/slmodem (GPL-2.0).🤖 Generated with Claude Code