A multi-line answering soft-modem bridge for Asterisk over AudioSocket
  • C 99.3%
  • Makefile 0.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
alex_sze 708dc62d0d
Document timer defaults and the helper environment inheritance boundary
The README described receive-driven transmit pacing as unconditional, but helper_main selects it only when TX_CLOCK is rx and reads prefill only in that mode. Make the default and conditional behavior explicit.

Audio settings must reach slmodemd before it forks and execs the helper. A later broker export does not update that inherited environment. Explain separate role and execution-identity checks; no bridge source, build or service behavior changes.
2026-09-13 09:40:00 -04:00
.gitignore Initial commit: slmbridge, extracted from LiteNet Connectivity Solutions 2026-08-27 19:31:21 -04:00
LICENSE Initial commit: slmbridge, extracted from LiteNet Connectivity Solutions 2026-08-27 19:31:21 -04:00
Makefile Initial commit: slmbridge, extracted from LiteNet Connectivity Solutions 2026-08-27 19:31:21 -04:00
README.md Document timer defaults and the helper environment inheritance boundary 2026-09-13 09:40:00 -04:00
slmbridge.c Sync slmbridge.c and slmbridge_rstest.c from the LiteNet tree 2026-09-13 04:41:21 -04:00
slmbridge_rstest.c Sync slmbridge.c and slmbridge_rstest.c from the LiteNet tree 2026-09-13 04:41:21 -04:00

slmbridge

A multi-line answering soft-modem bridge for Asterisk over AudioSocket.

AI-generated code — use at your own risk. The entirety of slmbridge (including slmbridge.c, slmbridge_rstest.c, and this documentation) was written by a generative AI system, not audited line-by-line by a human engineer with domain expertise in telephony, DSP, or C systems programming beyond the review that produced this disclaimer. It has not undergone a professional security audit and has not been tested against adversarial or malformed input.

This code interacts with subprocess execution, raw sockets, and file descriptors passed across process boundaries — categories of code where bugs can have consequences beyond a crash (resource exhaustion, unexpected subprocess behavior, mishandling of untrusted network input). It is provided strictly "as is," with no warranty of correctness, safety, security, or fitness for any purpose. By using, deploying, modifying, or distributing this software, you assume all risk of any flaws, defects, vulnerabilities, data loss, service disruption, or other harm that may result — direct or indirect. Do not deploy this in a production or security-sensitive environment without independent review by someone qualified to evaluate it. See LICENSE for the full legal disclaimer of warranty and liability.

slmbridge lets slmodemd — Smart Link's Linux softmodem daemon — answer incoming calls delivered to Asterisk as AudioSocket streams, and run several lines of it at once on a single machine.

The problem

slmodemd's DSP is the only open, working fast-modem implementation around (V.32, V.32bis, V.34, V.90, V.92). The reference project that reaches it over SIP, D-Modem, pairs it with a PJSIP process that can only place calls — its source has no incoming-call handler at all. For anything that needs to answer the phone, that's fatal.

slmbridge replaces that PJSIP process outright instead of patching answer support into it. Asterisk already answers the call, already owns the SIP stack, and already hands audio to arbitrary programs over AudioSocket, so "cannot answer" simply stops being a problem to solve.

It also fixes two problems that sit underneath that one:

  • The 9,600 Hz / 8,000 Hz mismatch. slmodemd's DSP defaults to a 9,600 Hz media rate; AudioSocket delivers 8,000 Hz. Left alone, that's a resampler sitting directly in the modem's signal path on every frame in both directions. slmbridge runs a 6:5 polyphase resampler at the boundary instead, and the DSP itself can be built natively for 8,000 Hz if you have the coefficient tables to support it.

    The self-test asserts flatness across 300–3,900 Hz, not 300–3,400. That detail is the whole point: an earlier version asserted 300–3,400 and passed while the filter it was testing was 7 dB down at 3,888 Hz — the top of the 3,429-baud V.34 constellation. A test whose passband stops below the signal band cannot see the defect it exists to catch. If a future rate needs more than 3,900 Hz, move that number first and let it fail.

    Three filter profiles are selectable at runtime with SLMBRIDGE_RS_PROFILE: the default 96/112-tap Kaiser pair (flat to 3,900), legacy (the original 32-tap Blackman pair, kept as an exact rollback), and narrow, which deliberately band-limits the receive direction to SLMBRIDGE_RS_FC (default 3,800 Hz). narrow is an experiment, not a recommendation — it intentionally fails the transparency check, which is why the self-test reports it rather than asserting on it.

  • Clock drift. slmodemd has no clock of its own — it advances at whatever rate its device delivers audio. An implementation that reads at one clock and writes at an independent one lets the two drift apart, which shows up as accumulating latency and eventually dropped/repeated frames. With SLMBRIDGE_TX_CLOCK=rx, the audio helper paces one output frame per received audio frame. The default uses timer pacing. The setting must reach the helper process that performs the audio relay.

