Last active
September 28, 2021 14:59
-
-
Save duhaime/b8947b22feb0cf2d7ab8c10daeb3bf9f to your computer and use it in GitHub Desktop.
MIDI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from fractions import Fraction | |
import music21 | |
import glob | |
# remove leading / trailing whitespace | |
music21.defaults.ticksAtStart = 0 | |
notes = set([12*j + i for i in [0,2,4,5,7,9,11] for j in [0,1,2,3,4,5,6,7,8,9,10] ]) | |
def constrain_pitch(val): | |
# keep the pitch in C major, no incidentals | |
while val not in notes: | |
val = int(val) + 1 | |
if val > max(notes): | |
val - 2 | |
return val | |
def get_score(path, **kwargs): | |
# return a score object | |
if True: | |
s = music21.converter.parse(path, | |
forceSource=False, | |
quantizePost=False, | |
quarterLengthDivisors=(4,3), # smaller numbers mean fewer different note durations | |
).stripTies(inPlace=True) | |
else: | |
m = music21.midi.MidiFile() | |
m.open(path) | |
m.read() | |
m.close() | |
if kwargs.get('remove_percussion', True): | |
tracks = [t for t in m.tracks if not any([e.channel == 10 for e in t.events])] | |
else: | |
tracks = m.tracks | |
s = music21.stream.Score() | |
music21.midi.translate.midiTracksToStreams(tracks, | |
inputM21=s, | |
forceSource=False, | |
quantizePost=False, | |
ticksPerQuarter=m.ticksPerQuarterNote, | |
quarterLengthDivisors=(4,3), | |
) | |
return s | |
def midi_to_string(path, **kwargs): | |
d = { | |
'32nd': 1/32, | |
'16th': 1/16, | |
'eighth': 1/8, | |
'quarter': 1/4, | |
'half': 1/2, | |
'whole': 1, | |
} | |
score = get_score(path, **kwargs) | |
# transpose to c major / a minor | |
key = score.analyze('key') | |
assert key.mode in ['major', 'minor'] | |
interval = 60 - key.tonic.midi | |
if key.mode == 'minor': interval -= 3 | |
score = score.transpose(interval) | |
# convert midi to string | |
s = '' | |
last_offset = 0 | |
for n in score.flat.notes: | |
# get the note/chord duration | |
duration = n.duration.components[0].type # n.duration.type returns complex for tied notes | |
if not duration or duration == 'zero': continue | |
duration = float(Fraction(d.get(duration, duration))) | |
# get the offset since the previous note/chord | |
delta = float(Fraction(n.offset - last_offset)) | |
if delta: s += 'w{} '.format(round(delta, 4)) | |
# add the note/chord to the string | |
for i in [n] if isinstance(n, music21.note.Note) else n.notes: | |
try: | |
pitch = i.pitch.midi | |
if kwargs.get('constrain'): pitch = constrain_pitch(pitch) | |
s += 'n{}_{} '.format(pitch, round(duration, 4)) | |
except: | |
print(' * could not parse note', i) | |
last_offset = n.offset | |
return s | |
def string_to_midi(s): | |
# convert string back to midi | |
time = 0 | |
stream = music21.stream.Stream() | |
for i in s.split(): | |
if i.startswith('n'): | |
note, duration = i.lstrip('n').split('_') | |
n = music21.note.Note(int(note)) | |
n.duration.quarterLength = float(duration) * 4 | |
stream.insertIntoNoteOrChord(time, n) | |
elif i.startswith('w'): | |
duration = float(Fraction(i.lstrip('w'))) | |
time += duration | |
else: | |
print('did not expect', i) | |
return stream |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"cells": [ | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"id": "7592aa63", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"%load_ext autoreload\n", | |
"%autoreload 2\n", | |
"\n", | |
"from fractions import Fraction\n", | |
"import music21\n", | |
"import glob\n", | |
"\n", | |
"def midi_to_string(path):\n", | |
" d = {\n", | |
" '32nd': 1/32,\n", | |
" '16th': 1/16,\n", | |
" 'eighth': 1/8,\n", | |
" 'quarter': 1/4,\n", | |
" 'half': 1/2,\n", | |
" 'whole': 1,\n", | |
" }\n", | |
" score = music21.converter.parse(path, forceSource=False, quantizePost=False)\n", | |
" # validate time signature is 4/4\n", | |
" time_signature = score.parts[0].timeSignature\n", | |
" if time_signature: assert time_signature.ratioString == '4/4'\n", | |
" # validate mode is major / minor\n", | |
" key = score.analyze('key')\n", | |
" assert key.mode == 'minor' or key.mode == 'major'\n", | |
" # transpose to c major / a minor\n", | |
" interval = 60 - key.tonic.midi\n", | |
" if key.mode == 'minor': interval -= 3\n", | |
" score = score.transpose(interval)\n", | |
" # convert midi to string\n", | |
" s = ''\n", | |
" last_offset = 0\n", | |
" for n in score.flat.notes:\n", | |
" if not isinstance(n, music21.chord.Chord):\n", | |
" note = n.pitch.midi\n", | |
" duration = n.duration.components[0].type # n.duration.type returns complex for tied notes\n", | |
" duration = float(Fraction(d.get(duration, duration)))\n", | |
" offset = n.offset\n", | |
" delta = float(Fraction(offset - last_offset))\n", | |
" if delta: s += 'w{} '.format(round(delta, 4))\n", | |
" s += 'n{}_{} '.format(note, round(duration, 4))\n", | |
" last_offset = offset\n", | |
" return s\n", | |
"\n", | |
"def string_to_midi(s):\n", | |
" # convert string back to midi\n", | |
" time = 0\n", | |
" stream = music21.stream.Stream()\n", | |
" for i in s.split():\n", | |
" if i.startswith('n'):\n", | |
" note, duration = i.lstrip('n').split('_')\n", | |
" n = music21.note.Note(int(note))\n", | |
" n.duration.quarterLength = float(duration) * 4\n", | |
" stream.insertIntoNoteOrChord(time, n)\n", | |
" elif i.startswith('w'):\n", | |
" duration = float(Fraction(i.lstrip('w')))\n", | |
" time += duration\n", | |
" else:\n", | |
" print('did not expect', i)\n", | |
" return stream" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"id": "6e8add3b", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"s = midi_to_string('data/exodus/ambrosia.midi')" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"id": "4a55cde4", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"midi = string_to_midi(s)\n", | |
"midi.write('midi', fp='converted.midi')" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"id": "8c66d809", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"from collections import defaultdict\n", | |
"from nltk import ngrams\n", | |
"import random\n", | |
"\n", | |
"def markov(s, sequence_length=4, output_length=250):\n", | |
" # train markov model\n", | |
" d = defaultdict(list)\n", | |
" tokens = list(ngrams(s.split(), sequence_length))\n", | |
" for idx, i in enumerate(tokens[:-1]):\n", | |
" d[i].append(tokens[idx+1])\n", | |
" # sample from markov model\n", | |
" generated = [random.choice(tokens)]\n", | |
" while len(generated) < output_length:\n", | |
" generated.append(random.choice(d[generated[-1]]))\n", | |
" # format the result into a string\n", | |
" return ' '.join([' '.join(i) for i in generated])\n", | |
"\n", | |
"generated = markov(s)\n", | |
"midi = string_to_midi(generated)" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"id": "11f869f4", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"midi.write('midi', fp='generated.midi')" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"id": "646db079", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"s" | |
] | |
} | |
], | |
"metadata": { | |
"kernelspec": { | |
"display_name": "Python 3 (ipykernel)", | |
"language": "python", | |
"name": "python3" | |
}, | |
"language_info": { | |
"codemirror_mode": { | |
"name": "ipython", | |
"version": 3 | |
}, | |
"file_extension": ".py", | |
"mimetype": "text/x-python", | |
"name": "python", | |
"nbconvert_exporter": "python", | |
"pygments_lexer": "ipython3", | |
"version": "3.7.11" | |
} | |
}, | |
"nbformat": 4, | |
"nbformat_minor": 5 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment