root/usr.bin/sndiod/midi.c
/*      $OpenBSD: midi.c,v 1.32 2024/12/20 07:35:56 ratchov Exp $       */
/*
 * Copyright (c) 2008-2012 Alexandre Ratchov <alex@caoua.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "abuf.h"
#include "defs.h"
#include "dev.h"
#include "file.h"
#include "midi.h"
#include "miofile.h"
#include "sysex.h"
#include "utils.h"

int  port_open(struct port *);
void port_imsg(void *, unsigned char *, int);
void port_omsg(void *, unsigned char *, int);
void port_fill(void *, int);
void port_exit(void *);

struct midiops port_midiops = {
        port_imsg,
        port_omsg,
        port_fill,
        port_exit
};

#define MIDI_NEP 32
struct midi midi_ep[MIDI_NEP];
struct port *port_list = NULL;
unsigned int midi_portnum = 0;

struct midithru {
        unsigned int txmask, rxmask;
#define MIDITHRU_NMAX 32
} midithru[MIDITHRU_NMAX];

/*
 * length of voice and common messages (status byte included)
 */
const unsigned int voice_len[] = { 3, 3, 3, 3, 2, 2, 3 };
const unsigned int common_len[] = { 0, 2, 3, 2, 0, 0, 1, 1 };

size_t
midiev_fmt(char *buf, size_t size, unsigned char *ev, size_t len)
{
        const char *sep = "";
        char *end = buf + size;
        char *p = buf;
        int i;

        for (i = 0; i < len; i++) {
                if (i == 1)
                        sep = " ";
                p += snprintf(p, p < end ? end - p : 0, "%s%02x", sep, ev[i]);
        }

        return p - buf;
}

void
midi_init(void)
{
}

void
midi_done(void)
{
}

struct midi *
midi_new(struct midiops *ops, void *arg, int mode)
{
        int i;
        struct midi *ep;

        for (i = 0, ep = midi_ep;; i++, ep++) {
                if (i == MIDI_NEP)
                        return NULL;
                if (ep->ops == NULL)
                        break;
        }
        ep->ops = ops;
        ep->arg = arg;
        ep->used = 0;
        ep->len = 0;
        ep->idx = 0;
        ep->st = 0;
        ep->last_st = 0;
        ep->txmask = 0;
        ep->num = i;
        ep->self = 1 << i;
        ep->tickets = 0;
        ep->mode = mode;

        /*
         * the output buffer is the client input
         */
        if (ep->mode & MODE_MIDIIN)
                abuf_init(&ep->obuf, MIDI_BUFSZ);
        midi_tickets(ep);
        return ep;
}

void
midi_del(struct midi *ep)
{
        int i;
        struct midi *peer;

        ep->txmask = 0;
        for (i = 0; i < MIDI_NEP; i++) {
                peer = midi_ep + i;
                if (peer->txmask & ep->self) {
                        peer->txmask &= ~ep->self;
                        midi_tickets(peer);
                }
        }
        for (i = 0; i < MIDITHRU_NMAX; i++) {
                midithru[i].txmask &= ~ep->self;
                midithru[i].rxmask &= ~ep->self;
        }
        ep->ops = NULL;
        if (ep->mode & MODE_MIDIIN) {
                abuf_done(&ep->obuf);
        }
}

/*
 * connect two midi endpoints
 */
void
midi_link(struct midi *ep, struct midi *peer)
{
        if (ep->mode & MODE_MIDIOUT) {
                ep->txmask |= peer->self;
                midi_tickets(ep);
        }
        if (ep->mode & MODE_MIDIIN) {
#ifdef DEBUG
                if (ep->obuf.used > 0) {
                        logx(0, "midi%u: linked with non-empty buffer", ep->num);
                        panic();
                }
#endif
                /* ep has empty buffer, so no need to call midi_tickets() */
                peer->txmask |= ep->self;
        }
}

/*
 * return the list of endpoints the given one receives from
 */
unsigned int
midi_rxmask(struct midi *ep)
{
        int i, rxmask;

        for (rxmask = 0, i = 0; i < MIDI_NEP; i++) {
                if ((midi_ep[i].txmask & ep->self) == 0)
                        continue;
                rxmask |= midi_ep[i].self;
        }

        return rxmask;
}

/*
 * add the midi endpoint in the ``tag'' midi thru box
 */
