import copy
from chiptunesak.base import *
from chiptunesak.chirp import Note
# This is not the required version: use any version >= to this
LP_VERSION = '2.18.2'
# TODO:
# - Refactor common code out of export_clip_to_lilypond and export_song_to_lilypond?
lp_pitches = {
'sharps': ["c", "cis", "d", "dis", "e", "f", "fis", "g", "gis", "a", "ais", "b"],
'flats': ["c", "des", "d", "ees", "e", "f", "ges", "g", "aes", "a", "bes", "b"],
}
lp_durations = {
Fraction(4, 1): '1', Fraction(3, 1): '2.', Fraction(2, 1): '2', Fraction(3, 2): '4.', Fraction(1, 1): '4',
Fraction(3, 4): '8.', Fraction(1, 2): '8', Fraction(3, 8): '16.', Fraction(1, 4): '16',
Fraction(3, 16): '32.', Fraction(1, 8): '32', Fraction(3, 32): '64.', Fraction(1, 16): '64'
}
def lp_pitch_to_note_name(note_num, pitches, octave_offset=-3):
"""
Gets the Lilypond note name for a given pitch.
:param note_num: MIDI note number
:param pitches: Set of pitches to use (sharp or flat)
:param octave_offset: Octave offset (the default is 4, which is the lilypond standard)
:return: Lilypond pitch name
"""
if not 0 <= note_num <= 127:
raise ChiptuneSAKValueError("Illegal note number %d" % note_num)
octave_num = ((note_num - constants.C0_MIDI_NUM) // 12) + octave_offset
if octave_num >= 0:
octave = "'" * octave_num
else:
octave = "," * abs(octave_num)
pitch = note_num % 12
return "%s%s" % (pitches[pitch], octave)
def make_lp_notes(note_name, duration, ppq):
"""
Makes a series of Lilypond notes/rests to fill a specified duration
:param note_name: Lilypond note name (from lp_pitch_to_note_name) or 'r' for rest.
:param duration: Duration of the note in ppq ticks
:param ppq: ppq from the song in which the note exists
:return: String representing the notes in Lilypond format
"""
if duration <= 0:
raise ChiptuneSAKValueError("Illegal note duration: %d" % duration)
durs = decompose_duration(duration, ppq, lp_durations)
if note_name == 'r':
retval = ' '.join("%s%s" % (note_name, lp_durations[f]) for f in durs)
else:
retval = '~ '.join("%s%s" % (note_name, lp_durations[f]) for f in durs)
return retval
def avg_pitch(track):
"""
Gives the average pitch for a track
:param track: an MChirpTrack
:return: average pitch as MIDI note number
"""
total = sum(n.note_num for measure in track.measures for n in measure.events if isinstance(n, Note))
number = sum(1 for measure in track.measures for n in measure.events if isinstance(n, Note))
if number == 0:
raise ChiptuneSAKContentError("Track %s has no notes" % track.name)
return total / number
[docs]class Lilypond(ChiptuneSAKIO):
@classmethod
def cts_type(cls):
return 'Lilypond'
def __init__(self):
ChiptuneSAKIO.__init__(self)
self.set_options(format='song')
self.current_pitch_set = lp_pitches['sharps']
self.current_clef = 'treble'
self.current_ottava = 0
@property
def format(self):
return self.get_option('format')[0].lower()
[docs] def to_bin(self, mchirp_song, **kwargs):
"""
Exports MChirp to lilypond text
:param mchirp_song: song to export
:type mchirp_song: MChirpSong
:return: lilypond text
:rtype: str
:keyword options:
* **format** (string) - format, either 'song' or 'clip'
* **autosort** (bool) - sort tracks from highest to lowest average pitch
* **measures** (list) - list of contiguous measures, from one track.
Required for 'clip' format, ignored otherwise.
"""
self.set_options(**kwargs)
if self.format == 'c':
measures = list(self.get_option('measures', []))
return self.export_clip_to_lilypond(mchirp_song, measures)
elif self.format == 's':
return self.export_song_to_lilypond(mchirp_song)
else:
raise ChiptuneSAKValueError(f"Unrecognized format {self.format}")
[docs] def to_file(self, mchirp_song, filename, **kwargs):
"""
Exports MChirp to lilypond source file
:param mchirp_song: song to export
:type mchirp_song: MChirpSong
:param filename: filename to write
:type filename: str
:return: lilypond text
:rtype: str
:keyword options: see to_bin()
"""
self.set_options(**kwargs)
with open(filename, 'w') as f:
f.write(self.to_bin(mchirp_song, **kwargs))
def clef(self, t_range):
avg = sum(t_range) / len(t_range)
clef = self.current_clef
if self.current_clef == 'treble' and avg < 60:
clef = 'bass'
elif self.current_clef == 'bass' and avg > 60:
clef = 'treble'
return clef
def ottava(self, note_num):
ottava = self.current_ottava
bass_transitions = (41 - 3 * self.current_ottava, 66 + 3 * self.current_ottava)
treble_transitions = (55 + 3 * self.current_ottava, 84 - 3 * self.current_ottava)
if self.current_clef == 'bass':
if note_num < bass_transitions[0]:
ottava = -1
elif note_num > bass_transitions[1]:
ottava = 1
else:
ottava = 0
else:
if note_num < treble_transitions[0]:
ottava = -1
elif note_num > treble_transitions[1]:
ottava = 1
else:
ottava = 0
return ottava
[docs] def measure_to_lilypond(self, measure):
"""
Converts contents of a measure into Lilypond text
:param measure: A ctsMeasure.Measure object
:return: Lilypond text encoding the measure content.
"""
measure_contents = []
measure_notes = [e.note_num for e in measure.events if isinstance(e, Note)]
if len(measure_notes) > 0:
measure_range = (min(measure_notes), max(measure_notes))
measure_clef = self.clef(measure_range)
if measure_clef != self.current_clef:
self.current_clef = measure_clef
measure_contents.append("\\clef %s" % self.current_clef)
for e in measure.events:
if isinstance(e, Note):
note_ottava = self.ottava(e.note_num)
if note_ottava != self.current_ottava:
self.current_ottava = note_ottava
measure_contents.append("\\ottava #%d" % self.current_ottava)
f = Fraction(e.duration / self.ppq).limit_denominator(64)
if f in lp_durations:
measure_contents.append(
"%s%s%s" % (lp_pitch_to_note_name(e.note_num, self.current_pitch_set),
lp_durations[f], '~' if e.tied_from else ''))
else:
measure_contents.append(make_lp_notes(
lp_pitch_to_note_name(e.note_num, self.current_pitch_set),
e.duration, self.ppq))
elif isinstance(e, Rest):
f = Fraction(e.duration / self.ppq).limit_denominator(64)
if f in lp_durations:
measure_contents.append("r%s" % (lp_durations[f]))
else:
measure_contents.append(make_lp_notes('r', e.duration, self.ppq))
elif isinstance(e, Triplet):
measure_contents.append('\\tuplet 3/2 {')
for te in e.content:
if isinstance(te, Note):
te_duration = te.duration * Fraction(3 / 2)
f = Fraction(te_duration / self.ppq).limit_denominator(64)
if f in lp_durations:
measure_contents.append(
"%s%s%s" % (lp_pitch_to_note_name(te.note_num, self.current_pitch_set),
lp_durations[f], '~' if te.tied_from else ''))
else:
measure_contents.append(make_lp_notes(
lp_pitch_to_note_name(te.note_num, self.current_pitch_set),
te_duration, self.ppq))
elif isinstance(te, Rest):
measure_contents.append(make_lp_notes('r', te.duration * Fraction(3 / 2), self.ppq))
measure_contents.append('}')
elif isinstance(e, MeasureMarker):
measure_contents.append('|')
elif isinstance(e, TimeSignatureEvent):
if e.num != self.current_time_signature.num or e.denom != self.current_time_signature.denom:
measure_contents.append('\\time %d/%d' % (e.num, e.denom))
self.current_time_signature = copy.copy(e)
elif isinstance(e, KeySignatureEvent):
if e.key.key_signature != self.current_key_signature:
key_name = e.key.key_name
self.current_pitch_set = lp_pitches[e.key.accidentals()]
key_name = key_name.replace('#', 'is')
key_name = key_name.replace('b', 'es')
if e.key.key_signature.type == 'minor':
measure_contents.append('\\key %s \\minor' % (key_name.lower()[:-1]))
else:
measure_contents.append('\\key %s \\major' % (key_name.lower()))
self.current_key_signature = copy.copy(e.key.key_signature)
return measure_contents
[docs] def export_clip_to_lilypond(self, mchirp_song, measures):
"""
Turns a set of measures into Lilypond suitable for use as a clip. All the music will be on a single line
with no margins. It is recommended that this clip be turned into Lilypond using the command line:
``lilypond -ddelete-intermediate-files -dbackend=eps -dresolution=600 -dpixmap-format=pngalpha --png <filename>``
:param mchirp_song: ChirpSong from which the measures were taken.
:type mchirp_song: MChirpSong
:param measures: List of measures.
:type measures: list
:return: Lilypond markup ascii
:rtype: str
"""
if len(measures) < 1:
raise ChiptuneSAKContentError("No measures to export!")
# Set these to the default so that they will change on the first measure.
self.current_time_signature = TimeSignatureEvent(0, 4, 4)
self.current_key_signature = key.ChirpKey('C').key_signature
self.current_clef = 'treble'
self.current_ottava = 0
self.ppq = mchirp_song.metadata.ppq
output = []
ks = mchirp_song.get_key_signature(measures[0].start_time)
if ks.start_time < measures[0].start_time:
measures[0].events.insert(0, KeySignatureEvent(measures[0].start_time, ks.key))
ts = mchirp_song.get_time_signature(measures[0].start_time)
if ts.start_time < measures[0].start_time:
measures[0].events.insert(0, TimeSignatureEvent(measures[0].start_time, ts.num, ts.denom))
output.append('\\version "%s"' % LP_VERSION)
output.append('''
\\paper {
indent=0\\mm line-width=120\\mm oddHeaderMarkup = ##f
evenHeaderMarkup = ##f oddFooterMarkup = ##f evenFooterMarkup = ##f
page-breaking = #ly:one-line-breaking }
''')
note_range = (min(e.note_num for m in measures for e in m.events if isinstance(e, Note)),
max(e.note_num for m in measures for e in m.events if isinstance(e, Note)))
self.current_clef = self.clef(note_range)
self.current_ottava = 0
output.append('\\new Staff {')
output.append('\\clef %s' % self.current_clef)
for im, m in enumerate(measures):
measure_contents = self.measure_to_lilypond(m)
output.append(' '.join(measure_contents))
output.append('}')
return '\n'.join(output)
[docs] def export_song_to_lilypond(self, mchirp_song):
"""
Converts a song to Lilypond format. Optimized for multi-page PDF output of the song.
Recommended lilypond command:
``lilypond <filename>``
:param mchirp_song: ChirpSong to convert to Lilypond format
:type mchirp_song: MChirpSong
:return: Lilypond markup ascii
:rtype: str
"""
# Set these to the default, so that they will change on the first measure.
self.current_time_signature = TimeSignatureEvent(0, 4, 4)
self.current_key_signature = key.ChirpKey('C').key_signature
self.current_clef = 'treble'
self.current_ottava = 0
self.ppq = mchirp_song.metadata.ppq
output = []
output.append('\\version "%s"' % LP_VERSION)
output.append('\\header {')
if len(mchirp_song.metadata.name) > 0:
output.append(' title = "%s"' % mchirp_song.metadata.name)
output.append('composer = "%s"' % mchirp_song.metadata.composer)
output.append('}')
# ---- end of headers ----
tracks = [t for t in mchirp_song.tracks]
if self.get_option('autosort', False):
tracks = sorted([t for t in mchirp_song.tracks], key=avg_pitch, reverse=True)
output.append('\\new StaffGroup <<')
for it, t in enumerate(tracks):
self.current_time_signature = TimeSignatureEvent(0, 4, 4)
self.current_key_signature = key.ChirpKey('C').key_signature
measures = copy.copy(t.measures)
track_range = (min(e.note_num for m in t.measures for e in m.events if isinstance(e, Note)),
max(e.note_num for m in t.measures for e in m.events if isinstance(e, Note)))
self.current_clef = self.clef(track_range)
self.current_ottava = 0
output.append('\\new Staff \\with { instrumentName = #"%s" } {' % t.name)
output.append('\\clef %s' % self.current_clef)
for im, m in enumerate(measures):
output.append("%% measure %d" % (im + 1))
measure_contents = self.measure_to_lilypond(m)
output.append(' '.join(measure_contents))
output.append('\\bar "||"')
output.append('}')
output.append('>>\n')
return '\n'.join(output)