Skip to content

Commit 91127df

Browse files
AudioFlinger: optimize WAV volume scaling
1 parent 756136f commit 91127df

File tree

3 files changed

+138
-3
lines changed

3 files changed

+138
-3
lines changed

internal_filesystem/lib/mpos/audio/audioflinger.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
_i2s_pins = None # I2S pin configuration dict (created per-stream)
1919
_buzzer_instance = None # PWM buzzer instance
2020
_current_stream = None # Currently playing stream
21-
_volume = 70 # System volume (0-100)
21+
_volume = 25 # System volume (0-100)
2222
_stream_lock = None # Thread lock for stream management
2323

2424

@@ -290,6 +290,8 @@ def set_volume(volume):
290290
"""
291291
global _volume
292292
_volume = max(0, min(100, volume))
293+
if _current_stream:
294+
_current_stream.set_volume(_volume)
293295

294296

295297
def get_volume():

internal_filesystem/lib/mpos/audio/stream_rtttl.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,3 +229,6 @@ def play(self):
229229
# Ensure buzzer is off
230230
self.buzzer.duty_u16(0)
231231
self._is_playing = False
232+
233+
def set_volume(self, vol):
234+
self.volume = vol

internal_filesystem/lib/mpos/audio/stream_wav.py

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,128 @@ def _scale_audio(buf: ptr8, num_bytes: int, scale_fixed: int):
2828
buf[i] = sample & 255
2929
buf[i + 1] = (sample >> 8) & 255
3030

31+
import micropython
32+
@micropython.viper
33+
def _scale_audio_optimized(buf: ptr8, num_bytes: int, scale_fixed: int):
34+
"""
35+
Very fast 16-bit volume scaling using only shifts + adds.
36+
- 100 % and above → no change
37+
- < ~12.5 % → pure right-shift (fastest)
38+
- otherwise → high-quality shift/add approximation
39+
"""
40+
if scale_fixed >= 32768: # 100 % or more
41+
return
42+
if scale_fixed <= 0: # muted
43+
for i in range(num_bytes):
44+
buf[i] = 0
45+
return
46+
47+
# --------------------------------------------------------------
48+
# Very low volumes → simple right-shift (super cheap)
49+
# --------------------------------------------------------------
50+
if scale_fixed < 4096: # < ~12.5 %
51+
shift: int = 0
52+
tmp: int = 32768
53+
while tmp > scale_fixed:
54+
shift += 1
55+
tmp >>= 1
56+
for i in range(0, num_bytes, 2):
57+
lo: int = int(buf[i])
58+
hi: int = int(buf[i + 1])
59+
s: int = (hi << 8) | lo
60+
if hi & 128: # sign extend
61+
s -= 65536
62+
s >>= shift
63+
buf[i] = s & 255
64+
buf[i + 1] = (s >> 8) & 255
65+
return
66+
67+
# --------------------------------------------------------------
68+
# Medium → high volumes: sample * scale_fixed // 32768
69+
# approximated with shifts + adds only
70+
# --------------------------------------------------------------
71+
# Build a 16-bit mask:
72+
# bit 0 → add (s >> 15)
73+
# bit 1 → add (s >> 14)
74+
# ...
75+
# bit 15 → add s (>> 0)
76+
mask: int = 0
77+
bit_value: int = 16384 # starts at 2^-1
78+
remaining: int = scale_fixed
79+
80+
shift_idx: int = 1 # corresponds to >>1
81+
while bit_value > 0:
82+
if remaining >= bit_value:
83+
mask |= (1 << (16 - shift_idx)) # correct bit position
84+
remaining -= bit_value
85+
bit_value >>= 1
86+
shift_idx += 1
87+
88+
# Apply the mask
89+
for i in range(0, num_bytes, 2):
90+
lo: int = int(buf[i])
91+
hi: int = int(buf[i + 1])
92+
s: int = (hi << 8) | lo
93+
if hi & 128:
94+
s -= 65536
95+
96+
result: int = 0
97+
if mask & 0x8000: result += s # >>0
98+
if mask & 0x4000: result += (s >> 1)
99+
if mask & 0x2000: result += (s >> 2)
100+
if mask & 0x1000: result += (s >> 3)
101+
if mask & 0x0800: result += (s >> 4)
102+
if mask & 0x0400: result += (s >> 5)
103+
if mask & 0x0200: result += (s >> 6)
104+
if mask & 0x0100: result += (s >> 7)
105+
if mask & 0x0080: result += (s >> 8)
106+
if mask & 0x0040: result += (s >> 9)
107+
if mask & 0x0020: result += (s >>10)
108+
if mask & 0x0010: result += (s >>11)
109+
if mask & 0x0008: result += (s >>12)
110+
if mask & 0x0004: result += (s >>13)
111+
if mask & 0x0002: result += (s >>14)
112+
if mask & 0x0001: result += (s >>15)
113+
114+
# Clamp to 16-bit signed range
115+
if result > 32767:
116+
result = 32767
117+
elif result < -32768:
118+
result = -32768
119+
120+
buf[i] = result & 255
121+
buf[i + 1] = (result >> 8) & 255
122+
123+
import micropython
124+
@micropython.viper
125+
def _scale_audio_rough(buf: ptr8, num_bytes: int, scale_fixed: int):
126+
"""Rough volume scaling for 16-bit audio samples using right shifts for performance."""
127+
if scale_fixed >= 32768:
128+
return
129+
130+
# Determine the shift amount
131+
shift: int = 0
132+
threshold: int = 32768
133+
while shift < 16 and scale_fixed < threshold:
134+
shift += 1
135+
threshold >>= 1
136+
137+
# If shift is 16 or more, set buffer to zero (volume too low)
138+
if shift >= 16:
139+
for i in range(num_bytes):
140+
buf[i] = 0
141+
return
142+
143+
# Apply right shift to each 16-bit sample
144+
for i in range(0, num_bytes, 2):
145+
lo: int = int(buf[i])
146+
hi: int = int(buf[i + 1])
147+
sample: int = (hi << 8) | lo
148+
if hi & 128:
149+
sample -= 65536
150+
sample >>= shift
151+
buf[i] = sample & 255
152+
buf[i + 1] = (sample >> 8) & 255
31153

32154
class WAVStream:
33155
"""
@@ -251,7 +373,12 @@ def play(self):
251373
print(f"WAVStream: Playing {data_size} bytes (volume {self.volume}%)")
252374
f.seek(data_start)
253375

254-
chunk_size = 4096
376+
# smaller chunk size means less jerks but buffer can run empty
377+
# at 22050 Hz, 16-bit, 2-ch, 4096/4 = 1024 samples / 22050 = 46ms
378+
# 4096 => audio stutters during quasibird
379+
# 8192 => no audio stutters and quasibird runs at ~16 fps => good compromise!
380+
# 16384 => no audio stutters during quasibird but low framerate (~8fps)
381+
chunk_size = 4096*2
255382
bytes_per_original_sample = (bits_per_sample // 8) * channels
256383
total_original = 0
257384

@@ -287,7 +414,7 @@ def play(self):
287414
scale = self.volume / 100.0
288415
if scale < 1.0:
289416
scale_fixed = int(scale * 32768)
290-
_scale_audio(raw, len(raw), scale_fixed)
417+
_scale_audio_optimized(raw, len(raw), scale_fixed)
291418

292419
# 4. Output to I2S
293420
if self._i2s:
@@ -313,3 +440,6 @@ def play(self):
313440
if self._i2s:
314441
self._i2s.deinit()
315442
self._i2s = None
443+
444+
def set_volume(self, vol):
445+
self.volume = vol

0 commit comments

Comments
 (0)