66
77# ----------------------------------------------------------------------
88# AudioPlayer – robust, volume-controllable WAV player (MONO + STEREO)
9+ # Auto-up-samples any rate < 22050 Hz to 22050 Hz for MAX98357
910# ----------------------------------------------------------------------
1011class AudioPlayer :
11- # class-level defaults
1212_i2s = None
13- _volume = 50 # 0-100
13+ _volume = 50 # 0-100
1414_keep_running = True
1515
16+ # ------------------------------------------------------------------
17+ # WAV header parser
18+ # ------------------------------------------------------------------
1619@staticmethod
1720def find_data_chunk (f ):
18- """Skip chunks until 'data' is found → (data_start, data_size, sample_rate, channels)"""
21+ """Return (data_start, data_size, sample_rate, channels)"""
1922f .seek (0 )
2023if f .read (4 )!= b'RIFF' :
2124raise ValueError ("Not a RIFF file" )
@@ -68,6 +71,34 @@ def stop_playing(cls):
6871print ("stop_playing()" )
6972cls ._keep_running = False
7073
74+ # ------------------------------------------------------------------
75+ # Helper: up-sample a raw PCM buffer (zero-order-hold)
76+ # ------------------------------------------------------------------
77+ @staticmethod
78+ 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+ """
84+ if factor == 1 :
85+ return raw
86+
87+ upsampled = bytearray (len (raw )* factor )
88+ out_idx = 0
89+ # each sample = 2 bytes
90+ for i in range (0 ,len (raw ),2 ):
91+ lo = raw [i ]
92+ hi = raw [i + 1 ]
93+ for _ in range (factor ):
94+ upsampled [out_idx ]= lo
95+ upsampled [out_idx + 1 ]= hi
96+ out_idx += 2
97+ return upsampled
98+
99+ # ------------------------------------------------------------------
100+ # Main playback routine
101+ # ------------------------------------------------------------------
71102@classmethod
72103def play_wav (cls ,filename ):
73104cls ._keep_running = True
@@ -77,15 +108,28 @@ def play_wav(cls, filename):
77108file_size = st [6 ]
78109print (f"File size:{ file_size } bytes" )
79110
80- data_start ,data_size ,sample_rate ,channels = cls .find_data_chunk (f )
81- print (f"data chunk:{ data_size } bytes @{ sample_rate } Hz,{ channels } -channel" )
111+ # ----- parse header ------------------------------------------------
112+ data_start ,data_size ,original_rate ,channels = cls .find_data_chunk (f )
113+
114+ # ----- decide playback rate (force >= 22050 Hz) --------------------
115+ target_rate = 22050
116+ if original_rate >= target_rate :
117+ playback_rate = original_rate
118+ upsample_factor = 1
119+ else :
120+ # find smallest integer factor so original * factor >= target
121+ upsample_factor = (target_rate + original_rate - 1 )// original_rate
122+ playback_rate = original_rate * upsample_factor
123+
124+ print (f"Original:{ original_rate } Hz → Playback:{ playback_rate } Hz "
125+ f"(factor{ upsample_factor } ),{ channels } -ch" )
82126
83127if data_size > file_size - data_start :
84128data_size = file_size - data_start
85129
86- # ---- I2S init ------------------------------------------------
87- i2s_format = machine .I2S .MONO if channels == 1 else machine .I2S .STEREO
130+ # ----- I2S init ----------------------------------------------------
88131try :
132+ i2s_format = machine .I2S .MONO if channels == 1 else machine .I2S .STEREO
89133cls ._i2s = machine .I2S (
901340 ,
91135sck = machine .Pin (2 ,machine .Pin .OUT ),
@@ -94,18 +138,18 @@ def play_wav(cls, filename):
94138mode = machine .I2S .TX ,
95139bits = 16 ,
96140format = i2s_format ,
97- rate = sample_rate ,
141+ rate = playback_rate ,
98142ibuf = 32000
99143 )
100144except Exception as e :
101145print (f"Warning: simulating playback (I2S init failed):{ e } " )
102146
103- print (f"Playing{ data_size } bytes (vol{ cls ._volume } %) …" )
147+ print (f"Playing{ data_size } original bytes (vol{ cls ._volume } %) …" )
104148f .seek (data_start )
105149
150+ # ----- Viper volume scaler (works on any buffer) -------------------
106151@micropython .viper
107152def scale_audio (buf :ptr8 ,num_bytes :int ,scale_fixed :int ):
108- # Process 16-bit samples (2 bytes each)
109153for i in range (0 ,num_bytes ,2 ):
110154lo = int (buf [i ])
111155hi = int (buf [i + 1 ])
@@ -121,39 +165,43 @@ def scale_audio(buf: ptr8, num_bytes: int, scale_fixed: int):
121165buf [i + 1 ]= (sample >> 8 )& 255
122166
123167chunk_size = 4096
124- bytes_per_sample = 2 * channels # 2 bytes per channel
125- total = 0
168+ bytes_per_original_sample = 2 * channels # 2 bytes per channel
169+ total_original = 0
126170
127- while total < data_size :
171+ while total_original < data_size :
128172if not cls ._keep_running :
129173print ("Playback stopped by user." )
130174break
131175
132- to_read = min ( chunk_size , data_size - total )
133- # Ensure we read full samples
134- to_read -= (to_read % bytes_per_sample )
176+ # read a chunk of *original* data
177+ to_read = min ( chunk_size , data_size - total_original )
178+ to_read -= (to_read % bytes_per_original_sample )
135179if to_read <= 0 :
136180break
137181
138182raw = bytearray (f .read (to_read ))
139183if not raw :
140184break
141185
142- # Apply volume scaling (in-place, per sample)
186+ # ----- up-sample if needed ---------------------------------
187+ if upsample_factor > 1 :
188+ raw = cls ._upsample_buffer (raw ,upsample_factor )
189+
190+ # ----- volume scaling ---------------------------------------
143191scale = cls ._volume / 100.0
144192if scale < 1.0 :
145193scale_fixed = int (scale * 32768 )
146194scale_audio (raw ,len (raw ),scale_fixed )
147195
148- #Write to I2S (stereo interleaves L,R,L,R...)
196+ #----- output ------------------------------------------------
149197if cls ._i2s :
150198cls ._i2s .write (raw )
151199else :
152- #Simulate timing
153- num_samples = len (raw )// bytes_per_sample
154- time .sleep (num_samples / sample_rate )
200+ #simulate timing with the *playback* rate
201+ num_samples = len (raw )// ( 2 * channels )
202+ time .sleep (num_samples / playback_rate )
155203
156- total += len ( raw )
204+ total_original += to_read # count original bytes only
157205
158206print ("Playback finished." )
159207except Exception as e :