AudioPlayer: support 8, 16, 24 and 32 bits per sample

This commit is contained in:
Thomas Farstrike
2025-10-28 19:36:20 +01:00
parent d3be0ce94b
commit b2329aa975
@@ -5,8 +5,9 @@ import micropython
# ----------------------------------------------------------------------
# AudioPlayer robust, volume-controllable WAV player (MONO + STEREO)
# Auto-up-samples any rate < 22050 Hz to 22050 Hz for MAX98357
# AudioPlayer robust, volume-controllable WAV player
# Supports 8 / 16 / 24 / 32-bit PCM, mono + stereo
# Auto-up-samples any rate < 22050 Hz to >=22050 Hz
# ----------------------------------------------------------------------
class AudioPlayer:
_i2s = None
@@ -14,11 +15,11 @@ class AudioPlayer:
_keep_running = True
# ------------------------------------------------------------------
# WAV header parser
# WAV header parser returns bit-depth
# ------------------------------------------------------------------
@staticmethod
def find_data_chunk(f):
"""Return (data_start, data_size, sample_rate, channels)"""
"""Return (data_start, data_size, sample_rate, channels, bits_per_sample)"""
f.seek(0)
if f.read(4) != b'RIFF':
raise ValueError("Not a RIFF file")
@@ -29,6 +30,7 @@ class AudioPlayer:
pos = 12
sample_rate = None
channels = None
bits_per_sample = None
while pos < file_size:
f.seek(pos)
chunk_id = f.read(4)
@@ -45,10 +47,11 @@ class AudioPlayer:
if channels not in (1, 2):
raise ValueError("Only mono or stereo supported")
sample_rate = int.from_bytes(fmt[4:8], 'little')
if int.from_bytes(fmt[14:16], 'little') != 16:
raise ValueError("Only 16-bit supported")
bits_per_sample = int.from_bytes(fmt[14:16], 'little')
if bits_per_sample not in (8, 16, 24, 32):
raise ValueError("Only 8/16/24/32-bit PCM supported")
elif chunk_id == b'data':
return f.tell(), chunk_size, sample_rate, channels
return f.tell(), chunk_size, sample_rate, channels, bits_per_sample
pos += 8 + chunk_size
if chunk_size % 2:
pos += 1
@@ -72,21 +75,14 @@ class AudioPlayer:
cls._keep_running = False
# ------------------------------------------------------------------
# Helper: up-sample a raw PCM buffer (zero-order-hold)
# 1. Up-sample 16-bit buffer (zero-order-hold)
# ------------------------------------------------------------------
@staticmethod
def _upsample_buffer(raw: bytearray, factor: int) -> bytearray:
"""
Duplicate each 16-bit sample `factor` times.
Input: interleaved L,R,L,R... (or mono)
Output: same layout, each sample repeated `factor` times.
"""
if factor == 1:
return raw
upsampled = bytearray(len(raw) * factor)
out_idx = 0
# each sample = 2 bytes
for i in range(0, len(raw), 2):
lo = raw[i]
hi = raw[i + 1]
@@ -96,6 +92,64 @@ class AudioPlayer:
out_idx += 2
return upsampled
# ------------------------------------------------------------------
# 2. Convert 8-bit to 16-bit (non-viper, Viper-safe)
# ------------------------------------------------------------------
@staticmethod
def _convert_8_to_16(buf: bytearray) -> bytearray:
out = bytearray(len(buf) * 2)
j = 0
for i in range(len(buf)):
u8 = buf[i]
s16 = (u8 - 128) << 8
out[j] = s16 & 0xFF
out[j + 1] = (s16 >> 8) & 0xFF
j += 2
return out
# ------------------------------------------------------------------
# 3. Convert 24-bit to 16-bit (non-viper)
# ------------------------------------------------------------------
@staticmethod
def _convert_24_to_16(buf: bytearray) -> bytearray:
samples = len(buf) // 3
out = bytearray(samples * 2)
j = 0
for i in range(samples):
b0 = buf[j]
b1 = buf[j + 1]
b2 = buf[j + 2]
s24 = (b2 << 16) | (b1 << 8) | b0
if b2 & 0x80:
s24 -= 0x1000000
s16 = s24 >> 8
out[i * 2] = s16 & 0xFF
out[i * 2 + 1] = (s16 >> 8) & 0xFF
j += 3
return out
# ------------------------------------------------------------------
# 4. Convert 32-bit to 16-bit (non-viper)
# ------------------------------------------------------------------
@staticmethod
def _convert_32_to_16(buf: bytearray) -> bytearray:
samples = len(buf) // 4
out = bytearray(samples * 2)
j = 0
for i in range(samples):
b0 = buf[j]
b1 = buf[j + 1]
b2 = buf[j + 2]
b3 = buf[j + 3]
s32 = (b3 << 24) | (b2 << 16) | (b1 << 8) | b0
if b3 & 0x80:
s32 -= 0x100000000
s16 = s32 >> 16
out[i * 2] = s16 & 0xFF
out[i * 2 + 1] = (s16 >> 8) & 0xFF
j += 4
return out
# ------------------------------------------------------------------
# Main playback routine
# ------------------------------------------------------------------
@@ -109,25 +163,25 @@ class AudioPlayer:
print(f"File size: {file_size} bytes")
# ----- parse header ------------------------------------------------
data_start, data_size, original_rate, channels = cls.find_data_chunk(f)
data_start, data_size, original_rate, channels, bits_per_sample = \
cls.find_data_chunk(f)
# ----- decide playback rate (force >= 22050 Hz) --------------------
# ----- decide playback rate (force >=22050 Hz) --------------------
target_rate = 22050
if original_rate >= target_rate:
playback_rate = original_rate
upsample_factor = 1
else:
# find smallest integer factor so original * factor >= target
upsample_factor = (target_rate + original_rate - 1) // original_rate
playback_rate = original_rate * upsample_factor
print(f"Original: {original_rate} Hz → Playback: {playback_rate} Hz "
f"(factor {upsample_factor}), {channels}-ch")
print(f"Original: {original_rate} Hz, {bits_per_sample}-bit, {channels}-ch "
f"to Playback: {playback_rate} Hz (factor {upsample_factor})")
if data_size > file_size - data_start:
data_size = file_size - data_start
# ----- I2S init ----------------------------------------------------
# ----- I2S init (always 16-bit) ----------------------------------
try:
i2s_format = machine.I2S.MONO if channels == 1 else machine.I2S.STEREO
cls._i2s = machine.I2S(
@@ -144,10 +198,10 @@ class AudioPlayer:
except Exception as e:
print(f"Warning: simulating playback (I2S init failed): {e}")
print(f"Playing {data_size} original bytes (vol {cls._volume}%) ")
print(f"Playing {data_size} original bytes (vol {cls._volume}%) ...")
f.seek(data_start)
# ----- Viper volume scaler (works on any buffer) -------------------
# ----- Viper volume scaler (16-bit only) -------------------------
@micropython.viper
def scale_audio(buf: ptr8, num_bytes: int, scale_fixed: int):
for i in range(0, num_bytes, 2):
@@ -165,7 +219,7 @@ class AudioPlayer:
buf[i+1] = (sample >> 8) & 255
chunk_size = 4096
bytes_per_original_sample = 2 * channels # 2 bytes per channel
bytes_per_original_sample = (bits_per_sample // 8) * channels
total_original = 0
while total_original < data_size:
@@ -173,7 +227,7 @@ class AudioPlayer:
print("Playback stopped by user.")
break
# read a chunk of *original* data
# ---- read a whole-sample chunk of original data -------------
to_read = min(chunk_size, data_size - total_original)
to_read -= (to_read % bytes_per_original_sample)
if to_read <= 0:
@@ -183,25 +237,33 @@ class AudioPlayer:
if not raw:
break
# ----- up-sample if needed ---------------------------------
# ---- 1. Convert bit-depth to 16-bit (non-viper) -------------
if bits_per_sample == 8:
raw = cls._convert_8_to_16(raw)
elif bits_per_sample == 24:
raw = cls._convert_24_to_16(raw)
elif bits_per_sample == 32:
raw = cls._convert_32_to_16(raw)
# 16-bit to unchanged
# ---- 2. Up-sample if needed ---------------------------------
if upsample_factor > 1:
raw = cls._upsample_buffer(raw, upsample_factor)
# ----- volume scaling ---------------------------------------
# ---- 3. Volume scaling --------------------------------------
scale = cls._volume / 100.0
if scale < 1.0:
scale_fixed = int(scale * 32768)
scale_audio(raw, len(raw), scale_fixed)
# ----- output ------------------------------------------------
# ---- 4. Output ---------------------------------------------
if cls._i2s:
cls._i2s.write(raw)
else:
# simulate timing with the *playback* rate
num_samples = len(raw) // (2 * channels)
time.sleep(num_samples / playback_rate)
total_original += to_read # count original bytes only
total_original += to_read
print("Playback finished.")
except Exception as e: