Skip to content

mod_sndfile: allow overriding the WAV write subtype via a file-string param#3083

Open
celliso1 wants to merge 1 commit into
signalwire:masterfrom
celliso1:mod_sndfile-write-subtype
Open

mod_sndfile: allow overriding the WAV write subtype via a file-string param#3083
celliso1 wants to merge 1 commit into
signalwire:masterfrom
celliso1:mod_sndfile-write-subtype

Conversation

@celliso1

@celliso1 celliso1 commented Jul 15, 2026

Copy link
Copy Markdown

Description

On write, mod_sndfile unconditionally OR-s SF_FORMAT_PCM_16 into the format
for every standard sample rate, and no file extension maps a WAV container to a
companded (u-law/A-law) payload — .ul/.ulaw produce a headerless
SF_FORMAT_RAW file, not RIFF/WAVE. As a result there is no way to record, for
example, a G.711 call into a RIFF/WAVE file whose payload stays u-law: the
recording is always expanded to 16-bit PCM, doubling the size on disk and
losing the bit-exact relationship to what crossed the wire.

This adds an optional subtype parameter to the standard file-string prefix,
so the caller can choose the encoded subtype while the container stays implied
by the extension:

session:recordFile("{subtype=ulaw}/tmp/foo.wav")

Accepted values: ulaw, alaw, pcm16, pcm24, pcm32. Write mode only —
reads continue to autodetect the subtype from the header. If the requested
subtype is not valid for the container implied by the extension (for example
ulaw on a .flac target), mod_sndfile logs a warning and falls back to the
container's default subtype rather than failing the open; an unrecognized value
is likewise ignored with a warning. The change is therefore inert unless a
caller explicitly requests a supported subtype, and a bad param never costs the
recording.

Recording a G.711 call as u-law WAV is lossless with respect to the wire, half
the size of the PCM-16 file produced today, and trivially decodable by anything
that reads WAV.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Code cleanup / refactor

Related Issues

No existing issue found for companded WAV write support; happy to open a
tracking issue if the maintainers prefer one.

Testing

  • Added/updated unit tests
  • Tested manually
  • Tested with live SignalWire credentials (if applicable)

Unit tests were added to tests/unit/switch_core_file.c:

  • {subtype=ulaw} on a .wav target produces a RIFF/WAVE file with format
    tag 7 (ITU G.711 u-law) and 8-bit samples that decodes back within u-law
    quantization tolerance.
  • An unrecognized subtype falls back to PCM_16 (format tag 1).
  • An unsupported container/subtype pair ({subtype=ulaw} on a .flac target)
    still opens successfully and yields a valid FLAC file — proving the
    warn-and-fall-back path rather than a failed open.

Manual: recorded a live inbound G.711u call via record_session to a
{subtype=ulaw} .wav target, confirmed the container/subtype with file(1)
(RIFF/WAVE, format tag 7, 8-bit), then decoded and played the file back and
re-transcribed it successfully.

Checklist

  • I have read the CONTRIBUTING guidelines
  • My code follows the project's style guidelines
  • I have added tests for my changes (if applicable)
  • I have updated documentation (if applicable)
  • All existing tests pass

Additional Notes

Why not just re-encode to a compressed format?
For telephony audio that will be archived and later transcribed or analyzed,
re-encoding the recording to a low-bitrate lossy codec (e.g. MP3) is a poor
fit: lossy compression at telephony bitrates degrades the exact tokens that
downstream ASR/NLP extract — digit sequences and spelled names — even when
overall intelligibility is preserved. This is not hypothetical: internal A/B
testing that transcribed lossy-re-encoded copies against losslessly-stored
copies of the same calls showed a measurable, repeatable increase in misspelled
names and corrupted number sequences. Storing the native companded payload in a
WAV container sidesteps that entirely — lossless with respect to the wire, half
the size of PCM-16, and readable by any WAV consumer.

Documentation

The parse site in mod_sndfile.c carries an inline comment
describing the parameter and its accepted values. The wiki/params entry below is
ready to drop into the mod_sndfile / file-string documentation so the parameter
is discoverable outside the source:

subtype (write)

Overrides the encoded sample format (subtype) that mod_sndfile writes,
while the container is still selected by the file extension. Supplied as a
file-string prefix param, e.g. via record_session, recordFile, or any
API that opens a file through mod_sndfile:

record_session {subtype=ulaw}/tmp/foo.wav      # RIFF/WAVE container, u-law payload
uuid_record <uuid> start {subtype=alaw}/tmp/foo.wav
session:recordFile("{subtype=pcm24}/tmp/foo.wav")
Value Encoded subtype Notes
ulaw ITU G.711 µ-law, 8-bit half the size of PCM-16, lossless vs. a G.711µ leg
alaw ITU G.711 A-law, 8-bit half the size of PCM-16, lossless vs. a G.711a leg
pcm16 16-bit signed PCM current default
pcm24 24-bit signed PCM
pcm32 32-bit signed PCM
  • Write only. On read, mod_sndfile autodetects the subtype from the
    file header; the param is ignored.
  • An unrecognized value is ignored (a warning is logged) and the
    container's default subtype is kept.
  • A subtype the container cannot hold (for example ulaw on a .flac
    target) is validated with libsndfile's sf_format_check(); if it fails,
    mod_sndfile logs a warning and falls back to the container's default
    subtype rather than failing the open. A bad subtype never costs the
    recording — worst case you get the container's default encoding.

… param

On write, mod_sndfile unconditionally OR-s SF_FORMAT_PCM_16 into the format
for every standard sample rate, and no extension maps a WAV container to a
companded (u-law/A-law) payload -- the .ul/.ulaw extensions produce
headerless SF_FORMAT_RAW files, not RIFF/WAVE. As a result there is no way
to record, for example, a G.711 call to a RIFF/WAVE file whose payload is
u-law: the recording is always expanded to 16-bit PCM, doubling the size of
audio that was 8-bit companded on the wire and losing the bit-exact
relationship to it.

Honor an optional "subtype" parameter from the standard file-string prefix
so the caller can choose the encoded subtype while keeping the container
implied by the extension:

    session:recordFile("{subtype=ulaw}/tmp/foo.wav")

Accepted values: ulaw, alaw, pcm16, pcm24, pcm32. Write mode only; reads
continue to autodetect the subtype from the header. If the requested
subtype is not valid for the container implied by the extension (for
example ulaw on a .flac target), mod_sndfile logs a warning and falls back
to the container's default subtype rather than failing the open;
unrecognized values are likewise ignored with a warning. The change is
therefore inert unless a caller explicitly requests a supported subtype.

Adds unit tests in tests/unit/switch_core_file.c: subtype=ulaw produces a
RIFF/WAVE file with format tag 7 and 8-bit samples that decodes back within
u-law quantization tolerance, an unrecognized subtype falls back to PCM_16,
and an unsupported container/subtype pair (ulaw on .flac) still opens and
yields a valid FLAC.

Signed-off-by: Calvin Ellison <cellison@youmail.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@celliso1

celliso1 commented Jul 15, 2026

Copy link
Copy Markdown
Author

cc @andywolk — small, self-contained follow-up in the same area as #3061 (compute/storage cost). This adds an opt-in file-string param to override the WAV write subtype in mod_sndfile; default behavior is unchanged. Would appreciate a review when you have a moment. @jakubkarolczyk @s3rj1k in case you're closer to the formats code.

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.

2 participants