#!/usr/bin/env bash # Remove driver-inserted silence from a capture, exactly. # # tools/port/strip-padding in.wav out.wav # # WHEN THIS IS VALID, AND WHEN IT IS VANDALISM. The distinction is the whole # tool and getting it backwards destroys the artefact: # # * PulseAudio's monitor SUBSTITUTES silence. It advances on a wall clock and # replaces audio that existed when the producer was late. Information is # gone; deleting the holes compresses time unevenly and repairs nothing. # DO NOT RUN THIS ON A MONITOR CAPTURE. # * Xenia's ALSA writer PADS. It inserts silence between samples the guest # emitted when its ring is empty (`alsa_audio_driver.cc:359`). Nothing is # lost and nothing is overwritten, so removing the padding is EXACT -- it # hands back the contiguous stream the guest produced. # # CONTROLLED, not argued. A real music+SFX bed (137.37 s, with 454 zero runs of # its own) had 1 149 holes inserted at 8.37/s to +9.9 % length, matching the # observed ALSA profile, then was stripped: # # original vs itself r 1.000 lag 0.0 s margin +0.141 [ceiling] # PADDED vs original r 0.436 lag -12.2 s margin +0.006 [destroyed] # STRIPPED vs original r 1.000 lag 0.0 s margin +0.142 [recovered] # # Frame counts: original 6 593 984, stripped 6 559 880, and the original stripped # of its own genuine zero runs 6 560 044 -- a difference of 164 frames, 3.4 ms in # 137 s, from inserted holes abutting genuine ones and merging. # # ⚠️ It removes GENUINE silence too, and cannot tell the two apart -- that is why # the reference above is the unstripped original: recovery does not depend on # stripping both sides. On this material the genuine runs total 0.71 s in 137 s # and cost nothing measurable. On material that is mostly silence they would. set -euo pipefail in="${1:?usage: strip-padding IN.wav OUT.wav}"; out="${2:?usage: strip-padding IN.wav OUT.wav}" python3 - "$in" "$out" <<'PYEOF' import array, struct, sys, wave src, dst = sys.argv[1], sys.argv[2] w = wave.open(src); ch = w.getnchannels(); rate = w.getframerate() if w.getsampwidth() != 2: print("strip-padding: 16-bit PCM only (got %d-bit)" % (w.getsampwidth()*8)); raise SystemExit(2) n = w.getnframes(); a = array.array('h'); a.frombytes(w.readframes(n)); w.close() MIN = max(1, rate // 1000) # a gap is a run, not a sample sil = bytearray(n) for f in range(n): b = f * ch if not any(a[b+c] for c in range(ch)): sil[f] = 1 keep = array.array('h'); f = 0; removed = 0; holes = 0 while f < n: s = f if sil[f]: while f < n and sil[f]: f += 1 if f - s < MIN: keep.extend(a[s*ch:f*ch]) else: removed += f - s; holes += 1 else: while f < n and not sil[f]: f += 1 keep.extend(a[s*ch:f*ch]) k = len(keep) // ch o = wave.open(dst + ".partial", "wb") # temp name, renamed on completion o.setnchannels(ch); o.setsampwidth(2); o.setframerate(rate) o.writeframes(keep.tobytes()); o.close() import os; os.replace(dst + ".partial", dst) print("%s: %d frames (%.3f s) -> %s: %d frames (%.3f s)" % (src, n, n/rate, dst, k, k/rate)) print(" removed %d run(s) totalling %.3f s (%.2f %% of the input)" % (holes, removed/rate, 100.0*removed/n)) PYEOF