# -*- coding: utf-8 -*- """ VoiceMorphPreset — Dataclass fuer Stimmveraenderungs-Parameter. Laedt verschachtelte JSON-Presets und mappt sie auf flache Felder. """ from __future__ import annotations import json from dataclasses import dataclass, field from pathlib import Path @dataclass class VoiceMorphPreset: """Parameter-Set fuer eine Stimmveraenderung.""" preset_id: str label: str # Basis-Effekte pitch_semitones: float = 0.0 # Halbtoene: + = hoeher, - = tiefer speed_factor: float = 1.0 # >1 = schneller, <1 = langsamer formant_extra: float = 0.0 # VTLP: >0 = hoehere, <0 = tiefere Formanten breathiness: float = 0.0 # 0.0-0.3: Aspirationsrauschen shimmer: float = 0.0 # 0.0-0.05: Amplituden-Variation # Dynamik loud_pitch_boost: float = 0.0 # Halbtoene extra bei lauten Stellen pause_scale: float = 1.0 # >1 = laengere Pausen loud_tempo_scale: float = 1.0 # <1 = laute Stellen schneller # Reverb reverb_room_size: float = 0.0 # 0.0-1.0 (0 = deaktiviert) reverb_damping: float = 0.5 # 0.0-1.0 reverb_wet: float = 0.0 # 0.0-1.0 (0 = deaktiviert) # Filter lowpass_cutoff: float = 0.0 # Hz (0 = deaktiviert) lowpass_order: int = 2 # Filterordnung 1-6 highpass_cutoff: float = 0.0 # Hz (0 = deaktiviert) highpass_order: int = 2 # Filterordnung 1-6 # Flanger flanger_rate: float = 0.0 # LFO Hz (0 = deaktiviert) flanger_depth: float = 0.002 # Max Delay in Sekunden flanger_wet: float = 0.0 # 0.0-1.0 (0 = deaktiviert) # Distortion distortion_gain: float = 1.0 # Verstaerkung (1.0 = kein Effekt) distortion_threshold: float = 0.8 # Soft-Clip Schwelle # Trimming ltrim: float = 0.0 # Sekunden am Anfang abschneiden rtrim: float = 0.0 # Sekunden am Ende abschneiden # Roh-Daten fuer spaetere Erweiterungen _extra: dict = field(default_factory=dict) @classmethod def from_dict(cls, data: dict) -> VoiceMorphPreset: """Parst verschachtelte JSON-Struktur in flache Dataclass.""" preset_id = data["preset_id"] label = data["label"] # Bekannte verschachtelte Keys extrahieren pitch = data.get("pitch", {}) speed = data.get("speed", {}) formant = data.get("formant", {}) breath = data.get("breathiness", {}) shim = data.get("shimmer", {}) dynamics = data.get("dynamics", {}) reverb = data.get("reverb", {}) lowpass = data.get("lowpass", {}) highpass = data.get("highpass", {}) flanger = data.get("flanger", {}) distortion = data.get("distortion", {}) trim = data.get("trim", {}) # Unbekannte Keys sammeln known_keys = {"preset_id", "label", "pitch", "speed", "formant", "breathiness", "shimmer", "dynamics", "reverb", "lowpass", "highpass", "flanger", "distortion", "trim"} extra = {k: v for k, v in data.items() if k not in known_keys} return cls( preset_id=preset_id, label=label, pitch_semitones=pitch.get("semitones", 0.0), speed_factor=speed.get("factor", 1.0), formant_extra=formant.get("shift", 0.0), breathiness=breath.get("amount", 0.0), shimmer=shim.get("amount", 0.0), loud_pitch_boost=dynamics.get("loud_pitch_boost", 0.0), pause_scale=dynamics.get("pause_scale", 1.0), loud_tempo_scale=dynamics.get("loud_tempo_scale", 1.0), reverb_room_size=reverb.get("room_size", 0.0), reverb_damping=reverb.get("damping", 0.5), reverb_wet=reverb.get("wet", 0.0), lowpass_cutoff=lowpass.get("cutoff", 0.0), lowpass_order=lowpass.get("order", 2), highpass_cutoff=highpass.get("cutoff", 0.0), highpass_order=highpass.get("order", 2), flanger_rate=flanger.get("rate", 0.0), flanger_depth=flanger.get("depth", 0.002), flanger_wet=flanger.get("wet", 0.0), distortion_gain=distortion.get("gain", 1.0), distortion_threshold=distortion.get("threshold", 0.8), ltrim=trim.get("ltrim", 0.0), rtrim=trim.get("rtrim", 0.0), _extra=extra, ) def to_dict(self) -> dict: """Serialisiert zurueck in verschachtelte JSON-Struktur.""" result: dict = { "preset_id": self.preset_id, "label": self.label, } if abs(self.pitch_semitones) > 1e-6: result["pitch"] = {"semitones": self.pitch_semitones} if abs(self.speed_factor - 1.0) > 1e-6: result["speed"] = {"factor": self.speed_factor} if abs(self.formant_extra) > 1e-6: result["formant"] = {"shift": self.formant_extra} if self.breathiness > 1e-6: result["breathiness"] = {"amount": self.breathiness} if self.shimmer > 1e-6: result["shimmer"] = {"amount": self.shimmer} # Dynamik-Block nur wenn mindestens ein Wert gesetzt dyn: dict = {} if abs(self.loud_pitch_boost) > 1e-6: dyn["loud_pitch_boost"] = self.loud_pitch_boost if abs(self.pause_scale - 1.0) > 1e-6: dyn["pause_scale"] = self.pause_scale if abs(self.loud_tempo_scale - 1.0) > 1e-6: dyn["loud_tempo_scale"] = self.loud_tempo_scale if dyn: result["dynamics"] = dyn # Reverb-Block if self.reverb_wet > 1e-6: rev: dict = {"wet": self.reverb_wet} if abs(self.reverb_room_size) > 1e-6: rev["room_size"] = self.reverb_room_size if abs(self.reverb_damping - 0.5) > 1e-6: rev["damping"] = self.reverb_damping result["reverb"] = rev # Lowpass-Block if self.lowpass_cutoff > 1e-6: lp: dict = {"cutoff": self.lowpass_cutoff} if self.lowpass_order != 2: lp["order"] = self.lowpass_order result["lowpass"] = lp # Highpass-Block if self.highpass_cutoff > 1e-6: hp: dict = {"cutoff": self.highpass_cutoff} if self.highpass_order != 2: hp["order"] = self.highpass_order result["highpass"] = hp # Flanger-Block if self.flanger_wet > 1e-6: fl: dict = {"wet": self.flanger_wet} if abs(self.flanger_rate) > 1e-6: fl["rate"] = self.flanger_rate if abs(self.flanger_depth - 0.002) > 1e-6: fl["depth"] = self.flanger_depth result["flanger"] = fl # Distortion-Block if self.distortion_gain > 1.001: dist: dict = {"gain": self.distortion_gain} if abs(self.distortion_threshold - 0.8) > 1e-6: dist["threshold"] = self.distortion_threshold result["distortion"] = dist # Trim-Block trim_dict: dict = {} if self.ltrim > 1e-6: trim_dict["ltrim"] = self.ltrim if self.rtrim > 1e-6: trim_dict["rtrim"] = self.rtrim if trim_dict: result["trim"] = trim_dict # Extra-Daten anhaengen result.update(self._extra) return result def load_presets_from_directory(preset_dir: Path | str) -> list[VoiceMorphPreset]: """Laedt alle .json-Dateien im Verzeichnis und gibt Presets zurueck.""" preset_dir = Path(preset_dir) presets: list[VoiceMorphPreset] = [] if not preset_dir.is_dir(): return presets for json_file in sorted(preset_dir.glob("*.json")): try: with open(json_file, "r", encoding="utf-8") as f: data = json.load(f) if isinstance(data, list): for entry in data: presets.append(VoiceMorphPreset.from_dict(entry)) elif isinstance(data, dict): presets.append(VoiceMorphPreset.from_dict(data)) except (json.JSONDecodeError, KeyError, TypeError): # Fehlerhafte Dateien ueberspringen continue return presets