aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorrcerc <88944439+rcerc@users.noreply.github.com>2025-06-19 16:39:47 -0400
committerrcerc <88944439+rcerc@users.noreply.github.com>2025-06-19 18:20:38 -0400
commit5179ace697f702d7103c9aa6e26478363e0f6a4d (patch)
tree92c3b9393f3ea2d197400368cf106ffa89f14c6b
Rudimentary streaming of raw MIDI over multicast UDP
-rw-r--r--Makefile15
-rw-r--r--default.nix1
-rw-r--r--midimcast-client.c229
-rw-r--r--midimcast-server.c153
-rw-r--r--midimcast.c33
-rw-r--r--midimcast.h111
-rw-r--r--midimcast.nix27
7 files changed, 569 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..203d5d0
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,15 @@
+CFLAGS = -Wall -Wextra -Werror
+
+.PHONY: all install
+
+all: ${out}/bin/midimcast-client ${out}/bin/midimcast-server
+
+install: all
+
+${out}/bin/midimcast-client: ${src}/midimcast-client.c ${src}/midimcast.c
+ mkdir -p ${out}/bin
+ ${CC} ${CFLAGS} -o $@ $^
+
+${out}/bin/midimcast-server: ${src}/midimcast-server.c ${src}/midimcast.c
+ mkdir -p ${out}/bin
+ ${CC} ${CFLAGS} -lasound -o $@ $^
diff --git a/default.nix b/default.nix
new file mode 100644
index 0000000..9748e35
--- /dev/null
+++ b/default.nix
@@ -0,0 +1 @@
+(import <nixpkgs> { }).callPackage ./midimcast.nix { }
diff --git a/midimcast-client.c b/midimcast-client.c
new file mode 100644
index 0000000..a8904cc
--- /dev/null
+++ b/midimcast-client.c
@@ -0,0 +1,229 @@
+#include <arpa/inet.h>
+#include <endian.h>
+#include <poll.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "midimcast.h"
+
+bool midimcast_debug;
+
+// Container of messsage to be relayed
+struct baton {
+ struct baton *next;
+ size_t midi_size;
+ struct msg msg;
+};
+
+static void env_read_client(long *const delay, const char **const soundfont)
+{
+ const char *const delay_str = getenv("RELAY_DELAY");
+ if (delay_str) {
+ errno = 0;
+ *delay = strtol(delay_str, NULL, 0);
+ if (errno)
+ err_std_nchk(strtol);
+ } else {
+ *delay = 3000;
+ }
+ debug("Using intentional relay delay %ld ms", *delay);
+
+ *soundfont = getenv("SOUNDFONT");
+ if (!soundfont)
+ err("`$SOUNDFONT` unset");
+ debug("Using soundfont %s", *soundfont);
+}
+
+static int in_sock_init(struct sockaddr_in *const addr,
+ const char *const group,
+ const uint16_t port) {
+ const int sock = err_std_neg(socket, AF_INET, SOCK_DGRAM, 0);
+
+ // Allow immediate address reuse
+ int reuse_addr = true;
+ err_std(setsockopt,
+ sock,
+ SOL_SOCKET,
+ SO_REUSEADDR,
+ (void *)&reuse_addr,
+ sizeof(reuse_addr));
+
+ // Bind address to socket
+ memset(addr, 0, sizeof(*addr));
+ addr->sin_family = AF_INET;
+ addr->sin_addr.s_addr = htonl(INADDR_ANY);
+ addr->sin_port = htons(port);
+ err_std(bind, sock, (struct sockaddr *)addr, sizeof(*addr));
+
+ // Join multicast group
+ struct ip_mreq mreq;
+ mreq.imr_multiaddr.s_addr = inet_addr(group);
+ mreq.imr_interface.s_addr = htonl(INADDR_ANY);
+ err_std(setsockopt,
+ sock,
+ IPPROTO_IP,
+ IP_ADD_MEMBERSHIP,
+ (void *)&mreq,
+ sizeof(mreq));
+
+ return sock;
+}
+
+static size_t msg_read(struct msg *const msg,
+ const int sock,
+ const struct sockaddr_in *const addr) {
+ socklen_t addr_size = sizeof(*addr); // Overwritten below
+ const ssize_t nread = err_std_neg(recvfrom,
+ sock,
+ msg,
+ sizeof(*msg),
+ 0,
+ (struct sockaddr *)addr,
+ &addr_size);
+
+ debug("Message length %zu", nread);
+ if (nread > (ssize_t)sizeof(*msg))
+ err("Message longer than %zu", sizeof(*msg));
+
+ const size_t header_size = ((void *)&msg->midi - (void *)&msg->ts);
+ if (nread < (ssize_t)header_size)
+ err("Message shorter than %zu", header_size);
+
+ msg->ts = le64toh(msg->ts);
+
+ return nread - header_size;
+}
+
+void baton_read(struct baton *const baton,
+ const int in_sock,
+ const struct sockaddr_in *const addr) {
+ baton->midi_size = msg_read(&baton->msg, in_sock, addr);
+
+ const uint64_t ts_dst = now();
+ int64_t latency = (int64_t)ts_dst - (int64_t)baton->msg.ts;
+ debug("Message received: ts_src = %lu, ts_dst = %lu, latency = %ld, midi_size = %lu",
+ baton->msg.ts,
+ ts_dst,
+ latency,
+ baton->midi_size);
+ if (latency < 0)
+ warn("Clocks unsynchronised by over %lu ms", -latency);
+}
+
+void baton_put(struct baton *baton, struct baton **const first_baton)
+{
+ // TODO: Simplify conditionals
+ struct baton *prev_baton = *first_baton;
+ struct baton *next_baton = *first_baton ? (**first_baton).next : NULL;
+ while (next_baton && next_baton->msg.ts < baton->msg.ts) {
+ prev_baton = next_baton;
+ next_baton = next_baton->next;
+ }
+ baton->next = next_baton;
+ if (prev_baton)
+ prev_baton->next = baton;
+ else
+ *first_baton = baton;
+}
+
+int timeout(const struct baton *const first_baton, const long delay)
+{
+ if (first_baton) {
+ uint64_t have = now(), want = first_baton->msg.ts + delay;
+ if (have < want) {
+ const int timeout = want - have;
+ debug("Relaying in %d ms", timeout);
+ return timeout;
+ } else {
+ warn("Relaying %lu ms late", have - want);
+ return 0;
+ }
+ } else {
+ debug("Nothing to relay right now");
+ return -1;
+ }
+}
+
+void baton_write(const struct baton *const baton, FILE *const out_file)
+{
+ size_t write_count = fwrite(baton->msg.midi,
+ 1,
+ baton->midi_size,
+ out_file);
+ if (write_count < baton->midi_size)
+ err_std_nchk(fwrite);
+ err_std(fflush, out_file);
+}
+
+struct baton *baton_take(struct baton **const first_baton)
+{
+ struct baton *const first_baton_old = *first_baton;
+ *first_baton = (**first_baton).next;
+ return first_baton_old;
+}
+
+void batons_relay(const char *const mcast_group,
+ const uint16_t mcast_port,
+ const long delay,
+ const int out_fd) {
+ struct sockaddr_in addr;
+ const int in_sock = in_sock_init(&addr, mcast_group, mcast_port);
+ struct pollfd in_sock_poll = {.fd = in_sock, .events = POLLIN};
+
+ struct baton *first_baton = NULL;
+
+ FILE *const out_file = err_std_null(fdopen, out_fd, "w");
+
+ for (;;) {
+ if (err_std_neg(poll, &in_sock_poll, 1, timeout(first_baton, delay))) {
+ struct baton *baton = err_std_null(malloc, sizeof(*baton));
+ baton_read(baton, in_sock, &addr);
+ baton_put(baton, &first_baton);
+ } else {
+ baton_write(first_baton, out_file);
+ free(baton_take(&first_baton));
+ }
+ }
+}
+
+void synthesise(const int fd, const char *const soundfont)
+{
+
+ err_std(dup2, fd, STDIN_FILENO);
+
+ debug("Starting synthesiser");
+ err_std(execlp,
+ "fluidsynth",
+ "fluidsynth",
+ "--no-shell",
+ "--server",
+ "--gain=1",
+ "--audio-driver=pulseaudio",
+ "-o", "midi.driver=oss",
+ "-o", "midi.oss.device=/dev/stdin",
+ soundfont,
+ NULL);
+}
+
+int main()
+{
+ const char *mcast_group;
+ uint16_t mcast_port;
+ long delay;
+ const char *soundfont;
+ env_read_common(&mcast_group, &mcast_port);
+ env_read_client(&delay, &soundfont);
+
+ int pipes_fds[2];
+ err_std(pipe, pipes_fds);
+
+ if (err_std_neg(fork))
+ batons_relay(mcast_group, mcast_port, delay, pipes_fds[1]);
+ else
+ synthesise(pipes_fds[0], soundfont);
+}
diff --git a/midimcast-server.c b/midimcast-server.c
new file mode 100644
index 0000000..a731668
--- /dev/null
+++ b/midimcast-server.c
@@ -0,0 +1,153 @@
+#include <alsa/asoundlib.h>
+#include <arpa/inet.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include "midimcast.h"
+
+bool midimcast_debug;
+
+int snd_rawmidi_virtual_open(snd_rawmidi_t **inputp,
+ snd_rawmidi_t **outputp,
+ const char *name,
+ snd_seq_t *seq_handle,
+ int port,
+ int merge,
+ int mode);
+
+static void env_read_server(const char **const midi_in)
+{
+ *midi_in = getenv("MIDI_IN");
+ if (!*midi_in)
+ err("`MIDI_IN` unset");
+ debug("Using MIDI input %s", *midi_in);
+}
+
+// TODO: Debug messages
+// TODO: Const stuff
+static void midi_open(const char *const midi_in,
+ snd_seq_t **seq,
+ snd_rawmidi_t **input)
+{
+ err_snd(snd_seq_open, seq, "default", SND_SEQ_OPEN_INPUT, 0);
+ err_snd(snd_seq_set_client_name, *seq, "MIDI Broadcaster");
+
+ const int port = snd_seq_create_simple_port(*seq,
+ "MIDI Broadcaster Input",
+ SND_SEQ_PORT_CAP_WRITE
+ | SND_SEQ_PORT_CAP_SYNC_WRITE
+ | SND_SEQ_PORT_CAP_SUBS_WRITE,
+ SND_SEQ_PORT_TYPE_MIDI_GENERIC);
+
+ err_snd(snd_rawmidi_virtual_open,
+ input,
+ NULL,
+ "MIDI Broadcaster",
+ *seq,
+ port,
+ true,
+ SND_RAWMIDI_READ_STANDARD);
+
+ snd_seq_client_info_t *client_info;
+ snd_seq_client_info_alloca(&client_info);
+ snd_seq_client_info_set_client(client_info, -1);
+ snd_seq_addr_t src, dst;
+ for (;;) {
+ if (snd_seq_query_next_client(*seq, client_info))
+ err("Input '%s' absent", midi_in);
+
+ if (strcmp(snd_seq_client_info_get_name(client_info), midi_in) == 0) {
+ src.client = snd_seq_client_info_get_client(client_info);
+
+ snd_seq_port_info_t *port_info;
+ snd_seq_port_info_alloca(&port_info);
+ snd_seq_port_info_set_client(port_info, src.client);
+ snd_seq_port_info_set_port(port_info, -1);
+ if (snd_seq_query_next_port(*seq, port_info))
+ err("Input '%s' has no ports", midi_in);
+ src.port = snd_seq_port_info_get_port(port_info);
+
+ break;
+ }
+ }
+
+ dst.client = snd_seq_client_id(*seq);
+ dst.port = port;
+
+ snd_seq_port_subscribe_t *sub;
+ snd_seq_port_subscribe_alloca(&sub);
+ snd_seq_port_subscribe_set_sender(sub, &src);
+ snd_seq_port_subscribe_set_dest(sub, &dst);
+ err_snd(snd_seq_subscribe_port, *seq, sub);
+}
+
+// TODO: Naming
+static int sock_init(struct sockaddr_in *addr,
+ const char *const mcast_group,
+ const uint16_t mcast_port)
+{
+ memset(addr, 0, sizeof(*addr));
+ addr->sin_family = AF_INET;
+ addr->sin_addr.s_addr = inet_addr(mcast_group);
+ addr->sin_port = htons(mcast_port);
+
+ return err_std_neg(socket, AF_INET, SOCK_DGRAM, 0);
+}
+
+static void msg_write(struct msg *const msg,
+ const size_t midi_size,
+ const int out_sock,
+ struct sockaddr_in *addr)
+{
+ msg->ts = htole64(msg->ts);
+ const size_t header_size = (void *)&msg->midi - (void *)&msg->ts;
+ const size_t msg_size = header_size + midi_size;
+ err_std_neg(sendto,
+ out_sock,
+ msg,
+ msg_size,
+ 0,
+ (struct sockaddr *)addr,
+ sizeof(*addr));
+ // TODO: Check enough written?
+}
+
+int main()
+{
+ const char *mcast_group;
+ uint16_t mcast_port;
+ const char *midi_in;
+ env_read_common(&mcast_group, &mcast_port);
+ env_read_server(&midi_in);
+
+ snd_seq_t *seq;
+ snd_rawmidi_t *input;
+ midi_open(midi_in, &seq, &input);
+
+ struct sockaddr_in addr;
+ const int sock = sock_init(&addr, mcast_group, mcast_port);
+
+ for (;;) {
+ struct msg msg;
+
+ ssize_t nread = err_snd_neg(snd_rawmidi_read,
+ input,
+ &msg.midi,
+ sizeof(msg.midi));
+ debug("Read %ld bytes", nread);
+
+ msg.ts = now();
+ msg_write(&msg, nread, sock, &addr);
+ }
+
+ // TODO: Make these run
+ err_snd(snd_rawmidi_close, input);
+ err_snd(snd_seq_close, seq);
+ err_std(close, sock);
+
+ return EXIT_SUCCESS;
+}
diff --git a/midimcast.c b/midimcast.c
new file mode 100644
index 0000000..6ad047f
--- /dev/null
+++ b/midimcast.c
@@ -0,0 +1,33 @@
+#include <inttypes.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <time.h>
+
+#include "midimcast.h"
+
+void env_read_common(const char **const mcast_group, uint16_t *const mcast_port)
+{
+ midimcast_debug = getenv("DEBUG") != NULL;
+
+ *mcast_group = getenv("MCAST_GROUP");
+ if (!*mcast_group)
+ *mcast_group = "224.212.21.175";
+ debug("Using multicast group %s", *mcast_group);
+
+ const char *const mcast_port_str = getenv("MCAST_PORT");
+ if (mcast_port_str) {
+ const long mcast_port_untrunc = strtol(mcast_port_str, NULL, 0);
+ if (mcast_port_untrunc < 0 || mcast_port_untrunc > UINT16_MAX)
+ err("Port %ld does not exist", mcast_port_untrunc);
+ *mcast_port = mcast_port_untrunc;
+ } else {
+ *mcast_port = 8000;
+ }
+ debug("Using multicast port %" PRIu16, *mcast_port);
+}
+
+uint64_t now() {
+ struct timespec ts;
+ err_std(clock_gettime, CLOCK_REALTIME, &ts);
+ return ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
+}
diff --git a/midimcast.h b/midimcast.h
new file mode 100644
index 0000000..26cab34
--- /dev/null
+++ b/midimcast.h
@@ -0,0 +1,111 @@
+#ifndef __MIDIMCAST_H__
+#define __MIDIMCAST_H__
+
+#include <errno.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define debug(FMT, ...) \
+ if (midimcast_debug) { \
+ fprintf(stderr, \
+ "[DEBUG] %s:%u:%s: " FMT "\n", \
+ __FILE__, \
+ __LINE__, \
+ __func__ __VA_OPT__(,) \
+ __VA_ARGS__); \
+ }
+
+#define warn(FMT, ...) \
+ fprintf(stderr, "[WARNING] %s:%u:%s: " FMT "\n", \
+ __FILE__, \
+ __LINE__, \
+ __func__ __VA_OPT__(,) \
+ __VA_ARGS__);
+
+#define err(FMT, ...) \
+ ({ \
+ fprintf(stderr, "[ERROR] %s:%u:%s: " FMT "\n", \
+ __FILE__, \
+ __LINE__, \
+ __func__ __VA_OPT__(,) \
+ __VA_ARGS__); \
+ exit(EXIT_FAILURE); \
+ })
+
+#define err_std_nchk(CALLEE) \
+ ({ \
+ fprintf(stderr, "[ERROR] %s:%u:%s:%s: %s\n", \
+ __FILE__, \
+ __LINE__, \
+ __func__, \
+ #CALLEE, \
+ strerror(errno)); \
+ exit(EXIT_FAILURE); \
+ })
+
+#define err_std(CALLEE, ...) \
+ if (CALLEE(__VA_ARGS__)) \
+ err_std_nchk(CALLEE);
+
+#define err_std_neg(CALLEE, ...) \
+ ({ \
+ const typeof(CALLEE(__VA_ARGS__)) ret = \
+ CALLEE(__VA_ARGS__); \
+ if (ret < 0) \
+ err_std_nchk(CALLEE); \
+ ret; \
+ })
+
+#define err_std_null(CALLEE, ...) \
+ ({ \
+ const typeof(CALLEE(__VA_ARGS__)) ret = \
+ CALLEE(__VA_ARGS__); \
+ if (!ret) \
+ err_std_nchk(CALLEE); \
+ ret; \
+ })
+
+#define err_snd_nchk(CALLEE, ERR) \
+ ({ \
+ fprintf(stderr, "[ERROR] %s:%u:%s:%s: %s\n", \
+ __FILE__, \
+ __LINE__, \
+ __func__, \
+ #CALLEE, \
+ snd_strerror(ERR)); \
+ exit(EXIT_FAILURE); \
+ })
+
+
+#define err_snd(CALLEE, ...) \
+ ({ \
+ const int err = CALLEE(__VA_ARGS__); \
+ if (err) \
+ err_snd_nchk(CALLEE, err); \
+ })
+
+#define err_snd_neg(CALLEE, ...) \
+ ({ \
+ const typeof(CALLEE(__VA_ARGS__)) ret \
+ = CALLEE(__VA_ARGS__); \
+ if (ret < 0) \
+ err_snd_nchk(CALLEE, ret); \
+ ret; \
+ })
+
+struct msg {
+ uint64_t ts;
+ uint8_t midi[0x10]; // Large enough buffer
+} __attribute__((packed));
+
+extern uint64_t now();
+
+extern bool midimcast_debug;
+
+void env_read_common(const char **const mcast_group,
+ uint16_t *const mcast_port);
+
+#endif
diff --git a/midimcast.nix b/midimcast.nix
new file mode 100644
index 0000000..c52d720
--- /dev/null
+++ b/midimcast.nix
@@ -0,0 +1,27 @@
+{
+ lib,
+ stdenv,
+ makeBinaryWrapper,
+ alsa-lib,
+ fluidsynth,
+ soundfont-generaluser,
+}:
+stdenv.mkDerivation rec {
+ name = "midimcast";
+ src = ./.;
+ nativeBuildInputs = [ makeBinaryWrapper ];
+ buildInputs = [
+ alsa-lib
+ fluidsynth
+ ];
+ SOUNDFONT = "${soundfont-generaluser}/share/soundfonts/GeneralUser-GS.sf2";
+ fixupPhase = ''
+ runHook preFixup
+
+ wrapProgram $out/bin/midimcast-client \
+ --set-default SOUNDFONT "$SOUNDFONT" \
+ --suffix PATH : ${lib.makeBinPath buildInputs}
+
+ runHook postFixup
+ '';
+}