dsp.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. # -*- coding: utf-8 -*-
  2. """
  3. Basis-DSP-Funktionen fuer Voice-Morphing.
  4. Pitch-Shift, VTLP, Breathiness, Shimmer, Fade, Speed — rein numpy/scipy.
  5. """
  6. from __future__ import annotations
  7. import numpy as np
  8. def pitch_shift(audio: np.ndarray, sample_rate: int, semitones: float) -> np.ndarray:
  9. """
  10. Pitch-Shift via Sinc-Resampling.
  11. Positive Semitones = hoeher (Signal wird kuerzer).
  12. Negative Semitones = tiefer (Signal wird laenger).
  13. Nutzt scipy.signal.resample (bandlimitierte Sinc-Interpolation).
  14. """
  15. from scipy.signal import resample
  16. if abs(semitones) < 0.05:
  17. return audio.copy()
  18. audio = audio.astype(np.float64)
  19. factor = 2.0 ** (semitones / 12.0)
  20. new_len = int(len(audio) / factor)
  21. if new_len < 10:
  22. return audio.copy()
  23. result = resample(audio, new_len)
  24. # Amplitude beibehalten
  25. orig_rms = np.sqrt(np.mean(audio ** 2)) + 1e-10
  26. result_rms = np.sqrt(np.mean(result ** 2)) + 1e-10
  27. if result_rms > 1e-10:
  28. result *= orig_rms / result_rms
  29. return result
  30. def speed_change(audio: np.ndarray, sample_rate: int, factor: float) -> np.ndarray:
  31. """
  32. Aendert die Geschwindigkeit ohne Pitch-Aenderung.
  33. factor > 1.0 = schneller (kuerzeres Signal).
  34. factor < 1.0 = langsamer (laengeres Signal).
  35. Nutzt STFT-basiertes Phase-Vocoder-Prinzip via Resampling.
  36. """
  37. from scipy.signal import resample
  38. if abs(factor - 1.0) < 0.005:
  39. return audio.copy()
  40. audio = audio.astype(np.float64)
  41. new_len = int(len(audio) / factor)
  42. if new_len < 10:
  43. return audio.copy()
  44. result = resample(audio, new_len)
  45. # Amplitude beibehalten
  46. orig_rms = np.sqrt(np.mean(audio ** 2)) + 1e-10
  47. result_rms = np.sqrt(np.mean(result ** 2)) + 1e-10
  48. if result_rms > 1e-10:
  49. result *= orig_rms / result_rms
  50. return result
  51. def vtlp(audio: np.ndarray, sample_rate: int, alpha: float,
  52. n_fft: int = 1024, hop_length: int = 256) -> np.ndarray:
  53. """
  54. Vocal Tract Length Perturbation — Formant-Verschiebung.
  55. Warpt NUR die Magnitude-Spektren, behaelt originale Phasen.
  56. alpha > 1.0 = hoehere Formanten, alpha < 1.0 = tiefere.
  57. """
  58. if abs(alpha - 1.0) < 0.005:
  59. return audio.copy()
  60. audio = audio.astype(np.float64)
  61. n_fft_half = n_fft // 2 + 1
  62. f_nyq = sample_rate / 2.0
  63. f_hi = min(4800.0, f_nyq * 0.85)
  64. hop = hop_length
  65. window = np.hanning(n_fft)
  66. n_frames = max(1, (len(audio) - n_fft) // hop + 1)
  67. output = np.zeros(len(audio) + n_fft, dtype=np.float64)
  68. norm = np.zeros(len(output), dtype=np.float64)
  69. # Warp-Mapping vorberechnen
  70. freqs = np.fft.rfftfreq(n_fft, d=1.0 / sample_rate)
  71. warp_indices = np.zeros(n_fft_half, dtype=np.float64)
  72. for i in range(n_fft_half):
  73. warped_freq = _vtlp_warp_freq(freqs[i], alpha, f_hi, f_nyq)
  74. warp_indices[i] = warped_freq / (sample_rate / n_fft)
  75. for fi in range(n_frames):
  76. start = fi * hop
  77. end = start + n_fft
  78. if end > len(audio):
  79. break
  80. frame = audio[start:end] * window
  81. spectrum = np.fft.rfft(frame)
  82. magnitude = np.abs(spectrum)
  83. phase = np.angle(spectrum)
  84. # Nur Magnitude warpen
  85. warped_mag = np.zeros_like(magnitude)
  86. for i in range(n_fft_half):
  87. src = warp_indices[i]
  88. lo = int(np.floor(src))
  89. hi_idx = lo + 1
  90. frac = src - lo
  91. if 0 <= lo < n_fft_half:
  92. warped_mag[i] += (1.0 - frac) * magnitude[lo]
  93. if 0 <= hi_idx < n_fft_half:
  94. warped_mag[i] += frac * magnitude[hi_idx]
  95. warped_spectrum = warped_mag * np.exp(1j * phase)
  96. frame_out = np.fft.irfft(warped_spectrum, n=n_fft)
  97. output[start:end] += frame_out * window
  98. norm[start:end] += window ** 2
  99. mask = norm > 1e-8
  100. output[mask] /= norm[mask]
  101. return output[:len(audio)]
  102. def _vtlp_warp_freq(freq: float, alpha: float, f_hi: float, f_nyq: float) -> float:
  103. """Piecewise-lineare Frequenz-Warping (Jaitly & Hinton 2013)."""
  104. if freq <= f_hi * min(alpha, 1.0):
  105. return freq * alpha
  106. return f_nyq - (f_nyq - f_hi * alpha) / (f_nyq - f_hi * min(alpha, 1.0)) * (f_nyq - freq)
  107. def add_breathiness(audio: np.ndarray, sample_rate: int, amount: float) -> np.ndarray:
  108. """
  109. Fuegt sanfte Hauchigkeit (Aspiration) hinzu.
  110. Nutzt Lowpass-gefiltertes Rauschen das der Amplituden-Huellkurve folgt.
  111. """
  112. if amount < 0.005:
  113. return audio
  114. audio = audio.astype(np.float64)
  115. noise = np.random.randn(len(audio))
  116. from scipy.signal import butter, sosfilt
  117. nyq = sample_rate / 2.0
  118. cutoff = min(3500.0 / nyq, 0.95)
  119. sos = butter(2, cutoff, btype="low", output="sos")
  120. filtered_noise = sosfilt(sos, noise)
  121. # Amplituden-Huellkurve (50ms Glaettung)
  122. envelope = np.abs(audio)
  123. kernel_size = int(sample_rate * 0.05)
  124. if kernel_size > 1:
  125. kernel = np.ones(kernel_size) / kernel_size
  126. envelope = np.convolve(envelope, kernel, mode="same")
  127. filtered_noise *= envelope
  128. audio_rms = np.sqrt(np.mean(audio ** 2)) + 1e-10
  129. noise_rms = np.sqrt(np.mean(filtered_noise ** 2)) + 1e-10
  130. filtered_noise *= audio_rms / noise_rms
  131. mixed = audio + amount * filtered_noise
  132. max_val = np.max(np.abs(mixed))
  133. if max_val > 1.0:
  134. mixed /= max_val
  135. return mixed
  136. def add_shimmer(audio: np.ndarray, sample_rate: int, amount: float) -> np.ndarray:
  137. """Sanfter Shimmer via niederfrequente Amplitudenmodulation."""
  138. if amount < 0.005:
  139. return audio
  140. audio = audio.astype(np.float64)
  141. mod_noise = np.random.randn(len(audio))
  142. from scipy.signal import butter, sosfilt
  143. nyq = sample_rate / 2.0
  144. cutoff = min(200.0 / nyq, 0.95)
  145. sos = butter(2, cutoff, btype="low", output="sos")
  146. modulation = sosfilt(sos, mod_noise)
  147. max_mod = np.max(np.abs(modulation))
  148. if max_mod > 0:
  149. modulation /= max_mod
  150. return audio * (1.0 + amount * modulation)
  151. def apply_fade(audio: np.ndarray, sample_rate: int, fade_ms: float = 15.0) -> np.ndarray:
  152. """Wendet Fade-In/Out an um Klick-Artefakte zu vermeiden."""
  153. fade_samples = int(sample_rate * fade_ms / 1000)
  154. if fade_samples < 2 or len(audio) < 2 * fade_samples:
  155. return audio
  156. audio = audio.copy()
  157. audio[:fade_samples] *= np.linspace(0.0, 1.0, fade_samples)
  158. audio[-fade_samples:] *= np.linspace(1.0, 0.0, fade_samples)
  159. return audio