55
66
77# ----------------------------------------------------------------------
8- # AudioPlayer – robust, volume-controllable WAV player (MONO + STEREO)
9- # Auto-up-samples any rate < 22050 Hz to 22050 Hz for MAX98357
8+ # AudioPlayer – robust, volume-controllable WAV player
9+ # Supports 8 / 16 / 24 / 32-bit PCM, mono + stereo
10+ # Auto-up-samples any rate < 22050 Hz to >=22050 Hz
1011# ----------------------------------------------------------------------
1112class AudioPlayer :
1213 _i2s = None
1314 _volume = 50 # 0-100
1415 _keep_running = True
1516
1617 # ------------------------------------------------------------------
17- # WAV header parser
18+ # WAV header parser – returns bit-depth
1819 # ------------------------------------------------------------------
1920 @staticmethod
2021 def find_data_chunk (f ):
21- """Return (data_start, data_size, sample_rate, channels)"""
22+ """Return (data_start, data_size, sample_rate, channels, bits_per_sample )"""
2223 f .seek (0 )
2324 if f .read (4 ) != b'RIFF' :
2425 raise ValueError ("Not a RIFF file" )
@@ -29,6 +30,7 @@ def find_data_chunk(f):
2930 pos = 12
3031 sample_rate = None
3132 channels = None
33+ bits_per_sample = None
3234 while pos < file_size :
3335 f .seek (pos )
3436 chunk_id = f .read (4 )
@@ -45,10 +47,11 @@ def find_data_chunk(f):
4547 if channels not in (1 , 2 ):
4648 raise ValueError ("Only mono or stereo supported" )
4749 sample_rate = int .from_bytes (fmt [4 :8 ], 'little' )
48- if int .from_bytes (fmt [14 :16 ], 'little' ) != 16 :
49- raise ValueError ("Only 16-bit supported" )
50+ bits_per_sample = int .from_bytes (fmt [14 :16 ], 'little' )
51+ if bits_per_sample not in (8 , 16 , 24 , 32 ):
52+ raise ValueError ("Only 8/16/24/32-bit PCM supported" )
5053 elif chunk_id == b'data' :
51- return f .tell (), chunk_size , sample_rate , channels
54+ return f .tell (), chunk_size , sample_rate , channels , bits_per_sample
5255 pos += 8 + chunk_size
5356 if chunk_size % 2 :
5457 pos += 1
@@ -72,21 +75,14 @@ def stop_playing(cls):
7275 cls ._keep_running = False
7376
7477 # ------------------------------------------------------------------
75- # Helper: up -sample a raw PCM buffer (zero-order-hold)
78+ # 1. Up -sample 16-bit buffer (zero-order-hold)
7679 # ------------------------------------------------------------------
7780 @staticmethod
7881 def _upsample_buffer (raw : bytearray , factor : int ) -> bytearray :
79- """
80- Duplicate each 16-bit sample `factor` times.
81- Input: interleaved L,R,L,R... (or mono)
82- Output: same layout, each sample repeated `factor` times.
83- """
8482 if factor == 1 :
8583 return raw
86-
8784 upsampled = bytearray (len (raw ) * factor )
8885 out_idx = 0
89- # each sample = 2 bytes
9086 for i in range (0 , len (raw ), 2 ):
9187 lo = raw [i ]
9288 hi = raw [i + 1 ]
@@ -96,6 +92,64 @@ def _upsample_buffer(raw: bytearray, factor: int) -> bytearray:
9692 out_idx += 2
9793 return upsampled
9894
95+ # ------------------------------------------------------------------
96+ # 2. Convert 8-bit to 16-bit (non-viper, Viper-safe)
97+ # ------------------------------------------------------------------
98+ @staticmethod
99+ def _convert_8_to_16 (buf : bytearray ) -> bytearray :
100+ out = bytearray (len (buf ) * 2 )
101+ j = 0
102+ for i in range (len (buf )):
103+ u8 = buf [i ]
104+ s16 = (u8 - 128 ) << 8
105+ out [j ] = s16 & 0xFF
106+ out [j + 1 ] = (s16 >> 8 ) & 0xFF
107+ j += 2
108+ return out
109+
110+ # ------------------------------------------------------------------
111+ # 3. Convert 24-bit to 16-bit (non-viper)
112+ # ------------------------------------------------------------------
113+ @staticmethod
114+ def _convert_24_to_16 (buf : bytearray ) -> bytearray :
115+ samples = len (buf ) // 3
116+ out = bytearray (samples * 2 )
117+ j = 0
118+ for i in range (samples ):
119+ b0 = buf [j ]
120+ b1 = buf [j + 1 ]
121+ b2 = buf [j + 2 ]
122+ s24 = (b2 << 16 ) | (b1 << 8 ) | b0
123+ if b2 & 0x80 :
124+ s24 -= 0x1000000
125+ s16 = s24 >> 8
126+ out [i * 2 ] = s16 & 0xFF
127+ out [i * 2 + 1 ] = (s16 >> 8 ) & 0xFF
128+ j += 3
129+ return out
130+
131+ # ------------------------------------------------------------------
132+ # 4. Convert 32-bit to 16-bit (non-viper)
133+ # ------------------------------------------------------------------
134+ @staticmethod
135+ def _convert_32_to_16 (buf : bytearray ) -> bytearray :
136+ samples = len (buf ) // 4
137+ out = bytearray (samples * 2 )
138+ j = 0
139+ for i in range (samples ):
140+ b0 = buf [j ]
141+ b1 = buf [j + 1 ]
142+ b2 = buf [j + 2 ]
143+ b3 = buf [j + 3 ]
144+ s32 = (b3 << 24 ) | (b2 << 16 ) | (b1 << 8 ) | b0
145+ if b3 & 0x80 :
146+ s32 -= 0x100000000
147+ s16 = s32 >> 16
148+ out [i * 2 ] = s16 & 0xFF
149+ out [i * 2 + 1 ] = (s16 >> 8 ) & 0xFF
150+ j += 4
151+ return out
152+
99153 # ------------------------------------------------------------------
100154 # Main playback routine
101155 # ------------------------------------------------------------------
@@ -109,25 +163,25 @@ def play_wav(cls, filename):
109163 print (f"File size: { file_size } bytes" )
110164
111165 # ----- parse header ------------------------------------------------
112- data_start , data_size , original_rate , channels = cls .find_data_chunk (f )
166+ data_start , data_size , original_rate , channels , bits_per_sample = \
167+ cls .find_data_chunk (f )
113168
114- # ----- decide playback rate (force >= 22050 Hz) --------------------
169+ # ----- decide playback rate (force >=22050 Hz) --------------------
115170 target_rate = 22050
116171 if original_rate >= target_rate :
117172 playback_rate = original_rate
118173 upsample_factor = 1
119174 else :
120- # find smallest integer factor so original * factor >= target
121175 upsample_factor = (target_rate + original_rate - 1 ) // original_rate
122176 playback_rate = original_rate * upsample_factor
123177
124- print (f"Original: { original_rate } Hz → Playback: { playback_rate } Hz "
125- f"(factor { upsample_factor } ), { channels } -ch " )
178+ print (f"Original: { original_rate } Hz, { bits_per_sample } -bit, { channels } -ch "
179+ f"to Playback: { playback_rate } Hz (factor { upsample_factor } )" )
126180
127181 if data_size > file_size - data_start :
128182 data_size = file_size - data_start
129183
130- # ----- I2S init ------------------ ----------------------------------
184+ # ----- I2S init (always 16-bit) ----------------------------------
131185 try :
132186 i2s_format = machine .I2S .MONO if channels == 1 else machine .I2S .STEREO
133187 cls ._i2s = machine .I2S (
@@ -144,10 +198,10 @@ def play_wav(cls, filename):
144198 except Exception as e :
145199 print (f"Warning: simulating playback (I2S init failed): { e } " )
146200
147- print (f"Playing { data_size } original bytes (vol { cls ._volume } %) … " )
201+ print (f"Playing { data_size } original bytes (vol { cls ._volume } %) ... " )
148202 f .seek (data_start )
149203
150- # ----- Viper volume scaler (works on any buffer) -------------------
204+ # ----- Viper volume scaler (16-bit only) ------ -------------------
151205 @micropython .viper
152206 def scale_audio (buf : ptr8 , num_bytes : int , scale_fixed : int ):
153207 for i in range (0 , num_bytes , 2 ):
@@ -165,15 +219,15 @@ def scale_audio(buf: ptr8, num_bytes: int, scale_fixed: int):
165219 buf [i + 1 ] = (sample >> 8 ) & 255
166220
167221 chunk_size = 4096
168- bytes_per_original_sample = 2 * channels # 2 bytes per channel
222+ bytes_per_original_sample = ( bits_per_sample // 8 ) * channels
169223 total_original = 0
170224
171225 while total_original < data_size :
172226 if not cls ._keep_running :
173227 print ("Playback stopped by user." )
174228 break
175229
176- # read a chunk of * original* data
230+ # ---- read a whole-sample chunk of original data -------------
177231 to_read = min (chunk_size , data_size - total_original )
178232 to_read -= (to_read % bytes_per_original_sample )
179233 if to_read <= 0 :
@@ -183,25 +237,33 @@ def scale_audio(buf: ptr8, num_bytes: int, scale_fixed: int):
183237 if not raw :
184238 break
185239
186- # ----- up-sample if needed ---------------------------------
240+ # ---- 1. Convert bit-depth to 16-bit (non-viper) -------------
241+ if bits_per_sample == 8 :
242+ raw = cls ._convert_8_to_16 (raw )
243+ elif bits_per_sample == 24 :
244+ raw = cls ._convert_24_to_16 (raw )
245+ elif bits_per_sample == 32 :
246+ raw = cls ._convert_32_to_16 (raw )
247+ # 16-bit to unchanged
248+
249+ # ---- 2. Up-sample if needed ---------------------------------
187250 if upsample_factor > 1 :
188251 raw = cls ._upsample_buffer (raw , upsample_factor )
189252
190- # ----- volume scaling - --------------------------------------
253+ # ---- 3. Volume scaling --------------------------------------
191254 scale = cls ._volume / 100.0
192255 if scale < 1.0 :
193256 scale_fixed = int (scale * 32768 )
194257 scale_audio (raw , len (raw ), scale_fixed )
195258
196- # ----- output --- ---------------------------------------------
259+ # ---- 4. Output ---------------------------------------------
197260 if cls ._i2s :
198261 cls ._i2s .write (raw )
199262 else :
200- # simulate timing with the *playback* rate
201263 num_samples = len (raw ) // (2 * channels )
202264 time .sleep (num_samples / playback_rate )
203265
204- total_original += to_read # count original bytes only
266+ total_original += to_read
205267
206268 print ("Playback finished." )
207269 except Exception as e :
0 commit comments