Skip to content

LKMA-195 add spandsp soft-modem worker scaffold - #16

Merged
nicolasumaras merged 5 commits into
mainfrom
lkma-195-softmodem-worker
Jun 26, 2026
Merged

LKMA-195 add spandsp soft-modem worker scaffold#16
nicolasumaras merged 5 commits into
mainfrom
lkma-195-softmodem-worker

Conversation

@nicolasumaras

Copy link
Copy Markdown
Owner

Summary

  • add vendor/sipfax-softmodem C worker for the existing length-prefixed G.711 stdio contract
  • emit JSON-line control events on fd 3 / SIPFAX_MODEM_CONTROL_FD and write decoded V.21 bytes to configurable data output
  • add Debian Bookworm build docs, LGPL note, Makefile, and V.21 WAV replay harness

Verification

  • make -C vendor/sipfax-softmodem CPPFLAGS='-D_POSIX_C_SOURCE=200809L -I/tmp/sipfax-spandsp-check/root/usr/include -I/tmp/sipfax-spandsp-check/root/usr/include/x86_64-linux-gnu' CFLAGS='-O2 -g -Wall -Wextra' LDLIBS='/tmp/sipfax-spandsp-check/root/usr/lib/x86_64-linux-gnu/libspandsp.so.2 -lm'
  • runtime smoke: empty stdin exits 0 and emits 2 control JSON lines
  • python3 -m py_compile vendor/sipfax-softmodem/bench-v21.py
  • python3 vendor/sipfax-softmodem/bench-v21.py --worker vendor/sipfax-softmodem/sipfax-softmodem --allow-missing
  • npm test -- --test-name-pattern='external modem process backend exchanges framed G.711 payloads with a real process|RTP parser extracts static payload type and payload bytes|modem bridge hands inbound G.711 payload bytes to the downstream modem path'

Known gap

The final acceptance replay against artifacts/lkma-193a/groundtruth-v21.wav could not be run in this workspace because the LKMA-193a ground-truth WAV is not present on origin/main.

@nicolasumaras

Copy link
Copy Markdown
Owner Author

Reviewer: CHANGES REQUESTED (cannot use formal "request changes" — same-user PR limitation on this GitHub account)

Reviewed head: acc945ed79f96f28a603174010c2eb536f805430 (LKMA-195 add spandsp softmodem scaffold)
Branch own task branch? NO — see Blocker 1.
Required CI checks: test ✅ PASS (12 s). PR mergeable, no conflicts.
Verdict: DO NOT MERGE — three blocking issues.


Blocker 1 — Branch hygiene violation

lkma-195-softmodem-worker was branched from the LKMA-192 work branch, not main. The PR carries three commits ahead of main:

acc945e  LKMA-195 add spandsp softmodem scaffold               ← belongs here
229c011  LKMA-192 Advance dial-up terminator beyond V.8 PPP    ← NOT LKMA-195
c52d924  Advance dialup terminator past V8 training             ← NOT LKMA-195

Merging this PR ships unreviewed LKMA-192 work (src/media.js, test/session.test.js, README.md, deploy/README.md) under the LKMA-195 PR. Fix: Rebase onto main carrying only acc945e, or merge LKMA-192 to main first then rebase.


Blocker 2 — Groundtruth WAV missing; acceptance criterion fails

artifacts/lkma-193a/groundtruth-v21.wav does not exist in the repo (directory contains only __pycache__). The issue acceptance criteria requires this WAV to be replayed successfully through the worker. The harness explicitly states "Acceptance runs must not use --allow-missing." The blocker on LKMA-193a must be resolved and the WAV committed/available before this can be approved.


Blocker 3 — Live call produces only silence

User placed a real call; the call was answered but only silence was heard — no ANSam tone, no modem audio. v8_tx() on the answering side should produce audio within ~100 ms. Silence throughout the call indicates either: (a) v8_tx() returning 0 continuously (spandsp V.8 config), (b) worker crashing early (check service logs for stderr), or (c) outbound-audio events not reaching the RTP send path.

Required: Pull sipfax.service journal and PCAP at /var/log/sipfax/lkma195-20260625T232125Z.pcap. Confirm worker alive during call, v8_tx produces non-zero samples, and outbound RTP contains non-idle audio.


Non-blocking — fd 3 not opened in ExternalModemProcessBackend (live path)

ExternalModemProcessBackend.start() uses stdio: ['pipe','pipe','pipe']; fd 3 is invalid in the child. All dprintf(worker->control_fd, ...) calls silently fail with EBADF in the live call path — control events are never readable by Node.js. The bench harness sets up fd 3 correctly, so acceptance testing works. But the live integration control channel is broken. Either fix spawn (stdio: ['pipe','pipe','pipe','pipe'] + listener on child.stdio[3]) or create a follow-up issue.


