| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 |
- # -*- coding: utf-8 -*-
- """
- Audio Processor Interface.
- Basis-Interface für alle Audio-Prozessoren.
- """
- from abc import ABC, abstractmethod
- from enum import IntEnum
- from typing import TYPE_CHECKING
- if TYPE_CHECKING:
- from trixy_core.audio.processing.context import AudioProcessingContext, AudioType
- class ProcessorPriority(IntEnum):
- """
- Prioritäten für Audio-Prozessoren.
- Niedrigere Werte werden zuerst ausgeführt.
- """
- # Früheste Verarbeitung
- FIRST = 0
- # Analyse (nur lesen, nicht modifizieren)
- ANALYSIS = 100
- # Effekte die früh angewendet werden
- PRE_EFFECTS = 200
- # Ducking (Lautstärke bei Wakeword/Conversation)
- DUCKING = 300
- # Crossfade und Übergänge
- CROSSFADE = 400
- # Equalizer und Frequenz-Anpassungen
- EQUALIZER = 500
- # Späte Effekte
- POST_EFFECTS = 600
- # Lautstärke-Anpassung
- VOLUME = 700
- # Limiter/Compressor (Clipping verhindern)
- LIMITER = 800
- # Letzte Verarbeitung
- LAST = 900
- # Standard für Plugins ohne spezifische Priorität
- DEFAULT = 500
- class AudioProcessor(ABC):
- """
- Basis-Klasse für Audio-Prozessoren.
- Audio-Prozessoren verarbeiten PCM-Audio-Chunks in einer Pipeline.
- Jeder Prozessor erhält den Chunk und einen Kontext mit Informationen
- über den aktuellen Zustand (Position, Wakeword, etc.).
- Beispiel:
- class DuckingProcessor(AudioProcessor):
- def __init__(self):
- super().__init__(
- processor_id="ducking",
- name="Ducking",
- priority=ProcessorPriority.DUCKING,
- )
- self.duck_factor = 0.3
- def process(self, chunk: bytes, context: AudioProcessingContext) -> bytes:
- if context.should_duck:
- return self._apply_volume(chunk, self.duck_factor)
- return chunk
- """
- def __init__(
- self,
- processor_id: str,
- name: str,
- priority: int = ProcessorPriority.DEFAULT,
- enabled: bool = True,
- audio_types: list["AudioType"] | None = None,
- ) -> None:
- """
- Initialisiert den Prozessor.
- Args:
- processor_id: Eindeutige ID
- name: Anzeigename
- priority: Reihenfolge in der Pipeline (niedriger = früher)
- enabled: Ist der Prozessor aktiv?
- audio_types: Liste der unterstützten Audio-Typen (None = alle)
- """
- self._id = processor_id
- self._name = name
- self._priority = priority
- self._enabled = enabled
- self._audio_types = audio_types # None = alle Typen
- @property
- def id(self) -> str:
- """Eindeutige ID des Prozessors."""
- return self._id
- @property
- def name(self) -> str:
- """Anzeigename."""
- return self._name
- @property
- def priority(self) -> int:
- """Priorität in der Pipeline (niedriger = früher)."""
- return self._priority
- @property
- def enabled(self) -> bool:
- """Ist der Prozessor aktiv?"""
- return self._enabled
- @enabled.setter
- def enabled(self, value: bool) -> None:
- """Aktiviert/Deaktiviert den Prozessor."""
- self._enabled = value
- @property
- def audio_types(self) -> list["AudioType"] | None:
- """
- Unterstützte Audio-Typen.
- Returns:
- Liste der Typen oder None für alle Typen.
- """
- return self._audio_types
- def supports_audio_type(self, audio_type: "AudioType") -> bool:
- """
- Prüft ob dieser Prozessor einen Audio-Typ unterstützt.
- Args:
- audio_type: Der zu prüfende Audio-Typ
- Returns:
- True wenn unterstützt (oder wenn alle Typen unterstützt werden)
- """
- if self._audio_types is None:
- return True # Alle Typen unterstützt
- return audio_type in self._audio_types
- def should_process(self, context: "AudioProcessingContext") -> bool:
- """
- Prüft ob dieser Prozessor für den Kontext aktiv sein sollte.
- Berücksichtigt: enabled, audio_type
- Args:
- context: Der Verarbeitungskontext
- Returns:
- True wenn der Prozessor aktiv sein sollte
- """
- if not self._enabled:
- return False
- return self.supports_audio_type(context.audio_type)
- @abstractmethod
- def process(
- self,
- chunk: bytes,
- context: "AudioProcessingContext",
- ) -> bytes:
- """
- Verarbeitet einen Audio-Chunk.
- Args:
- chunk: PCM-Audio-Daten (16-bit, little-endian)
- context: Verarbeitungskontext mit Zustandsinformationen
- Returns:
- Verarbeiteter Audio-Chunk (gleiche Länge wie Eingabe)
- Note:
- - Der zurückgegebene Chunk MUSS die gleiche Länge haben
- - Für Crossfade: Zweiten Track separat laden und mixen
- - Bei Fehlern: Unveränderten Chunk zurückgeben
- """
- pass
- def on_track_start(self, context: "AudioProcessingContext") -> None:
- """
- Callback wenn ein neuer Track startet.
- Args:
- context: Verarbeitungskontext
- """
- pass
- def on_track_end(self, context: "AudioProcessingContext") -> None:
- """
- Callback wenn ein Track endet.
- Args:
- context: Verarbeitungskontext
- """
- pass
- def on_state_change(self, state_name: str, value: bool) -> None:
- """
- Callback bei Zustandsänderungen.
- Args:
- state_name: Name des Zustands (z.B. "wakeword_active")
- value: Neuer Wert
- """
- pass
- def reset(self) -> None:
- """
- Setzt den Prozessor zurück.
- Wird aufgerufen wenn die Wiedergabe gestoppt wird.
- """
- pass
- def get_config(self) -> dict:
- """
- Liefert die aktuelle Konfiguration.
- Returns:
- Konfigurations-Dictionary
- """
- return {
- "id": self._id,
- "name": self._name,
- "priority": self._priority,
- "enabled": self._enabled,
- }
- def __repr__(self) -> str:
- return (
- f"{self.__class__.__name__}("
- f"id={self._id!r}, "
- f"priority={self._priority}, "
- f"enabled={self._enabled})"
- )
- # =============================================================================
- # Hilfsfunktionen für Audio-Verarbeitung
- # =============================================================================
- def apply_volume(chunk: bytes, volume: float) -> bytes:
- """
- Wendet Lautstärke auf PCM-Daten an.
- Args:
- chunk: PCM-Audio-Daten (16-bit, little-endian)
- volume: Lautstärke-Faktor (0.0 - 1.0+)
- Returns:
- Modifizierter Chunk
- """
- import struct
- if volume == 1.0:
- return chunk
- if volume == 0.0:
- return b'\x00' * len(chunk)
- # 16-bit Samples entpacken
- num_samples = len(chunk) // 2
- samples = struct.unpack(f"<{num_samples}h", chunk)
- # Lautstärke anwenden mit Clipping
- adjusted = [
- max(-32768, min(32767, int(s * volume)))
- for s in samples
- ]
- return struct.pack(f"<{num_samples}h", *adjusted)
- def mix_chunks(chunk1: bytes, chunk2: bytes, mix: float = 0.5) -> bytes:
- """
- Mischt zwei Audio-Chunks.
- Args:
- chunk1: Erster Chunk
- chunk2: Zweiter Chunk
- mix: Mix-Verhältnis (0.0 = nur chunk1, 1.0 = nur chunk2)
- Returns:
- Gemischter Chunk
- """
- import struct
- if len(chunk1) != len(chunk2):
- # Längen angleichen (kürzeren mit Stille auffüllen)
- max_len = max(len(chunk1), len(chunk2))
- chunk1 = chunk1.ljust(max_len, b'\x00')
- chunk2 = chunk2.ljust(max_len, b'\x00')
- num_samples = len(chunk1) // 2
- samples1 = struct.unpack(f"<{num_samples}h", chunk1)
- samples2 = struct.unpack(f"<{num_samples}h", chunk2)
- # Mixen
- factor1 = 1.0 - mix
- factor2 = mix
- mixed = [
- max(-32768, min(32767, int(s1 * factor1 + s2 * factor2)))
- for s1, s2 in zip(samples1, samples2)
- ]
- return struct.pack(f"<{num_samples}h", *mixed)
- def fade_chunk(
- chunk: bytes,
- fade_in: bool = False,
- fade_out: bool = False,
- ) -> bytes:
- """
- Wendet Fade-In oder Fade-Out auf einen Chunk an.
- Args:
- chunk: PCM-Audio-Daten
- fade_in: Fade von 0 auf 1
- fade_out: Fade von 1 auf 0
- Returns:
- Chunk mit Fade
- """
- import struct
- if not fade_in and not fade_out:
- return chunk
- num_samples = len(chunk) // 2
- samples = struct.unpack(f"<{num_samples}h", chunk)
- result = []
- for i, sample in enumerate(samples):
- progress = i / max(1, num_samples - 1)
- if fade_in:
- factor = progress
- elif fade_out:
- factor = 1.0 - progress
- else:
- factor = 1.0
- result.append(max(-32768, min(32767, int(sample * factor))))
- return struct.pack(f"<{num_samples}h", *result)
|