morpher.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. # -*- coding: utf-8 -*-
  2. """
  3. AudioMorpher — Zentrale Klasse fuer Voice-Morphing.
  4. Verwaltet Presets und wendet DSP-Effekte in definierter Reihenfolge an.
  5. """
  6. from __future__ import annotations
  7. from pathlib import Path
  8. import numpy as np
  9. from trixy_core.audio_morpher.presets import VoiceMorphPreset, load_presets_from_directory
  10. from trixy_core.audio_morpher.dsp import (
  11. pitch_shift, speed_change, vtlp, add_breathiness, add_shimmer, apply_fade,
  12. )
  13. from trixy_core.audio_morpher.dynamics import (
  14. loud_pitch_boost, scale_pauses, loud_tempo_scale,
  15. )
  16. from trixy_core.audio_morpher.effects import (
  17. apply_reverb, apply_lowpass, apply_highpass, apply_flanger, apply_distortion,
  18. )
  19. # Standard-Preset-Verzeichnis
  20. _DEFAULT_PRESET_DIR = Path(__file__).resolve().parents[2] / "assets" / "default" / "audio_morpher"
  21. class AudioMorpher:
  22. """
  23. Voice-Morphing mit Preset-Verwaltung.
  24. Laedt Presets aus JSON-Dateien und wendet DSP-Effekte in
  25. definierter Reihenfolge an.
  26. """
  27. def __init__(self, preset_dir: Path | str | None = None) -> None:
  28. """Laedt alle .json-Dateien im Verzeichnis."""
  29. self._preset_dir = Path(preset_dir) if preset_dir else _DEFAULT_PRESET_DIR
  30. self._presets: list[VoiceMorphPreset] = []
  31. self._preset_map: dict[str, VoiceMorphPreset] = {}
  32. self.reload_presets()
  33. @property
  34. def presets(self) -> list[VoiceMorphPreset]:
  35. """Alle geladenen Presets."""
  36. return self._presets
  37. @property
  38. def preset_ids(self) -> list[str]:
  39. """IDs aller geladenen Presets."""
  40. return [p.preset_id for p in self._presets]
  41. def get_preset(self, preset_id: str) -> VoiceMorphPreset | None:
  42. """Gibt Preset anhand der ID zurueck oder None."""
  43. return self._preset_map.get(preset_id)
  44. def reload_presets(self, preset_dir: Path | str | None = None) -> None:
  45. """Laedt Presets neu aus dem Verzeichnis."""
  46. if preset_dir is not None:
  47. self._preset_dir = Path(preset_dir)
  48. self._presets = load_presets_from_directory(self._preset_dir)
  49. self._preset_map = {p.preset_id: p for p in self._presets}
  50. def morph(
  51. self,
  52. audio: np.ndarray,
  53. sample_rate: int,
  54. preset: VoiceMorphPreset | str,
  55. intensity: float = 1.0,
  56. ) -> np.ndarray:
  57. """
  58. Wendet Voice-Morphing auf Audio an.
  59. Reihenfolge:
  60. 1. Dynamik (loud_pitch_boost, scale_pauses, loud_tempo_scale)
  61. 2. Speed
  62. 3. Pitch-Shift
  63. 4. VTLP (formant_extra)
  64. 5. Breathiness
  65. 6. Shimmer
  66. """
  67. if isinstance(preset, str):
  68. resolved = self._preset_map.get(preset)
  69. if resolved is None:
  70. return audio
  71. preset = resolved
  72. if intensity < 0.01:
  73. return audio
  74. audio = audio.astype(np.float64)
  75. # Effekt-Parameter mit Intensitaet skalieren
  76. semitones = preset.pitch_semitones * intensity
  77. spd = 1.0 + (preset.speed_factor - 1.0) * intensity
  78. formant = preset.formant_extra * intensity
  79. breath = preset.breathiness * intensity
  80. shim = preset.shimmer * intensity
  81. lpb = preset.loud_pitch_boost * intensity
  82. ps = 1.0 + (preset.pause_scale - 1.0) * intensity
  83. lts = 1.0 + (preset.loud_tempo_scale - 1.0) * intensity
  84. # 1. Dynamik-Effekte
  85. if abs(lpb) > 0.05:
  86. audio = loud_pitch_boost(audio, sample_rate, lpb)
  87. if abs(ps - 1.0) > 0.005:
  88. audio = scale_pauses(audio, sample_rate, ps)
  89. if abs(lts - 1.0) > 0.005:
  90. audio = loud_tempo_scale(audio, sample_rate, lts)
  91. # 2. Speed
  92. if abs(spd - 1.0) > 0.005:
  93. audio = speed_change(audio, sample_rate, spd)
  94. # 3. Pitch-Shift
  95. if abs(semitones) > 0.1:
  96. audio = pitch_shift(audio, sample_rate, semitones)
  97. # 4. VTLP (Formant-Verschiebung)
  98. if abs(formant) > 0.01:
  99. alpha = 1.0 + formant
  100. audio = vtlp(audio, sample_rate, alpha)
  101. # 5. Breathiness
  102. if breath > 0.005:
  103. audio = add_breathiness(audio, sample_rate, breath)
  104. # 6. Shimmer
  105. if shim > 0.005:
  106. audio = add_shimmer(audio, sample_rate, shim)
  107. # 7. Filter (Lowpass / Highpass)
  108. if preset.lowpass_cutoff > 10.0:
  109. audio = apply_lowpass(audio, sample_rate, preset.lowpass_cutoff, preset.lowpass_order)
  110. if preset.highpass_cutoff > 10.0:
  111. audio = apply_highpass(audio, sample_rate, preset.highpass_cutoff, preset.highpass_order)
  112. # 8. Distortion
  113. if preset.distortion_gain > 1.001:
  114. audio = apply_distortion(audio, sample_rate, preset.distortion_gain, preset.distortion_threshold)
  115. # 9. Flanger
  116. if preset.flanger_wet > 0.005:
  117. audio = apply_flanger(audio, sample_rate, preset.flanger_rate, preset.flanger_depth, preset.flanger_wet)
  118. # 10. Reverb
  119. if preset.reverb_wet > 0.005:
  120. audio = apply_reverb(audio, sample_rate, preset.reverb_room_size, preset.reverb_damping, preset.reverb_wet)
  121. # 11. Trim — Anfang/Ende abschneiden (z.B. VTLP-Artefakte)
  122. ltrim_samples = int(sample_rate * preset.ltrim) if preset.ltrim > 0.001 else 0
  123. rtrim_samples = int(sample_rate * preset.rtrim) if preset.rtrim > 0.001 else 0
  124. if ltrim_samples + rtrim_samples > 0 and ltrim_samples + rtrim_samples < len(audio):
  125. end = len(audio) - rtrim_samples if rtrim_samples > 0 else len(audio)
  126. audio = audio[ltrim_samples:end]
  127. # 8. Fade-In/Out gegen Klick-Artefakte
  128. audio = apply_fade(audio, sample_rate)
  129. return audio
  130. def apply_voice_morph(
  131. audio: np.ndarray,
  132. sample_rate: int,
  133. preset: VoiceMorphPreset | str,
  134. intensity: float = 1.0,
  135. ) -> np.ndarray:
  136. """Convenience-Funktion — erstellt AudioMorpher und wendet Preset an."""
  137. morpher = AudioMorpher()
  138. return morpher.morph(audio, sample_rate, preset, intensity)