tape_processor.py 2.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. """
  2. Tape Processor — Simulates tape saturation and applies safety filters.
  3. """
  4. import numpy as np
  5. from .eq_engine import EQBand, EQEngine
  6. from .calibrator import TAPE_PROFILES
  7. class TapeProcessor:
  8. def __init__(self, sample_rate: int = 44100):
  9. self.sample_rate = sample_rate
  10. self.tape_type = "Type I (Ferric)"
  11. self.enable_highpass = True
  12. self.enable_lowpass = True
  13. self.enable_saturation = True
  14. self.saturation_threshold = 0.95
  15. # Internal filter engine for safety filters
  16. self._filter_engine = EQEngine(sample_rate)
  17. self._update_filters()
  18. def _update_filters(self):
  19. profile = TAPE_PROFILES.get(self.tape_type, TAPE_PROFILES["Type I (Ferric)"])
  20. bands = []
  21. if self.enable_highpass:
  22. bands.append(EQBand(freq=profile["low_cut"], gain=0.0, q=0.707, filter_type='highpass'))
  23. if self.enable_lowpass:
  24. bands.append(EQBand(freq=profile["high_cut"], gain=0.0, q=0.707, filter_type='lowpass'))
  25. self._filter_engine.bands = bands
  26. self._filter_engine.update_filters()
  27. def set_tape_type(self, tape_type: str):
  28. if tape_type != self.tape_type:
  29. self.tape_type = tape_type
  30. self._update_filters()
  31. def set_sample_rate(self, sr: int):
  32. self.sample_rate = sr
  33. self._filter_engine.set_sample_rate(sr)
  34. def process_pre_eq(self, chunk: np.ndarray) -> np.ndarray:
  35. if chunk.size == 0:
  36. return chunk
  37. # 1. Apply safety HP/LP filters
  38. if self.enable_highpass or self.enable_lowpass:
  39. chunk = self._filter_engine.process(chunk)
  40. return chunk
  41. def process_post_eq(self, chunk: np.ndarray) -> np.ndarray:
  42. if chunk.size == 0:
  43. return chunk
  44. # 2. Soft Saturation
  45. if self.enable_saturation:
  46. # Scale so threshold maps to where tanh starts rounding off
  47. scale = 1.0 / max(0.1, self.saturation_threshold)
  48. chunk = np.tanh(chunk * scale) / scale
  49. return chunk
  50. def reset_state(self):
  51. self._filter_engine.reset_state()