Compare commits

..

2 Commits

Author SHA1 Message Date
Tanner 4b0774b3fa feat: add volume normalization using FFmpeg loudnorm filter
Co-authored-by: aider (gemini/gemini-3.1-pro-preview) <aider@aider.chat>
2026-07-28 17:08:17 -06:00
Tanner 43c59d2c28 chore: set default volume to 50 2026-07-28 17:08:16 -06:00
+30 -10
View File
@@ -11,7 +11,7 @@ import av
class PlayerState: class PlayerState:
def __init__(self): def __init__(self):
self.paused = True self.paused = True
self.volume = 100.0 self.volume = 50.0
self.time_pos = 0.0 self.time_pos = 0.0
self.seek_request = None self.seek_request = None
self.running = True self.running = True
@@ -26,6 +26,18 @@ def playback_thread(file_path, state):
stream = container.streams.audio[0] stream = container.streams.audio[0]
resampler = av.AudioResampler(format='s16', layout='stereo', rate=48000) resampler = av.AudioResampler(format='s16', layout='stereo', rate=48000)
def build_graph():
graph = av.filter.Graph()
src = graph.add_abuffer(template=stream)
loudnorm = graph.add("loudnorm", "I=-16:TP=-1.5:LRA=11")
sink = graph.add("abuffersink")
src.link_to(loudnorm)
loudnorm.link_to(sink)
graph.configure()
return graph
graph = build_graph()
with open(fifo_path, 'wb') as fifo: with open(fifo_path, 'wb') as fifo:
iterator = container.decode(stream) iterator = container.decode(stream)
while state.running: while state.running:
@@ -41,6 +53,7 @@ def playback_thread(file_path, state):
with state.lock: with state.lock:
state.seek_request = None state.seek_request = None
iterator = container.decode(stream) iterator = container.decode(stream)
graph = build_graph() # Reset filter graph on seek
continue continue
if paused: if paused:
@@ -57,17 +70,24 @@ def playback_thread(file_path, state):
with state.lock: with state.lock:
state.time_pos = float(frame.pts * stream.time_base) state.time_pos = float(frame.pts * stream.time_base)
resampled_frames = resampler.resample(frame) graph.push(frame)
for r_frame in resampled_frames: while True:
arr = r_frame.to_ndarray() try:
filtered_frame = graph.pull()
except (av.AVError, BlockingIOError):
break
# Apply volume resampled_frames = resampler.resample(filtered_frame)
if vol != 100.0: for r_frame in resampled_frames:
multiplier = max(0.0, vol / 100.0) arr = r_frame.to_ndarray()
arr = arr.astype(np.float32) * multiplier
arr = np.clip(arr, -32768, 32767).astype(np.int16)
fifo.write(arr.tobytes()) # Apply volume
if vol != 100.0:
multiplier = max(0.0, vol / 100.0)
arr = arr.astype(np.float32) * multiplier
arr = np.clip(arr, -32768, 32767).astype(np.int16)
fifo.write(arr.tobytes())
def handle_ipc_client(conn, state): def handle_ipc_client(conn, state):
buffer = "" buffer = ""