diff --git a/internal_filesystem/apps/com.micropythonos.musicplayer/assets/audio_player.py b/internal_filesystem/apps/com.micropythonos.musicplayer/assets/audio_player.py index 26425b79..192bbd05 100644 --- a/internal_filesystem/apps/com.micropythonos.musicplayer/assets/audio_player.py +++ b/internal_filesystem/apps/com.micropythonos.musicplayer/assets/audio_player.py @@ -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: