presets.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. # -*- coding: utf-8 -*-
  2. """
  3. VoiceMorphPreset — Dataclass fuer Stimmveraenderungs-Parameter.
  4. Laedt verschachtelte JSON-Presets und mappt sie auf flache Felder.
  5. """
  6. from __future__ import annotations
  7. import json
  8. from dataclasses import dataclass, field
  9. from pathlib import Path
  10. @dataclass
  11. class VoiceMorphPreset:
  12. """Parameter-Set fuer eine Stimmveraenderung."""
  13. preset_id: str
  14. label: str
  15. # Basis-Effekte
  16. pitch_semitones: float = 0.0 # Halbtoene: + = hoeher, - = tiefer
  17. speed_factor: float = 1.0 # >1 = schneller, <1 = langsamer
  18. formant_extra: float = 0.0 # VTLP: >0 = hoehere, <0 = tiefere Formanten
  19. breathiness: float = 0.0 # 0.0-0.3: Aspirationsrauschen
  20. shimmer: float = 0.0 # 0.0-0.05: Amplituden-Variation
  21. # Dynamik
  22. loud_pitch_boost: float = 0.0 # Halbtoene extra bei lauten Stellen
  23. pause_scale: float = 1.0 # >1 = laengere Pausen
  24. loud_tempo_scale: float = 1.0 # <1 = laute Stellen schneller
  25. # Reverb
  26. reverb_room_size: float = 0.0 # 0.0-1.0 (0 = deaktiviert)
  27. reverb_damping: float = 0.5 # 0.0-1.0
  28. reverb_wet: float = 0.0 # 0.0-1.0 (0 = deaktiviert)
  29. # Filter
  30. lowpass_cutoff: float = 0.0 # Hz (0 = deaktiviert)
  31. lowpass_order: int = 2 # Filterordnung 1-6
  32. highpass_cutoff: float = 0.0 # Hz (0 = deaktiviert)
  33. highpass_order: int = 2 # Filterordnung 1-6
  34. # Flanger
  35. flanger_rate: float = 0.0 # LFO Hz (0 = deaktiviert)
  36. flanger_depth: float = 0.002 # Max Delay in Sekunden
  37. flanger_wet: float = 0.0 # 0.0-1.0 (0 = deaktiviert)
  38. # Distortion
  39. distortion_gain: float = 1.0 # Verstaerkung (1.0 = kein Effekt)
  40. distortion_threshold: float = 0.8 # Soft-Clip Schwelle
  41. # Trimming
  42. ltrim: float = 0.0 # Sekunden am Anfang abschneiden
  43. rtrim: float = 0.0 # Sekunden am Ende abschneiden
  44. # Roh-Daten fuer spaetere Erweiterungen
  45. _extra: dict = field(default_factory=dict)
  46. @classmethod
  47. def from_dict(cls, data: dict) -> VoiceMorphPreset:
  48. """Parst verschachtelte JSON-Struktur in flache Dataclass."""
  49. preset_id = data["preset_id"]
  50. label = data["label"]
  51. # Bekannte verschachtelte Keys extrahieren
  52. pitch = data.get("pitch", {})
  53. speed = data.get("speed", {})
  54. formant = data.get("formant", {})
  55. breath = data.get("breathiness", {})
  56. shim = data.get("shimmer", {})
  57. dynamics = data.get("dynamics", {})
  58. reverb = data.get("reverb", {})
  59. lowpass = data.get("lowpass", {})
  60. highpass = data.get("highpass", {})
  61. flanger = data.get("flanger", {})
  62. distortion = data.get("distortion", {})
  63. trim = data.get("trim", {})
  64. # Unbekannte Keys sammeln
  65. known_keys = {"preset_id", "label", "pitch", "speed", "formant",
  66. "breathiness", "shimmer", "dynamics", "reverb",
  67. "lowpass", "highpass", "flanger", "distortion", "trim"}
  68. extra = {k: v for k, v in data.items() if k not in known_keys}
  69. return cls(
  70. preset_id=preset_id,
  71. label=label,
  72. pitch_semitones=pitch.get("semitones", 0.0),
  73. speed_factor=speed.get("factor", 1.0),
  74. formant_extra=formant.get("shift", 0.0),
  75. breathiness=breath.get("amount", 0.0),
  76. shimmer=shim.get("amount", 0.0),
  77. loud_pitch_boost=dynamics.get("loud_pitch_boost", 0.0),
  78. pause_scale=dynamics.get("pause_scale", 1.0),
  79. loud_tempo_scale=dynamics.get("loud_tempo_scale", 1.0),
  80. reverb_room_size=reverb.get("room_size", 0.0),
  81. reverb_damping=reverb.get("damping", 0.5),
  82. reverb_wet=reverb.get("wet", 0.0),
  83. lowpass_cutoff=lowpass.get("cutoff", 0.0),
  84. lowpass_order=lowpass.get("order", 2),
  85. highpass_cutoff=highpass.get("cutoff", 0.0),
  86. highpass_order=highpass.get("order", 2),
  87. flanger_rate=flanger.get("rate", 0.0),
  88. flanger_depth=flanger.get("depth", 0.002),
  89. flanger_wet=flanger.get("wet", 0.0),
  90. distortion_gain=distortion.get("gain", 1.0),
  91. distortion_threshold=distortion.get("threshold", 0.8),
  92. ltrim=trim.get("ltrim", 0.0),
  93. rtrim=trim.get("rtrim", 0.0),
  94. _extra=extra,
  95. )
  96. def to_dict(self) -> dict:
  97. """Serialisiert zurueck in verschachtelte JSON-Struktur."""
  98. result: dict = {
  99. "preset_id": self.preset_id,
  100. "label": self.label,
  101. }
  102. if abs(self.pitch_semitones) > 1e-6:
  103. result["pitch"] = {"semitones": self.pitch_semitones}
  104. if abs(self.speed_factor - 1.0) > 1e-6:
  105. result["speed"] = {"factor": self.speed_factor}
  106. if abs(self.formant_extra) > 1e-6:
  107. result["formant"] = {"shift": self.formant_extra}
  108. if self.breathiness > 1e-6:
  109. result["breathiness"] = {"amount": self.breathiness}
  110. if self.shimmer > 1e-6:
  111. result["shimmer"] = {"amount": self.shimmer}
  112. # Dynamik-Block nur wenn mindestens ein Wert gesetzt
  113. dyn: dict = {}
  114. if abs(self.loud_pitch_boost) > 1e-6:
  115. dyn["loud_pitch_boost"] = self.loud_pitch_boost
  116. if abs(self.pause_scale - 1.0) > 1e-6:
  117. dyn["pause_scale"] = self.pause_scale
  118. if abs(self.loud_tempo_scale - 1.0) > 1e-6:
  119. dyn["loud_tempo_scale"] = self.loud_tempo_scale
  120. if dyn:
  121. result["dynamics"] = dyn
  122. # Reverb-Block
  123. if self.reverb_wet > 1e-6:
  124. rev: dict = {"wet": self.reverb_wet}
  125. if abs(self.reverb_room_size) > 1e-6:
  126. rev["room_size"] = self.reverb_room_size
  127. if abs(self.reverb_damping - 0.5) > 1e-6:
  128. rev["damping"] = self.reverb_damping
  129. result["reverb"] = rev
  130. # Lowpass-Block
  131. if self.lowpass_cutoff > 1e-6:
  132. lp: dict = {"cutoff": self.lowpass_cutoff}
  133. if self.lowpass_order != 2:
  134. lp["order"] = self.lowpass_order
  135. result["lowpass"] = lp
  136. # Highpass-Block
  137. if self.highpass_cutoff > 1e-6:
  138. hp: dict = {"cutoff": self.highpass_cutoff}
  139. if self.highpass_order != 2:
  140. hp["order"] = self.highpass_order
  141. result["highpass"] = hp
  142. # Flanger-Block
  143. if self.flanger_wet > 1e-6:
  144. fl: dict = {"wet": self.flanger_wet}
  145. if abs(self.flanger_rate) > 1e-6:
  146. fl["rate"] = self.flanger_rate
  147. if abs(self.flanger_depth - 0.002) > 1e-6:
  148. fl["depth"] = self.flanger_depth
  149. result["flanger"] = fl
  150. # Distortion-Block
  151. if self.distortion_gain > 1.001:
  152. dist: dict = {"gain": self.distortion_gain}
  153. if abs(self.distortion_threshold - 0.8) > 1e-6:
  154. dist["threshold"] = self.distortion_threshold
  155. result["distortion"] = dist
  156. # Trim-Block
  157. trim_dict: dict = {}
  158. if self.ltrim > 1e-6:
  159. trim_dict["ltrim"] = self.ltrim
  160. if self.rtrim > 1e-6:
  161. trim_dict["rtrim"] = self.rtrim
  162. if trim_dict:
  163. result["trim"] = trim_dict
  164. # Extra-Daten anhaengen
  165. result.update(self._extra)
  166. return result
  167. def load_presets_from_directory(preset_dir: Path | str) -> list[VoiceMorphPreset]:
  168. """Laedt alle .json-Dateien im Verzeichnis und gibt Presets zurueck."""
  169. preset_dir = Path(preset_dir)
  170. presets: list[VoiceMorphPreset] = []
  171. if not preset_dir.is_dir():
  172. return presets
  173. for json_file in sorted(preset_dir.glob("*.json")):
  174. try:
  175. with open(json_file, "r", encoding="utf-8") as f:
  176. data = json.load(f)
  177. if isinstance(data, list):
  178. for entry in data:
  179. presets.append(VoiceMorphPreset.from_dict(entry))
  180. elif isinstance(data, dict):
  181. presets.append(VoiceMorphPreset.from_dict(data))
  182. except (json.JSONDecodeError, KeyError, TypeError):
  183. # Fehlerhafte Dateien ueberspringen
  184. continue
  185. return presets