fuzzix dot org

18th century procedural music with RtMidi

5 Sep 2021

Mozart's dice is a game which maps dice rolls to small sections of music, which may then be pieced together to form an endlessly mutating composition. The "Minuet" board consists of 16 columns and 11 rows, numbered two to twelve. To compose each section, we work across the columms left-to-right, throwing a pair of dice each time to select the row. The number in the cell at the resulting column and row position corresponds to a bar of music.

RtMidi is a cross-platform library and API for realtime MIDI input and output. How might we play an endless game of Mozart's dice and present the game's output as a MIDI instrument?

Much of the work of digitising the game has been taken care of. Steev's MIDI Library includes the game boards in its source, as well as MIDI files for each square of the board. As I don't have much confidence as a C programmer, let's make use of a Perl binding to RtMidi. We can schedule events with IO::Async.

my $device = MIDI::RtMidi::FFI::Device->new;
$device->open_port_by_name( qr/loopmidi/i );

my $loop = IO::Async::Loop->new;

LoopMIDI is a "virtual loopback MIDI cable" for Windows, allowing you to connect pieces of MIDI-capable software together. Linux and MacOS should allow connecting directly to your synth software by name.

The Minuet game board may be brought over more-or-less directly from midilib:

my $MinuetTable = [
 96,  22, 141,  41, 105, 122,  11,  30,  70, 121,  26,   9, 112,  49, 109,  14,
 32,   6, 128,  63, 146,  46, 134,  81, 117,  39, 126,  56, 174,  18, 116,  83,
 ...
 54, 130,  10, 103,  28,  37, 106,   5,  35,  20, 108,  92,  12, 124,  44, 131,
];

...which can then be expanded to MIDI::Opus instances containing the music bars from the corresponding MIDI files:

my $MinuetMids = [
    map {
        MIDI::Opus->new(
            { 'from_file' => "$THISDIR/midilib/src/MIDIFiles/M$_.MID" } )
    } $MinuetTable->@*
];

We now have a game board composed of MIDI data. As the board is stored in a 1-dimensional array, a little work is required to address rows and columns. The index in the array may be found at row x total columns + column. We can build a track selection function which iterates through columns, selecting a random row for each one:

sub rand_track {
    state $col = 0;
    $col %= 16;
    my $row = int( rand(11) );
    my $selection = ( $row * 16 ) + $col;
    $col++;
    $MinuetMids->[ $selection ]->tracks_r->[0];
}

We now have the board and the dice rolls. The next step is scheduling real time note playback events, which means extracting timing data from our MIDI tracks. Time to step into the wonderful world of MIDI time:

my $ppq = ppq( $MinuetMids->[0] );
my $tempo = tempo( $MinuetMids->[0]->tracks_r->[0] );
my $signature = signature( $MinuetMids->[0]->tracks_r->[0] );
my $tick = ( ( $tempo / $ppq ) / 1_000_000 );
my $bar = ( $tempo * $signature->{numerator} ) / 1_000_000;

As the Mozart's dice game has a single tempo, we can pull this info from the first (or any) of the Minuet board's MIDI tracks.

PPQ is "pulses per quarter", which is the number of MIDI "ticks" or events in a quarter note. A common value for PPQ is 480.

Tempo (or MicroTempo) is the number of microseconds per quarter (or beat). A minute is 60,000,000 microseconds long. A tempo of 600,000 corresponds to 100 beats per minute (BPM).

Signature is the MIDI's time signature, or beats per bar, which is 3/2 in our case. That is, three half-note beats per bar.

Tick is the MIDI tick time in seconds. This is derived from ticks per beat and microseconds per beat. Our tick is 0.00125s.

Bar is the length of a single bar which is beats per minute * number of beats, 1.8s in our case. With the bar time, we can now start scheduling dice rolls:

my $timer = IO::Async::Timer::Periodic->new(
    interval => $bar,
    reschedule => 'hard',
    on_tick => sub {
        schedule_notes( rand_track, $tick );
    }
);
$loop->add( $timer->start );

The schedule_notes function receives a random track. Events in the track are filtered to just include note on/off. For each event, a timer is scheduled to send the event on the midi device:

sub schedule_notes( $track, $tick ) {
    my $time;
    my @events = grep { $_->[0] =~ /^note/ } $track->events;
    my @timers = map {
        my $event = $_->[0];
        my $dtime = $_->[1];
        my $note  = $_->[-2];
        my $vel   = $_->[-1];
        $time += $dtime;
        IO::Async::Timer::Countdown->new(
            delay => $time * $tick,
            on_expire => sub {
                $device->send_event( $event => $note, $vel );
            }
        );
    } @events;
    $loop->add( $_->start ) for ( @timers );
}

For each event, we extract the event type (note on or off), the "delta" time, plus note and velocity. Velocity is how hard the keyboard key was hit.

The event's real time is accumulated from all the delta times seen in the track so far. As delta time is in ticks, we multiply it by the seconds-per-tick value in $tick to get the event time in seconds.

Now that we have scheduled dice roll and music events, the last step is to run the event loop "forever":

$loop->run;

There are some additional functions not described here to extract MIDI data and such — full source is on GitHub : mozart.pl. Finally, you can hear a sample of the music produced below:

I am not a musician, nor am I much of a programmer. Please forgive any glaring mistakes in the above.