Finding the 40 ms gaps a bad encoder left in a live set

这篇内容暂时只有 English 版本,以下是原文。

A friend sent me a 2h14m live set as a single 320 kbps MP3, ripped from a radio broadcast. It sounded fine on speakers, but through headphones at every track boundary there was a tiny click, and the timestamps in the cue sheet stopped matching the audio after the first half hour. That drift was the tell: the file had been re-encoded at least twice and padded somewhere in the middle.

I didn’t want to re-rip anything, so I went looking for the gaps. Decoding to mono 44.1 kHz WAV and scanning for near-silence longer than 25 ms:

import numpy as np, soundfile as sf
x, sr = sf.read('set.wav', dtype='float32')
if x.ndim > 1: x = x.mean(axis=1)
win = int(sr * 0.025)
rms = np.sqrt(np.convolve(x*x, np.ones(win)/win, 'same'))
quiet = rms < 0.004
# collapse runs of quiet into spans
idx = np.flatnonzero(np.diff(np.r_[False, quiet, False]))
spans = list(zip(idx[::2], idx[1::2]))
for a, b in spans:
    if b - a > sr * 0.02:
        print(f'{a/sr:8.2f}s  {(b-a)/sr*1000:5.1f} ms  gap')

That found 37 candidates. Sixteen were real track changes, twenty-one were encoder padding around the loudest transients. The distinction matters: a real gap has audio on both sides at working level, padding sits next to a peak. I sorted by the RMS of the 200 ms before and after the span and kept anything where both sides were above -18 dBFS and the span itself was under 120 ms. That left 19, of which 17 matched my friend’s handwritten cue list once I added the cumulative offset.

Why it worked: MP3 frame padding is always short and always hugs a peak, so amplitude context filters it cheaply. The cost is that quiet passages with real fades get thrown out, and I accepted that; I only needed boundaries, not an exhaustive segmentation. I also had to resample the whole thing to a single rate first, because the source had two sample rates stapled together and the RMS windows were meaningless across the splice.

Where this breaks: sets with continuous mixes and no gaps at all (my own DJ mixes are all beatmatched, so this returns nothing useful), recordings with a noise floor above -40 dBFS, and anything with hard limiting that lifts the noise floor between tracks. I once ran the same script on a cassette rip and got 140 false gaps from tape hiss. The threshold has to move with the material; 0.004 is not universal.

The part I haven’t solved: the drift is still there in the middle, roughly 60 ms accumulated over 2 hours. I patched the cue sheet by hand, but I’d like a script that finds the drift itself instead of me eyeballing it. How do you handle clock drift when the source is a single continuous file?

0
赞同度 100% · 0 条评论 · 4 阅读

0 条评论

支持 Markdown · 回复会实时推送给对方

还没有评论,写下第一条。