generate_test_mixes.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Generiert Crossfade-Test-Mixe.
  5. Dieses Script erstellt verschiedene Crossfade-Demonstrationen:
  6. 1. Sinuswellen mit verschiedenen Frequenzen
  7. 2. Verschiedene Crossfade-Kurven (linear, cosine, exponential)
  8. 3. Verschiedene Crossfade-Dauern
  9. Die generierten WAV-Dateien können zum Anhören verwendet werden,
  10. um die Crossfade-Qualität zu überprüfen.
  11. Verwendung:
  12. python plugins/crossfade/tests/generate_test_mixes.py
  13. Optional mit echten MP3s (erfordert pydub + ffmpeg):
  14. python plugins/crossfade/tests/generate_test_mixes.py --with-music
  15. """
  16. import argparse
  17. import math
  18. import struct
  19. import sys
  20. from pathlib import Path
  21. # Füge Plugin-Pfad hinzu
  22. sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
  23. from plugins.crossfade import crossfade_audio, save_wav
  24. # =============================================================================
  25. # Konstanten
  26. # =============================================================================
  27. OUTPUT_DIR = Path("./assets/default/music/crossfade_tests")
  28. SAMPLE_RATE = 48000
  29. CHANNELS = 2
  30. # =============================================================================
  31. # Audio-Generierung
  32. # =============================================================================
  33. def generate_sine_wave(
  34. duration_ms: int,
  35. frequency: int = 440,
  36. amplitude: float = 0.7,
  37. ) -> bytes:
  38. """
  39. Generiert einen Sinus-Ton.
  40. Args:
  41. duration_ms: Dauer in Millisekunden
  42. frequency: Frequenz in Hz
  43. amplitude: Amplitude (0.0 - 1.0)
  44. Returns:
  45. PCM-Daten (16-bit stereo)
  46. """
  47. sample_count = int(SAMPLE_RATE * duration_ms / 1000)
  48. samples = []
  49. for i in range(sample_count):
  50. t = i / SAMPLE_RATE
  51. value = int(amplitude * 32767 * math.sin(2 * math.pi * frequency * t))
  52. # Stereo
  53. samples.append(value)
  54. samples.append(value)
  55. return struct.pack(f"<{len(samples)}h", *samples)
  56. def generate_chord(
  57. duration_ms: int,
  58. frequencies: list[int],
  59. amplitude: float = 0.5,
  60. ) -> bytes:
  61. """
  62. Generiert einen Akkord (mehrere Frequenzen).
  63. Args:
  64. duration_ms: Dauer
  65. frequencies: Liste von Frequenzen
  66. amplitude: Amplitude pro Frequenz
  67. Returns:
  68. PCM-Daten
  69. """
  70. sample_count = int(SAMPLE_RATE * duration_ms / 1000)
  71. samples = []
  72. for i in range(sample_count):
  73. t = i / SAMPLE_RATE
  74. value = 0.0
  75. for freq in frequencies:
  76. value += amplitude * math.sin(2 * math.pi * freq * t)
  77. value = int(value * 32767 / len(frequencies))
  78. value = max(-32768, min(32767, value))
  79. samples.append(value)
  80. samples.append(value)
  81. return struct.pack(f"<{len(samples)}h", *samples)
  82. def generate_sweep(
  83. duration_ms: int,
  84. start_freq: int = 200,
  85. end_freq: int = 2000,
  86. amplitude: float = 0.7,
  87. ) -> bytes:
  88. """
  89. Generiert einen Frequenz-Sweep.
  90. Args:
  91. duration_ms: Dauer
  92. start_freq: Start-Frequenz
  93. end_freq: End-Frequenz
  94. amplitude: Amplitude
  95. Returns:
  96. PCM-Daten
  97. """
  98. sample_count = int(SAMPLE_RATE * duration_ms / 1000)
  99. samples = []
  100. for i in range(sample_count):
  101. t = i / SAMPLE_RATE
  102. progress = i / sample_count
  103. freq = start_freq + (end_freq - start_freq) * progress
  104. value = int(amplitude * 32767 * math.sin(2 * math.pi * freq * t))
  105. samples.append(value)
  106. samples.append(value)
  107. return struct.pack(f"<{len(samples)}h", *samples)
  108. def generate_beat(
  109. duration_ms: int,
  110. bpm: int = 120,
  111. base_freq: int = 80,
  112. amplitude: float = 0.8,
  113. ) -> bytes:
  114. """
  115. Generiert einen einfachen Beat.
  116. Args:
  117. duration_ms: Dauer
  118. bpm: Beats pro Minute
  119. base_freq: Basis-Frequenz
  120. amplitude: Amplitude
  121. Returns:
  122. PCM-Daten
  123. """
  124. sample_count = int(SAMPLE_RATE * duration_ms / 1000)
  125. samples = []
  126. beat_samples = int(SAMPLE_RATE * 60 / bpm) # Samples pro Beat
  127. for i in range(sample_count):
  128. t = i / SAMPLE_RATE
  129. beat_phase = (i % beat_samples) / beat_samples
  130. # Envelope: Schneller Attack, langsamer Decay
  131. envelope = max(0, 1.0 - beat_phase * 2) if beat_phase < 0.5 else 0
  132. value = int(envelope * amplitude * 32767 * math.sin(2 * math.pi * base_freq * t))
  133. samples.append(value)
  134. samples.append(value)
  135. return struct.pack(f"<{len(samples)}h", *samples)
  136. # =============================================================================
  137. # Demo-Generierung
  138. # =============================================================================
  139. def create_frequency_demo():
  140. """Erstellt Demo mit verschiedenen Frequenzen."""
  141. print("Erstelle Frequenz-Demo...")
  142. # Zwei verschiedene Frequenzen (Quinte)
  143. audio1 = generate_sine_wave(10000, frequency=440) # A4
  144. audio2 = generate_sine_wave(10000, frequency=660) # E5 (Quinte über A4)
  145. for curve in ["linear", "cosine", "exponential"]:
  146. result = crossfade_audio(
  147. audio1, audio2,
  148. crossfade_ms=3000,
  149. sample_rate=SAMPLE_RATE,
  150. channels=CHANNELS,
  151. fade_curve=curve,
  152. )
  153. path = OUTPUT_DIR / f"frequency_crossfade_{curve}.wav"
  154. save_wav(result, path)
  155. duration_s = len(result) / (SAMPLE_RATE * CHANNELS * 2)
  156. print(f" {path.name} ({duration_s:.1f}s)")
  157. def create_duration_demo():
  158. """Erstellt Demo mit verschiedenen Crossfade-Dauern."""
  159. print("Erstelle Dauer-Demo...")
  160. # Zwei Akkorde
  161. audio1 = generate_chord(10000, [262, 330, 392]) # C-Dur
  162. audio2 = generate_chord(10000, [294, 370, 440]) # D-Dur
  163. for duration_ms in [500, 1000, 2000, 4000]:
  164. result = crossfade_audio(
  165. audio1, audio2,
  166. crossfade_ms=duration_ms,
  167. sample_rate=SAMPLE_RATE,
  168. channels=CHANNELS,
  169. fade_curve="cosine",
  170. )
  171. path = OUTPUT_DIR / f"duration_crossfade_{duration_ms}ms.wav"
  172. save_wav(result, path)
  173. duration_s = len(result) / (SAMPLE_RATE * CHANNELS * 2)
  174. print(f" {path.name} ({duration_s:.1f}s)")
  175. def create_sweep_demo():
  176. """Erstellt Demo mit Frequenz-Sweeps."""
  177. print("Erstelle Sweep-Demo...")
  178. audio1 = generate_sweep(8000, start_freq=200, end_freq=1000)
  179. audio2 = generate_sweep(8000, start_freq=1000, end_freq=200)
  180. result = crossfade_audio(
  181. audio1, audio2,
  182. crossfade_ms=2000,
  183. sample_rate=SAMPLE_RATE,
  184. channels=CHANNELS,
  185. fade_curve="cosine",
  186. )
  187. path = OUTPUT_DIR / "sweep_crossfade.wav"
  188. save_wav(result, path)
  189. duration_s = len(result) / (SAMPLE_RATE * CHANNELS * 2)
  190. print(f" {path.name} ({duration_s:.1f}s)")
  191. def create_beat_demo():
  192. """Erstellt Demo mit Beats."""
  193. print("Erstelle Beat-Demo...")
  194. audio1 = generate_beat(10000, bpm=120, base_freq=60)
  195. audio2 = generate_beat(10000, bpm=140, base_freq=80)
  196. result = crossfade_audio(
  197. audio1, audio2,
  198. crossfade_ms=3000,
  199. sample_rate=SAMPLE_RATE,
  200. channels=CHANNELS,
  201. fade_curve="linear",
  202. )
  203. path = OUTPUT_DIR / "beat_crossfade.wav"
  204. save_wav(result, path)
  205. duration_s = len(result) / (SAMPLE_RATE * CHANNELS * 2)
  206. print(f" {path.name} ({duration_s:.1f}s)")
  207. def create_chain_demo():
  208. """Erstellt Demo mit verketteten Crossfades."""
  209. print("Erstelle Chain-Demo (4 Tracks)...")
  210. # Vier verschiedene Sounds
  211. tracks = [
  212. generate_sine_wave(8000, frequency=262), # C4
  213. generate_chord(8000, [330, 392, 494]), # E-Akkord
  214. generate_sweep(8000, start_freq=300, end_freq=600),
  215. generate_beat(8000, bpm=100),
  216. ]
  217. # Verketten mit Crossfade
  218. result = tracks[0]
  219. for i, track in enumerate(tracks[1:], 2):
  220. result = crossfade_audio(
  221. result, track,
  222. crossfade_ms=2000,
  223. sample_rate=SAMPLE_RATE,
  224. channels=CHANNELS,
  225. fade_curve="cosine",
  226. )
  227. print(f" Track {i} hinzugefügt...")
  228. path = OUTPUT_DIR / "chain_crossfade_4_tracks.wav"
  229. save_wav(result, path)
  230. duration_s = len(result) / (SAMPLE_RATE * CHANNELS * 2)
  231. print(f" {path.name} ({duration_s:.1f}s)")
  232. def create_music_demo():
  233. """Erstellt Demo mit echten Musikdateien (erfordert pydub + ffmpeg)."""
  234. try:
  235. from pydub import AudioSegment
  236. except ImportError:
  237. print("pydub nicht installiert - überspringe Musik-Demo")
  238. return
  239. music_dir = Path("./assets/default/music")
  240. if not music_dir.exists():
  241. print("Musik-Verzeichnis nicht gefunden - überspringe Musik-Demo")
  242. return
  243. mp3_files = sorted(music_dir.glob("*.mp3"))
  244. if len(mp3_files) < 2:
  245. print("Nicht genug Musikdateien - überspringe Musik-Demo")
  246. return
  247. print("Erstelle Musik-Demo...")
  248. def load_audio(path: Path, duration_ms: int = 15000) -> bytes:
  249. """Lädt und konvertiert Audio."""
  250. audio = AudioSegment.from_file(path)
  251. audio = audio.set_channels(2)
  252. audio = audio.set_frame_rate(SAMPLE_RATE)
  253. audio = audio.set_sample_width(2)
  254. audio = audio[:duration_ms]
  255. return audio.raw_data
  256. # Erste zwei Tracks laden
  257. try:
  258. audio1 = load_audio(mp3_files[0])
  259. audio2 = load_audio(mp3_files[1])
  260. except Exception as e:
  261. print(f"Fehler beim Laden: {e}")
  262. print("(ffmpeg installiert?)")
  263. return
  264. # Crossfade mit verschiedenen Kurven
  265. for curve in ["linear", "cosine", "exponential"]:
  266. result = crossfade_audio(
  267. audio1, audio2,
  268. crossfade_ms=5000,
  269. sample_rate=SAMPLE_RATE,
  270. channels=CHANNELS,
  271. fade_curve=curve,
  272. )
  273. name1 = mp3_files[0].stem[:20]
  274. name2 = mp3_files[1].stem[:20]
  275. path = OUTPUT_DIR / f"music_{curve}_{name1}_to_{name2}.wav"
  276. save_wav(result, path)
  277. duration_s = len(result) / (SAMPLE_RATE * CHANNELS * 2)
  278. print(f" {path.name} ({duration_s:.1f}s)")
  279. # Demo-Mix mit allen verfügbaren Tracks (max 4)
  280. print("Erstelle Demo-Mix...")
  281. tracks_to_use = mp3_files[:4]
  282. result = load_audio(tracks_to_use[0], 20000)
  283. for mp3_file in tracks_to_use[1:]:
  284. audio = load_audio(mp3_file, 20000)
  285. result = crossfade_audio(
  286. result, audio,
  287. crossfade_ms=5000,
  288. sample_rate=SAMPLE_RATE,
  289. channels=CHANNELS,
  290. fade_curve="cosine",
  291. )
  292. path = OUTPUT_DIR / "music_demo_mix.wav"
  293. save_wav(result, path)
  294. duration_s = len(result) / (SAMPLE_RATE * CHANNELS * 2)
  295. size_mb = len(result) / 1024 / 1024
  296. print(f" {path.name} ({duration_s:.1f}s, {size_mb:.1f}MB)")
  297. # =============================================================================
  298. # Hauptprogramm
  299. # =============================================================================
  300. def main():
  301. parser = argparse.ArgumentParser(description="Generiert Crossfade-Test-Mixe")
  302. parser.add_argument(
  303. "--with-music",
  304. action="store_true",
  305. help="Auch echte Musikdateien verwenden (erfordert pydub + ffmpeg)",
  306. )
  307. args = parser.parse_args()
  308. # Output-Verzeichnis erstellen
  309. OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
  310. print(f"Ausgabe-Verzeichnis: {OUTPUT_DIR.absolute()}\n")
  311. # Generiere alle Demos
  312. create_frequency_demo()
  313. create_duration_demo()
  314. create_sweep_demo()
  315. create_beat_demo()
  316. create_chain_demo()
  317. if args.with_music:
  318. print()
  319. create_music_demo()
  320. print(f"\nFertig! Alle Dateien in: {OUTPUT_DIR.absolute()}")
  321. print("\nZum Anhören:")
  322. print(f" - Linux: aplay {OUTPUT_DIR}/<datei>.wav")
  323. print(f" - macOS: afplay {OUTPUT_DIR}/<datei>.wav")
  324. print(f" - Windows: start {OUTPUT_DIR}\\<datei>.wav")
  325. if __name__ == "__main__":
  326. main()