k-l-lambda's picture
added node-addon-lilypond
f65fe85
# This file is part of LilyPond, the GNU music typesetter.
#
# Copyright (C) 2001--2020 Han-Wen Nienhuys <[email protected]>
# Jan Nieuwenhuizen <[email protected]>
#
#
# LilyPond is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# LilyPond is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with LilyPond. If not, see <http://www.gnu.org/licenses/>.
# import midi
# s = open ("s.midi").read ()
# midi.parse_track (s)
# midi.parse (s)
#
#
# returns a MIDI file as the tuple
#
# ((format, division), TRACKLIST) # division (>0) = TPQN*4
# # or (<0) TBD
#
# each track is an EVENTLIST, where EVENT is
#
# (time, (type, ARG1, [ARG2])) # time = cumulative delta time
# MIDI event:
# type = MIDI status+channel >= x80
# META-event = xFF:
# type = meta-event type <= x7F
# ARG1 = length
# ARG2 = data
import array
import struct
class error (Exception):
pass
# class warning (Exception): pass
def _add_constants():
channelVoiceMessages = (
(0x80, "NOTE_OFF"),
(0x90, "NOTE_ON"),
(0xA0, "POLYPHONIC_KEY_PRESSURE"),
(0xB0, "CONTROLLER_CHANGE"),
(0xC0, "PROGRAM_CHANGE"),
(0xD0, "CHANNEL_KEY_PRESSURE"),
(0xE0, "PITCH_BEND"),
)
channelModeMessages = (
(0x78, "ALL_SOUND_OFF"),
(0x79, "RESET_ALL_CONTROLLERS"),
(0x7A, "LOCAL_CONTROL"),
(0x7B, "ALL_NOTES_OFF"),
(0x7C, "OMNI_MODE_OFF"),
(0x7D, "OMNI_MODE_ON"),
(0x7E, "MONO_MODE_ON"),
(0x7F, "POLY_MODE_ON"),
)
metaEvents = (
(0x00, "SEQUENCE_NUMBER"),
(0x01, "TEXT_EVENT"),
(0x02, "COPYRIGHT_NOTICE"),
(0x03, "SEQUENCE_TRACK_NAME"),
(0x04, "INSTRUMENT_NAME"),
(0x05, "LYRIC"), # renamed LYRIC_DISPLAY MIDI RP-26
(0x06, "MARKER"),
(0x07, "CUE_POINT"),
(0x08, "PROGRAM_NAME"), # added MIDI RP-19
(0X09, "DEVICE_NAME"), # added MIDI RP-19
(0x20, "MIDI_CHANNEL_PREFIX"),
(0x21, "MIDI_PORT"),
(0x2F, "END_OF_TRACK"),
(0x51, "SET_TEMPO"),
(0x54, "SMTPE_OFFSET"),
(0x58, "TIME_SIGNATURE"),
(0x59, "KEY_SIGNATURE"),
(0x60, "XMF_PATCH_TYPE_PREFIX"), # added MIDI RP-32
(0x7F, "SEQUENCER_SPECIFIC_META_EVENT"),
(0xFF, "META_EVENT"),
)
globals().update((desc, msg) for msg, desc in
channelVoiceMessages + channelModeMessages + metaEvents)
_add_constants()
def _get_variable_length_number(nextbyte, getbyte):
sum = 0
while nextbyte >= 0x80:
sum = (sum + (nextbyte & 0x7F)) << 7
nextbyte = getbyte()
return sum + nextbyte
def _first_command_is_repeat(status, nextbyte, getbyte):
raise error('the first midi command in the track is a repeat')
def _read_two_bytes(status, nextbyte, getbyte):
return status, nextbyte
def _read_three_bytes(status, nextbyte, getbyte):
return status, nextbyte, getbyte()
def _read_string(nextbyte, getbyte):
length = _get_variable_length_number(nextbyte, getbyte)
return ''.join(chr(getbyte()) for i in range(length))
def _read_f0_byte(status, nextbyte, getbyte):
if status == 0xff:
return status, nextbyte, _read_string(getbyte(), getbyte)
return status, _read_string(nextbyte, getbyte)
_read_midi_event = (
_first_command_is_repeat, # 0
None, # 10
None, # 20
None, # 30
None, # 40
None, # 50
None, # 60 data entry???
None, # 70 all notes off???
_read_three_bytes, # 80 note off
_read_three_bytes, # 90 note on
_read_three_bytes, # a0 poly aftertouch
_read_three_bytes, # b0 control
_read_two_bytes, # c0 prog change
_read_two_bytes, # d0 ch aftertouch
_read_three_bytes, # e0 pitchwheel range
_read_f0_byte, # f0
)
def _parse_track_body(data, clocks_max):
# This seems to be the fastest way of getting bytes in order as integers.
dataiter = iter(array.array('B', data))
getbyte = dataiter.__next__
time = 0
status = 0
try:
for nextbyte in dataiter:
time += _get_variable_length_number(nextbyte, getbyte)
if clocks_max and time > clocks_max:
break
nextbyte = getbyte()
if nextbyte >= 0x80:
status = nextbyte
nextbyte = getbyte()
yield time, _read_midi_event[status >> 4](status, nextbyte, getbyte)
except StopIteration:
# If the track ended just before the start of an event, the for loop
# will exit normally. If it ends anywhere else, we end up here.
print(len(list(dataiter)))
raise error('a track ended in the middle of a MIDI command')
def _parse_hunk(data, pos, type, magic):
if data[pos:pos+4] != magic:
raise error('expected %r, got %r' % (magic, data[pos:pos+4]))
try:
length, = struct.unpack('>I', data[pos+4:pos+8])
except struct.error:
raise error(
'the %s header is truncated (may be an incomplete download)' % type)
endpos = pos + 8 + length
data = data[pos+8:endpos]
if len(data) != length:
raise error(
'the %s is truncated (may be an incomplete download)' % type)
return data, endpos
def _parse_tracks(midi, pos, num_tracks, clocks_max):
if num_tracks > 256:
raise error('too many tracks: %d' % num_tracks)
for i in range(num_tracks):
trackdata, pos = _parse_hunk(midi, pos, 'track', b'MTrk')
yield list(_parse_track_body(trackdata, clocks_max))
# if pos < len(midi):
# warn
def parse_track(track, clocks_max=None):
track_body, end = _parse_hunk(track, 0, 'track', b'MTrk')
# if end < len(track):
# warn
return list(_parse_track_body(track_body, clocks_max))
def parse(midi, clocks_max=None):
header, first_track_pos = _parse_hunk(midi, 0, 'file', b'MThd')
try:
format, num_tracks, division = struct.unpack('>3H', header[:6])
except struct.error:
raise error('the file header is too short')
# if division < 0:
# raise error ('cannot handle non-metrical time')
tracks = list(_parse_tracks(midi, first_track_pos, num_tracks, clocks_max))
return (format, division*4), tracks