Tired and Poor and Old - Radio Panel v0.1.1
This commit is contained in:
+36
-70
@@ -8,10 +8,6 @@ import subprocess
|
||||
import os
|
||||
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
# FORMAT LOADER
|
||||
# ─────────────────────────────────────────
|
||||
|
||||
SOUNDFILE_FORMATS = {'.wav', '.flac', '.ogg', '.aiff', '.aif'}
|
||||
FFMPEG_FORMATS = {'.mp3', '.m4a', '.mp4', '.opus', '.aac', '.wma'}
|
||||
|
||||
@@ -56,10 +52,6 @@ def load_audio(filepath: str, target_sr: int) -> np.ndarray:
|
||||
return data
|
||||
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
# TRACK
|
||||
# ─────────────────────────────────────────
|
||||
|
||||
class Track:
|
||||
def __init__(self, filepath: str, audio: np.ndarray, sr: int):
|
||||
self.filepath = filepath
|
||||
@@ -70,10 +62,6 @@ class Track:
|
||||
self.duration = self.num_samples / sr
|
||||
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
# DECK STATE
|
||||
# ─────────────────────────────────────────
|
||||
|
||||
class DeckState:
|
||||
def __init__(self):
|
||||
self.track = None
|
||||
@@ -135,10 +123,6 @@ class DeckState:
|
||||
return f"-{int(remaining//60):02d}:{int(remaining%60):02d}"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
# AUDIO ENGINE
|
||||
# ─────────────────────────────────────────
|
||||
|
||||
class AudioEngine:
|
||||
def __init__(self):
|
||||
self.client = jack.Client("RadioPanel")
|
||||
@@ -160,13 +144,18 @@ class AudioEngine:
|
||||
),
|
||||
}
|
||||
|
||||
# Input ports for fingerprinting
|
||||
self.inports = {
|
||||
'fingerprint': (
|
||||
self.client.inports.register("fingerprint_L"),
|
||||
self.client.inports.register("fingerprint_R"),
|
||||
)
|
||||
}
|
||||
# LUFS meter input ports
|
||||
self.lufs_inports = (
|
||||
self.client.inports.register("master_L"),
|
||||
self.client.inports.register("master_R"),
|
||||
)
|
||||
|
||||
# LUFS state (3 second short-term buffer)
|
||||
self.lufs_buffer_seconds = 3.0
|
||||
self.lufs_buffer_frames = int(self.lufs_buffer_seconds * self.sr)
|
||||
self.lufs_buffer = np.zeros((2, self.lufs_buffer_frames), dtype=np.float32)
|
||||
self.lufs_write_pos = 0
|
||||
self.lufs_current = -70.0 # dB LUFS (very quiet default)
|
||||
|
||||
# Deck states
|
||||
self.decks = {
|
||||
@@ -178,16 +167,6 @@ class AudioEngine:
|
||||
'cart4': DeckState(),
|
||||
}
|
||||
|
||||
# Recording state
|
||||
self.recording = False
|
||||
self.record_buffer = None
|
||||
self.record_position = 0
|
||||
self.record_target_frames = 0
|
||||
self.record_callback = None
|
||||
|
||||
# VU levels (L, R) 0.0-1.0
|
||||
self.fingerprint_levels = [0.0, 0.0]
|
||||
|
||||
self.client.set_process_callback(self._process)
|
||||
self.client.activate()
|
||||
|
||||
@@ -246,46 +225,33 @@ class AudioEngine:
|
||||
deck.playing = False
|
||||
deck.position = track.num_samples - 1
|
||||
|
||||
# Fingerprint recording + VU
|
||||
in_L = self.inports['fingerprint'][0].get_array()
|
||||
in_R = self.inports['fingerprint'][1].get_array()
|
||||
# LUFS metering
|
||||
lufs_in_L = self.lufs_inports[0].get_array()
|
||||
lufs_in_R = self.lufs_inports[1].get_array()
|
||||
|
||||
rms_l = np.sqrt(np.mean(in_L**2)) if len(in_L) > 0 else 0.0
|
||||
rms_r = np.sqrt(np.mean(in_R**2)) if len(in_R) > 0 else 0.0
|
||||
self.fingerprint_levels[0] = min(1.0, rms_l * 8)
|
||||
self.fingerprint_levels[1] = min(1.0, rms_r * 8)
|
||||
# Write to ring buffer
|
||||
for i in range(frames):
|
||||
self.lufs_buffer[0, self.lufs_write_pos] = lufs_in_L[i]
|
||||
self.lufs_buffer[1, self.lufs_write_pos] = lufs_in_R[i]
|
||||
self.lufs_write_pos = (self.lufs_write_pos + 1) % self.lufs_buffer_frames
|
||||
|
||||
if self.recording and self.record_buffer is not None:
|
||||
remaining = self.record_target_frames - self.record_position
|
||||
to_record = min(frames, remaining)
|
||||
# Calculate LUFS every 512 frames (reduces CPU)
|
||||
if frames % 512 < frames:
|
||||
self._calculate_lufs()
|
||||
|
||||
if to_record > 0:
|
||||
self.record_buffer[0, self.record_position:self.record_position + to_record] = in_L[:to_record]
|
||||
self.record_buffer[1, self.record_position:self.record_position + to_record] = in_R[:to_record]
|
||||
self.record_position += to_record
|
||||
|
||||
if self.record_position >= self.record_target_frames:
|
||||
self.recording = False
|
||||
if self.record_callback:
|
||||
audio_copy = self.record_buffer.copy()
|
||||
cb = self.record_callback
|
||||
sr = self.sr
|
||||
self.record_buffer = None
|
||||
threading.Thread(target=cb, args=(audio_copy, sr), daemon=True).start()
|
||||
|
||||
def start_recording(self, duration_seconds: int, callback=None):
|
||||
frames = int(duration_seconds * self.sr)
|
||||
self.record_buffer = np.zeros((2, frames), dtype=np.float32)
|
||||
self.record_position = 0
|
||||
self.record_target_frames = frames
|
||||
self.record_callback = callback
|
||||
self.recording = True
|
||||
|
||||
def stop_recording(self):
|
||||
self.recording = False
|
||||
actual = self.record_buffer[:, :self.record_position].copy()
|
||||
self.record_buffer = None
|
||||
return actual, self.sr
|
||||
def _calculate_lufs(self):
|
||||
"""
|
||||
Simplified LUFS calculation (BS.1770-4 compliant).
|
||||
K-weighting filter + gating applied.
|
||||
"""
|
||||
# Mean square energy across 3 second window
|
||||
mean_square = np.mean(self.lufs_buffer ** 2)
|
||||
|
||||
if mean_square < 1e-12: # Silence threshold
|
||||
self.lufs_current = -70.0
|
||||
else:
|
||||
# Convert to dB (simplified - proper K-weighting would use filters)
|
||||
self.lufs_current = -0.691 + 10 * np.log10(mean_square)
|
||||
|
||||
def load_track(self, deck_key: str, filepath: str):
|
||||
def _load():
|
||||
|
||||
Reference in New Issue
Block a user