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
1720 def 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)"""
1922 f .seek (0 )
2023 if f .read (4 ) != b'RIFF' :
2124 raise ValueError ("Not a RIFF file" )
@@ -68,6 +71,34 @@ def stop_playing(cls):
6871 print ("stop_playing()" )
6972 cls ._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
72103 def play_wav (cls , filename ):
73104 cls ._keep_running = True
@@ -77,15 +108,28 @@ def play_wav(cls, filename):
77108 file_size = st [6 ]
78109 print (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
83127 if data_size > file_size - data_start :
84128 data_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 ----------------------------------------------------
88131 try :
132+ i2s_format = machine .I2S .MONO if channels == 1 else machine .I2S .STEREO
89133 cls ._i2s = machine .I2S (
90134 0 ,
91135 sck = machine .Pin (2 , machine .Pin .OUT ),
@@ -94,18 +138,18 @@ def play_wav(cls, filename):
94138 mode = machine .I2S .TX ,
95139 bits = 16 ,
96140 format = i2s_format ,
97- rate = sample_rate ,
141+ rate = playback_rate ,
98142 ibuf = 32000
99143 )
100144 except Exception as e :
101145 print (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 } %) …" )
104148 f .seek (data_start )
105149
150+ # ----- Viper volume scaler (works on any buffer) -------------------
106151 @micropython .viper
107152 def scale_audio (buf : ptr8 , num_bytes : int , scale_fixed : int ):
108- # Process 16-bit samples (2 bytes each)
109153 for i in range (0 , num_bytes , 2 ):
110154 lo = int (buf [i ])
111155 hi = int (buf [i + 1 ])
@@ -121,39 +165,43 @@ def scale_audio(buf: ptr8, num_bytes: int, scale_fixed: int):
121165 buf [i + 1 ] = (sample >> 8 ) & 255
122166
123167 chunk_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 :
128172 if not cls ._keep_running :
129173 print ("Playback stopped by user." )
130174 break
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 )
135179 if to_read <= 0 :
136180 break
137181
138182 raw = bytearray (f .read (to_read ))
139183 if not raw :
140184 break
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 ---------------------------------------
143191 scale = cls ._volume / 100.0
144192 if scale < 1.0 :
145193 scale_fixed = int (scale * 32768 )
146194 scale_audio (raw , len (raw ), scale_fixed )
147195
148- # Write to I2S (stereo interleaves L,R,L,R...)
196+ # ----- output ------------------------------------------------
149197 if cls ._i2s :
150198 cls ._i2s .write (raw )
151199 else :
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
158206 print ("Playback finished." )
159207 except Exception as e :
0 commit comments