Spaces:
Running
Running
File size: 2,412 Bytes
055747e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | """
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)
|