Skip to content

Instantly share code, notes, and snippets.

@duhaime
Last active September 28, 2021 14:59
Show Gist options
  • Save duhaime/b8947b22feb0cf2d7ab8c10daeb3bf9f to your computer and use it in GitHub Desktop.
Save duhaime/b8947b22feb0cf2d7ab8c10daeb3bf9f to your computer and use it in GitHub Desktop.
MIDI
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
Display the source blob
Display the rendered blob
Raw
{
"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