9cd8292acc
Lint & Validate / lint (push) Has been cancelled
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)
182 lines
5.5 KiB
Python
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
|