Skip to content

Commit

Permalink
Fix channel number issue in MIDI output (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
salu133445 committed Jan 1, 2021
1 parent ded4b52 commit 3310035
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 30 deletions.
1 change: 1 addition & 0 deletions muspy/inputs/midi.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class MIDIError(Exception):


def _is_drum(channel):
# Mido numbers channels 0 to 15 instead of 1 to 16
return channel == 9


Expand Down
105 changes: 75 additions & 30 deletions muspy/outputs/midi.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def to_mido_lyric(lyric: Lyric) -> MetaMessage:


def to_mido_note_on_note_off(
note: Note, channel: int, use_note_on_as_note_off: bool = True
note: Note, channel: int, use_note_off_message: bool = False
) -> Tuple[Message, Message]:
"""Return a Note object as mido Message objects.
Expand All @@ -180,9 +180,12 @@ def to_mido_note_on_note_off(
Note object to convert.
channel : int
Channel of the MIDI message.
use_note_on_as_note_off : bool
Whether to use a note on message with zero velocity instead of a
note off message. Defaults to True.
use_note_off_message : bool, optional
Whether to use note-off messages. If False, note-on messages
with zero velocity are used instead. The advantage to using
note-on messages at zero velocity is that it can avoid sending
additional status bytes when Running Status is employed.
Defaults to False.
Returns
-------
Expand All @@ -200,45 +203,56 @@ def to_mido_note_on_note_off(
velocity=velocity,
channel=channel,
)
if use_note_on_as_note_off:
if use_note_off_message:
note_off_msg = Message(
"note_on",
"note_off",
time=note.end,
note=note.pitch,
velocity=0,
velocity=64,
channel=channel,
)
else:
note_off_msg = Message(
"note_off",
"note_on",
time=note.end,
note=note.pitch,
velocity=velocity,
velocity=0,
channel=channel,
)

return note_on_msg, note_off_msg


def to_mido_track(
track: Track, use_note_on_as_note_off: bool = True
track: Track,
channel: Optional[int] = None,
use_note_off_message: bool = False,
) -> MidiTrack:
"""Return a Track object as a mido MidiTrack object.
Parameters
----------
track : :class:`muspy.Track` object
Track object to convert.
use_note_on_as_note_off : bool
Whether to use a note on message with zero velocity instead of a
note off message.
use_note_off_message : bool, optional
Whether to use note-off messages. If False, note-on messages
with zero velocity are used instead. The advantage to using
note-on messages at zero velocity is that it can avoid sending
additional status bytes when Running Status is employed.
Defaults to False.
channel : int, optional
Channel number. Defaults to 10 for drums and 0 for other
instruments.
Returns
-------
:class:`mido.MidiTrack` object
Converted mido MidiTrack object.
"""
if channel is None:
channel = 9 if track.is_drum else 0

# Create a new MIDI track
midi_track = MidiTrack()

Expand All @@ -247,15 +261,18 @@ def to_mido_track(
midi_track.append(MetaMessage("track_name", name=track.name))

# Program change messages
channel = 9 if track.is_drum else 0
midi_track.append(
Message("program_change", program=track.program, channel=channel,)
Message("program_change", program=track.program, channel=channel)
)

# Note on and note off messages
for note in track.notes:
midi_track.extend(
to_mido_note_on_note_off(note, channel, use_note_on_as_note_off)
to_mido_note_on_note_off(
note,
channel=channel,
use_note_off_message=use_note_off_message,
)
)

# End of track message
Expand All @@ -267,16 +284,19 @@ def to_mido_track(
return midi_track


def to_mido(music: "Music", use_note_on_as_note_off: bool = True):
def to_mido(music: "Music", use_note_off_message: bool = False):
"""Return a Music object as a MidiFile object.
Parameters
----------
music : :class:`muspy.Music` object
Music object to convert.
use_note_on_as_note_off : bool
Whether to use a note on message with zero velocity instead of a
note off message.
use_note_off_message : bool, optional
Whether to use note-off messages. If False, note-on messages
with zero velocity are used instead. The advantage to using
note-on messages at zero velocity is that it can avoid sending
additional status bytes when Running Status is employed.
Defaults to False.
Returns
-------
Expand All @@ -291,16 +311,38 @@ def to_mido(music: "Music", use_note_on_as_note_off: bool = True):
midi.tracks.append(to_mido_meta_track(music))

# Iterate over music tracks
for track in music.tracks:
midi.tracks.append(to_mido_track(track, use_note_on_as_note_off))
for i, track in enumerate(music.tracks):
# NOTE: Many softwares use the same instrument for messages of
# the same channel in different tracks. Thus, we want to assign
# a unique channel number for each track. MIDI has 15 channels
# for instruments other than drums, so we increment the channel
# number for each track (skipping the drum channel) and go back
# to 0 once we run out of channels.

# Assign channel number
if track.is_drum:
# Mido numbers channels 0 to 15 instead of 1 to 16
channel = 9
else:
# MIDI has 15 channels for instruments other than drums
channel = i % 15
# Avoid drum channel
if channel > 8:
channel += 1

midi.tracks.append(
to_mido_track(
track,
channel=channel,
use_note_off_message=use_note_off_message,
)
)

return midi


def write_midi_mido(
path: Union[str, Path],
music: "Music",
use_note_on_as_note_off: bool = True,
path: Union[str, Path], music: "Music", use_note_off_message: bool = False,
):
"""Write a Music object to a MIDI file using mido as backend.
Expand All @@ -310,12 +352,15 @@ def write_midi_mido(
Path to write the MIDI file.
music : :class:`muspy.Music` object
Music object to write.
use_note_on_as_note_off : bool
Whether to use a note on message with zero velocity instead of a
note off message.
use_note_off_message : bool, optional
Whether to use note-off messages. If False, note-on messages
with zero velocity are used instead. The advantage to using
note-on messages at zero velocity is that it can avoid sending
additional status bytes when Running Status is employed.
Defaults to False.
"""
midi = to_mido(music, use_note_on_as_note_off=use_note_on_as_note_off)
midi = to_mido(music, use_note_off_message=use_note_off_message)
midi.save(str(path))


Expand Down Expand Up @@ -492,7 +537,7 @@ def write_midi(
Path to write the MIDI file.
music : :class:`muspy.Music`
Music object to write.
backend: {'mido', 'pretty_midi'}
backend: {'mido', 'pretty_midi'}, optional
Backend to use. Defaults to 'mido'.
See Also
Expand Down

0 comments on commit 3310035

Please sign in to comment.