Skip to content

Commit b2329aa

Browse files
AudioPlayer: support 8, 16, 24 and 32 bits per sample
1 parent d3be0ce commit b2329aa

File tree

1 file changed

+92
-30
lines changed
  • internal_filesystem/apps/com.micropythonos.musicplayer/assets

1 file changed

+92
-30
lines changed

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

Lines changed: 92 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,21 @@
55

66

77
# ----------------------------------------------------------------------
8-
# AudioPlayer – robust, volume-controllable WAV player (MONO + STEREO)
9-
# Auto-up-samples any rate < 22050 Hz to 22050 Hz for MAX98357
8+
# AudioPlayer – robust, volume-controllable WAV player
9+
# Supports 8 / 16 / 24 / 32-bit PCM, mono + stereo
10+
# Auto-up-samples any rate < 22050 Hz to >=22050 Hz
1011
# ----------------------------------------------------------------------
1112
class AudioPlayer:
1213
_i2s = None
1314
_volume = 50 # 0-100
1415
_keep_running = True
1516

1617
# ------------------------------------------------------------------
17-
# WAV header parser
18+
# WAV header parser – returns bit-depth
1819
# ------------------------------------------------------------------
1920
@staticmethod
2021
def find_data_chunk(f):
21-
"""Return (data_start, data_size, sample_rate, channels)"""
22+
"""Return (data_start, data_size, sample_rate, channels, bits_per_sample)"""
2223
f.seek(0)
2324
if f.read(4) != b'RIFF':
2425
raise ValueError("Not a RIFF file")
@@ -29,6 +30,7 @@ def find_data_chunk(f):
2930
pos = 12
3031
sample_rate = None
3132
channels = None
33+
bits_per_sample = None
3234
while pos < file_size:
3335
f.seek(pos)
3436
chunk_id = f.read(4)
@@ -45,10 +47,11 @@ def find_data_chunk(f):
4547
if channels not in (1, 2):
4648
raise ValueError("Only mono or stereo supported")
4749
sample_rate = int.from_bytes(fmt[4:8], 'little')
48-
if int.from_bytes(fmt[14:16], 'little') != 16:
49-
raise ValueError("Only 16-bit supported")
50+
bits_per_sample = int.from_bytes(fmt[14:16], 'little')
51+
if bits_per_sample not in (8, 16, 24, 32):
52+
raise ValueError("Only 8/16/24/32-bit PCM supported")
5053
elif chunk_id == b'data':
51-
return f.tell(), chunk_size, sample_rate, channels
54+
return f.tell(), chunk_size, sample_rate, channels, bits_per_sample
5255
pos += 8 + chunk_size
5356
if chunk_size % 2:
5457
pos += 1
@@ -72,21 +75,14 @@ def stop_playing(cls):
7275
cls._keep_running = False
7376