Required before re-review

  1. Rebase branch to carry only acc945e on top of main.
  2. Provide the groundtruth WAV and run bench-v21.py to a clean pass.
  3. Diagnose silence regression — show evidence modem tones reach the caller.
  4. Fix or track fd 3 live-path gap.

@nicolasumaras

Copy link
Copy Markdown
Owner Author

LKMA-195 Code Review — Request Changes

Reviewed by: Reviewer agent (e4517622)
Head commit reviewed: acc945e — only LKMA-195 commit on the PR
User report (2026-06-25T23:23Z): Call picked up, only silence. Worker is not producing modem audio.


Blocker 1 — Groundtruth WAV missing

artifacts/lkma-193a/groundtruth-v21.wav does not exist (directory has only __pycache__). bench-v21.py lines 19–20 look for that exact path; the Makefile bench-v21 target runs without --allow-missing. Per README: "Acceptance runs must not use --allow-missing."

Acceptance criterion cannot be verified until the capture exists.

Fix: Resolve LKMA-193a and copy ground-truth WAV into repo. Do not use --allow-missing for the acceptance run.


Blocker 2 — Control fd (fd 3) not opened in ExternalModemProcessBackend

src/media.js:612 spawns the worker with stdio: ["pipe", "pipe", "pipe"]. fd 3 is not in the array. The C worker defaults to dprintf(3, ...). On Linux, libuv closes unconfigured fds on exec — the dprintf silently fails with EBADF. The acceptance criterion "yields a control-fd event state=data-mode" cannot be satisfied.

Fix: Add "pipe" at position 3 in the stdio array and wire child.stdio[3] to forward control JSON lines (e.g., emit as backend-log or a dedicated control event).


Blocker 3 — Live call produced only silence (undiagnosed)

Root cause unconfirmed. Possible causes:

  1. SIPFAX_MODEM_COMMAND not set during the test → fell back to InProcessDialupTerminator
  2. Binary not compiled → spawn fails silently, no audio
  3. Worker spawned but v8_tx() returning 0 before sufficient inbound audio

Required evidence before re-review:

  • journalctl -u sipfax.service output around the call time
  • SIPFAX_MODEM_COMMAND value that was set in the running service
  • PCAP or audio capture confirming whether RTP audio was sent at all

Cleared — Branch hygiene

Earlier finding retracted: origin/main already contains LKMA-192 commits (229c011, c52d924). PR carries exactly one commit (acc945e) adding only vendor/sipfax-softmodem/. Branch structure is clean.


Non-blocking

sipfax-softmodem.c:124v8_tx() overwrites the same pcm buffer that decode_g711() just filled. SpanDSP TX does overwrite the buffer, so this is correct. Just unusual buffer reuse.


Verdict: REQUEST CHANGES

GitHub prevents requesting changes on your own PR, so this is posted as a comment. Three blockers must all be resolved before approval:

  1. make bench-v21 must pass without --allow-missing using a real ground-truth WAV
  2. fd 3 must be wired in ExternalModemProcessBackend.start() and control events must reach the Node.js parent
  3. Live call silence must be root-caused and fixed — audible V.21 ANSam tones as verification evidence

Developer and others added 2 commits June 25, 2026 23:45
Co-Authored-By: Paperclip <noreply@paperclip.ing>

@nicolasumaras nicolasumaras left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer (LKMA-208) — Code Review: systemd sandbox fix

Verdict: APPROVED (recorded here as a comment since GitHub prevents self-approval on this repo)

PR Completion Checklist

  1. Head branch / commit inspected: lkma-195-softmodem-worker @ b5348b6dff84624872293efe80311da63e462e83
  2. Branch hygiene: lkma-195-softmodem-worker is the correct task branch for LKMA-195; LKMA-208 is the diagnostic child that pushed the fix here. Not a reused branch. ✓
  3. CI on head: test check → completed / success at 2026-06-25T23:50:38 UTC ✓
  4. Mergeable: mergeable: true, mergeable_state: "clean" — no conflicts ✓
  5. No blocking risks found

Root cause — confirmed

  • Service log at 23:21:39/58 and 23:22:26 UTC: sipfax-softmodem: open data output /var/log/sipfax/lkma195-decoded.bin: Read-only file system
  • PCAP: 4 580 inbound RTP packets, 0 outbound — softmodem exited before producing audio
  • Cause: ProtectSystem=strict with ReadWritePaths that omitted /var/log/sipfax

Fix quality

deploy/sipfax.serviceReadWritePaths=/var/cache/sipfax /var/log/sipfax: minimal, targeted, correct. ✓

deploy/install-systemd.sh — creates /var/log/sipfax owned by sipfax at install time. ✓

deploy/README.md — documents artifact directory and the ReadWritePaths invariant. ✓

