Skip to content

Commit 33d8e36

Browse files
committed
feat: added midi receive option to decode incoming MIDI CC/Note and other type of messages
feat: added a new test tool to ingest midi messages on a virtual port fix: made all console log format the same starting with "[C] " or "[Python] " fix: renamed all test codes with the prefix of "test_" in the repository
1 parent f53b73f commit 33d8e36

4 files changed

Lines changed: 377 additions & 46 deletions

File tree

clock.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@
1313
BPM = 120
1414
PPQN = 24 # Pulses Per Quarter Note
1515

16+
# ALSA MIDI Event Types (from alsa/asoundlib.h)
17+
class MidiEventType:
18+
NOTEON = 6
19+
NOTEOFF = 7
20+
CONTROLLER = 10
21+
PGMCHANGE = 11
22+
PITCHBEND = 13
23+
CHANPRESS = 14
24+
KEYPRESS = 27
25+
1626
# Global state
1727
running = True
1828
midi_lib = None
@@ -21,6 +31,29 @@
2131
# use float BPM with 0.1 precision
2232
current_bpm = float(BPM)
2333

34+
# Define callback function type: void callback(int event_type, int channel, int param1, int param2, int param3)
35+
MIDI_EVENT_CALLBACK = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int)
36+
37+
def midi_event_callback(event_type, channel, param1, param2, param3):
38+
"""Callback function that receives MIDI events from C library and logs them."""
39+
# Channels are 0-indexed in C, but we display 1-indexed for users
40+
display_channel = channel + 1
41+
42+
if event_type == MidiEventType.NOTEON:
43+
print(f"[Python] [MIDI IN] NOTE ON - Channel: {display_channel}, Note: {param1}, Velocity: {param2}")
44+
elif event_type == MidiEventType.NOTEOFF:
45+
print(f"[Python] [MIDI IN] NOTE OFF - Channel: {display_channel}, Note: {param1}, Velocity: {param2}")
46+
elif event_type == MidiEventType.CONTROLLER:
47+
print(f"[Python] [MIDI IN] CC - Channel: {display_channel}, Controller: {param1}, Value: {param2}")
48+
elif event_type == MidiEventType.PGMCHANGE:
49+
print(f"[Python] [MIDI IN] PROG CHG - Channel: {display_channel}, Program: {param1}")
50+
elif event_type == MidiEventType.PITCHBEND:
51+
print(f"[Python] [MIDI IN] PITCH BND - Channel: {display_channel}, Value: {param1} (range -8192 to 8191)")
52+
elif event_type == MidiEventType.CHANPRESS:
53+
print(f"[Python] [MIDI IN] AFTERTOUCH - Channel: {display_channel}, Value: {param1}")
54+
elif event_type == MidiEventType.KEYPRESS:
55+
print(f"[Python] [MIDI IN] POLY AFTERTOUCH - Channel: {display_channel}, Note: {param1}, Value: {param2}")
56+
2457
def change_tempo(new_bpm):
2558
"""Change the tempo of the MIDI clock (applies to the C library).
2659
@@ -67,15 +100,15 @@ def main():
67100
lib_path = os.path.join(os.path.dirname(__file__), 'liblinkbridge.so')
68101

69102
if not os.path.exists(lib_path):
70-
print(f"Error: Library not found at {lib_path}")
71-
print("Please compile the library first:")
72-
print(" gcc -shared -fPIC -o liblinkbridge.so midi_clock_lib.c -lasound")
103+
print(f"[Python] Error: Library not found at {lib_path}")
104+
print("[Python] Please compile the library first:")
105+
print("[Python] gcc -shared -fPIC -o liblinkbridge.so midi_clock_lib.c -lasound")
73106
return 1
74107

75108
try:
76109
midi_lib = ctypes.CDLL(lib_path)
77110
except OSError as e:
78-
print(f"Error loading library: {e}")
111+
print(f"[Python] Error loading library: {e}")
79112
return 1
80113

81114
# Define function prototypes
@@ -93,6 +126,13 @@ def main():
93126
midi_lib.midi_set_tempo.restype = ctypes.c_int
94127
midi_lib.midi_set_tempo.argtypes = [ctypes.c_int]
95128

129+
# Setup event callback
130+
midi_lib.midi_register_event_callback.restype = None
131+
midi_lib.midi_register_event_callback.argtypes = [MIDI_EVENT_CALLBACK]
132+
133+
# Create a persistent reference to the callback to prevent garbage collection
134+
callback_func = MIDI_EVENT_CALLBACK(midi_event_callback)
135+
96136
print("[Python] Python MIDI Clock Generator")
97137
print("[Python] ============================")
98138
print(f"[Python] BPM: {BPM}, PPQN: {PPQN}")
@@ -103,6 +143,9 @@ def main():
103143
if midi_lib.midi_init() < 0:
104144
print("[Python] Error: Failed to initialize MIDI")
105145
return 1
146+
147+
# Register the event callback
148+
midi_lib.midi_register_event_callback(callback_func)
106149

107150
# Set tempo in the C queue to match Python BPM (send tenths as int)
108151
if midi_lib.midi_set_tempo(int(round(current_bpm * 10.0))) < 0:

midi_clock_lib.c

Lines changed: 123 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
#define PPQN 24
77
#define QUEUE_TEMPO_PPQ 96
88

9+
// Event callback type - called when MIDI events are received
10+
// event_type: one of the SND_SEQ_EVENT_* constants
11+
// channel: MIDI channel (0-15)
12+
// param1, param2, param3: event-specific parameters
13+
typedef void (*midi_event_callback_t)(int event_type, int channel, int param1, int param2, int param3);
14+
915
// Global handles
1016
static snd_seq_t *seq_handle = NULL;
1117
static int port_id = -1;
@@ -16,9 +22,21 @@ static char virtualMidiPortName[] = "LinkBridge MIDI Clock";
1622
previously queued events) */
1723
static snd_seq_tick_time_t max_scheduled_tick = 0;
1824

25+
// Global callback function pointer
26+
static midi_event_callback_t event_callback = NULL;
27+
1928
// Forward declarations
2029
static int has_write_capability(snd_seq_t *seq, int client, int port);
21-
// static int has_read_capability(snd_seq_t *seq, int client, int port);
30+
static int has_read_capability(snd_seq_t *seq, int client, int port);
31+
32+
// Register a callback function for MIDI events
33+
// Call this from Python to set up the event handler
34+
void midi_register_event_callback(midi_event_callback_t callback) {
35+
event_callback = callback;
36+
if (callback != NULL) {
37+
printf("[C] MIDI event callback registered\n");
38+
}
39+
}
2240

2341
// Connect to all existing MIDI ports on startup (except ports 0 and 14)
2442
static void midi_connect_all_ports(void) {
@@ -55,14 +73,14 @@ static void midi_connect_all_ports(void) {
5573

5674

5775
if (has_write_capability(seq_handle, client, port)) {
58-
printf("Connecting %s -> %d:%d\n", virtualMidiPortName, client, port);
76+
printf("[C] Connecting %s -> %d:%d\n", virtualMidiPortName, client, port);
5977
snd_seq_connect_to(seq_handle, port_id, client, port);
6078
}
6179

62-
// if (has_read_capability(seq_handle, client, port)) {
63-
// printf("Connecting %d:%d -> %s\n", client, port, virtualMidiPortName);
64-
// snd_seq_connect_from(seq_handle, port_id, client, port);
65-
// }
80+
if (has_read_capability(seq_handle, client, port)) {
81+
printf("[C] Connecting %d:%d -> %s\n", client, port, virtualMidiPortName);
82+
snd_seq_connect_from(seq_handle, port_id, client, port);
83+
}
6684
}
6785
}
6886
}
@@ -72,11 +90,11 @@ static void midi_connect_all_ports(void) {
7290
int midi_init(void) {
7391
int err;
7492
snd_seq_queue_tempo_t *queue_tempo;
75-
76-
// Open ALSA sequencer
93+
printf("[C] Initializing MIDI subsystem...\n");
94+
// Open ALSA sequencer
7795
err = snd_seq_open(&seq_handle, "default", SND_SEQ_OPEN_DUPLEX, 0);
7896
if (err < 0) {
79-
fprintf(stderr, "Error opening ALSA sequencer: %s\n", snd_strerror(err));
97+
fprintf(stderr, "[C] Error opening ALSA sequencer: %s\n", snd_strerror(err));
8098
return -1;
8199
}
82100

@@ -91,7 +109,7 @@ int midi_init(void) {
91109
SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ | SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE,
92110
SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION);
93111
if (port_id < 0) {
94-
fprintf(stderr, "Error creating port: %s\n", snd_strerror(port_id));
112+
fprintf(stderr, "[C] Error creating port: %s\n", snd_strerror(port_id));
95113
snd_seq_close(seq_handle);
96114
seq_handle = NULL;
97115
return -1;
@@ -103,17 +121,17 @@ int midi_init(void) {
103121
port_id,
104122
SND_SEQ_CLIENT_SYSTEM,
105123
SND_SEQ_PORT_SYSTEM_ANNOUNCE)) < 0) {
106-
fprintf(stderr, "Cannot subscribe to system announce: %s\n",
124+
fprintf(stderr, "[C] Cannot subscribe to system announce: %s\n",
107125
snd_strerror(err));
108126
exit(1);
109127
}
110128

111-
printf("Listening for PORT_START events...\n");
129+
printf("[C] Listening for PORT_START events...\n");
112130

113131
// Create queue
114132
queue_id = snd_seq_alloc_queue(seq_handle);
115133
if (queue_id < 0) {
116-
fprintf(stderr, "Error creating queue: %s\n", snd_strerror(queue_id));
134+
fprintf(stderr, "[C] Error creating queue: %s\n", snd_strerror(queue_id));
117135
snd_seq_close(seq_handle);
118136
seq_handle = NULL;
119137
return -1;
@@ -126,7 +144,7 @@ int midi_init(void) {
126144
snd_seq_queue_tempo_set_ppq(queue_tempo, QUEUE_TEMPO_PPQ);
127145
err = snd_seq_set_queue_tempo(seq_handle, queue_id, queue_tempo);
128146
if (err < 0) {
129-
fprintf(stderr, "Error setting queue tempo: %s\n", snd_strerror(err));
147+
fprintf(stderr, "[C] Error setting queue tempo: %s\n", snd_strerror(err));
130148
snd_seq_free_queue(seq_handle, queue_id);
131149
snd_seq_close(seq_handle);
132150
seq_handle = NULL;
@@ -150,11 +168,11 @@ int midi_init(void) {
150168
// Returns 0 on success, -1 on error
151169
int midi_set_tempo(int bpm10) {
152170
if (seq_handle == NULL) {
153-
fprintf(stderr, "Error: MIDI not initialized\n");
171+
fprintf(stderr, "[C] Error: MIDI not initialized\n");
154172
return -1;
155173
}
156174
if (bpm10 <= 0) {
157-
fprintf(stderr, "Error: invalid BPM (tenths) %d\n", bpm10);
175+
fprintf(stderr, "[C] Error: invalid BPM (tenths) %d\n", bpm10);
158176
return -1;
159177
}
160178

@@ -187,7 +205,7 @@ int midi_set_tempo(int bpm10) {
187205

188206
int err = snd_seq_event_output(seq_handle, &ev);
189207
if (err < 0) {
190-
fprintf(stderr, "Error enqueuing tempo event: %s\n", snd_strerror(err));
208+
fprintf(stderr, "[C] Error enqueuing tempo event: %s\n", snd_strerror(err));
191209
return -1;
192210
}
193211
snd_seq_drain_output(seq_handle);
@@ -201,7 +219,7 @@ int midi_set_tempo(int bpm10) {
201219
// Returns 0 on success, -1 on error
202220
int midi_send_start(void) {
203221
if (seq_handle == NULL) {
204-
fprintf(stderr, "Error: MIDI not initialized\n");
222+
fprintf(stderr, "[C] Error: MIDI not initialized\n");
205223
return -1;
206224
}
207225

@@ -230,7 +248,7 @@ int midi_send_start(void) {
230248
// Returns 0 on success, -1 on error
231249
int midi_send_clock(void) {
232250
if (seq_handle == NULL) {
233-
fprintf(stderr, "Error: MIDI not initialized\n");
251+
fprintf(stderr, "[C] Error: MIDI not initialized\n");
234252
return -1;
235253
}
236254

@@ -255,7 +273,7 @@ int midi_send_clock(void) {
255273
// Returns 0 on success, -1 on error
256274
int midi_send_stop(void) {
257275
if (seq_handle == NULL) {
258-
fprintf(stderr, "Error: MIDI not initialized\n");
276+
fprintf(stderr, "[C] Error: MIDI not initialized\n");
259277
return -1;
260278
}
261279

@@ -325,25 +343,25 @@ static int has_write_capability(snd_seq_t *seq, int client, int port)
325343
(caps & SND_SEQ_PORT_CAP_SUBS_WRITE);
326344
}
327345

328-
// // Check if the MIDI port is able to receive MIDI messages
329-
// static int has_read_capability(snd_seq_t *seq, int client, int port)
330-
// {
331-
// snd_seq_port_info_t *pinfo;
332-
// snd_seq_port_info_alloca(&pinfo);
346+
// Check if the MIDI port is able to receive MIDI messages
347+
static int has_read_capability(snd_seq_t *seq, int client, int port)
348+
{
349+
snd_seq_port_info_t *pinfo;
350+
snd_seq_port_info_alloca(&pinfo);
333351

334-
// if (snd_seq_get_any_port_info(seq, client, port, pinfo) < 0)
335-
// return 0;
352+
if (snd_seq_get_any_port_info(seq, client, port, pinfo) < 0)
353+
return 0;
336354

337-
// unsigned int caps = snd_seq_port_info_get_capability(pinfo);
355+
unsigned int caps = snd_seq_port_info_get_capability(pinfo);
338356

339-
// return (caps & SND_SEQ_PORT_CAP_READ) &&
340-
// (caps & SND_SEQ_PORT_CAP_SUBS_READ);
341-
// }
357+
return (caps & SND_SEQ_PORT_CAP_READ) &&
358+
(caps & SND_SEQ_PORT_CAP_SUBS_READ);
359+
}
342360

343361
// Poll ALSA sequencer events for PORT_START when a new MIDI device is connected
344362
int midi_read_events(void) {
345363
if (seq_handle == NULL) {
346-
fprintf(stderr, "Error: MIDI not initialized\n");
364+
fprintf(stderr, "[C] Error: MIDI not initialized\n");
347365
return -1;
348366
}
349367

@@ -362,11 +380,11 @@ int midi_read_events(void) {
362380
if (client == snd_seq_client_id(seq_handle))
363381
continue;
364382

365-
printf("New port: %d:%d\n", client, port);
383+
printf("[C] New port: %d:%d\n", client, port);
366384

367385
if (has_write_capability(seq_handle, client, port)) {
368386

369-
printf("Connecting %s -> %d:%d\n", virtualMidiPortName, client, port);
387+
printf("[C] Connecting %s -> %d:%d\n", virtualMidiPortName, client, port);
370388

371389
snd_seq_connect_to(
372390
seq_handle,
@@ -375,16 +393,79 @@ int midi_read_events(void) {
375393
port);
376394
}
377395

378-
// if (has_read_capability(seq_handle, client, port)) {
396+
if (has_read_capability(seq_handle, client, port)) {
379397

380-
// printf("Connecting %d:%d -> %s\n", client, port,virtualMidiPortName);
398+
printf("[C] Connecting %d:%d -> %s\n", client, port,virtualMidiPortName);
381399

382-
// snd_seq_connect_from(
383-
// seq_handle,
384-
// port_id,
385-
// client,
386-
// port);
387-
// }
400+
snd_seq_connect_from(
401+
seq_handle,
402+
port_id,
403+
client,
404+
port);
405+
}
406+
}
407+
else if (event->type == SND_SEQ_EVENT_NOTEON) {
408+
if (event_callback != NULL) {
409+
event_callback(SND_SEQ_EVENT_NOTEON,
410+
event->data.note.channel,
411+
event->data.note.note,
412+
event->data.note.velocity,
413+
0);
414+
}
415+
}
416+
else if (event->type == SND_SEQ_EVENT_NOTEOFF) {
417+
if (event_callback != NULL) {
418+
event_callback(SND_SEQ_EVENT_NOTEOFF,
419+
event->data.note.channel,
420+
event->data.note.note,
421+
event->data.note.velocity,
422+
0);
423+
}
424+
}
425+
else if (event->type == SND_SEQ_EVENT_CONTROLLER) {
426+
if (event_callback != NULL) {
427+
event_callback(SND_SEQ_EVENT_CONTROLLER,
428+
event->data.control.channel,
429+
event->data.control.param,
430+
event->data.control.value,
431+
0);
432+
}
433+
}
434+
else if (event->type == SND_SEQ_EVENT_PGMCHANGE) {
435+
if (event_callback != NULL) {
436+
event_callback(SND_SEQ_EVENT_PGMCHANGE,
437+
event->data.control.channel,
438+
event->data.control.value,
439+
0,
440+
0);
441+
}
442+
}
443+
else if (event->type == SND_SEQ_EVENT_PITCHBEND) {
444+
if (event_callback != NULL) {
445+
event_callback(SND_SEQ_EVENT_PITCHBEND,
446+
event->data.control.channel,
447+
event->data.control.value,
448+
0,
449+
0);
450+
}
451+
}
452+
else if (event->type == SND_SEQ_EVENT_CHANPRESS) {
453+
if (event_callback != NULL) {
454+
event_callback(SND_SEQ_EVENT_CHANPRESS,
455+
event->data.control.channel,
456+
event->data.control.value,
457+
0,
458+
0);
459+
}
460+
}
461+
else if (event->type == SND_SEQ_EVENT_KEYPRESS) {
462+
if (event_callback != NULL) {
463+
event_callback(SND_SEQ_EVENT_KEYPRESS,
464+
event->data.note.channel,
465+
event->data.note.note,
466+
event->data.note.velocity,
467+
0);
468+
}
388469
}
389470
count++;
390471
}

0 commit comments

Comments
 (0)