55
66
77# ----------------------------------------------------------------------
8- # AudioPlayer – robust, volume-controllable WAV player
8+ # AudioPlayer – robust, volume-controllable WAV player (MONO + STEREO)
99# ----------------------------------------------------------------------
1010class AudioPlayer :
11- # class-level defaults (shared by every instance)
12- _i2s = None # the I2S object (created once per playback)
13- _volume = 50 # 0-100 (100 = full scale)
11+ # class-level defaults
12+ _i2s = None
13+ _volume = 50 # 0-100
1414_keep_running = True
1515
1616@staticmethod
1717def find_data_chunk (f ):
18- """Skip chunks until 'data' is found → (data_start, data_size, sample_rate)"""
18+ """Skip chunks until 'data' is found → (data_start, data_size, sample_rate, channels )"""
1919f .seek (0 )
2020if f .read (4 )!= b'RIFF' :
2121raise ValueError ("Not a RIFF file" )
@@ -25,6 +25,7 @@ def find_data_chunk(f):
2525
2626pos = 12
2727sample_rate = None
28+ channels = None
2829while pos < file_size :
2930f .seek (pos )
3031chunk_id = f .read (4 )
@@ -38,14 +39,13 @@ def find_data_chunk(f):
3839if int .from_bytes (fmt [0 :2 ],'little' )!= 1 :
3940raise ValueError ("Only PCM supported" )
4041channels = int .from_bytes (fmt [2 :4 ],'little' )
41- if channels != 1 :
42- raise ValueError ("Only mono supported" )
42+ if channels not in ( 1 , 2 ) :
43+ raise ValueError ("Only monoor stereo supported" )
4344sample_rate = int .from_bytes (fmt [4 :8 ],'little' )
4445if int .from_bytes (fmt [14 :16 ],'little' )!= 16 :
4546raise ValueError ("Only 16-bit supported" )
4647elif chunk_id == b'data' :
47- return f .tell (),chunk_size ,sample_rate
48- # next chunk (pad byte if odd length)
48+ return f .tell (),chunk_size ,sample_rate ,channels
4949pos += 8 + chunk_size
5050if chunk_size % 2 :
5151pos += 1
@@ -56,37 +56,35 @@ def find_data_chunk(f):
5656# ------------------------------------------------------------------
5757@classmethod
5858def set_volume (cls ,volume :int ):
59- """Set playback volume 0-100 (100 = full scale)."""
60- volume = max (0 ,min (100 ,volume ))# clamp
59+ volume = max (0 ,min (100 ,volume ))
6160cls ._volume = volume
6261
6362@classmethod
6463def get_volume (cls )-> int :
65- """Return current volume 0-100."""
6664return cls ._volume
6765
68- # @classmethod
69- def stop_playing ():
66+ @classmethod
67+ def stop_playing (cls ):
7068print ("stop_playing()" )
71- AudioPlayer ._keep_running = False
69+ cls ._keep_running = False
7270
7371@classmethod
7472def play_wav (cls ,filename ):
75- AudioPlayer ._keep_running = True
76- """Play a large mono 16-bit PCM WAV file with on-the-fly volume."""
73+ cls ._keep_running = True
7774try :
7875with open (filename ,'rb' )as f :
7976st = os .stat (filename )
8077file_size = st [6 ]
8178print (f"File size:{ file_size } bytes" )
8279
83- data_start ,data_size ,sample_rate = cls .find_data_chunk (f )
84- print (f"data chunk:{ data_size } bytes @{ sample_rate } Hz" )
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 " )
8582
8683if data_size > file_size - data_start :
8784data_size = file_size - data_start
8885
8986# ---- I2S init ------------------------------------------------
87+ i2s_format = machine .I2S .MONO if channels == 1 else machine .I2S .STEREO
9088try :
9189cls ._i2s = machine .I2S (
92900 ,
@@ -95,18 +93,19 @@ def play_wav(cls, filename):
9593sd = machine .Pin (16 ,machine .Pin .OUT ),
9694mode = machine .I2S .TX ,
9795bits = 16 ,
98- format = machine . I2S . MONO ,
96+ format = i2s_format ,
9997rate = sample_rate ,
10098ibuf = 32000
10199 )
102100except Exception as e :
103- print (f"Warning: simulating playbackdue to error initializing I2Saudio device :{ e } " )
101+ print (f"Warning: simulating playback( I2Sinit failed) :{ e } " )
104102
105103print (f"Playing{ data_size } bytes (vol{ cls ._volume } %) …" )
106104f .seek (data_start )
107105
108106@micropython .viper
109107def scale_audio (buf :ptr8 ,num_bytes :int ,scale_fixed :int ):
108+ # Process 16-bit samples (2 bytes each)
110109for i in range (0 ,num_bytes ,2 ):
111110lo = int (buf [i ])
112111hi = int (buf [i + 1 ])
@@ -121,32 +120,39 @@ def scale_audio(buf: ptr8, num_bytes: int, scale_fixed: int):
121120buf [i ]= sample & 255
122121buf [i + 1 ]= (sample >> 8 )& 255
123122
124- chunk_size = 4096 # 4 KB → safe on ESP32
125-
123+ chunk_size = 4096
124+ bytes_per_sample = 2 * channels # 2 bytes per channel
126125total = 0
126+
127127while total < data_size :
128- # Progress:
129- #if total % 51 == 0:
130- # print('.', end='')
131- if not AudioPlayer ._keep_running :
132- print ("_keep_running = False, stopping..." )
128+ if not cls ._keep_running :
129+ print ("Playback stopped by user." )
133130break
131+
134132to_read = min (chunk_size ,data_size - total )
135- raw = bytearray (f .read (to_read ))# mutable for in-place scaling
133+ # Ensure we read full samples
134+ to_read -= (to_read % bytes_per_sample )
135+ if to_read <= 0 :
136+ break
137+
138+ raw = bytearray (f .read (to_read ))
136139if not raw :
137140break
138141
139- #---- fast viper scaling (in-place) ----
140- scale = cls ._volume / 100.0 # adjust the volume on the fly instead of at the start of playback
142+ #Apply volume scaling (in-place, per sample)
143+ scale = cls ._volume / 100.0
141144if scale < 1.0 :
142145scale_fixed = int (scale * 32768 )
143146scale_audio (raw ,len (raw ),scale_fixed )
144- # ---------------------------------------
145147
148+ # Write to I2S (stereo interleaves L,R,L,R...)
146149if cls ._i2s :
147150cls ._i2s .write (raw )
148151else :
149- time .sleep ((to_read / 2 )/ 44100 )# 16 bits (2 bytes) per sample at 44100 samples/s
152+ # Simulate timing
153+ num_samples = len (raw )// bytes_per_sample
154+ time .sleep (num_samples / sample_rate )
155+
150156total += len (raw )
151157
152158print ("Playback finished." )