| """Audio post-FX bus for the Un-Language Slider. |
| |
| Closes the dry-TTS-sounds-like-a-phone-call gap with pedalboard DSP — NOT a Fraser claim, |
| just a way to keep the toy from undermining itself. All effects are toggleable in the UI; |
| default preset is gentle ('lush'). All processing is done on a stereo signal. |
| |
| Effects (in order): |
| 1. Gain stage |
| 2. Light chorus (modulation) |
| 3. Slap delay (short stereo delay, low feedback) |
| 4. Plate reverb (long tail, ~50% wet) |
| 5. Octave-up self-layer mixed under (subtle harmonics) |
| |
| API: |
| from scripts.post_fx import apply_post_fx |
| y_wet, sr = apply_post_fx(y, sr, preset="lush", octave_mix=0.18) |
| """ |
|
|
| from __future__ import annotations |
|
|
| import numpy as np |
|
|
|
|
| PRESETS = { |
| "dry": {"chorus": 0.0, "delay": 0.0, "reverb": 0.0, "octave_mix": 0.0}, |
| "subtle": {"chorus": 0.20, "delay": 0.10, "reverb": 0.25, "octave_mix": 0.08}, |
| "lush": {"chorus": 0.35, "delay": 0.15, "reverb": 0.50, "octave_mix": 0.18}, |
| "cathedral": {"chorus": 0.30, "delay": 0.20, "reverb": 0.75, "octave_mix": 0.22}, |
| } |
|
|
|
|
| def _to_stereo(y: np.ndarray) -> np.ndarray: |
| if y.ndim == 1: |
| return np.stack([y, y], axis=0) |
| return y |
|
|
|
|
| def _pitch_shift(y: np.ndarray, sr: int, semitones: float) -> np.ndarray: |
| """Phase-vocoder pitch shift via librosa (kept dependency-light).""" |
| import librosa |
| if y.ndim == 2: |
| return np.stack([librosa.effects.pitch_shift(y=y[c], sr=sr, n_steps=semitones) |
| for c in range(y.shape[0])], axis=0) |
| return librosa.effects.pitch_shift(y=y, sr=sr, n_steps=semitones) |
|
|
|
|
| def apply_post_fx(y: np.ndarray, sr: int, preset: str = "subtle", |
| chorus: float | None = None, delay: float | None = None, |
| reverb: float | None = None, octave_mix: float | None = None) -> tuple[np.ndarray, int]: |
| """Apply the post-FX bus. Returns (wet_stereo, sr).""" |
| p = PRESETS.get(preset, PRESETS["subtle"]).copy() |
| if chorus is not None: p["chorus"] = chorus |
| if delay is not None: p["delay"] = delay |
| if reverb is not None: p["reverb"] = reverb |
| if octave_mix is not None: p["octave_mix"] = octave_mix |
|
|
| from pedalboard import Pedalboard, Chorus, Delay, Reverb, Gain |
| y = _to_stereo(np.asarray(y, dtype=np.float32)) |
|
|
| |
| if p["octave_mix"] > 0: |
| y_oct = _pitch_shift(y, sr, semitones=12.0).astype(np.float32) |
| |
| L = min(y.shape[-1], y_oct.shape[-1]) |
| y = y[..., :L] + p["octave_mix"] * y_oct[..., :L] |
|
|
| |
| board_fx = [Gain(gain_db=-1.0)] |
| if p["chorus"] > 0: |
| board_fx.append(Chorus(rate_hz=0.8, depth=0.25, centre_delay_ms=8.0, |
| feedback=0.0, mix=float(p["chorus"]))) |
| if p["delay"] > 0: |
| board_fx.append(Delay(delay_seconds=0.11, feedback=0.15, mix=float(p["delay"]))) |
| if p["reverb"] > 0: |
| board_fx.append(Reverb(room_size=0.8, damping=0.35, wet_level=float(p["reverb"]), |
| dry_level=1.0 - 0.5 * float(p["reverb"]), width=1.0)) |
|
|
| board = Pedalboard(board_fx) |
| |
| wet = board(y.astype(np.float32), sample_rate=sr) |
| |
| peak = float(np.max(np.abs(wet))) if wet.size else 1.0 |
| if peak > 0.98: |
| wet = wet * (0.98 / peak) |
| return wet, sr |
|
|
|
|
| def main(): |
| import argparse |
| import soundfile as sf |
| p = argparse.ArgumentParser() |
| p.add_argument("--in", dest="inp", required=True) |
| p.add_argument("--out", required=True) |
| p.add_argument("--preset", default="lush", choices=list(PRESETS.keys())) |
| args = p.parse_args() |
| y, sr = sf.read(args.inp, always_2d=False) |
| if y.ndim == 2: |
| y = y.T |
| wet, sr = apply_post_fx(y, sr, preset=args.preset) |
| sf.write(args.out, wet.T, sr) |
| print(f"wrote {args.out} (preset={args.preset}, sr={sr}, dur={wet.shape[-1]/sr:.2f}s)") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|