void
midi_tag(struct midi *ep, unsigned int tag)
{
        struct midi *peer;
        struct midithru *t = midithru + tag;
        int i;

        if (ep->mode & MODE_MIDIOUT) {
                ep->txmask |= t->txmask;
                midi_tickets(ep);
        }
        if (ep->mode & MODE_MIDIIN) {
#ifdef DEBUG
                if (ep->obuf.used > 0) {
                        logx(0, "midi%u: tagged with non-empty buffer", ep->num);
                        panic();
                }
#endif
                for (i = 0; i < MIDI_NEP; i++) {
                        if (!(t->rxmask & (1 << i)))
                                continue;
                        peer = midi_ep + i;
                        peer->txmask |= ep->self;
                }
        }
        if (ep->mode & MODE_MIDIOUT)
                t->rxmask |= ep->self;
        if (ep->mode & MODE_MIDIIN)
                t->txmask |= ep->self;
}

/*
 * return the list of tags
 */
unsigned int
midi_tags(struct midi *ep)
{
        int i;
        struct midithru *t;
        unsigned int tags;

        tags = 0;
        for (i = 0; i < MIDITHRU_NMAX; i++) {
                t = midithru + i;
                if ((t->txmask | t->rxmask) & ep->self)
                        tags |= 1 << i;
        }
        return tags;
}

/*
 * broadcast the given message to other endpoints
 */
void
midi_send(struct midi *iep, unsigned char *msg, int size)
{
#ifdef DEBUG
        char str[128];
#endif
        struct midi *oep;
        int i;

#ifdef DEBUG
        logx(4, "midi%u: sending: %s", iep->num,
            (midiev_fmt(str, sizeof(str), msg, size), str));
#endif
        for (i = 0; i < MIDI_NEP ; i++) {
                if ((iep->txmask & (1 << i)) == 0)
                        continue;
                oep = midi_ep + i;
                if (msg[0] <= 0x7f) {
                        if (oep->owner != iep)
                                continue;
                } else if (msg[0] <= 0xf7)
                        oep->owner = iep;
#ifdef DEBUG
                logx(4, "midi%u -> midi%u", iep->num, oep->num);
#endif
                oep->ops->omsg(oep->arg, msg, size);
        }
}

/*
 * determine if we have gained more input tickets, and if so call the
 * fill() call-back to notify the i/o layer that it can send more data
 */
void
midi_tickets(struct midi *iep)
{
        int i, tickets, avail, maxavail;
        struct midi *oep;

        /*
         * don't request iep->ops->fill() too often as it generates
         * useless network traffic: wait until we reach half of the
         * max tickets count. As in the worst case (see comment below)
         * one ticket may consume two bytes, the max ticket count is
         * BUFSZ / 2 and halt of it is simply BUFSZ / 4.
         */
        if (iep->tickets >= MIDI_BUFSZ / 4)
                return;

        maxavail = MIDI_BUFSZ;
        for (i = 0; i < MIDI_NEP ; i++) {
                if ((iep->txmask & (1 << i)) == 0)
                        continue;
                oep = midi_ep + i;
                avail = oep->obuf.len - oep->obuf.used;
                if (maxavail > avail)
                        maxavail = avail;
        }

        /*
         * in the worst case output message is twice the
         * input message (2-byte messages with running status)
         */
        tickets = maxavail / 2 - iep->tickets;
        if (tickets > 0) {
                iep->tickets += tickets;
                iep->ops->fill(iep->arg, tickets);
        }
}

/*
 * recalculate tickets of endpoints sending data to this one
 */
void
midi_fill(struct midi *oep)
{
        int i;
        struct midi *iep;

        for (i = 0; i < MIDI_NEP; i++) {
                iep = midi_ep + i;
                if (iep->txmask & oep->self)
                        midi_tickets(iep);
        }
}

/*
 * parse then give data chunk, and calling imsg() for each message
 */
