@@ -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
32154class 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