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>
48 lines
1.7 KiB
Python
48 lines
1.7 KiB
Python
"""How loud UI sounds are, and the album picker's two sounds."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import wave
|
|
from collections.abc import Callable
|
|
|
|
import pytest
|
|
|
|
from musicmouse.blubb import RATE, confirm_wav, tick_wav
|
|
from musicmouse.sound import MIN_PLAYER_VOLUME, ui_level
|
|
|
|
|
|
def test_ui_sounds_follow_the_music_volume() -> None:
|
|
assert ui_level(0.8, 1.0) == pytest.approx(0.8)
|
|
assert ui_level(1.0, 0.5) == pytest.approx(0.5)
|
|
|
|
|
|
def test_ui_sounds_never_drop_below_the_minimum_player_volume() -> None:
|
|
# A muted player must not mute the timer, nor make the album picker silent.
|
|
assert ui_level(0.0, 1.0) == pytest.approx(MIN_PLAYER_VOLUME)
|
|
assert ui_level(0.0, 0.5) == pytest.approx(MIN_PLAYER_VOLUME * 0.5)
|
|
|
|
|
|
def test_parents_can_still_turn_ui_sounds_off() -> None:
|
|
assert ui_level(0.8, 0.0) == 0.0
|
|
|
|
|
|
@pytest.mark.parametrize(("make", "longest"), [(tick_wav, 0.05), (confirm_wav, 0.4)])
|
|
def test_the_picker_sounds_are_short_mono_wavs(make: Callable[..., bytes], longest: float) -> None:
|
|
with wave.open(io.BytesIO(make(gain=0.5))) as wav:
|
|
assert (wav.getnchannels(), wav.getsampwidth(), wav.getframerate()) == (1, 2, RATE)
|
|
assert 0 < wav.getnframes() / RATE < longest
|
|
|
|
|
|
def test_the_gain_sets_the_peak() -> None:
|
|
def peak(data: bytes) -> int:
|
|
with wave.open(io.BytesIO(data)) as wav:
|
|
frames = wav.readframes(wav.getnframes())
|
|
return max(
|
|
abs(int.from_bytes(frames[i : i + 2], "little", signed=True))
|
|
for i in range(0, len(frames), 2)
|
|
)
|
|
|
|
assert peak(tick_wav(gain=1.0)) == pytest.approx(32767, abs=1)
|
|
assert peak(tick_wav(gain=0.25)) == pytest.approx(32767 / 4, abs=1)
|