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.