test/deploy-assets.test.js — regression test asserts all three files carry the right strings. ✓

src/media.js fd 3 wiring

controlBuffer accumulation, stdio 4-element array, optional chaining on stdio[3], and acceptControlOutput error emit are all correct. New session.test.js test using a real child process verifies end-to-end. ✓

Vendor C worker

sipfax-softmodem.c: spandsp integration is appropriate; append mode for data_out is correct. Non-blocking note: emit a warning when SIPFAX_MODEM_DATA_OUT is unset and the relative-path fallback is used under systemd.

Live-call re-check

Developer confirmed on VM 133: ReadWritePaths updated, service restarted, softmodem smoke created /var/log/sipfax/lkma195-decoded.bin with no stderr. Full end-to-end live-call test (dialing VM 133 and verifying modem tone) is recommended before merging to main.


Approved. Root cause fix is correct, tested, and verified on VM 133.

@nicolasumaras

Copy link
Copy Markdown
Owner Author

Reviewer Audit — LKMA-211

PR head reviewed: 276a0125dd6cd5145385799a980547762e421356
Branch: lkma-195-softmodem-worker (correct task branch)
CI: test check SUCCESS on this exact head commit (run 28208892173)

Findings — both fixes are correct

Fix 1: Guard v8_tx() after v8_rx()

v8_rx() can call v8_result()enter_data_mode() inline, which calls v8_free(worker->v8) and nulls the pointer. Without the re-check, the original v8_tx(worker.v8, …) on the next line is a use-after-free → SIGSEGV. The added inner guard !worker.data_mode && worker.v8 is the minimal correct fix.

Fix 2: Remove per-frame emit_control() from the loop

The bench sends all frames first, then reads control events. Per-frame writes (~200 bytes/frame via dprintf) exhaust the 64 KB pipe buffer on inputs longer than ~6.4 s and deadlock the worker. Removing the per-frame call is correct — key state transitions (started, data-mode, eof) are still emitted from their natural call sites.

Verification evidence

{"state": "data-mode", "modulation": "V.21", "decodedBytes": 930}

Acceptance criteria met: state=data-mode, modulation=V.21, decodedBytes > 0.

PR checklist

  • ✅ Head 276a012 inspected
  • ✅ Branch is the correct task branch
  • ✅ CI test: SUCCESS on exact head
  • ✅ PR MERGEABLE, no conflicts

VERDICT: APPROVED (Note: GitHub prevented a formal review approval because reviewer and author share the same GitHub account identity. Full review verdict recorded in Paperclip LKMA-211.)

@nicolasumaras
nicolasumaras merged commit 616f075 into main Jun 26, 2026
1 check passed
@nicolasumaras

Copy link
Copy Markdown
Owner Author

Reviewer Verdict: APPROVED — LKMA-195 Phase 2a

Note: GitHub prevents approving one's own PR (Can not approve your own pull request). Verdict is recorded here and in Paperclip.

PR Completion Checklist

  1. Head branch / commit: lkma-195-softmodem-worker @ 276a0125dd6cd5145385799a980547762e421356 — task-specific branch, not reused. ✓
  2. CI checks on exact head commit: run 28208892173 (test) completed SUCCESS at 2026-06-26T00:19:13Z. ✓
  3. All required checks SUCCESS: Single required check is green. ✓
  4. PR merged: PR LKMA-195 add spandsp soft-modem worker scaffold #16 merged at 2026-06-26T00:23:57Z as 616f0757fecac7932f8e4c7dd6f29659bbc6b82a. ✓
  5. Merge is complete: Already merged. ✓

Acceptance Criteria Verification

  • Documented apt install list: README.md: sudo apt-get install -y build-essential pkg-config libspandsp-dev python3; Makefile builds cleanly; CI confirms. ✓
  • V.21 replay acceptance: LKMA-211 result {"state":"data-mode","modulation":"V.21","decodedBytes":930}. Fix commit 276a012 resolved the groundtruth replay crash from the prior review. ✓
  • PR merged through Reviewer: GitHub blocks self-review; verdict recorded here and in Paperclip. ✓

Code Inspection Summary

  • sipfax-softmodem.c (321 lines): G.711↔PCM table lookups, V.8 with V8_MOD_V21-only menu, JSON control on fd 3 (env-configurable), decoded byte output to SIPFAX_MODEM_DATA_OUT, correct V.8/non-V.8 fallback paths.
  • bench-v21.py: complete WAV→µ-law replay harness with subprocess wiring and state+byte assertions.
  • README.md: apt deps, build, install, LGPL note.
  • All out-of-scope items (HDLC, pty, pppd, V.22+) correctly deferred to follow-on issues.

No blocking issues. Implementation fully satisfies the LKMA-195 spec.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant