continuator_front / backend /app /midi_export.py
pachet's picture
Add session MIDI ZIP export
eacc693
Raw
History Blame Contribute Delete
2.04 kB
from __future__ import annotations
from io import BytesIO
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile
import mido
from .schemas import PhrasePayload
MIDI_EXPORT_TEMPO = 500_000
MIDI_EXPORT_TICKS_PER_BEAT = 480
def phrase_payload_to_midi_bytes(payload: PhrasePayload) -> bytes:
midi_file = mido.MidiFile(type=0, ticks_per_beat=MIDI_EXPORT_TICKS_PER_BEAT)
track = mido.MidiTrack()
midi_file.tracks.append(track)
track.append(mido.MetaMessage("set_tempo", tempo=MIDI_EXPORT_TEMPO, time=0))
current_seconds = 0.0
previous_tick = 0
for event in payload.events:
current_seconds += float(event.delta_seconds)
absolute_tick = max(
previous_tick,
int(
round(
mido.second2tick(
current_seconds,
MIDI_EXPORT_TICKS_PER_BEAT,
MIDI_EXPORT_TEMPO,
)
)
),
)
delta_ticks = absolute_tick - previous_tick
previous_tick = absolute_tick
velocity = int(event.velocity) if event.type == "note_on" else 0
track.append(
mido.Message(
event.type,
note=int(event.note),
velocity=velocity,
channel=int(event.channel),
time=delta_ticks,
)
)
track.append(mido.MetaMessage("end_of_track", time=0))
buffer = BytesIO()
midi_file.save(file=buffer)
return buffer.getvalue()
def write_phrase_payload_midi(payload: PhrasePayload, path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(phrase_payload_to_midi_bytes(payload))
def midi_files_to_zip_bytes(files: list[tuple[str, bytes]]) -> bytes:
buffer = BytesIO()
with ZipFile(buffer, mode="w", compression=ZIP_DEFLATED) as archive:
for file_name, content in files:
archive.writestr(file_name, content)
return buffer.getvalue()