Skip to content

core: fix garbage tail in stereo recordings on media bug close - #3105

Open
PegasusS wants to merge 1 commit into
signalwire:masterfrom
PegasusS:fix-stereo-record-drain-len
Open

core: fix garbage tail in stereo recordings on media bug close#3105
PegasusS wants to merge 1 commit into
signalwire:masterfrom
PegasusS:fix-stereo-record-drain-len

Conversation

@PegasusS

@PegasusS PegasusS commented Aug 6, 2026

Copy link
Copy Markdown

core: fix garbage tail in stereo recordings on media bug close

Problem

Every recording produced with a stereo record profile ends with a short
full-scale broadband burst — audible as a click. It is present in the PCM
before any encoding, so it survives every codec and every post-processing
step, and it appears once per switch_ivr_record_session() close (including
each segment when recordings are split).

Root cause

record_callback() in src/switch_ivr_async.c, SWITCH_ABC_TYPE_CLOSE branch:

while (switch_core_media_bug_read(bug, &frame, SWITCH_TRUE) == SWITCH_STATUS_SUCCESS) {
    len = (switch_size_t) frame.datalen / 2;                 /* <-- missing / frame.channels */
    if (len && switch_core_file_write(rh->fh, mask ? null_data : data, &len) != SWITCH_STATUS_SUCCESS) {

Two facts make this wrong for stereo:

  1. switch_core_media_bug_read() doubles frame->datalen when SMBF_STEREO
    is set (it interleaves both legs into the caller's buffer).
  2. The len argument of switch_core_file_write() is expressed in
    per-channel sample frames, not in total samples.

So datalen / 2 yields twice the correct frame count. switch_core_file_write()
then consumes len * channels * 2 bytes from data[], i.e. twice what
switch_core_media_bug_read() actually filled. The surplus comes from the
uninitialised tail of the on-stack data[SWITCH_RECOMMENDED_BUFFER_SIZE]
buffer and is encoded into the file.

The SWITCH_ABC_TYPE_READ_PING branch of the very same function already has
the correct form
:

len = (switch_size_t) frame.datalen / 2 / frame.channels;

The two branches simply disagree; this patch makes CLOSE match READ_PING.

Quantitative check

For the common 8 kHz stereo case the surplus is exactly

640 bytes = 160 stereo sample pairs @ 8000 Hz = 20.000 ms

Measured burst widths across 27 affected recordings had a median of 19.9 ms,
with 14 of 27 falling inside a 1.3 ms band (18.6–19.9 ms) — consistent with a
fixed-length artefact rather than signal-dependent noise.

Affected versions

The CLOSE branch has been missing the divisor in every release checked, while
READ_PING has always had it:

version CLOSE branch READ_PING branch
master (2026-08-01) … / 2; … / 2 / frame.channels;
v1.10.12 … / 2; … / 2 / frame.channels;
v1.10.11 … / 2; … / 2 / frame.channels;
v1.10.7 … / 2; … / 2 / frame.channels;
v1.10.0 … / 2; … / 2 / frame.channels;
v1.8.7 … / 2; … / 2 / frame.channels;

This is a long-standing defect, not a regression.

Reproduction

  1. Record a session with a stereo record profile (RECORD_STEREO=true).
  2. Stop the recording (uuid_record … stop, or hang up).
  3. Inspect the last ~20 ms of the resulting file: a full-scale, broadband,
    dual-channel burst is present, including on channels that were digitally
    silent for the entire call.

The last point is the cleanest discriminator: the burst appears on a channel
that carried no audio at all, so it cannot be signal.

Verification performed

The one-line change was built and deployed on two systems doing continuous
stereo recording:

  • Build fidelity — rebuilding the unpatched tree reproduced the deployed
    libfreeswitch.so byte-for-byte (whole file, not just .text),
    establishing that the build environment matched the running binary.
  • Change containment — the patched build differed in 22 functions across
    67 regions, with a displacement distribution of {-0x10: 66, +0x10: 1}:
    every difference is pure code displacement, no semantic change outside
    record_callback().
  • Instruction-level — integer division count inside record_callback()
    went from 4 to 5, with the new division located inside the CLOSE drain loop,
    ahead of the switch_core_media_bug_read() call.
  • Behaviour — objective detection over post-fix recordings: defect-signature
    events went from 27 to 0 on the first system; on the second, 11 calls /
    49 recording seams produced 0 hits, with a same-run positive control over
    pre-fix material confirming the detector still fires on the defect.

Note on side effects

switch_core_file_write() advances rh->fh->samples_out by len. Because the
patch halves len for stereo, samples_out also halves. Deployments that set a
non-zero RECORD_MIN_SEC should be aware that the CLOSE branch compares
samples_out against samplerate * min_sec and may call switch_file_remove();
recordings that previously sat just above that threshold could now fall below it.
The previous value was inflated, so the new comparison is the correct one — but
it is a visible behaviour change for anyone who tuned RECORD_MIN_SEC against
the inflated figure.

The SWITCH_ABC_TYPE_CLOSE drain loop in record_callback() computes the
write length without dividing by the channel count, so on stereo
recordings it asks switch_core_file_write() for twice as many sample
frames as switch_core_media_bug_read() actually produced. The surplus is
read from the uninitialised tail of the on-stack buffer and encoded into
the file, producing a short full-scale broadband burst at the end of
every recording.

The READ_PING branch of the same function already divides by
frame.channels; this makes the CLOSE branch consistent with it.

switch_core_media_bug_read() doubles frame->datalen when SMBF_STEREO is
set, while the len argument of switch_core_file_write() is expressed in
per-channel sample frames. Dividing only by 2 therefore yields twice the
correct value.

With the default 8 kHz stereo recording profile the surplus is
640 bytes = 160 stereo samples = 20.000 ms per close. Measured burst
widths on affected files had a median of 19.9 ms.

Signed-off-by: shihaifeng <shihf_dc@163.com>
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