# -*- coding: utf-8 -*- """ AudioMorpher — Zentrale Klasse fuer Voice-Morphing. Verwaltet Presets und wendet DSP-Effekte in definierter Reihenfolge an. """ from __future__ import annotations from pathlib import Path import numpy as np from trixy_core.audio_morpher.presets import VoiceMorphPreset, load_presets_from_directory from trixy_core.audio_morpher.dsp import ( pitch_shift, speed_change, vtlp, add_breathiness, add_shimmer, apply_fade, ) from trixy_core.audio_morpher.dynamics import ( loud_pitch_boost, scale_pauses, loud_tempo_scale, ) from trixy_core.audio_morpher.effects import ( apply_reverb, apply_lowpass, apply_highpass, apply_flanger, apply_distortion, ) # Standard-Preset-Verzeichnis _DEFAULT_PRESET_DIR = Path(__file__).resolve().parents[2] / "assets" / "default" / "audio_morpher" class AudioMorpher: """ Voice-Morphing mit Preset-Verwaltung. Laedt Presets aus JSON-Dateien und wendet DSP-Effekte in definierter Reihenfolge an. """ def __init__(self, preset_dir: Path | str | None = None) -> None: """Laedt alle .json-Dateien im Verzeichnis.""" self._preset_dir = Path(preset_dir) if preset_dir else _DEFAULT_PRESET_DIR self._presets: list[VoiceMorphPreset] = [] self._preset_map: dict[str, VoiceMorphPreset] = {} self.reload_presets() @property def presets(self) -> list[VoiceMorphPreset]: """Alle geladenen Presets.""" return self._presets @property def preset_ids(self) -> list[str]: """IDs aller geladenen Presets.""" return [p.preset_id for p in self._presets] def get_preset(self, preset_id: str) -> VoiceMorphPreset | None: """Gibt Preset anhand der ID zurueck oder None.""" return self._preset_map.get(preset_id) def reload_presets(self, preset_dir: Path | str | None = None) -> None: """Laedt Presets neu aus dem Verzeichnis.""" if preset_dir is not None: self._preset_dir = Path(preset_dir) self._presets = load_presets_from_directory(self._preset_dir) self._preset_map = {p.preset_id: p for p in self._presets} def morph( self, audio: np.ndarray, sample_rate: int, preset: VoiceMorphPreset | str, intensity: float = 1.0, ) -> np.ndarray: """ Wendet Voice-Morphing auf Audio an. Reihenfolge: 1. Dynamik (loud_pitch_boost, scale_pauses, loud_tempo_scale) 2. Speed 3. Pitch-Shift 4. VTLP (formant_extra) 5. Breathiness 6. Shimmer """ if isinstance(preset, str): resolved = self._preset_map.get(preset) if resolved is None: return audio preset = resolved if intensity < 0.01: return audio audio = audio.astype(np.float64) # Effekt-Parameter mit Intensitaet skalieren semitones = preset.pitch_semitones * intensity spd = 1.0 + (preset.speed_factor - 1.0) * intensity formant = preset.formant_extra * intensity breath = preset.breathiness * intensity shim = preset.shimmer * intensity lpb = preset.loud_pitch_boost * intensity ps = 1.0 + (preset.pause_scale - 1.0) * intensity lts = 1.0 + (preset.loud_tempo_scale - 1.0) * intensity # 1. Dynamik-Effekte if abs(lpb) > 0.05: audio = loud_pitch_boost(audio, sample_rate, lpb) if abs(ps - 1.0) > 0.005: audio = scale_pauses(audio, sample_rate, ps) if abs(lts - 1.0) > 0.005: audio = loud_tempo_scale(audio, sample_rate, lts) # 2. Speed if abs(spd - 1.0) > 0.005: audio = speed_change(audio, sample_rate, spd) # 3. Pitch-Shift if abs(semitones) > 0.1: audio = pitch_shift(audio, sample_rate, semitones) # 4. VTLP (Formant-Verschiebung) if abs(formant) > 0.01: alpha = 1.0 + formant audio = vtlp(audio, sample_rate, alpha) # 5. Breathiness if breath > 0.005: audio = add_breathiness(audio, sample_rate, breath) # 6. Shimmer if shim > 0.005: audio = add_shimmer(audio, sample_rate, shim) # 7. Filter (Lowpass / Highpass) if preset.lowpass_cutoff > 10.0: audio = apply_lowpass(audio, sample_rate, preset.lowpass_cutoff, preset.lowpass_order) if preset.highpass_cutoff > 10.0: audio = apply_highpass(audio, sample_rate, preset.highpass_cutoff, preset.highpass_order) # 8. Distortion if preset.distortion_gain > 1.001: audio = apply_distortion(audio, sample_rate, preset.distortion_gain, preset.distortion_threshold) # 9. Flanger if preset.flanger_wet > 0.005: audio = apply_flanger(audio, sample_rate, preset.flanger_rate, preset.flanger_depth, preset.flanger_wet) # 10. Reverb if preset.reverb_wet > 0.005: audio = apply_reverb(audio, sample_rate, preset.reverb_room_size, preset.reverb_damping, preset.reverb_wet) # 11. Trim — Anfang/Ende abschneiden (z.B. VTLP-Artefakte) ltrim_samples = int(sample_rate * preset.ltrim) if preset.ltrim > 0.001 else 0 rtrim_samples = int(sample_rate * preset.rtrim) if preset.rtrim > 0.001 else 0 if ltrim_samples + rtrim_samples > 0 and ltrim_samples + rtrim_samples < len(audio): end = len(audio) - rtrim_samples if rtrim_samples > 0 else len(audio) audio = audio[ltrim_samples:end] # 8. Fade-In/Out gegen Klick-Artefakte audio = apply_fade(audio, sample_rate) return audio def apply_voice_morph( audio: np.ndarray, sample_rate: int, preset: VoiceMorphPreset | str, intensity: float = 1.0, ) -> np.ndarray: """Convenience-Funktion — erstellt AudioMorpher und wendet Preset an.""" morpher = AudioMorpher() return morpher.morph(audio, sample_rate, preset, intensity)