Files
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

224 lines
6.8 KiB
Python

"""Tests for MIDI types and message handling."""
import pytest
from src.midi.types import (
MIDIMessage,
MIDIMessageType,
MIDIMapping,
MixerParameter,
ParameterCategory,
ParameterType,
MIDISystemMessage,
)
class TestMIDIMessageType:
def test_from_status_cc(self):
assert MIDIMessageType.from_status(0xB0) == MIDIMessageType.CONTROL_CHANGE
assert MIDIMessageType.from_status(0xB3) == MIDIMessageType.CONTROL_CHANGE
def test_from_status_note_on(self):
assert MIDIMessageType.from_status(0x90) == MIDIMessageType.NOTE_ON
assert MIDIMessageType.from_status(0x9F) == MIDIMessageType.NOTE_ON
def test_from_status_note_off(self):
assert MIDIMessageType.from_status(0x80) == MIDIMessageType.NOTE_OFF
def test_channel(self):
assert MIDIMessageType.channel(0xB0) == 0
assert MIDIMessageType.channel(0xB3) == 3
assert MIDIMessageType.channel(0xBF) == 15
class TestMIDIMessage:
def test_cc_message(self):
msg = MIDIMessage(
msg_type=MIDIMessageType.CONTROL_CHANGE,
channel=0,
controller=7,
value=100,
)
assert msg.is_cc
assert not msg.is_nrpn
assert msg.controller == 7
assert msg.value == 100
assert msg.value_normalised == pytest.approx(100 / 127, abs=0.01)
def test_note_on_message(self):
msg = MIDIMessage(
msg_type=MIDIMessageType.NOTE_ON,
channel=1,
controller=60,
value=127,
)
assert not msg.is_cc
assert msg.channel == 1
def test_pitch_bend_message(self):
msg = MIDIMessage(
msg_type=MIDIMessageType.PITCH_BEND,
channel=0,
value=8192, # Centre
)
assert msg.value_normalised == pytest.approx(0.0, abs=0.01)
msg2 = MIDIMessage(
msg_type=MIDIMessageType.PITCH_BEND,
channel=0,
value=16383,
)
assert msg2.value_normalised == pytest.approx(1.0, abs=0.02)
def test_nrpn_message(self):
msg = MIDIMessage(
msg_type=MIDIMessageType.CONTROL_CHANGE,
channel=0,
is_nrpn=True,
nrpn_msb=0,
nrpn_lsb=1,
value=1000,
)
assert msg.is_nrpn
assert msg.nrpn_number == 1 # (0 << 7) | 1
def test_nrpn_number_14bit(self):
msg = MIDIMessage(
msg_type=MIDIMessageType.CONTROL_CHANGE,
channel=0,
is_nrpn=True,
nrpn_msb=0x01,
nrpn_lsb=0x02,
)
assert msg.nrpn_number == (0x01 << 7) | 0x02
def test_repr(self):
msg = MIDIMessage(
msg_type=MIDIMessageType.CONTROL_CHANGE,
channel=0,
controller=7,
value=100,
source_device="TestDevice",
)
r = repr(msg)
assert "CONTROL_CHANGE" in r
assert "CC=7" in r
assert "val=100" in r
assert "TestDevice" in r
class TestMIDIMapping:
def test_cc_mapping_match(self):
m = MIDIMapping(
msg_type=MIDIMessageType.CONTROL_CHANGE,
channel=0,
controller=7,
param_type=ParameterType.VOLUME,
param_channel=0,
)
msg = MIDIMessage(
msg_type=MIDIMessageType.CONTROL_CHANGE,
channel=0,
controller=7,
value=100,
)
assert m.matches(msg)
def test_cc_mapping_no_match_wrong_channel(self):
m = MIDIMapping(
msg_type=MIDIMessageType.CONTROL_CHANGE,
channel=0,
controller=7,
)
msg = MIDIMessage(
msg_type=MIDIMessageType.CONTROL_CHANGE,
channel=1,
controller=7,
value=100,
)
assert not m.matches(msg)
def test_cc_mapping_no_match_wrong_controller(self):
m = MIDIMapping(
msg_type=MIDIMessageType.CONTROL_CHANGE,
channel=0,
controller=7,
)
msg = MIDIMessage(
msg_type=MIDIMessageType.CONTROL_CHANGE,
channel=0,
controller=10,
value=100,
)
assert not m.matches(msg)
def test_device_filter(self):
m = MIDIMapping(
msg_type=MIDIMessageType.CONTROL_CHANGE,
channel=0,
controller=7,
source_device="X-Touch",
)
msg_match = MIDIMessage(
msg_type=MIDIMessageType.CONTROL_CHANGE,
channel=0,
controller=7,
value=100,
source_device="X-Touch",
)
msg_no_match = MIDIMessage(
msg_type=MIDIMessageType.CONTROL_CHANGE,
channel=0,
controller=7,
value=100,
source_device="MIDImix",
)
assert m.matches(msg_match)
assert not m.matches(msg_no_match)
def test_nrpn_mapping_match(self):
m = MIDIMapping(
msg_type=MIDIMessageType.CONTROL_CHANGE,
channel=0,
is_nrpn=True,
nrpn_number=258, # (2 << 7) | 2
)
msg = MIDIMessage(
msg_type=MIDIMessageType.CONTROL_CHANGE,
channel=0,
is_nrpn=True,
nrpn_msb=2,
nrpn_lsb=2,
value=8192,
)
assert m.matches(msg)
def test_scale_value_linear(self):
m = MIDIMapping(midi_min=0, midi_max=127, param_min=0.0, param_max=1.0)
assert m.scale_value(0) == pytest.approx(0.0)
assert m.scale_value(127) == pytest.approx(1.0)
assert m.scale_value(63) == pytest.approx(0.5, abs=0.02)
def test_scale_value_invert(self):
m = MIDIMapping(midi_min=0, midi_max=127, param_min=0.0, param_max=1.0, invert=True)
assert m.scale_value(0) == pytest.approx(1.0)
assert m.scale_value(127) == pytest.approx(0.0)
def test_scale_value_db_range(self):
m = MIDIMapping(midi_min=0, midi_max=127, param_min=-60.0, param_max=12.0)
assert m.scale_value(0) == pytest.approx(-60.0)
assert m.scale_value(127) == pytest.approx(12.0)
def test_scale_value_exponential(self):
m = MIDIMapping(midi_min=0, midi_max=127, param_min=0.0, param_max=1.0, curve="exponential")
assert m.scale_value(0) == pytest.approx(0.0)
assert m.scale_value(127) == pytest.approx(1.0)
# Should be non-linear (value at 50% is < 50%)
assert m.scale_value(63) < 0.5
def test_disabled_mapping(self):
m = MIDIMapping(controller=7, enabled=False)
msg = MIDIMessage(MIDIMessageType.CONTROL_CHANGE, 0, 7, 100)
# The engine skips disabled mappings; matches() still works but caller checks enabled
assert m.matches(msg)
assert not m.enabled