Usage
Client and ports
To interface ALSA sequencer a client needs to be created. This is done by
creating a SequencerClient
object:
from alsa_midi import SequencerClient
client = SequencerClient("my client")
To receive or send MIDI events at least one port will be needed too:
port = client.create_port("inout")
By default a generic bi-directional MIDI port with full subscription access is
created. Additional arguments SequencerClient.create_port()
method can be used to change that:
from alsa_midi import WRITE_PORT, PortType
input_port = client.create_port("input",
caps=WRITE_PORT,
type=PortType.MIDI_GENERIC | PortType.MIDI_GM | PortType.SYNTHESIZER)
Note: use WRITE_PORT
(or PortCaps.WRITE
) for creating input
ports (ports other clients can write to) and READ_PORT
(or
PortCaps.READ
) for creating output ports (that other clients can read
from).
External port discovery
Client and port discovery can be done using
SequencerClient.query_next_client()
and
SequencerClient.query_next_port()
methods, which are Python interface to
the original the ALSA API calls.
There is also a convenience SequencerClient.list_ports()
method
provided do get list of all available and relevant ports at once. The result is
also sorted in a way, that the first entry should be the most useful one
(according to some heuristics).
To get a list of available ports for MIDI output:
out_ports = client.list_ports(output=True)
To get a list of hardware MIDI input ports:
in_ports = client.list_ports(input=True, type=PortType.MIDI_GENERIC | PortType.HARDWARE)
Port subscriptions
ALSA sequencer keeps a list of subscriptions (connections) between ports. Events can then be sent either to a specific port or to all connected ports, the latter being often more useful.
Creating a connection of own port to some other port is as easy as calling the
Port.connect_to()
or Port.connect_from()
method:
dest_port = client.list_ports(output=True)[0]
port.connect_to(dest_port)
source_port = client.list_ports(input=True)[0]
input_port.connect_from(source_port)
Ports can be disconnected using Port.disconnect_to()
or
Port.disconnect_from()
method, appropriately.
Client can also manage connection between ports on other clients with
SequencerClient.subscribe_port()
and
SequencerClient.unsubscribe_port()
methods.
Event output
Events, which are instances of Event
subclasses can be sent out using
the SequencerClient.event_output()
method. It does not send the events immediately,
unless the buffer gets full, so SequencerClient.drain_output()
has to be called afterwards:
import time
from alsa_midi import NoteOnEvent, NoteOffEvent
for event in NoteOnEvent(note=60), NoteOnEvent(note=64), NoteOnEvent(note=67):
client.event_output(event, port=port)
client.drain_output()
time.sleep(1)
for event in NoteOffEvent(note=60), NoteOffEvent(note=64), NoteOffEvent(note=67):
client.event_output(event, port=port)
client.drain_output()
Event input
Incoming events can be received using the SequencerClient.event_input()
method:
while True:
event = client.event_input()
print(repr(event))
Queues
What makes ALSA sequencer a sequencer is precise control of event timing using event queues.
A queue is created using the SequencerClient.create_queue()
method:
queue = client.create_queue("my queue")
Then queue tempo should be set:
beats_per_minute = 120
ticks_per_quarter_note = 96
queue.set_tempo(int(60.0 * 1000000 / beats_per_minute), ticks_per_quarter_note)
And the queue start command should be issued (will be executed after SequencerClient.drain_output():
queue.start()
The queue now might be used for placing output events in time:
for event in NoteOnEvent(note=60, tick=0), NoteOffEvent(note=60, tick=96):
client.event_output(event, port=port)
client.drain_output()
Queues can also be used for setting timestamps (in MIDI ticks or seconds and nanoseconds) on incoming events:
port = client.create_port("input", WRITE_PORT,
timestamping=True,
timestamp_real=True,
timestamp_queue=queue)
while True:
event = client.event_input()
print("Time:", event.time, "Event:", repr(event))
Asynchronous Interface
python-alsa-midi can work with asyncio
event loop. For this task there is AsyncSequencerClient
class.
It is mostly the same as AsyncSequencerClient
, but
event_input()
,
drain_output()
,
event_output()
and
event_output_direct()
are coroutines here.
Example:
import asyncio
from alsa_midi import (AsyncSequencerClient, READ_PORT, WRITE_PORT,
NoteOnEvent, NoteOffEvent)
async def play_chord(client):
port = client.create_port("output", READ_PORT)
port.connect_to(client.list_ports(output=True)[0])
for event in NoteOnEvent(note=60), NoteOnEvent(note=64), NoteOnEvent(note=67):
await client.event_output(event, port=port)
await client.drain_output()
await asyncio.sleep(1)
for event in NoteOffEvent(note=60), NoteOffEvent(note=64), NoteOffEvent(note=67):
await client.event_output(event, port=port)
await client.drain_output()
async def show_input(client):
port = client.create_port("input", WRITE_PORT)
port.connect_to(client.list_ports(input=True)[0])
while True:
event = await client.event_input()
print(repr(event))
client = AsyncSequencerClient("async example")
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(play_chord(client), show_input(client)))
Direct access to ALSA API
The raw ALSA cffi bindings are available through alsa_midi.alsa
and
alsa_midi.ffi
objects:
from alsa_midi import alsa, ffi
version = alsa.snd_asoundlib_version()
print(ffi.string(version).decode())
Thread safety
SequencerClient
, AsyncSequencerClient
objects and
Port
and Queue
objects created through them should only be
used in a single thread and a single asyncio
loop.
Event
objects do not contain any external resources, so the can be
safely passed around. Simultaneous access from different threads should still
be avoided.
MIDO back-end
The package also includes a MIDO back-end module. To use it set shell variable $MIDO_BACKEND to ‘alsa_midi.mido_backend’ or programmatically:
mido.set_backend('alsa_midi.mido_backend')