diff options
author | rcerc <88944439+rcerc@users.noreply.github.com> | 2025-06-19 16:39:47 -0400 |
---|---|---|
committer | rcerc <88944439+rcerc@users.noreply.github.com> | 2025-06-19 18:20:38 -0400 |
commit | 5179ace697f702d7103c9aa6e26478363e0f6a4d (patch) | |
tree | 92c3b9393f3ea2d197400368cf106ffa89f14c6b |
Rudimentary streaming of raw MIDI over multicast UDP
-rw-r--r-- | Makefile | 15 | ||||
-rw-r--r-- | default.nix | 1 | ||||
-rw-r--r-- | midimcast-client.c | 229 | ||||
-rw-r--r-- | midimcast-server.c | 153 | ||||
-rw-r--r-- | midimcast.c | 33 | ||||
-rw-r--r-- | midimcast.h | 111 | ||||
-rw-r--r-- | midimcast.nix | 27 |
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 + ''; +} |