Finding a compact representation of melody and rhythm.

First we'll create some functions to turn single notes, chords and rests into these Triplets.

# test
from testing import test_eq

quarter = duration.Duration("quarter")


def loud(n: note.Note, velocity=120) -> note.Note:
    n.volume = volume.Volume(velocity=velocity)
    return n


test_eq(
    ("C3", "quarter", 120), _parse_single_note(loud(note.Note("C3", duration=quarter)))
)
test_eq(None, _parse_single_note(note.Note("C3", duration=duration.Duration(34.0))))

test_eq(
    ("1.3.5", "quarter", 80),
    _parse_chord(loud(chord.Chord([1, 3, 5], duration=quarter), velocity=80)),
)
test_eq(None, _parse_chord(chord.Chord([1, 3, 5], duration=duration.Duration(0.0))))

test_eq(("R", "quarter", 0), _parse_rest(note.Rest(duration=quarter)))
test_eq(None, _parse_rest(note.Rest(duration=duration.Duration(0.0))))

As a normalization step, we need to transpose all the music to the same key, so we'll choose C for convenience.

# test
from testing import test_eq

s = converter.parse("data/ff4-main.mid")
originalKey = s.analyze("key")

transposed, mode = _transpose_to_C(s)
transposedKey = transposed.analyze("key")

test_eq(key.Key("CM"), transposedKey)
test_eq(originalKey.mode, mode)
# test
from testing import test_eq

s = converter.parse("data/rugrats.mid")
row = _parse_part(s.parts[0].recurse(), "major")

test_eq("piano", row["instrument"])
test_eq(102.0, row["bpm"])
test_eq("major", row["mode"])
test_eq("4/4", row["time_signature"])
test_eq(
    [("R", "eighth", 0), ("R", "breve", 0), ("C3", "eighth", 103)], row["notes"][0:3]
)

parse_midi_file[source]

parse_midi_file(file:str)

Attempts to parse a midi file into a Dataframe. Returns a Dataframe or None, together with the number of notes processed.

row_to_triplets[source]

row_to_triplets(df:DataFrame, row_index:int)

Takes a DataFrame at a specific row and turns that into a triplet.

!ls data
ff4-airship.mid ff4-main.mid    ff4-town.mid    midi.tar.gz     rugrats.mid
# test
from testing import test_eq, path

df = parse_midi_file(path("data/ff4-main.mid"))

test_eq(1, len(df))
test_eq("piano", df.get("instrument")[0])

triplets = row_to_triplets(df, 0)

test_eq(
    [("E5", "eighth", 110), ("B4", "eighth", 110), ("G4", "eighth", 110)], triplets[0:3]
)
test_eq(1, len(df))

df = parse_midi_file(path("data/rugrats.mid"))
test_eq(1, len(df))

Turning triplets back into MIDI streams

Another interesting thing is to be able to go from triplets to MIDI streams, display their scores and even an audible IPython widget to play the audio.

But first of all, some utilities to turn these triplets into music21 elements:

triplet_to_note[source]

triplet_to_note(triplet:Tuple[str, float, int])

triplets_to_stream[source]

triplets_to_stream(triplets:Collection[Tuple[str, float, int]])

write_midi[source]

write_midi(stream, name)

Now let's try to visualize and listen to these streams.

If you're running Mac or Linux, uncomment the right brew or apt call:

# skip
# hide

#!brew install fluidsynth
#!apt install fluidsynth
#!wget ftp://ftp.osuosl.org/pub/musescore/soundfont/MuseScore_General/MuseScore_General.sf3
#!pip install midi2audio
--2019-12-08 01:55:32--  ftp://ftp.osuosl.org/pub/musescore/soundfont/MuseScore_General/MuseScore_General.sf3
           => ‘MuseScore_General.sf3’
Resolving ftp.osuosl.org (ftp.osuosl.org)... 140.211.166.134, 64.50.236.52, 64.50.233.100
Connecting to ftp.osuosl.org (ftp.osuosl.org)|140.211.166.134|:21... connected.
Logging in as anonymous ... Logged in!
==> SYST ... done.    ==> PWD ... done.
==> TYPE I ... done.  ==> CWD (1) /pub/musescore/soundfont/MuseScore_General ... done.
==> SIZE MuseScore_General.sf3 ... 39893918
==> PASV ... done.    ==> RETR MuseScore_General.sf3 ... done.
Length: 39893918 (38M) (unauthoritative)

MuseScore_General.s 100%[===================>]  38.04M  4.47MB/s    in 12s     

2019-12-08 01:55:48 (3.23 MB/s) - ‘MuseScore_General.sf3’ saved [39893918]

Requirement already satisfied: midi2audio in /Users/txus/Code/codegram/neuralmusic/env/lib/python3.7/site-packages (0.1.1)
# skip
# hide

%load_ext music21.ipython21.ipExtension

import json, random

from IPython.core.display import display, HTML, Javascript
from IPython.display import Audio

from midi2audio import FluidSynth


def show_score(score):
    xml = open(score.write("musicxml")).read()
    show_xml(xml)


def show_xml(xml):
    DIV_ID = "OSMD-div-" + str(random.randint(0, 1000000))
    # print("DIV_ID", DIV_ID)
    msg = "loading OpenSheetMusicDisplay"
    msg = ""
    display(
        HTML('<div style="background: white" id="' + DIV_ID + '">{}</div>'.format(msg))
    )

    # print('xml length:', len(xml))

    script = """
    console.log("loadOSMD()");
    function loadOSMD() { 
        return new Promise(function(resolve, reject){

            if (window.opensheetmusicdisplay) {
                console.log("already loaded")
                return resolve(window.opensheetmusicdisplay)
            }
            console.log("loading osmd for the first time")
            // OSMD script has a 'define' call which conflicts with requirejs
            var _define = window.define // save the define object 
            window.define = undefined // now the loaded script will ignore requirejs
            var s = document.createElement( 'script' );
            s.setAttribute( 'src', "https://cdn.jsdelivr.net/npm/opensheetmusicdisplay@0.3.1/build/opensheetmusicdisplay.min.js" );
            //s.setAttribute( 'src', "/custom/opensheetmusicdisplay.js" );
            s.onload=function(){
                window.define = _define
                console.log("loaded OSMD for the first time",opensheetmusicdisplay)
                resolve(opensheetmusicdisplay);
            };
            document.body.appendChild( s ); // browser will try to load the new script tag
        }) 
    }
    loadOSMD().then((OSMD)=>{
        console.log("loaded OSMD",OSMD)
        var div_id = "";
            console.log(div_id)
        window.openSheetMusicDisplay = new OSMD.OpenSheetMusicDisplay(div_id);
        openSheetMusicDisplay
            .load()
            .then(
              function() {
                console.log("rendering data")
                openSheetMusicDisplay.render();
              }
            );
    })
    """.replace(
        "", DIV_ID
    ).replace(
        "", json.dumps(xml)
    )
    display(Javascript(script))
    return DIV_ID


def view_song(triplets, name="song"):
    stream = triplets_to_stream(triplets)
    write_midi(stream, f"{name}")

    FluidSynth("MuseScore_General.sf3").midi_to_audio(f"{name}.mid", f"{name}.wav")
    show_score(stream)
    return Audio(f"{name}.wav")
# skip
triplets = row_to_triplets(df, 0)
view_song(triplets)