Skip to content

Instantly share code, notes, and snippets.

@kastnerkyle
Last active December 26, 2021 07:54
Show Gist options
  • Save kastnerkyle/682979133e732c4cd3ab9467e5c70273 to your computer and use it in GitHub Desktop.
Save kastnerkyle/682979133e732c4cd3ab9467e5c70273 to your computer and use it in GitHub Desktop.
Example of getting Bach from MusicXML using music21
"""
Example of iterating Bach Chorales and getting individual voice parts
In this case, want specifically 4 voice pieces only
Also transpose to key of C (major or minor depending on piece)
Also shows how to write out all the xml as midi
"""
# Author: Kyle Kastner
# License: BSD 3-Clause
# Based on StackOverflow answer
# http://stackoverflow.com/questions/36647054/music21-getting-all-notes-with-durations
# midi writing modified from tests inside music21
from music21 import corpus, interval, pitch
import time
import numpy as np
import os
def write_midi(pitch_block, duration_block, outfile="out.mid",
qpm_multiplier=1024, tempo_multiplier=1.0):
# Assumes any element with
from music21.midi import MidiTrack, MidiFile, MidiEvent, DeltaTime
# duration, pitch, velocity
qpm_mult = qpm_multiplier
all_mt = []
for i in range(pitch_block.shape[0]):
mt = MidiTrack(1)
t = 0
t_last = 0
pitch_slice = pitch_block[i, :]
duration_slice = duration_block[i, :]
beat_slice = list((qpm_mult * duration_slice).astype("int32"))
pitch_slice = list(pitch_slice.astype("int32"))
for d, p in zip(beat_slice, pitch_slice):
if (p == -1) or (d == -1):
# bypass
continue
dt = DeltaTime(mt)
dt.time = t - t_last
mt.events.append(dt)
me = MidiEvent(mt)
me.type = "NOTE_ON"
me.channel = 1
me.time = None
me.pitch = p
me.velocity = 90
mt.events.append(me)
# add note off / velocity zero message
dt = DeltaTime(mt)
dt.time = d
# add to track events
mt.events.append(dt)
me = MidiEvent(mt)
me.type = "NOTE_ON"
me.channel = 1
me.time = None
me.pitch = p
me.velocity = 0
mt.events.append(me)
t_last = t + d
t += d
# add end of track
dt = DeltaTime(mt)
dt.time = 0
mt.events.append(dt)
me = MidiEvent(mt)
me.type = "END_OF_TRACK"
me.channel = 1
me.data = ''
mt.events.append(me)
all_mt.append(mt)
mf = MidiFile()
mf.ticksPerQuarterNote = int(tempo_multiplier * qpm_mult)
for mt in all_mt:
mf.tracks.append(mt)
mf.open(outfile, 'wb')
mf.write()
mf.close()
start = time.time()
all_bach_paths = corpus.getComposer('bach')
print("Total number of Bach pieces to process from music21: %i" % len(all_bach_paths))
skipped = 0
processed = 0
n_major = 0
n_minor = 0
all_major = []
all_minor = []
for it, p_bach in enumerate(all_bach_paths):
if "riemenschneider" in p_bach:
# skip certain files we don't care about
skipped += 1
continue
p = corpus.parse(p_bach)
if len(p.parts) != 4:
print("Skipping file %i, %s due to undesired voice count..." % (it, p_bach))
skipped += 1
continue
print("Processing %i, %s ..." % (it, p_bach))
k = p.analyze('key')
print("Original key: %s" % k)
i = interval.Interval(k.tonic, pitch.Pitch('C'))
p = p.transpose(i)
k = p.analyze('key')
print("Transposed key: %s" % k)
if 'major' in k.name:
n_major += 1
elif 'minor' in k.name:
n_minor += 1
else:
raise ValueError('Unknown key %s' % k.name)
try:
parts = []
parts_times = []
for i, pi in enumerate(p.parts):
part = []
part_time = []
for n in pi.stream().flat.notesAndRests:
if n.isRest:
part.append(0)
else:
part.append(n.midi)
part_time.append(n.duration.quarterLength)
parts.append(part)
parts_times.append(part_time)
# Create a "block" of events and times
cumulative_times = map(lambda x: list(np.cumsum(x)), parts_times)
event_points = sorted(list(set(sum(cumulative_times, []))))
maxlen = max(map(len, cumulative_times))
# -1 marks invalid / unused
part_block = np.zeros((len(p.parts), maxlen)).astype("int32") - 1
ctime_block = np.zeros((len(p.parts), maxlen)).astype("float32") - 1
time_block = np.zeros((len(p.parts), maxlen)).astype("float32") - 1
# create numpy array for easier indexing
for i in range(len(parts)):
part_block[i, :len(parts[i])] = parts[i]
ctime_block[i, :len(cumulative_times[i])] = cumulative_times[i]
time_block[i, :len(parts_times[i])] = parts_times[i]
event_block = np.zeros((len(p.parts), len(event_points))) - 1
etime_block = np.zeros((len(p.parts), len(event_points))) - 1
for i, e in enumerate(event_points):
idx = zip(*np.where(ctime_block == e))
for ix in idx:
event_block[ix[0], i] = part_block[ix[0], ix[1]]
etime_block[ix[0], i] = time_block[ix[0], ix[1]]
bach_name = "_".join(p_bach.split(os.sep)[-1].split(".")[:-1])
midi_outfile = bach_name + ".mid"
write_midi(event_block, etime_block,
outfile="midifiles/" + midi_outfile,
tempo_multiplier=1.0)
# Grouping
processed += 1
except AttributeError:
skipped += 1
# Edge case for Chord error? Should be flat container but some piece is different
continue
stop = time.time()
print("Total skipped count: %i" % skipped)
print("Total processed count: %i" % processed)
print("Total major: %i" % n_major)
print("Total minor: %i" % n_minor)
print("Total processing time (seconds): %f" % (stop - start))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment