|
| 1 | +from mpos.apps import Activity |
| 2 | +import mpos.sdcard |
| 3 | +import mpos.ui |
| 4 | +# play music |
| 5 | +import machine |
| 6 | +import uos |
| 7 | +from machine import I2S, Pin |
| 8 | + |
| 9 | +class MusicPlayer(Activity): |
| 10 | + |
| 11 | + # Widgets: |
| 12 | + file_explorer = None |
| 13 | + |
| 14 | + def onCreate(self): |
| 15 | + screen = lv.obj() |
| 16 | + # the user might have recently plugged in the sd card so try to mount it |
| 17 | + mpos.sdcard.mount_with_optional_format('/sdcard') |
| 18 | + self.file_explorer = lv.file_explorer(screen) |
| 19 | + self.file_explorer.explorer_open_dir('M:/') |
| 20 | + self.file_explorer.align(lv.ALIGN.CENTER, 0, 0) |
| 21 | + self.file_explorer.add_event_cb(self.file_explorer_event_cb, lv.EVENT.ALL, None) |
| 22 | + self.setContentView(screen) |
| 23 | + |
| 24 | + def onResume(self, screen): |
| 25 | + # the user might have recently plugged in the sd card so try to mount it |
| 26 | + mpos.sdcard.mount_with_optional_format('/sdcard') # would be good to refresh the file_explorer so the /sdcard folder shows up |
| 27 | + |
| 28 | + def file_explorer_event_cb(self, event): |
| 29 | + event_code = event.get_code() |
| 30 | + if event_code not in [2,19,23,24,25,26,27,28,29,30,31,32,33,47,49,52]: |
| 31 | + name = mpos.ui.get_event_name(event_code) |
| 32 | + print(f"file_explorer_event_cb {event_code} with name {name}") |
| 33 | + if event_code == lv.EVENT.VALUE_CHANGED: |
| 34 | + path = self.file_explorer.explorer_get_current_path() |
| 35 | + clean_path = path[2:] if path[1] == ':' else path |
| 36 | + file = self.file_explorer.explorer_get_selected_file_name() |
| 37 | + fullpath = f"{clean_path}{file}" |
| 38 | + print(f"Selected: {fullpath}") |
| 39 | + if fullpath.lower().endswith('.wav'): |
| 40 | + self.play_wav(fullpath) |
| 41 | + else: |
| 42 | + print("INFO: ignoring unsupported file format") |
| 43 | + |
| 44 | + def parse_wav_header(self, f): |
| 45 | + """Parse standard WAV header (44 bytes) and return channels, sample_rate, bits_per_sample, data_size.""" |
| 46 | + header = f.read(44) |
| 47 | + if header[0:4] != b'RIFF' or header[8:12] != b'WAVE' or header[12:16] != b'fmt ': |
| 48 | + raise ValueError("Invalid WAV file") |
| 49 | + audio_format = int.from_bytes(header[20:22], 'little') |
| 50 | + if audio_format != 1: # PCM only |
| 51 | + raise ValueError("Only PCM WAV supported") |
| 52 | + channels = int.from_bytes(header[22:24], 'little') |
| 53 | + sample_rate = int.from_bytes(header[24:28], 'little') |
| 54 | + bits_per_sample = int.from_bytes(header[34:36], 'little') |
| 55 | + # Skip to data chunk |
| 56 | + f.read(8) # 'data' + size |
| 57 | + data_size = int.from_bytes(f.read(4), 'little') |
| 58 | + return channels, sample_rate, bits_per_sample, data_size |
| 59 | + |
| 60 | + def play_wav(self, filename): |
| 61 | + """Play WAV file via I2S to MAX98357A.""" |
| 62 | + with open(filename, 'rb') as f: |
| 63 | + try: |
| 64 | + channels, sample_rate, bits_per_sample, data_size = self.parse_wav_header(f) |
| 65 | + if bits_per_sample != 16: |
| 66 | + raise ValueError("Only 16-bit audio supported") |
| 67 | + if channels != 1: |
| 68 | + raise ValueError("Only mono audio supported (convert with -ac 1 in FFmpeg)") |
| 69 | + |
| 70 | + # Configure I2S (TX mode for output) |
| 71 | + i2s = I2S(0, # I2S peripheral 0 |
| 72 | + sck=Pin(2, Pin.OUT), # BCK |
| 73 | + ws=Pin(47, Pin.OUT), # LRCK |
| 74 | + sd=Pin(16, Pin.OUT), # DIN |
| 75 | + mode=I2S.TX, |
| 76 | + bits=16, |
| 77 | + format=I2S.MONO, |
| 78 | + rate=sample_rate, |
| 79 | + ibuf=16000) # Internal buffer size (adjust if audio stutters) |
| 80 | + |
| 81 | + print(f"Playing {data_size} bytes at {sample_rate} Hz...") |
| 82 | + |
| 83 | + # Stream data in chunks (16-bit = 2 bytes per sample) |
| 84 | + chunk_size = 1024 * 2 # 1KB chunks (tune for your RAM) |
| 85 | + total_read = 0 |
| 86 | + while total_read < data_size: |
| 87 | + chunk = f.read(min(chunk_size, data_size - total_read)) |
| 88 | + if not chunk: |
| 89 | + break |
| 90 | + i2s.write(chunk) # Direct byte stream (little-endian matches I2S) |
| 91 | + total_read += len(chunk) |
| 92 | + |
| 93 | + print("Playback finished.") |
| 94 | + except Exception as e: |
| 95 | + print(f"Error: {e}") |
| 96 | + finally: |
| 97 | + i2s.deinit() # Clean up |
0 commit comments