Skip to content

Commit ac21794

Browse files
stream_wav.py: use i2s.shift() for volume scaling
Should be faster, and simpler. Audio while playing QuasiBird stutters, but that doesn't seem to be caused by this change here, because it also happens with the previous volume scaling + viper method...
1 parent 64f3fd7 commit ac21794

1 file changed

Lines changed: 38 additions & 140 deletions

File tree

internal_filesystem/lib/mpos/audio/stream_wav.py

Lines changed: 38 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,12 @@
11
# WAVStream - WAV File Playback Stream for AudioManager
2-
# Supports 8/16/24/32-bit PCM, mono+stereo, auto-upsampling, volume control
3-
# Uses synchronous playback in a separate thread for non-blocking operation
2+
# Supports 8/16/24/32-bit PCM, mono+stereo, auto-upsampling.
3+
# Uses synchronous playback in a separate thread for non-blocking operation.
44

55
import machine
6-
import micropython
76
import os
8-
import sys
97
import time
108

11-
# Toggle to enable I2S.shift-based volume scaling when available.
12-
# Set to False to use legacy software scaling only.
13-
USE_I2S_SHIFT_VOLUME = True
14-
15-
# Volume scaling function - Viper-optimized for ESP32 performance
16-
# NOTE: The line below is automatically commented out by build_mpos.sh during
17-
# Unix/macOS builds (cross-compiler doesn't support Viper), then uncommented after build.
18-
@micropython.viper
19-
def _scale_audio(buf: ptr8, num_bytes: int, scale_fixed: int):
20-
"""Fast volume scaling for 16-bit audio samples using Viper (ESP32 native code emitter)."""
21-
for i in range(0, num_bytes, 2):
22-
lo = int(buf[i])
23-
hi = int(buf[i + 1])
24-
sample = (hi << 8) | lo
25-
if hi & 128:
26-
sample -= 65536
27-
sample = (sample * scale_fixed) // 32768
28-
if sample > 32767:
29-
sample = 32767
30-
elif sample < -32768:
31-
sample = -32768
32-
buf[i] = sample & 255
33-
buf[i + 1] = (sample >> 8) & 255
34-
9+
'''
3510
@micropython.viper
3611
def _scale_audio_optimized(buf: ptr8, num_bytes: int, scale_fixed: int):
3712
if scale_fixed >= 32768:
@@ -71,94 +46,7 @@ def _scale_audio_optimized(buf: ptr8, num_bytes: int, scale_fixed: int):
7146
7247
buf[i] = r & 0xFF
7348
buf[i+1] = (r >> 8) & 0xFF
74-
75-
@micropython.viper
76-
def _scale_audio_rough(buf: ptr8, num_bytes: int, scale_fixed: int):
77-
"""Rough volume scaling for 16-bit audio samples using right shifts for performance."""
78-
if scale_fixed >= 32768:
79-
return
80-
81-
# Determine the shift amount
82-
shift: int = 0
83-
threshold: int = 32768
84-
while shift < 16 and scale_fixed < threshold:
85-
shift += 1
86-
threshold >>= 1
87-
88-
# If shift is 16 or more, set buffer to zero (volume too low)
89-
if shift >= 16:
90-
for i in range(num_bytes):
91-
buf[i] = 0
92-
return
93-
94-
# Apply right shift to each 16-bit sample
95-
for i in range(0, num_bytes, 2):
96-
lo: int = int(buf[i])
97-
hi: int = int(buf[i + 1])
98-
sample: int = (hi << 8) | lo
99-
if hi & 128:
100-
sample -= 65536
101-
sample >>= shift
102-
buf[i] = sample & 255
103-
buf[i + 1] = (sample >> 8) & 255
104-
105-
@micropython.viper
106-
def _scale_audio_shift(buf: ptr8, num_bytes: int, shift: int):
107-
"""Rough volume scaling for 16-bit audio samples using right shifts for performance."""
108-
if shift <= 0:
109-
return
110-
111-
# If shift is 16 or more, set buffer to zero (volume too low)
112-
if shift >= 16:
113-
for i in range(num_bytes):
114-
buf[i] = 0
115-
return
116-
117-
# Apply right shift to each 16-bit sample
118-
for i in range(0, num_bytes, 2):
119-
lo: int = int(buf[i])
120-
hi: int = int(buf[i + 1])
121-
sample: int = (hi << 8) | lo
122-
if hi & 128:
123-
sample -= 65536
124-
sample >>= shift
125-
buf[i] = sample & 255
126-
buf[i + 1] = (sample >> 8) & 255
127-
128-
@micropython.viper
129-
def _scale_audio_powers_of_2(buf: ptr8, num_bytes: int, shift: int):
130-
if shift <= 0:
131-
return
132-
if shift >= 16:
133-
for i in range(num_bytes):
134-
buf[i] = 0
135-
return
136-
137-
# Unroll the sign-extend + shift into one tight loop with no inner branch
138-
inv_shift: int = 16 - shift
139-
for i in range(0, num_bytes, 2):
140-
s: int = int(buf[i]) | (int(buf[i+1]) << 8)
141-
if s & 0x8000: # only one branch, highly predictable when shift fixed shift
142-
s |= -65536 # sign extend using OR (faster than subtract!)
143-
s <<= inv_shift # bring the bits we want into lower 16
144-
s >>= 16 # arithmetic shift right by 'shift' amount
145-
buf[i] = s & 0xFF
146-
buf[i+1] = (s >> 8) & 0xFF
147-
148-
149-
# Would be faster to use a lookup table here
150-
def _volume_to_shift(scale_fixed):
151-
"""Convert fixed-point volume (0..32768) to a right-shift amount (0..16)."""
152-
if scale_fixed >= 32768:
153-
return 0
154-
if scale_fixed <= 0:
155-
return 16
156-
shift = 0
157-
threshold = 32768
158-
while shift < 16 and scale_fixed < threshold:
159-
shift += 1
160-
threshold >>= 1
161-
return shift
49+
'''
16250

