Skip to content

Instantly share code, notes, and snippets.

@X-Raym
Forked from kastnerkyle/bach_parse_example.py
Created September 28, 2017 20:46
Show Gist options
  • Save X-Raym/fb1f21ac34a80ee9b57efb5d9d5e184e to your computer and use it in GitHub Desktop.
Save X-Raym/fb1f21ac34a80ee9b57efb5d9d5e184e 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.flat.notesAndRests:
if n.isRest:
part.append(0)
else:
part.append(pitch.Pitch(n))
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