A big thanks to Colin who helped review this article and made it human readable!

There are designers, and then there is everyone else. I am most definitely not a designer, but then again, Dynamo was not built for just designers—it was built for users and for uses that we knew we couldn’t fully anticipate. Dynamo is an extremely flexible system, and with each new feature introduction comes new possibilities. This write-up represents my experiment with one such new functionality in Dynamo: periodic evaluation (we are finalizing the design, soon as all the details are ironed out, this new feature will be enabled in the daily build).

What better way to wish you Happy Holidays (and to test out my project) than to make Dynamo sing to you?!

Getting the Data

A MIDI file (short for “Musical Instrument Digital Interface”) contains information including tracks with MIDI commands. These commands are sent to a speaker during playback, resulting in the music that we hear.

In order to pass these MIDI commands through Dynamo to a MIDI output device, we must first have a way to load them into a Dynamo graph. For this exercise, I chose to use Microsoft Excel. Why not, right?

This is where the C# MIDI Toolkit comes in handy. This open source toolkit is capable of loading any MIDI file. With a few tweaks, I was able to write track information (MIDI commands) out to a CSV format. Each command is written out to the CSV file in a tick-command value pair, with each tick, in milliseconds, marking the absolute time at which a MIDI command should be sent to the output. This process results in two Excel worksheets, one for each of the two tracks in the MIDI file I used. (Column headers are added for illustration only.)

excel-white-christmas-track

The following partial Dynamo graph imports one of these Excel worksheets into Dynamo. The File.FromPath node converts the file path, which is just a string, into a file object to be “watched” by Dynamo for changes. Then I chose the sheet name, called “Track0”, and I read the data with Excel.ReadFromFile. Then List.Transpose is used to flip row-oriented data into column-oriented data for downstream consumption (we would like to have ticks and commands in separate lists).

Reading Excel file with Excel node

Tempo and Playback Speed

The playback speed of a MIDI file is determined by both its tempo and time division values.

Tempo is measured in microseconds per quarter note. This value is specified by “Set Tempo” meta-event (an event that is not sent or received over a MIDI port), and in our case it has a value of 600,000. This means a quarter note lasts 600,000 microseconds or 600 milliseconds.

The time division is measured in pulses per quarter note (PPQN), which specifies the number of ticks that correspond to a quarter note. For the MIDI file in this experiment, we will use 192 ticks per quarter note.

With these two pieces of information, it is possible to translate an absolute tick value for any given MIDI command into real world time. Consider the following highlighted MIDI command and its absolute tick value:

MIDI Commands vs. Ticks

The MIDI command in the second column should be sent when the absolute tick count reaches 384. Given that a quarter note = 192 ticks and lasts for 600 milliseconds, the command should be sent just as 384 ticks = 2 quarter notes = 1200 milliseconds has elapsed. Or, more generally, in case you like to see the math:

Time Conversion  Equation

Next in the Dynamo graph, the absolute tick values imported from the Excel worksheet are translated into milliseconds since the playback is based on real world time. The following graph shows how:

Time Conversion

MIDI Playback

We do not need more than a very primitive way to render our MIDI data for this experiment—all we need is a handful of Win32 API calls and minimal time management code around them, and voila! We have a bare-bones MIDI player. The following sections describe these classes. (Feel free to skip ahead if you so choose!)

Track Class

This class serves as a container for the actual data for a single MIDI track. A node of this type is constructed by taking two input arrays of the same size: time values in milliseconds and MIDI commands.

public class Track
{
    private readonly List milliseconds;
    private readonly List commands;

    private Track(IEnumerable milliseconds, IEnumerable commands)
    {
        this.milliseconds = new List(milliseconds);
        this.commands = new List(commands);
    }

    public static Track FromTrackData(
        double[] milliseconds, double[] commands)
    {
        return new Track(milliseconds, commands);
    }

    internal void FetchCommands(double millisecond, ICollection fetched)
    {
        while (milliseconds.Count > 0)
        {
            if (milliseconds[0] >= millisecond)
                break;

            fetched.Add(commands[0]);
            milliseconds.RemoveAt(0);
            commands.RemoveAt(0);
        }
    }
}

