]> git.alsa-project.org Git - alsa-oss.git/commitdiff
alsa: Add OSS sequencer API emulation
authorTakashi Iwai <tiwai@suse.de>
Tue, 8 Sep 2026 12:46:50 +0000 (14:46 +0200)
committerTakashi Iwai <tiwai@suse.de>
Wed, 9 Sep 2026 15:09:37 +0000 (17:09 +0200)
Implement OSS /dev/sequencer and /dev/music emulation via the ALSA
sequencer API. Enumerate available MIDI ports at open time, subscribe
to them, and translate OSS 4/8-byte event records to/from ALSA
sequencer events.

A background pthread handles incoming ALSA events and converts them
to OSS format in a ring buffer; a pipe wakes poll/select waiters.
SNDCTL_SEQ_SYNC uses an ECHO event echo-back to synchronize with the
ALSA queue.

Signed-off-by: Takashi Iwai <tiwai@suse.de>
alsa/Makefile.am
alsa/alsa-oss-emul.h
alsa/alsa-oss.c
alsa/sequencer.c [new file with mode: 0644]

index 8ce94f7e90cd245e3c9c7f448ec3eb8444099074..f88e35e9612bb3a6fd610c7c823cd3fabcbeaa86 100644 (file)
@@ -13,6 +13,6 @@ libaoss_la_LIBADD = libalsatoss.la
 libaoss_la_LDFLAGS = -version-info $(COMPATNUM)
 
 libalsatoss_la_CFLAGS = @ALSA_CFLAGS@
-libalsatoss_la_SOURCES = pcm.c mixer.c
+libalsatoss_la_SOURCES = pcm.c mixer.c sequencer.c
 libalsatoss_la_LIBADD = @ALSA_LIBS@
 libalsatoss_la_LDFLAGS = -version-info $(COMPATNUM)
index 44396a43038c019bb22aa3749a6bb001e9569335..a2027325b50d54a77e3f8b599b8a048aa1d80eb3 100644 (file)
@@ -62,4 +62,16 @@ extern int lib_oss_mixer_open(const char *pathname, int flags, ...);
 extern int lib_oss_mixer_close(int fd);
 extern int lib_oss_mixer_ioctl(int fd, unsigned long int request, ...);
 
+extern int lib_oss_seq_open(const char *pathname, int flags);
+extern int lib_oss_seq_close(int fd);
+extern ssize_t lib_oss_seq_read(int fd, void *buf, size_t count);
+extern ssize_t lib_oss_seq_write(int fd, const void *buf, size_t count);
+extern int lib_oss_seq_ioctl(int fd, unsigned long int request, ...);
+extern int lib_oss_seq_poll_fds(int fd);
+extern int lib_oss_seq_poll_prepare(int fd, int fmode, struct pollfd *ufds);
+extern int lib_oss_seq_poll_result(int fd, struct pollfd *ufds);
+extern int lib_oss_seq_select_prepare(int fd, int fmode, fd_set *rfds, fd_set *wfds, fd_set *efds);
+extern int lib_oss_seq_select_result(int fd, fd_set *rfds, fd_set *wfds, fd_set *efds);
+/* Note: wfds/efds are unused in the select helpers above */
+
 #endif /* __ALSA_OSS_EMUL_H */
index 19227557affa2f7d224f956f2239627bc8df4b97..c4a3760544e4994a0c716a95a2beebaccf5bb34a 100644 (file)
@@ -94,6 +94,7 @@ typedef struct ops {
 typedef enum {
        FD_OSS_DSP,
        FD_OSS_MIXER,
+       FD_OSS_SEQ,
        FD_CLASSES,
 } fd_class_t;                                                            
 
@@ -119,6 +120,10 @@ static inline int is_oss_device(int fd)
        return fd >= 0 && fd < open_max && fds[fd];
 }
 
+#define is_oss_pcm_class(fd)   (fds[fd]->class == FD_OSS_DSP)
+#define is_oss_seq_class(fd)   (fds[fd]->class == FD_OSS_SEQ)
+#define is_oss_pcm_or_seq_class(fd)    (is_oss_pcm_class(fd) || is_oss_seq_class(fd))
+
 static int is_dsp_device(const char *pathname)
 {
        if(!pathname) return 0;
@@ -139,6 +144,14 @@ static int is_mixer_device(const char *pathname)
        return 0;
 }
 
+static int is_seq_device(const char *pathname)
+{
+       if(!pathname) return 0;
+       if(strncmp(pathname,"/dev/sequencer",14) == 0) return 1;
+       if(strncmp(pathname,"/dev/music",10) == 0) return 1;
+       return 0;
+}
+
 static int oss_pcm_fcntl(int fd, int cmd, ...)
 {
        int result;
@@ -173,6 +186,27 @@ static int oss_pcm_fcntl(int fd, int cmd, ...)
        return -1;
 }
 
+static int oss_seq_fcntl(int fd, int cmd, ...)
+{
+       va_list args;
+       long arg;
+
+       va_start(args, cmd);
+       arg = va_arg(args, long);
+       va_end(args);
+
+       switch (cmd) {
+       case F_GETFL:
+               return fds[fd]->oflags;
+       case F_SETFL:
+               fds[fd]->oflags = (fds[fd]->oflags & ~O_NONBLOCK) |
+                       (arg & O_NONBLOCK);
+               return 0;
+       default:
+               return _fcntl(fd, cmd, arg);
+       }
+}
+
 static int oss_mixer_fcntl(int fd, int cmd, ...)
 {
        int result;
@@ -242,6 +276,15 @@ static ops_t ops[FD_CLASSES] = {
                .mmap = bad_mmap,
                .munmap = bad_munmap,
        },
+        [FD_OSS_SEQ] = {
+               .close = lib_oss_seq_close,
+               .write = lib_oss_seq_write,
+               .read = lib_oss_seq_read,
+               .ioctl = lib_oss_seq_ioctl,
+               .fcntl = oss_seq_fcntl,
+               .mmap = bad_mmap,
+               .munmap = bad_munmap,
+       },
 };
 
 static int dsp_open_helper(const char *file, int oflag)
@@ -282,6 +325,29 @@ static int mixer_open_helper(const char *file, int oflag)
                fds[fd]->oflags = oflag;
        }
        return fd;
+}
+
+static int seq_open_helper(const char *file, int oflag)
+{
+       int fd;
+       fd = lib_oss_seq_open(file, oflag);
+       if (fd >= 0) {
+               int nfds;
+               fds[fd] = calloc(sizeof(fd_t), 1);
+               if (fds[fd] == NULL) {
+                       ops[FD_OSS_SEQ].close(fd);
+                       errno = ENOMEM;
+                       return -1;
+               }
+               fds[fd]->class = FD_OSS_SEQ;
+               fds[fd]->oflags = oflag;
+               nfds = lib_oss_seq_poll_fds(fd);
+               if (nfds > 0) {
+                       fds[fd]->poll_fds = nfds;
+                       poll_fds_add += nfds;
+               }
+       }
+       return fd;
 } 
 
 #define DECL_OPEN(name, callback) \
@@ -301,6 +367,8 @@ int name(const char *file, int oflag, ...) \
                fd = dsp_open_helper(file, oflag); \
        else if (is_mixer_device(file)) \
                fd = mixer_open_helper(file, oflag); \
+       else if (is_seq_device(file)) \
+               fd = seq_open_helper(file, oflag); \
        else { \
                fd = callback(file, oflag, mode); \
                if (fd >= 0) \
@@ -478,7 +546,7 @@ void dump_select(int nfds, fd_set *rfds, fd_set *wfds, fd_set *efds,
 }
 #endif
 
-static int poll_with_pcm(struct pollfd *pfds, unsigned long nfds, int timeout);
+static int poll_with_pcm_or_seq(struct pollfd *pfds, unsigned long nfds, int timeout);
 
 int poll(struct pollfd *pfds, unsigned long nfds, int timeout)
 {
@@ -491,14 +559,14 @@ int poll(struct pollfd *pfds, unsigned long nfds, int timeout)
                int fd = pfds[k].fd;
                if (! is_oss_device(fd))
                        continue;
-               if (fds[fd]->class == FD_OSS_DSP)
-                       return poll_with_pcm(pfds, nfds, timeout);
+               if (is_oss_pcm_or_seq_class(fd))
+                       return poll_with_pcm_or_seq(pfds, nfds, timeout);
        }
        return _poll(pfds, nfds, timeout);
 }
 
 
