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
55import machine
6- import micropython
76import os
8- import sys
97import 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
3611def _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
16351class 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