Timer Class

A time management class keeps track of the current elapsed time in milliseconds. Its sole purpose is to increment the time by a specific amount each time its Tick method is called. The Timer class provides a single reference of time that is tolerant to delays between consecutive evaluations. For example, if the first call to Tick takes longer than the desirable duration to finish, the second call to Tick will only increase the time by an amount appropriate to maintaining overall regularity.

public class Timer
{
    private double currentMilliseconds;
    private readonly double incrementInMs;

    private Timer(double incrementInMs)
    {
        this.incrementInMs = incrementInMs;
    }

    public static Timer FromIncrement(double milliseconds)
    {
        return new Timer(milliseconds);
    }

    public double Tick()
    {
        currentMilliseconds += incrementInMs;
        return currentMilliseconds;
    }
}

Sequencer Class

A node of this type is constructed by an array of input Track nodes. Its main purpose is to retrieve MIDI commands from Track objects and feed them through to Win32 APIs for MIDI playback.

public class Sequencer : IDisposable
{
    #region Win32 Midi Output Functions and Constants

    [DllImport("winmm.dll")]
    private static extern int midiOutOpen(ref int handle, int deviceID,
        MidiOutProc proc, int instance, int flags);

    [DllImport("winmm.dll")]
    private static extern int midiOutClose(int handle);

    [DllImport("winmm.dll")]
    protected static extern int midiOutShortMsg(int handle, int message);

    protected delegate void MidiOutProc(int handle,
        int msg, int instance, int param1, int param2);

    #endregion

    private int deviceId;
    private int deviceHandle;
    private const int CALLBACK_FUNCTION = 196608;

    private readonly Track[] tracks;

    private Sequencer(Track[] tracks)
    {
        this.tracks = tracks;

        midiOutOpen(ref deviceHandle, deviceId,
            HandleMessage, 0, CALLBACK_FUNCTION);
    }

    public static Sequencer FromTracks(Track[] tracks)
    {
        return new Sequencer(tracks);
    }

    public void Dispose()
    {
        if (deviceHandle != 0)
        {
            midiOutClose(deviceHandle);
            deviceHandle = 0;
        }
    }

    public Sequencer ProcessTick(double milliseconds)
    {
        if (deviceHandle == 0)
            return this;

        var commands = new List();
        foreach (var track in tracks)
            track.FetchCommands(milliseconds, commands);

        foreach (var command in commands)
        {
            var message = ((int) command);
            midiOutShortMsg(deviceHandle, message);
        }

        return this;
    }

    private void HandleMessage(int handle, int msg,
        int instance, int param1, int param2)
    {
    }
}

Challenges

Managing something as time-sensitive as playing a MIDI file is a challenge. Dynamo’s scheduler thread in which evaluations take place is not of real-time priority and can be affected by various external factors. Just one example, the screen recording software I used for the demo video affects MIDI playback slightly whenever it writes to disk.

Even without external factors, graph evaluation is not guaranteed to complete within an acceptable interval before the next batch of MIDI commands must be sent, causing hiccups during playback.

Luckily for the sample MIDI file though, the gap between MIDI command batches averages around 300 milliseconds (although a few commands have start time that is slightly off from an even multiple of 300 milliseconds), and the gaps are too small to notice. So if we really wanted to, we could set the graph-wide evaluation period to be 300 milliseconds and still maintain a reasonable playback result.

Putting It All Together

In order to periodically send MIDI commands to the output device, the graph needs to be reevaluated at a fixed interval of 300 milliseconds. The only two nodes that must be reevaluated are Timer.Tick and Sequencer.ProcessTick. The following image illustrates the way to mark both the nodes as requiring update whenever the periodic evaluation kicks in.

Dynamo Periodic Update Context Menu

This is the completed graph for the experiment (click to enlarge):

Complete graph for MIDI playback in Dynamo

Video Demo

The video below demonstrates the whole workflow (see the HD version on YouTube for better visual quality):

 

Happy holidays!

With that, I wish everyone in Dynamo community and my fellow teammates a Merry Christmas and a happy new year!