How it works

slmbridge is two small processes, not one:

  • The broker listens for AudioSocket connections from Asterisk and forks a slmodemd instance per call.
  • The helper is what slmodemd execs as its --exec answer-side helper. It inherits the AudioSocket connection from the broker and relays raw PCM between slmodemd's socketpair and AudioSocket's 20 ms framing, resampling as it goes.
Asterisk ──AudioSocket (TCP)──▶ broker ──forks──▶ slmodemd
                                   │                  │
                                   └──inherited fd──▶ helper (this binary)
                                                       │
                                          raw PCM over socketpair

Because slmodemd hardcodes device 0 internally, running more than one instance needs each one told apart — slmbridge handles the multi-line bookkeeping so a pool of lines can run concurrently on one host without fighting over the same device node.

Both carriers of a V.34 call

A V.34 call negotiates its two directions independently, and they are often not the same rate. A modem's CONNECT result code reports only its own side, so a single number describes half the link.

slmodemd reports the answering side's receive rate in CONNECT — the rate the caller is transmitting at, i.e. upstream from the subscriber. Its transmit rate, the one carrying data down to the subscriber, is reported separately on a TxRate: line, and only when bit 2 of S-register 70 is set:

AT&A4              (S70 bit 2; do this after ATZ, which restores the defaults)

With that set, slmbridge parses both and writes them to the session file:

$ cat /run/litenet/sessions/<peer-ip>
31200
down=33600 up=31200

Line 1 is unchanged and always will be. It is the CONNECT figure, and existing readers parse the file as a bare integer. Anything added goes on a later line.

If you consume that file, take the first line only. A whole-file digit-strip (tr -dc '0-9') silently concatenates the two lines into 3120033600 — a number no reader can reject, because any integer is a plausible rate to code that only bounds it below. That bug shipped here, in the PPP ip-up hook, on the day the second line was added.

Without AT&A4 the second line is simply absent, which is the behaviour every release before this one had.

Environment

Everything slmbridge reads. Unset means the default in the right-hand column.

Variable Default What it does
SLMBRIDGE_ASOCK_PATH — AudioSocket listen path, broker mode
SLMBRIDGE_TTY — the pty slmodemd created, handed to pppd at CONNECT
SLMBRIDGE_PPPD — pppd arguments; unset means do not start pppd
SLMBRIDGE_NETNS — network namespace to run pppd in
SLMBRIDGE_SESSION_IP — names the session file; unset means write none
SLMBRIDGE_AT_INIT ATZ;ATX4;AT+MS=132,1,1200,14400;AT+MS? sent once at startup, ;-separated
SLMBRIDGE_AT_CMD ATA sent per call to answer; at most 62 bytes before the added CR
SLMBRIDGE_DSP_RATE 8000 the DSP's sample rate. Resamples only when this is not 8000
SLMBRIDGE_TX_CLOCK unset: timer pacing rx paces transmit off the receive clock
SLMBRIDGE_RX_PREFILL 2 initial buffered frames when TX_CLOCK=rx
SLMBRIDGE_RING_FRAMES 16, or 64 when resampling ring size in 20 ms frames
SLMBRIDGE_ELASTIC — elastic buffering between the two clocks
SLMBRIDGE_RS_PROFILE — resampler profile, see the resampler notes
SLMBRIDGE_RS_FC — resampler cutoff in Hz
SLMBRIDGE_RS_DUMP — dump resampler coefficients and exit; diagnostics only

Every variable is also read under the older LITENET_ prefix when the SLMBRIDGE_ spelling is unset (bridge_env() in slmbridge.c). The bridge was extracted from a deployment that configures it that way, and the fallback is deliberate and permanent; new configuration should use SLMBRIDGE_. The one variable the bridge sets, the pty handed to pppd, is exported under both names.

