| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- # -*- 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
|