Figures: several albums per figure, picked on the LED ring
A figure folder that holds album folders instead of audio can play any of them, up to six, in folder order (depth-first, alphabetical). Each is its own album, titled "Fuchs - <folder>" and of the figure's kind. Putting the figure down plays the album it played last, or the first, straight away; another figure placed in between forgets the position in that album, not the album itself. Meanwhile the ring offers all of them: one section per album from a fixed palette, edges shifting towards the neighbours' hue, the selected one bright and wobbling. The knob moves the selection one section per detent and wraps; pressing plays the selected album, and left alone for 8 s the picker fades into the figure's colours and the music carries on. While open, the device hands the knob to the picker (`capture_knob`): turns do not change the volume and the press is not published to Home Assistant. A tick per step and a confirm "pling" go with it. The firmware only draws: one new ring effect, LED_WHEEL_EFFECT_SEGMENT_SELECT (23), re-sent on every detent with the section to glide from. The look is tunable through a temporary `general.figure_selection` config section until the right values become constants. Every UI sound - the screen's blubb, the parents' timer, the picker - now follows the music volume with a floor, times a new parent-mode "Effekt-Lautstärke" (`general.effects_volume`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,14 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from musicmouse.config import DEFAULT_AUDIO_EXTENSIONS, load_config
|
||||
from musicmouse.library import Album, Analyzer, MusicLibrary, default_worker_count
|
||||
from musicmouse.effects import SEGMENT_SELECT_MAX_SEGMENTS
|
||||
from musicmouse.library import (
|
||||
MAX_FIGURE_ALBUMS,
|
||||
Album,
|
||||
Analyzer,
|
||||
MusicLibrary,
|
||||
default_worker_count,
|
||||
)
|
||||
from musicmouse.library.analysis import ANALYZER_VERSION, BeatGrid, TrackAnalysis, TrackCurves
|
||||
from musicmouse.library.cache import LibraryCache
|
||||
from musicmouse.library.colors import colors_from_id
|
||||
@@ -92,15 +99,72 @@ async def test_figure_playlists_are_keyed_by_figure_name(config_dir: Path) -> No
|
||||
playlists = library.figure_playlists()
|
||||
|
||||
assert set(playlists) == {"fuchs", "eule"}
|
||||
assert len(playlists["fuchs"]) == 3
|
||||
assert [track.path.name for track in playlists["fuchs"].tracks] == [
|
||||
(fuchs,) = playlists["fuchs"]
|
||||
assert len(fuchs) == 3
|
||||
assert [track.path.name for track in fuchs.tracks] == [
|
||||
"00 - track.mp3",
|
||||
"01 - track.mp3",
|
||||
"02 - track.mp3",
|
||||
]
|
||||
# The playlist carries the album it came from, which is how a front-end answers
|
||||
# "what is playing?" without keeping its own copy of that state.
|
||||
assert playlists["fuchs"].album_id == album_named(library, "Fuchs").id
|
||||
assert fuchs.album_id == album_named(library, "Fuchs").id
|
||||
|
||||
|
||||
def _figure_with_albums(config_dir: Path, figure: str, *folders: str) -> None:
|
||||
"""Replace a figure's audio with one album per folder (relative to the figure)."""
|
||||
figure_folder = config_dir / "music" / "Figuren" / figure
|
||||
for track in figure_folder.glob("*.mp3"):
|
||||
track.unlink()
|
||||
for folder in folders:
|
||||
write_track(figure_folder / folder / "01 - track.mp3", title=f"In {folder}")
|
||||
|
||||
|
||||
async def test_a_figure_with_album_subfolders_offers_each_in_folder_order(
|
||||
config_dir: Path,
|
||||
) -> None:
|
||||
# Depth-first and alphabetical, however deep an album sits.
|
||||
_figure_with_albums(
|
||||
config_dir, "fuchs", "B Lieder", "A Geschichten/Teil 2", "A Geschichten/Teil 1"
|
||||
)
|
||||
library = await build(config_dir)
|
||||
|
||||
choices = library.figure_playlists()["fuchs"]
|
||||
|
||||
albums = [library.get(playlist.album_id) for playlist in choices]
|
||||
assert [album.title if album else None for album in albums] == [
|
||||
"Fuchs - Teil 1",
|
||||
"Fuchs - Teil 2",
|
||||
"Fuchs - B Lieder",
|
||||
]
|
||||
assert all(album is not None and album.figure == "fuchs" for album in albums)
|
||||
|
||||
|
||||
async def test_a_figure_album_subfolder_takes_the_figures_kind(config_dir: Path) -> None:
|
||||
_figure_with_albums(config_dir, "eule", "Folge 1", "Folge 2")
|
||||
library = await build(config_dir)
|
||||
|
||||
albums = [library.get(p.album_id) for p in library.figure_playlists()["eule"]]
|
||||
assert len(albums) == 2
|
||||
assert {album.kind if album else None for album in albums} == {"book"}
|
||||
|
||||
|
||||
async def test_a_figure_offers_no_more_albums_than_the_ring_has_room_for(
|
||||
config_dir: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
_figure_with_albums(config_dir, "fuchs", *(f"Album {n}" for n in range(MAX_FIGURE_ALBUMS + 2)))
|
||||
library = await build(config_dir)
|
||||
|
||||
choices = library.figure_playlists()["fuchs"]
|
||||
|
||||
assert len(choices) == MAX_FIGURE_ALBUMS
|
||||
assert "only the first 6" in caplog.text
|
||||
# Browsing still shows every one of them.
|
||||
assert sum(album.figure == "fuchs" for album in library.albums) == MAX_FIGURE_ALBUMS + 2
|
||||
|
||||
|
||||
def test_the_album_cap_is_the_rings_section_count() -> None:
|
||||
assert MAX_FIGURE_ALBUMS == SEGMENT_SELECT_MAX_SEGMENTS
|
||||
|
||||
|
||||
async def test_music_takes_its_title_and_artist_from_the_tags(config_dir: Path) -> None:
|
||||
|
||||
Reference in New Issue
Block a user