From d149883a08cef72a4cb55337b9c2684ed6bceb72 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Mon, 27 Oct 2025 23:06:31 +0100 Subject: [PATCH] Add simple Music Player app Currently only handles mono wav files - to be improved. --- .../META-INF/MANIFEST.JSON | 24 +++++ .../assets/music_player.py | 97 ++++++++++++++++++ .../res/mipmap-mdpi/icon_64x64.png | Bin 0 -> 5245 bytes 3 files changed, 121 insertions(+) create mode 100644 internal_filesystem/apps/com.micropythonos.musicplayer/META-INF/MANIFEST.JSON create mode 100644 internal_filesystem/apps/com.micropythonos.musicplayer/assets/music_player.py create mode 100644 internal_filesystem/apps/com.micropythonos.musicplayer/res/mipmap-mdpi/icon_64x64.png diff --git a/internal_filesystem/apps/com.micropythonos.musicplayer/META-INF/MANIFEST.JSON b/internal_filesystem/apps/com.micropythonos.musicplayer/META-INF/MANIFEST.JSON new file mode 100644 index 00000000..7f5d7332 --- /dev/null +++ b/internal_filesystem/apps/com.micropythonos.musicplayer/META-INF/MANIFEST.JSON @@ -0,0 +1,24 @@ +{ +"name": "Music Player", +"publisher": "MicroPythonOS", +"short_description": "Player audio files", +"long_description": "Traverse around the filesystem and play audio files that you select.", +"icon_url": "https://apps.micropythonos.com/apps/com.micropythonos.musicplayer/icons/com.micropythonos.musicplayer_0.0.1_64x64.png", +"download_url": "https://apps.micropythonos.com/apps/com.micropythonos.musicplayer/mpks/com.micropythonos.musicplayer_0.0.1.mpk", +"fullname": "com.micropythonos.musicplayer", +"version": "0.0.1", +"category": "development", +"activities": [ + { + "entrypoint": "assets/music_player.py", + "classname": "MusicPlayer", + "intent_filters": [ + { + "action": "main", + "category": "launcher" + } + ] + } + ] +} + diff --git a/internal_filesystem/apps/com.micropythonos.musicplayer/assets/music_player.py b/internal_filesystem/apps/com.micropythonos.musicplayer/assets/music_player.py new file mode 100644 index 00000000..7175b8ce --- /dev/null +++ b/internal_filesystem/apps/com.micropythonos.musicplayer/assets/music_player.py @@ -0,0 +1,97 @@ +from mpos.apps import Activity +import mpos.sdcard +import mpos.ui +# play music +import machine +import uos +from machine import I2S, Pin + +class MusicPlayer(Activity): + + # Widgets: + file_explorer = None + + def onCreate(self): + screen = lv.obj() + # the user might have recently plugged in the sd card so try to mount it + mpos.sdcard.mount_with_optional_format('/sdcard') + self.file_explorer = lv.file_explorer(screen) + self.file_explorer.explorer_open_dir('M:/') + self.file_explorer.align(lv.ALIGN.CENTER, 0, 0) + self.file_explorer.add_event_cb(self.file_explorer_event_cb, lv.EVENT.ALL, None) + self.setContentView(screen) + + def onResume(self, screen): + # the user might have recently plugged in the sd card so try to mount it + mpos.sdcard.mount_with_optional_format('/sdcard') # would be good to refresh the file_explorer so the /sdcard folder shows up + + def file_explorer_event_cb(self, event): + event_code = event.get_code() + if event_code not in [2,19,23,24,25,26,27,28,29,30,31,32,33,47,49,52]: + name = mpos.ui.get_event_name(event_code) + print(f"file_explorer_event_cb {event_code} with name {name}") + if event_code == lv.EVENT.VALUE_CHANGED: + path = self.file_explorer.explorer_get_current_path() + clean_path = path[2:] if path[1] == ':' else path + file = self.file_explorer.explorer_get_selected_file_name() + fullpath = f"{clean_path}{file}" + print(f"Selected: {fullpath}") + if fullpath.lower().endswith('.wav'): + self.play_wav(fullpath) + else: + print("INFO: ignoring unsupported file format") + + def parse_wav_header(self, f): + """Parse standard WAV header (44 bytes) and return channels, sample_rate, bits_per_sample, data_size.""" + header = f.read(44) + if header[0:4] != b'RIFF' or header[8:12] != b'WAVE' or header[12:16] != b'fmt ': + raise ValueError("Invalid WAV file") + audio_format = int.from_bytes(header[20:22], 'little') + if audio_format != 1: # PCM only + raise ValueError("Only PCM WAV supported") + channels = int.from_bytes(header[22:24], 'little') + sample_rate = int.from_bytes(header[24:28], 'little') + bits_per_sample = int.from_bytes(header[34:36], 'little') + # Skip to data chunk + f.read(8) # 'data' + size + data_size = int.from_bytes(f.read(4), 'little') + return channels, sample_rate, bits_per_sample, data_size + + def play_wav(self, filename): + """Play WAV file via I2S to MAX98357A.""" + with open(filename, 'rb') as f: + try: + channels, sample_rate, bits_per_sample, data_size = self.parse_wav_header(f) + if bits_per_sample != 16: + raise ValueError("Only 16-bit audio supported") + if channels != 1: + raise ValueError("Only mono audio supported (convert with -ac 1 in FFmpeg)") + + # Configure I2S (TX mode for output) + i2s = I2S(0, # I2S peripheral 0 + sck=Pin(2, Pin.OUT), # BCK + ws=Pin(47, Pin.OUT), # LRCK + sd=Pin(16, Pin.OUT), # DIN + mode=I2S.TX, + bits=16, + format=I2S.MONO, + rate=sample_rate, + ibuf=16000) # Internal buffer size (adjust if audio stutters) + + print(f"Playing {data_size} bytes at {sample_rate} Hz...") + + # Stream data in chunks (16-bit = 2 bytes per sample) + chunk_size = 1024 * 2 # 1KB chunks (tune for your RAM) + total_read = 0 + while total_read < data_size: + chunk = f.read(min(chunk_size, data_size - total_read)) + if not chunk: + break + i2s.write(chunk) # Direct byte stream (little-endian matches I2S) + total_read += len(chunk) + + print("Playback finished.") + except Exception as e: + print(f"Error: {e}") + finally: + i2s.deinit() # Clean up diff --git a/internal_filesystem/apps/com.micropythonos.musicplayer/res/mipmap-mdpi/icon_64x64.png b/internal_filesystem/apps/com.micropythonos.musicplayer/res/mipmap-mdpi/icon_64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..9e6923cb92465cead39f4afc246329c53d6bf460 GIT binary patch literal 5245 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 Q0|Nttr>mdKI;Vst09vvY5C8xG literal 0 HcmV?d00001