Skip to content

Commit 1984857

Browse files
AudioPlayer: support stereo
1 parent f43684a commit 1984857

1 file changed

Lines changed: 39 additions & 33 deletions

File tree

  • internal_filesystem/apps/com.micropythonos.musicplayer/assets

internal_filesystem/apps/com.micropythonos.musicplayer/assets/audio_player.py

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@
55

66

77
# ----------------------------------------------------------------------
8-
# AudioPlayer – robust, volume-controllable WAV player
8+
# AudioPlayer – robust, volume-controllable WAV player (MONO + STEREO)
99
# ----------------------------------------------------------------------
1010
class 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

Comments
 (0)