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.

Revisions

  1. duhaime revised this gist Sep 28, 2021. 1 changed file with 99 additions and 0 deletions.
    99 changes: 99 additions & 0 deletions functions.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,99 @@
    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
  2. duhaime revised this gist Sep 24, 2021. 2 changed files with 161 additions and 58 deletions.
    161 changes: 161 additions & 0 deletions midi.ipynb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,161 @@
    {
    "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
    }
    58 changes: 0 additions & 58 deletions midi.py
    Original file line number Diff line number Diff line change
    @@ -1,58 +0,0 @@
    from fractions import Fraction
    import music21

    path = 'data/exodus/ambrosia.midi'
    score = music21.converter.parse(path, forceSource=True, quantizePost=False)

    # validate time signature is 4/4
    time_signature = score.parts[0].timeSignature
    if time_signature: assert time_signature.ratioString == '4/4'

    # validate mode is major / minor
    key = score.analyze('key')
    assert key.mode == 'minor' or key.mode == 'major'

    # transpose to c major / a 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:
    if not isinstance(n, music21.chord.Chord):
    note = n.pitch.midi
    duration = n.duration.type # n.duration.quarterLength
    offset = n.offset
    delta = offset - last_offset
    if delta: s += 'wait_{} '.format(delta)
    s += 'note_{}_{} '.format(note, duration)
    last_offset = offset

    # convert string back to midi
    d = {
    '32nd': 1/32,
    '16th': 1/16,
    'eighth': 1/8,
    'quarter': 1/4,
    'half': 1/2,
    'whole': 1,
    }
    time = 0
    stream = music21.stream.Stream()
    for i in s.split():
    if i.startswith('note'):
    note, duration = i.lstrip('note_').split('_')
    n = music21.note.Note(int(note))
    n.duration.type = duration
    stream.insertIntoNoteOrChord(time, n)
    elif i.startswith('wait'):
    duration = float(Fraction(i.lstrip('wait_')))
    time += duration
    else:
    print('did not expect', i)
    stream.write('midi', fp='yeet.midi')

    # play the result
    stream.show('midi')
  3. duhaime revised this gist Sep 23, 2021. 1 changed file with 1 addition and 2 deletions.
    3 changes: 1 addition & 2 deletions midi.py
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,4 @@
    from fractions import Fraction
    import music21

    path = 'data/exodus/ambrosia.midi'
    @@ -30,8 +31,6 @@
    last_offset = offset

    # convert string back to midi
    from fractions import Fraction

    d = {
    '32nd': 1/32,
    '16th': 1/16,
  4. duhaime created this gist Sep 23, 2021.
    59 changes: 59 additions & 0 deletions midi.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,59 @@
    import music21

    path = 'data/exodus/ambrosia.midi'
    score = music21.converter.parse(path, forceSource=True, quantizePost=False)

    # validate time signature is 4/4
    time_signature = score.parts[0].timeSignature
    if time_signature: assert time_signature.ratioString == '4/4'

    # validate mode is major / minor
    key = score.analyze('key')
    assert key.mode == 'minor' or key.mode == 'major'

    # transpose to c major / a 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:
    if not isinstance(n, music21.chord.Chord):
    note = n.pitch.midi
    duration = n.duration.type # n.duration.quarterLength
    offset = n.offset
    delta = offset - last_offset
    if delta: s += 'wait_{} '.format(delta)
    s += 'note_{}_{} '.format(note, duration)
    last_offset = offset

    # convert string back to midi
    from fractions import Fraction

    d = {
    '32nd': 1/32,
    '16th': 1/16,
    'eighth': 1/8,
    'quarter': 1/4,
    'half': 1/2,
    'whole': 1,
    }
    time = 0
    stream = music21.stream.Stream()
    for i in s.split():
    if i.startswith('note'):
    note, duration = i.lstrip('note_').split('_')
    n = music21.note.Note(int(note))
    n.duration.type = duration
    stream.insertIntoNoteOrChord(time, n)
    elif i.startswith('wait'):
    duration = float(Fraction(i.lstrip('wait_')))
    time += duration
    else:
    print('did not expect', i)
    stream.write('midi', fp='yeet.midi')

    # play the result
    stream.show('midi')