bridge.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import os
  2. import sys
  3. import time
  4. import socket
  5. import struct
  6. import numpy as np
  7. import fcntl
  8. # Lobotomize the Linux kernel pipe so it can't hoard future audio
  9. try:
  10. F_SETPIPE_SZ = 1031
  11. fcntl.fcntl(sys.stdin.fileno(), F_SETPIPE_SZ, 4096)
  12. except Exception:
  13. pass
  14. # Network settings
  15. UDP_IP = os.environ.get("UDP_IP", "239.0.0.1")
  16. UDP_PORT = int(os.environ.get("UDP_PORT", "11988"))
  17. # Audio stream constants
  18. SAMPLE_RATE = 44100
  19. CHUNK_SIZE = 1024
  20. CHUNK_DURATION = CHUNK_SIZE / SAMPLE_RATE
  21. FRAME_BYTES = CHUNK_SIZE * 4
  22. MAX_SILENT_FRAMES = 43 # ~1 second of silence at 43.06 FPS
  23. # Hardcoded DSP tuning
  24. FREQ_MIN = 44.0 # Aligned with physical 43.06Hz FFT bin resolution
  25. FREQ_MAX = 12000.0 # Capture crisp cymbals and high transients
  26. SILENCE_THRESHOLD = 0.5
  27. GAIN_MIN = 800.0 # Floor: prevents squashing heavily mastered EDM
  28. GAIN_MAX = 8500.0 # Ceiling: prevents boosting background tape hiss
  29. TILT_EXPONENT = 0.42 # Treble tilt compensation
  30. DECAY_RATE = 0.95 # Faster recovery (~1s) for actual snap and bounce
  31. CONTRAST_EXPONENT = 1.2 # >1.0 crushes noise floor & exaggerates beats
  32. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
  33. sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
  34. # Pre-compute Hanning window
  35. HANNING_WINDOW = np.hanning(CHUNK_SIZE).astype(np.float32)
  36. # Compute 16 logarithmic frequency bands
  37. FREQ_EDGES = np.logspace(np.log10(FREQ_MIN), np.log10(FREQ_MAX), 17)
  38. fft_freqs = np.fft.rfftfreq(CHUNK_SIZE, 1.0 / SAMPLE_RATE)
  39. # Pre-compute logarithmic treble tilt weights
  40. band_centers = np.sqrt(FREQ_EDGES[:-1] * FREQ_EDGES[1:])
  41. TILT_WEIGHTS = ((band_centers / FREQ_MIN) ** TILT_EXPONENT).astype(np.float32)
  42. # Build a C-optimized Matrix for dot-product binning
  43. # Shape: (16 bands, 513 FFT bins).
  44. BIN_MATRIX = np.zeros((16, len(fft_freqs)), dtype=np.float32)
  45. for i in range(16):
  46. low = FREQ_EDGES[i]
  47. high = FREQ_EDGES[i + 1]
  48. idx = np.where((fft_freqs >= low) & (fft_freqs < high) & (fft_freqs > 0))[0]
  49. if len(idx) == 0:
  50. non_zero_bins = np.where(fft_freqs > 0)[0]
  51. closest = non_zero_bins[np.argmin(np.abs(fft_freqs[non_zero_bins] - low))]
  52. idx = [closest]
  53. # The mean is just 1.0 / count. Multiply by the tilt weight immediately.
  54. BIN_MATRIX[i, idx] = (1.0 / len(idx)) * TILT_WEIGHTS[i]
  55. # State variables
  56. sample_smth = 0.0
  57. silence_frames = 0
  58. running_peak = 0.05
  59. STRUCT_FMT_V2 = "<6s2xffB3x16sd"
  60. start_time = None
  61. frames_processed = 0
  62. while True:
  63. t0 = time.perf_counter()
  64. raw_data = sys.stdin.buffer.read(FRAME_BYTES)
  65. read_duration = time.perf_counter() - t0
  66. # Short-read survival patch
  67. if not raw_data:
  68. break
  69. if len(raw_data) < FRAME_BYTES:
  70. continue
  71. # Micro-reset baseline to permanently kill accumulated audio pipeline drift
  72. if read_duration > 0.015:
  73. start_time = time.perf_counter()
  74. frames_processed = 0
  75. running_peak = 0.05
  76. if start_time is None:
  77. start_time = time.perf_counter()
  78. # Audio ingestion and magnitude extraction
  79. audio = np.frombuffer(raw_data, dtype=np.int16).astype(np.float32)
  80. left = audio[0::2]
  81. right = audio[1::2]
  82. mono = (left + right) * (1.0 / 65536.0)
  83. raw_mag = float(np.max(np.abs(mono)) * 255.0)
  84. sample_smth = 0.7 * sample_smth + 0.3 * raw_mag
  85. sample_peak = 1 if raw_mag > (sample_smth * 1.5 + 20) else 0
  86. if raw_mag < SILENCE_THRESHOLD:
  87. silence_frames += 1
  88. else:
  89. silence_frames = 0
  90. if silence_frames <= MAX_SILENT_FRAMES:
  91. windowed = mono * HANNING_WINDOW
  92. fft_vals = np.abs(np.fft.rfft(windowed)) * (2.0 / CHUNK_SIZE)
  93. # C-optimized single matrix multiplication replaces the 16-step for loop and tilt math
  94. tilted_energies = BIN_MATRIX @ fft_vals
  95. # Track rolling peak
  96. current_max = float(np.max(tilted_energies))
  97. if current_max > running_peak:
  98. running_peak = current_max
  99. else:
  100. running_peak = max(0.005, running_peak * DECAY_RATE)
  101. # Dynamic gain bounded by floor and ceiling
  102. dynamic_gain = np.clip(1.0 / running_peak, GAIN_MIN / 255.0, GAIN_MAX / 255.0)
  103. # Normalize 0.0 to 1.0, apply contrast exponent, scale to 0-255
  104. normalized = np.clip(tilted_energies * dynamic_gain, 0.0, 1.0)
  105. contrasted = (normalized ** CONTRAST_EXPONENT) * 255.0
  106. fft_result = bytes(np.clip(contrasted, 0, 255).astype(np.uint8))
  107. payload = struct.pack(
  108. STRUCT_FMT_V2,
  109. b"00002\x00",
  110. float(raw_mag),
  111. float(sample_smth),
  112. sample_peak,
  113. fft_result,
  114. float(raw_mag)
  115. )
  116. try:
  117. sock.sendto(payload, (UDP_IP, UDP_PORT))
  118. except Exception:
  119. pass
  120. # Metronome pacing
  121. frames_processed += 1
  122. target_time = start_time + (frames_processed * CHUNK_DURATION)
  123. sleep_time = target_time - time.perf_counter()
  124. if sleep_time > 0:
  125. time.sleep(sleep_time)
  126. elif sleep_time < -1.0:
  127. start_time = time.perf_counter()
  128. frames_processed = 0