The broker and audio helper are separate processes. slmodemd forks and executes the helper, so audio settings such as clock, prefill and DSP rate must be passed to slmodemd before it starts. Exporting them later for the broker does not change the modem's existing environment or its future helper's inheritance. Verify each process role separately: a broker showing RX clocking does not establish that the helper is using it. Likewise, verify that the helper's actual unprivileged identity can execute the configured binary.

The answer command is prepared once, before the broker opens listeners or the modem. Commands of 63 bytes or more fail startup explicitly: the 64-byte buffer must also contain the added CR and terminating NUL. Earlier versions could send NUL in place of CR at the boundary or pass an out-of-bounds length to write(). The default ATA, empty configuration and commands through 62 bytes retain their existing behavior. This guard does not validate AT syntax or repair short writes.

make check exercises the shipping formatter for every length from 0 through 4096, including exact CR/NUL placement and surrounding memory canaries, alongside the resampler checks. The guard was additionally exercised through inert broker paths under AddressSanitizer and UndefinedBehaviorSanitizer on macOS and Linux. These are memory-safety and command-boundary checks, not physical modem speed or reliability evidence.

Building

make          # builds slmbridge
make check    # tests the answer-command formatter and resampler

Requires a C compiler and pthread/libm. No other dependencies — this binary does no DSP of its own and links against nothing GPL or proprietary; it talks to slmodemd as a subprocess over a pipe, not as a library.

You'll separately need a working slmodemd build (from Smart Link's sl-modem-daemon sources) and a SIP/Asterisk setup that exposes calls to it over AudioSocket via app_audiosocket/res_audiosocket. slmodemd's DSP component itself ships under a proprietary license from Smart Link and is not included here — see their own licensing terms before building or distributing it.

Known limitation: what it transmits when the ring runs dry

When there is no slmodemd output ready to send, slmbridge fills the frame with a constant — the last sample value, or zero — and counts it as starved in the exit line. That is deliberate and it is about AudioSocket's 2-second liveness timeout, which a constant satisfies. As a signal it is the worst available filler: a whole frame of DC, all energy at 0 Hz, an amplitude step at each edge, and nothing for the far end's carrier or timing recovery to track through.

Measured upstream on a V.34 line, starvation is strongly associated with handshake failure — 0.174 events/second on calls that failed against 0.059 on calls that connected, with every failure above 0.083/s (Mann-Whitney p < 0.00001, n = 72). The absolute counts are small, 1–2 per call, which is the point: what matters is where the frame lands. One DC frame in data mode is absorbed by error control; the same frame during V.34 Phase 3/4 training breaks the far end's acquisition.

For V.32bis this is not a practical problem and the code is deliberately left alone: at 2400 baud a lost 20 ms is ridden through and LAPM retransmits, and lines running that way log zero HDLC frame errors across a day.

Replacing the fill with a repeat of the last good frame is the obvious idea. It was tried and it is not a fix — 13/16 versus 10/16 connects (Fisher p = 0.433), and the build carrying it showed an unexplained regression to lower connect speeds. If you are going to attempt this, start from those numbers rather than from the intuition.

Watch starved= in the helper's exit line. If it is more than one or two per call, the ring is being drained faster than slmodemd fills it, and SLMBRIDGE_RX_PREFILL is the dial — at the cost of path delay, which on a V.34 link is itself expensive.

Attribution

slmbridge doesn't link against or vendor any of the following — it just talks to them as a subprocess or across a call — but it exists to plug the gap between them, and none of this works without their prior art:

Project Author License
slmodemd Smart Link Ltd (Debian sl-modem-daemon packaging) BSD-3-Clause (packaging); DSP itself is Smart Link's proprietary blob
D-Modem Stroz Friedberg / Aon plc GPL-2.0
AudioSocket Seán C McCord, CyCore Systems GPL-2.0
Asterisk Sangoma / Digium and the Asterisk project GPL-2.0

slmbridge itself was extracted from LiteNet Connectivity Solutions' dial-up ISP stack, where it's what lets a real 14.4k-class modem call in and get answered by software instead of hardware.

License

MIT — see LICENSE. This covers slmbridge's own code only; the third-party components it interoperates with carry their own licenses as listed above.