Setup instructions for playing music

PortMidi can be used to play (virtual or electronic) musical instruments in real-time. In the following, we briefly outline how to configure your computer for this case.

Here, we will use two standalone virutal instruments to get a uncomplicated setup. The more powerful option is of course the use of a digitial audio workstation (DAW) which can also recived and forward MIDI data to the instruments loaded within the DAW.

The following text is a selection of options and clearly subject to personal opinion.

Overview

In principle three steps are sufficient:

  • Install standalone virtual instrument.
  • Create MIDI output channel (with Pm_CreateVirtualOutput on linux and macOS, with loopMIDI on Windows).
  • Configure instrument to listen to MIDI and sent note on/off commands to play music.

Installation

The easiest option is to install a standalone virtual instrument such as

If you want to try a DAW instead (which gives also access to virtual instruments without standalone mode). A DAW allows to record audio and MIDI, arrange MIDI tracks and load virtual instruments and audio effects.

One free open-source cross-platform option is LMMS. Another 'almost free' option is Reaper, which has a pretty generous trail option (just a dialog bugging at the start-up, even after the expire of the trial phase). Obviously, many free and paid alternatives exist.

Create MIDI output channel

First, we need to create a new MIDI output channel for sending MIDI messages from Julia to the virtual instrument. On Windows this requires an extra program, for the other platforms portmidi itself can create the channel.

Linux and macOS

On linux and macOS one can use the Pm_CreateVirtualOutput(name::String, interface::String, deviceinfo::Ptr) function, see C docs for the explaination. The third input should always be C_NULL. The output is either the error message or the new id.

This part of the documentation is untested, please open an issue if the instructions no not work

using PortMidi

Pm_Initialize()

# create virtual input
interface = Sys.islinux() ? "ALSA" : "CoreMIDI"
id_or_err = Pm_CreateVirtualOutput("Julia MIDI Out", interface, C_NULL) 

# check error
if Int(id_or_err) < 0
    error("Could not open a virtual output device.\n\
           PortMidi error: '", string(id_or_err), "'")
end

# obtain ID
id = Int(id_or_err)

Windows

The Pm_CreateVirtualOutput function is not implemented since the interface MMSystem does not provide a protable solution for this task.

Creating a MIDI channel with loopMIDI

We can use the free program loopMIDI instead.

  • Install loopMIDI. (Restart might be required.)
  • Create a new MIDI channel called loopMIDI Port and click the + button.

Finding an existing MIDI channel with PortMIDI

After the MIDI channel is created, we just need to obtain its ID. Note that the ID index is 0-based. Important note: The first call of `PmInitialize()needs to happen after the MIDI channel was created. Alternatively, one needs to callPmTerminate()first beforePmInitialize()` to obtain the correct list of current channels._

The following code uses the Pm_GetDeviceInfo function which returns a C pointer to a PmDeviceInfo which has inparticular a name and output field, which we check to get the right channel.

using PortMIDI

Pm_Initialize()

id = findfirst(x -> unsafe_string(x.name) == "loopMIDI Port" && x.output > 0, (unsafe_load(Pm_GetDeviceInfo(i)) for i in 0:Pm_CountDevices()-1))-1

if isnothing(id)
    error("Could not find MIDI channel 'loopMIDI Port'.")
end

Start virtual instrument and select the MIDI channel as input

With standalone instruments: We now need to select the MIDI port Julia MIDI Out or loopMIDI Port in our virtual instrument. All programs have the options button in the top-left of the Window (Decent sampler/Surge XT: 'Options button', Vital: 'click the logo'). One might need to configure the audio output as well, depending on the setup.

If using a DAW: Select in the options the right MIDI input and configure the audio driver for your system. Afterward, load a virtual instrument and arm the track to listen to MIDI input.

Open output channel with PortMidi

Once we have obtained the ID of a working MIDI output channel and setup the instruments, we can open the MIDI channel and send messages. Below is one small example:

Create a reference to a pointer for the MIDI stream.

stream = Ref{Ptr{PortMidi.PortMidiStream}}(C_NULL)
Pm_OpenOutput(stream, id, C_NULL, 0, C_NULL, C_NULL, 0)
# don't forget to call later: Pm_Close(stream[])

MIDI note messages have to be compressed into 32 bits. We can use the Pm_Message function for this task, see C docs on PmEvents and Pm_Message for more details.

Here, we just need to know that we need to provide a note on and note off messages. That is,

  • a flag either 0x90 or 0x80 for note on/off events
  • the semitone of the note we want to play C4 = 60
  • the velocity of the keystroke

The following helper function allows us to play a note with a given duration.

Note_ON = 0x90
Note_OFF = 0x80
C4 = 60

function play_note(stream, note, velocity, duration)
    Pm_WriteShort(stream[], 0, Pm_Message(Note_ON, note, velocity))
    sleep(duration)
    Pm_WriteShort(stream[], 0, Pm_Message(Note_OFF, note, velocity))
end

Finally, we can play some notes:

for i in 1:12
    @async play_note(stream, C4 + rand([-5,-2,0,2,5]), rand(80:120), 0.4 + 0.05*rand())
    sleep(0.5 + 0.05*rand())
end

The @async helps us to meet the beat. However, for accurate timing one might need to use a different approach. (Ideas are welcome!)

If everything worked, you should head a small tune.

Cleanup

Afterwards, we should close the MIDI stream and terminate PortMidi:

Pm_Close(stream[])
Pm_Terminate()