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
1717 def 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 )"""
1919 f .seek (0 )
2020 if f .read (4 ) != b'RIFF' :
2121 raise ValueError ("Not a RIFF file" )
@@ -25,6 +25,7 @@ def find_data_chunk(f):
2525
2626 pos = 12
2727 sample_rate = None
28+ channels = None
2829 while pos < file_size :
2930 f .seek (pos )
3031 chunk_id = f .read (4 )
@@ -38,14 +39,13 @@ def find_data_chunk(f):
3839 if int .from_bytes (fmt [0 :2 ], 'little' ) != 1 :
3940 raise ValueError ("Only PCM supported" )
4041 channels = 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 mono or stereo supported" )
4344 sample_rate = int .from_bytes (fmt [4 :8 ], 'little' )
4445 if int .from_bytes (fmt [14 :16 ], 'little' ) != 16 :
4546 raise ValueError ("Only 16-bit supported" )
4647 elif 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
4949 pos += 8 + chunk_size
5050 if chunk_size % 2 :
5151 pos += 1
@@ -56,37 +56,35 @@ def find_data_chunk(f):
5656 # ------------------------------------------------------------------
5757 @classmethod
5858 def 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 ))
6160 cls ._volume = volume
6261
6362 @classmethod
6463 def get_volume (cls ) -> int :
65- """Return current volume 0-100."""
6664 return cls ._volume
6765
68- # @classmethod
69- def stop_playing ():
66+ @classmethod
67+ def stop_playing (cls ):
7068 print ("stop_playing()" )
71- AudioPlayer ._keep_running = False
69+ cls ._keep_running = False
7270
7371 @classmethod
7472 def 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
7774 try :
7875 with open (filename , 'rb' ) as f :
7976 st = os .stat (filename )
8077 file_size = st [6 ]
8178 print (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
8683 if data_size > file_size - data_start :
8784 data_size = file_size - data_start
8885
8986 # ---- I2S init ------------------------------------------------
87+ i2s_format = machine .I2S .MONO if channels == 1 else machine .I2S .STEREO
9088 try :
9189 cls ._i2s = machine .I2S (
9290 0 ,
@@ -95,18 +93,19 @@ def play_wav(cls, filename):
9593 sd = machine .Pin (16 , machine .Pin .OUT ),
9694 mode = machine .I2S .TX ,
9795 bits = 16 ,
98- format = machine . I2S . MONO ,
96+ format = i2s_format ,
9997 rate = sample_rate ,
10098 ibuf = 32000
10199 )
102100 except Exception as e :
103- print (f"Warning: simulating playback due to error initializing I2S audio device : { e } " )
101+ print (f"Warning: simulating playback ( I2S init failed) : { e } " )
104102
105103 print (f"Playing { data_size } bytes (vol { cls ._volume } %) …" )
106104 f .seek (data_start )
107105
108106 @micropython .viper
109107 def scale_audio (buf : ptr8 , num_bytes : int , scale_fixed : int ):
108+ # Process 16-bit samples (2 bytes each)
110109 for i in range (0 , num_bytes , 2 ):
111110 lo = int (buf [i ])
112111 hi = int (buf [i + 1 ])
@@ -121,32 +120,39 @@ def scale_audio(buf: ptr8, num_bytes: int, scale_fixed: int):
121120 buf [i ] = sample & 255
122121 buf [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
126125 total = 0
126+
127127 while 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." )
133130 break
131+
134132 to_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 ))
136139 if not raw :
137140 break
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
141144 if scale < 1.0 :
142145 scale_fixed = int (scale * 32768 )
143146 scale_audio (raw , len (raw ), scale_fixed )
144- # ---------------------------------------
145147
148+ # Write to I2S (stereo interleaves L,R,L,R...)
146149 if cls ._i2s :
147150 cls ._i2s .write (raw )
148151 else :
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+
150156 total += len (raw )
151157
152158 print ("Playback finished." )
0 commit comments