void
midi_in(struct midi *iep, unsigned char *idata, int icount)
{
        int i;
        unsigned char c;

        for (i = 0; i < icount; i++) {
                c = *idata++;
                if (c >= 0xf8) {
                        if (c != MIDI_ACK)
                                iep->ops->imsg(iep->arg, &c, 1);
                } else if (c == SYSEX_END) {
                        if (iep->st == SYSEX_START) {
                                iep->msg[iep->idx++] = c;
                                iep->ops->imsg(iep->arg, iep->msg, iep->idx);
                        }

                        /*
                         * There are bogus MIDI sources that keep
                         * state across sysex; Linux virmidi ports fed
                         * by the sequencer is an example. We
                         * workaround this by saving the current
                         * status and restoring it at the end of the
                         * sysex.
                         */
                        iep->st = iep->last_st;
                        if (iep->st)
                                iep->len = voice_len[(iep->st >> 4) & 7];
                        iep->idx = 0;
                } else if (c >= 0xf0) {
                        iep->msg[0] = c;
                        iep->len = common_len[c & 7];
                        iep->st = c;
                        iep->idx = 1;
                } else if (c >= 0x80) {
                        iep->msg[0] = c;
                        iep->len = voice_len[(c >> 4) & 7];
                        iep->last_st = iep->st = c;
                        iep->idx = 1;
                } else if (iep->st) {
                        if (iep->idx == 0 && iep->st != SYSEX_START)
                                iep->msg[iep->idx++] = iep->st;
                        iep->msg[iep->idx++] = c;
                        if (iep->idx == iep->len) {
                                iep->ops->imsg(iep->arg, iep->msg, iep->idx);
                                if (iep->st >= 0xf0)
                                        iep->st = 0;
                                iep->idx = 0;
                        } else if (iep->idx == MIDI_MSGMAX) {
                                /* sysex continued */
                                iep->ops->imsg(iep->arg, iep->msg, iep->idx);
                                iep->idx = 0;
                        }
                }
        }
        iep->tickets -= icount;
        if (iep->tickets < 0)
                iep->tickets = 0;
        midi_tickets(iep);
}

/*
 * store the given message in the output buffer
 */
void
midi_out(struct midi *oep, unsigned char *idata, int icount)
{
#ifdef DEBUG
        char str[128];
#endif
        unsigned char *odata;
        int ocount;

        while (icount > 0) {
                if (oep->obuf.used == oep->obuf.len) {
#ifdef DEBUG
                        logx(2, "midi%u: too slow, discarding %d bytes",
                            oep->num, oep->obuf.used);
#endif
                        abuf_rdiscard(&oep->obuf, oep->obuf.used);
                        oep->owner = NULL;
                        return;
                }
                odata = abuf_wgetblk(&oep->obuf, &ocount);
                if (ocount > icount)
                        ocount = icount;
                memcpy(odata, idata, ocount);
#ifdef DEBUG
                logx(4, "midi%u: out: %s", oep->num,
                    (midiev_fmt(str, sizeof(str), odata, ocount), str));
#endif
                abuf_wcommit(&oep->obuf, ocount);
                icount -= ocount;
                idata += ocount;
        }
}

/*
 * disconnect clients attached to this end-point
 */
void
midi_abort(struct midi *p)
{
        int i;
        struct midi *ep;

        for (i = 0; i < MIDI_NEP; i++) {
                ep = midi_ep + i;
                if ((ep->txmask & p->self) || (p->txmask & ep->self))
                        ep->ops->exit(ep->arg);
        }
}

/*
 * connect to "nep" all endpoints currently connected to "oep"
 */
void
midi_migrate(struct midi *oep, struct midi *nep)
{
        struct midithru *t;
        struct midi *ep;
        int i;

        for (i = 0; i < MIDITHRU_NMAX; i++) {
                t = midithru + i;
                if (t->txmask & oep->self) {
                        t->txmask &= ~oep->self;
                        t->txmask |= nep->self;
                }
                if (t->rxmask & oep->self) {
                        t->rxmask &= ~oep->self;
                        t->rxmask |= nep->self;
                }
        }

        for (i = 0; i < MIDI_NEP; i++) {
                ep = midi_ep + i;
                if (ep->txmask & oep->self) {
                        ep->txmask &= ~oep->self;
                        ep->txmask |= nep->self;
                }
        }

        for (i = 0; i < MIDI_NEP; i++) {
                ep = midi_ep + i;
                if (oep->txmask & ep->self) {
                        oep->txmask &= ~ep->self;
                        nep->txmask |= ep->self;
                }
        }
}

void
port_imsg(void *arg, unsigned char *msg, int size)
{
        struct port *p = arg;

        midi_send(p->midi, msg, size);
}


