audio: replace struct pack/unpack with array module in _apply_gain

Eliminates format string construction and intermediate tuple/list
allocations. Also drains stale frames from queues on stop().
This commit is contained in:
Username
2026-02-24 16:25:32 +01:00
parent 88e8d4d923
commit 7a2c8e3a5d

View File

@@ -7,9 +7,9 @@ Playback path: pymumble (decodes) -> raw PCM -> queue -> speakers.
from __future__ import annotations
import array
import logging
import queue
import struct
log = logging.getLogger(__name__)
@@ -21,13 +21,13 @@ DTYPE = "int16"
def _apply_gain(pcm: bytes, gain: float) -> bytes:
"""Scale int16 PCM samples by gain factor with clipping."""
n = len(pcm) // 2
if n == 0:
if len(pcm) < 2:
return pcm
fmt = f"<{n}h"
samples = struct.unpack(fmt, pcm[: n * 2])
scaled = [max(-32768, min(32767, int(s * gain))) for s in samples]
return struct.pack(fmt, *scaled)
samples = array.array("h")
samples.frombytes(pcm[: len(pcm) & ~1])
for i in range(len(samples)):
samples[i] = max(-32768, min(32767, int(samples[i] * gain)))
return samples.tobytes()
class AudioPipeline:
@@ -82,13 +82,19 @@ class AudioPipeline:
log.info("audio pipeline started (rate=%d)", self._sample_rate)
def stop(self):
"""Close audio streams."""
"""Close audio streams and drain stale frames."""
for stream in (self._input_stream, self._output_stream):
if stream is not None:
stream.stop()
stream.close()
self._input_stream = None
self._output_stream = None
for q in (self._capture_queue, self._playback_queue):
while not q.empty():
try:
q.get_nowait()
except queue.Empty:
break
log.info("audio pipeline stopped")
@property
@@ -154,9 +160,15 @@ class AudioPipeline:
except queue.Empty:
outdata[:] = b"\x00" * len(outdata)
def get_capture_frame(self) -> bytes | None:
"""Retrieve next captured PCM frame for transmission."""
def get_capture_frame(self, timeout: float = 0.0) -> bytes | None:
"""Retrieve next captured PCM frame for transmission.
Args:
timeout: Seconds to wait for a frame. 0 returns immediately.
"""
try:
if timeout > 0:
return self._capture_queue.get(timeout=timeout)
return self._capture_queue.get_nowait()
except queue.Empty:
return None