Files
musicmouse/python-backend/tests/test_sound.py
Martin Bauer d465a74cde 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>
2026-10-04 00:29:18 +02:00

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)