Files
raspberry-pi-mixer/tests/test_midi_learn.py
T
shawn 9cd8292acc
Lint & Validate / lint (push) Has been cancelled
Phase 1-4: Audio stack, mixer engine, MIDI, and network API
P2-R1: ALSA + JACK2 low-latency config (scripts, quirks, tuning)
P2-R2: Carla integration (build scripts, 8ch rack config, NAM LV2 support)
P2-R3: Plugin manager, categories, blacklist, NAM model support
P3-R1: Mixer DSP engine (channel strip, routing matrix, bus mgr, automation)
P4-R1: MIDI engine (learn mode, clock sync, HID discovery, mapping store)
P4-R2: Network API (OSC server, FastAPI REST, WebSocket, auth, rate limiter)
P5-R1: Touchscreen UI evaluation + main entry point
docs: Audio stack, Carla integration, MIDI support, UI evaluation
tests: Full test suite (292 passing)
2026-05-19 20:39:17 -04:00

182 lines
5.5 KiB
Python

"""Tests for MIDI learn mode."""
import pytest
from src.midi.types import (
MIDIMessage,
MIDIMessageType,
MIDIMapping,
ParameterType,
)
from src.midi.midi_learn import MIDILearn, LearnState
class TestMIDILearn:
@pytest.fixture
def learn(self):
return MIDILearn()
def test_initial_idle(self, learn):
assert learn.is_idle
assert not learn.is_listening
def test_start_learn_sets_listening(self, learn):
learn.start_learn(ParameterType.VOLUME, channel=0)
assert learn.is_listening
assert not learn.is_idle
assert "volume" in learn.session.param_type.value
def test_feed_cc_triggers_capture(self, learn):
learn.start_learn(ParameterType.VOLUME, channel=0)
msg = MIDIMessage(
msg_type=MIDIMessageType.CONTROL_CHANGE,
channel=0,
controller=7,
value=100,
)
result = learn.feed_message(msg)
assert result is None # Still in CAPTURED, waiting for confirm
assert learn.is_captured
assert learn.session.captured_cc == 7
assert learn.session.captured_channel == 0
def test_confirm_returns_mapping(self, learn):
learn.start_learn(ParameterType.VOLUME, channel=0)
msg = MIDIMessage(
msg_type=MIDIMessageType.CONTROL_CHANGE,
channel=0,
controller=7,
value=100,
)
learn.feed_message(msg)
mapping = learn.confirm()
assert mapping is not None
assert mapping.param_type == ParameterType.VOLUME
assert mapping.param_channel == 0
assert mapping.controller == 7
assert mapping.channel == 0
assert learn.is_idle # Reset after confirm
def test_confirm_without_capture_returns_none(self, learn):
assert learn.confirm() is None
def test_discard_resets(self, learn):
learn.start_learn(ParameterType.VOLUME, channel=0)
learn.feed_message(MIDIMessage(
MIDIMessageType.CONTROL_CHANGE, 0, 7, 100,
))
learn.discard()
assert learn.is_idle
def test_note_also_captured(self, learn):
"""Note On/Off messages should also be captured (for transport triggers)."""
learn.start_learn(ParameterType.PLAY)
msg = MIDIMessage(
msg_type=MIDIMessageType.NOTE_ON,
channel=0,
controller=60,
value=127,
)
learn.feed_message(msg)
mapping = learn.confirm()
assert mapping is not None
assert mapping.msg_type == MIDIMessageType.NOTE_ON
def test_nrpn_capture(self, learn):
learn.start_learn(ParameterType.VOLUME, channel=0)
msg = MIDIMessage(
msg_type=MIDIMessageType.CONTROL_CHANGE,
channel=0,
is_nrpn=True,
nrpn_msb=0x01,
nrpn_lsb=0x02,
value=8192,
)
learn.feed_message(msg)
mapping = learn.confirm()
assert mapping is not None
assert mapping.is_nrpn
assert mapping.nrpn_number == (0x01 << 7) | 0x02
def test_learned_callback_fires(self, learn):
fired = []
learn.on_learned(lambda m: fired.append(m))
learn.start_learn(ParameterType.VOLUME, 0)
learn.feed_message(MIDIMessage(
MIDIMessageType.CONTROL_CHANGE, 0, 7, 100,
))
learn.confirm()
assert len(fired) == 1
assert fired[0].controller == 7
class TestBatchLearn:
@pytest.fixture
def learn(self):
return MIDILearn()
def test_batch_learn_full_sequence(self, learn):
params = [
(ParameterType.VOLUME, 0, "CH0 Vol"),
(ParameterType.PAN, 0, "CH0 Pan"),
(ParameterType.MUTE, 0, "CH0 Mute"),
]
learn.start_batch(params)
assert learn.is_batch
assert learn.session.batch_index == 0
# Wiggle control 1 → volume
r1 = learn.feed_message(MIDIMessage(
MIDIMessageType.CONTROL_CHANGE, 0, 70, 100,
))
assert r1 is not None
assert r1.param_type == ParameterType.VOLUME
assert r1.controller == 70
assert learn.session.batch_index == 1 # Advanced
# Wiggle control 2 → pan
r2 = learn.feed_message(MIDIMessage(
MIDIMessageType.CONTROL_CHANGE, 0, 10, 64,
))
assert r2 is not None
assert r2.param_type == ParameterType.PAN
assert r2.controller == 10
# Wiggle control 3 → mute (last one)
r3 = learn.feed_message(MIDIMessage(
MIDIMessageType.CONTROL_CHANGE, 0, 1, 127,
))
assert r3 is not None
assert r3.param_type == ParameterType.MUTE
# Batch should be complete
assert learn.is_idle
assert len(learn.session.batch_mappings) == 3
def test_batch_ignores_extra_messages_after_complete(self, learn):
params = [(ParameterType.VOLUME, 0, "CH0 Vol")]
learn.start_batch(params)
learn.feed_message(MIDIMessage(MIDIMessageType.CONTROL_CHANGE, 0, 70, 100))
# Batch done, extra message should return None
r = learn.feed_message(MIDIMessage(MIDIMessageType.CONTROL_CHANGE, 0, 71, 100))
assert r is None
assert learn.is_idle
def test_status_text(self, learn):
learn.start_learn(ParameterType.VOLUME, 0)
assert "Listening" in learn.status_text
learn.feed_message(MIDIMessage(MIDIMessageType.CONTROL_CHANGE, 0, 7, 100))
assert "Captured" in learn.status_text
assert "CC7" in learn.status_text