Spaces:
Running
Running
| """ | |
| 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) | |