k-l-lambda's picture
added node-addon-lilypond
f65fe85
#!@TARGET_PYTHON@
#
# midi2ly.py -- LilyPond midi import script
# This file is part of LilyPond, the GNU music typesetter.
#
# Copyright (C) 1998--2020 Han-Wen Nienhuys <hanwen@xs4all.nl>
# Jan Nieuwenhuizen <janneke@gnu.org>
#
# 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/>.
'''
TODO:
'''
import gettext
import math
import os
import sys
"""
@relocate-preamble@
"""
# Load translation and install _() into Python's builtins namespace.
gettext.install('lilypond', '@localedir@')
import lilylib as ly
################################################################
# CONSTANTS
LINE_BELL = 60
scale_steps = [0, 2, 4, 5, 7, 9, 11]
global_options = None
clocks_per_1 = 1536
clocks_per_4 = 0
time = None
reference_note = 0
start_quant_clocks = 0
duration_quant_clocks = 0
allowed_tuplet_clocks = []
bar_max = 0
################################################################
program_name = sys.argv[0]
program_version = '@TOPLEVEL_VERSION@'
authors = ('Jan Nieuwenhuizen <janneke@gnu.org>',
'Han-Wen Nienhuys <hanwen@xs4all.nl>')
errorport = sys.stderr
def identify():
sys.stdout.write('%s (GNU LilyPond) %s\n' %
(program_name, program_version))
def warranty():
identify()
sys.stdout.write('''
%s
%s
%s
%s
''' % (_('Copyright (c) %s by') % '1998--2020',
'\n '.join(authors),
_('Distributed under terms of the GNU General Public License.'),
_('It comes with NO WARRANTY.')))
def progress(s):
errorport.write(s + '\n')
def warning(s):
progress(_("warning: ") + s)
def error(s):
progress(_("error: ") + s)
raise Exception(_("Exiting... "))
def debug(s):
if global_options.debug:
progress("debug: " + s)
def strip_extension(f, ext):
(p, e) = os.path.splitext(f)
if e == ext:
e = ''
return p + e
class Duration:
allowed_durs = (1, 2, 4, 8, 16, 32, 64, 128)
def __init__(self, clocks):
self.clocks = clocks
(self.dur, self.num, self.den) = self.dur_num_den(clocks)
def dur_num_den(self, clocks):
for i in range(len(allowed_tuplet_clocks)):
if clocks == allowed_tuplet_clocks[i]:
return global_options.allowed_tuplets[i]
dur = 0
num = 1
den = 1
g = math.gcd(int(clocks), clocks_per_1)
if g:
(dur, num) = (clocks_per_1 / g, clocks / g)
if not dur in self.allowed_durs:
dur = 4
num = clocks
den = clocks_per_4
return (dur, num, den)
def dump(self):
if self.den == 1:
if self.num == 1:
s = '%d' % self.dur
elif self.num == 3 and self.dur != 1:
s = '%d.' % (self.dur / 2)
else:
s = '%d*%d' % (self.dur, self.num)
else:
s = '%d*%d/%d' % (self.dur, self.num, self.den)
global reference_note
reference_note.duration = self
return s
def compare(self, other):
return self.clocks - other.clocks
def sign(x):
if x >= 0:
return 1
else:
return -1
class Note:
names = (0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6)
alterations = (0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0)
alteration_names = ('eses', 'es', '', 'is', 'isis')
def __init__(self, clocks, pitch, velocity):
self.pitch = pitch
self.velocity = velocity
# hmm
self.clocks = clocks
self.duration = Duration(clocks)
(self.octave, self.notename, self.alteration) = self.o_n_a()
def o_n_a(self):
# major scale: do-do
# minor scale: la-la (= + 5) '''
n = self.names[(self.pitch) % 12]
a = self.alterations[(self.pitch) % 12]
key = global_options.key
if not key:
key = Key(0, 0, 0)
if a and key.flats:
a = - self.alterations[(self.pitch) % 12]
n = (n - a) % 7
# By tradition, all scales now consist of a sequence
# of 7 notes each with a distinct name, from amongst
# a b c d e f g. But, minor scales have a wide
# second interval at the top - the 'leading note' is
# sharped. (Why? it just works that way! Anything
# else doesn't sound as good and isn't as flexible at
# saying things. In medieval times, scales only had 6
# notes to avoid this problem - the hexachords.)
# So, the d minor scale is d e f g a b-flat c-sharp d
# - using d-flat for the leading note would skip the
# name c and duplicate the name d. Why isn't c-sharp
# put in the key signature? Tradition. (It's also
# supposedly based on the Pythagorean theory of the
# cycle of fifths, but that really only applies to
# major scales...) Anyway, g minor is g a b-flat c d
# e-flat f-sharp g, and all the other flat minor keys
# end up with a natural leading note. And there you
# have it.
# John Sankey <bf250@freenet.carleton.ca>
#
# Let's also do a-minor: a b c d e f gis a
#
# --jcn
o = self.pitch / 12 - 4
if key.minor:
# as -> gis
if (key.sharps == 0 and key.flats == 0
and n == 5 and a == -1):
n = 4
a = 1
# des -> cis
elif key.flats == 1 and n == 1 and a == -1:
n = 0
a = 1
# ges -> fis
elif key.flats == 2 and n == 4 and a == -1:
n = 3
a = 1
# g -> fisis
elif key.sharps == 5 and n == 4 and a == 0:
n = 3
a = 2
# d -> cisis
elif key.sharps == 6 and n == 1 and a == 0:
n = 0
a = 2
# a -> gisis
elif key.sharps == 7 and n == 5 and a == 0:
n = 4
a = 2
# b -> ces
if key.flats >= 6 and n == 6 and a == 0:
n = 0
a = -1
o = o + 1
# e -> fes
if key.flats >= 7 and n == 2 and a == 0:
n = 3
a = -1
# f -> eis
if key.sharps >= 3 and n == 3 and a == 0:
n = 2
a = 1
# c -> bis
if key.sharps >= 4 and n == 0 and a == 0:
n = 6
a = 1
o = o - 1
return (o, n, a)
def __repr__(self):
s = chr((self.notename + 2) % 7 + ord('a'))
return 'Note(%s %s)' % (s, self.duration.dump())
def dump(self, dump_dur=True):
global reference_note
s = chr((self.notename + 2) % 7 + ord('a'))
s = s + self.alteration_names[self.alteration + 2]
if global_options.absolute_pitches:
commas = self.octave
else:
delta = self.pitch - reference_note.pitch
commas = sign(delta) * (abs(delta) // 12)
if (((sign(delta)
* (self.notename - reference_note.notename) + 7)
% 7 >= 4)
or ((self.notename == reference_note.notename)
and (abs(delta) > 4) and (abs(delta) < 12))):
commas = commas + sign(delta)
if commas > 0:
s = s + "'" * commas
elif commas < 0:
s = s + "," * -commas
if (dump_dur
and (self.duration.compare(reference_note.duration)
or global_options.explicit_durations)):
s = s + self.duration.dump()
# Chords need to handle their reference duration themselves
reference_note = self
# TODO: move space
return s + ' '
class Time:
def __init__(self, num, den):
self.clocks = 0
self.num = num
self.den = den
def bar_clocks(self):
return clocks_per_1 * self.num / self.den
def __repr__(self):
return 'Time(%d/%d)' % (self.num, self.den)
def dump(self):
global time
time = self
return '\n ' + '\\time %d/%d ' % (self.num, self.den) + '\n '
class Tempo:
def __init__(self, seconds_per_1):
self.clocks = 0
self.seconds_per_1 = seconds_per_1
def __repr__(self):
return 'Tempo(%d)' % self.bpm()
def bpm(self):
return 4 * 60 / self.seconds_per_1
def dump(self):
return '\n ' + '\\tempo 4 = %d ' % (self.bpm()) + '\n '
class Clef:
clefs = ('"bass_8"', 'bass', 'violin', '"violin^8"')
def __init__(self, type):
self.type = type
def __repr__(self):
return 'Clef(%s)' % self.clefs[self.type]
def dump(self):
return '\n \\clef %s\n ' % self.clefs[self.type]
class Key:
key_sharps = ('c', 'g', 'd', 'a', 'e', 'b', 'fis')
key_flats = ('BUG', 'f', 'bes', 'es', 'as', 'des', 'ges')
def __init__(self, sharps, flats, minor):
self.clocks = 0
self.flats = flats
self.sharps = sharps
self.minor = minor
def dump(self):
global_options.key = self
s = ''
if self.sharps and self.flats:
pass
else:
if self.flats:
k = (ord('cfbeadg'[self.flats % 7]) -
ord('a') - 2 - 2 * self.minor + 7) % 7
else:
k = (ord('cgdaebf'[self.sharps % 7]) -
ord('a') - 2 - 2 * self.minor + 7) % 7
if not self.minor:
name = chr((k + 2) % 7 + ord('a'))
else:
name = chr((k + 2) % 7 + ord('a'))
# fis cis gis dis ais eis bis
sharps = (2, 4, 6, 1, 3, 5, 7)
# bes es as des ges ces fes
flats = (6, 4, 2, 7, 5, 3, 1)
a = 0
if self.flats:
if flats[k] <= self.flats:
a = -1
else:
if sharps[k] <= self.sharps:
a = 1
if a:
name = name + Note.alteration_names[a + 2]
s = '\\key ' + name
if self.minor:
s = s + ' \\minor'
else:
s = s + ' \\major'
return '\n\n ' + s + '\n '
class Text:
text_types = (
'SEQUENCE_NUMBER',
'TEXT_EVENT',
'COPYRIGHT_NOTICE',
'SEQUENCE_TRACK_NAME',
'INSTRUMENT_NAME',
'LYRIC',
'MARKER',
'CUE_POINT',
'PROGRAM_NAME',
'DEVICE_NAME', )
@staticmethod
def _text_only(chr):
if ((' ' <= chr <= '~') or chr in ['\n', '\r']):
return chr
else:
return '~'
def __init__(self, type, text):
self.clocks = 0
self.type = type
self.text = ''.join(map(self._text_only, text))
def dump(self):
# urg, we should be sure that we're in a lyrics staff
s = ''
if self.type == midi.LYRIC:
s = '"%s"' % self.text
d = Duration(self.clocks)
if (global_options.explicit_durations
or d.compare(reference_note.duration)):
s = s + Duration(self.clocks).dump()
s = s + ' '
elif (self.text.strip()
and self.type == midi.SEQUENCE_TRACK_NAME
and not self.text == 'control track'
and not self.track.lyrics_p_):
text = self.text.replace('(MIDI)', '').strip()
if text:
s = '\n \\set Staff.instrumentName = "%(text)s"\n ' % locals(
)
elif self.text.strip():
s = '\n % [' + self.text_types[self.type] + '] ' + \
self.text + '\n '
return s
def __repr__(self):
return 'Text(%d=%s)' % (self.type, self.text)
def get_voice(channel, music):
debug('channel: ' + str(channel) + '\n')
return unthread_notes(music)
class Channel:
def __init__(self, number):
self.number = number
self.events = []
self.music = None
def add(self, event):
self.events.append(event)
def get_voice(self):
if not self.music:
self.music = self.parse()
return get_voice(self.number, self.music)
def parse(self):
pitches = {}
notes = []
music = []
last_lyric = 0
last_time = 0
for e in self.events:
t = e[0]
if start_quant_clocks:
t = quantise_clocks(t, start_quant_clocks)
if (e[1][0] == midi.NOTE_OFF
or (e[1][0] == midi.NOTE_ON and e[1][2] == 0)):
debug('%d: NOTE OFF: %s' % (t, e[1][1]))
if not e[1][2]:
debug(' ...treated as OFF')
end_note(pitches, notes, t, e[1][1])
elif e[1][0] == midi.NOTE_ON:
if e[1][1] not in pitches:
debug('%d: NOTE ON: %s' % (t, e[1][1]))
pitches[e[1][1]] = (t, e[1][2])
else:
debug('...ignored')
# all include ALL_NOTES_OFF
elif (e[1][0] >= midi.ALL_SOUND_OFF
and e[1][0] <= midi.POLY_MODE_ON):
for i in pitches:
end_note(pitches, notes, t, i)
elif e[1][0] == midi.META_EVENT:
if e[1][1] == midi.END_OF_TRACK:
for i in pitches:
end_note(pitches, notes, t, i)
break
elif e[1][1] == midi.SET_TEMPO:
(u0, u1, u2) = list(map(ord, e[1][2]))
us_per_4 = u2 + 256 * (u1 + 256 * u0)
seconds_per_1 = us_per_4 * 4 / 1e6
music.append((t, Tempo(seconds_per_1)))
elif e[1][1] == midi.TIME_SIGNATURE:
(num, dur, clocks4, count32) = list(map(ord, e[1][2]))
den = 2 ** dur
music.append((t, Time(num, den)))
elif e[1][1] == midi.KEY_SIGNATURE:
(alterations, minor) = list(map(ord, e[1][2]))
sharps = 0
flats = 0
if alterations < 127:
sharps = alterations
else:
flats = 256 - alterations
k = Key(sharps, flats, minor)
if not t and global_options.key:
# At t == 0, a set --key overrides us
k = global_options.key
music.append((t, k))
# ugh, must set key while parsing
# because Note init uses key
# Better do Note.calc () at dump time?
global_options.key = k
elif (e[1][1] == midi.LYRIC
or (global_options.text_lyrics
and e[1][1] == midi.TEXT_EVENT)):
self.lyrics_p_ = True
if last_lyric:
last_lyric.clocks = t - last_time
music.append((last_time, last_lyric))
last_time = t
last_lyric = Text(midi.LYRIC, e[1][2])
elif (e[1][1] >= midi.SEQUENCE_NUMBER
and e[1][1] <= midi.CUE_POINT):
text = Text(e[1][1], e[1][2])
text.track = self
music.append((t, text))
if text.type == midi.SEQUENCE_TRACK_NAME:
self.name = text.text
else:
if global_options.verbose:
sys.stderr.write("SKIP: %s\n" % repr(e))
else:
if global_options.verbose:
sys.stderr.write("SKIP: %s\n" % repr(e))
if last_lyric:
# last_lyric.clocks = t - last_time
# hmm
last_lyric.clocks = clocks_per_4
music.append((last_time, last_lyric))
last_lyric = 0
i = 0
while len(notes):
if i < len(music) and notes[0][0] >= music[i][0]:
i = i + 1
else:
music.insert(i, notes[0])
del notes[0]
return music
class Track (Channel):
def __init__(self):
Channel.__init__(self, None)
self.name = None
self.channels = {}
self.lyrics_p_ = False
def _add(self, event):
self.events.append(event)
def add(self, event, channel=None):
if channel is None:
self._add(event)
else:
self.channels[channel] = self.channels.get(
channel, Channel(channel))
self.channels[channel].add(event)
def get_voices(self):
return ([self.get_voice()]
+ [self.channels[k].get_voice()
for k in sorted(self.channels.keys())])
def create_track(events):
track = Track()
for e in events:
data = list(e[1])
if data[0] > 0x7f and data[0] < 0xf0:
channel = data[0] & 0x0f
e = (e[0], tuple([data[0] & 0xf0] + data[1:]))
track.add(e, channel)
else:
track.add(e)
return track
def quantise_clocks(clocks, quant):
q = int(clocks / quant) * quant
if q != clocks:
for tquant in allowed_tuplet_clocks:
if int(clocks / tquant) * tquant == clocks:
return clocks
if 2 * (clocks - q) > quant:
q = q + quant
return q
def end_note(pitches, notes, t, e):
try:
(lt, vel) = pitches[e]
del pitches[e]
i = len(notes) - 1
while i > 0:
if notes[i][0] > lt:
i = i - 1
else:
break
d = t - lt
if duration_quant_clocks:
d = quantise_clocks(d, duration_quant_clocks)
if not d:
d = duration_quant_clocks
notes.insert(i + 1,
(lt, Note(d, e, vel)))
except KeyError:
pass
def unthread_notes(channel):
threads = []
while channel:
thread = []
end_busy_t = 0
start_busy_t = 0
todo = []
for e in channel:
t = e[0]
if (e[1].__class__ == Note
and ((t == start_busy_t
and e[1].clocks + t == end_busy_t)
or t >= end_busy_t)):
thread.append(e)
start_busy_t = t
end_busy_t = t + e[1].clocks
elif (e[1].__class__ == Time
or e[1].__class__ == Key
or e[1].__class__ == Text
or e[1].__class__ == Tempo):
thread.append(e)
else:
todo.append(e)
threads.append(thread)
channel = todo
return threads
def dump_skip(skip, clocks):
return skip + Duration(clocks).dump() + ' '
def dump(d):
return d.dump()
def dump_chord(ch):
s = ''
notes = []
for i in ch:
if i.__class__ == Note:
notes.append(i)
else:
s = s + i.dump()
if len(notes) == 1:
s = s + dump(notes[0])
elif len(notes) > 1:
global reference_note
reference_dur = reference_note.duration
s = s + '<'
s = s + notes[0].dump(dump_dur=False)
r = reference_note
for i in notes[1:]:
s = s + i.dump(dump_dur=False)
s = s + '>'
if (r.duration.compare(reference_dur)
or global_options.explicit_durations):
s = s + r.duration.dump()
s = s + ' '
reference_note = r
return s
def dump_bar_line(last_bar_t, t, bar_count):
s = ''
bar_t = time.bar_clocks()
if t - last_bar_t >= bar_t:
bar_count = bar_count + (t - last_bar_t) / bar_t
if t - last_bar_t == bar_t:
s = '\n | %% %(bar_count)d\n ' % locals()
last_bar_t = t
else:
# urg, this will barf at meter changes
last_bar_t = last_bar_t + (t - last_bar_t) / bar_t * bar_t
return (s, last_bar_t, bar_count)
def dump_voice(thread, skip):
global reference_note, time
ref = Note(0, 4*12, 0)
if not reference_note:
reference_note = ref
else:
ref.duration = reference_note.duration
reference_note = ref
last_e = None
chs = []
ch = []
for e in thread:
if last_e and last_e[0] == e[0]:
ch.append(e[1])
else:
if ch:
chs.append((last_e[0], ch))
ch = [e[1]]
last_e = e
if ch:
chs.append((last_e[0], ch))
t = 0
last_t = 0
last_bar_t = 0
bar_count = 1
lines = ['']
for ch in chs:
t = ch[0]
i = lines[-1].rfind('\n') + 1
if len(lines[-1][i:]) > LINE_BELL:
lines.append('')
if t - last_t > 0:
d = t - last_t
if bar_max and t > time.bar_clocks() * bar_max:
d = time.bar_clocks() * bar_max - last_t
lines[-1] = lines[-1] + dump_skip(skip, d)
elif t - last_t < 0:
errorport.write('BUG: time skew')
(s, last_bar_t, bar_count) = dump_bar_line(last_bar_t,
t, bar_count)
if bar_max and bar_count > bar_max:
break
lines[-1] = lines[-1] + s
lines[-1] = lines[-1] + dump_chord(ch[1])
clocks = 0
for i in ch[1]:
if i.clocks > clocks:
clocks = i.clocks
last_t = t + clocks
(s, last_bar_t, bar_count) = dump_bar_line(last_bar_t,
last_t, bar_count)
lines[-1] = lines[-1] + s
return '\n '.join(lines) + '\n'
def number2ascii(i):
s = ''
i += 1
while i > 0:
m = (i - 1) % 26
s = '%c' % (m + ord('A')) + s
i = (i - m) // 26
return s
def get_track_name(i):
return 'track' + number2ascii(i)
def get_channel_name(i):
return 'channel' + number2ascii(i)
def get_voice_name(i, zero_too_p=False):
if i or zero_too_p:
return 'voice' + number2ascii(i)
return ''
def lst_append(lst, x):
lst.append(x)
return lst
def get_voice_layout(average_pitch):
d = {}
for i in range(len(average_pitch)):
d[average_pitch[i]] = lst_append(d.get(average_pitch[i], []), i)
s = list(reversed(sorted(average_pitch)))
non_empty = len([x for x in s if x])
names = ['One', 'Two']
if non_empty > 2:
names = ['One', 'Three', 'Four', 'Two']
layout = ['' for x in range(len(average_pitch))]
for i, n in zip(s, names):
if i:
v = d[i]
if isinstance(v, list):
d[i] = v[1:]
v = v[0]
layout[v] = n
return layout
def dump_track(track, n):
s = '\n'
track_name = get_track_name(n)
average_pitch = track_average_pitch(track)
voices = len([x for x in average_pitch[1:] if x])
clef = get_best_clef(average_pitch[0])
c = 0
vv = 0
for channel in track:
v = 0
channel_name = get_channel_name(c)
c += 1
for voice in channel:
voice_name = get_voice_name(v)
voice_id = track_name + channel_name + voice_name
item = voice_first_item(voice)
if item and item.__class__ == Note:
skip = 'r'
if global_options.skip:
skip = 's'
s += '%(voice_id)s = ' % locals()
if not global_options.absolute_pitches:
s += '\\relative c '
elif item and item.__class__ == Text:
skip = '" "'
s += '%(voice_id)s = \\lyricmode ' % locals()
else:
skip = '\\skip '
s += '%(voice_id)s = ' % locals()
s += '{\n'
if not n and not vv and global_options.key:
s += global_options.key.dump()
if average_pitch[vv+1] and voices > 1:
vl = get_voice_layout(average_pitch[1:])[vv]
if vl:
s += ' \\voice' + vl + '\n'
else:
if not global_options.quiet:
warning(
_('found more than 5 voices on a staff, expect bad output'))
s += ' ' + dump_voice(voice, skip)
s += '}\n\n'
v += 1
vv += 1
s += '%(track_name)s = <<\n' % locals()
if clef.type != 2:
s += clef.dump() + '\n'
c = 0
vv = 0
for channel in track:
v = 0
channel_name = get_channel_name(c)
c += 1
for voice in channel:
voice_context_name = get_voice_name(vv, zero_too_p=True)
voice_name = get_voice_name(v)
v += 1
vv += 1
voice_id = track_name + channel_name + voice_name
item = voice_first_item(voice)
context = 'Voice'
if item and item.__class__ == Text:
context = 'Lyrics'
s += ' \\context %(context)s = %(voice_context_name)s \\%(voice_id)s\n' % locals()
s += '>>\n\n'
return s
def voice_first_item(voice):
for event in voice:
if (event[1].__class__ == Note
or (event[1].__class__ == Text
and event[1].type == midi.LYRIC)):
return event[1]
return None
def channel_first_item(channel):
for voice in channel:
first = voice_first_item(voice)
if first:
return first
return None
def track_first_item(track):
for channel in track:
first = channel_first_item(channel)
if first:
return first
return None
def track_average_pitch(track):
i = 0
p = [0]
v = 1
for channel in track:
for voice in channel:
c = 0
p.append(0)
for event in voice:
if event[1].__class__ == Note:
i += 1
c += 1
p[v] += event[1].pitch
if c:
p[0] += p[v]
p[v] = p[v] / c
v += 1
if i:
p[0] = p[0] / i
return p
def get_best_clef(average_pitch):
if average_pitch:
if average_pitch <= 3*12:
return Clef(0)
elif average_pitch <= 5*12:
return Clef(1)
elif average_pitch >= 7*12:
return Clef(3)
return Clef(2)
class Staff:
def __init__(self, track):
self.voices = track.get_voices()
def dump(self, i):
return dump_track(self.voices, i)
def convert_midi(in_file, out_file):
global midi
import midi
global clocks_per_1, clocks_per_4, key
global start_quant_clocks
global duration_quant_clocks
global allowed_tuplet_clocks
global time
full_content = open(in_file, 'rb').read()
clocks_max = bar_max * clocks_per_1 * 2
midi_dump = midi.parse(full_content, clocks_max)
clocks_per_1 = midi_dump[0][1]
clocks_per_4 = clocks_per_1 / 4
time = Time(4, 4)
if global_options.start_quant:
start_quant_clocks = clocks_per_1 / global_options.start_quant
if global_options.duration_quant:
duration_quant_clocks = clocks_per_1 / global_options.duration_quant
allowed_tuplet_clocks = []
for (dur, num, den) in global_options.allowed_tuplets:
allowed_tuplet_clocks.append(clocks_per_1 / dur * num / den)
if global_options.verbose:
print('allowed tuplet clocks:', allowed_tuplet_clocks)
tracks = [create_track(t) for t in midi_dump[1]]
# urg, parse all global track events, such as Key first
# this fixes key in different voice/staff problem
for t in tracks:
t.music = t.parse()
prev = None
staves = []
for t in tracks:
voices = t.get_voices()
if ((t.name and prev and prev.name)
and t.name.split(':')[0] == prev.name.split(':')[0]):
# staves[-1].voices += voices
# all global track events first
staves[-1].voices = ([staves[-1].voices[0]]
+ [voices[0]]
+ staves[-1].voices[1:]
+ voices[1:])
else:
staves.append(Staff(t))
prev = t
tag = '%% Lily was here -- automatically converted by %s from %s' % (
program_name, in_file)
s = tag
s += r'''
\version "2.14.0"
'''
s += r'''
\layout {
\context {
\Voice
\remove "Note_heads_engraver"
\consists "Completion_heads_engraver"
\remove "Rest_engraver"
\consists "Completion_rest_engraver"
}
}
'''
for i in global_options.include_header:
s += '\n%% included from %(i)s\n' % locals()
s += open(i, encoding='utf8').read()
if s[-1] != '\n':
s += '\n'
s += '% end\n'
for i, t in enumerate(staves):
s += t.dump(i)
s += '\n\\score {\n <<\n'
control_track = False
i = 0
for i, staff in enumerate(staves):
track_name = get_track_name(i)
item = track_first_item(staff.voices)
staff_name = track_name
context = None
if not i and not item and len(staves) > 1:
control_track = track_name
continue
elif (item and item.__class__ == Note):
context = 'Staff'
if control_track:
s += ' \\context %(context)s=%(staff_name)s \\%(control_track)s\n' % locals()
elif item and item.__class__ == Text:
context = 'Lyrics'
if context:
s += ' \\context %(context)s=%(staff_name)s \\%(track_name)s\n' % locals()
s = s + r''' >>
\layout {}
\midi {}
}
'''
if not global_options.quiet:
progress(_("%s output to `%s'...") % ('LY', out_file))
if out_file == '-':
handle = sys.stdout
else:
handle = open(out_file, 'w', encoding='utf8')
handle.write(s)
handle.close()
def get_option_parser():
p = ly.get_option_parser(usage=_("%s [OPTION]... FILE") % 'midi2ly',
description=_(
"Convert %s to LilyPond input.\n") % 'MIDI',
add_help_option=False)
p.add_option('-a', '--absolute-pitches',
action='store_true',
help=_('print absolute pitches'))
p.add_option('-d', '--duration-quant',
metavar=_('DUR'),
help=_('quantise note durations on DUR'))
p.add_option('-D', '--debug',
action='store_true',
help=_('debug printing'))
p.add_option('-e', '--explicit-durations',
action='store_true',
help=_('print explicit durations'))
p.add_option('-h', '--help',
action='help',
help=_('show this help and exit'))
p.add_option('-i', '--include-header',
help=_('prepend FILE to output'),
action='append',
default=[],
metavar=_('FILE'))
p.add_option('-k', '--key', help=_('set key: ALT=+sharps|-flats; MINOR=1'),
metavar=_('ALT[:MINOR]'),
default=None),
p.add_option('-o', '--output', help=_('write output to FILE'),
metavar=_('FILE'),
action='store')
p.add_option('-p', '--preview', help=_('preview of first 4 bars'),
action='store_true')
p.add_option('-q', '--quiet',
action="store_true",
help=_("suppress progress messages and warnings about excess voices"))
p.add_option('-s', '--start-quant', help=_('quantise note starts on DUR'),
metavar=_('DUR'))
p.add_option('-S', '--skip',
action="store_true",
help=_("use s instead of r for rests"))
p.add_option('-t', '--allow-tuplet',
metavar=_('DUR*NUM/DEN'),
action='append',
dest='allowed_tuplets',
help=_('allow tuplet durations DUR*NUM/DEN'),
default=[])
p.add_option('-V', '--verbose', help=_('be verbose'),
action='store_true')
p.version = 'midi2ly (LilyPond) @TOPLEVEL_VERSION@'
p.add_option('--version',
action='version',
help=_('show version number and exit'))
p.add_option('-w', '--warranty', help=_('show warranty and copyright'),
action='store_true',)
p.add_option('-x', '--text-lyrics', help=_('treat every text as a lyric'),
action='store_true')
p.add_option_group(_('Examples'),
description=r'''
$ midi2ly --key=-2:1 --duration-quant=32 --allow-tuplet=4*2/3 --allow-tuplet=2*4/3 foo.midi
''')
p.add_option_group('',
description=(
_('Report bugs via %s')
% 'bug-lilypond@gnu.org') + '\n')
return p
def do_options():
opt_parser = get_option_parser()
(options, args) = opt_parser.parse_args()
if options.warranty:
warranty()
sys.exit(0)
if not args or args[0] == '-':
opt_parser.print_help()
sys.stderr.write('\n%s: %s %s\n' % (program_name, _('error: '),
_('no files specified on command line.')))
sys.exit(2)
if options.duration_quant:
options.duration_quant = int(options.duration_quant)
if options.key:
(alterations, minor) = list(
map(int, (options.key + ':0').split(':')))[0:2]
sharps = 0
flats = 0
if alterations >= 0:
sharps = alterations
else:
flats = - alterations
options.key = Key(sharps, flats, minor)
if options.start_quant:
options.start_quant = int(options.start_quant)
global bar_max
if options.preview:
bar_max = 4
options.allowed_tuplets = [list(map(int, a.replace('/', '*').split('*')))
for a in options.allowed_tuplets]
if options.verbose:
sys.stderr.write('Allowed tuplets: %s\n' %
repr(options.allowed_tuplets))
global global_options
global_options = options
return args
def main():
files = do_options()
exts = ['.midi', '.mid', '.MID']
for f in files:
g = f
for e in exts:
g = strip_extension(g, e)
if not os.path.exists(f):
for e in exts:
n = g + e
if os.path.exists(n):
f = n
break
if not global_options.output:
outdir = '.'
outbase = os.path.basename(g)
o = outbase + '-midi.ly'
elif (global_options.output[-1] == os.sep
or os.path.isdir(global_options.output)):
outdir = global_options.output
outbase = os.path.basename(g)
o = os.path.join(outdir, outbase + '-midi.ly')
else:
o = global_options.output
(outdir, outbase) = os.path.split(o)
if outdir and outdir != '.' and not os.path.exists(outdir):
os.mkdir(outdir, 0o777)
convert_midi(f, o)
if __name__ == '__main__':
main()