def test_midi_export_mode_0(self): m = self._export_and_read(mode=0) msg = ('Number of parts {} does not equal number of tracks {} while ' 'testing part_voice_assign_mode=0 in save_score_midi'.format( len(self.parts_list), len(m.tracks))) self.assertEqual(len(self.parts_list), len(m.tracks), msg) for part, track in zip(self.parts_list, m.tracks): # voices per note in part vc = note_voices(part) # channels per note in track ch = note_channels(track) vcp = partition(lambda x: -1 if x is None else x, vc) chp = partition(lambda x: x, ch) vc_list = [len(vcp[x]) for x in sorted(vcp.keys())] ch_list = [len(chp[x]) for x in sorted(chp.keys())] msg = ('Track channels should have {} notes respectively, ' 'but they have {}'.format(vc_list, ch_list)) self.assertEqual(vc_list, ch_list, msg) ts_part = sum(1 for _ in part.iter_all(score.TimeSignature)) ts_track = sum(1 for e in track if e.type == 'time_signature') msg = ('Track should have {} time signatures respectively, ' 'but has {}'.format(ts_part, ts_track)) self.assertEqual(ts_part, ts_track, msg) ks_part = sum(1 for _ in part.iter_all(score.KeySignature)) ks_track = sum(1 for e in track if e.type == 'key_signature') msg = ('Track should have {} key signatures respectively, ' 'but has {}'.format(ks_part, ks_track)) self.assertEqual(ks_part, ks_track, msg)
def test_midi_import_mode_0(self): parts = load_score_midi(self.tmpfile.name, part_voice_assign_mode=0) by_track = partition(itemgetter(0), self.notes_per_tr_ch.keys()) msg = ('Number of parts {} does not equal number of tracks {} while ' 'testing part_voice_assign_mode=0 in load_score_midi').format( len(parts), len(by_track)) self.assertEqual(len(parts), len(by_track), msg) for part, tr in zip(parts, by_track): msg = '{} should be a Part instance but it is not'.format(part) self.assertTrue(isinstance(part, score.Part), msg) n_track_notes = sum(self.notes_per_tr_ch[tr_ch] for tr_ch in by_track[tr]) part_notes = part.notes n_part_notes = len(part_notes) msg = 'Part should have {} notes but it has'.format( n_track_notes, n_part_notes) self.assertEqual(n_track_notes, n_part_notes, msg) n_ch_notes = [ self.notes_per_tr_ch[tr_ch] for tr_ch in by_track[tr] ] n_voice_notes = [ len(vn) for v, vn in partition( lambda x: x, [n.voice for n in part_notes]).items() ] msg = ( 'Part voices should have {} notes respectively, but they have {}' .format(n_ch_notes, n_voice_notes)) self.assertEqual(n_ch_notes, n_voice_notes, msg)
def test_midi_import_mode_2(self): part = load_score_midi(self.tmpfile.name, part_voice_assign_mode=2) msg = '{} should be a Part instance but it is not'.format(part) self.assertTrue(isinstance(part, score.Part), msg) by_track = partition(itemgetter(0), self.notes_per_tr_ch.keys()) by_voice = partition(lambda x: x.voice, part.notes) n_track_notes = [ sum(self.notes_per_tr_ch[tr_ch] for tr_ch in tr_chs) for tr_chs in by_track.values() ] n_voice_notes = ([len(notes) for notes in by_voice.values()]) msg = ('Number of notes per voice {} does not match number of ' 'notes per track {}'.format(n_voice_notes, n_track_notes)) self.assertEqual(n_voice_notes, n_track_notes, msg)
def test_midi_import_mode_1(self): parts = load_score_midi(self.tmpfile.name, part_voice_assign_mode=1) by_track = partition(itemgetter(0), self.notes_per_tr_ch.keys()) msg = ( 'Number of partgroups {} does not equal number of tracks {} while ' 'testing part_voice_assign_mode=0 in load_score_midi').format( len(parts), len(by_track)) self.assertEqual(len(parts), len(by_track), msg) for part_group, tr in zip(parts, by_track): msg = '{} should be a PartGroup instance but it is not' self.assertTrue(isinstance(part_group, score.PartGroup), msg) n_parts = len(part_group.children) n_channels = len(by_track[tr]) msg = ( 'PartGroup should have as many parts as there are ' 'channels in the corresponding track {}, but it has {}'.format( n_channels, n_parts)) self.assertEqual(n_parts, n_channels, msg) for part, tr_ch in zip(part_group.children, by_track[tr]): notes_in_track = self.notes_per_tr_ch[tr_ch] notes_in_part = len(part.notes) msg = 'Part should have {} notes but it has {}'.format( notes_in_track, notes_in_part) self.assertEqual(notes_in_part, notes_in_track)
def add_voices(part): by_staff = partition(attrgetter('staff'), part.notes_tied) max_voice = 0 for staff, notes in by_staff.items(): voices = estimate_voices(notes_to_notearray(notes)) assert len(voices) == len(notes) for n, voice in zip(notes, voices): assert voice > 0 n.voice = voice + max_voice n_next = n while n_next.tie_next: n_next = n_next.tie_next n_next.voice = voice + max_voice max_voice = np.max(voices) if any([n.voice is None for n in part.notes]): # Hack to add voices to notes not included in a staff! # not musically meaningful ev = 1 for n in part.notes: if n.voice is None: n.voice = max_voice + ev ev += 1
def add_staffs_v1(part): # assign staffs by first estimating voices jointly, then assigning voices to staffs notes = part.notes_tied # estimate voices in strictly monophonic way voices = estimate_voices(notes_to_notearray(notes), monophonic_voices=True) # for v, note in zip(voices, notes): # print(note.start.t, note.midi_pitch, v) # group notes by voice by_voice = partition(itemgetter(0), zip(voices, notes)) clefs = {} for v, vnotes in by_voice.items(): # voice numbers may be recycled throughout the piece, so we split by # time gap t_diffs = np.diff([n.start.t for _, n in vnotes]) t_threshold = np.inf # np.median(t_diffs)+1 note_groups = np.split([note for _, note in vnotes], np.where(t_diffs > t_threshold)[0] + 1) # for each note group estimate the clef for note_group in note_groups: if len(note_group) > 0: pitches = [n.midi_pitch for n in note_group] clef = tuple(estimate_clef_properties(pitches).items()) staff = clefs.setdefault(clef, len(clefs)) # print((note_group[0].start.t, note_group[-1].end.t), # (np.min(pitches), np.max(pitches), np.mean(pitches)), clef) for n in note_group: n.staff = staff n_tied = n.tie_next while n_tied: n_tied.staff = staff n_tied = n_tied.tie_next # re-order the staffs to a fixed order (see CLEF_ORDER), rather than by # first appearance clef_list = list((dict(clef), i) for clef, i in clefs.items()) clef_list.sort(key=lambda x: x[0].get('octave_change', 0)) clef_list.sort(key=lambda x: CLEF_ORDER.index(x[0].get('sign', 'G'))) staff_map = dict((j, i + 1) for i, (_, j) in enumerate(clef_list)) for n in notes: n.staff = staff_map[n.staff] for i, (clef_properties, _) in enumerate(clef_list): part.add(score.Clef(number=i + 1, **clef_properties), 0)
def test_midi_import_mode_3(self): parts = load_score_midi(self.tmpfile.name, part_voice_assign_mode=3) by_track = partition(itemgetter(0), self.notes_per_tr_ch.keys()) msg = ('Number of parts {} does not equal number of tracks {}'.format( len(parts), len(by_track))) self.assertEqual(len(parts), len(by_track), msg) for part, tr in zip(parts, by_track): msg = '{} should be a Part instance but it is not'.format(part) self.assertTrue(isinstance(part, score.Part), msg) n_track_notes = sum(self.notes_per_tr_ch[tr_ch] for tr_ch in by_track[tr]) part_notes = part.notes n_part_notes = len(part_notes) msg = 'Part should have {} notes but it has'.format( n_track_notes, n_part_notes) self.assertEqual(n_track_notes, n_part_notes, msg)
def test_midi_export_mode_2(self): m = self._export_and_read(mode=2) msg = ('Number of tracks {} does not equal 1 while ' 'testing part_voice_assign_mode=2 in save_score_midi'.format( len(m.tracks))) self.assertEqual(1, len(m.tracks), msg) n_channels_trg = len(self.parts_list) note_ch = note_channels(m.tracks[0]) by_channel = partition(lambda x: x, note_ch) channels = sorted(by_channel.keys()) n_channels = len(channels) msg = ('Number of channels {} does not equal {} while ' 'testing part_voice_assign_mode=2 in save_score_midi'.format( n_channels, n_channels_trg)) self.assertEqual(n_channels_trg, n_channels, msg) for part, ch in zip(self.parts_list, channels): n_notes_trg = len(part.notes_tied) n_notes = len(by_channel[ch]) msg = ('Number of notes in channel {} should be ' '{} while testing ' 'part_voice_assign_mode=2 in save_score_midi'.format( n_notes, n_notes_trg)) self.assertEqual(n_notes_trg, n_notes, msg)
def split_datasets_by_piece(datasets, test_size=0.2, valid_size=0.2): by_piece = partition(lambda d: '_'.join(d.name.split('_')[:-1]), datasets) pieces = list(by_piece.keys()) RNG.shuffle(pieces) n_test = max(1, int(np.round(test_size * len(pieces)))) n_valid = max(1, int(np.round(valid_size * len(pieces)))) n_train = len(pieces) - n_test - n_valid if n_train < 1: raise Exception('Not enough pieces to split datasets according ' 'to the specified test/validation proportions') test_pieces = pieces[:n_test] valid_pieces = pieces[n_test:n_test + n_valid] train_pieces = pieces[n_test + n_valid:] test_set = [d for pd in [by_piece[p] for p in test_pieces] for d in pd] valid_set = [d for pd in [by_piece[p] for p in valid_pieces] for d in pd] train_set = [d for pd in [by_piece[p] for p in train_pieces] for d in pd] return (ConcatDataset(train_set), ConcatDataset(valid_set), ConcatDataset(test_set))
def save_score_midi(parts, out, part_voice_assign_mode=0, velocity=64, anacrusis_behavior="shift"): """Write data from Part objects to a MIDI file Parameters ---------- parts : Part, PartGroup or list of these The musical score to be saved. out : str or file-like object Either a filename or a file-like object to write the MIDI data to. part_voice_assign_mode : {0, 1, 2, 3, 4, 5}, optional This keyword controls how part and voice information is associated to track and channel information in the MIDI file. The semantics of the modes is as follows: 0 Write one track for each Part, with channels assigned by voices 1 Write one track for each PartGroup, with channels assigned by Parts (voice info is lost) (There can be multiple levels of partgroups, I suggest using the highest level of partgroup/part) [note: this will e.g. lead to all strings into the same track] Each part not in a PartGroup will be assigned its own track 2 Write a single track with channels assigned by Part (voice info is lost) 3 Write one track per Part, and a single channel for all voices (voice info is lost) 4 Write a single track with a single channel (Part and voice info is lost) 5 Return one track per <Part, voice> combination, each track having a single channel. The default mode is 0. velocity : int, optional Default velocity for all MIDI notes. Defaults to 64. anacrusis_behavior : {"shift", "pad_bar"}, optional Strategy to deal with anacrusis. If "shift", all time points are shifted by the anacrusis (i.e., the first note starts at 0). If "pad_bar", the "incomplete" bar of the anacrusis is padded with silence. Defaults to 'shift'. """ ppq = get_ppq(parts) events = defaultdict(lambda: defaultdict(list)) meta_events = defaultdict(lambda: defaultdict(list)) event_keys = OrderedDict() tempos = {} quarter_maps = [part.quarter_map for part in score.iter_parts(parts)] first_time_point = min(qm(0) for qm in quarter_maps) ftp = 0 # Deal with anacrusis if first_time_point < 0: if anacrusis_behavior == "shift": ftp = first_time_point elif anacrusis_behavior == "pad_bar": time_signatures = [] for qm, part in zip(quarter_maps, score.iter_parts(parts)): ts_beats, ts_beat_type = part.time_signature_map(0) time_signatures.append((ts_beats, ts_beat_type, qm(0))) # sort ts according to time time_signatures.sort(key=lambda x: x[2]) ftp = -time_signatures[0][0] / (time_signatures[0][1] / 4) else: raise Exception( 'Invalid anacrusis_behavior value, must be one of ("shift", "pad_bar")' ) for qm, part in zip(quarter_maps, score.iter_parts(parts)): pg = get_partgroup(part) notes = part.notes_tied def to_ppq(t): # convert div times to new ppq return int(ppq * (qm(t) - ftp)) for tp in part.iter_all(score.Tempo): tempos[to_ppq(tp.start.t)] = MetaMessage( "set_tempo", tempo=tp.microseconds_per_quarter) for ts in part.iter_all(score.TimeSignature): meta_events[part][to_ppq(ts.start.t)].append( MetaMessage("time_signature", numerator=ts.beats, denominator=ts.beat_type)) for ks in part.iter_all(score.KeySignature): meta_events[part][to_ppq(ks.start.t)].append( MetaMessage("key_signature", key=ks.name)) for note in notes: # key is a tuple (part_group, part, voice) that will be # converted into a (track, channel) pair. key = (pg, part, note.voice) events[key][to_ppq(note.start.t)].append( Message("note_on", note=note.midi_pitch)) events[key][to_ppq(note.start.t + note.duration_tied)].append( Message("note_off", note=note.midi_pitch)) event_keys[key] = True tr_ch_map = map_to_track_channel(list(event_keys.keys()), part_voice_assign_mode) # replace original event keys (partgroup, part, voice) by (track, ch) keys: for key in list(events.keys()): evs_by_time = events[key] del events[key] tr, ch = tr_ch_map[key] for t, evs in evs_by_time.items(): events[tr][t].extend((ev.copy(channel=ch) for ev in evs)) # figure out in which tracks to replicate the time/key signatures of each part part_track_map = partition(lambda x: x[0][1], tr_ch_map.items()) for part, rest in part_track_map.items(): part_track_map[part] = set(x[1][0] for x in rest) # add the time/key sigs to their corresponding tracks for part, m_events in meta_events.items(): tracks = part_track_map[part] for tr in tracks: for t, me in m_events.items(): events[tr][t] = me + events[tr][t] n_tracks = max(tr for tr, _ in tr_ch_map.values()) + 1 tracks = [MidiTrack() for _ in range(n_tracks)] # tempo events are handled differently from key/time sigs because the have a # global effect. Instead of adding to each relevant track, like the key/time # sig events, we add them only to the first track for t, tp in tempos.items(): events[0][t].insert(0, tp) for tr, events_by_time in events.items(): t_prev = 0 for t in sorted(events_by_time.keys()): evs = events_by_time[t] delta = t - t_prev for ev in evs: tracks[tr].append(ev.copy(time=delta)) delta = 0 t_prev = t midi_type = 0 if n_tracks == 1 else 1 mf = MidiFile(type=midi_type, ticks_per_beat=ppq) for track in tracks: mf.tracks.append(track) if out: if hasattr(out, "write"): mf.save(file=out) else: mf.save(out)
def linearize_segment_contents(part, start, end, state): """ Determine the document order of events starting between `start` (inclusive) and `end` (exlusive). (notes, directions, divisions, time signatures). """ notes = part.iter_all(score.GenericNote, start=start, end=end, include_subclasses=True) notes_by_voice = partition(lambda n: n.voice or 0, notes) if len(notes_by_voice) == 0: # if there are no notes in this segment, we add a rest # NOTE: altering the part instance while exporting is bad! # rest = score.Rest() # part.add(start.t, rest, end.t) # notes_by_voice = {0: [rest]} notes_by_voice[None] = [] # make sure there is no polyphony within voices by assigning any violating # notes to a new (free) voice. remove_voice_polyphony(notes_by_voice) # fill_gaps_with_rests(notes_by_voice, start, end, part) # # redo # notes = part.iter_all(score.GenericNote, # start=start, end=end, # include_subclasses=True) # notes_by_voice = partition(lambda n: n.voice or 0, notes) voices_e = defaultdict(list) for voice in sorted(notes_by_voice.keys()): voice_notes = notes_by_voice[voice] # sort by pitch voice_notes.sort(key=lambda n: n.midi_pitch if hasattr(n, 'midi_pitch') else -1, reverse=True) # grace notes should precede other notes at the same onset voice_notes.sort(key=lambda n: not isinstance(n, score.GraceNote)) # voice_notes.sort(key=lambda n: -n.duration) voice_notes.sort(key=lambda n: n.start.t) for n in voice_notes: if isinstance(n, score.GraceNote): # check if it is the first in its sequence if not n.grace_prev: # if so we add the whole grace sequence at once to ensure # the correct order for m in n.iter_grace_seq(): note_e = do_note(m, end.t, part, voice, state['note_id_counter']) voices_e[voice].append(note_e) else: note_e = do_note(n, end.t, part, voice, state['note_id_counter']) voices_e[voice].append(note_e) add_chord_tags(voices_e[voice]) attributes_e = do_attributes(part, start, end) directions_e = do_directions(part, start, end, state['range_counter']) prints_e = do_prints(part, start, end) barline_e = do_barlines(part, start, end) other_e = attributes_e + directions_e + barline_e + prints_e contents = merge_measure_contents(voices_e, other_e, start.t) return contents
def save_score_midi(parts, out, part_voice_assign_mode=0, velocity=64): """Write data from Part objects to a MIDI file Parameters ---------- parts : Part, PartGroup or list of these The musical score to be saved. out : str or file-like object Either a filename or a file-like object to write the MIDI data to. part_voice_assign_mode : {0, 1, 2, 3, 4, 5}, optional This keyword controls how part and voice information is associated to track and channel information in the MIDI file. The semantics of the modes is as follows: 0 Write one track for each Part, with channels assigned by voices 1 Write one track for each PartGroup, with channels assigned by Parts (voice info is lost) (There can be multiple levels of partgroups, I suggest using the highest level of partgroup/part) [note: this will e.g. lead to all strings into the same track] Each part not in a PartGroup will be assigned its own track 2 Write a single track with channels assigned by Part (voice info is lost) 3 Write one track per Part, and a single channel for all voices (voice info is lost) 4 Write a single track with a single channel (Part and voice info is lost) 5 Return one track per <Part, voice> combination, each track having a single channel. velocity : int, optional Default velocity for all MIDI notes. """ ppq = get_ppq(parts) events = defaultdict(lambda: defaultdict(list)) meta_events = defaultdict(lambda: defaultdict(list)) event_keys = OrderedDict() tempos = {} for i, part in enumerate(score.iter_parts(parts)): pg = get_partgroup(part) notes = part.notes_tied qm = part.quarter_map q_offset = qm(part.first_point.t) def to_ppq(t): # convert div times to new ppq return int(ppq * qm(t)) for tp in part.iter_all(score.Tempo): tempos[to_ppq(tp.start.t)] = MetaMessage( 'set_tempo', tempo=tp.microseconds_per_quarter) for ts in part.iter_all(score.TimeSignature): meta_events[part][to_ppq(ts.start.t)].append( MetaMessage('time_signature', numerator=ts.beats, denominator=ts.beat_type)) for ks in part.iter_all(score.KeySignature): meta_events[part][to_ppq(ks.start.t)].append( MetaMessage('key_signature', key=ks.name)) for note in notes: # key is a tuple (part_group, part, voice) that will be converted into a (track, channel) pair. key = (pg, part, note.voice) events[key][to_ppq(note.start.t)].append( Message('note_on', note=note.midi_pitch)) events[key][to_ppq(note.end_tied.t)].append( Message('note_off', note=note.midi_pitch)) event_keys[key] = True tr_ch_map = map_to_track_channel(list(event_keys.keys()), part_voice_assign_mode) # replace original event keys (partgroup, part, voice) by (track, ch) keys: for key in list(events.keys()): evs_by_time = events[key] del events[key] tr, ch = tr_ch_map[key] for t, evs in evs_by_time.items(): events[tr][t].extend((ev.copy(channel=ch) for ev in evs)) # figure out in which tracks to replicate the time/key signatures of each part part_track_map = partition(lambda x: x[0][1], tr_ch_map.items()) for part, rest in part_track_map.items(): part_track_map[part] = set(x[1][0] for x in rest) # add the time/key sigs to their corresponding tracks for part, m_events in meta_events.items(): tracks = part_track_map[part] for tr in tracks: for t, me in m_events.items(): events[tr][t] = me + events[tr][t] n_tracks = max(tr for tr, _ in tr_ch_map.values()) + 1 tracks = [MidiTrack() for _ in range(n_tracks)] # tempo events are handled differently from key/time sigs because the have a # global effect. Instead of adding to each relevant track, like the key/time # sig events, we add them only to the first track track0_events = events[0] for t, tp in tempos.items(): events[0][t].insert(0, tp) for tr, events_by_time in events.items(): t_prev = 0 for t in sorted(events_by_time.keys()): evs = events_by_time[t] delta = t - t_prev for ev in evs: tracks[tr].append(ev.copy(time=delta)) delta = 0 t_prev = t midi_type = 0 if n_tracks == 1 else 1 mf = MidiFile(type=midi_type, ticks_per_beat=ppq) for track in tracks: mf.tracks.append(track) if out: if hasattr(out, 'write'): mf.save(file=out) else: mf.save(out)
def make_plot(fig, axs, targets, onsets=None, xticks=None, title=None, xlabel=None, start=None, end=None, keep_zoom=False): names = targets.dtype.names xlims = [] ylims = [] for ax in axs: if keep_zoom: xlims.append(list(ax.get_xlim())) ylims.append(list(ax.get_ylim())) ax.clear() n_targets = len(names) if onsets is None: x = np.arange(len(targets)) else: x = onsets w = len(x) / 30 h = n_targets if end is not None: idx = x < end x = x[idx] targets = targets[idx] if start is not None: idx = x >= start x = x[idx] targets = targets[idx] if n_targets == 1: axs = [axs] # fig.set_size_inches(w, h) if title: fig.suptitle(title) by_onset = partition(lambda ix: ix[1], enumerate(x)) for k, v in by_onset.items(): by_onset[k] = np.array([i for i, _ in v]) for i, name in enumerate(names): target = targets[name] targets[np.isnan(target)] = 0 axs[i].plot(x, target, '.', label=name) if xticks is not None: axs[i].set_xticks(xticks['t']) axs[i].set_xticklabels(xticks['label']) axs[i].xaxis.grid() tt = [] vv = [] for t, v in by_onset.items(): tt.append(t) vv.append(np.mean(target[v])) # axs[i].plot(tt, vv, label='{} (mean)'.format(name)) axs[i].plot(tt, vv) axs[i].legend(frameon=False, loc=2) if keep_zoom: axs[0].set_xlim(xlims[0]) for xlim, ylim, ax in zip(xlims, ylims, axs): ax.set_ylim(ylim) return fig, axs
def add_clefs(part): by_staff = partition(attrgetter('staff'), part.notes_tied) for staff, notes in by_staff.items(): part.add(score.Clef(number=staff, **estimate_clef_properties([n.midi_pitch for n in notes])), 0)