| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411 |
- #!/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}/<datei>.wav")
- print(f" - macOS: afplay {OUTPUT_DIR}/<datei>.wav")
- print(f" - Windows: start {OUTPUT_DIR}\\<datei>.wav")
- if __name__ == "__main__":
- main()
|