From 5179ace697f702d7103c9aa6e26478363e0f6a4d Mon Sep 17 00:00:00 2001 From: rcerc <88944439+rcerc@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:39:47 -0400 Subject: Rudimentary streaming of raw MIDI over multicast UDP --- midimcast-client.c | 229 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 midimcast-client.c (limited to 'midimcast-client.c') 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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); +} -- cgit v1.2.3