processor.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. # -*- coding: utf-8 -*-
  2. """
  3. VoiceMorphProcessor — Pipeline-Bridge fuer Audio-Processing.
  4. Nur laengen-neutrale Effekte: VTLP, Breathiness, Shimmer, loud_pitch_boost.
  5. Kein pitch_shift, speed, pause_scale, loud_tempo_scale (aendern Chunk-Laenge).
  6. """
  7. from __future__ import annotations
  8. import struct
  9. from typing import TYPE_CHECKING
  10. import numpy as np
  11. from trixy_core.audio.processing.processor import AudioProcessor, ProcessorPriority
  12. from trixy_core.audio_morpher.dsp import vtlp, add_breathiness, add_shimmer
  13. from trixy_core.audio_morpher.dynamics import loud_pitch_boost
  14. from trixy_core.audio_morpher.morpher import AudioMorpher
  15. from trixy_core.audio_morpher.presets import VoiceMorphPreset
  16. if TYPE_CHECKING:
  17. from trixy_core.audio.processing.context import AudioProcessingContext
  18. class VoiceMorphProcessor(AudioProcessor):
  19. """
  20. Audio-Pipeline-Prozessor fuer Voice-Morphing.
  21. Wendet nur laengen-neutrale Effekte an (Chunk-Laenge bleibt gleich):
  22. - VTLP (Formant-Verschiebung)
  23. - Breathiness
  24. - Shimmer
  25. - loud_pitch_boost
  26. """
  27. def __init__(
  28. self,
  29. preset: VoiceMorphPreset | str = "",
  30. intensity: float = 1.0,
  31. morpher: AudioMorpher | None = None,
  32. ) -> None:
  33. super().__init__(
  34. processor_id="voice_morph",
  35. name="Voice Morph",
  36. priority=ProcessorPriority.PRE_EFFECTS,
  37. )
  38. self._intensity = intensity
  39. self._morpher = morpher or AudioMorpher()
  40. # Preset aufloesen
  41. if isinstance(preset, str) and preset:
  42. self._preset = self._morpher.get_preset(preset)
  43. elif isinstance(preset, VoiceMorphPreset):
  44. self._preset = preset
  45. else:
  46. self._preset = None
  47. @property
  48. def preset(self) -> VoiceMorphPreset | None:
  49. return self._preset
  50. @preset.setter
  51. def preset(self, value: VoiceMorphPreset | str | None) -> None:
  52. if isinstance(value, str):
  53. self._preset = self._morpher.get_preset(value)
  54. else:
  55. self._preset = value
  56. @property
  57. def intensity(self) -> float:
  58. return self._intensity
  59. @intensity.setter
  60. def intensity(self, value: float) -> None:
  61. self._intensity = max(0.0, min(1.0, value))
  62. def process(
  63. self,
  64. chunk: bytes,
  65. context: "AudioProcessingContext",
  66. ) -> bytes:
  67. """Wendet laengen-neutrale Voice-Morph-Effekte auf PCM-Chunk an."""
  68. if self._preset is None or self._intensity < 0.01:
  69. return chunk
  70. # PCM 16-bit → float64
  71. num_samples = len(chunk) // 2
  72. if num_samples < 4:
  73. return chunk
  74. samples = struct.unpack(f"<{num_samples}h", chunk)
  75. audio = np.array(samples, dtype=np.float64) / 32768.0
  76. # Sample-Rate aus Kontext (Standard: 16000 fuer Pipeline)
  77. sample_rate = getattr(context, "sample_rate", 16000)
  78. # Nur laengen-neutrale Effekte anwenden
  79. formant = self._preset.formant_extra * self._intensity
  80. breath = self._preset.breathiness * self._intensity
  81. shim = self._preset.shimmer * self._intensity
  82. lpb = self._preset.loud_pitch_boost * self._intensity
  83. if abs(lpb) > 0.05:
  84. audio = loud_pitch_boost(audio, sample_rate, lpb)
  85. if abs(formant) > 0.01:
  86. alpha = 1.0 + formant
  87. audio = vtlp(audio, sample_rate, alpha)
  88. if breath > 0.005:
  89. audio = add_breathiness(audio, sample_rate, breath)
  90. if shim > 0.005:
  91. audio = add_shimmer(audio, sample_rate, shim)
  92. # float64 → PCM 16-bit
  93. audio_int = np.clip(audio * 32768.0, -32768, 32767).astype(np.int16)
  94. return struct.pack(f"<{num_samples}h", *audio_int)
  95. def get_config(self) -> dict:
  96. config = super().get_config()
  97. config["preset"] = self._preset.preset_id if self._preset else None
  98. config["intensity"] = self._intensity
  99. return config