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.