| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175 |
- # -*- 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)
|