PortMIDI

NAME

Audio::PortMIDI - Raku MIDI access using the portmidi library

SYNOPSIS


use Audio::PortMIDI;

my $pm = Audio::PortMIDI.new;

my $dev = $pm.default-output-device;

say $dev;


my $stream = $pm.open-output($dev, 32);

# Play 1/8th note middle C
my $note-on = Audio::PortMIDI::Event.new(event-type => NoteOn, channel => 1, data-one => 60, data-two => 127, timestamp => 0);
my $note-off = Audio::PortMIDI::Event.new(event-type => NoteOff, channel => 1, data-one => 60, data-two => 127, timestamp => 0);

$stream.write($note-on);
sleep .25;
$stream.write($note-off);

$stream.close;

$pm.terminate;

See also the examples directory in the distributtion for more complete examples.

DESCRIPTION

This allows you to get MIDI data into or out of your Raku programs. It provides the minimum abstraction to construct and unpack MIDI messages and send and receive them via some interface available on your system, be that ALSA on Linux, CoreMidi on Mac OS/X or whatever it is that Windows uses. Depending on the way that the portmidi library is built there may be other interfaces available.

The MIDI specification itself doesn't particularly provide for the arrangement of the events themselves in time and this is assumed to be the responsibility of the calling application.

You almost certainly will want to familiarise yourself to some extent with the MIDI protocol and especially the types of messages that are sent as the interface of PortMIDI and hence this module is fairly "close to the wire".

One thing that should be noted is that currently this uses the default time source provided by the PortMIDI library, which you can access in your own programs, if synchronization to an alternative clock is required the native subroutines would support it, just not available through the public interface at this time.

METHODS

The majority of the methods will throw an exception X::PortMIDI if a problem is encountered with the underlying library call.

method new

method new() returns Audio::PortMIDI

This is the constructor of the class, it will call initialize and start the default time source.

method initialize

method initialize()

This initializes the portmidi library and should be called before using any of the other methods, however it is called by the constructor so in practice it should not be necessary in most programs.

method terminate

method terminate()

This allows the portmidi libray to free up any resources it may have allocated and close any connections, this may not be necessary in short running programs but it is good practice to do it anyway ( I have noticed no ill-effects on Linux but your system may differ.)

method count-devices

method count-devices() returns Int

This returns the number of MIDI devices available on your system. The devices are numbered 0 .. count-devices - 1. Typically each physical (or virtual device) will be counted as two devices if it provides both input and output and may provide a different number of input and output devices. If there is some problem enumerating the devices then an exception will be thrown.

method device-info

method device-info(Int $device-id) returns DeviceInfo

This returns a Audio::PortMIDI::DeviceInfo object describing the device specified as the integer device ID, which should be between 0 and count-devices - 1. If the number is out of range or there is some other problem getting the device information an exception will be thrown.

method devices

method devices()

This returns the list of all the devices on the system as Audio::PortMIDI::DeviceInfo objects. which is really just a shortcut combining the above two methods. If there is a problem getting the list then an exception will be thrown.

method default-input-device

method default-input-device() returns DeviceInfo

This returns Audio::PortMIDI::DeviceInfo representing the default device that can be used for input on your system (if one is configured,) it may be be more or less useful depending on the configuration of your MIDI system, for instance on Linux with ALSA a default "MIDI Thru" device will always be created irrespective of the presence of any other MIDI devices and this will usually be returned as the default (for both input and output.)

method default-output-device

method default-outpur-device() returns DeviceInfo

This returns Audio::PortMIDI::DeviceInfo representing the default device that can be used for output on your system (if one is configured,) it may be be more or less useful depending on the configuration of your MIDI system, for instance on Linux with ALSA a default "MIDI Thru" device will always be created irrespective of the presence of any other MIDI devices and this will usually be returned as the default (for both input and output.)

method open-input

