dynamics.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. # -*- coding: utf-8 -*-
  2. """
  3. Dynamik-Effekte fuer Voice-Morphing.
  4. Lautstaerke-abhaengige Pitch-/Tempo-Aenderungen und Pausen-Skalierung.
  5. """
  6. from __future__ import annotations
  7. import numpy as np
  8. def loud_pitch_boost(
  9. audio: np.ndarray,
  10. sample_rate: int,
  11. boost_semitones: float,
  12. threshold_db: float = -20.0,
  13. ) -> np.ndarray:
  14. """
  15. Hebt den Pitch bei lauten Stellen an.
  16. Berechnet die Lautstaerke in Fenstern und shiftet laute Abschnitte
  17. um bis zu boost_semitones nach oben (proportional zur Lautstaerke).
  18. Laengen-neutral: arbeitet frameweise mit gleicher Ausgabelaenge.
  19. """
  20. if abs(boost_semitones) < 0.05:
  21. return audio.copy()
  22. audio = audio.astype(np.float64)
  23. frame_len = int(sample_rate * 0.03) # 30ms Fenster
  24. if frame_len < 4 or len(audio) < frame_len:
  25. return audio.copy()
  26. threshold_linear = 10.0 ** (threshold_db / 20.0)
  27. output = audio.copy()
  28. n_frames = len(audio) // frame_len
  29. for i in range(n_frames):
  30. start = i * frame_len
  31. end = start + frame_len
  32. frame = audio[start:end]
  33. rms = np.sqrt(np.mean(frame ** 2))
  34. if rms < threshold_linear:
  35. continue
  36. # Proportionaler Boost basierend auf RMS ueber Threshold
  37. loudness_ratio = min(1.0, (rms - threshold_linear) / (1.0 - threshold_linear + 1e-10))
  38. semitones = boost_semitones * loudness_ratio
  39. # Pitch-Shift im Frame (Resampling + Laenge beibehalten)
  40. factor = 2.0 ** (semitones / 12.0)
  41. new_len = int(frame_len / factor)
  42. if new_len < 4:
  43. continue
  44. from scipy.signal import resample
  45. shifted = resample(frame, new_len)
  46. # Auf Originallaenge bringen (Zuschneiden oder Auffuellen)
  47. if len(shifted) >= frame_len:
  48. output[start:end] = shifted[:frame_len]
  49. else:
  50. output[start:start + len(shifted)] = shifted
  51. output[start + len(shifted):end] = 0.0
  52. return output
  53. def scale_pauses(
  54. audio: np.ndarray,
  55. sample_rate: int,
  56. pause_scale: float,
  57. silence_threshold_db: float = -40.0,
  58. min_pause_ms: float = 50.0,
  59. ) -> np.ndarray:
  60. """
  61. Skaliert die Laenge von Pausen (Stille-Abschnitten).
  62. pause_scale > 1.0 = laengere Pausen, < 1.0 = kuerzere Pausen.
  63. Aendert die Gesamtlaenge des Signals.
  64. """
  65. if abs(pause_scale - 1.0) < 0.005:
  66. return audio.copy()
  67. audio = audio.astype(np.float64)
  68. threshold_linear = 10.0 ** (silence_threshold_db / 20.0)
  69. min_pause_samples = int(sample_rate * min_pause_ms / 1000)
  70. # Stille-Erkennung frameweise (10ms)
  71. frame_len = int(sample_rate * 0.01)
  72. if frame_len < 1:
  73. return audio.copy()
  74. is_silent = np.zeros(len(audio), dtype=bool)
  75. n_frames = len(audio) // frame_len
  76. for i in range(n_frames):
  77. start = i * frame_len
  78. end = start + frame_len
  79. rms = np.sqrt(np.mean(audio[start:end] ** 2))
  80. if rms < threshold_linear:
  81. is_silent[start:end] = True
  82. # Rest am Ende
  83. if n_frames * frame_len < len(audio):
  84. tail = audio[n_frames * frame_len:]
  85. if np.sqrt(np.mean(tail ** 2)) < threshold_linear:
  86. is_silent[n_frames * frame_len:] = True
  87. # Segmente sammeln (Sprache / Stille)
  88. segments: list[tuple[bool, np.ndarray]] = []
  89. seg_start = 0
  90. current_silent = is_silent[0]
  91. for i in range(1, len(audio)):
  92. if is_silent[i] != current_silent:
  93. segments.append((current_silent, audio[seg_start:i]))
  94. seg_start = i
  95. current_silent = is_silent[i]
  96. segments.append((current_silent, audio[seg_start:]))
  97. # Pausen skalieren
  98. from scipy.signal import resample
  99. output_parts: list[np.ndarray] = []
  100. for silent, segment in segments:
  101. if silent and len(segment) >= min_pause_samples:
  102. new_len = max(1, int(len(segment) * pause_scale))
  103. scaled = resample(segment, new_len)
  104. output_parts.append(scaled)
  105. else:
  106. output_parts.append(segment)
  107. return np.concatenate(output_parts) if output_parts else audio.copy()
  108. def loud_tempo_scale(
  109. audio: np.ndarray,
  110. sample_rate: int,
  111. scale: float,
  112. threshold_db: float = -20.0,
  113. ) -> np.ndarray:
  114. """
  115. Skaliert das Tempo bei lauten Stellen.
  116. scale < 1.0 = laute Stellen schneller, > 1.0 = langsamer.
  117. Aendert die Gesamtlaenge des Signals.
  118. """
  119. if abs(scale - 1.0) < 0.005:
  120. return audio.copy()
  121. audio = audio.astype(np.float64)
  122. threshold_linear = 10.0 ** (threshold_db / 20.0)
  123. frame_len = int(sample_rate * 0.05) # 50ms Fenster
  124. if frame_len < 4 or len(audio) < frame_len:
  125. return audio.copy()
  126. from scipy.signal import resample
  127. output_parts: list[np.ndarray] = []
  128. n_frames = len(audio) // frame_len
  129. for i in range(n_frames):
  130. start = i * frame_len
  131. end = start + frame_len
  132. frame = audio[start:end]
  133. rms = np.sqrt(np.mean(frame ** 2))
  134. if rms >= threshold_linear:
  135. # Laut — Tempo skalieren
  136. loudness_ratio = min(1.0, (rms - threshold_linear) / (1.0 - threshold_linear + 1e-10))
  137. frame_scale = 1.0 + (scale - 1.0) * loudness_ratio
  138. new_len = max(4, int(frame_len * frame_scale))
  139. output_parts.append(resample(frame, new_len))
  140. else:
  141. output_parts.append(frame)
  142. # Rest am Ende
  143. if n_frames * frame_len < len(audio):
  144. output_parts.append(audio[n_frames * frame_len:])
  145. return np.concatenate(output_parts) if output_parts else audio.copy()