Browse Source

Added contrast exponent, expaned readme

Nicole Portas 2 weeks ago
parent
commit
901a71d654
2 changed files with 25 additions and 13 deletions
  1. 10 0
      README.md
  2. 15 13
      bridge.py

+ 10 - 0
README.md

@@ -51,6 +51,7 @@ Instead of relying on analog or I2S microphones wired to microcontrollers which
 | `GAIN_MIN` | `800.0` | Lower dynamic gain floor (prevents squashing compressed EDM/metal). |
 | `GAIN_MAX` | `8500.0` | Upper dynamic gain ceiling (prevents amplifying background noise). |
 | `TILT_EXPONENT`| `0.42` | Treble tilt compensation exponent (1/f pink noise curve). |
+| `CONTRAST_EXPONENT` | `2.0` | Non-linear power curve exponent. Crushes mid-level musical clutter to exaggerate transient punches and beats. |
 | `DECAY_RATE` | `0.998` | Per-frame decay multiplier for the AGC ceiling (~10s slow decay). |
 | `SILENCE_THRESHOLD` | `0.5` | Peak threshold (0–255 scale) below which the stream is gated. |
 | `UDP_IP` | `239.0.0.1` | Multicast group IP for WLED AudioReactive. |
@@ -186,6 +187,15 @@ Final_Bands = clamp(Tilted_Energies * Dynamic_Gain, 0, 255)
 
 ```
 
+### Dynamic Contrast Power Curve
+
+To prevent sustained instruments and background harmonics from keeping the visualizer bars hovering statically mid-scale, an exponential power curve is applied to normalized bands:
+
+Contrasted_Energy = (Normalized_Energy ^ CONTRAST_EXPONENT) * 255.0
+
+Setting `CONTRAST_EXPONENT` between `1.8` and `2.2` aggressively pulls low-to-mid level audio down toward zero while preserving full ceiling hits on transient peaks. This gives the LED matrix a snappy, rhythmic bounce with distinct drops between beats instead of a flat, muddy visual floor.
+
+
 ## Technical Specifications
 
 -   **Packet Format:** 44-byte WLED AudioReactive V2 struct layout (`<6s2xffB3x16sd`).

+ 15 - 13
bridge.py

@@ -24,13 +24,14 @@ SILENCE_THRESHOLD = float(os.environ.get("SILENCE_THRESHOLD", "0.5"))
 # Dynamic AGC and Frequency Tilt overrides
 GAIN_MIN = float(os.environ.get("GAIN_MIN", "800.0"))       # Floor: prevents squashing heavily mastered EDM
 GAIN_MAX = float(os.environ.get("GAIN_MAX", "8500.0"))      # Ceiling: prevents boosting background tape hiss
-TILT_EXPONENT = float(os.environ.get("TILT_EXPONENT", "0.42")) # Logarithmic treble compensation curve
-DECAY_RATE = float(os.environ.get("DECAY_RATE", "0.998"))   # ~10-second slow recovery decay per frame
+TILT_EXPONENT = float(os.environ.get("TILT_EXPONENT", "0.42")) # Treble tilt compensation
+DECAY_RATE = float(os.environ.get("DECAY_RATE", "0.995"))   # Slightly faster recovery (~5s) for better bounce
+CONTRAST_EXPONENT = float(os.environ.get("CONTRAST_EXPONENT", "2.0")) # >1.0 crushes noise floor & exaggerates beats
 
 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
 sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
 
-# Pre-compute Hanning window to save CPU cycles inside the loop
+# Pre-compute Hanning window
 HANNING_WINDOW = np.hanning(CHUNK_SIZE).astype(np.float32)
 
 # Compute 16 logarithmic frequency bands
@@ -48,14 +49,14 @@ for i in range(16):
         idx = [closest]
     bins_idx.append(idx)
 
-# Pre-compute logarithmic treble tilt weights (1/f pink noise balance)
+# Pre-compute logarithmic treble tilt weights
 band_centers = np.sqrt(FREQ_EDGES[:-1] * FREQ_EDGES[1:])
 TILT_WEIGHTS = ((band_centers / FREQ_MIN) ** TILT_EXPONENT).astype(np.float32)
 
 # State variables
 sample_smth = 0.0
 silence_frames = 0
-running_peak = 0.05  # Initial baseline ceiling
+running_peak = 0.05
 STRUCT_FMT_V2 = "<6s2xffB3x16sd"
 
 start_time = None
@@ -69,7 +70,7 @@ while True:
     if not raw_data or len(raw_data) < FRAME_BYTES:
         break
 
-    # Gap Detector: If the pipe sat empty for >200ms, reset the clock & AGC baseline
+    # Reset metronome and baseline on gap/pause
     if read_duration > 0.2:
         start_time = time.perf_counter()
         frames_processed = 0
@@ -102,22 +103,23 @@ while True:
         for i in range(16):
             raw_energies[i] = np.mean(fft_vals[bins_idx[i]])
 
-        # Apply logarithmic pink noise compensation
+        # Apply logarithmic treble tilt
         tilted_energies = raw_energies * TILT_WEIGHTS
 
-        # Asymmetric AGC: Instant attack, slow crawl decay
+        # Track rolling peak
         current_max = float(np.max(tilted_energies))
         if current_max > running_peak:
             running_peak = current_max
         else:
             running_peak = max(0.005, running_peak * DECAY_RATE)
 
-        # Dynamic gain bounded within strict sanity limits
-        dynamic_gain = np.clip(255.0 / running_peak, GAIN_MIN, GAIN_MAX)
+        # Dynamic gain bounded by floor and ceiling
+        dynamic_gain = np.clip(1.0 / running_peak, GAIN_MIN / 255.0, GAIN_MAX / 255.0)
 
-        # Scale into 8-bit unsigned integer array
-        scaled = np.clip(tilted_energies * dynamic_gain, 0, 255).astype(np.uint8)
-        fft_result = bytes(scaled)
+        # Normalize 0.0 to 1.0, apply contrast exponent, scale to 0-255
+        normalized = np.clip(tilted_energies * dynamic_gain, 0.0, 1.0)
+        contrasted = (normalized ** CONTRAST_EXPONENT) * 255.0
+        fft_result = bytes(np.clip(contrasted, 0, 255).astype(np.uint8))
 
         payload = struct.pack(
             STRUCT_FMT_V2,