feat: add tts effects module (bandpass + reverb + optional pitch)
This commit is contained in:
parent
4f3b827a47
commit
bd6bc294ad
1 changed files with 71 additions and 0 deletions
71
effects.py
Normal file
71
effects.py
Normal file
|
|
@ -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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue