""" Tape Processor — Simulates tape saturation and applies safety filters. """ import numpy as np from .eq_engine import EQBand, EQEngine from .calibrator import TAPE_PROFILES class TapeProcessor: def __init__(self, sample_rate: int = 44100): self.sample_rate = sample_rate self.tape_type = "Type I (Ferric)" self.enable_highpass = True self.enable_lowpass = True self.enable_saturation = True self.saturation_threshold = 0.95 # Internal filter engine for safety filters self._filter_engine = EQEngine(sample_rate) self._update_filters() def _update_filters(self): profile = TAPE_PROFILES.get(self.tape_type, TAPE_PROFILES["Type I (Ferric)"]) bands = [] if self.enable_highpass: bands.append(EQBand(freq=profile["low_cut"], gain=0.0, q=0.707, filter_type='highpass')) if self.enable_lowpass: bands.append(EQBand(freq=profile["high_cut"], gain=0.0, q=0.707, filter_type='lowpass')) self._filter_engine.bands = bands self._filter_engine.update_filters() def set_tape_type(self, tape_type: str): if tape_type != self.tape_type: self.tape_type = tape_type self._update_filters() def set_sample_rate(self, sr: int): self.sample_rate = sr self._filter_engine.set_sample_rate(sr) def process_pre_eq(self, chunk: np.ndarray) -> np.ndarray: if chunk.size == 0: return chunk # 1. Apply safety HP/LP filters if self.enable_highpass or self.enable_lowpass: chunk = self._filter_engine.process(chunk) return chunk def process_post_eq(self, chunk: np.ndarray) -> np.ndarray: if chunk.size == 0: return chunk # 2. Soft Saturation if self.enable_saturation: # Scale so threshold maps to where tanh starts rounding off scale = 1.0 / max(0.1, self.saturation_threshold) chunk = np.tanh(chunk * scale) / scale return chunk def reset_state(self): self._filter_engine.reset_state()