From bd6bc294adc4c541ea1c8822f5bf10148765c972 Mon Sep 17 00:00:00 2001 From: Bossiara13 <236771060+DmitryBykov-ISPO@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:58:20 +0300 Subject: [PATCH] feat: add tts effects module (bandpass + reverb + optional pitch) --- effects.py | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 effects.py diff --git a/effects.py b/effects.py new file mode 100644 index 0000000..93d5e12 --- /dev/null +++ b/effects.py @@ -0,0 +1,71 @@ +import numpy as np +from scipy.signal import butter, sosfiltfilt + + +def _ensure_float32_mono(audio): + arr = np.asarray(audio, dtype=np.float32) + if arr.ndim > 1: + arr = arr.mean(axis=1).astype(np.float32) + return arr + + +def apply_bandpass(audio, sr, low_hz=200, high_hz=7000, order=4): + arr = _ensure_float32_mono(audio) + nyq = 0.5 * sr + low = max(low_hz, 1) / nyq + high = min(high_hz, int(nyq) - 1) / nyq + if not (0 < low < high < 1): + return arr + sos = butter(order, [low, high], btype='band', output='sos') + return sosfiltfilt(sos, arr).astype(np.float32) + + +def apply_reverb(audio, sr, wet=0.20, decay_ms=100, taps=4): + arr = _ensure_float32_mono(audio) + if wet <= 0 or decay_ms <= 0: + return arr + dry = max(0.0, 1.0 - wet) + out = dry * arr + gap = max(1, int(sr * (decay_ms / 1000.0) / taps)) + gain = wet + for i in range(1, taps + 1): + delay = gap * i + gain *= 0.6 + if delay >= len(arr): + break + padded = np.zeros_like(arr) + padded[delay:] = arr[:len(arr) - delay] + out += gain * padded + peak = float(np.max(np.abs(out))) if out.size else 0.0 + if peak > 0.99: + out *= 0.99 / peak + return out.astype(np.float32) + + +def apply_pitch(audio, sr, semitones=0): + arr = _ensure_float32_mono(audio) + if semitones == 0: + return arr + factor = 2.0 ** (semitones / 12.0) + n_out = int(round(len(arr) / factor)) + if n_out <= 1: + return arr + src_idx = np.linspace(0, len(arr) - 1, num=n_out).astype(np.float32) + lo = np.floor(src_idx).astype(np.int64) + hi = np.clip(lo + 1, 0, len(arr) - 1) + frac = (src_idx - lo).astype(np.float32) + resampled = (1.0 - frac) * arr[lo] + frac * arr[hi] + return resampled.astype(np.float32) + + +def process(audio, sr, bandpass=None, reverb=None, pitch_semitones=0): + out = _ensure_float32_mono(audio) + if bandpass is not None: + low, high = bandpass + out = apply_bandpass(out, sr, low, high) + if reverb is not None: + wet, decay_ms = reverb + out = apply_reverb(out, sr, wet=wet, decay_ms=decay_ms) + if pitch_semitones: + out = apply_pitch(out, sr, semitones=pitch_semitones) + return out