#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Generiert Crossfade-Test-Mixe. Dieses Script erstellt verschiedene Crossfade-Demonstrationen: 1. Sinuswellen mit verschiedenen Frequenzen 2. Verschiedene Crossfade-Kurven (linear, cosine, exponential) 3. Verschiedene Crossfade-Dauern Die generierten WAV-Dateien können zum Anhören verwendet werden, um die Crossfade-Qualität zu überprüfen. Verwendung: python plugins/crossfade/tests/generate_test_mixes.py Optional mit echten MP3s (erfordert pydub + ffmpeg): python plugins/crossfade/tests/generate_test_mixes.py --with-music """ import argparse import math import struct import sys from pathlib import Path # Füge Plugin-Pfad hinzu sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) from plugins.crossfade import crossfade_audio, save_wav # ============================================================================= # Konstanten # ============================================================================= OUTPUT_DIR = Path("./assets/default/music/crossfade_tests") SAMPLE_RATE = 48000 CHANNELS = 2 # ============================================================================= # Audio-Generierung # ============================================================================= def generate_sine_wave( duration_ms: int, frequency: int = 440, amplitude: float = 0.7, ) -> bytes: """ Generiert einen Sinus-Ton. Args: duration_ms: Dauer in Millisekunden frequency: Frequenz in Hz amplitude: Amplitude (0.0 - 1.0) Returns: PCM-Daten (16-bit stereo) """ sample_count = int(SAMPLE_RATE * duration_ms / 1000) samples = [] for i in range(sample_count): t = i / SAMPLE_RATE value = int(amplitude * 32767 * math.sin(2 * math.pi * frequency * t)) # Stereo samples.append(value) samples.append(value) return struct.pack(f"<{len(samples)}h", *samples) def generate_chord( duration_ms: int, frequencies: list[int], amplitude: float = 0.5, ) -> bytes: """ Generiert einen Akkord (mehrere Frequenzen). Args: duration_ms: Dauer frequencies: Liste von Frequenzen amplitude: Amplitude pro Frequenz Returns: PCM-Daten """ sample_count = int(SAMPLE_RATE * duration_ms / 1000) samples = [] for i in range(sample_count): t = i / SAMPLE_RATE value = 0.0 for freq in frequencies: value += amplitude * math.sin(2 * math.pi * freq * t) value = int(value * 32767 / len(frequencies)) value = max(-32768, min(32767, value)) samples.append(value) samples.append(value) return struct.pack(f"<{len(samples)}h", *samples) def generate_sweep( duration_ms: int, start_freq: int = 200, end_freq: int = 2000, amplitude: float = 0.7, ) -> bytes: """ Generiert einen Frequenz-Sweep. Args: duration_ms: Dauer start_freq: Start-Frequenz end_freq: End-Frequenz amplitude: Amplitude Returns: PCM-Daten """ sample_count = int(SAMPLE_RATE * duration_ms / 1000) samples = [] for i in range(sample_count): t = i / SAMPLE_RATE progress = i / sample_count freq = start_freq + (end_freq - start_freq) * progress value = int(amplitude * 32767 * math.sin(2 * math.pi * freq * t)) samples.append(value) samples.append(value) return struct.pack(f"<{len(samples)}h", *samples) def generate_beat( duration_ms: int, bpm: int = 120, base_freq: int = 80, amplitude: float = 0.8, ) -> bytes: """ Generiert einen einfachen Beat. Args: duration_ms: Dauer bpm: Beats pro Minute base_freq: Basis-Frequenz amplitude: Amplitude Returns: PCM-Daten """ sample_count = int(SAMPLE_RATE * duration_ms / 1000) samples = [] beat_samples = int(SAMPLE_RATE * 60 / bpm) # Samples pro Beat for i in range(sample_count): t = i / SAMPLE_RATE beat_phase = (i % beat_samples) / beat_samples # Envelope: Schneller Attack, langsamer Decay envelope = max(0, 1.0 - beat_phase * 2) if beat_phase < 0.5 else 0 value = int(envelope * amplitude * 32767 * math.sin(2 * math.pi * base_freq * t)) samples.append(value) samples.append(value) return struct.pack(f"<{len(samples)}h", *samples) # ============================================================================= # Demo-Generierung # ============================================================================= def create_frequency_demo(): """Erstellt Demo mit verschiedenen Frequenzen.""" print("Erstelle Frequenz-Demo...") # Zwei verschiedene Frequenzen (Quinte) audio1 = generate_sine_wave(10000, frequency=440) # A4 audio2 = generate_sine_wave(10000, frequency=660) # E5 (Quinte über A4) for curve in ["linear", "cosine", "exponential"]: result = crossfade_audio( audio1, audio2, crossfade_ms=3000, sample_rate=SAMPLE_RATE, channels=CHANNELS, fade_curve=curve, ) path = OUTPUT_DIR / f"frequency_crossfade_{curve}.wav" save_wav(result, path) duration_s = len(result) / (SAMPLE_RATE * CHANNELS * 2) print(f" {path.name} ({duration_s:.1f}s)") def create_duration_demo(): """Erstellt Demo mit verschiedenen Crossfade-Dauern.""" print("Erstelle Dauer-Demo...") # Zwei Akkorde audio1 = generate_chord(10000, [262, 330, 392]) # C-Dur audio2 = generate_chord(10000, [294, 370, 440]) # D-Dur for duration_ms in [500, 1000, 2000, 4000]: result = crossfade_audio( audio1, audio2, crossfade_ms=duration_ms, sample_rate=SAMPLE_RATE, channels=CHANNELS, fade_curve="cosine", ) path = OUTPUT_DIR / f"duration_crossfade_{duration_ms}ms.wav" save_wav(result, path) duration_s = len(result) / (SAMPLE_RATE * CHANNELS * 2) print(f" {path.name} ({duration_s:.1f}s)") def create_sweep_demo(): """Erstellt Demo mit Frequenz-Sweeps.""" print("Erstelle Sweep-Demo...") audio1 = generate_sweep(8000, start_freq=200, end_freq=1000) audio2 = generate_sweep(8000, start_freq=1000, end_freq=200) result = crossfade_audio( audio1, audio2, crossfade_ms=2000, sample_rate=SAMPLE_RATE, channels=CHANNELS, fade_curve="cosine", ) path = OUTPUT_DIR / "sweep_crossfade.wav" save_wav(result, path) duration_s = len(result) / (SAMPLE_RATE * CHANNELS * 2) print(f" {path.name} ({duration_s:.1f}s)") def create_beat_demo(): """Erstellt Demo mit Beats.""" print("Erstelle Beat-Demo...") audio1 = generate_beat(10000, bpm=120, base_freq=60) audio2 = generate_beat(10000, bpm=140, base_freq=80) result = crossfade_audio( audio1, audio2, crossfade_ms=3000, sample_rate=SAMPLE_RATE, channels=CHANNELS, fade_curve="linear", ) path = OUTPUT_DIR / "beat_crossfade.wav" save_wav(result, path) duration_s = len(result) / (SAMPLE_RATE * CHANNELS * 2) print(f" {path.name} ({duration_s:.1f}s)") def create_chain_demo(): """Erstellt Demo mit verketteten Crossfades.""" print("Erstelle Chain-Demo (4 Tracks)...") # Vier verschiedene Sounds tracks = [ generate_sine_wave(8000, frequency=262), # C4 generate_chord(8000, [330, 392, 494]), # E-Akkord generate_sweep(8000, start_freq=300, end_freq=600), generate_beat(8000, bpm=100), ] # Verketten mit Crossfade result = tracks[0] for i, track in enumerate(tracks[1:], 2): result = crossfade_audio( result, track, crossfade_ms=2000, sample_rate=SAMPLE_RATE, channels=CHANNELS, fade_curve="cosine", ) print(f" Track {i} hinzugefügt...") path = OUTPUT_DIR / "chain_crossfade_4_tracks.wav" save_wav(result, path) duration_s = len(result) / (SAMPLE_RATE * CHANNELS * 2) print(f" {path.name} ({duration_s:.1f}s)") def create_music_demo(): """Erstellt Demo mit echten Musikdateien (erfordert pydub + ffmpeg).""" try: from pydub import AudioSegment except ImportError: print("pydub nicht installiert - überspringe Musik-Demo") return music_dir = Path("./assets/default/music") if not music_dir.exists(): print("Musik-Verzeichnis nicht gefunden - überspringe Musik-Demo") return mp3_files = sorted(music_dir.glob("*.mp3")) if len(mp3_files) < 2: print("Nicht genug Musikdateien - überspringe Musik-Demo") return print("Erstelle Musik-Demo...") def load_audio(path: Path, duration_ms: int = 15000) -> bytes: """Lädt und konvertiert Audio.""" audio = AudioSegment.from_file(path) audio = audio.set_channels(2) audio = audio.set_frame_rate(SAMPLE_RATE) audio = audio.set_sample_width(2) audio = audio[:duration_ms] return audio.raw_data # Erste zwei Tracks laden try: audio1 = load_audio(mp3_files[0]) audio2 = load_audio(mp3_files[1]) except Exception as e: print(f"Fehler beim Laden: {e}") print("(ffmpeg installiert?)") return # Crossfade mit verschiedenen Kurven for curve in ["linear", "cosine", "exponential"]: result = crossfade_audio( audio1, audio2, crossfade_ms=5000, sample_rate=SAMPLE_RATE, channels=CHANNELS, fade_curve=curve, ) name1 = mp3_files[0].stem[:20] name2 = mp3_files[1].stem[:20] path = OUTPUT_DIR / f"music_{curve}_{name1}_to_{name2}.wav" save_wav(result, path) duration_s = len(result) / (SAMPLE_RATE * CHANNELS * 2) print(f" {path.name} ({duration_s:.1f}s)") # Demo-Mix mit allen verfügbaren Tracks (max 4) print("Erstelle Demo-Mix...") tracks_to_use = mp3_files[:4] result = load_audio(tracks_to_use[0], 20000) for mp3_file in tracks_to_use[1:]: audio = load_audio(mp3_file, 20000) result = crossfade_audio( result, audio, crossfade_ms=5000, sample_rate=SAMPLE_RATE, channels=CHANNELS, fade_curve="cosine", ) path = OUTPUT_DIR / "music_demo_mix.wav" save_wav(result, path) duration_s = len(result) / (SAMPLE_RATE * CHANNELS * 2) size_mb = len(result) / 1024 / 1024 print(f" {path.name} ({duration_s:.1f}s, {size_mb:.1f}MB)") # ============================================================================= # Hauptprogramm # ============================================================================= def main(): parser = argparse.ArgumentParser(description="Generiert Crossfade-Test-Mixe") parser.add_argument( "--with-music", action="store_true", help="Auch echte Musikdateien verwenden (erfordert pydub + ffmpeg)", ) args = parser.parse_args() # Output-Verzeichnis erstellen OUTPUT_DIR.mkdir(parents=True, exist_ok=True) print(f"Ausgabe-Verzeichnis: {OUTPUT_DIR.absolute()}\n") # Generiere alle Demos create_frequency_demo() create_duration_demo() create_sweep_demo() create_beat_demo() create_chain_demo() if args.with_music: print() create_music_demo() print(f"\nFertig! Alle Dateien in: {OUTPUT_DIR.absolute()}") print("\nZum Anhören:") print(f" - Linux: aplay {OUTPUT_DIR}/.wav") print(f" - macOS: afplay {OUTPUT_DIR}/.wav") print(f" - Windows: start {OUTPUT_DIR}\\.wav") if __name__ == "__main__": main()