-static int poll_with_pcm(struct pollfd *pfds, unsigned long nfds, int timeout)
+static int poll_with_pcm_or_seq(struct pollfd *pfds, unsigned long nfds, int timeout)
 {
        unsigned int k;
        unsigned int nfds1;
@@ -508,7 +576,7 @@ static int poll_with_pcm(struct pollfd *pfds, unsigned long nfds, int timeout)
        nfds1 = 0;
        for (k = 0; k < nfds; ++k) {
                int fd = pfds[k].fd;
-               if (is_oss_device(fd) && fds[fd]->class == FD_OSS_DSP) {
+               if (is_oss_device(fd) && is_oss_pcm_or_seq_class(fd)) {
                        unsigned short events = pfds[k].events;
                        int fmode = 0;
                        if ((events & (POLLIN|POLLOUT)) == (POLLIN|POLLOUT))
@@ -517,7 +585,10 @@ static int poll_with_pcm(struct pollfd *pfds, unsigned long nfds, int timeout)
                                fmode = O_RDONLY;
                        else
                                fmode = O_WRONLY;
-                       count = lib_oss_pcm_poll_prepare(fd, fmode, &pfds1[nfds1]);
+                       if (is_oss_pcm_class(fd))
+                               count = lib_oss_pcm_poll_prepare(fd, fmode, &pfds1[nfds1]);
+                       else
+                               count = lib_oss_seq_poll_prepare(fd, fmode, &pfds1[nfds1]);
                        if (count < 0)
                                return -1;
                        nfds1 += count;
@@ -547,8 +618,13 @@ static int poll_with_pcm(struct pollfd *pfds, unsigned long nfds, int timeout)
        for (k = 0; k < nfds; ++k) {
                int fd = pfds[k].fd;
                unsigned int revents;
-               if (is_oss_device(fd) && fds[fd]->class == FD_OSS_DSP) {
-                       int result = lib_oss_pcm_poll_result(fd, &pfds1[nfds1]);
+               if (is_oss_device(fd) && is_oss_pcm_or_seq_class(fd)) {
+                       int result;
+
+                       if (is_oss_pcm_class(fd))
+                               result = lib_oss_pcm_poll_result(fd, &pfds1[nfds1]);
+                       else
+                               result = lib_oss_seq_poll_result(fd, &pfds1[nfds1]);
                        revents = 0;
                        if (result < 0) {
                                revents |= POLLNVAL;
@@ -557,7 +633,10 @@ static int poll_with_pcm(struct pollfd *pfds, unsigned long nfds, int timeout)
                                           ((result & OSS_WAIT_EVENT_READ) ? POLLIN : 0) |
                                           ((result & OSS_WAIT_EVENT_WRITE) ? POLLOUT : 0);
                        }
-                       nfds1 += lib_oss_pcm_poll_fds(fd);
+                       if (is_oss_pcm_class(fd))
+                               nfds1 += lib_oss_pcm_poll_fds(fd);
+                       else
+                               nfds1 += lib_oss_seq_poll_fds(fd);
                } else {
                        revents = pfds1[nfds1].revents;
                        nfds1++;
@@ -577,8 +656,8 @@ static int poll_with_pcm(struct pollfd *pfds, unsigned long nfds, int timeout)
        return count;
 }
 
-static int select_with_pcm(int nfds, fd_set *rfds, fd_set *wfds, fd_set *efds,
-                          struct timeval *timeout);
+static int select_with_pcm_or_seq(int nfds, fd_set *rfds, fd_set *wfds,
+                                 fd_set *efds, struct timeval *timeout);
 
 int select(int nfds, fd_set *rfds, fd_set *wfds, fd_set *efds,
           struct timeval *timeout)
@@ -594,15 +673,15 @@ int select(int nfds, fd_set *rfds, fd_set *wfds, fd_set *efds,
                int e = (efds && FD_ISSET(fd, efds));
                if (!(r || w || e))
                        continue;
-               if (is_oss_device(fd) && fds[fd]->class == FD_OSS_DSP)
-                       return select_with_pcm(nfds, rfds, wfds, efds, timeout);
+               if (is_oss_device(fd) && is_oss_pcm_or_seq_class(fd))
+                       return select_with_pcm_or_seq(nfds, rfds, wfds, efds, timeout);
        }
        return _select(nfds, rfds, wfds, efds, timeout);
 }
 
 
-static int select_with_pcm(int nfds, fd_set *rfds, fd_set *wfds, fd_set *efds,
-                          struct timeval *timeout)
+static int select_with_pcm_or_seq(int nfds, fd_set *rfds, fd_set *wfds,
+                                 fd_set *efds, struct timeval *timeout)
 {
        fd_set _rfds1, _wfds1, _efds1;
        fd_set *rfds1, *wfds1, *efds1;
@@ -632,7 +711,7 @@ static int select_with_pcm(int nfds, fd_set *rfds, fd_set *wfds, fd_set *efds,
                int e = (efds && FD_ISSET(fd, efds));
                if (!(r || w || e))
                        continue;
-               if (is_oss_device(fd) && fds[fd]->class == FD_OSS_DSP) {
+               if (is_oss_device(fd) && is_oss_pcm_or_seq_class(fd)) {
                        int res, fmode = 0;
                        
                        if (r & w)
@@ -641,8 +720,12 @@ static int select_with_pcm(int nfds, fd_set *rfds, fd_set *wfds, fd_set *efds,
                                fmode = O_RDONLY;
                        else
                                fmode = O_WRONLY;
-                       res = lib_oss_pcm_select_prepare(fd, fmode, rfds1, wfds1,
-                                                        e ? efds1 : NULL);
+                       if (is_oss_pcm_class(fd))
+                               res = lib_oss_pcm_select_prepare(fd, fmode, rfds1, wfds1,
+                                                                e ? efds1 : NULL);
+                       else
+                               res = lib_oss_seq_select_prepare(fd, fmode, rfds1, wfds1,
+                                                                e ? efds1 : NULL);
                        if (res < 0)
                                return -1;
                        if (nfds1 < res + 1)
@@ -683,8 +766,13 @@ static int select_with_pcm(int nfds, fd_set *rfds, fd_set *wfds, fd_set *efds,
                int r1, w1, e1;
                if (!(r || w || e))
                        continue;
-               if (is_oss_device(fd) && fds[fd]->class == FD_OSS_DSP) {
-                       int result = lib_oss_pcm_select_result(fd, rfds1, wfds1, efds1);
+               if (is_oss_device(fd) && is_oss_pcm_or_seq_class(fd)) {
+                       int result;
+
+                       if (is_oss_pcm_class(fd))
+                               result = lib_oss_pcm_select_result(fd, rfds1, wfds1, efds1);
+                       else
+                               result = lib_oss_seq_select_result(fd, rfds1, wfds1, efds1);
                        r1 = w1 = e1 = 0;
                        if (result < 0 && e) {
                                if (efds)
diff --git a/alsa/sequencer.c b/alsa/sequencer.c
new file mode 100644 (file)
index 0000000..2ac45fb
--- /dev/null
@@ -0,0 +1,1460 @@
+/*
+ *  OSS -> ALSA sequencer compatibility layer
+ *  Copyright (c) by Takashi Iwai <tiwai@suse.de>
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ */
+
+#define _GNU_SOURCE
+#include <sys/types.h>
+#include <sys/poll.h>
+#include <sys/stat.h>
+#include <stdarg.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <alsa/asoundlib.h>
+#include <linux/soundcard.h>
+#include "alsa-local.h"
+
+/* OSS sequencer modes */
+#define OSS_SEQ_MODE_SYNTH  0   /* /dev/sequencer */
+#define OSS_SEQ_MODE_MUSIC  1   /* /dev/music */
+
+/* OSS event sizes */
+#define SHORT_EVENT_SIZE 4
+#define LONG_EVENT_SIZE  8
+
+/* Read queue size (events) */
+#define READQ_SIZE 1024
+
+/* SEQ_EXTENDED sub-command indices (0-7, not MIDI status bytes) */
+#define SEQ_EXT_NOTEOFF   0
+#define SEQ_EXT_NOTEON    1
+#define SEQ_EXT_PGMCHANGE 3
+
+/* OSS event record union (matches kernel layout) */
+union evrec {
+       struct { unsigned char code, parm1, dev, parm2; } s;
+       struct { unsigned char code, chn, note, vel; } n;
+       struct { unsigned char code, dev, cmd, chn, note, parm; unsigned short dummy; } v;
+       struct { unsigned char code, dev, cmd, chn, p1, p2, p3, p4; } e;
+       struct { unsigned char code, dev, cmd, chn, p1, p2; short val; } l;
+       struct { unsigned char code, cmd, dummy1, dummy2; unsigned int time; } t;
+       struct { unsigned char code, dev; unsigned char buf[6]; } x;
+       unsigned int echo;
+       unsigned char c[8];
+};
+
+#define ev_is_long(ev) ((ev)->s.code >= 128)
+
+/* MIDI device entry */
+typedef struct {
+       int client;
+       int port;
+       snd_seq_addr_t addr;
+       unsigned int caps;
+       char name[64];
+       int opened;             /* PERM_READ | PERM_WRITE */
+       snd_midi_event_t *coder;
+} oss_midi_dev_t;
+
+/* Timer state */
+typedef struct {
+       int oss_tempo;          /* BPM */
+       int oss_timebase;       /* PPQ */
+       unsigned int cur_tick;
+       int realtime;
+       int running;
+} oss_seq_timer_t;
+
+/* Read queue (ring buffer of OSS events) */
+typedef struct {
+       union evrec buf[READQ_SIZE];
+       int head, tail, qlen;
+       int pipe_rd, pipe_wr;
+       unsigned int input_tick;
+} oss_readq_t;
+
+/* Main sequencer state */
+typedef struct oss_seq_s {
+       int fileno;             /* /dev/null fd used as OSS fd */
+       snd_seq_t *seq;
+       int client, port, queue;
+       snd_seq_addr_t addr;
+       int seq_mode;           /* OSS_SEQ_MODE_SYNTH or OSS_SEQ_MODE_MUSIC */
+       int file_mode;          /* O_RDONLY / O_WRONLY / O_RDWR */
+       int nonblock;
+       int max_mididev;
+       oss_midi_dev_t *midi_devs;
+       oss_seq_timer_t timer;
+       oss_readq_t *readq;
+       struct oss_seq_s *next;
+} oss_seq_t;
+
+static oss_seq_t *seq_list = NULL;
+
+/* Internal helpers */
+
+static oss_seq_t *find_seq(int fd)
+{
+       oss_seq_t *dp;
+
+       for (dp = seq_list; dp; dp = dp->next) {
+               if (dp->fileno == fd)
+                       break;
+       }
+       return dp;
+}
+
+static void readq_put(oss_readq_t *rq, union evrec *ev, int long_ev)
+{
+       int len = long_ev ? LONG_EVENT_SIZE : SHORT_EVENT_SIZE;
+       char c = 0;
+
+       if (rq->qlen < READQ_SIZE) {
+               memcpy(&rq->buf[rq->tail], ev, len);
+               rq->tail = (rq->tail + 1) % READQ_SIZE;
+               rq->qlen++;
+               write(rq->pipe_wr, &c, 1);
+       }
+}
+
+/* Put SEQ_WAIT timestamp if tick changed (SYNTH mode) */
+static void readq_put_timestamp_synth(oss_readq_t *rq, unsigned int tick)
+{
+       union evrec ev;
+
+       if (tick == rq->input_tick)
+               return;
+       rq->input_tick = tick;
+       memset(&ev, 0, sizeof(ev));
+       ev.echo = (tick << 8) | SEQ_WAIT;
+       readq_put(rq, &ev, 0);
+}
+
+/* Put EV_TIMING/TMR_WAIT_ABS timestamp if tick changed (MUSIC mode) */
+static void readq_put_timestamp_music(oss_readq_t *rq, unsigned int tick)
+{
+       union evrec ev;
+
+       if (tick == rq->input_tick)
+               return;
+       rq->input_tick = tick;
+       memset(&ev, 0, sizeof(ev));
+       ev.t.code = EV_TIMING;
+       ev.t.cmd = TMR_WAIT_ABS;
+       ev.t.time = tick;
+       readq_put(rq, &ev, 1);
+}
+
+/* ALSA queue / timer control */
+
+static void apply_queue_tempo(oss_seq_t *dp)
+{
+       snd_seq_queue_tempo_t *tempo;
+
+       snd_seq_queue_tempo_alloca(&tempo);
+       snd_seq_queue_tempo_set_tempo(tempo, 60000000 / dp->timer.oss_tempo);
+       snd_seq_queue_tempo_set_ppq(tempo, dp->timer.oss_timebase);
+       snd_seq_set_queue_tempo(dp->seq, dp->queue, tempo);
+}
+
+static void send_queue_control(oss_seq_t *dp, int type)
+{
+       snd_seq_event_t ev;
+
+       snd_seq_ev_clear(&ev);
+       ev.type = type;
+       snd_seq_ev_set_fixed(&ev);
+       ev.data.queue.queue = dp->queue;
+       snd_seq_ev_set_dest(&ev, SND_SEQ_CLIENT_SYSTEM, SND_SEQ_PORT_SYSTEM_TIMER);
+       snd_seq_ev_set_direct(&ev);
+       snd_seq_event_output_direct(dp->seq, &ev);
+}
+
+static void timer_start(oss_seq_t *dp)
+{
+       apply_queue_tempo(dp);
+       send_queue_control(dp, SND_SEQ_EVENT_START);
+       dp->timer.running = 1;
+       dp->timer.cur_tick = 0;
+       dp->timer.realtime = 0;
+}
+
+static void timer_stop(oss_seq_t *dp)
+{
+       send_queue_control(dp, SND_SEQ_EVENT_STOP);
+       dp->timer.running = 0;
+}
+
+static void timer_continue(oss_seq_t *dp)
+{
+       send_queue_control(dp, SND_SEQ_EVENT_CONTINUE);
+       dp->timer.running = 1;
+}
+
+/* Event processing: OSS to ALSA */
+
+/*
+ * Returns 1 if the event is a pure timing event (consumed), 0 otherwise.
+ */
+static int process_timer_event(oss_seq_t *dp, union evrec *ev)
+{
+       unsigned int parm, tick;
+
+       if (ev->t.code == EV_TIMING) {
+               parm = ev->t.time;
+               switch (ev->t.cmd) {
+               case TMR_WAIT_REL:
+                       dp->timer.cur_tick += parm;
+                       dp->timer.realtime = 0;
+                       return 1;
+               case TMR_WAIT_ABS:
+                       if (parm == 0)
+                               dp->timer.realtime = 1;
+                       else if (parm >= dp->timer.cur_tick) {
+                               dp->timer.realtime = 0;
+                               dp->timer.cur_tick = parm;
+                       }
+                       return 1;
+               case TMR_START:
+                       timer_start(dp);
+                       return 1;
+               case TMR_STOP:
+                       timer_stop(dp);
+                       return 1;
+               case TMR_CONTINUE:
+                       timer_continue(dp);
+                       return 1;
+               case TMR_TEMPO:
+                       if (parm > 0)
+                               dp->timer.oss_tempo = parm;
+                       if (dp->timer.running)
+                               apply_queue_tempo(dp);
+                       return 1;
+               default:
+                       return 0;
+               }
+       } else if (ev->s.code == SEQ_WAIT) {
+               /* 4-byte wait: tick in bytes 1-3 */
+               tick = (ev->echo >> 8) & 0xffffff;
+               if (tick > dp->timer.cur_tick) {
+                       dp->timer.cur_tick = tick;
+                       dp->timer.realtime = 0;
+               }
+               return 1;
+       }
+       return 0;
+}
+
+static void set_dest_midi(oss_seq_t *dp, snd_seq_event_t *ev, int dev)
+{
+       if (dev >= 0 && dev < dp->max_mididev)
+               snd_seq_ev_set_dest(ev, dp->midi_devs[dev].client, dp->midi_devs[dev].port);
+       else
+               snd_seq_ev_set_dest(ev, SND_SEQ_ADDRESS_SUBSCRIBERS, 0);
+}
+
+/*
+ * Translate a single OSS event record to an ALSA event.
+ * Returns 0 on success, negative on error/skip, 1 if already sent (sysex/midiputc).
+ */
+static int process_event(oss_seq_t *dp, union evrec *ev, snd_seq_event_t *out)
+{
+       int dev, chn, note, vel, sysex_len, i;
+       union evrec tmp;
+       oss_midi_dev_t *md;
+       snd_seq_event_t mev;
+       long encode_rc;
+       unsigned char sysex_buf[6];
+
+       snd_seq_ev_clear(out);
+       out->source = dp->addr;
+
+       switch (ev->s.code) {
+       case EV_CHN_VOICE:
+               dev = ev->v.dev;
+               chn = ev->v.chn;
+               note = ev->v.note;
+               vel = ev->v.parm;
+               set_dest_midi(dp, out, dev);
+               switch (ev->v.cmd) {
+               case MIDI_NOTEON:
+                       if (vel == 0)
+                               snd_seq_ev_set_noteoff(out, chn, note, 0);
+                       else
+                               snd_seq_ev_set_noteon(out, chn, note, vel);
+                       break;
+               case MIDI_NOTEOFF:
+                       snd_seq_ev_set_noteoff(out, chn, note, vel);
+                       break;
+               case MIDI_KEY_PRESSURE:
+                       out->type = SND_SEQ_EVENT_KEYPRESS;
+                       out->data.note.channel = chn;
+                       out->data.note.note = note;
+                       out->data.note.velocity = vel;
+                       break;
+               default:
+                       return -EINVAL;
+               }
+               break;
+
+       case EV_CHN_COMMON:
+               dev = ev->l.dev;
+               chn = ev->l.chn;
+               set_dest_midi(dp, out, dev);
+               switch (ev->l.cmd) {
+               case MIDI_PGM_CHANGE:
+                       snd_seq_ev_set_pgmchange(out, chn, ev->l.p1);
+                       break;
+               case MIDI_CTL_CHANGE:
+                       snd_seq_ev_set_controller(out, chn, ev->l.p1, ev->l.val);
+                       break;
+               case MIDI_PITCH_BEND:
+                       snd_seq_ev_set_pitchbend(out, chn, ev->l.val - 8192);
+                       break;
+               case MIDI_CHN_PRESSURE:
+                       snd_seq_ev_set_chanpress(out, chn, ev->l.val);
+                       break;
+               default:
+                       return -EINVAL;
+               }
+               break;
+
+       case EV_TIMING:
+               switch (ev->t.cmd) {
+               case TMR_ECHO:
+                       /* schedule an ECHO event that loops back */
+                       if (dp->seq_mode == OSS_SEQ_MODE_SYNTH) {
+                               memset(&tmp, 0, sizeof(tmp));
+                               tmp.echo = (ev->t.time << 8) | SEQ_ECHO;
+                       } else {
+                               memcpy(&tmp, ev, LONG_EVENT_SIZE);
+                       }
+                       out->type = SND_SEQ_EVENT_ECHO;
+                       snd_seq_ev_set_dest(out, dp->addr.client, dp->addr.port);
+                       memcpy(&out->data, &tmp, LONG_EVENT_SIZE);
+                       break;
+               /* TMR_START/STOP/CONTINUE/TEMPO already handled in process_timer_event */
+               default:
+                       return -EINVAL;
+               }
+               break;
+
+       case SEQ_EXTENDED:
+               if (dp->seq_mode != OSS_SEQ_MODE_SYNTH)
+                       return -EINVAL;
+               dev = ev->e.dev;
+               chn = ev->e.chn;
+               set_dest_midi(dp, out, dev);
+               switch (ev->e.cmd) {
+               case SEQ_EXT_NOTEOFF:
+                       snd_seq_ev_set_noteoff(out, chn, ev->e.p1, ev->e.p2);
+                       break;
+               case SEQ_EXT_NOTEON:
+                       snd_seq_ev_set_noteon(out, chn, ev->e.p1, ev->e.p2);
+                       break;
+               case SEQ_EXT_PGMCHANGE:
+                       snd_seq_ev_set_pgmchange(out, chn, ev->e.p1);
+                       break;
+               case SEQ_AFTERTOUCH:  /* 4 */
+                       snd_seq_ev_set_chanpress(out, chn, ev->e.p1);
+                       break;
+               case SEQ_BALANCE:     /* 5 */
+                       snd_seq_ev_set_controller(out, chn, 10 /* CTL_PAN */, ev->e.p1);
+                       break;
+               case SEQ_CONTROLLER:  /* 6 */
+                       snd_seq_ev_set_controller(out, chn, ev->e.p1, ev->e.p2);
+                       break;
+               default:
+                       return -EINVAL;
+               }
+               break;
+
+       case SEQ_MIDIPUTC:
+               dev = ev->s.dev;
+               if (dev < 0 || dev >= dp->max_mididev)
+                       return -ENXIO;
+               md = &dp->midi_devs[dev];
+               if (!md->coder)
+                       return -ENXIO;
+               snd_seq_ev_clear(&mev);
+               mev.source = dp->addr;
+               set_dest_midi(dp, &mev, dev);
+               encode_rc = snd_midi_event_encode_byte(md->coder, ev->s.parm1, &mev);
+               if (encode_rc > 0) {
+                       if (dp->timer.realtime || !dp->timer.running)
+                               snd_seq_ev_set_direct(&mev);
+                       else
+                               snd_seq_ev_schedule_tick(&mev, dp->queue, 0, dp->timer.cur_tick);
+                       snd_seq_event_output(dp->seq, &mev);
+               }
+               return 1; /* already handled */
+
+       case EV_SYSEX:
+               dev = ev->x.dev;
+               sysex_len = 0;
+               for (i = 0; i < 6; i++) {
+                       sysex_buf[i] = ev->x.buf[i];
+                       if (sysex_buf[i] == 0xff)
+                               break;
+                       sysex_len++;
+               }
+               if (sysex_len == 0)
+                       return -EINVAL;
+               set_dest_midi(dp, out, dev);
+               snd_seq_ev_set_variable(out, sysex_len, sysex_buf);
+               out->type = SND_SEQ_EVENT_SYSEX;
+               /* must output immediately since sysex_buf is stack-allocated */
+               if (dp->timer.realtime || !dp->timer.running)
+                       snd_seq_ev_set_direct(out);
+               else
+                       snd_seq_ev_schedule_tick(out, dp->queue, 0, dp->timer.cur_tick);
+               snd_seq_event_output(dp->seq, out);
+               return 1; /* already handled */
+
+       case SEQ_ECHO:
+               if (dp->seq_mode == OSS_SEQ_MODE_MUSIC)
+                       return -EINVAL;
+               out->type = SND_SEQ_EVENT_ECHO;
+               snd_seq_ev_set_dest(out, dp->addr.client, dp->addr.port);
+               memcpy(&out->data, ev, SHORT_EVENT_SIZE);
+               break;
+
+       /* Old-style 4-byte events (SYNTH mode only) */
+       default:
+               if (dp->seq_mode == OSS_SEQ_MODE_MUSIC)
+                       return -EINVAL;
+               switch (ev->s.code) {
+               case SEQ_NOTEON:
+                       set_dest_midi(dp, out, 0);
+                       snd_seq_ev_set_noteon(out, ev->n.chn, ev->n.note, ev->n.vel);
+                       break;
+               case SEQ_NOTEOFF:
+                       set_dest_midi(dp, out, 0);
+                       snd_seq_ev_set_noteoff(out, ev->n.chn, ev->n.note, 0);
+                       break;
+               case SEQ_PGMCHANGE:
+                       set_dest_midi(dp, out, 0);
+                       snd_seq_ev_set_pgmchange(out, ev->n.chn, ev->n.note);
+                       break;
+               case SEQ_SYNCTIMER:
+                       timer_start(dp);
+                       return -EINVAL;
+               default:
+                       return -EINVAL;
+               }
+               break;
+       }
+       return 0;
+}
+
+/* Event processing: ALSA to OSS */
+
+/* Decode ALSA event to OSS SEQ_MIDIPUTC records */
+static void send_midi_bytes(oss_seq_t *dp, int dev, snd_seq_event_t *ev)
+{
+       oss_midi_dev_t *md;
+       unsigned char buf[16];
+       unsigned int tick;
+       union evrec rec;
+       long len;
+       int i;
+
+       if (dev < 0 || dev >= dp->max_mididev)
+               return;
+       md = &dp->midi_devs[dev];
+       if (!md->coder)
+               return;
+       len = snd_midi_event_decode(md->coder, buf, sizeof(buf), ev);
+       if (len <= 0)
+               return;
+       tick = ev->time.tick;
+       readq_put_timestamp_synth(dp->readq, tick);
+       for (i = 0; i < len; i++) {
+               memset(&rec, 0, sizeof(rec));
+               rec.c[0] = SEQ_MIDIPUTC;
+               rec.c[1] = buf[i];
+               rec.c[2] = dev;
+               readq_put(dp->readq, &rec, 0);
+       }
+}
+
+/* Find which midi_dev this ALSA address belongs to */
+static int find_midi_dev_by_addr(oss_seq_t *dp, snd_seq_addr_t *addr)
+{
+       int i;
+
+       for (i = 0; i < dp->max_mididev; i++) {
+               if (dp->midi_devs[i].addr.client == addr->client &&
+                   dp->midi_devs[i].addr.port == addr->port)
+                       return i;
+       }
+       return -1;
+}
+
+/* Translate ALSA event to OSS EV_CHN_VOICE/EV_CHN_COMMON (MUSIC mode) */
+static void send_synth_event(oss_seq_t *dp, int dev, snd_seq_event_t *ev)
+{
+       unsigned int tick = ev->time.tick;
+       union evrec rec;
+
+       readq_put_timestamp_music(dp->readq, tick);
+       memset(&rec, 0, sizeof(rec));
+       switch (ev->type) {
+       case SND_SEQ_EVENT_NOTEON:
+               rec.v.code = EV_CHN_VOICE;
+               rec.v.dev = dev;
+               rec.v.cmd = MIDI_NOTEON;
+               rec.v.chn = ev->data.note.channel;
+               rec.v.note = ev->data.note.note;
+               rec.v.parm = ev->data.note.velocity;
+               readq_put(dp->readq, &rec, 1);
+               break;
+       case SND_SEQ_EVENT_NOTEOFF:
+               rec.v.code = EV_CHN_VOICE;
+               rec.v.dev = dev;
+               rec.v.cmd = MIDI_NOTEOFF;
+               rec.v.chn = ev->data.note.channel;
+               rec.v.note = ev->data.note.note;
+               rec.v.parm = ev->data.note.velocity;
+               readq_put(dp->readq, &rec, 1);
+               break;
+       case SND_SEQ_EVENT_KEYPRESS:
+               rec.v.code = EV_CHN_VOICE;
+               rec.v.dev = dev;
+               rec.v.cmd = MIDI_KEY_PRESSURE;
+               rec.v.chn = ev->data.note.channel;
+               rec.v.note = ev->data.note.note;
+               rec.v.parm = ev->data.note.velocity;
+               readq_put(dp->readq, &rec, 1);
+               break;
+       case SND_SEQ_EVENT_CONTROLLER:
+               rec.l.code = EV_CHN_COMMON;
+               rec.l.dev = dev;
+               rec.l.cmd = MIDI_CTL_CHANGE;
+               rec.l.chn = ev->data.control.channel;
+               rec.l.p1 = ev->data.control.param;
+               rec.l.val = ev->data.control.value;
+               readq_put(dp->readq, &rec, 1);
+               break;
+       case SND_SEQ_EVENT_PGMCHANGE:
+               rec.l.code = EV_CHN_COMMON;
+               rec.l.dev = dev;
+               rec.l.cmd = MIDI_PGM_CHANGE;
+               rec.l.chn = ev->data.control.channel;
+               rec.l.p1 = ev->data.control.value;
+               readq_put(dp->readq, &rec, 1);
+               break;
+       case SND_SEQ_EVENT_PITCHBEND:
+               rec.l.code = EV_CHN_COMMON;
+               rec.l.dev = dev;
+               rec.l.cmd = MIDI_PITCH_BEND;
+               rec.l.chn = ev->data.control.channel;
+               rec.l.val = ev->data.control.value + 8192;
+               readq_put(dp->readq, &rec, 1);
+               break;
+       case SND_SEQ_EVENT_CHANPRESS:
+               rec.l.code = EV_CHN_COMMON;
+               rec.l.dev = dev;
+               rec.l.cmd = MIDI_CHN_PRESSURE;
+               rec.l.chn = ev->data.control.channel;
+               rec.l.val = ev->data.control.value;
+               readq_put(dp->readq, &rec, 1);
+               break;
+       default:
+               break;
+       }
+}
+
+/*
+ * Fetch available ALSA events and dispatch them.
+ * Non-echo events go into readq; echo events go into readq unless want_sync
+ * is set and the echo is a SEQ_SYNCTIMER, in which case returns 1 (sync done).
+ */
+static int handle_alsa_input(oss_seq_t *dp, int want_sync)
+{
+       snd_seq_event_t *ev;
+       int dev;
+
+       snd_seq_event_input_pending(dp->seq, 1);
+       while (snd_seq_event_input_pending(dp->seq, 0) > 0) {
+               if (snd_seq_event_input(dp->seq, &ev) < 0)
+                       break;
+               if (ev->type == SND_SEQ_EVENT_ECHO) {
+                       union evrec *rec = (union evrec *)&ev->data;
+                       if (want_sync && rec->s.code == SEQ_SYNCTIMER)
+                               return 1;
+                       if (dp->readq)
+                               readq_put(dp->readq, rec, ev_is_long(rec));
+               } else if (dp->readq) {
+                       dev = find_midi_dev_by_addr(dp, &ev->source);
+                       if (dp->seq_mode == OSS_SEQ_MODE_MUSIC)
+                               send_synth_event(dp, dev, ev);
+                       else
+                               send_midi_bytes(dp, dev, ev);
+               }
+       }
+       return 0;
+}
+
+/* MIDI device enumeration */
+
+#define PERM_READ  (SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ)
+#define PERM_WRITE (SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE)
+
+static int enumerate_midi_devs(oss_seq_t *dp)
+{
+       snd_seq_client_info_t *cinfo;
+       snd_seq_port_info_t *pinfo;
+       oss_midi_dev_t *md, *tmp;
+       unsigned int type, caps;
+       int count = 0;
+       int alloc = 16;
+       int cli;
+
+       snd_seq_client_info_alloca(&cinfo);
+       snd_seq_port_info_alloca(&pinfo);
+
+       dp->midi_devs = malloc(alloc * sizeof(oss_midi_dev_t));
+       if (!dp->midi_devs)
+               return -ENOMEM;
+
+       snd_seq_client_info_set_client(cinfo, -1);
+       while (snd_seq_query_next_client(dp->seq, cinfo) >= 0) {
+               cli = snd_seq_client_info_get_client(cinfo);
+               if (cli == dp->client)
+                       continue;
+               snd_seq_port_info_set_client(pinfo, cli);
+               snd_seq_port_info_set_port(pinfo, -1);
+               while (snd_seq_query_next_port(dp->seq, pinfo) >= 0) {
+                       type = snd_seq_port_info_get_type(pinfo);
+                       caps = snd_seq_port_info_get_capability(pinfo);
+                       if (!(type & SND_SEQ_PORT_TYPE_MIDI_GENERIC))
+                               continue;
+                       if (!(caps & (PERM_READ | PERM_WRITE)))
+                               continue;
+                       if (count >= alloc) {
+                               alloc *= 2;
+                               tmp = realloc(dp->midi_devs,
+                                             alloc * sizeof(oss_midi_dev_t));
+                               if (!tmp)
+                                       return -ENOMEM;
+                               dp->midi_devs = tmp;
+                       }
+                       md = &dp->midi_devs[count];
+                       memset(md, 0, sizeof(*md));
+                       md->client = snd_seq_port_info_get_client(pinfo);
+                       md->port   = snd_seq_port_info_get_port(pinfo);
+                       md->addr.client = md->client;
+                       md->addr.port   = md->port;
+                       md->caps   = caps;
+                       strncpy(md->name, snd_seq_port_info_get_name(pinfo),
+                               sizeof(md->name) - 1);
+                       count++;
+               }
+       }
+       dp->max_mididev = count;
+       return 0;
+}
+
+/* Subscribe / unsubscribe MIDI ports */
+
+static int subscribe_midi(oss_seq_t *dp, int dev, int for_read)
+{
+       snd_seq_port_subscribe_t *sub;
+       oss_midi_dev_t *md;
+       int rc;
+
+       snd_seq_port_subscribe_alloca(&sub);
+       md = &dp->midi_devs[dev];
+
+       if (for_read) {
+               snd_seq_port_subscribe_set_sender(sub, &md->addr);
+               snd_seq_port_subscribe_set_dest(sub, &dp->addr);
+               snd_seq_port_subscribe_set_time_update(sub, 1);
+               snd_seq_port_subscribe_set_queue(sub, dp->queue);
+               snd_seq_port_subscribe_set_time_real(sub, 0);
+               rc = snd_seq_subscribe_port(dp->seq, sub);
+               if (rc < 0)
+                       return rc;
+               md->opened |= PERM_READ;
+       } else {
+               snd_seq_port_subscribe_set_sender(sub, &dp->addr);
+               snd_seq_port_subscribe_set_dest(sub, &md->addr);
+               rc = snd_seq_subscribe_port(dp->seq, sub);
+               if (rc < 0)
+                       return rc;
+               md->opened |= PERM_WRITE;
+       }
+       return 0;
+}
+
+static void unsubscribe_midi(oss_seq_t *dp, int dev)
+{
+       snd_seq_port_subscribe_t *sub;
+       oss_midi_dev_t *md;
+
+       snd_seq_port_subscribe_alloca(&sub);
+       md = &dp->midi_devs[dev];
+
+       if (md->opened & PERM_READ) {
+               snd_seq_port_subscribe_set_sender(sub, &md->addr);
+               snd_seq_port_subscribe_set_dest(sub, &dp->addr);
+               snd_seq_unsubscribe_port(dp->seq, sub);
+       }
+       if (md->opened & PERM_WRITE) {
+               snd_seq_port_subscribe_set_sender(sub, &dp->addr);
+               snd_seq_port_subscribe_set_dest(sub, &md->addr);
+               snd_seq_unsubscribe_port(dp->seq, sub);
+       }
+       md->opened = 0;
+}
+
+/* Wait for the ALSA queue to finish playing before close */
+static void drain_at_close(oss_seq_t *dp)
+{
+       snd_seq_event_t ev, *evp;
+       union evrec rec;
+       struct pollfd pfds[4];
+       int nfds, rc;
+
+       if (!dp->timer.running || dp->timer.realtime)
+               return;
+
+       /*
+        * Schedule a sentinel ECHO at cur_tick; when it arrives back, the
+        * queue has delivered all events up to that tick
+        */
+       snd_seq_ev_clear(&ev);
+       ev.type = SND_SEQ_EVENT_ECHO;
+       memset(&rec, 0, sizeof(rec));
+       rec.t.code = SEQ_SYNCTIMER;
+       rec.t.time = dp->timer.cur_tick;
+       memcpy(&ev.data, &rec, sizeof(rec));
+       snd_seq_ev_set_dest(&ev, dp->addr.client, dp->addr.port);
+       snd_seq_ev_schedule_tick(&ev, dp->queue, 0, dp->timer.cur_tick);
+       snd_seq_event_output(dp->seq, &ev);
+       snd_seq_drain_output(dp->seq);
+
+       nfds = snd_seq_poll_descriptors_count(dp->seq, POLLIN);
+       if (nfds <= 0 || nfds > (int)(sizeof(pfds) / sizeof(pfds[0])))
+               return;
+       snd_seq_poll_descriptors(dp->seq, pfds, nfds, POLLIN);
+
+       for (;;) {
+               rc = poll(pfds, nfds, 5000);
+               if (rc < 0) {
+                       if (errno == EINTR)
+                               continue;
+                       break;
+               }
+               if (rc == 0)
+                       break; /* timeout: give up waiting */
+               /* fetch from kernel into userspace buffer */
+               snd_seq_event_input_pending(dp->seq, 1);
+               while (snd_seq_event_input_pending(dp->seq, 0) > 0) {
+                       if (snd_seq_event_input(dp->seq, &evp) < 0)
+                               return;
+                       if (evp->type == SND_SEQ_EVENT_ECHO) {
+                               union evrec *r = (union evrec *)&evp->data;
+                               if (r->s.code == SEQ_SYNCTIMER)
+                                       return;
+                       }
+               }
+       }
+}
+
+/* Reset helpers */
+
+static void seq_all_notes_off(oss_seq_t *dp)
+{
+       snd_seq_event_t ev;
+       int i, ch;
+
+       for (i = 0; i < dp->max_mididev; i++) {
+               if (!(dp->midi_devs[i].opened & PERM_WRITE))
+                       continue;
+               snd_seq_ev_clear(&ev);
+               ev.source = dp->addr;
+               snd_seq_ev_set_dest(&ev, dp->midi_devs[i].client, dp->midi_devs[i].port);
+               snd_seq_ev_set_direct(&ev);
+               for (ch = 0; ch < 16; ch++) {
+                       snd_seq_ev_set_controller(&ev, ch, 123 /* all notes off */, 0);
+                       snd_seq_event_output_direct(dp->seq, &ev);
+               }
+       }
+}
+
+static void readq_clear(oss_readq_t *rq)
+{
+       char buf[64];
+
+       rq->head = rq->tail = rq->qlen = 0;
+       while (read(rq->pipe_rd, buf, sizeof(buf)) > 0)
+               ;
+}
+
+/* Public API: open / close */
+
+int lib_oss_seq_open(const char *pathname, int flags)
+{
+       int seq_mode, oflags, rc, readable, writable, i;
+       int pipefds[2];
+       unsigned int port_caps, port_type;
+       oss_seq_t *dp;
+
+       if (strcmp(pathname, "/dev/music") == 0)
+               seq_mode = OSS_SEQ_MODE_MUSIC;
+       else
+               seq_mode = OSS_SEQ_MODE_SYNTH;
+
+       dp = calloc(1, sizeof(*dp));
+       if (!dp)
+               return -ENOMEM;
+
+       dp->seq_mode = seq_mode;
+       dp->file_mode = flags & O_ACCMODE;
+       dp->timer.oss_tempo = 60;
+       dp->timer.oss_timebase = 100;
+       dp->timer.realtime = 1;
+
+       oflags = flags & O_ACCMODE;
+       dp->fileno = open("/dev/null", oflags);
+       if (dp->fileno < 0) {
+               free(dp);
+               return -errno;
+       }
+
+       rc = snd_seq_open(&dp->seq, "default", SND_SEQ_OPEN_DUPLEX, 0);
+       if (rc < 0) {
+               close(dp->fileno);
+               free(dp);
+               errno = -rc;
+               return -1;
+       }
+
+       snd_seq_set_client_name(dp->seq, "OSS sequencer");
+       dp->client = snd_seq_client_id(dp->seq);
+
+       port_caps = SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_WRITE |
+                   SND_SEQ_PORT_CAP_SUBS_READ | SND_SEQ_PORT_CAP_SUBS_WRITE;
+       port_type = SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION;
+       dp->port = snd_seq_create_simple_port(dp->seq, "OSS port", port_caps, port_type);
+       if (dp->port < 0) {
+               rc = dp->port;
+               goto err_close_seq;
+       }
+       dp->addr.client = dp->client;
+       dp->addr.port   = dp->port;
+
+       dp->queue = snd_seq_alloc_queue(dp->seq);
+       if (dp->queue < 0) {
+               rc = dp->queue;
+               goto err_delete_port;
+       }
+       apply_queue_tempo(dp);
+
+       rc = enumerate_midi_devs(dp);
+       if (rc < 0)
+               goto err_free_queue;
+
+       readable = (dp->file_mode == O_RDONLY || dp->file_mode == O_RDWR);
+       if (readable) {
+               dp->readq = calloc(1, sizeof(*dp->readq));
+               if (!dp->readq) {
+                       rc = -ENOMEM;
+                       goto err_free_devs;
+               }
+               if (pipe2(pipefds, O_NONBLOCK) < 0) {
+                       rc = -errno;
+                       goto err_free_readq;
+               }
+               dp->readq->pipe_rd = pipefds[0];
+               dp->readq->pipe_wr = pipefds[1];
+
+               for (i = 0; i < dp->max_mididev; i++) {
+                       if (dp->midi_devs[i].caps & PERM_READ)
+                               subscribe_midi(dp, i, 1);
+               }
+       }
+
+       writable = (dp->file_mode == O_WRONLY || dp->file_mode == O_RDWR);
+       if (writable) {
+               for (i = 0; i < dp->max_mididev; i++) {
+                       if (dp->midi_devs[i].caps & PERM_WRITE)
+                               subscribe_midi(dp, i, 0);
+               }
+               for (i = 0; i < dp->max_mididev; i++) {
+                       snd_midi_event_new(256, &dp->midi_devs[i].coder);
+                       if (dp->midi_devs[i].coder)
+                               snd_midi_event_no_status(dp->midi_devs[i].coder, 1);
+               }
+       }
+
+       dp->next = seq_list;
+       seq_list = dp;
+
+       return dp->fileno;
+
+err_free_readq:
+       if (dp->readq) {
+               free(dp->readq);
+       }
+err_free_devs:
+       for (i = 0; i < dp->max_mididev; i++) {
+               if (dp->midi_devs[i].coder)
+                       snd_midi_event_free(dp->midi_devs[i].coder);
+       }
+       free(dp->midi_devs);
+err_free_queue:
+       snd_seq_free_queue(dp->seq, dp->queue);
+err_delete_port:
+       snd_seq_delete_simple_port(dp->seq, dp->port);
+err_close_seq:
+       snd_seq_close(dp->seq);
+       close(dp->fileno);
+       free(dp);
+       errno = -rc;
+       return -1;
+}
+
+int lib_oss_seq_close(int fd)
+{
+       oss_seq_t *dp, *prev = NULL;
+       int i;
+
+       for (dp = seq_list; dp; prev = dp, dp = dp->next) {
+               if (dp->fileno == fd)
+                       break;
+       }
+       if (!dp) {
+               errno = EBADF;
+               return -1;
+       }
+       if (prev)
+               prev->next = dp->next;
+       else
+               seq_list = dp->next;
+
+       drain_at_close(dp);
+
+       for (i = 0; i < dp->max_mididev; i++) {
+               unsubscribe_midi(dp, i);
+               if (dp->midi_devs[i].coder)
+                       snd_midi_event_free(dp->midi_devs[i].coder);
+       }
+       free(dp->midi_devs);
+
+       snd_seq_drop_output(dp->seq);
+       snd_seq_free_queue(dp->seq, dp->queue);
+       snd_seq_delete_simple_port(dp->seq, dp->port);
+       snd_seq_close(dp->seq);
+
+       if (dp->readq) {
+               close(dp->readq->pipe_rd);
+               close(dp->readq->pipe_wr);
+               free(dp->readq);
+       }
+
+       close(fd);
+
+       free(dp);
+       return 0;
+}
+
+/* Public API: read / write */
+
+ssize_t lib_oss_seq_read(int fd, void *buf, size_t count)
+{
+       oss_seq_t *dp = find_seq(fd);
+       oss_readq_t *rq;
+       unsigned char *p = buf;
+       union evrec *rec;
+       char tmp[64];
+       size_t copied = 0;
+       int sz, nfds;
+       struct pollfd pfds[8];
+
+       if (!dp) {
+               errno = EBADF;
+               return -1;
+       }
+       if (!dp->readq) {
+               errno = EINVAL;
+               return -1;
+       }
+       rq = dp->readq;
+
+       while (copied == 0) {
+               if (rq->qlen == 0) {
+                       if (dp->nonblock) {
+                               handle_alsa_input(dp, 0);
+                               if (rq->qlen == 0) {
+                                       errno = EAGAIN;
+                                       return -1;
+                               }
+                       } else {
+                               nfds = snd_seq_poll_descriptors_count(dp->seq, POLLIN);
+                               if (nfds > (int)(sizeof(pfds) / sizeof(pfds[0])))
+                                       nfds = sizeof(pfds) / sizeof(pfds[0]);
+                               do {
+                                       snd_seq_poll_descriptors(dp->seq, pfds, nfds, POLLIN);
+                                       while (poll(pfds, nfds, -1) < 0 && errno == EINTR)
+                                               ;
+                                       handle_alsa_input(dp, 0);
+                               } while (rq->qlen == 0);
+                       }
+                       continue;
+               }
+               while (rq->qlen > 0 && copied + LONG_EVENT_SIZE <= count) {
+                       rec = &rq->buf[rq->head];
+                       sz = ev_is_long(rec) ? LONG_EVENT_SIZE : SHORT_EVENT_SIZE;
+                       if (copied + sz > count)
+                               break;
+                       memcpy(p + copied, rec, sz);
+                       copied += sz;
+                       rq->head = (rq->head + 1) % READQ_SIZE;
+                       rq->qlen--;
+               }
+               if (copied == 0 && count < SHORT_EVENT_SIZE) {
+                       errno = EINVAL;
+                       return -1;
+               }
+               if (copied == 0)
+                       break; /* event too large for buffer */
+       }
+       if (rq->qlen == 0) {
+               while (read(rq->pipe_rd, tmp, sizeof(tmp)) > 0)
+                       ;
+       }
+       return copied;
+}
+
+ssize_t lib_oss_seq_write(int fd, const void *buf, size_t count)
+{
+       oss_seq_t *dp = find_seq(fd);
+       const unsigned char *p = buf;
+       union evrec ev;
+       snd_seq_event_t sev;
+       size_t pos = 0;
+       int is_long, evlen, rc;
+
+       if (!dp) {
+               errno = EBADF;
+               return -1;
+       }
+
+       while (pos + SHORT_EVENT_SIZE <= count) {
+               memset(&ev, 0, sizeof(ev));
+               memcpy(&ev, p + pos, SHORT_EVENT_SIZE);
+
+               if (ev.s.code == SEQ_FULLSIZE) {
+                       pos += SHORT_EVENT_SIZE;
+                       continue;
+               }
+
+               is_long = ev_is_long(&ev);
+               evlen = is_long ? LONG_EVENT_SIZE : SHORT_EVENT_SIZE;
+
+               if (pos + evlen > count)
+                       break;
+
+               if (is_long)
+                       memcpy(&ev, p + pos, LONG_EVENT_SIZE);
+
+               pos += evlen;
+
+               if (process_timer_event(dp, &ev))
+                       continue;
+
+               rc = process_event(dp, &ev, &sev);
+               if (rc < 0)
+                       continue;
+               if (rc == 1)
+                       continue; /* already sent (sysex/midiputc) */
+
+               if (dp->timer.realtime || !dp->timer.running)
+                       snd_seq_ev_set_direct(&sev);
+               else
+                       snd_seq_ev_schedule_tick(&sev, dp->queue, 0, dp->timer.cur_tick);
+
+               snd_seq_event_output(dp->seq, &sev);
+       }
+
+       snd_seq_drain_output(dp->seq);
+       return pos;
+}
+
+/* Public API: ioctl */
+
+int lib_oss_seq_ioctl(int fd, unsigned long request, ...)
+{
+       va_list args;
+       void *arg;
+       oss_seq_t *dp;
+       snd_seq_event_t sev;
+       union evrec ev, rec;
+       snd_seq_client_pool_t *pool;
+       struct synth_info *sinfo;
+       struct midi_info *minfo;
+       struct pollfd pfds[4];
+       int dev, val, rc, nfds;
+
+       va_start(args, request);
+       arg = va_arg(args, void *);
+       va_end(args);
+
+       dp = find_seq(fd);
+       if (!dp) {
+               errno = EBADF;
+               return -1;
+       }
+
+       switch (request) {
+       case SNDCTL_SEQ_RESET:
+               seq_all_notes_off(dp);
+               snd_seq_drop_output(dp->seq);
+               if (dp->readq)
+                       readq_clear(dp->readq);
+               dp->timer.cur_tick = 0;
+               dp->timer.realtime = 1;
+               return 0;
+
+       case SNDCTL_SEQ_PANIC:
+               seq_all_notes_off(dp);
+               snd_seq_drop_output(dp->seq);
+               if (dp->readq)
+                       readq_clear(dp->readq);
+               errno = EINVAL;
+               return -1;
+
+       case SNDCTL_SEQ_SYNC:
+               snd_seq_ev_clear(&sev);
+               sev.type = SND_SEQ_EVENT_ECHO;
+               memset(&rec, 0, sizeof(rec));
+               rec.t.code = SEQ_SYNCTIMER;
+               rec.t.time = dp->timer.cur_tick;
+               memcpy(&sev.data, &rec, sizeof(rec));
+               snd_seq_ev_set_dest(&sev, dp->addr.client, dp->addr.port);
+               if (!dp->timer.running || dp->timer.realtime)
+                       snd_seq_ev_set_direct(&sev);
+               else
+                       snd_seq_ev_schedule_tick(&sev, dp->queue, 0, dp->timer.cur_tick);
+               snd_seq_event_output(dp->seq, &sev);
+               snd_seq_drain_output(dp->seq);
+
+               nfds = snd_seq_poll_descriptors_count(dp->seq, POLLIN);
+               if (nfds > (int)(sizeof(pfds) / sizeof(pfds[0])))
+                       nfds = sizeof(pfds) / sizeof(pfds[0]);
+               for (;;) {
+                       snd_seq_poll_descriptors(dp->seq, pfds, nfds, POLLIN);
+                       rc = poll(pfds, nfds, 5000);
+                       if (rc < 0) {
+                               if (errno == EINTR)
+                                       continue;
+                               break;
+                       }
+                       if (rc == 0)
+                               break; /* timeout */
+                       if (handle_alsa_input(dp, 1))
+                               break; /* sync echo received */
+               }
+               return 0;
+
+       case SNDCTL_SEQ_NRSYNTHS:
+               if (dp->seq_mode == OSS_SEQ_MODE_MUSIC)
+                       *(int *)arg = dp->max_mididev;
+               else
+                       *(int *)arg = 0;
+               return 0;
+
+       case SNDCTL_SEQ_NRMIDIS:
+               *(int *)arg = dp->max_mididev;
+               return 0;
+
+       case SNDCTL_SEQ_GETTIME:
+               *(int *)arg = dp->timer.cur_tick;
+               return 0;
+
+       case SNDCTL_SEQ_CTRLRATE:
+               *(int *)arg = (dp->timer.oss_tempo * dp->timer.oss_timebase + 30) / 60;
+               return 0;
+
+       case SNDCTL_SEQ_GETINCOUNT:
+               *(int *)arg = dp->readq ? dp->readq->qlen : 0;
+               return 0;
+
+       case SNDCTL_SEQ_GETOUTCOUNT:
+               snd_seq_client_pool_alloca(&pool);
+               if (snd_seq_get_client_pool(dp->seq, pool) >= 0)
+                       *(int *)arg = snd_seq_client_pool_get_output_room(pool);
+               else
+                       *(int *)arg = 0;
+               return 0;
+
+       case SNDCTL_SEQ_THRESHOLD:
+               return 0;
+
+       case SNDCTL_SEQ_RESETSAMPLES:
+       case SNDCTL_SYNTH_MEMAVL:
+       case SNDCTL_FM_4OP_ENABLE:
+               errno = EINVAL;
+               return -1;
+
+       case SNDCTL_SEQ_TESTMIDI:
+               dev = *(int *)arg;
+               if (dev < 0 || dev >= dp->max_mididev) {
+                       errno = ENXIO;
+                       return -1;
+               }
+               return 0;
+
+       case SNDCTL_SEQ_OUTOFBAND:
+               if (!arg) {
+                       errno = EINVAL;
+                       return -1;
+               }
+               memcpy(&ev, arg, LONG_EVENT_SIZE);
+               snd_seq_ev_clear(&sev);
+               rc = process_event(dp, &ev, &sev);
+               if (rc < 0) {
+                       errno = -rc;
+                       return -1;
+               }
+               if (rc == 0) {
+                       snd_seq_ev_set_direct(&sev);
+                       snd_seq_event_output_direct(dp->seq, &sev);
+               }
+               return 0;
+
+       case SNDCTL_SYNTH_INFO:
+       case SNDCTL_SYNTH_ID:
+               sinfo = arg;
+               dev = sinfo->device;
+               if (dp->seq_mode != OSS_SEQ_MODE_MUSIC || dev < 0 || dev >= dp->max_mididev) {
+                       errno = ENXIO;
+                       return -1;
+               }
+               memset(sinfo, 0, sizeof(*sinfo));
+               sinfo->device = dev;
+               sinfo->synth_type = SYNTH_TYPE_MIDI;
+               sinfo->synth_subtype = 0;
+               sinfo->nr_voices = 16;
+               snprintf(sinfo->name, sizeof(sinfo->name), "%s", dp->midi_devs[dev].name);
+               return 0;
+
+       case SNDCTL_MIDI_INFO:
+               minfo = arg;
+               dev = minfo->device;
+               if (dev < 0 || dev >= dp->max_mididev) {
+                       errno = ENXIO;
+                       return -1;
+               }
+               memset(minfo, 0, sizeof(*minfo));
+               minfo->device = dev;
+               snprintf(minfo->name, sizeof(minfo->name), "%s", dp->midi_devs[dev].name);
+               return 0;
+
+       case SNDCTL_MIDI_PRETIME:
+               return 0;
+
+       case SNDCTL_TMR_TIMEBASE:
+               val = *(int *)arg;
+               if (val > 0) {
+                       dp->timer.oss_timebase = val;
+                       if (dp->timer.running)
+                               apply_queue_tempo(dp);
+               }
+               *(int *)arg = dp->timer.oss_timebase;
+               return 0;
+
+       case SNDCTL_TMR_TEMPO:
+               val = *(int *)arg;
+               if (val > 0) {
+                       dp->timer.oss_tempo = val;
+                       if (dp->timer.running)
+                               apply_queue_tempo(dp);
+               }
+               *(int *)arg = dp->timer.oss_tempo;
+               return 0;
+
+       case SNDCTL_TMR_START:
+               timer_start(dp);
+               return 0;
+
+       case SNDCTL_TMR_STOP:
+               timer_stop(dp);
+               return 0;
+
+       case SNDCTL_TMR_CONTINUE:
+               timer_continue(dp);
+               return 0;
+
+       case SNDCTL_TMR_METRONOME:
+               return 0;
+
+       case SNDCTL_TMR_SOURCE:
+               *(int *)arg = TMR_INTERNAL;
+               return 0;
+
+       case SNDCTL_TMR_SELECT:
+               return 0;
+
+       case OSS_GETVERSION:
+               *(int *)arg = 0x030901;
+               return 0;
+
+       default:
+               errno = EINVAL;
+               return -1;
+       }
+}
+
+/* Public API: poll support */
+
+int lib_oss_seq_poll_fds(int fd)
+{
+       oss_seq_t *dp = find_seq(fd);
+       int n = 0;
+
+       if (!dp)
+               return 0;
+       if (dp->readq) {
+               n++;  /* pipe_rd */
+               n += snd_seq_poll_descriptors_count(dp->seq, POLLIN);
+       }
+       if (dp->file_mode == O_WRONLY || dp->file_mode == O_RDWR)
+               n += snd_seq_poll_descriptors_count(dp->seq, POLLOUT);
+       /* subtract 1 because the caller already accounts for the base fd */
+       return n > 0 ? n - 1 : 0;
+}
+
+int lib_oss_seq_poll_prepare(int fd, int fmode, struct pollfd *ufds)
+{
+       oss_seq_t *dp = find_seq(fd);
+       int n = 0, cnt;
+
+       if (!dp)
+               return 0;
+       if ((fmode == O_RDONLY || fmode == O_RDWR) && dp->readq) {
+               ufds[n].fd = dp->readq->pipe_rd;
+               ufds[n].events = POLLIN;
+               ufds[n].revents = 0;
+               n++;
+               cnt = snd_seq_poll_descriptors(dp->seq, &ufds[n],
+                                              snd_seq_poll_descriptors_count(dp->seq, POLLIN),
+                                              POLLIN);
+               n += cnt;
+       }
+       if (fmode == O_WRONLY || fmode == O_RDWR) {
+               cnt = snd_seq_poll_descriptors(dp->seq, &ufds[n],
+                                              snd_seq_poll_descriptors_count(dp->seq, POLLOUT),
+                                              POLLOUT);
+               n += cnt;
+       }
+       return n;
+}
+
+int lib_oss_seq_poll_result(int fd, struct pollfd *ufds)
+{
+       oss_seq_t *dp = find_seq(fd);
+       unsigned short revents = 0;
+       int result = 0, n = 0, cnt;
+
+       if (!dp)
+               return OSS_WAIT_EVENT_ERROR;
+       if (dp->readq) {
+               if (ufds[n].revents & (POLLIN | POLLHUP))
+                       result |= OSS_WAIT_EVENT_READ;
+               if (ufds[n].revents & POLLERR)
+                       result |= OSS_WAIT_EVENT_ERROR;
+               n++;
+               cnt = snd_seq_poll_descriptors_count(dp->seq, POLLIN);
+               snd_seq_poll_descriptors_revents(dp->seq, &ufds[n], cnt, &revents);
+               if (revents & POLLIN)
+                       result |= OSS_WAIT_EVENT_READ;
+               if (revents & POLLERR)
+                       result |= OSS_WAIT_EVENT_ERROR;
+               n += cnt;
+       }
+       if (dp->file_mode == O_WRONLY || dp->file_mode == O_RDWR) {
+               cnt = snd_seq_poll_descriptors_count(dp->seq, POLLOUT);
+               snd_seq_poll_descriptors_revents(dp->seq, &ufds[n], cnt, &revents);
+               if (revents & POLLOUT)
+                       result |= OSS_WAIT_EVENT_WRITE;
+               if (revents & POLLERR)
+                       result |= OSS_WAIT_EVENT_ERROR;
+       }
+       return result;
+}
+
+/* select wrappers (thin; poll is the primary interface) */
+
+int lib_oss_seq_select_prepare(int fd, int fmode, fd_set *rfds,
+                              fd_set *wfds __attribute__((unused)),
+                              fd_set *efds __attribute__((unused)))
+{
+       oss_seq_t *dp = find_seq(fd);
+       struct pollfd tmp[8];
+       int maxfd = 0, n, i;
+
+       if (!dp)
+               return 0;
+       if ((fmode == O_RDONLY || fmode == O_RDWR) && dp->readq) {
+               FD_SET(dp->readq->pipe_rd, rfds);
+               if (dp->readq->pipe_rd > maxfd)
+                       maxfd = dp->readq->pipe_rd;
+               n = snd_seq_poll_descriptors_count(dp->seq, POLLIN);
+               if (n > (int)(sizeof(tmp) / sizeof(tmp[0])))
+                       n = sizeof(tmp) / sizeof(tmp[0]);
+               n = snd_seq_poll_descriptors(dp->seq, tmp, n, POLLIN);
+               for (i = 0; i < n; i++) {
+                       if (tmp[i].events & POLLIN) {
+                               FD_SET(tmp[i].fd, rfds);
+                               if (tmp[i].fd > maxfd)
+                                       maxfd = tmp[i].fd;
+                       }
+               }
+       }
+       return maxfd;
+}
+
+int lib_oss_seq_select_result(int fd, fd_set *rfds,
+                             fd_set *wfds __attribute__((unused)),
+                             fd_set *efds __attribute__((unused)))
+{
+       oss_seq_t *dp = find_seq(fd);
+       struct pollfd tmp[8];
+       int result = 0, n, i;
+
+       if (!dp)
+               return OSS_WAIT_EVENT_ERROR;
+       if (dp->readq && rfds) {
+               if (FD_ISSET(dp->readq->pipe_rd, rfds))
+                       result |= OSS_WAIT_EVENT_READ;
+               n = snd_seq_poll_descriptors_count(dp->seq, POLLIN);
+               if (n > (int)(sizeof(tmp) / sizeof(tmp[0])))
+                       n = sizeof(tmp) / sizeof(tmp[0]);
+               n = snd_seq_poll_descriptors(dp->seq, tmp, n, POLLIN);
+               for (i = 0; i < n; i++) {
+                       if ((tmp[i].events & POLLIN) && FD_ISSET(tmp[i].fd, rfds)) {
+                               result |= OSS_WAIT_EVENT_READ;
+                               break;
+                       }
+               }
+       }
+       return result;
+}