virtual_keyboard / midi.py
FJFehr's picture
Full refactoring and design of the repo
055747e
"""
MIDI Utilities
Functions for working with MIDI events and files.
"""
import io
import mido
from mido import MidiFile, MidiTrack, Message, MetaMessage
from config import MIDI_DEFAULTS
def events_to_midbytes(events, ticks_per_beat=None, tempo_bpm=None):
"""
Convert browser MIDI events to a .mid file format.
Args:
events: List of dicts with {type, note, velocity, time, channel}
ticks_per_beat: MIDI ticks per beat resolution (default: from config)
tempo_bpm: Tempo in beats per minute (default: from config)
Returns:
bytes: Complete MIDI file as bytes
"""
if ticks_per_beat is None:
ticks_per_beat = MIDI_DEFAULTS["ticks_per_beat"]
if tempo_bpm is None:
tempo_bpm = MIDI_DEFAULTS["tempo_bpm"]
mid = MidiFile(ticks_per_beat=ticks_per_beat)
track = MidiTrack()
mid.tracks.append(track)
# Set tempo meta message
tempo = mido.bpm2tempo(tempo_bpm)
track.append(MetaMessage("set_tempo", tempo=tempo, time=0))
# Sort events by time and convert to MIDI messages
evs = sorted(events, key=lambda e: e.get("time", 0.0))
last_time = 0.0
for ev in evs:
# Skip malformed events
if "time" not in ev or "type" not in ev or "note" not in ev:
continue
# Calculate delta time in ticks
dt_sec = max(0.0, ev["time"] - last_time)
last_time = ev["time"]
ticks = int(round(dt_sec * (ticks_per_beat * tempo_bpm) / 60.0))
# Create MIDI message
ev_type = ev["type"]
note = int(ev["note"])
vel = int(ev.get("velocity", 0))
channel = int(ev.get("channel", 0))
if ev_type == "note_on":
msg = Message(
"note_on", note=note, velocity=vel, time=ticks, channel=channel
)
else:
msg = Message(
"note_off", note=note, velocity=vel, time=ticks, channel=channel
)
track.append(msg)
# Write to bytes buffer
buf = io.BytesIO()
mid.save(file=buf)
buf.seek(0)
return buf.read()
def validate_event(event: dict) -> bool:
"""
Validate that an event has required fields.
Args:
event: MIDI event dictionary
Returns:
bool: True if valid, False otherwise
"""
required = {"type", "note", "time", "velocity"}
return all(field in event for field in required)