Spaces:
Sleeping
Sleeping
| import tempfile | |
| from collections import defaultdict | |
| import numpy as np | |
| import pretty_midi | |
| from ..align import create_beat_to_time_fn | |
| from .internal import ( | |
| Harmony, | |
| KeyChanges, | |
| Melody, | |
| MeterChanges, | |
| Note, | |
| TempoChanges, | |
| _ImmutableIterable, | |
| ) | |
| from .theorytab import ( | |
| TheorytabChord, | |
| TheorytabKey, | |
| TheorytabMeter, | |
| TheorytabNote, | |
| TheorytabTempo, | |
| TheorytabValueError, | |
| ) | |
| from .utils import theorytab_find_applicable | |
| _LILY_HEADER_TEMPLATE = r""" | |
| \header {{ | |
| {title_line} | |
| {composer_line} | |
| }} | |
| """.strip() | |
| _LILY_TEMPLATE = r""" | |
| #(set-default-paper-size "letter") | |
| {header} | |
| << | |
| \new ChordNames {{ | |
| \set majorSevenSymbol = \markup {{ maj7 }} | |
| \set additionalPitchPrefix = #"add" | |
| \chordmode {{ | |
| {harmony} | |
| }} | |
| }} | |
| \new Staff {{ | |
| {{ | |
| \clef {clef} | |
| \key {key} | |
| \time {meter} | |
| \tempo {tempo} | |
| {melody} | |
| }} | |
| }} | |
| >> | |
| \version "2.18.2" | |
| """.strip() | |
| _NUM_SIXTEENTHS_TO_LILY_NAME = { | |
| 1: "16", | |
| 3: "8.", | |
| 2: "8", | |
| 4: "4", | |
| 6: "4.", | |
| 8: "2", | |
| 12: "2.", | |
| 16: "1", | |
| } | |
| class LeadSheet(_ImmutableIterable): | |
| def __new__( | |
| cls, | |
| meter_changes, | |
| tempo_changes, | |
| key_changes, | |
| harmony, | |
| melody, | |
| total_num_tertiary=None, | |
| ): | |
| # Run value checks | |
| meter_changes = MeterChanges(*meter_changes) | |
| tempo_changes = TempoChanges(*tempo_changes) | |
| key_changes = KeyChanges(*key_changes) | |
| harmony = Harmony(*harmony) | |
| melody = Melody(*melody) | |
| # Compute tertiary per group (measure) | |
| if len(meter_changes) != 1: | |
| raise NotImplementedError() | |
| meter = meter_changes[0][1] | |
| assert meter in [(3, 2, 2), (4, 2, 2)] | |
| # TODO: Ensure all changes start on downbeats | |
| # Compute and/or validate total_num_tertiary | |
| if total_num_tertiary is None: | |
| total_num_tertiary = 1 | |
| user_defined = False | |
| else: | |
| if not isinstance(total_num_tertiary, int): | |
| raise TypeError() | |
| if total_num_tertiary <= 0: | |
| raise ValueError() | |
| user_defined = True | |
| for l in [meter_changes, tempo_changes, key_changes, harmony, melody]: | |
| if len(l) == 0: | |
| continue | |
| if len(l[-1]) == 2: | |
| o, _ = l[-1] | |
| d = 1 | |
| else: | |
| o, d, _ = l[-1] | |
| if (o + d) > total_num_tertiary: | |
| if user_defined: | |
| raise ValueError() | |
| else: | |
| total_num_tertiary = o + d | |
| # Round up to nearest measure | |
| # NOTE: We always need at least one measure because key/meter are defined at the beginning | |
| tertiary_per_group = int(np.prod(meter)) | |
| while total_num_tertiary % tertiary_per_group != 0: | |
| total_num_tertiary += 1 | |
| assert isinstance(total_num_tertiary, int) | |
| assert total_num_tertiary % tertiary_per_group == 0 | |
| assert total_num_tertiary // tertiary_per_group > 0 | |
| return super().__new__( | |
| cls, | |
| ( | |
| meter_changes, | |
| tempo_changes, | |
| key_changes, | |
| harmony, | |
| melody, | |
| total_num_tertiary, | |
| ), | |
| ) | |
| def as_lily( | |
| self, | |
| clef="treble", | |
| adjust_melody_octave=True, | |
| skip_unknown_chords=False, | |
| artist=None, | |
| title=None, | |
| ): | |
| if clef not in ["treble", "bass"]: | |
| raise ValueError() | |
| ( | |
| meter_changes, | |
| tempo_changes, | |
| key_changes, | |
| harmony, | |
| melody, | |
| total_num_tertiary, | |
| ) = self | |
| # Format meter | |
| assert len(meter_changes) == 1 | |
| meter = meter_changes[0][1] | |
| assert meter in [(3, 2, 2), (4, 2, 2)] | |
| tertiary_per_group = int(np.prod(meter)) | |
| meter_lily = meter.as_lily() | |
| meter_lily = f"{meter_lily[0]}/{meter_lily[1]}" | |
| # Format tempo | |
| if len(tempo_changes) != 1: | |
| raise NotImplementedError() | |
| assert len(tempo_changes) == 1 | |
| tempo = tempo_changes[0][1] | |
| tempo_lily = tempo.as_lily(meter) | |
| tempo_lily = f"{tempo_lily[0]} = {tempo_lily[1]}" | |
| # Format key | |
| if len(key_changes) != 1: | |
| raise NotImplementedError() | |
| key = key_changes[0][1] | |
| key_lily = key.as_lily() | |
| key_lily = f"{key_lily[0]} \\{key_lily[1]}" | |
| # Add in rests in between chords | |
| chords_and_rests = [] | |
| if len(harmony) == 0: | |
| chords_and_rests.append((total_num_tertiary, None)) | |
| else: | |
| for i, (t, c) in enumerate(harmony): | |
| if i == 0 and t > 0: | |
| chords_and_rests.append((t, None)) | |
| if i + 1 < len(harmony): | |
| d = harmony[i + 1][0] - t | |
| else: | |
| d = total_num_tertiary - t | |
| chords_and_rests.append((d, c)) | |
| assert all(d > 0 for d, _ in chords_and_rests) | |
| assert sum(d for d, _ in chords_and_rests) == total_num_tertiary | |
| # Format chords | |
| harmony = [] | |
| for d, c in chords_and_rests: | |
| # NOTE: c is None means no chord *change*, cs is (None, None) means *change* to N.C. | |
| if c is None: | |
| lpc = ("s", "") | |
| else: | |
| lpc = c.as_lily(key) | |
| if not isinstance(lpc[1], str): | |
| assert isinstance(lpc[1], tuple) | |
| if not skip_unknown_chords: | |
| raise ValueError("Unknown chord") | |
| lpc = ("s", "") | |
| lp = f"{lpc[0]}16*{d}" | |
| if len(lpc[1]) > 0: | |
| lp += ":" + lpc[1] | |
| harmony.append(lp) | |
| harmony_lily = " ".join(harmony) | |
| # Adjust melody to be centered in clef | |
| # NOTE: Finds the octave where the most melody are on the staff lines | |
| if adjust_melody_octave and len(melody) > 0: | |
| if clef == "treble": | |
| midi_pitch_range = (63, 78) # Eb at bottom of treble clef, F# at top | |
| elif clef == "bass": | |
| midi_pitch_range = (42, 58) # Gb at bottom of bass clef, A# at top | |
| else: | |
| assert False | |
| midi_pitches = np.array([ns.as_midi_pitch() for _, _, ns in melody]) | |
| candidate_octaves = np.arange(-1000, 1000) | |
| midi_pitches_adjusted = (candidate_octaves * 12)[ | |
| :, np.newaxis | |
| ] + midi_pitches[np.newaxis, :] | |
| midi_pitches_onstaff = np.logical_and( | |
| midi_pitches_adjusted >= midi_pitch_range[0], | |
| midi_pitches_adjusted <= midi_pitch_range[1], | |
| ) | |
| best_octave = int( | |
| candidate_octaves[ | |
| np.argmax(midi_pitches_onstaff.astype(np.int64).sum(axis=1)) | |
| ] | |
| ) | |
| melody = [(s, d, Note(ns[0], ns[1] + best_octave)) for s, d, ns in melody] | |
| # Add in rests in between melody | |
| last_offset = 0 | |
| notes_and_rests = [] | |
| for t, d, ns in melody: | |
| assert t >= last_offset | |
| if t != last_offset: | |
| notes_and_rests.append((t - last_offset, None)) | |
| notes_and_rests.append((d, ns)) | |
| last_offset = t + d | |
| t = total_num_tertiary | |
| assert t >= last_offset | |
| if t != last_offset: | |
| notes_and_rests.append((t - last_offset, None)) | |
| assert all(d > 0 for d, _ in notes_and_rests) | |
| assert sum(d for d, _ in notes_and_rests) == total_num_tertiary | |
| # Beaming logic | |
| bar_to_notes = defaultdict(list) | |
| t = 0 | |
| for d, ns in notes_and_rests: | |
| tied = False | |
| while d > 0: | |
| bar = t // tertiary_per_group | |
| bar_remaining = ((bar + 1) * tertiary_per_group) - t | |
| consumed = min(d, bar_remaining) | |
| if consumed not in _NUM_SIXTEENTHS_TO_LILY_NAME: | |
| while _NUM_SIXTEENTHS_TO_LILY_NAME.get(consumed, ".").endswith("."): | |
| consumed -= 1 | |
| d -= consumed | |
| t += consumed | |
| if ns is None: | |
| lp = "r" | |
| else: | |
| lp = "".join(ns.as_lily(key)) | |
| lp += _NUM_SIXTEENTHS_TO_LILY_NAME[consumed] | |
| lp += "~" if d > 0 else "" | |
| bar_to_notes[bar].append(lp) | |
| tied = True | |
| # Format notes | |
| melody_lily = " | ".join([" ".join(notes) for _, notes in bar_to_notes.items()]) | |
| header = "" | |
| if title is not None or artist is not None: | |
| header = _LILY_HEADER_TEMPLATE.format( | |
| title_line="" if title is None else f'title = "{title}"', | |
| composer_line="" if artist is None else f'composer = "{artist}"', | |
| ) | |
| return _LILY_TEMPLATE.format( | |
| header=header, | |
| clef=clef, | |
| key=key_lily, | |
| meter=meter_lily, | |
| tempo=tempo_lily, | |
| harmony=harmony_lily, | |
| melody=melody_lily, | |
| ) | |
| def as_midi(self, pulse_to_time_fn=None, adjust_melody_octave=True): | |
| ( | |
| meter_changes, | |
| tempo_changes, | |
| _, | |
| harmony, | |
| melody, | |
| total_num_tertiary, | |
| ) = self | |
| # Assumptions | |
| assert len(meter_changes) == 1 | |
| meter = meter_changes[0][1] | |
| assert meter in [(3, 2, 2), (4, 2, 2)] | |
| tertiary_per_group = int(np.prod(meter)) | |
| tertiary_per_pulse = int(np.prod(meter[1:])) | |
| # Create tertiary_to_time_fn | |
| if pulse_to_time_fn is None: | |
| if len(tempo_changes) != 1: | |
| raise NotImplementedError() | |
| tempo = tempo_changes[0][1] | |
| pps = tempo[0] / 60 | |
| tertiaries = [0, tertiary_per_pulse] | |
| times = [0, 1 / pps] | |
| tertiary_to_time_fn = create_beat_to_time_fn(tertiaries, times) | |
| else: | |
| tertiary_to_time_fn = lambda t: pulse_to_time_fn(t / tertiary_per_pulse) | |
| # Adjust melody to be mostly in midi octave 5 | |
| if adjust_melody_octave and len(melody) > 0: | |
| midi_pitch_range = (60, 71) | |
| midi_pitches = np.array([ns.as_midi_pitch() for _, _, ns in melody]) | |
| candidate_octaves = np.arange(-1000, 1000) | |
| midi_pitches_adjusted = (candidate_octaves * 12)[ | |
| :, np.newaxis | |
| ] + midi_pitches[np.newaxis, :] | |
| midi_pitches_onstaff = np.logical_and( | |
| midi_pitches_adjusted >= midi_pitch_range[0], | |
| midi_pitches_adjusted <= midi_pitch_range[1], | |
| ) | |
| best_octave = int( | |
| candidate_octaves[ | |
| np.argmax(midi_pitches_onstaff.astype(np.int64).sum(axis=1)) | |
| ] | |
| ) | |
| melody = [(s, d, Note(ns[0], ns[1] + best_octave)) for s, d, ns in melody] | |
| # Create click | |
| click = pretty_midi.Instrument(program=0, is_drum=True) | |
| for t in range(0, total_num_tertiary, tertiary_per_pulse): | |
| velocity = 75 | |
| pitch = 31 | |
| # Downbeat | |
| if t % tertiary_per_group == 0: | |
| velocity = 100 | |
| pitch = 37 | |
| click.notes.append( | |
| pretty_midi.Note( | |
| velocity, | |
| pitch, | |
| tertiary_to_time_fn(t), | |
| tertiary_to_time_fn(t + tertiary_per_pulse), | |
| ) | |
| ) | |
| # Create harmony | |
| harmony_ins = pretty_midi.Instrument(program=24) # Acoustic Guitar (nylon) | |
| for i, (t, c) in enumerate(harmony): | |
| if i + 1 < len(harmony): | |
| d = harmony[i + 1][0] - t | |
| else: | |
| d = total_num_tertiary - t | |
| for p in c.as_midi_pitches(): | |
| harmony_ins.notes.append( | |
| pretty_midi.Note( | |
| 67, | |
| p, | |
| tertiary_to_time_fn(t), | |
| tertiary_to_time_fn(t + d), | |
| ) | |
| ) | |
| # Create melody | |
| melody_ins = pretty_midi.Instrument(program=0) | |
| for t, d, ns in melody: | |
| melody_ins.notes.append( | |
| pretty_midi.Note( | |
| 100, | |
| ns.as_midi_pitch(), | |
| tertiary_to_time_fn(t), | |
| tertiary_to_time_fn(t + d), | |
| ) | |
| ) | |
| # Create MIDI | |
| midi = pretty_midi.PrettyMIDI() | |
| midi.instruments.extend([click, harmony_ins, melody_ins]) | |
| with tempfile.NamedTemporaryFile() as f: | |
| midi.write(f.name) | |
| with open(f.name, "rb") as f: | |
| return f.read() | |
| def from_theorytab( | |
| cls, | |
| analysis, | |
| ignore_inversion=True, | |
| skip_bad_notes_and_chords=False, | |
| ): | |
| # For theorytab we always subdivide into 1/4 beat | |
| beat_to_tertiary = lambda b: round(b * 4) | |
| end_beat = analysis["endBeat"] - 1 | |
| # Meters | |
| meter_changes = [] | |
| last_meter = None | |
| for ttm in analysis["meters"]: | |
| ttm = TheorytabMeter(ttm) | |
| meter = ttm.as_meter() | |
| if meter != last_meter: | |
| meter_changes.append((beat_to_tertiary(ttm["beat"] - 1), meter)) | |
| last_meter = meter | |
| # Tempos | |
| tempo_changes = [] | |
| last_tempo = None | |
| for ttt in analysis["tempos"]: | |
| ttt = TheorytabTempo(ttt) | |
| tempo = ttt.as_tempo() | |
| if tempo != last_tempo: | |
| tempo_changes.append((beat_to_tertiary(ttt["beat"] - 1), tempo)) | |
| last_tempo = tempo | |
| # Keys | |
| ttk_keys = [] | |
| key_changes = [] | |
| last_key = None | |
| for ttk in analysis["keys"]: | |
| ttk = TheorytabKey(ttk) | |
| ttk_keys.append(ttk) | |
| key = ttk.as_key() | |
| if key != last_key: | |
| key_changes.append((beat_to_tertiary(ttk["beat"] - 1), key)) | |
| last_key = key | |
| # Chords | |
| harmony = [] | |
| last_chord = None | |
| for ttc in analysis["chords"]: | |
| try: | |
| ttc = TheorytabChord(ttc) | |
| except TheorytabValueError as e: | |
| if not skip_bad_notes_and_chords: | |
| raise e | |
| continue | |
| if ttc.will_sound(): | |
| chord = ttc.as_chord( | |
| theorytab_find_applicable(ttk_keys, ttc), | |
| root_position=ignore_inversion, | |
| ) | |
| if chord != last_chord: | |
| ob = ttc["beat"] - 1 | |
| db = ttc["duration"] | |
| if ob + db > (end_beat + 1e-6): | |
| raise ValueError() | |
| harmony.append((beat_to_tertiary(ob), chord)) | |
| last_chord = chord | |
| # Notes | |
| melody = [] | |
| for ttn in analysis["notes"]: | |
| try: | |
| ttn = TheorytabNote(ttn) | |
| except TheorytabValueError as e: | |
| if not skip_bad_notes_and_chords: | |
| raise e | |
| continue | |
| if ttn.will_sound(): | |
| ob = ttn["beat"] - 1 | |
| db = ttn["duration"] | |
| if ob + db > (end_beat + 1e-6): | |
| raise ValueError() | |
| melody.append( | |
| ( | |
| beat_to_tertiary(ob), | |
| beat_to_tertiary(db), | |
| ttn.as_note(theorytab_find_applicable(ttk_keys, ttn)), | |
| ) | |
| ) | |
| # Trim extra changes | |
| total_num_tertiary = beat_to_tertiary(end_beat) | |
| meter_changes = [m for m in meter_changes if m[0] < total_num_tertiary] | |
| tempo_changes = [t for t in tempo_changes if t[0] < total_num_tertiary] | |
| key_changes = [k for k in key_changes if k[0] < total_num_tertiary] | |
| return cls( | |
| meter_changes, | |
| tempo_changes, | |
| key_changes, | |
| harmony, | |
| melody, | |
| total_num_tertiary=total_num_tertiary, | |
| ) | |