saysynth.lib.midi
Utilities for loading and processing midi files.
1""" 2Utilities for loading and processing midi files. 3""" 4 5import os 6from typing import Any, Dict, Generator, List 7 8import mido 9from midi_utils import midi_to_note 10 11 12def _load(midi_file: str) -> List[mido.Message]: 13 """ 14 Load and validate a midi file, returning a list of messages 15 Args: 16 midi_file: A path to a midifile. 17 """ 18 if not os.path.exists(midi_file): 19 raise ValueError(f"{midi_file} does not exist") 20 21 mid = mido.MidiFile(midi_file) 22 23 # TODO: handle multi-track midi files 24 if len(mid.tracks) > 1: 25 raise NotImplementedError( 26 "There is not currently support for multi-track midifiles." 27 ) 28 29 # filter valid messages 30 messages = [msg for msg in mid if msg.type in ["note_on", "note_off"]] 31 32 # validate first message 33 if messages[0].type != "note_on": 34 raise ValueError( 35 "This midi file does not start with a note_on message. " 36 "Reformat and try again" 37 ) 38 39 # validate final message 40 if messages[-1].type != "note_off": 41 raise ValueError( 42 "This midi file does not end with a note_off message. " 43 "Reformat and try again" 44 ) 45 return messages 46 47 48def process(midi_file: str) -> Generator[Dict[str, Any], None, None]: 49 """ 50 Load a midi file and yield say-friendly parameters. 51 52 Args: 53 midi_file: A path to a midifile. 54 """ 55 messages = _load(midi_file) 56 total_time = 0 57 for i, curr_msg in enumerate(messages): 58 total_time += curr_msg.time 59 prev_msg = messages[i - 1] 60 61 # ignore first note_on message and only yield when 62 # we get a note off message 63 if i > 0 and curr_msg.type == "note_off": 64 if prev_msg.type != "note_on": 65 raise ValueError( 66 f"Overlapping note {midi_to_note(prev_msg.note)} found at message #{i} @ {total_time:.2f}s. " 67 "Only fully monophonic tracks are supported. " 68 "Reformat and try again." 69 ) 70 # if previous note on message has a delta time, yield silence 71 if prev_msg.time > 0: 72 yield { 73 "type": "silence", 74 "note": None, 75 "duration": prev_msg.time * 1000.0, 76 "velocity": 0, 77 } 78 # yield the note with computed duration 79 yield { 80 "type": "note", 81 "note": prev_msg.note, 82 "duration": curr_msg.time * 1000.0, 83 "velocity": prev_msg.velocity, 84 }
def
process(midi_file: str) -> Generator[Dict[str, Any], NoneType, NoneType]:
49def process(midi_file: str) -> Generator[Dict[str, Any], None, None]: 50 """ 51 Load a midi file and yield say-friendly parameters. 52 53 Args: 54 midi_file: A path to a midifile. 55 """ 56 messages = _load(midi_file) 57 total_time = 0 58 for i, curr_msg in enumerate(messages): 59 total_time += curr_msg.time 60 prev_msg = messages[i - 1] 61 62 # ignore first note_on message and only yield when 63 # we get a note off message 64 if i > 0 and curr_msg.type == "note_off": 65 if prev_msg.type != "note_on": 66 raise ValueError( 67 f"Overlapping note {midi_to_note(prev_msg.note)} found at message #{i} @ {total_time:.2f}s. " 68 "Only fully monophonic tracks are supported. " 69 "Reformat and try again." 70 ) 71 # if previous note on message has a delta time, yield silence 72 if prev_msg.time > 0: 73 yield { 74 "type": "silence", 75 "note": None, 76 "duration": prev_msg.time * 1000.0, 77 "velocity": 0, 78 } 79 # yield the note with computed duration 80 yield { 81 "type": "note", 82 "note": prev_msg.note, 83 "duration": curr_msg.time * 1000.0, 84 "velocity": prev_msg.velocity, 85 }
Load a midi file and yield say-friendly parameters.
Arguments:
- midi_file: A path to a midifile.