Sfoglia il codice sorgente

Add Dynamic Range Compressor for Movie Mode

Your Name 1 mese fa
parent
commit
422acd5cfc
3 ha cambiato i file con 70 aggiunte e 1 eliminazioni
  1. 5 0
      core/audio_player.py
  2. 50 0
      core/compressor.py
  3. 15 1
      ui/player_panel.py

+ 5 - 0
core/audio_player.py

@@ -10,6 +10,7 @@ import threading
 from .file_loader import load_audio, AudioData, normalize_audio
 from .eq_engine import EQEngine
 from .tape_processor import TapeProcessor
+from .compressor import Compressor
 from .audio_device import DeviceManager
 
 class AudioPlayer:
@@ -19,6 +20,7 @@ class AudioPlayer:
     def __init__(self, eq_engine: EQEngine, tape_processor: TapeProcessor, device_manager: DeviceManager):
         self.eq_engine = eq_engine
         self.tape_processor = tape_processor
+        self.compressor = Compressor()
         self.device_manager = device_manager
         
         self.audio_data: Optional[AudioData] = None
@@ -171,6 +173,9 @@ class AudioPlayer:
         chunk = self.eq_engine.process(chunk)
         chunk = self.tape_processor.process_post_eq(chunk)
         
+        # Dynamic Range Compression
+        chunk = self.compressor.process(chunk)
+        
         # Apply output volume
         if self.output_volume != 1.0:
             chunk = chunk * self.output_volume

+ 50 - 0
core/compressor.py

@@ -0,0 +1,50 @@
+import numpy as np
+
+class Compressor:
+    """
+    A fast, block-based dynamic range compressor designed to run in real-time.
+    Perfect for movie soundtracks to reduce dynamic range (makes quiet dialog louder, clamps loud explosions).
+    """
+    def __init__(self):
+        self.enabled = False
+        self.threshold_db = -20.0
+        self.ratio = 4.0
+        self.makeup_gain_db = 6.0
+        
+        # Smoothing factors for chunk-based envelope (0.0 to 1.0)
+        self.attack_factor = 0.8   # Fast attack to catch peaks
+        self.release_factor = 0.05 # Slower release for smooth pumping
+        
+        self._current_gain_db = 0.0
+        
+    def process(self, chunk: np.ndarray) -> np.ndarray:
+        if not self.enabled or chunk.size == 0:
+            return chunk
+            
+        # Calculate RMS of the entire chunk
+        # Using peak detection or RMS. RMS is better for overall loudness, peak is better for hard limiting.
+        # Movie dialogue is better managed with RMS.
+        rms = np.sqrt(np.mean(chunk**2))
+        rms_db = 20 * np.log10(max(rms, 1e-6))
+        
+        # Calculate target gain reduction
+        if rms_db > self.threshold_db:
+            overshoot = rms_db - self.threshold_db
+            target_gain_db = -overshoot * (1.0 - 1.0 / self.ratio)
+        else:
+            target_gain_db = 0.0
+            
+        # Smooth the gain reduction to prevent clicks
+        if target_gain_db < self._current_gain_db:
+            # Gain is going down (compressing harder) -> Attack
+            self._current_gain_db += (target_gain_db - self._current_gain_db) * self.attack_factor
+        else:
+            # Gain is going up (recovering) -> Release
+            self._current_gain_db += (target_gain_db - self._current_gain_db) * self.release_factor
+            
+        # Combine with makeup gain and convert to linear scale
+        total_gain_db = self._current_gain_db + self.makeup_gain_db
+        linear_gain = 10 ** (total_gain_db / 20.0)
+        
+        # Multiply chunk (numpy vectorized)
+        return chunk * linear_gain

+ 15 - 1
ui/player_panel.py

@@ -43,8 +43,9 @@ class PlayerPanel(QWidget):
         self.settings = QSettings("TapeRecorder", "Player")
         self._init_ui()
         
-        # Init volume
+        # Init volume and compressor
         self.player.output_volume = self.settings.value("volume", 100, type=int) / 100.0
+        self.player.compressor.enabled = self.settings.value("compressor", False, type=bool)
         
         # Connect player callbacks
         # Note: These are called from the audio thread. 
@@ -123,6 +124,12 @@ class PlayerPanel(QWidget):
         btn_layout.addWidget(self.play_btn)
         btn_layout.addWidget(self.stop_btn)
         btn_layout.addWidget(self.norm_cb)
+        
+        self.comp_cb = QCheckBox("Movie Mode (Comp)")
+        self.comp_cb.setChecked(self.settings.value("compressor", False, type=bool))
+        self.comp_cb.clicked.connect(self._on_comp_changed)
+        btn_layout.addWidget(self.comp_cb)
+        
         btn_layout.addWidget(QLabel(" | "))
         
         self.vol_slider = QSlider(Qt.Orientation.Horizontal)
@@ -285,6 +292,13 @@ class PlayerPanel(QWidget):
 
     def _on_norm_changed(self):
         self.settings.setValue("normalize_volume", self.norm_cb.isChecked())
+        # Reload track if playing to apply normalization immediately?
+        # Actually normalize is done at load time, so we just inform the user.
+        
+    def _on_comp_changed(self):
+        enabled = self.comp_cb.isChecked()
+        self.settings.setValue("compressor", enabled)
+        self.player.compressor.enabled = enabled
 
     def _load_track(self, file_path, auto_play=False):
         try: