Skip to content

Commit 5f4b520

Browse files
committed
Init
0 parents  commit 5f4b520

File tree

2 files changed

+680
-0
lines changed

2 files changed

+680
-0
lines changed

alsa.py

Lines changed: 395 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,395 @@
1+
#!/usr/bin/env python
2+
3+
import ctypes
4+
import ctypes.util
5+
import select
6+
import signal
7+
import sys
8+
from typing import Set, Tuple
9+
10+
# This list is the "source of truth" for your MIDI setup.
11+
# Use the format "Client Name:Port Name" or "Client Number:Port Number".
12+
DESIRED_CONNECTIONS = [
13+
("a2j:Midi Through", "Midi Fighter Twister:Midi Fighter Twister MIDI 1"),
14+
# Example: ("20:0", "128:0"),
15+
]
16+
17+
# --- ctypes ALSA Library Definitions ---
18+
19+
# Find and load the ALSA library
20+
libasound_path = ctypes.util.find_library("asound")
21+
if not libasound_path:
22+
print("ALSA library (libasound) not found.", file=sys.stderr)
23+
sys.exit(1)
24+
try:
25+
alsalib = ctypes.CDLL(libasound_path)
26+
except OSError as e:
27+
print(f"Error loading ALSA library: {e}", file=sys.stderr)
28+
sys.exit(1)
29+
30+
# --- Basic ALSA Types and Structures ---
31+
snd_seq_t = ctypes.c_void_p
32+
snd_seq_client_info_t = ctypes.c_void_p
33+
snd_seq_port_info_t = ctypes.c_void_p
34+
snd_seq_port_subscribe_t = ctypes.c_void_p
35+
snd_seq_query_subscribe_t = ctypes.c_void_p
36+
37+
38+
class snd_seq_addr(ctypes.Structure):
39+
_fields_ = [("client", ctypes.c_ubyte), ("port", ctypes.c_ubyte)]
40+
41+
42+
class snd_seq_event(ctypes.Structure):
43+
_fields_ = [
44+
("type", ctypes.c_ubyte),
45+
("flags", ctypes.c_ubyte),
46+
("tag", ctypes.c_char),
47+
("queue", ctypes.c_ubyte),
48+
("time", ctypes.c_void_p), # snd_seq_timestamp_t
49+
("source", snd_seq_addr),
50+
("dest", snd_seq_addr),
51+
("data", ctypes.c_void_p),
52+
] # union
53+
54+
55+
# --- ALSA Constants ---
56+
SND_SEQ_OPEN_DUPLEX = 2
57+
SND_SEQ_NONBLOCK = 1
58+
SND_SEQ_PORT_CAP_WRITE = 1 << 1
59+
SND_SEQ_QUERY_SUBS_READ = 1
60+
61+
# Event types that trigger a reconciliation
62+
RELEVANT_EVENTS = {60, 61, 62, 63, 64, 65, 66, 67}
63+
64+
# --- ALSA Function Prototypes ---
65+
# Suppress the default ALSA error handler
66+
SND_ERROR_HANDLER_T = ctypes.CFUNCTYPE(
67+
None, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p
68+
)
69+
alsalib.snd_lib_error_set_handler.argtypes = [SND_ERROR_HANDLER_T]
70+
71+
# Sequencer handle
72+
alsalib.snd_seq_open.argtypes = [
73+
ctypes.POINTER(snd_seq_t),
74+
ctypes.c_char_p,
75+
ctypes.c_int,
76+
ctypes.c_int,
77+
]
78+
alsalib.snd_seq_close.argtypes = [snd_seq_t]
79+
alsalib.snd_seq_set_client_name.argtypes = [snd_seq_t, ctypes.c_char_p]
80+
alsalib.snd_seq_client_id.argtypes = [snd_seq_t]
81+
alsalib.snd_seq_client_id.restype = ctypes.c_int
82+
83+
# Polling for events
84+
alsalib.snd_seq_poll_descriptors_count.argtypes = [snd_seq_t, ctypes.c_short]
85+
alsalib.snd_seq_poll_descriptors.argtypes = [
86+
snd_seq_t,
87+
ctypes.c_void_p,
88+
ctypes.c_uint,
89+
ctypes.c_short,
90+
] # pollfd*
91+
alsalib.snd_seq_event_input.argtypes = [
92+
snd_seq_t,
93+
ctypes.POINTER(ctypes.POINTER(snd_seq_event)),
94+
]
95+
alsalib.snd_seq_event_input.restype = ctypes.c_int
96+
97+
# Creating subscriptions (making connections)
98+
alsalib.snd_seq_port_subscribe_malloc.argtypes = [
99+
ctypes.POINTER(snd_seq_port_subscribe_t)
100+
]
101+
alsalib.snd_seq_port_subscribe_set_sender.argtypes = [
102+
snd_seq_port_subscribe_t,
103+
ctypes.POINTER(snd_seq_addr),
104+
]
105+
alsalib.snd_seq_port_subscribe_set_dest.argtypes = [
106+
snd_seq_port_subscribe_t,
107+
ctypes.POINTER(snd_seq_addr),
108+
]
109+
alsalib.snd_seq_subscribe_port.argtypes = [snd_seq_t, snd_seq_port_subscribe_t]
110+
alsalib.snd_seq_port_subscribe_free.argtypes = [snd_seq_port_subscribe_t]
111+
alsalib.snd_seq_parse_address.argtypes = [
112+
snd_seq_t,
113+
ctypes.POINTER(snd_seq_addr),
114+
ctypes.c_char_p,
115+
]
116+
117+
# Querying clients and ports
118+
alsalib.snd_seq_client_info_malloc.argtypes = [ctypes.POINTER(snd_seq_client_info_t)]
119+
alsalib.snd_seq_client_info_set_client.argtypes = [snd_seq_client_info_t, ctypes.c_int]
120+
alsalib.snd_seq_query_next_client.argtypes = [snd_seq_t, snd_seq_client_info_t]
121+
alsalib.snd_seq_client_info_get_client.argtypes = [snd_seq_client_info_t]
122+
alsalib.snd_seq_client_info_get_name.argtypes = [snd_seq_client_info_t]
123+
alsalib.snd_seq_client_info_get_name.restype = ctypes.c_char_p
124+
alsalib.snd_seq_client_info_free.argtypes = [snd_seq_client_info_t]
125+
alsalib.snd_seq_port_info_malloc.argtypes = [ctypes.POINTER(snd_seq_port_info_t)]
126+
alsalib.snd_seq_port_info_set_client.argtypes = [snd_seq_port_info_t, ctypes.c_int]
127+
alsalib.snd_seq_port_info_set_port.argtypes = [snd_seq_port_info_t, ctypes.c_int]
128+
alsalib.snd_seq_query_next_port.argtypes = [snd_seq_t, snd_seq_port_info_t]
129+
alsalib.snd_seq_port_info_get_name.argtypes = [snd_seq_port_info_t]
130+
alsalib.snd_seq_port_info_get_name.restype = ctypes.c_char_p
131+
alsalib.snd_seq_port_info_get_addr.argtypes = [snd_seq_port_info_t]
132+
alsalib.snd_seq_port_info_get_addr.restype = ctypes.POINTER(snd_seq_addr)
133+
alsalib.snd_seq_port_info_get_capability.argtypes = [snd_seq_port_info_t]
134+
alsalib.snd_seq_port_info_get_capability.restype = ctypes.c_uint
135+
alsalib.snd_seq_port_info_free.argtypes = [snd_seq_port_info_t]
136+
alsalib.snd_seq_get_any_port_info.argtypes = [
137+
snd_seq_t,
138+
ctypes.c_int,
139+
ctypes.c_int,
140+
snd_seq_port_info_t,
141+
]
142+
143+
# Querying subscriptions (reading connections)
144+
alsalib.snd_seq_query_subscribe_malloc.argtypes = [
145+
ctypes.POINTER(snd_seq_query_subscribe_t)
146+
]
147+
alsalib.snd_seq_query_subscribe_set_root.argtypes = [
148+
snd_seq_query_subscribe_t,
149+
ctypes.POINTER(snd_seq_addr),
150+
]
151+
alsalib.snd_seq_query_subscribe_set_type.argtypes = [
152+
snd_seq_query_subscribe_t,
153+
ctypes.c_int,
154+
]
155+
alsalib.snd_seq_query_subscribe_set_index.argtypes = [
156+
snd_seq_query_subscribe_t,
157+
ctypes.c_int,
158+
]
159+
alsalib.snd_seq_query_port_subscribers.argtypes = [snd_seq_t, snd_seq_query_subscribe_t]
160+
alsalib.snd_seq_query_subscribe_get_addr.argtypes = [snd_seq_query_subscribe_t]
161+
alsalib.snd_seq_query_subscribe_get_addr.restype = ctypes.POINTER(snd_seq_addr)
162+
alsalib.snd_seq_query_subscribe_get_index.argtypes = [snd_seq_query_subscribe_t]
163+
alsalib.snd_seq_query_subscribe_get_index.restype = ctypes.c_int
164+
alsalib.snd_seq_query_subscribe_free.argtypes = [snd_seq_query_subscribe_t]
165+
166+
167+
# --- Global State ---
168+
seq = None
169+
running = True
170+
171+
172+
# --- Helper Functions ---
173+
def get_current_state() -> Tuple[Set[str], Set[Tuple[str, str]]]:
174+
"""
175+
Gets the current state of the ALSA sequencer.
176+
Returns (all_ports, all_connections)
177+
"""
178+
if not seq:
179+
return set(), set()
180+
181+
available_ports, connections = set(), set()
182+
client_ports = {} # { client_id: { port_id: "Full Port String" } }
183+
184+
cinfo_ptr = snd_seq_client_info_t()
185+
pinfo_ptr = snd_seq_port_info_t()
186+
alsalib.snd_seq_client_info_malloc(ctypes.byref(cinfo_ptr))
187+
alsalib.snd_seq_port_info_malloc(ctypes.byref(pinfo_ptr))
188+
189+
# First pass: Get all clients and ports, build a map for easy lookup
190+
alsalib.snd_seq_client_info_set_client(cinfo_ptr, -1)
191+
while alsalib.snd_seq_query_next_client(seq, cinfo_ptr) >= 0:
192+
client_id = alsalib.snd_seq_client_info_get_client(cinfo_ptr)
193+
client_name = alsalib.snd_seq_client_info_get_name(cinfo_ptr).decode("utf-8")
194+
client_ports[client_id] = {}
195+
196+
alsalib.snd_seq_port_info_set_client(pinfo_ptr, client_id)
197+
alsalib.snd_seq_port_info_set_port(pinfo_ptr, -1)
198+
while alsalib.snd_seq_query_next_port(seq, pinfo_ptr) >= 0:
199+
addr = alsalib.snd_seq_port_info_get_addr(pinfo_ptr).contents
200+
port_name = alsalib.snd_seq_port_info_get_name(pinfo_ptr).decode("utf-8")
201+
full_port_str = f"{client_name}:{port_name}"
202+
available_ports.add(full_port_str)
203+
client_ports[addr.client][addr.port] = full_port_str
204+
205+
# Second pass: Iterate through writable ports and query their subscribers
206+
for client_id, ports in client_ports.items():
207+
for port_id, source_full_str in ports.items():
208+
alsalib.snd_seq_get_any_port_info(seq, client_id, port_id, pinfo_ptr)
209+
caps = alsalib.snd_seq_port_info_get_capability(pinfo_ptr)
210+
211+
if caps & SND_SEQ_PORT_CAP_WRITE: # This is a source port
212+
query_ptr = snd_seq_query_subscribe_t()
213+
alsalib.snd_seq_query_subscribe_malloc(ctypes.byref(query_ptr))
214+
sender_addr = snd_seq_addr(client=client_id, port=port_id)
215+
alsalib.snd_seq_query_subscribe_set_root(
216+
query_ptr, ctypes.byref(sender_addr)
217+
)
218+
alsalib.snd_seq_query_subscribe_set_type(
219+
query_ptr, SND_SEQ_QUERY_SUBS_READ
220+
)
221+
alsalib.snd_seq_query_subscribe_set_index(query_ptr, 0)
222+
223+
while alsalib.snd_seq_query_port_subscribers(seq, query_ptr) >= 0:
224+
sub_addr = alsalib.snd_seq_query_subscribe_get_addr(
225+
query_ptr
226+
).contents
227+
try:
228+
dest_full_str = client_ports[sub_addr.client][sub_addr.port]
229+
connections.add((source_full_str, dest_full_str))
230+
except KeyError:
231+
# Connection to a non-existent port, can be ignored
232+
pass
233+
index = alsalib.snd_seq_query_subscribe_get_index(query_ptr)
234+
alsalib.snd_seq_query_subscribe_set_index(query_ptr, index + 1)
235+
236+
alsalib.snd_seq_query_subscribe_free(query_ptr)
237+
238+
alsalib.snd_seq_client_info_free(cinfo_ptr)
239+
alsalib.snd_seq_port_info_free(pinfo_ptr)
240+
return available_ports, connections
241+
242+
243+
def connect_alsa_ports(source_str: str, dest_str: str) -> bool:
244+
"""Connects two ALSA ports using the subscription mechanism."""
245+
if not seq:
246+
return False
247+
248+
sender = snd_seq_addr()
249+
dest = snd_seq_addr()
250+
251+
if (
252+
alsalib.snd_seq_parse_address(
253+
seq, ctypes.byref(sender), source_str.encode("utf-8")
254+
)
255+
< 0
256+
):
257+
print(f" [ERROR] Cannot parse source address: {source_str}", file=sys.stderr)
258+
return False
259+
if (
260+
alsalib.snd_seq_parse_address(seq, ctypes.byref(dest), dest_str.encode("utf-8"))
261+
< 0
262+
):
263+
print(
264+
f" [ERROR] Cannot parse destination address: {dest_str}", file=sys.stderr
265+
)
266+
return False
267+
268+
sub_ptr = snd_seq_port_subscribe_t()
269+
alsalib.snd_seq_port_subscribe_malloc(ctypes.byref(sub_ptr))
270+
alsalib.snd_seq_port_subscribe_set_sender(sub_ptr, ctypes.byref(sender))
271+
alsalib.snd_seq_port_subscribe_set_dest(sub_ptr, ctypes.byref(dest))
272+
273+
success = False
274+
if alsalib.snd_seq_subscribe_port(seq, sub_ptr) == 0:
275+
print(f" [OK] Ensuring connection: {source_str} -> {dest_str}")
276+
success = True
277+
278+
alsalib.snd_seq_port_subscribe_free(sub_ptr)
279+
return success
280+
281+
282+
def reconcile_connections():
283+
"""Compares the desired state with the current state and makes connections."""
284+
print("\n--- Reconciling ALSA MIDI Connections ---")
285+
available_ports, current_connections = get_current_state()
286+
287+
if not available_ports:
288+
print(" [WARN] No ALSA MIDI ports available.")
289+
return
290+
291+
for source, dest in DESIRED_CONNECTIONS:
292+
if (source, dest) in current_connections:
293+
continue
294+
if source in available_ports and dest in available_ports:
295+
print(" [!] Missing connection detected. Restoring...")
296+
connect_alsa_ports(source, dest)
297+
298+
299+
def signal_handler(sig, frame):
300+
"""Handles signals and sets the running flag to false."""
301+
global running
302+
print("\nSignal received, shutting down...")
303+
running = False
304+
305+
306+
# --- Main Application ---
307+
def main():
308+
global seq
309+
310+
seq_ptr = snd_seq_t()
311+
if (
312+
alsalib.snd_seq_open(
313+
ctypes.byref(seq_ptr), b"default", SND_SEQ_OPEN_DUPLEX, SND_SEQ_NONBLOCK
314+
)
315+
< 0
316+
):
317+
print("Error opening ALSA sequencer.", file=sys.stderr)
318+
sys.exit(1)
319+
seq = seq_ptr
320+
321+
alsalib.snd_seq_set_client_name(seq, b"py-alsa-ctypes-manager")
322+
323+
# Subscribe to the announce port to receive system-wide events
324+
sub_ptr = snd_seq_port_subscribe_t()
325+
alsalib.snd_seq_port_subscribe_malloc(ctypes.byref(sub_ptr))
326+
327+
sender = snd_seq_addr(client=0, port=0) # System Announce port is 0:0
328+
dest_client_id = alsalib.snd_seq_client_id(seq)
329+
dest = snd_seq_addr(client=dest_client_id, port=0) # Our client's first port
330+
alsalib.snd_seq_port_subscribe_set_sender(sub_ptr, ctypes.byref(sender))
331+
alsalib.snd_seq_port_subscribe_set_dest(sub_ptr, ctypes.byref(dest))
332+
333+
if alsalib.snd_seq_subscribe_port(seq, sub_ptr) < 0:
334+
print("Could not subscribe to announce port.", file=sys.stderr)
335+
alsalib.snd_seq_close(seq)
336+
sys.exit(1)
337+
alsalib.snd_seq_port_subscribe_free(sub_ptr)
338+
339+
# Set up polling
340+
poll_count = alsalib.snd_seq_poll_descriptors_count(seq, select.POLLIN)
341+
342+
# The C struct pollfd has {int fd; short events; short revents;}
343+
class pollfd(ctypes.Structure):
344+
_fields_ = [
345+
("fd", ctypes.c_int),
346+
("events", ctypes.c_short),
347+
("revents", ctypes.c_short),
348+
]
349+
350+
poll_fds = (pollfd * poll_count)()
351+
alsalib.snd_seq_poll_descriptors(
352+
seq, ctypes.byref(poll_fds), poll_count, select.POLLIN
353+
)
354+
355+
poller = select.poll()
356+
for pfd in poll_fds:
357+
poller.register(pfd.fd, select.POLLIN)
358+
359+
signal.signal(signal.SIGINT, signal_handler)
360+
signal.signal(signal.SIGTERM, signal_handler)
361+
362+
try:
363+
reconcile_connections()
364+
print(
365+
"\n--- Initial setup complete. Waiting for ALSA events... (Press Ctrl+C to exit) ---"
366+
)
367+
368+
while running:
369+
if poller.poll(1000): # 1 second timeout
370+
event_ptr = ctypes.POINTER(snd_seq_event)()
371+
reconciliation_needed = False
372+
while alsalib.snd_seq_event_input(seq, ctypes.byref(event_ptr)) >= 0:
373+
event = event_ptr.contents
374+
if event and event.type in RELEVANT_EVENTS:
375+
reconciliation_needed = True
376+
377+
if reconciliation_needed:
378+
print("ALSA Event detected, triggering reconciliation.")
379+
reconcile_connections()
380+
381+
finally:
382+
print("Exiting ALSA connection manager.")
383+
if seq:
384+
alsalib.snd_seq_close(seq)
385+
386+
387+
if __name__ == "__main__":
388+
# Define a no-op handler to suppress ALSA's default error messages
389+
@SND_ERROR_HANDLER_T
390+
def py_error_handler(filename, line, function, err, fmt):
391+
pass
392+
393+
alsalib.snd_lib_error_set_handler(py_error_handler)
394+
395+
main()

0 commit comments

Comments
 (0)