| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852 |
- # -*- coding: utf-8 -*-
- """
- Tests für Crossfade-Plugin.
- Diese Tests:
- 1. Testen den CrossfadeProcessor
- 2. Testen die crossfade_audio() Funktion
- 3. Erstellen echte Crossfade-Mixes aus Musikdateien
- 4. Speichern Testdateien zum Anhören
- """
- import struct
- import pytest
- from pathlib import Path
- from unittest.mock import MagicMock, AsyncMock
- # Plugin-Imports
- from plugins.crossfade import (
- CrossfadePlugin,
- CrossfadeProcessor,
- CrossfadeConfig,
- crossfade_audio,
- save_wav,
- load_wav,
- )
- from trixy_core.audio.processing import (
- AudioProcessingContext,
- ProcessorPriority,
- )
- # =============================================================================
- # Konstanten
- # =============================================================================
- MUSIC_DIR = Path("./assets/default/music")
- OUTPUT_DIR = Path("./assets/default/music/crossfade_tests")
- # Audio-Parameter
- SAMPLE_RATE = 48000
- CHANNELS = 2
- BIT_DEPTH = 16
- BYTES_PER_MS = (SAMPLE_RATE * CHANNELS * 2) // 1000
- # =============================================================================
- # Hilfsfunktionen
- # =============================================================================
- def has_pydub() -> bool:
- """Prüft ob pydub und ffmpeg verfügbar sind."""
- import subprocess
- try:
- from pydub import AudioSegment
- # Prüfe auch ob ffprobe verfügbar ist
- result = subprocess.run(
- ["ffprobe", "-version"],
- capture_output=True,
- timeout=5,
- )
- return result.returncode == 0
- except (ImportError, FileNotFoundError, subprocess.TimeoutExpired):
- return False
- def has_music_files() -> bool:
- """Prüft ob Musikdateien verfügbar sind."""
- return MUSIC_DIR.exists() and any(MUSIC_DIR.glob("*.mp3"))
- def get_music_files() -> list[Path]:
- """Liefert alle Musikdateien."""
- if not MUSIC_DIR.exists():
- return []
- files = list(MUSIC_DIR.glob("*.mp3"))
- files.extend(MUSIC_DIR.glob("*.wav"))
- files.extend(MUSIC_DIR.glob("*.ogg"))
- files.extend(MUSIC_DIR.glob("*.flac"))
- return sorted(files)
- def decode_mp3_to_pcm(path: Path, duration_ms: int | None = None) -> tuple[bytes, int, int]:
- """
- Dekodiert MP3 zu PCM mit pydub.
- Args:
- path: Pfad zur MP3-Datei
- duration_ms: Maximale Dauer in ms (None = komplett)
- Returns:
- Tuple (pcm_data, sample_rate, channels)
- """
- from pydub import AudioSegment
- audio = AudioSegment.from_file(path)
- # Auf Stereo und 48kHz konvertieren
- audio = audio.set_channels(2)
- audio = audio.set_frame_rate(SAMPLE_RATE)
- audio = audio.set_sample_width(2) # 16-bit
- # Kürzen wenn gewünscht
- if duration_ms:
- audio = audio[:duration_ms]
- return audio.raw_data, audio.frame_rate, audio.channels
- def generate_sine_wave(
- duration_ms: int,
- frequency: int = 440,
- sample_rate: int = 48000,
- channels: int = 2,
- amplitude: float = 0.5,
- ) -> bytes:
- """
- Generiert einen Sinus-Ton als PCM-Daten.
- Args:
- duration_ms: Dauer in Millisekunden
- frequency: Frequenz in Hz
- sample_rate: Sample-Rate
- channels: Anzahl Kanäle
- amplitude: Amplitude (0.0 - 1.0)
- Returns:
- PCM-Daten (16-bit signed, little-endian)
- """
- import math
- 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: Gleiches Sample für beide Kanäle
- for _ in range(channels):
- samples.append(value)
- return struct.pack(f"<{len(samples)}h", *samples)
- def generate_test_audio(duration_ms: int, pattern: str = "sine") -> bytes:
- """
- Generiert Test-Audio.
- Args:
- duration_ms: Dauer
- pattern: "sine", "silence", "noise"
- Returns:
- PCM-Daten
- """
- if pattern == "silence":
- sample_count = int(SAMPLE_RATE * duration_ms / 1000) * CHANNELS
- return struct.pack(f"<{sample_count}h", *([0] * sample_count))
- elif pattern == "noise":
- import random
- sample_count = int(SAMPLE_RATE * duration_ms / 1000) * CHANNELS
- samples = [random.randint(-16384, 16384) for _ in range(sample_count)]
- return struct.pack(f"<{sample_count}h", *samples)
- else:
- return generate_sine_wave(duration_ms)
- # =============================================================================
- # Tests für CrossfadeConfig
- # =============================================================================
- class TestCrossfadeConfig:
- """Tests für CrossfadeConfig."""
- def test_default_values(self):
- """Testet Standard-Werte."""
- config = CrossfadeConfig()
- assert config.duration_ms == 5000
- assert config.fade_curve == "linear"
- assert config.min_track_duration_ms == 30000
- assert config.prebuffer_duration_ms == 6000
- assert config.enabled is True
- def test_custom_values(self):
- """Testet benutzerdefinierte Werte."""
- config = CrossfadeConfig(
- duration_ms=3000,
- fade_curve="cosine",
- min_track_duration_ms=20000,
- enabled=False,
- )
- assert config.duration_ms == 3000
- assert config.fade_curve == "cosine"
- assert config.min_track_duration_ms == 20000
- assert config.enabled is False
- # =============================================================================
- # Tests für CrossfadeProcessor
- # =============================================================================
- class TestCrossfadeProcessor:
- """Tests für CrossfadeProcessor."""
- @pytest.fixture
- def processor(self):
- """Erstellt einen CrossfadeProcessor."""
- return CrossfadeProcessor(
- config=CrossfadeConfig(
- duration_ms=2000,
- min_track_duration_ms=5000,
- )
- )
- @pytest.fixture
- def mock_track(self):
- """Erstellt einen Mock-Track."""
- track = MagicMock()
- track.id = "track_1"
- track.path = Path("/fake/path.mp3")
- track.duration_ms = 180000 # 3 Minuten
- track.metadata.format = "mp3"
- return track
- @pytest.fixture
- def mock_next_track(self):
- """Erstellt einen Mock für den nächsten Track."""
- track = MagicMock()
- track.id = "track_2"
- track.path = Path("/fake/path2.mp3")
- track.duration_ms = 200000
- track.metadata.format = "mp3"
- return track
- def test_processor_creation(self, processor):
- """Testet Prozessor-Erstellung."""
- assert processor.id == "crossfade"
- assert processor.name == "Crossfade"
- assert processor.priority == ProcessorPriority.CROSSFADE
- assert processor.enabled is True
- def test_processor_priority(self, processor):
- """Testet korrekte Priorität."""
- assert processor.priority == 400 # ProcessorPriority.CROSSFADE
- def test_process_passthrough_when_disabled(self, processor):
- """Testet Passthrough wenn deaktiviert."""
- processor.enabled = False
- chunk = b'\x00' * 1000
- context = AudioProcessingContext()
- result = processor.process(chunk, context)
- assert result == chunk
- def test_process_passthrough_no_next_track(self, processor, mock_track):
- """Testet Passthrough ohne nächsten Track."""
- chunk = b'\x00' * 1000
- context = AudioProcessingContext(
- track=mock_track,
- next_track=None,
- position_ms=178000, # Nahe Ende
- duration_ms=180000,
- sample_rate=SAMPLE_RATE,
- channels=CHANNELS,
- )
- result = processor.process(chunk, context)
- # Kein Crossfade ohne nächsten Track, aber fade-out möglich
- assert len(result) == len(chunk)
- def test_process_passthrough_not_near_end(self, processor, mock_track, mock_next_track):
- """Testet Passthrough wenn nicht nahe am Ende."""
- chunk = generate_test_audio(100) # 100ms
- context = AudioProcessingContext(
- track=mock_track,
- next_track=mock_next_track,
- position_ms=60000, # Mitte des Tracks
- duration_ms=180000,
- sample_rate=SAMPLE_RATE,
- channels=CHANNELS,
- )
- result = processor.process(chunk, context)
- # Nicht in Crossfade-Region - unveränderter Chunk
- assert result == chunk
- def test_process_fades_out_near_end_with_next(self, processor, mock_track, mock_next_track):
- """Testet Fade-Out nahe am Ende mit nächstem Track aber ohne Puffer."""
- chunk = generate_test_audio(100)
- context = AudioProcessingContext(
- track=mock_track,
- next_track=mock_next_track, # Nächster Track vorhanden
- position_ms=179000, # 1 Sekunde vor Ende
- duration_ms=180000,
- sample_rate=SAMPLE_RATE,
- channels=CHANNELS,
- )
- # Kein Puffer geladen - nur Fade-Out anwenden
- result = processor.process(chunk, context)
- # Chunk sollte leiser sein (fade-out ohne gemischten nächsten Track)
- assert len(result) == len(chunk)
- # Vergleiche RMS-Lautstärke
- original_rms = calculate_rms(chunk)
- result_rms = calculate_rms(result)
- # Bei 1 Sekunde verbleibend und 2 Sekunden Crossfade: 50% Lautstärke
- assert result_rms < original_rms * 0.6 # Sollte deutlich leiser sein
- def test_preload_from_pcm(self, processor):
- """Testet direktes Laden von PCM-Daten."""
- pcm_data = generate_test_audio(1000)
- processor.preload_from_pcm(pcm_data)
- assert processor._next_track_loaded is True
- assert len(processor._next_track_buffer) > 0
- def test_crossfade_mixing(self, processor, mock_track, mock_next_track):
- """Testet Mischen während Crossfade."""
- # Nächsten Track vorladen
- next_audio = generate_sine_wave(3000, frequency=880) # Andere Frequenz
- processor.preload_from_pcm(next_audio)
- # Kontext nahe am Ende
- context = AudioProcessingContext(
- track=mock_track,
- next_track=mock_next_track,
- position_ms=179000, # 1 Sekunde vor Ende
- duration_ms=180000,
- sample_rate=SAMPLE_RATE,
- channels=CHANNELS,
- )
- chunk = generate_sine_wave(100, frequency=440) # 100ms bei 440Hz
- result = processor.process(chunk, context)
- # Ergebnis sollte gemischtes Audio sein
- assert len(result) == len(chunk)
- # Sollte sich vom Original unterscheiden (weil gemischt)
- assert result != chunk
- def test_on_track_start(self, processor, mock_track, mock_next_track):
- """Testet on_track_start."""
- context = AudioProcessingContext(
- track=mock_track,
- next_track=mock_next_track,
- duration_ms=180000,
- )
- processor.on_track_start(context)
- assert processor._current_track == mock_track
- assert processor._next_track == mock_next_track
- assert processor._is_crossfading is False
- def test_on_track_end(self, processor, mock_track):
- """Testet on_track_end."""
- processor._is_crossfading = True
- context = AudioProcessingContext(track=mock_track)
- processor.on_track_end(context)
- assert processor._is_crossfading is False
- assert processor._crossfades_performed == 1
- def test_reset(self, processor):
- """Testet Reset."""
- processor._is_crossfading = True
- processor.preload_from_pcm(b'\x00' * 1000)
- processor.reset()
- assert processor._is_crossfading is False
- assert processor._next_track_loaded is False
- assert len(processor._next_track_buffer) == 0
- def test_get_config(self, processor):
- """Testet get_config."""
- config = processor.get_config()
- assert config["id"] == "crossfade"
- assert config["crossfade_duration_ms"] == 2000
- assert config["fade_curve"] == "linear"
- assert "crossfades_performed" in config
- assert "is_crossfading" in config
- def test_track_too_short(self, processor, mock_track, mock_next_track):
- """Testet, dass zu kurze Tracks nicht crossfaden."""
- mock_track.duration_ms = 3000 # Kürzer als min_track_duration_ms
- context = AudioProcessingContext(
- track=mock_track,
- next_track=mock_next_track,
- position_ms=2000,
- duration_ms=3000,
- sample_rate=SAMPLE_RATE,
- channels=CHANNELS,
- )
- chunk = generate_test_audio(100)
- result = processor.process(chunk, context)
- # Sollte unverändert sein (Track zu kurz)
- assert result == chunk
- def calculate_rms(pcm_data: bytes) -> float:
- """Berechnet RMS-Lautstärke."""
- samples = struct.unpack(f"<{len(pcm_data)//2}h", pcm_data)
- if not samples:
- return 0.0
- rms = (sum(s**2 for s in samples) / len(samples)) ** 0.5
- return rms
- # =============================================================================
- # Tests für crossfade_audio()
- # =============================================================================
- class TestCrossfadeAudio:
- """Tests für die crossfade_audio() Funktion."""
- def test_crossfade_sine_waves(self):
- """Testet Crossfade zwischen zwei Sinuswellen."""
- # Zwei verschiedene Frequenzen
- audio1 = generate_sine_wave(5000, frequency=440) # A4
- audio2 = generate_sine_wave(5000, frequency=880) # A5
- result = crossfade_audio(
- audio1, audio2,
- crossfade_ms=2000,
- sample_rate=SAMPLE_RATE,
- channels=CHANNELS,
- )
- # Ergebnis sollte kürzer sein als beide zusammen
- expected_length = len(audio1) + len(audio2) - (2000 * BYTES_PER_MS)
- assert len(result) == expected_length
- def test_crossfade_linear_curve(self):
- """Testet lineare Fade-Kurve."""
- audio1 = generate_test_audio(3000)
- audio2 = generate_test_audio(3000)
- result = crossfade_audio(
- audio1, audio2,
- crossfade_ms=1000,
- fade_curve="linear",
- )
- assert len(result) > 0
- def test_crossfade_cosine_curve(self):
- """Testet Cosinus-Fade-Kurve."""
- audio1 = generate_test_audio(3000)
- audio2 = generate_test_audio(3000)
- result = crossfade_audio(
- audio1, audio2,
- crossfade_ms=1000,
- fade_curve="cosine",
- )
- assert len(result) > 0
- def test_crossfade_exponential_curve(self):
- """Testet exponentielle Fade-Kurve."""
- audio1 = generate_test_audio(3000)
- audio2 = generate_test_audio(3000)
- result = crossfade_audio(
- audio1, audio2,
- crossfade_ms=1000,
- fade_curve="exponential",
- )
- assert len(result) > 0
- def test_crossfade_too_short_audio(self):
- """Testet Verhalten bei zu kurzen Audio-Daten."""
- audio1 = generate_test_audio(500) # 500ms
- audio2 = generate_test_audio(500)
- # Crossfade länger als Audio - sollte einfach verketten
- result = crossfade_audio(
- audio1, audio2,
- crossfade_ms=2000,
- )
- # Sollte beide komplett enthalten
- assert len(result) == len(audio1) + len(audio2)
- # =============================================================================
- # Tests für WAV I/O
- # =============================================================================
- class TestWavIO:
- """Tests für WAV Ein-/Ausgabe."""
- def test_save_and_load_wav(self, tmp_path):
- """Testet Speichern und Laden."""
- pcm_data = generate_test_audio(1000)
- wav_path = tmp_path / "test.wav"
- save_wav(pcm_data, wav_path, SAMPLE_RATE, CHANNELS)
- assert wav_path.exists()
- loaded, sample_rate, channels = load_wav(wav_path)
- assert sample_rate == SAMPLE_RATE
- assert channels == CHANNELS
- assert len(loaded) == len(pcm_data)
- def test_save_creates_directories(self, tmp_path):
- """Testet, dass Verzeichnisse erstellt werden."""
- wav_path = tmp_path / "nested" / "dir" / "test.wav"
- pcm_data = generate_test_audio(100)
- save_wav(pcm_data, wav_path)
- assert wav_path.exists()
- # =============================================================================
- # Integrationstests mit echten Musikdateien
- # =============================================================================
- @pytest.mark.skipif(not has_pydub(), reason="pydub nicht installiert")
- @pytest.mark.skipif(not has_music_files(), reason="Keine Musikdateien vorhanden")
- class TestRealMusicCrossfade:
- """
- Integrationstests mit echten Musikdateien.
- Diese Tests erstellen echte Crossfade-Mixes und speichern sie
- in ./assets/default/music/crossfade_tests/
- """
- @pytest.fixture(autouse=True)
- def setup_output_dir(self):
- """Erstellt Output-Verzeichnis."""
- OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
- def test_crossfade_two_tracks(self):
- """Testet Crossfade zwischen zwei echten Tracks."""
- files = get_music_files()
- if len(files) < 2:
- pytest.skip("Nicht genug Musikdateien")
- file1, file2 = files[0], files[1]
- # Lade nur die ersten 15 Sekunden von jedem Track
- audio1, sr1, ch1 = decode_mp3_to_pcm(file1, duration_ms=15000)
- audio2, sr2, ch2 = decode_mp3_to_pcm(file2, duration_ms=15000)
- # Crossfade erstellen (5 Sekunden)
- crossfaded = crossfade_audio(
- audio1, audio2,
- crossfade_ms=5000,
- sample_rate=SAMPLE_RATE,
- channels=CHANNELS,
- fade_curve="linear",
- )
- # Speichern
- output_path = OUTPUT_DIR / f"crossfade_linear_{file1.stem}_to_{file2.stem}.wav"
- save_wav(crossfaded, output_path)
- assert output_path.exists()
- print(f"\nGespeichert: {output_path}")
- def test_crossfade_different_curves(self):
- """Testet verschiedene Fade-Kurven."""
- files = get_music_files()
- if len(files) < 2:
- pytest.skip("Nicht genug Musikdateien")
- file1, file2 = files[0], files[1]
- # Lade Audio
- audio1, _, _ = decode_mp3_to_pcm(file1, duration_ms=15000)
- audio2, _, _ = decode_mp3_to_pcm(file2, duration_ms=15000)
- for curve in ["linear", "cosine", "exponential"]:
- crossfaded = crossfade_audio(
- audio1, audio2,
- crossfade_ms=5000,
- sample_rate=SAMPLE_RATE,
- channels=CHANNELS,
- fade_curve=curve,
- )
- output_path = OUTPUT_DIR / f"crossfade_{curve}_{file1.stem}_to_{file2.stem}.wav"
- save_wav(crossfaded, output_path)
- assert output_path.exists()
- print(f"Gespeichert ({curve}): {output_path}")
- def test_crossfade_different_durations(self):
- """Testet verschiedene Crossfade-Dauern."""
- files = get_music_files()
- if len(files) < 2:
- pytest.skip("Nicht genug Musikdateien")
- file1, file2 = files[0], files[1]
- audio1, _, _ = decode_mp3_to_pcm(file1, duration_ms=20000)
- audio2, _, _ = decode_mp3_to_pcm(file2, duration_ms=20000)
- for duration_ms in [1000, 3000, 5000, 8000]:
- crossfaded = crossfade_audio(
- audio1, audio2,
- crossfade_ms=duration_ms,
- sample_rate=SAMPLE_RATE,
- channels=CHANNELS,
- )
- output_path = OUTPUT_DIR / f"crossfade_{duration_ms}ms_{file1.stem}_to_{file2.stem}.wav"
- save_wav(crossfaded, output_path)
- assert output_path.exists()
- print(f"Gespeichert ({duration_ms}ms): {output_path}")
- def test_crossfade_chain_three_tracks(self):
- """Testet Verkettung von drei Tracks mit Crossfade."""
- files = get_music_files()
- if len(files) < 3:
- pytest.skip("Nicht genug Musikdateien")
- file1, file2, file3 = files[0], files[1], files[2]
- # Lade Audio
- audio1, _, _ = decode_mp3_to_pcm(file1, duration_ms=15000)
- audio2, _, _ = decode_mp3_to_pcm(file2, duration_ms=15000)
- audio3, _, _ = decode_mp3_to_pcm(file3, duration_ms=15000)
- # Erste Crossfade
- mixed1 = crossfade_audio(
- audio1, audio2,
- crossfade_ms=4000,
- sample_rate=SAMPLE_RATE,
- channels=CHANNELS,
- )
- # Zweite Crossfade
- mixed2 = crossfade_audio(
- mixed1, audio3,
- crossfade_ms=4000,
- sample_rate=SAMPLE_RATE,
- channels=CHANNELS,
- )
- output_path = OUTPUT_DIR / f"crossfade_chain_{file1.stem}_{file2.stem}_{file3.stem}.wav"
- save_wav(mixed2, output_path)
- assert output_path.exists()
- print(f"\nGespeichert (3-Track-Chain): {output_path}")
- def test_create_demo_mix(self):
- """Erstellt einen Demo-Mix aus allen verfügbaren Tracks."""
- files = get_music_files()
- if len(files) < 2:
- pytest.skip("Nicht genug Musikdateien")
- # Maximal 4 Tracks für Demo
- files = files[:4]
- crossfade_ms = 5000
- segment_length = 20000 # 20 Sekunden pro Track
- print(f"\nErstelle Demo-Mix aus {len(files)} Tracks...")
- # Ersten Track laden
- result, _, _ = decode_mp3_to_pcm(files[0], duration_ms=segment_length)
- print(f" Track 1: {files[0].name}")
- # Weitere Tracks hinzufügen
- for i, file in enumerate(files[1:], 2):
- audio, _, _ = decode_mp3_to_pcm(file, duration_ms=segment_length)
- print(f" Track {i}: {file.name}")
- result = crossfade_audio(
- result, audio,
- crossfade_ms=crossfade_ms,
- sample_rate=SAMPLE_RATE,
- channels=CHANNELS,
- fade_curve="cosine",
- )
- # Speichern
- output_path = OUTPUT_DIR / "demo_mix.wav"
- save_wav(result, output_path)
- assert output_path.exists()
- # Dauer berechnen
- duration_seconds = len(result) / (SAMPLE_RATE * CHANNELS * 2)
- print(f"\nDemo-Mix gespeichert: {output_path}")
- print(f"Dauer: {duration_seconds:.1f} Sekunden")
- print(f"Größe: {len(result) / 1024 / 1024:.1f} MB")
- # =============================================================================
- # Tests für CrossfadePlugin
- # =============================================================================
- class TestCrossfadePlugin:
- """Tests für CrossfadePlugin."""
- @pytest.fixture
- def plugin(self, tmp_path):
- """Erstellt ein CrossfadePlugin."""
- config = {
- "enabled": True,
- "crossfade_duration_ms": 5000,
- "fade_curve": "linear",
- }
- return CrossfadePlugin(
- application=None,
- plugin_path=tmp_path,
- config=config,
- )
- @pytest.mark.asyncio
- async def test_plugin_load(self, plugin):
- """Testet Plugin-Laden."""
- await plugin.on_load()
- assert plugin.processor is not None
- assert plugin.processor.config.duration_ms == 5000
- @pytest.mark.asyncio
- async def test_plugin_enable_disable(self, plugin):
- """Testet Aktivieren/Deaktivieren."""
- await plugin.on_load()
- await plugin.on_enable()
- assert plugin.processor.enabled is True
- await plugin.on_disable()
- assert plugin.processor.enabled is False
- @pytest.mark.asyncio
- async def test_plugin_config_change(self, plugin):
- """Testet Konfigurationsänderung."""
- await plugin.on_load()
- old_config = plugin.config.copy()
- plugin.config["crossfade_duration_ms"] = 3000
- await plugin.on_config_change(old_config)
- assert plugin.processor.config.duration_ms == 3000
- @pytest.mark.asyncio
- async def test_plugin_unload(self, plugin):
- """Testet Plugin-Entladen."""
- await plugin.on_load()
- await plugin.on_unload()
- assert plugin.processor is None
- @pytest.mark.asyncio
- async def test_plugin_stats(self, plugin):
- """Testet Statistiken."""
- await plugin.on_load()
- stats = plugin.get_stats()
- assert "crossfade_duration_ms" in stats
- assert "crossfades_performed" in stats
- # =============================================================================
- # Performance-Tests
- # =============================================================================
- class TestCrossfadePerformance:
- """Performance-Tests für Crossfade."""
- def test_crossfade_processing_speed(self):
- """Testet Verarbeitungsgeschwindigkeit."""
- import time
- processor = CrossfadeProcessor(
- config=CrossfadeConfig(duration_ms=2000)
- )
- # Nächsten Track vorladen
- next_audio = generate_test_audio(5000)
- processor.preload_from_pcm(next_audio)
- # Mock-Track für Kontext
- track = MagicMock()
- track.duration_ms = 180000
- # 100 Chunks verarbeiten (je 100ms)
- chunks = [generate_test_audio(100) for _ in range(100)]
- start = time.perf_counter()
- for i, chunk in enumerate(chunks):
- # Position im Crossfade-Bereich
- position_ms = 178000 + (i * 10) # 178-179 Sekunden
- context = AudioProcessingContext(
- track=track,
- next_track=track, # Nächster Track vorhanden
- position_ms=position_ms,
- duration_ms=180000,
- sample_rate=SAMPLE_RATE,
- channels=CHANNELS,
- )
- processor.process(chunk, context)
- elapsed = time.perf_counter() - start
- print(f"\n100 Chunks verarbeitet in {elapsed*1000:.2f}ms")
- print(f"Durchschnitt: {elapsed*10:.2f}ms pro Chunk")
- # Sollte schnell genug für Echtzeit sein (< 100ms pro Chunk)
- assert elapsed < 1.0 # 100 Chunks in unter 1 Sekunde
- def test_crossfade_audio_speed(self):
- """Testet Geschwindigkeit der crossfade_audio() Funktion."""
- import time
- # Zwei 30-Sekunden Tracks
- audio1 = generate_test_audio(30000)
- audio2 = generate_test_audio(30000)
- start = time.perf_counter()
- result = crossfade_audio(
- audio1, audio2,
- crossfade_ms=5000,
- sample_rate=SAMPLE_RATE,
- channels=CHANNELS,
- )
- elapsed = time.perf_counter() - start
- print(f"\n30s + 30s Tracks mit 5s Crossfade: {elapsed*1000:.2f}ms")
- # Sollte schnell sein (< 1 Sekunde)
- assert elapsed < 1.0
- assert len(result) > 0
|