| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- # -*- coding: utf-8 -*-
- """
- Basis-DSP-Funktionen fuer Voice-Morphing.
- Pitch-Shift, VTLP, Breathiness, Shimmer, Fade, Speed — rein numpy/scipy.
- """
- from __future__ import annotations
- import numpy as np
- def pitch_shift(audio: np.ndarray, sample_rate: int, semitones: float) -> np.ndarray:
- """
- Pitch-Shift via Sinc-Resampling.
- Positive Semitones = hoeher (Signal wird kuerzer).
- Negative Semitones = tiefer (Signal wird laenger).
- Nutzt scipy.signal.resample (bandlimitierte Sinc-Interpolation).
- """
- from scipy.signal import resample
- if abs(semitones) < 0.05:
- return audio.copy()
- audio = audio.astype(np.float64)
- factor = 2.0 ** (semitones / 12.0)
- new_len = int(len(audio) / factor)
- if new_len < 10:
- return audio.copy()
- result = resample(audio, new_len)
- # Amplitude beibehalten
- orig_rms = np.sqrt(np.mean(audio ** 2)) + 1e-10
- result_rms = np.sqrt(np.mean(result ** 2)) + 1e-10
- if result_rms > 1e-10:
- result *= orig_rms / result_rms
- return result
- def speed_change(audio: np.ndarray, sample_rate: int, factor: float) -> np.ndarray:
- """
- Aendert die Geschwindigkeit ohne Pitch-Aenderung.
- factor > 1.0 = schneller (kuerzeres Signal).
- factor < 1.0 = langsamer (laengeres Signal).
- Nutzt STFT-basiertes Phase-Vocoder-Prinzip via Resampling.
- """
- from scipy.signal import resample
- if abs(factor - 1.0) < 0.005:
- return audio.copy()
- audio = audio.astype(np.float64)
- new_len = int(len(audio) / factor)
- if new_len < 10:
- return audio.copy()
- result = resample(audio, new_len)
- # Amplitude beibehalten
- orig_rms = np.sqrt(np.mean(audio ** 2)) + 1e-10
- result_rms = np.sqrt(np.mean(result ** 2)) + 1e-10
- if result_rms > 1e-10:
- result *= orig_rms / result_rms
- return result
- def vtlp(audio: np.ndarray, sample_rate: int, alpha: float,
- n_fft: int = 1024, hop_length: int = 256) -> np.ndarray:
- """
- Vocal Tract Length Perturbation — Formant-Verschiebung.
- Warpt NUR die Magnitude-Spektren, behaelt originale Phasen.
- alpha > 1.0 = hoehere Formanten, alpha < 1.0 = tiefere.
- """
- if abs(alpha - 1.0) < 0.005:
- return audio.copy()
- audio = audio.astype(np.float64)
- n_fft_half = n_fft // 2 + 1
- f_nyq = sample_rate / 2.0
- f_hi = min(4800.0, f_nyq * 0.85)
- hop = hop_length
- window = np.hanning(n_fft)
- n_frames = max(1, (len(audio) - n_fft) // hop + 1)
- output = np.zeros(len(audio) + n_fft, dtype=np.float64)
- norm = np.zeros(len(output), dtype=np.float64)
- # Warp-Mapping vorberechnen
- freqs = np.fft.rfftfreq(n_fft, d=1.0 / sample_rate)
- warp_indices = np.zeros(n_fft_half, dtype=np.float64)
- for i in range(n_fft_half):
- warped_freq = _vtlp_warp_freq(freqs[i], alpha, f_hi, f_nyq)
- warp_indices[i] = warped_freq / (sample_rate / n_fft)
- for fi in range(n_frames):
- start = fi * hop
- end = start + n_fft
- if end > len(audio):
- break
- frame = audio[start:end] * window
- spectrum = np.fft.rfft(frame)
- magnitude = np.abs(spectrum)
- phase = np.angle(spectrum)
- # Nur Magnitude warpen
- warped_mag = np.zeros_like(magnitude)
- for i in range(n_fft_half):
- src = warp_indices[i]
- lo = int(np.floor(src))
- hi_idx = lo + 1
- frac = src - lo
- if 0 <= lo < n_fft_half:
- warped_mag[i] += (1.0 - frac) * magnitude[lo]
- if 0 <= hi_idx < n_fft_half:
- warped_mag[i] += frac * magnitude[hi_idx]
- warped_spectrum = warped_mag * np.exp(1j * phase)
- frame_out = np.fft.irfft(warped_spectrum, n=n_fft)
- output[start:end] += frame_out * window
- norm[start:end] += window ** 2
- mask = norm > 1e-8
- output[mask] /= norm[mask]
- return output[:len(audio)]
- def _vtlp_warp_freq(freq: float, alpha: float, f_hi: float, f_nyq: float) -> float:
- """Piecewise-lineare Frequenz-Warping (Jaitly & Hinton 2013)."""
- if freq <= f_hi * min(alpha, 1.0):
- return freq * alpha
- return f_nyq - (f_nyq - f_hi * alpha) / (f_nyq - f_hi * min(alpha, 1.0)) * (f_nyq - freq)
- def add_breathiness(audio: np.ndarray, sample_rate: int, amount: float) -> np.ndarray:
- """
- Fuegt sanfte Hauchigkeit (Aspiration) hinzu.
- Nutzt Lowpass-gefiltertes Rauschen das der Amplituden-Huellkurve folgt.
- """
- if amount < 0.005:
- return audio
- audio = audio.astype(np.float64)
- noise = np.random.randn(len(audio))
- from scipy.signal import butter, sosfilt
- nyq = sample_rate / 2.0
- cutoff = min(3500.0 / nyq, 0.95)
- sos = butter(2, cutoff, btype="low", output="sos")
- filtered_noise = sosfilt(sos, noise)
- # Amplituden-Huellkurve (50ms Glaettung)
- envelope = np.abs(audio)
- kernel_size = int(sample_rate * 0.05)
- if kernel_size > 1:
- kernel = np.ones(kernel_size) / kernel_size
- envelope = np.convolve(envelope, kernel, mode="same")
- filtered_noise *= envelope
- audio_rms = np.sqrt(np.mean(audio ** 2)) + 1e-10
- noise_rms = np.sqrt(np.mean(filtered_noise ** 2)) + 1e-10
- filtered_noise *= audio_rms / noise_rms
- mixed = audio + amount * filtered_noise
- max_val = np.max(np.abs(mixed))
- if max_val > 1.0:
- mixed /= max_val
- return mixed
- def add_shimmer(audio: np.ndarray, sample_rate: int, amount: float) -> np.ndarray:
- """Sanfter Shimmer via niederfrequente Amplitudenmodulation."""
- if amount < 0.005:
- return audio
- audio = audio.astype(np.float64)
- mod_noise = np.random.randn(len(audio))
- from scipy.signal import butter, sosfilt
- nyq = sample_rate / 2.0
- cutoff = min(200.0 / nyq, 0.95)
- sos = butter(2, cutoff, btype="low", output="sos")
- modulation = sosfilt(sos, mod_noise)
- max_mod = np.max(np.abs(modulation))
- if max_mod > 0:
- modulation /= max_mod
- return audio * (1.0 + amount * modulation)
- def apply_fade(audio: np.ndarray, sample_rate: int, fade_ms: float = 15.0) -> np.ndarray:
- """Wendet Fade-In/Out an um Klick-Artefakte zu vermeiden."""
- fade_samples = int(sample_rate * fade_ms / 1000)
- if fade_samples < 2 or len(audio) < 2 * fade_samples:
- return audio
- audio = audio.copy()
- audio[:fade_samples] *= np.linspace(0.0, 1.0, fade_samples)
- audio[-fade_samples:] *= np.linspace(1.0, 0.0, fade_samples)
- return audio
|