60 lines
2.3 KiB
Python
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.")
|