virtual_keyboard / tests /test_midi.py
github-actions[bot]
Deploy to HF Spaces
2e0c2a7
"""Tests for midi.py — MIDI event utilities."""
import io
import mido
import pytest
from midi import events_to_midbytes, validate_event
# ---------------------------------------------------------------------------
# validate_event
# ---------------------------------------------------------------------------
class TestValidateEvent:
def test_valid_event(self):
assert validate_event(
{"type": "note_on", "note": 60, "velocity": 100, "time": 0.0}
)
def test_missing_type(self):
assert not validate_event({"note": 60, "velocity": 100, "time": 0.0})
def test_missing_note(self):
assert not validate_event({"type": "note_on", "velocity": 100, "time": 0.0})
def test_missing_velocity(self):
assert not validate_event({"type": "note_on", "note": 60, "time": 0.0})
def test_missing_time(self):
assert not validate_event({"type": "note_on", "note": 60, "velocity": 100})
def test_empty_dict(self):
assert not validate_event({})
# ---------------------------------------------------------------------------
# events_to_midbytes
# ---------------------------------------------------------------------------
class TestEventsToMidbytes:
def test_returns_valid_midi_bytes(self, single_note_events):
mid_bytes = events_to_midbytes(single_note_events)
assert isinstance(mid_bytes, bytes)
assert len(mid_bytes) > 0
# Should start with MThd header.
assert mid_bytes[:4] == b"MThd"
def test_round_trip_note_count(self, c_major_chord_events):
"""The exported MIDI file should contain the same number of note messages."""
mid_bytes = events_to_midbytes(c_major_chord_events)
mid = mido.MidiFile(file=io.BytesIO(mid_bytes))
messages = [msg for msg in mid.tracks[0] if msg.type in ("note_on", "note_off")]
assert len(messages) == len(c_major_chord_events)
def test_preserves_note_values(self, melody_events):
mid_bytes = events_to_midbytes(melody_events)
mid = mido.MidiFile(file=io.BytesIO(mid_bytes))
note_ons = [msg.note for msg in mid.tracks[0] if msg.type == "note_on"]
expected = [e["note"] for e in melody_events if e["type"] == "note_on"]
assert note_ons == expected
def test_empty_events(self):
mid_bytes = events_to_midbytes([])
assert isinstance(mid_bytes, bytes)
# Should still be a valid MIDI file (header + empty track).
assert mid_bytes[:4] == b"MThd"
def test_custom_tempo(self, single_note_events):
mid_bytes = events_to_midbytes(single_note_events, tempo_bpm=60)
mid = mido.MidiFile(file=io.BytesIO(mid_bytes))
tempo_msgs = [msg for msg in mid.tracks[0] if msg.type == "set_tempo"]
assert len(tempo_msgs) == 1
assert mido.tempo2bpm(tempo_msgs[0].tempo) == pytest.approx(60, abs=0.01)
def test_skips_malformed_events(self):
events = [
{"type": "note_on", "note": 60, "velocity": 100, "time": 0.0},
{"bad": "event"}, # no type/note/time
{"type": "note_off", "note": 60, "velocity": 0, "time": 0.5},
]
mid_bytes = events_to_midbytes(events)
mid = mido.MidiFile(file=io.BytesIO(mid_bytes))
messages = [msg for msg in mid.tracks[0] if msg.type in ("note_on", "note_off")]
assert len(messages) == 2