Merge pull request 'pitch: treat bleed in the vocal stem as silence' (#15) from pitch-silence into main

Reviewed-on: #15
Reviewed-by: branderson <brad@branderson.io>
This commit was merged in pull request #15.
This commit is contained in:
2026-10-09 18:15:21 -07:00
2 changed files with 29 additions and 0 deletions
+11
View File
@@ -6,6 +6,11 @@ import numpy as np
from ..models import on_release from ..models import on_release
from ..pipeline import Ctx, Stage from ..pipeline import Ctx, Stage
# Separated vocal stems keep faint bleed that crepe still finds a pitch in, so
# frames this far below the song's loud vocal level count as silence. Relative
# rather than absolute, so it holds however loud the song was mastered.
SILENCE_BELOW_LOUD_DB = 25.0
@on_release @on_release
def unload_model() -> None: def unload_model() -> None:
@@ -18,6 +23,11 @@ def unload_model() -> None:
delattr(torchcrepe.infer, attr) delattr(torchcrepe.infer, attr)
def mute_silence(conf: np.ndarray, loudness: np.ndarray) -> None:
"""Zero the confidence of frames quiet enough to be bleed, in place."""
conf[loudness < np.quantile(loudness, 0.9) - SILENCE_BELOW_LOUD_DB] = 0.0
def track_pitch(vocals_path: Path, model: str, device: str): def track_pitch(vocals_path: Path, model: str, device: str):
import torchaudio import torchaudio
import torchcrepe import torchcrepe
@@ -33,6 +43,7 @@ def track_pitch(vocals_path: Path, model: str, device: str):
return_periodicity=True, batch_size=512) return_periodicity=True, batch_size=512)
f0 = f0[0].cpu().numpy() f0 = f0[0].cpu().numpy()
conf = periodicity[0].cpu().numpy() conf = periodicity[0].cpu().numpy()
mute_silence(conf, torchcrepe.loudness.a_weighted(waveform, 16000, 160)[0].cpu().numpy())
times = np.arange(len(f0)) * 0.01 times = np.arange(len(f0)) * 0.01
return times, f0, conf return times, f0, conf
+18
View File
@@ -17,3 +17,21 @@ def test_pitch_stage_writes_npz(tmp_path, monkeypatch):
data = np.load(work / "pitch.npz") data = np.load(work / "pitch.npz")
assert np.allclose(data["f0"], 220.0) assert np.allclose(data["f0"], 220.0)
assert data["times"].shape == data["conf"].shape assert data["times"].shape == data["conf"].shape
def test_mute_silence_drops_bleed_but_keeps_quiet_singing():
# 60 frames of singing around -30 dB, one quiet sung frame at -50 dB, and
# 40 frames of bleed at -60 dB: only the bleed is more than 25 dB below
# the song's loud level.
loudness = np.array([-30.0] * 60 + [-50.0] + [-60.0] * 40)
conf = np.full_like(loudness, 0.8)
pitch.mute_silence(conf, loudness)
assert (conf[:61] == 0.8).all()
assert (conf[61:] == 0.0).all()
def test_mute_silence_is_relative_to_the_songs_level():
loudness = np.array([-45.0] * 60 + [-75.0] * 40)
conf = np.full_like(loudness, 0.8)
pitch.mute_silence(conf, loudness)
assert (conf[:60] == 0.8).all() and (conf[60:] == 0.0).all()