MP3Player module to manage mp3 playing
This commit is contained in:
120
software/src/mp3player.py
Normal file
120
software/src/mp3player.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
import asyncio
|
||||
from array import array
|
||||
|
||||
|
||||
class MP3Player:
|
||||
def __init__(self, audiocore):
|
||||
self.audiocore = audiocore
|
||||
self.commands = []
|
||||
self.command_event = asyncio.Event()
|
||||
self.playlist = []
|
||||
self.mp3task = None
|
||||
|
||||
def set_playlist(self, mp3files):
|
||||
"""
|
||||
Set a new playlist and start playing from the first entry.
|
||||
For convenience a single file name can also be passed.
|
||||
"""
|
||||
if type(mp3files) is bytes:
|
||||
self.playlist = [mp3files]
|
||||
else:
|
||||
self.playlist = mp3files
|
||||
self._send_command('newplaylist')
|
||||
|
||||
def play_next(self):
|
||||
"""
|
||||
Skip to the next track in the playlist. Reaching the end of the playlist stops playback.
|
||||
"""
|
||||
self._send_command('next')
|
||||
|
||||
def play_prev(self):
|
||||
"""
|
||||
Skip to the previous track in the playlist.
|
||||
"""
|
||||
self._send_command('prev')
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop playback, remembering the current position in the playlist (but not inside a track).
|
||||
"""
|
||||
self._send_command('stop')
|
||||
|
||||
def play(self):
|
||||
"""
|
||||
Start playback.
|
||||
"""
|
||||
self._send_command('play')
|
||||
|
||||
def set_volume(self, volume: int):
|
||||
"""
|
||||
Set volume (0..255).
|
||||
"""
|
||||
self.audiocore.set_volume(volume)
|
||||
|
||||
def _send_command(self, command: str):
|
||||
self.commands.append(command)
|
||||
self.command_event.set()
|
||||
|
||||
async def _play_task(self, mp3path):
|
||||
known_underruns = 0
|
||||
data = array('b', range(512))
|
||||
try:
|
||||
print(b'Playing ' + mp3path)
|
||||
with open(mp3path, 'rb') as mp3file:
|
||||
while True:
|
||||
bytes_read = mp3file.readinto(data)
|
||||
if bytes_read == 0:
|
||||
# End of file
|
||||
break
|
||||
_, _, underruns = await self.audiocore.async_put(data[:bytes_read])
|
||||
if underruns > known_underruns:
|
||||
print(f"{underruns:x}")
|
||||
known_underruns = underruns
|
||||
# Intentionally do not use _send_command, we don't want to set command_event yet
|
||||
self.commands.append('done')
|
||||
finally:
|
||||
self.audiocore.flush()
|
||||
self.command_event.set()
|
||||
|
||||
def _play(self, mp3path):
|
||||
if self.mp3task is not None:
|
||||
self.mp3task.cancel()
|
||||
self.mp3task = None
|
||||
if mp3path is not None:
|
||||
self.mp3task = asyncio.create_task(self._play_task(mp3path))
|
||||
|
||||
async def task(self):
|
||||
playlist_pos = 0
|
||||
while True:
|
||||
await self.command_event.wait()
|
||||
self.command_event.clear()
|
||||
change_play = False
|
||||
while len(self.commands) > 0:
|
||||
command = self.commands.pop()
|
||||
if command == 'next' or command == 'done':
|
||||
if playlist_pos + 1 < len(self.playlist):
|
||||
playlist_pos += 1
|
||||
change_play = True
|
||||
else:
|
||||
# reaching the end of the playlist stops playback
|
||||
self._play(None)
|
||||
elif command == 'prev':
|
||||
if playlist_pos > 0:
|
||||
playlist_pos -= 1
|
||||
change_play = True
|
||||
elif command == 'stop':
|
||||
self._play(None)
|
||||
elif command == 'play':
|
||||
if self.mp3task is None:
|
||||
change_play = True
|
||||
elif command == 'newplaylist':
|
||||
if len(self.playlist) > 0:
|
||||
playlist_pos = 0
|
||||
change_play = True
|
||||
else:
|
||||
self._play(None)
|
||||
if change_play:
|
||||
self._play(self.playlist[playlist_pos])
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
|
||||
# Copyright (c) 2024-2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
import aiorepl
|
||||
import asyncio
|
||||
@@ -13,6 +13,7 @@ from micropython import const
|
||||
|
||||
# Own modules
|
||||
from audiocore import Audiocore
|
||||
from mp3player import MP3Player
|
||||
from rp2_neopixel import NeoPixel
|
||||
from rp2_sd import SDCard
|
||||
|
||||
@@ -42,30 +43,6 @@ async def rainbow(np, period=10):
|
||||
await asyncio.sleep_ms(20 - (now - before))
|
||||
|
||||
|
||||
async def play_mp3(audiocore, mp3file):
|
||||
known_underruns = 0
|
||||
while True:
|
||||
data = mp3file.read(4096)
|
||||
if len(data) == 0:
|
||||
# End of file
|
||||
break
|
||||
_, _, underruns = await audioctx.async_put(data)
|
||||
if underruns > known_underruns:
|
||||
print(f"{underruns:x}")
|
||||
known_underruns = underruns
|
||||
audioctx.flush()
|
||||
print("Decoding ended")
|
||||
|
||||
|
||||
async def play_mp3s(audiocore, mp3files):
|
||||
audiocore.set_volume(64)
|
||||
for name in mp3files:
|
||||
print(b'Playing ' + name)
|
||||
with open(name, "rb") as testfile:
|
||||
await play_mp3(audiocore, testfile)
|
||||
await asyncio.sleep_ms(1000)
|
||||
|
||||
|
||||
# Set 8 mA drive strength and fast slew rate
|
||||
machine.mem32[0x4001c004 + 6*4] = 0x67
|
||||
machine.mem32[0x4001c004 + 7*4] = 0x67
|
||||
@@ -122,12 +99,15 @@ asyncio.create_task(rainbow(np))
|
||||
# Test audio
|
||||
audioctx = Audiocore(Pin(8), Pin(6))
|
||||
|
||||
player = MP3Player(audioctx)
|
||||
|
||||
# high prio for proc 1
|
||||
machine.mem32[0x40030000 + 0x00] = 0x10
|
||||
|
||||
testfiles = [b'/sd/' + name for name in os.listdir(b'/sd') if name.endswith(b'mp3')]
|
||||
player.set_playlist(testfiles)
|
||||
asyncio.create_task(player.task())
|
||||
|
||||
asyncio.create_task(play_mp3s(audioctx, testfiles))
|
||||
|
||||
asyncio.create_task(aiorepl.task())
|
||||
asyncio.create_task(aiorepl.task({'player': player}))
|
||||
asyncio.get_event_loop().run_forever()
|
||||
|
||||
Reference in New Issue
Block a user