16351
class WAVStream:
16452
"""
@@ -168,6 +56,19 @@ class WAVStream:
16856

16957
WAVE_FORMAT_PCM = 0x1
17058
WAVE_FORMAT_EXTENSIBLE = 0xFFFE # often used for 24 and 32 bits per sample
59+
_VOLUME_TO_SHIFT = (
60+
16, 7, 6, 6, 5, 5, 5, 4, 4, 4,
61+
4, 4, 4, 3, 3, 3, 3, 3, 3, 3,
62+
3, 3, 3, 3, 3, 2, 2, 2, 2, 2,
63+
2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
64+
2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
65+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
66+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
67+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
68+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
69+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
70+
0,
71+
)
17172

17273
def __init__(
17374
self,
@@ -426,6 +327,15 @@ def _upsample_buffer(raw, factor):
426327
out_idx += 2
427328
return upsampled
428329

330+
@staticmethod
331+
def _volume_percent_to_shift(volume):
332+
"""Convert 0-100 volume percent to a 0-16 right-shift amount."""
333+
if volume <= 0:
334+
return 16
335+
if volume >= 100:
336+
return 0
337+
return WAVStream._VOLUME_TO_SHIFT[volume]
338+
429339
# ----------------------------------------------------------------------
430340
# Main playback routine
431341
# ----------------------------------------------------------------------
@@ -456,6 +366,7 @@ def play(self):
456366
self._playback_rate = playback_rate
457367
# ibuf = playback_rate # doesnt account for stereo vs mono...
458368
ibuf = 8192
369+
#ibuf = 32000 # old setting
459370

460371
print(f"WAVStream: {original_rate} Hz, {bits_per_sample}-bit, {channels}-ch")
461372
print(f"WAVStream: Playback at {playback_rate} Hz (factor {upsample_factor})")
@@ -487,10 +398,10 @@ def play(self):
487398

488399
# Configure MCLK pin if provided (must be done before I2S init)
489400
# On some MicroPython versions, machine.I2S() supports a mck argument
490-
# but not on ESP32S3 1.25.0 version, apparently.
401+
# but not on ESP32S3 1.27.0 version, apparently.
491402
if 'mck' in self.i2s_pins:
492403
mck_pin = machine.Pin(self.i2s_pins['mck'], machine.Pin.OUT)
493-
from machine import Pin, PWM
404+
from machine import PWM
494405
try:
495406
self._mck_pwm = PWM(mck_pin)
496407
freq, duty = WAVStream._get_freq_duty(playback_rate)
@@ -579,28 +490,15 @@ def play(self):
579490
if upsample_factor > 1:
580491
raw = self._upsample_buffer(raw, upsample_factor)
581492

582-
# 3. Volume scaling
583-
scale = self.volume / 100.0
584-
if scale < 1.0:
585-
scale_fixed = int(scale * 32768)
586-
if (
587-
USE_I2S_SHIFT_VOLUME
588-
and self._i2s
589-
and hasattr(self._i2s, "shift")
590-
):
591-
shift = _volume_to_shift(scale_fixed)
592-
if shift >= 16:
593-
for i in range(len(raw)):
594-
raw[i] = 0
595-
elif shift > 0:
596-
try:
597-
self._i2s.shift(buf=raw, bits=16, shift=-shift)
598-
except Exception as e:
599-
print(f"_i2s.shift got exception, falling back to software scaling: {e}")
600-
_scale_audio_optimized(raw, len(raw), scale_fixed)
601-
else:
602-
#print("_i2s has no shift attribute, falling back to software scaling")
603-
_scale_audio_optimized(raw, len(raw), scale_fixed)
493+
# 3. Volume scaling via I2S native right-shift.
494+
volume_shift = self._volume_percent_to_shift(self.volume)
495+
if self._i2s and volume_shift > 0:
496+
self._i2s.shift(buf=raw, bits=16, shift=-volume_shift)
497+
498+
# The old volume scaling method, left here for comparison purposes:
499+
#scale = self.volume / 100.0
500+
#scale_fixed = int(scale * 32768)
501+
#_scale_audio_optimized(raw, len(raw), scale_fixed)
604502

605503
# 4. Output to I2S (blocking write is OK - we're in a separate thread)
606504
if self._i2s:

0 commit comments

Comments
 (0)