Files
UmbraAudio/backup/author_voice_lab/scripts/probe_dsp.py
2026-10-18 19:01:04 -07:00

60 lines
2.3 KiB
Python

# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
"""Probe 2: DSP axis limits. What does pitch_shift_pv actually move?"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, "/Volumes/1TB_APL_Storage/UmbraAudio")
import librosa
import numpy as np
import soundfile as sf
from umbraaudio.generation.audio_effects import EFFECT_FN
# NOTE: apply_effects_by_name() only accepts effect NAMES and uses hardcoded
# defaults, so it cannot dial a parameter. A sweep needs EFFECT_FN directly.
_pv = EFFECT_FN["pitch_shift_pv"]
SRC = Path("/private/var/folders/y8/cbvhds297918lrxt2wd3btsr0000gn/T/opencode/probe_out/A_baseline.wav")
def measure(y, sr):
f0, _, _ = librosa.pyin(y.astype(np.float32), fmin=60.0, fmax=400.0, sr=sr)
v = f0[~np.isnan(f0)]
S = np.abs(librosa.stft(y.astype(np.float32), n_fft=2048, hop_length=512))
cen = float(librosa.feature.spectral_centroid(S=S, sr=sr).mean())
# Formant-ish proxies: energy-weighted centroid in low band vs overall
return float(np.median(v)), cen
def formant_band_ratio(y, sr):
"""Crude vocal-tract length proxy: spectral centroid of the voiced spectrum."""
S = np.abs(librosa.stft(y.astype(np.float32), n_fft=2048, hop_length=512))
freqs = librosa.fft_frequencies(sr=sr, n_fft=2048)
m = freqs <= 4000
return float((S[m] * freqs[m][:, None]).sum() / S[m].sum())
y, sr = sf.read(str(SRC), dtype="float32")
base_f0, base_cen = measure(y, sr)
base_ftr = formant_band_ratio(y, sr)
print(f"BASE F0med={base_f0:7.1f}Hz centroid={base_cen:7.0f}Hz LTAS4k={base_ftr:7.0f}Hz")
print("\n--- pitch_shift_pv sweep (semitones) ---")
print(f"{'semi':>6} {'F0med':>8} {'d_F0_st':>8} {'centroid':>9} {'d_cen_st':>8} {'LTAS4k':>8} {'d_ftr_st':>9}")
for st in (-12, -8, -4, 0, 4, 8, 12):
out = _pv(y, sr, semitones=float(st))
f0, cen = measure(out, sr)
ftr = formant_band_ratio(out, sr)
d_f0 = 12 * np.log2(f0 / base_f0)
d_cen = 12 * np.log2(cen / base_cen)
d_ftr = 12 * np.log2(ftr / base_ftr)
print(f"{st:>6} {f0:>8.1f} {d_f0:>8.1f} {cen:>9.0f} {d_cen:>8.1f} {ftr:>8.0f} {d_ftr:>9.1f}")
print("\nInterpretation: d_cen ~= d_f0 => full-spectrum shift => formants move WITH pitch.")
print("A true male->female conversion needs d_cen/d_F0 ~= +2.5 to +3 st, d_F0 ~= +9 to +12 st.")