aboutsummaryrefslogtreecommitdiff
path: root/midimcast-client.c
diff options
context:
space:
mode:
Diffstat (limited to 'midimcast-client.c')
-rw-r--r--midimcast-client.c229
1 files changed, 229 insertions, 0 deletions
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);
+}