7477
# ------------------------------------------------------------------
75-
# Helper: up-sample a raw PCM buffer (zero-order-hold)
78+
# 1. Up-sample 16-bit buffer (zero-order-hold)
7679
# ------------------------------------------------------------------
7780
@staticmethod
7881
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-
"""
8482
if factor == 1:
8583
return raw
86-
8784
upsampled = bytearray(len(raw) * factor)
8885
out_idx = 0
89-
# each sample = 2 bytes
9086
for i in range(0, len(raw), 2):
9187
lo = raw[i]
9288
hi = raw[i + 1]
@@ -96,6 +92,64 @@ def _upsample_buffer(raw: bytearray, factor: int) -> bytearray:
9692
out_idx += 2
9793
return upsampled
9894

95+
# ------------------------------------------------------------------
96+
# 2. Convert 8-bit to 16-bit (non-viper, Viper-safe)
97+
# ------------------------------------------------------------------
98+
@staticmethod
99+
def _convert_8_to_16(buf: bytearray) -> bytearray:
100+
out = bytearray(len(buf) * 2)
101+
j = 0
102+
for i in range(len(buf)):
103+
u8 = buf[i]
104+
s16 = (u8 - 128) << 8
105+
out[j] = s16 & 0xFF
106+
out[j + 1] = (s16 >> 8) & 0xFF
107+
j += 2
108+
return out
109+
110+
# ------------------------------------------------------------------
111+
# 3. Convert 24-bit to 16-bit (non-viper)
112+
# ------------------------------------------------------------------
113+
@staticmethod
114+
def _convert_24_to_16(buf: bytearray) -> bytearray:
115+
samples = len(buf) // 3
116+
out = bytearray(samples * 2)
117+
j = 0
118+
for i in range(samples):
119+
b0 = buf[j]
120+
b1 = buf[j + 1]
121+
b2 = buf[j + 2]
122+
s24 = (b2 << 16) | (b1 << 8) | b0
123+
if b2 & 0x80:
124+
s24 -= 0x1000000
125+
s16 = s24 >> 8
126+
out[i * 2] = s16 & 0xFF
127+
out[i * 2 + 1] = (s16 >> 8) & 0xFF
128+
j += 3
129+
return out
130+
131+
# ------------------------------------------------------------------
132+
# 4. Convert 32-bit to 16-bit (non-viper)
133+
# ------------------------------------------------------------------
134+
@staticmethod
135+
def _convert_32_to_16(buf: bytearray) -> bytearray:
136+
samples = len(buf) // 4
137+
out = bytearray(samples * 2)
138+
j = 0
139+
for i in range(samples):
140+
b0 = buf[j]
141+
b1 = buf[j + 1]
142+
b2 = buf[j + 2]
143+
b3 = buf[j + 3]
144+
s32 = (b3 << 24) | (b2 << 16) | (b1 << 8) | b0
145+
if b3 & 0x80:
146+
s32 -= 0x100000000
147+
s16 = s32 >> 16
148+
out[i * 2] = s16 & 0xFF
149+
out[i * 2 + 1] = (s16 >> 8) & 0xFF
150+
j += 4
151+
return out
152+
99153
# ------------------------------------------------------------------
100154
# Main playback routine
101155
# ------------------------------------------------------------------
@@ -109,25 +163,25 @@ def play_wav(cls, filename):
109163
print(f"File size: {file_size} bytes")
110164

111165
# ----- parse header ------------------------------------------------
112-
data_start, data_size, original_rate, channels = cls.find_data_chunk(f)
166+
data_start, data_size, original_rate, channels, bits_per_sample = \
167+
cls.find_data_chunk(f)
113168

114-
# ----- decide playback rate (force >= 22050 Hz) --------------------
169+
# ----- decide playback rate (force >=22050 Hz) --------------------
115170
target_rate = 22050
116171
if original_rate >= target_rate:
117172
playback_rate = original_rate
118173
upsample_factor = 1
119174
else:
120-
# find smallest integer factor so original * factor >= target
121175
upsample_factor = (target_rate + original_rate - 1) // original_rate
122176
playback_rate = original_rate * upsample_factor
123177

124-
print(f"Original: {original_rate} Hz → Playback: {playback_rate} Hz "
125-
f"(factor {upsample_factor}), {channels}-ch")
178+
print(f"Original: {original_rate} Hz, {bits_per_sample}-bit, {channels}-ch "
179+
f"to Playback: {playback_rate} Hz (factor {upsample_factor})")
126180

127181
if data_size > file_size - data_start:
128182
data_size = file_size - data_start
129183

130-
# ----- I2S init ----------------------------------------------------
184+
# ----- I2S init (always 16-bit) ----------------------------------
131185
try:
132186
i2s_format = machine.I2S.MONO if channels == 1 else machine.I2S.STEREO
133187
cls._i2s = machine.I2S(
@@ -144,10 +198,10 @@ def play_wav(cls, filename):
144198
except Exception as e:
145199
print(f"Warning: simulating playback (I2S init failed): {e}")
146200

147-
print(f"Playing {data_size} original bytes (vol {cls._volume}%) ")
201+
print(f"Playing {data_size} original bytes (vol {cls._volume}%) ...")
148202
f.seek(data_start)
149203

150-
# ----- Viper volume scaler (works on any buffer) -------------------
204+
# ----- Viper volume scaler (16-bit only) -------------------------
151205
@micropython.viper
152206
def scale_audio(buf: ptr8, num_bytes: int, scale_fixed: int):
153207
for i in range(0, num_bytes, 2):
@@ -165,15 +219,15 @@ def scale_audio(buf: ptr8, num_bytes: int, scale_fixed: int):
165219
buf[i+1] = (sample >> 8) & 255
166220

167221
chunk_size = 4096
168-
bytes_per_original_sample = 2 * channels # 2 bytes per channel
222+
bytes_per_original_sample = (bits_per_sample // 8) * channels
169223
total_original = 0
170224

171225
while total_original < data_size:
172226
if not cls._keep_running:
173227
print("Playback stopped by user.")
174228
break
175229

176-
# read a chunk of *original* data
230+
# ---- read a whole-sample chunk of original data -------------
177231
to_read = min(chunk_size, data_size - total_original)
178232
to_read -= (to_read % bytes_per_original_sample)
179233
if to_read <= 0:
@@ -183,25 +237,33 @@ def scale_audio(buf: ptr8, num_bytes: int, scale_fixed: int):
183237
if not raw:
184238
break
185239

186-
# ----- up-sample if needed ---------------------------------
240+
# ---- 1. Convert bit-depth to 16-bit (non-viper) -------------
241+
if bits_per_sample == 8:
242+
raw = cls._convert_8_to_16(raw)
243+
elif bits_per_sample == 24:
244+
raw = cls._convert_24_to_16(raw)
245+
elif bits_per_sample == 32:
246+
raw = cls._convert_32_to_16(raw)
247+
# 16-bit to unchanged
248+
249+
# ---- 2. Up-sample if needed ---------------------------------
187250
if upsample_factor > 1:
188251
raw = cls._upsample_buffer(raw, upsample_factor)
189252

190-
# ----- volume scaling ---------------------------------------
253+
# ---- 3. Volume scaling --------------------------------------
191254
scale = cls._volume / 100.0
192255
if scale < 1.0:
193256
scale_fixed = int(scale * 32768)
194257
scale_audio(raw, len(raw), scale_fixed)
195258

196-
# ----- output ------------------------------------------------
259+
# ---- 4. Output ---------------------------------------------
197260
if cls._i2s:
198261
cls._i2s.write(raw)
199262
else:
200-
# simulate timing with the *playback* rate
201263
num_samples = len(raw) // (2 * channels)
202264
time.sleep(num_samples / playback_rate)
203265

204-
total_original += to_read # count original bytes only
266+
total_original += to_read
205267

206268
print("Playback finished.")
207269
except Exception as e:

0 commit comments

Comments
 (0)