diff --git a/lib/libaudio/Makefile b/lib/libaudio/Makefile new file mode 100644 --- /dev/null +++ b/lib/libaudio/Makefile @@ -0,0 +1,22 @@ +LIB= audio +SRCS= ${LIB}.c +INCS= ${LIB}.h +MAN= ${LIB}.3 +VERSION_DEF= ${LIBCSRCDIR}/Versions.def +SYMBOL_MAPS= ${.CURDIR}/Symbol.map +LDFLAGS+= -lmixer -lnv + +MLINKS+= audio.3 midi.3 + +MLINKS+= audio.3 audio_open.3 +MLINKS+= audio.3 audio_close.3 +MLINKS+= audio.3 audio_read.3 +MLINKS+= audio.3 audio_write.3 +MLINKS+= audio.3 audio_set_vol.3 +MLINKS+= audio.3 audio_get_vol.3 +MLINKS+= audio.3 midi_open.3 +MLINKS+= audio.3 midi_close.3 +MLINKS+= audio.3 midi_read.3 +MLINKS+= audio.3 midi_write.3 + +.include diff --git a/lib/libaudio/Symbol.map b/lib/libaudio/Symbol.map new file mode 100644 --- /dev/null +++ b/lib/libaudio/Symbol.map @@ -0,0 +1,13 @@ +FBSD_1.8 { + audio_open; + audio_close; + audio_read; + audio_write; + audio_set_vol; + audio_get_vol; + + midi_open; + midi_close; + midi_read; + midi_write; +}; diff --git a/lib/libaudio/audio.h b/lib/libaudio/audio.h new file mode 100644 --- /dev/null +++ b/lib/libaudio/audio.h @@ -0,0 +1,113 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2024 The FreeBSD Foundation + * + * This software was developed by Christos Margiolis + * under sponsorship from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#ifndef _AUDIO_H_ +#define _AUDIO_H_ + +#include +#include + +#include +#include +#include + +#define AUDIO_DEFAULT_FORMAT AFMT_S16_NE +#define AUDIO_DEFAULT_CHANNELS 1 +#define AUDIO_DEFAULT_RATE 44100 + +#define AUDIO_VOLMIN 0 +#define AUDIO_VOLMAX 100 + +struct audio_channel { + char name[NAME_MAX]; + int unit; + int caps; + int format; + int channels; + int rate; + int min_rate; + int max_rate; + size_t bufsz; + size_t sample_size; + size_t frame_size; + /* TODO bps */ + int frame_total; + char *map; + struct audio_device *device; +}; + +struct audio_device { +#define AUDIO_REC 0x0001 +#define AUDIO_PLAY 0x0002 +#define AUDIO_NBIO 0x0004 +#define AUDIO_EXCL 0x0008 +#define AUDIO_MMAP 0x0010 + int open_mode; + int fd; + int unit; + char name[NAME_MAX]; + char desc[NAME_MAX]; + char devnode[NAME_MAX]; + bool is_default; + int caps; + int format; /* XXX Do we need it here as well? */ + int rate; /* XXX Do we need it here as well? */ + int channels; /* XXX Do we need it here as well? */ + struct mixer *mixer; + struct audio_channel *chan_in; + struct audio_channel *chan_out; +}; + +struct midi_device { +#define MIDI_IN 0x0001 +#define MIDI_OUT 0x0002 +#define MIDI_NBIO 0x0004 + int open_mode; + int fd; + /* TODO Add more once we have better support. */ +}; + +__BEGIN_DECLS + +struct audio_device *audio_open(const char *, int, int, int, int); +int audio_close(struct audio_device *); +ssize_t audio_read(struct audio_device *, void *, size_t); +ssize_t audio_write(struct audio_device *, void *, size_t); +int audio_set_vol(struct audio_channel *, int); +int audio_get_vol(struct audio_channel *); + +struct midi_device *midi_open(const char *, int); +int midi_close(struct midi_device *); +ssize_t midi_read(struct midi_device *, void *, size_t); +ssize_t midi_write(struct midi_device *, void *, size_t); + +__END_DECLS + +#endif /* _AUDIO_H_ */ diff --git a/lib/libaudio/audio.3 b/lib/libaudio/audio.3 new file mode 100644 --- /dev/null +++ b/lib/libaudio/audio.3 @@ -0,0 +1,54 @@ +.\"- +.\" SPDX-License-Identifier: BSD-2-Clause +.\" +.\" Copyright (c) 2024 The FreeBSD Foundation +.\" +.\" Portions of this software were developed by Christos Margiolis +.\" under sponsorship from the FreeBSD Foundation. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd June 19, 2024 +.Dt AUDIO 3 +.Os +.Sh NAME +.Nm audio_open , +.Nm audio_close , +.Nm audio_read , +.Nm audio_write , +.Nm audio_set_vol , +.Nm audio_get_vol , +.Nm midi_open , +.Nm midi_close , +.Nm midi_read , +.Nm midi_write +.Nd interface to the audio and MIDI subsystem +.Sh LIBRARY +Audio and MIDI library (libaudio, -laudio) +.Sh SYNOPSIS +.In audio.h +.Sh AUTHORS +The +.Nm +library was implemented by +.An Christos Margiolis Aq Mt christos@FreeBSD.org +under sponsorship from the FreeBSD Foundation. diff --git a/lib/libaudio/audio.c b/lib/libaudio/audio.c new file mode 100644 --- /dev/null +++ b/lib/libaudio/audio.c @@ -0,0 +1,704 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2024 The FreeBSD Foundation + * + * This software was developed by Christos Margiolis + * under sponsorship from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "audio.h" + +#define DSP_BASEPATH "/dev/dsp" + +#define LOG_HELPER(fmt, ...) \ + syslog(LOG_ERR, "%s:%d: " fmt "%s\n", __func__, __LINE__, __VA_ARGS__) +#define LOG(...) \ + LOG_HELPER(__VA_ARGS__, "") + +/* TODO Change name? */ +static int +_pollwait(int fd, int events) +{ + struct pollfd pfd; + + pfd.fd = fd; + pfd.events = events; + + for (;;) { + while (poll(&pfd, 1, -1) < 0) { + if (errno == EINTR) + continue; + LOG("poll() failed"); + return (-1); + } + if (pfd.revents & POLLHUP) { + errno = ENODEV; + return (-1); + } + if ((pfd.revents & events) != 0) + break; + } + + return (0); +} + +/* TODO Expose it in header file? */ +static int +audio_fmt2bytes(int fmt) +{ + if (fmt & (AFMT_S8 | AFMT_U8)) + return (1); + else if (fmt & (AFMT_S16_NE | AFMT_U16_NE)) + return (2); + else if (fmt & (AFMT_S24_NE | AFMT_U24_NE)) + return (3); + else if (fmt & (AFMT_S32_NE | AFMT_U32_NE)) + return (4); + else { + LOG("unknown format: %#08x", fmt); + return (0); + } +} + +struct audio_device * +audio_open(const char *path, int open_mode, int format, int channels, int rate) +{ + struct audio_device *d; + struct audio_channel *c; + struct sndstioc_nv_arg arg = {0}; + const nvlist_t * const *di; + const nvlist_t * const *cdi; + nvlist_t *nvl = NULL; + oss_audioinfo ai; + oss_sysinfo si; + audio_buf_info bi; + char buf[NAME_MAX]; + size_t i, n; + int stfd = -1, tmp; + + if ((d = calloc(1, sizeof(struct audio_device))) == NULL) { + LOG("cannot allocate audio_device"); + goto fail; + } + + open_mode &= AUDIO_REC | AUDIO_PLAY | AUDIO_NBIO | + AUDIO_EXCL | AUDIO_MMAP; + if (!open_mode) { + LOG("no valid open mode specified"); + errno = EINVAL; + goto fail; + } + + tmp = 0; + if ((open_mode & (AUDIO_REC | AUDIO_PLAY)) == (AUDIO_REC | AUDIO_PLAY)) + tmp |= O_RDWR; + else if (open_mode & AUDIO_REC) + tmp |= O_RDONLY; + else if (open_mode & AUDIO_PLAY) + tmp |= O_WRONLY; + else { + LOG("please specify at least one audio direction"); + errno = EINVAL; + goto fail; + } + if (open_mode & AUDIO_NBIO) + tmp |= O_NONBLOCK; + if (open_mode & AUDIO_EXCL) + tmp |= O_EXCL; + + if (path == NULL || strcmp(path, DSP_BASEPATH) == 0) { + d->unit = -1; + path = DSP_BASEPATH; + } else { + /* FIXME Handle relative and userdev paths */ + d->unit = strtol(path + strlen(DSP_BASEPATH), NULL, 10); + if (errno == EINVAL || errno == ERANGE) { + LOG("strtol(%s) failed", path + strlen(DSP_BASEPATH)); + goto fail; + } + } + + if ((d->fd = open(path, tmp)) < 0) { + LOG("open(%s, %#04x) failed", path, tmp); + goto fail; + } + + ai.dev = d->unit; + if (ioctl(d->fd, + open_mode & AUDIO_EXCL ? SNDCTL_AUDIOINFO_EX : SNDCTL_AUDIOINFO, + &ai) < 0) { + LOG("ioctl(SNDCTL_AUDIOINFO%s, %d) failed", + d->open_mode & AUDIO_EXCL ? "_EX" : "", d->unit); + goto fail; + } + d->unit = ai.dev; + d->caps = ai.caps; + d->is_default = d->unit == mixer_get_dunit(); + d->open_mode = open_mode; + strlcpy(d->desc, ai.name, sizeof(d->desc)); + strlcpy(d->devnode, ai.devnode, sizeof(d->devnode)); + + mixer_get_path(buf, sizeof(buf), d->unit); + d->mixer = mixer_open(buf); + + if (ioctl(d->fd, OSS_SYSINFO, &si) < 0) { + LOG("ioctl(OSS_SYSINFO) failed"); + goto fail; + } + + /* Memory map prior to FreeBSD 13.2 may use wrong buffer sizes. */ + if (strncmp(si.version, "1302000", 7) < 0) { + /* XXX Fail or just disable mmap silently? */ + if (d->open_mode & AUDIO_MMAP) { + LOG("memory map not working properly prior to 13.2"); + errno = ENODEV; + goto fail; + } + d->caps &= ~PCM_CAP_MMAP; + } + + if ((d->open_mode & (AUDIO_NBIO | AUDIO_MMAP)) && + (d->caps & PCM_CAP_TRIGGER) == 0) { + LOG("triggering not supported"); + errno = ENODEV; + goto fail; + } + + if ((d->open_mode & AUDIO_MMAP) && (d->caps & PCM_CAP_MMAP) == 0) { + LOG("memory mapping not supported"); + errno = ENODEV; + goto fail; + } + + /* + * Disable format conversions if we are in exclusive or memory map + * mode. + */ + if (d->open_mode & (AUDIO_EXCL | AUDIO_MMAP)) { + /* + * The OSS docs advise against checking the return value of + * this IOCTL. + */ + n = 0; + ioctl(d->fd, SNDCTL_DSP_COOKEDMODE, &n); + } + + /* Set sample format. */ + if (format <= 0) + format = AUDIO_DEFAULT_FORMAT; + tmp = format; + if (ioctl(d->fd, SNDCTL_DSP_SETFMT, &tmp) < 0) { + LOG("ioctl(SNDCTL_DSP_SETFMT, %#08x) failed", format); + goto fail; + } + if (tmp != format) + LOG("driver forced sample format: %#08x -> %#08x", format, tmp); + d->format = tmp; + + /* Set sample channels. */ + if (channels <= 0) + channels = AUDIO_DEFAULT_CHANNELS; + tmp = channels; + if (ioctl(d->fd, SNDCTL_DSP_CHANNELS, &tmp) < 0) { + LOG("ioctl(SNDCTL_DSP_CHANNELS, %d) failed", channels); + goto fail; + } + if (tmp != channels) + LOG("driver forced sample channels: %d -> %d", channels, tmp); + d->channels = tmp; + + /* Set sample rate. */ + if (rate <= 0) + rate = AUDIO_DEFAULT_RATE; + tmp = rate; + if (ioctl(d->fd, SNDCTL_DSP_SPEED, &tmp) < 0) { + LOG("ioctl(SNDCTL_DSP_SPEED, %d) failed", rate); + goto fail; + } + if (tmp != rate) + LOG("driver forced sample rate: %d -> %d", rate, tmp); + d->rate = tmp; + + /* + * Read /dev/sndstat nvlist for some contents we cannot fetch from the + * AUDIOINFO/ENGINEINFO IOCTLs. + */ + if ((stfd = open("/dev/sndstat", O_RDONLY)) < 0) { + LOG("open(/dev/sndstat) failed"); + goto fail; + } + if (ioctl(stfd, SNDSTIOC_REFRESH_DEVS, NULL) < 0) { + LOG("ioctl(SNDSTIOC_REFRESH_DEVS) failed"); + goto fail; + } + arg.nbytes = 0; + arg.buf = NULL; + if (ioctl(stfd, SNDSTIOC_GET_DEVS, &arg) < 0) { + LOG("ioctl(SNDSTIOC_GET_DEVS#1) failed"); + goto fail; + } + if ((arg.buf = malloc(arg.nbytes)) == NULL) { + LOG("cannot allocate sndstat buffer"); + goto fail; + } + if (ioctl(stfd, SNDSTIOC_GET_DEVS, &arg) < 0) { + LOG("ioctl(SNDSTIOC_GET_DEVS#2) failed"); + goto fail; + } + nvl = nvlist_unpack(arg.buf, arg.nbytes, 0); + di = nvlist_get_nvlist_array(nvl, SNDST_DSPS, &n); + + /* Search for our device. */ + for (i = 0; i < n; i++) + if (i == (size_t)d->unit) + break; + if (i == n) { + LOG("cannot find device %d in sndstat nvlist", d->unit); + errno = ENODEV; + goto fail; + } + + strlcpy(d->name, nvlist_get_string(di[i], SNDST_DSPS_NAMEUNIT), + sizeof(d->name)); + + if (!nvlist_exists(di[i], SNDST_DSPS_PROVIDER_INFO)) { + LOG("%s: provider info nvlist not found", d->name); + errno = ENODEV; + goto fail; + } + + cdi = nvlist_get_nvlist_array( + nvlist_get_nvlist(di[i], SNDST_DSPS_PROVIDER_INFO), + SNDST_DSPS_SOUND4_CHAN_INFO, &n); + + for (i = 0; i < n; i++) { + ai.dev = i; + if (ioctl(d->fd, SNDCTL_ENGINEINFO, &ai) < 0) { + LOG("%s: ioctl(SNDCTL_ENGINEINFO, %zu) failed", + d->name, i); + goto fail; + } + /* We only care about our process' channel(s). */ + if (getpid() != ai.pid) + continue; + if ((c = calloc(1, sizeof(struct audio_channel))) == NULL) { + LOG("%s: cannot allocate audio_channel", d->name); + goto fail; + } + c->device = d; + strlcpy(c->name, ai.name, sizeof(c->name)); + c->unit = ai.dev; + c->caps = ai.caps; + c->min_rate = ai.min_rate; + c->max_rate = ai.max_rate; + c->channels = d->channels; +#define NV(type, id) \ + nvlist_get_ ## type (cdi[i], SNDST_DSPS_SOUND4_CHAN_ ## id) + c->format = NV(number, FORMAT); + c->rate = NV(number, RATE); +#undef NV + /* TODO Handle buffer size assignment */ + if (c->caps & PCM_CAP_INPUT) { + if (ioctl(d->fd, SNDCTL_DSP_GETISPACE, &bi) < 0) { + LOG("%s: ioctl(SNDCTL_DSP_GETISPACE)", c->name); + goto fail; + } + tmp = PROT_READ; + d->chan_in = c; + } else if (c->caps & PCM_CAP_OUTPUT) { + if (ioctl(d->fd, SNDCTL_DSP_GETOSPACE, &bi) < 0) { + LOG("%s: ioctl(SNDCTL_DSP_GETOSPACE)", c->name); + goto fail; + } + tmp = PROT_WRITE; + d->chan_out = c; + } + + c->bufsz = bi.fragstotal * bi.fragsize; + c->sample_size = audio_fmt2bytes(c->format); + c->frame_size = c->sample_size * c->channels; + c->frame_total = c->bufsz / c->frame_size; + + /* Memory map buffer */ + if (d->open_mode & AUDIO_MMAP) { + c->map = mmap(NULL, c->bufsz, tmp, + MAP_FILE | MAP_SHARED, d->fd, 0); + if (c->map == MAP_FAILED) { + LOG("%s: cannot mmap %s buffer", + c->name, tmp == PROT_READ ? + "recording" : "playback"); + goto fail; + } + for (i = 0; i < c->bufsz; i++) + c->map[i] = 0; + } + } + if (d->chan_in == NULL && d->chan_out == NULL) { + LOG("%s: no channels allocated", d->name); + errno = ENODEV; + goto fail; + } + + /* Trigger channel start */ + if (d->open_mode & (AUDIO_NBIO | AUDIO_MMAP)) { + tmp = 0; + if (ioctl(d->fd, SNDCTL_DSP_SETTRIGGER, &tmp) < 0) { + LOG("%s: ioctl(SNDCTL_DSP_SETTRIGGER#1) failed", + d->name); + goto fail; + } + + tmp = 0; + if (d->chan_in != NULL) + tmp |= PCM_ENABLE_INPUT; + if (d->chan_out != NULL) + tmp |= PCM_ENABLE_OUTPUT; + if (ioctl(d->fd, SNDCTL_DSP_SETTRIGGER, &tmp) < 0) { + LOG("%s: ioctl(SNDCTL_DSP_SETTRIGGER#2) failed", + d->name); + goto fail; + } + } + + close(stfd); + free(arg.buf); + nvlist_destroy(nvl); + + return (d); + +fail: + close(stfd); + nvlist_destroy(nvl); + if (arg.buf != NULL) + free(arg.buf); + if (d != NULL) + audio_close(d); + + return (NULL); +} + +int +audio_close(struct audio_device *d) +{ + struct audio_channel *c; + int rc; + + if (d == NULL) + return (0); + + rc = close(d->fd); + if (d->chan_in != NULL) { + c = d->chan_in; + if (d->open_mode & AUDIO_MMAP) + munmap(c->map, c->bufsz); + free(c); + } + if (d->chan_out != NULL) { + c = d->chan_out; + if (d->open_mode & AUDIO_MMAP) + munmap(c->map, c->bufsz); + free(c); + } + free(d); + + return (rc); +} + +static ssize_t +audio_io(struct audio_device *d, void *buf, size_t nbytes, bool rd) +{ + struct audio_channel *c; + count_info ci; + char *data; + size_t todo; + ssize_t n; + + if (d == NULL) { + LOG("device is null"); + errno = EINVAL; + return (-1); + } + + if ((rd && d->chan_in == NULL) || (!rd && d->chan_out == NULL)) { + LOG("%s: no %s channel", d->name, rd ? "recording" : "playback"); + errno = EINVAL; + return (-1); + } + + c = rd ? d->chan_in : d->chan_out; + + if (nbytes == 0) + return (0); + if (nbytes > c->bufsz && rd) + nbytes = c->bufsz; + + for (data = buf, todo = nbytes; todo > 0;) { + /* Wait for events. */ + if (d->open_mode & (AUDIO_NBIO | AUDIO_MMAP) && + _pollwait(d->fd, rd ? POLLIN : POLLOUT) < 0) + break; + + /* FIXME */ + /* Calculate payload in case of mmap. */ + if (d->open_mode & AUDIO_MMAP) { + if (ioctl(d->fd, rd ? SNDCTL_DSP_GETIPTR : + SNDCTL_DSP_GETOPTR, &ci) < 0) { + LOG("%s: ioctl(SNDCTL_DSP_GET%sPTR) failed", + c->name, rd ? "I" : "O"); + break; + } + if (!ci.bytes) + continue; + ci.ptr %= c->bufsz; + n = c->bufsz - ci.ptr; + if ((size_t)n > todo) + n = todo; + LOG("todo=%ld, ptr=%d, n=%ld", todo, ci.ptr, n); + } + + /* Do the actual reading/writing. */ + if (rd) { + if (d->open_mode & AUDIO_MMAP) + memcpy(data, c->map + ci.ptr, n); + else + n = read(d->fd, data, todo); + } else { + if (d->open_mode & AUDIO_MMAP) + memcpy(c->map + ci.ptr, data, n); + else + n = write(d->fd, data, todo); + } + if ((d->open_mode & AUDIO_MMAP) == 0 && errno == EAGAIN) + continue; + if (n >= 0) { + data += n; + todo -= n; + } else { + LOG("%s: failed to %s: %s", rd ? "read" : "write", + c->name, strerror(errno)); + break; + } + } + + return (nbytes - todo); +} + +ssize_t +audio_read(struct audio_device *d, void *buf, size_t nbytes) +{ + return (audio_io(d, buf, nbytes, true)); +} + +ssize_t +audio_write(struct audio_device *d, void *buf, size_t nbytes) +{ + return (audio_io(d, buf, nbytes, false)); +} + +int +audio_set_vol(struct audio_channel *c, int vol) +{ + if (c == NULL) { + errno = EINVAL; + return (-1); + } + + /* + * We could disallow volumes outside the [AUDIO_VOLMIN, AUDIO_VOLMAX] + * range, but sound(4) will cap them anyway. + */ + if (c->caps & PCM_CAP_INPUT) { + if (ioctl(c->device->fd, SNDCTL_DSP_SETRECVOL, &vol) < 0) { + LOG("%s: ioctl(SNDCTL_DSP_SETRECVOL) failed", c->name); + return (-1); + } + } else if (c->caps & PCM_CAP_OUTPUT) { + if (ioctl(c->device->fd, SNDCTL_DSP_SETPLAYVOL, &vol) < 0) { + LOG("%s: ioctl(SNDCTL_DSP_SETPLAYVOL) failed", c->name); + return (-1); + } + } + + return (0); +} + +int +audio_get_vol(struct audio_channel *c) +{ + int vol = 0; + + if (c == NULL) { + errno = EINVAL; + return (-1); + } + + if (c->caps & PCM_CAP_INPUT) { + if (ioctl(c->device->fd, SNDCTL_DSP_GETRECVOL, &vol) < 0) { + LOG("%s: ioctl(SNDCTL_DSP_SETRECVOL) failed", c->name); + return (-1); + } + } else if (c->caps & PCM_CAP_OUTPUT) { + if (ioctl(c->device->fd, SNDCTL_DSP_GETPLAYVOL, &vol) < 0) { + LOG("%s: ioctl(SNDCTL_DSP_GETPLAYVOL) failed", c->name); + return (-1); + } + } + + return (vol); +} + +struct midi_device * +midi_open(const char *path, int open_mode) +{ + struct midi_device *d = NULL; + int tmp; + + if (path == NULL) { + LOG("path not specified"); + errno = EINVAL; + goto fail; + } + + if ((d = calloc(1, sizeof(struct midi_device))) == NULL) { + LOG("cannot allocate midi_device"); + goto fail; + } + + open_mode &= MIDI_IN | MIDI_OUT | MIDI_NBIO; + if (!open_mode) { + LOG("no valid open mode specified"); + errno = EINVAL; + goto fail; + } + + tmp = 0; + if ((open_mode & (MIDI_IN | MIDI_OUT)) == (MIDI_IN | MIDI_OUT)) + tmp |= O_RDWR; + else if (open_mode & MIDI_IN) + tmp |= O_RDONLY; + else if (open_mode & MIDI_OUT) + tmp |= O_WRONLY; + else { + LOG("please specify at least one midi direction"); + errno = EINVAL; + goto fail; + } + if (open_mode & MIDI_NBIO) + tmp |= O_NONBLOCK; + + /* XXX Path checks like in audio_open()? */ + /* TODO Somehow verify this is indeed a MIDI device */ + + if ((d->fd = open(path, tmp)) < 0) { + LOG("open(%s, %#04x) failed", path, tmp); + goto fail; + } + + d->open_mode = open_mode; + + return (d); + +fail: + if (d != NULL) + midi_close(d); + + return (NULL); +} + +int +midi_close(struct midi_device *d) +{ + int rc; + + rc = close(d->fd); + free(d); + + return (rc); +} + +static ssize_t +midi_io(struct midi_device *d, void *buf, size_t nbytes, bool rd) +{ + char *data; + size_t todo; + ssize_t n; + + if (d == NULL) { + LOG("device is null"); + errno = EINVAL; + return (-1); + } + + if (nbytes == 0) + return (0); + + /* + * XXX Do we need this loop if we usually read a handful of bytes only? + */ + for (data = buf, todo = nbytes; todo > 0;) { + if (_pollwait(d->fd, rd ? POLLIN : POLLOUT) < 0) + break; + + while ((n = rd ? read(d->fd, data, todo) : + write(d->fd, data, todo)) < 0) { + if (errno == EINTR) + continue; + if (errno != EAGAIN) { + LOG("failed to %s: %s", + rd ? "read" : "write", strerror(errno)); + goto out; + } + } + data += n; + todo -= n; + break; + } +out: + return (nbytes - todo); +} + +ssize_t +midi_read(struct midi_device *d, void *buf, size_t nbytes) +{ + return (midi_io(d, buf, nbytes, true)); +} + +ssize_t +midi_write(struct midi_device *d, void *buf, size_t nbytes) +{ + return (midi_io(d, buf, nbytes, false)); +}