Move playlist handling from mp3player to app
All checks were successful
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 8s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m28s

This completes the move of the nfc-mp3-demo to the new architecture.
This commit is contained in:
2025-05-20 20:20:08 +02:00
parent 7778147b66
commit ce02daad3a
3 changed files with 71 additions and 97 deletions

View File

@@ -21,9 +21,15 @@ class PlayerApp:
self.player = deps.mp3player(self)
self.nfc = deps.nfcreader(self)
self.buttons = deps.buttons(self) if deps.buttons is not None else None
self.mp3file = None
self.volume_pos = 3
self.player.set_volume(VOLUME_CURVE[self.volume_pos])
def __del__(self):
if self.mp3file is not None:
self.mp3file.close()
self.mp3file = None
def onTagChange(self, new_tag):
if new_tag is not None:
self.timer_manager.cancel(self.onTagRemoveDelay)
@@ -43,7 +49,7 @@ class PlayerApp:
self.player.stop()
return
testfiles.sort()
self.player.set_playlist(testfiles)
self._set_playlist(testfiles)
else:
self.timer_manager.schedule(time.ticks_ms() + 5000, self.onTagRemoveDelay)
@@ -61,4 +67,27 @@ class PlayerApp:
self.volume_pos = max(self.volume_pos - 1, 0)
self.player.set_volume(VOLUME_CURVE[self.volume_pos])
elif what == self.buttons.NEXT:
self.player.play_next()
self._play_next()
def onPlaybackDone(self):
self.mp3file.close()
self.mp3file = None
self._play_next()
def _set_playlist(self, files: list[bytes]):
self.playlist_pos = 0
self.playlist = files
self._play(self.playlist[self.playlist_pos])
def _play_next(self):
if self.playlist_pos + 1 < len(self.playlist):
self.playlist_pos += 1
self._play(self.playlist[self.playlist_pos])
def _play(self, filename: bytes):
if self.mp3file is not None:
self.player.stop()
self.mp3file.close()
self.mp3file = None
self.mp3file = open(filename, 'rb')
self.player.play(self.mp3file)

View File

@@ -88,15 +88,12 @@ def run():
# Setup MP3 player
with SDContext(mosi=Pin(3), miso=Pin(4), sck=Pin(2), ss=Pin(5), baudrate=15000000), \
AudioContext(Pin(8), Pin(6)) as audioctx:
player = MP3Player(audioctx)
player.set_volume(32)
asyncio.create_task(player.task())
# Setup NFC
reader = MFRC522(spi_id=1, sck=10, miso=12, mosi=11, cs=13, rst=9, tocard_retries=20)
# Setup app
deps = app.Dependencies(mp3player=lambda _: player,
deps = app.Dependencies(mp3player=lambda the_app: MP3Player(audioctx, the_app),
nfcreader=lambda the_app: Nfc(reader, the_app),
buttons=lambda the_app: Buttons(the_app))
the_app = app.PlayerApp(deps)

View File

@@ -3,51 +3,41 @@
import asyncio
from array import array
from utils import TimerManager
try:
from typing import TYPE_CHECKING # type: ignore
except ImportError:
TYPE_CHECKING = False
if TYPE_CHECKING:
import typing
class PlayerCallback(typing.Protocol):
def onPlaybackDone(self) -> None: ...
class MP3Player:
def __init__(self, audiocore):
def __init__(self, audiocore, cb: PlayerCallback):
self.audiocore = audiocore
self.commands = []
self.command_event = asyncio.Event()
self.playlist = []
self.mp3task = None
self.volume = 128
self.cb = cb
def set_playlist(self, mp3files):
def play(self, stream):
"""
Set a new playlist and start playing from the first entry.
For convenience a single file name can also be passed.
Play from byte stream.
"""
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')
if self.mp3task is not None:
self.mp3task.cancel()
self.mp3task = None
self.mp3task = asyncio.create_task(self._play_task(stream))
def stop(self):
"""
Stop playback, remembering the current position in the playlist (but not inside a track).
Stop playback
"""
self._send_command('stop')
def play(self):
"""
Start playback.
"""
self._send_command('play')
if self.mp3task is not None:
self.mp3task.cancel()
self.mp3task = None
def set_volume(self, volume: int):
"""
@@ -59,67 +49,25 @@ class MP3Player:
def get_volume(self) -> int:
return self.volume
def _send_command(self, command: str):
self.commands.append(command)
self.command_event.set()
async def _play_task(self, mp3path):
async def _play_task(self, stream):
known_underruns = 0
send_done = False
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')
while True:
bytes_read = stream.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
# Call onPlaybackDone after flush
send_done = True
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])
if send_done:
# Only call onPlaybackDone if exit due to end of stream
# Use timer with time 0 to call callback "immediately" but from a different task
TimerManager().schedule(0, self.cb.onPlaybackDone)