multi method open-input(DeviceInfo:D $dev, Int $buffer-size) returns Stream {
    multi method open-input(Int $device-id, Int $buffer-size) returns Stream {

This opens the specified device (which can be provided as either an integer device ID or a Audio::PortMIDI::DeviceInfo object,) for input returning a Audio::PortMIDI::Stream object. If the device is not available for input then an exception will be thrown.

The buffer size argument indicates the number of messages that should be buffered and should be tailored to suit your application, if the number of messages received without a read exceeds the size of the buffer then an exception may be thrown at the time of the next read, so you may want a higher value if you are expecting a high number of messages between each opportunity to read, this may of course slightly increase overall application latency. If you have a large number of messages (clocks, control changes, and so forth) that you are not interested in then you may set a filter on the Stream to reduce the number that are buffered for your application.

method open-output

multi method open-output(DeviceInfo:D $dev, Int $buffer-size, Int $latency = 0 ) returns Stream {
    multi method open-output(Int $device-id, Int $buffer-size, Int $latency = 0) returns Stream {

This opens the specified device (which can be provided as either an integer device ID or a Audio::PortMIDI::DeviceInfo object,) for output returning a Audio::PortMIDI::Stream object. If the device is not available for output then an exception will be thrown.

The buffer size argument is the maximum number of messages that can be buffered before being sent out by the device, If you exceed the buffer size then an exception will be thrown at the next attempt to write to the device, however the rate for a standard MIDI (serial) device is just a little less than a thousand messages a second so unless you are sending many notes at once with high frequency then you are unlikely to need a very large buffer.

The latency argument is a delay in milliseconds that is applied to determine when the output of the message will actually occur. If it is set to 0 (the default,) then the timestamp on a message is basically ignored and the message is sent immediately, if it is greater than 0 then the message will be delayed until the timestamp on the message + latency is equal to the application time reference. The portmidi time reference can be obtained from the Audio::PortMIDI::Time class method time and the timestamps should be constructed with reference to that value if this feature is to be used.

Audio::PortMIDI::DeviceInfo

This class represents a device that is returned by device-info and can be opened for input or output.

device-id

This is the integer device id of the device that may be passed to open-input or open-output

name

This is the name of the device, it may be duplicated if the physical device provides both input and output.

interface

This is the name if the host interface, such as "ALSA".

input

This is a Bool indicating whether this is an input device (i.e. whether it is valid to pass to open-input.)

output

This is a Bool indicating whether this is an output device (i.e. whether it is valid to pass to open-output.)

opened

This is a Bool indicating whether you have the device opened or not.

Audio::PortMIDI::Time

This provides access to the reference timer that is used by default by portmidi and provides timestamp values that (with the latency setting of open-output) can provide finer control over when the messages are sent. The timer itself is started when the first instance if Audio::PortMIDI is created in your application. All of the methods can be treated as "class methods" as there is only a single timer maintained by the portmidi library.

method start

method start()

This starts the timer, though it will always be started for you when you create an Audio::PortMIDI object, this may be useful if you want to use it in some other way.

method started

method started() returns Bool

This returns a Bool indicating whether the timer is running.

method time

method time() returns Int

This returns the monotonously increasing time reference value of the timer in milliseconds. It is only meaningful relative to other values obtained from the timer (i.e. it isn't the actual wallclock time,) these values can be used to create event timestamps with some offset for accurate timing of the despatch of the messages if a latency value was provided to open-output.

Audio::PortMIDI::Stream

Objects of this type are returned by open-input and open-output through which messages are read or written to the device. You will never construct one of these objects yourself.

method has-host-error

method has-host-error() returns Bool

This returns a Bool to indicate whether the host interface API has an error on the stream.

method set-filter

method set-filter(Int $filter)

This allows the setting of a filter on the stream that will exclude the specified types of messages from the input. It only makes sense for streams opened for input. The filter is the bitwise OR of values of the enumeration Audio::PortMIDI::Format :

  • Active

  • Sysex

  • Clock

  • Play

  • Tick

  • Fd

  • Undefined

  • Reset

  • Realtime

  • Note

  • ChannelAftertouch

  • PolyAftertouch

  • Aftertouch

  • Program

  • Control

  • Pitchbend

  • Mtc

  • SongPosition

  • SongSelect

  • Tune

  • Systemcommon

This can be used to reduce the number of messages that will be buffered for your stream to those that you are actually interested in (especially if you have a busy MIDI bus.)

method set-channel-mask

method set-channel-mask(*@channels)

This can be used to restrict the messages that you will receive to those on one or more specified channels (by default you will get messages for all channels,) you can supply up to 16 channel numbers in the range 0 .. 15 (that is the commonly referred channel number - 1). It is an error to supply more than sixteen or values outside that range.

method abort

method abort()

This closes the stream immediately, discarding any unsent or unread messages. After this is called reading or writing the stream will give rise to an exception.

method close

method close()

This closes the stream after all of the pending messages have been sent and will not accept any new messages to the buffer to read. It will give rise to an exception if a write is attempted after this is called.

method synchronize

method synchronize()

This would typically be used if the timer was started after the stream was opened, however this situation is unlikely to arise with this module as the timer is always started.

method poll

method poll() returns Bool

This returns (for streams opened for input,) a Bool indicating whether there are messages to be read. It does not block. It may be preferable to continuously calling read as it just checks whether there is anything in the input buffer rather than trying to obtain a message or messages.

method read

method read(Int $length)

For a stream opened for input this will attempt to obtain at most $length messages from the buffer and will return an array of Audio::PortMIDI::Event objects which will contain at most $length objects. If the buffer size that you specified to open-input has been overflowed since the last read then an exception will be thrown. If the stream is closed then an exception will be thrown. If the stream was not opened for input then an exception will be thrown.

method write

multi method write(Event @events)
    multi method write(Event $event)

For a stream that is opened for output, this will queue one or more Audio::PortMIDI::Event objects to be delivered to the device. The single candidate uses a native function that can discard the timestamp if it is not going to be used, and may be quicker, but the multiple candidate will typically endeavour to ensure that all the messages are despatched at the same time (or at the time appropriate for the timestamp and latency provided to open-output,) so will be better if "polyphonic" output is required for instance.

If the messages would overflow the buffer provided to open-output then an exception will be thrown. If the stream was not opened for output an exception will be thrown.

Audio::PortMIDI::Event

Objects of this type represent MIDI messages and are returned from read and passed to write on a stream, they provide a very thin abstraction over the MIDI message itself as well as the timestamp that may be used by portmidi for the timing of the delivery of the message but is not part of the MIDI specification.

An actual MIDI message on the wire comprises three bytes: a status byte that encodes the command and, for channel messages, the channel number and two data bytes of which one or both may be ignored for certain commands. (the values of the data bytes are actually 7 bits the high bit should be left unset, i.e. they are values 0 .. 127.) Whilst some sugar is provided for some message types you really should familiarise yourself with the MIDI specification if you want to get the most out of this.

A good summary of the structure of MIDI messages can be found in https://www.midi.org/specifications/item/table-1-summary-of-midi-message

method new

method new(Int :$event, Int :$!timestamp, Int :$!channel, Type :$!event-type, Int :$!data-one, Int :$!data-two, Int :$!status)

This is the constructor for objects of this class. Except for event the named arguments relate to the attributes of this class described below.

event is used when the object is being constructed from the value returned by portmidi in read (it is the timestamp and the entire MIDI message packed into a 64 bit Int,) you will almost certainly not need to use this in your own code.

If creating an object of this type you should provide timestamp (it can be 0 if you are not using the timing facilities described above, but if set to greater than 0 should never decrease.)

status comprises the channel and event-type and it doesn't make sense to provide all three to the constructor. Typically you should use channel and event-type for "channel messages" (e.g. "note on", "note off", "program change", "control change") and status for "system messages" such as MIDI clock where the specific message type is specified by the entire status byte.

timestamp

This is the timestamp of the message that will be used for scheduling the despatch of the message if a latency value was passed to open-output, it can be set to 0 or should be an increasing value in milliseconds. The time method of Audio::PortMIDI::Time provides values that can be used (possibly with an offset.)

channel

This is the Int channel number to be used for channel messages, it should be a value between 0 .. 15 (most documentation uses 1 .. 16 so you may need to subtract 1 from a value for the device if you have one.) It doesn't make sense as a channel number if it is a system message (it will actually be part of the "command".) Setting this (along with event-type,) does not make sense if status is being set.

event-type

This is a value of the enum Audio::PortMIDI::Event::Type (which is exported so it does not need to be fully qualified,):

NoteOff

This should be sent after a NoteOn with the same data-one and data-two as the original note, to turn the note off, this may not be necessary for instance with percussive instruments or those with a short envelope, however some instruments may get confused if they don't receive one so it's better to send it in all cases.

NoteOn

This starts a "note". data-one should be the "note number" in the range 0 .. 127 and data-two the "velocity" (again 0 .. 127.) Unless the target instrument is percussive or has a similar short envelope it should be followed by a NoteOff with the same data after a period that represents the length of the note to be sounded.

PolyphonicPressure

This represents a continued downward pressure on a keyboard after the note has been struck and before it has been released, it should have the "note number" in data-one (as above) and the "amount" of pressure in data-two (again 0 .. 127,) for most synthesizers it doesn't make sense unless it follows a NoteOn for the same note number and precedes the NoteOff - you can of course send as many as you like between the NoteOn and NoteOff as you want. Some documentation may refer to this as "after touch", the precise effect is entirely dependent on the device.

ControlChange

This sends a "control change" (often referred to as CC,) on the specified channel with the control number (in the range 0 .. 127) in data-one and the value in data-two. The documentation for your synthesizer should list those it understands, though some controls are reserved for special purposes. You are generally free to interpret them how you wish on reading messages.

ProgramChange

This requests that the "program" or "patch" is changed on the specified channel, the new program number is supplied in data-one, data-two should be ignored by most devices but ideally should be 0. The documentation of the device may describe the programs but often they can be user defined. Some synthesizers may allow multiple "banks" of programs which can be specified by a control change, but you will need to refer to the specific documentation about this.

ChannelPressure

This is similar to Polyphonic Pressure but applies to all active notes rather than just one "key". The data-one contains the amount of "Pressure" and data-two should ideally be 0 though it should be ignored by a device. As with PolyphonicPressure the precise effect on the sound is dependent on the actual device.

PitchBend

"pitch bend" will typically alter the pitch of all the active notes (though a device could conceivably interpret it differently) the amount of "bend" is specified as a 14 bit value with the 7 most significant bits in data-one and the 7 least significant in data-two. You can consider this as being 0 .. 127 of "coarse" modulation in data-one and 0 .. 127 of "fine" control in data-two if it is easier.

SystemMessage

This (for a read message,) indicates that the message is a "system" message and that, rather than being a separate command and channel, the whole status byte should be considered to be the "command". For a message that is to be sent you probably wouldn't set this but supply the appropriate status.

status

This is the entire status byte, if this has the highest four bits set (i.e. it has the value of 240 or greater) then the message is a "system message", and the event-type and channel are probably not meaningful themselves (the event-type is SystemMessage and the lower four bits indicate the actual message type.) If you set this then you do not need to set the event-type and channel and vice versa.

The most common values used are those related to the MIDI realtime clock that may control the tempo and playback of the receiving device or may be received to control the tempo in your application:

Clock

If a clock is being provided to or from a device events of this type are sent 24 times per quarter note (or every 0.020833 seconds at 120 beats per minute.) This sort of frequency should be doable in a Raku program though various scheduling delays may mean it will be difficult to maintain accuracy at higher tempos without using the portmidi timer.

The received clocks may be used for other than just setting the tempo on a receiving system, for instance synchronising the rate of an LFO or other modulation source.

Some devices may commence playing on receipt of the first few clocks rather than needing a Start message.

Some devices that emit a clock may alter the frequency of the clocks temporarily to affect some time based decoration (such as a fill or roll from a drum machine.)

Start

If this is received the current sequence will start playing and it is assumed that will be followed by a series of Clock messages indicating the tempo of playback. Playback is expected to start at the beginning.

Continue

If this is received and the sequence is stopped then playback will restart at the position where it was last stopped. Like Start it is assumed that this will be followed by a steady flow of Clock messages.

Stop

This will stop any running sequence on any attached device, typically the stream of Clock messages will stop as well.

Audio::PortMIDI v0.0.12

Raku MIDI access using the portmidi library

Authors

  • Jonathan Stowe
  • Pepe Schwarz

License

Artistic-2.0

Dependencies

Util::Bitfield

Test Dependencies

Provides

  • Audio::PortMIDI

The Camelia image is copyright 2009 by Larry Wall. "Raku" is trademark of the Yet Another Society. All rights reserved.