void
port_omsg(void *arg, unsigned char *msg, int size)
{
        struct port *p = arg;

        midi_out(p->midi, msg, size);
}

void
port_fill(void *arg, int count)
{
        /* no flow control */
}

void
port_exit(void *arg)
{
#ifdef DEBUG
        struct port *p = arg;

        logx(0, "midi%u: port exit", p->midi->num);
        panic();
#endif
}

/*
 * create a new midi port
 */
struct port *
port_new(char *path, unsigned int mode, int hold)
{
        struct port *c;

        c = xmalloc(sizeof(struct port));
        c->path = path;
        c->state = PORT_CFG;
        c->hold = hold;
        c->midi = midi_new(&port_midiops, c, mode);
        c->num = midi_portnum++;
        c->alt_next = c;
        c->next = port_list;
        port_list = c;
        return c;
}

/*
 * destroy the given midi port
 */
void
port_del(struct port *c)
{
        struct port **p;

        if (c->state != PORT_CFG)
                port_close(c);
        midi_del(c->midi);
        for (p = &port_list; *p != c; p = &(*p)->next) {
#ifdef DEBUG
                if (*p == NULL) {
                        logx(0, "port to delete not on list");
                        panic();
                }
#endif
        }
        *p = c->next;
        xfree(c);
}

int
port_ref(struct port *c)
{
#ifdef DEBUG
        logx(3, "midi%u: port requested", c->midi->num);
#endif
        if (c->state == PORT_CFG && !port_open(c))
                return 0;
        return 1;
}

void
port_unref(struct port *c)
{
        int i, rxmask;

#ifdef DEBUG
        logx(3, "midi%u: port released", c->midi->num);
#endif
        for (rxmask = 0, i = 0; i < MIDI_NEP; i++)
                rxmask |= midi_ep[i].txmask;
        if ((rxmask & c->midi->self) == 0 && c->midi->txmask == 0 &&
            c->state == PORT_INIT && !c->hold)
                port_drain(c);
}

struct port *
port_alt_ref(int num)
{
        struct port *a, *p;

        a = port_bynum(num);
        if (a == NULL)
                return NULL;

        /* circulate to first alt port */
        while (a->alt_next->num > a->num)
                a = a->alt_next;

        p = a;
        while (1) {
                if (port_ref(p))
                        break;
                p = p->alt_next;
                if (p == a)
                        return NULL;
        }

        return p;
}

struct port *
port_migrate(struct port *op)
{
        struct port *np;

        /* not opened */
        if (op->state == PORT_CFG)
                return op;

        np = op;
        while (1) {
                /* try next one, circulating through the list */
                np = np->alt_next;
                if (np == op) {
                        logx(2, "midi%u: no fall-back port found", op->midi->num);
                        return op;
                }

                if (port_ref(np))
                        break;
        }

        logx(2, "midi%u: switching to midi%u", op->midi->num, np->midi->num);

        midi_migrate(op->midi, np->midi);
        return np;
}

struct port *
port_bynum(int num)
{
        struct port *p;

        for (p = port_list; p != NULL; p = p->next) {
                if (p->num == num)
                        return p;
        }
        return NULL;
}

int
port_open(struct port *c)
{
        if (!port_mio_open(c)) {
                logx(1, "midi%u: failed to open midi port", c->midi->num);
                return 0;
        }
        c->state = PORT_INIT;
        return 1;
}

int
port_close(struct port *c)
{
#ifdef DEBUG
        if (c->state == PORT_CFG) {
                logx(0, "midi%u: can't close port (not opened)", c->midi->num);
                panic();
        }
#endif
        logx(2, "midi%u: closed", c->midi->num);
        c->state = PORT_CFG;
        port_mio_close(c);
        return 1;
}

void
port_drain(struct port *c)
{
        struct midi *ep = c->midi;

        if (!(ep->mode & MODE_MIDIOUT) || ep->obuf.used == 0)
                port_close(c);
        else {
                c->state = PORT_DRAIN;
#ifdef DEBUG
                logx(3, "midi%u: draining", c->midi->num);
#endif
        }
}

int
port_init(struct port *c)
{
        if (c->hold)
                return port_open(c);
        return 1;
}

void
port_done(struct port *c)
{
        if (c->state == PORT_INIT)
                port_drain(c);
}