root/usr/src/uts/common/io/audio/impl/audio_oss.c
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright (c) 2009, 2010, Oracle and/or its affiliates. All rights reserved.
 */

#include <sys/types.h>
#include <sys/open.h>
#include <sys/errno.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/audio/audio_oss.h>
#include <sys/file.h>
#include <sys/note.h>
#include <sys/sysmacros.h>
#include <sys/list.h>
#include "audio_client.h"

#define OSS_FMT         AFMT_S16_LE
#define OSS_RATE        48000
#define OSS_CHANNELS    2

typedef struct ossclient ossclient_t;
typedef struct ossdev ossdev_t;

static const struct {
        int     oss;
        int     fmt;
} oss_formats[] = {
        { AFMT_MU_LAW,          AUDIO_FORMAT_ULAW },
        { AFMT_A_LAW,           AUDIO_FORMAT_ALAW },
        { AFMT_U8,              AUDIO_FORMAT_U8 },
        { AFMT_S8,              AUDIO_FORMAT_S8 },
        { AFMT_S16_BE,          AUDIO_FORMAT_S16_BE },
        { AFMT_S16_LE,          AUDIO_FORMAT_S16_LE },
        { AFMT_U16_BE,          AUDIO_FORMAT_U16_BE },
        { AFMT_U16_LE,          AUDIO_FORMAT_U16_LE },
        { AFMT_S24_BE,          AUDIO_FORMAT_S24_BE },
        { AFMT_S24_LE,          AUDIO_FORMAT_S24_LE },
        { AFMT_S32_BE,          AUDIO_FORMAT_S32_BE },
        { AFMT_S32_LE,          AUDIO_FORMAT_S32_LE },
        { AFMT_S24_PACKED,      AUDIO_FORMAT_S24_PACKED },
        { AFMT_AC3,             AUDIO_FORMAT_AC3 },
        { AFMT_QUERY,           AUDIO_FORMAT_NONE }
};

/* common structure shared between both mixer and dsp nodes */
struct ossclient {
        ossdev_t                *o_ossdev;
        audio_client_t          *o_client;
        /* sndstat */
        kmutex_t                o_ss_lock;
        char                    *o_ss_buf;
        size_t                  o_ss_len;
        size_t                  o_ss_sz;
        size_t                  o_ss_off;
};

struct ossdev {
        audio_dev_t             *d_dev;

        uint_t                  d_nctrl;        /* num actual controls */
        uint_t                  d_nalloc;       /* num allocated controls */
        audio_ctrl_t            **d_ctrls;      /* array of control handles */
        oss_mixext              *d_exts;        /* array of mixer descs */

        int                     d_play_grp;
        int                     d_rec_grp;
        int                     d_mon_grp;
        int                     d_misc_grp;

        kmutex_t                d_mx;
        kcondvar_t              d_cv;
};

static int
oss_cnt_controls(audio_ctrl_t *ctrl, void *arg)
{
        int                     *pint = (int *)arg;
        int                     cnt;
        audio_ctrl_desc_t       desc;

        cnt = *pint;
        cnt++;
        *pint = cnt;

        if (auclnt_control_describe(ctrl, &desc) != 0)
                return (AUDIO_WALK_CONTINUE);

        if (desc.acd_flags & AUDIO_CTRL_FLAG_MULTI) {
                for (uint64_t mask = desc.acd_maxvalue; mask; mask >>= 1) {
                        if (mask & 1) {
                                cnt++;
                        }
                }
                *pint = cnt;
        }

        return (AUDIO_WALK_CONTINUE);
}

/*
 * Add one entry to the OSS user control table to internal control
 * helper table.
 *
 * This is used with auimpl_walk_controls. The table must be pre-
 * allocated before it is walk'd. This includes the root and
 * extended control markers!
 */
static int
oss_add_control(audio_ctrl_t *ctrl, void *arg)
{
        ossdev_t                *odev = arg;
        audio_ctrl_desc_t       desc;
        oss_mixext              *ext;
        int                     bit;
        uint64_t                mask;
        const char              *name;
        int                     parent;
        int                     flags;
        unsigned                scope;

        if (auclnt_control_describe(ctrl, &desc))
                return (AUDIO_WALK_CONTINUE);

        parent = 0;

        /*
         * Add appropriate group if not already done so.
         */
        if (desc.acd_flags & AUDIO_CTRL_FLAG_PLAY) {
                if (!odev->d_play_grp) {
                        ext = &odev->d_exts[odev->d_nctrl];
                        ext->ctrl = odev->d_nctrl;
                        ext->control_no = -1;
                        ext->type = MIXT_GROUP;
                        ext->desc = MIXEXT_SCOPE_OUTPUT;
                        ext->timestamp = gethrtime();
                        (void) snprintf(ext->id, sizeof (ext->id), "PLAYBACK");
                        odev->d_play_grp = odev->d_nctrl;
                        odev->d_nctrl++;
                }
                scope = MIXEXT_SCOPE_OUTPUT;
                parent = odev->d_play_grp;
        } else if (desc.acd_flags & AUDIO_CTRL_FLAG_REC) {
                if (!odev->d_rec_grp) {
                        ext = &odev->d_exts[odev->d_nctrl];
                        ext->ctrl = odev->d_nctrl;
                        ext->control_no = -1;
                        ext->type = MIXT_GROUP;
                        ext->desc = MIXEXT_SCOPE_INPUT;
                        ext->timestamp = gethrtime();
                        (void) snprintf(ext->id, sizeof (ext->id), "RECORD");
                        odev->d_rec_grp = odev->d_nctrl;
                        odev->d_nctrl++;
                }
                scope = MIXEXT_SCOPE_INPUT;
                parent = odev->d_rec_grp;
        } else if (desc.acd_flags & AUDIO_CTRL_FLAG_MONITOR) {
                if (!odev->d_mon_grp) {
                        ext = &odev->d_exts[odev->d_nctrl];
                        ext->ctrl = odev->d_nctrl;
                        ext->control_no = -1;
                        ext->type = MIXT_GROUP;
                        ext->desc = MIXEXT_SCOPE_MONITOR;
                        ext->timestamp = gethrtime();
                        (void) snprintf(ext->id, sizeof (ext->id), "MONITOR");
                        odev->d_mon_grp = odev->d_nctrl;
                        odev->d_nctrl++;
                }
                scope = MIXEXT_SCOPE_MONITOR;
                parent = odev->d_mon_grp;
        } else {
                if (!odev->d_misc_grp) {
                        ext = &odev->d_exts[odev->d_nctrl];
                        ext->ctrl = odev->d_nctrl;
                        ext->control_no = -1;
                        ext->type = MIXT_GROUP;
                        ext->desc = MIXEXT_SCOPE_OTHER;
                        ext->timestamp = gethrtime();
                        (void) snprintf(ext->id, sizeof (ext->id), "MISC");
                        odev->d_misc_grp = odev->d_nctrl;
                        odev->d_nctrl++;
                }
                scope = MIXEXT_SCOPE_OTHER;
                parent = odev->d_misc_grp;
        }

        name = desc.acd_name ? desc.acd_name : "";

        if (desc.acd_flags & AUDIO_CTRL_FLAG_MULTI) {
                ext = &odev->d_exts[odev->d_nctrl];
                ext->ctrl = odev->d_nctrl;
                ext->control_no = -1;
                ext->type = MIXT_GROUP;
                ext->timestamp = gethrtime();
                ext->parent = parent;
                ext->desc = scope;
                (void) snprintf(ext->id, sizeof (ext->id), "%s", name);
                (void) snprintf(ext->extname, sizeof (ext->extname),
                    "%s", name);
                parent = odev->d_nctrl++;
        }

        /* Next available open entry */
        ext = &odev->d_exts[odev->d_nctrl];

        /* Record the underlying control handle */
        odev->d_ctrls[odev->d_nctrl] = ctrl;

        /*
         * Now setup the oss entry
         */

        ext->ctrl = odev->d_nctrl;
        ext->control_no = -1;
        ext->maxvalue = (int)desc.acd_maxvalue;
        ext->minvalue = (int)desc.acd_minvalue;
        ext->timestamp = gethrtime();
        ext->parent = parent;
        ext->desc = scope;
        /* all controls should be pollable for now */
        flags = MIXF_POLL;

        /*
         * The following flags are intended to help out applications
         * which need to figure out where to place certain controls.
         * A few further words of guidance:
         *
         * Apps that just want a single master volume control should
         * adjust the control(s) that are labelled with MIXF_PCMVOL if
         * present.  They can fall back to adjusting all MAINVOL
         * levels instead, if no PCMVOL is present.
         *
         * Controls that are one type on a certain device might be a
         * different type on another device.  For example,
         * audiopci/ak4531 can adjust input gains for individual
         * levels, but lacks a master record gain.  AC'97, on the
         * other hand, has individual monitor gains for inputs, but
         * only a single master recording gain.
         */
        if (desc.acd_flags & AUDIO_CTRL_FLAG_READABLE)
                flags |= MIXF_READABLE;
        if (desc.acd_flags & AUDIO_CTRL_FLAG_WRITEABLE)
                flags |= MIXF_WRITEABLE;
        if (desc.acd_flags & AUDIO_CTRL_FLAG_CENTIBEL)
                flags |= MIXF_CENTIBEL;
        if (desc.acd_flags & AUDIO_CTRL_FLAG_DECIBEL)
                flags |= MIXF_DECIBEL;
        if (desc.acd_flags & AUDIO_CTRL_FLAG_MAINVOL)
                flags |= MIXF_MAINVOL;
        if (desc.acd_flags & AUDIO_CTRL_FLAG_PCMVOL)
                flags |= MIXF_PCMVOL;
        if (desc.acd_flags & AUDIO_CTRL_FLAG_RECVOL)
                flags |= MIXF_RECVOL;
        if (desc.acd_flags & AUDIO_CTRL_FLAG_MONVOL)
                flags |= MIXF_MONVOL;
        ext->flags = flags;

        (void) snprintf(ext->id, sizeof (ext->id), "%s", name);

        /*
         * For now just use the same extname as the real name.
         */
        (void) snprintf(ext->extname, sizeof (ext->extname), name);

        /*
         * Now we deal with various control types.
         */
        switch (desc.acd_type) {
        case AUDIO_CTRL_TYPE_BOOLEAN:
                ext->type = MIXT_ONOFF;
                ext->enumbit = -1;
                break;
        case AUDIO_CTRL_TYPE_STEREO:
                ext->type = MIXT_STEREOSLIDER;
                break;
        case AUDIO_CTRL_TYPE_MONO:
                ext->type = MIXT_MONOSLIDER;
                break;
        case AUDIO_CTRL_TYPE_ENUM:

                if (desc.acd_flags & AUDIO_CTRL_FLAG_MULTI) {
                        /*
                         * We turn AUDIO_CTRL_FLAG_MULTI into a group
                         * of checkboxes, since OSS can't represent it
                         * natively.
                         */
                        mask = desc.acd_maxvalue;
                        bit = 0;
                        while (mask) {
                                if (mask & 1) {
                                        ext = &odev->d_exts[odev->d_nctrl];
                                        (void) snprintf(ext->extname,
                                            sizeof (ext->extname), "%s.%s",
                                            name, desc.acd_enum[bit]);
                                        (void) snprintf(ext->id,
                                            sizeof (ext->id), "%s",
                                            desc.acd_enum[bit]);
                                        ext->ctrl = odev->d_nctrl;
                                        ext->control_no = -1;
                                        ext->parent = parent;
                                        ext->timestamp = gethrtime();
                                        ext->type = MIXT_ONOFF;
                                        ext->minvalue = 0;
                                        ext->maxvalue = 1;
                                        ext->enumbit = bit;
                                        ext->flags = flags;
                                        odev->d_ctrls[odev->d_nctrl] = ctrl;
                                        odev->d_nctrl++;
                                }
                                bit++;
                                mask >>= 1;
                        }
                        return (AUDIO_WALK_CONTINUE);
                } else {
                        /*
                         * NB: This is sufficient only for controls
                         * with a single value.  It cannot express the
                         * richer bitmask capabilities.
                         */
                        ext->type = MIXT_ENUM;
                        ext->minvalue = 0;

                        /*
                         * For an enumaration, we need to figure out
                         * which values are present, and set the
                         * appropriate mask and max value.
                         */
                        bzero(ext->enum_present, sizeof (ext->enum_present));
                        mask = desc.acd_maxvalue;
                        bit = 0;
                        while (mask) {
                                if (mask & 1) {
                                        ext->enum_present[bit / 8] |=
                                            (1 << (bit % 8));
                                }
                                mask >>= 1;
                                bit++;
                        }
                        ext->maxvalue = bit;
                }
                break;

        case AUDIO_CTRL_TYPE_METER:
        default:
                /* Its an unknown or unsupported (for now) control, skip */
                return (AUDIO_WALK_CONTINUE);
        }

        odev->d_nctrl++;

        return (AUDIO_WALK_CONTINUE);
}

/*
 * Free up an OSS user land control to internal control,
 * helper table.
 */
static void
oss_free_controls(ossdev_t *odev)
{
        kmem_free(odev->d_ctrls, sizeof (audio_ctrl_t *) * odev->d_nalloc);
        kmem_free(odev->d_exts, sizeof (oss_mixext) * odev->d_nalloc);
        odev->d_nctrl = 0;
        odev->d_nalloc = 0;
}

/*
 * Allocate and fill in an OSS user land controls to internal controls
 * helper table. This is done on one audio_dev device.
 */
static void
oss_alloc_controls(ossdev_t *odev)
{
        audio_dev_t             *d = odev->d_dev;
        int                     nctrl = 0;
        oss_mixext              *ext;
        oss_mixext_root         *root_data;

        /* Find out who many entries we need */
        auclnt_walk_controls(d, oss_cnt_controls, &nctrl);
        nctrl++;                /* Needs space for the device root node */
        nctrl++;                /* Needs space for the device ext marker */
        nctrl++;                /* Needs space for the play group */
        nctrl++;                /* Needs space for the record group */
        nctrl++;                /* Needs space for the monitor group */
        nctrl++;                /* Needs space for the tone group */
        nctrl++;                /* Needs space for the 3D group */
        nctrl++;                /* Needs space for the misc group */

        /* Allocate the OSS to boomer helper table */
        odev->d_nalloc = nctrl;
        odev->d_ctrls = kmem_zalloc(sizeof (audio_ctrl_t *) * nctrl, KM_SLEEP);
        odev->d_exts = kmem_zalloc(sizeof (oss_mixext) * nctrl, KM_SLEEP);

        /*
         * Setup special case outputs to output OSS routes helper tables
         */

        /*
         * Root node is first, that way all others parent is this one
         */
        ext = &odev->d_exts[odev->d_nctrl];
        ext->ctrl = 0;
        ext->parent = -1;
        ext->type = MIXT_DEVROOT;
        ext->timestamp = gethrtime();
        (void) snprintf(ext->id, sizeof (ext->id), "DEVROOT");
        /*
         * Root data... nobody should be using this though.
         */
        root_data = (oss_mixext_root *)&ext->data;
        (void) snprintf(root_data->name, sizeof (root_data->name), "%s",
            auclnt_get_dev_name(d));
        (void) snprintf(root_data->id, sizeof (root_data->id), "%s",
            auclnt_get_dev_name(d));

        odev->d_nctrl++;

        /*
         * Insert an extra marker -- needed to keep layout apps hapy.
         * This prevents some apps from assuming we are in "LEGACY" mode.
         */
        ext = &odev->d_exts[odev->d_nctrl];
        ext->ctrl = odev->d_nctrl;
        ext->control_no = -1;
        ext->type = MIXT_MARKER;
        ext->timestamp = gethrtime();
        ext->parent = 0;
        odev->d_nctrl++;

        /* Fill in the complete table now */
        auclnt_walk_controls(d, oss_add_control, odev);

        /* Update the update_counter reference counter for groups */
        for (nctrl = 0; nctrl < odev->d_nctrl; nctrl++) {
                int i;

                ext = &odev->d_exts[nctrl];
                i = ext->parent;
                while ((i >= 0) && (i < odev->d_nctrl)) {

                        ext = &odev->d_exts[i];
                        ASSERT(ext->parent < i);
                        ASSERT((ext->type == MIXT_GROUP) ||
                            (ext->type == MIXT_DEVROOT));
                        ext->update_counter++;
                        i = ext->parent;
                }
        }

        ASSERT(odev->d_nctrl <= odev->d_nalloc);
}

static int
oss_open(audio_client_t *c, int oflag)
{
        int             rv;
        ossdev_t        *odev;
        ossclient_t     *sc;
        audio_stream_t  *isp, *osp;

        isp = auclnt_input_stream(c);
        osp = auclnt_output_stream(c);

        /* note that OSS always uses nonblocking open() semantics */
        if ((rv = auclnt_open(c, oflag | FNDELAY)) != 0) {
                return (rv);
        }

        if ((sc = kmem_zalloc(sizeof (*sc), KM_NOSLEEP)) == NULL) {
                auclnt_close(c);
                return (ENOMEM);
        }
        auclnt_set_private(c, sc);

        odev = auclnt_get_minor_data(c, AUDIO_MINOR_DSP);

        /* set a couple of common fields */
        sc->o_client = c;
        sc->o_ossdev = odev;

        /* set all default parameters */
        if (oflag & FWRITE) {
                if (((rv = auclnt_set_format(osp, OSS_FMT)) != 0) ||
                    ((rv = auclnt_set_rate(osp, OSS_RATE)) != 0) ||
                    ((rv = auclnt_set_channels(osp, OSS_CHANNELS)) != 0)) {
                        goto failed;
                }
                /* default to 5 fragments to provide reasonable latency */
                auclnt_set_latency(osp, 5, 0);
        }

        if (oflag & FREAD) {
                if (((rv = auclnt_set_format(isp, OSS_FMT)) != 0) ||
                    ((rv = auclnt_set_rate(isp, OSS_RATE)) != 0) ||
                    ((rv = auclnt_set_channels(isp, OSS_CHANNELS)) != 0)) {
                        goto failed;
                }
                /* default to 5 fragments to provide reasonable latency */
                auclnt_set_latency(isp, 5, 0);
        }

        return (0);

failed:
        auclnt_close(c);
        return (rv);
}

static void
oss_close(audio_client_t *c)
{
        ossclient_t     *sc;

        sc = auclnt_get_private(c);

        if (ddi_can_receive_sig() || (ddi_get_pid() == 0)) {
                (void) auclnt_drain(c);
        }

        kmem_free(sc, sizeof (*sc));

        auclnt_close(c);
}

/*
 * This is used to generate an array of names for an enumeration
 */
static ushort_t
oss_set_enum(oss_mixer_enuminfo *ei, ushort_t nxt, const char *name)
{
        uint32_t        n;

        /* Get current entry to fill in */
        n = ei->nvalues;
        (void) snprintf(&ei->strings[nxt], ((sizeof (ei->strings) - nxt) - 1),
            "%s", name);
        ei->strindex[n] = nxt;

        /* Adjust everything for next entry */
        nxt += strnlen(name, ((sizeof (ei->strings) - nxt) - 1));
        ei->strings[nxt++] = '\0';

        ei->nvalues++;
        return (nxt);
}

/*
 * The following two functions are used to count the number of devices
 * in under the boomer framework.
 *
 * We actually report the highest "index", and then if an audio device
 * is not found, we report a bogus removed device for it in the actual
 * ioctls.  This goofiness is required to make the OSS API happy.
 */
int
oss_dev_walker(audio_dev_t *d, void *arg)
{
        int             *pcnt = arg;
        int             cnt;
        int             index;

        cnt = *pcnt;
        index = auclnt_get_dev_index(d);
        if ((index + 1) > cnt) {
                cnt = index + 1;
                *pcnt = cnt;
        }

        return (AUDIO_WALK_CONTINUE);
}

static int
oss_cnt_devs(void)
{
        int cnt = 0;

        auclnt_walk_devs(oss_dev_walker, &cnt);
        return (cnt);
}

static int
sndctl_dsp_speed(audio_client_t *c, int *ratep)
{
        int             rate;
        int             oflag;

        rate = *ratep;

        oflag = auclnt_get_oflag(c);
        if (oflag & FREAD) {
                (void) auclnt_set_rate(auclnt_input_stream(c), rate);
                *ratep = auclnt_get_rate(auclnt_input_stream(c));
        }

        if (oflag & FWRITE) {
                (void) auclnt_set_rate(auclnt_output_stream(c), rate);
                *ratep = auclnt_get_rate(auclnt_output_stream(c));
        }

        return (0);
}

static int
sndctl_dsp_setfmt(audio_client_t *c, int *fmtp)
{
        int             fmt;
        int             i;
        int             oflag;

        oflag = auclnt_get_oflag(c);

        if (*fmtp != AFMT_QUERY) {
                /* convert from OSS */
                for (i = 0; oss_formats[i].fmt != AUDIO_FORMAT_NONE; i++) {
                        if (oss_formats[i].oss == *fmtp) {
                                fmt = oss_formats[i].fmt;
                                break;
                        }
                }
                if (fmt == AUDIO_FORMAT_NONE) {
                        /* if format not known, return */
                        goto done;
                }

                if (oflag & FWRITE) {
                        (void) auclnt_set_format(auclnt_output_stream(c), fmt);
                }

                if (oflag & FREAD) {
                        (void) auclnt_set_format(auclnt_input_stream(c), fmt);
                }
        }

done:
        if (oflag & FWRITE) {
                fmt = auclnt_get_format(auclnt_output_stream(c));
        } else if (oflag & FREAD) {
                fmt = auclnt_get_format(auclnt_input_stream(c));
        }

        /* convert back to OSS */
        *(int *)fmtp = AFMT_QUERY;
        for (i = 0; oss_formats[i].fmt != AUDIO_FORMAT_NONE; i++) {
                if (oss_formats[i].fmt == fmt) {
                        *(int *)fmtp = oss_formats[i].oss;
                }
        }

        return (0);
}

static int
sndctl_dsp_getfmts(audio_client_t *c, int *fmtsp)
{
        _NOTE(ARGUNUSED(c));

        /*
         * For now, we support all the standard ones.  Later we might
         * add in conditional support for AC3.
         */
        *fmtsp = (AFMT_MU_LAW | AFMT_A_LAW |
            AFMT_U8 | AFMT_S8 |
            AFMT_S16_LE |AFMT_S16_BE |
            AFMT_S24_LE | AFMT_S24_BE |
            AFMT_S32_LE | AFMT_S32_BE |
            AFMT_S24_PACKED);

        return (0);
}

static int
sndctl_dsp_channels(audio_client_t *c, int *chanp)
{
        int             nchan;
        int             oflag;

        oflag = auclnt_get_oflag(c);

        nchan = *chanp;
        if (nchan != 0) {
                if (oflag & FWRITE) {
                        (void) auclnt_set_channels(auclnt_output_stream(c),
                            nchan);
                }

                if (oflag & FREAD) {
                        (void) auclnt_set_channels(auclnt_input_stream(c),
                            nchan);
                }
        }

        if (oflag & FWRITE) {
                nchan = auclnt_get_channels(auclnt_output_stream(c));
        } else if (oflag & FREAD) {
                nchan = auclnt_get_channels(auclnt_input_stream(c));
        }
        *chanp = nchan;
        return (0);
}

static int
sndctl_dsp_stereo(audio_client_t *c, int *onoff)
{
        int     nchan;

        switch (*onoff) {
        case 0:
                nchan = 1;
                break;
        case 1:
                nchan = 2;
                break;
        default:
                return (EINVAL);
        }

        return (sndctl_dsp_channels(c, &nchan));
}

static int
sndctl_dsp_post(audio_client_t *c)
{
        if (auclnt_get_oflag(c) & FWRITE) {
                audio_stream_t  *sp = auclnt_output_stream(c);
                auclnt_flush(sp);
                auclnt_clear_paused(sp);
        }
        return (0);
}

static int
sndctl_dsp_getcaps(audio_client_t *c, int *capsp)
{
        int             ncaps;
        int             osscaps = 0;

        ncaps = auclnt_get_dev_capab(auclnt_get_dev(c));

        if (ncaps & AUDIO_CLIENT_CAP_PLAY)
                osscaps |= PCM_CAP_OUTPUT;
        if (ncaps & AUDIO_CLIENT_CAP_RECORD)
                osscaps |= PCM_CAP_INPUT;
        if (ncaps & AUDIO_CLIENT_CAP_DUPLEX)
                osscaps |= PCM_CAP_DUPLEX;

        if (osscaps != 0) {
                osscaps |= PCM_CAP_TRIGGER | PCM_CAP_BATCH;
                if (!(ncaps & AUDIO_CLIENT_CAP_OPAQUE)) {
                        osscaps |= PCM_CAP_FREERATE | PCM_CAP_MULTI;
                }
        } else {
                /* This is the sndstat device! */
                osscaps = PCM_CAP_VIRTUAL;
        }

        *capsp = osscaps;
        return (0);
}

static int
sndctl_dsp_gettrigger(audio_client_t *c, int *trigp)
{
        int             triggers = 0;
        int             oflag;

        oflag = auclnt_get_oflag(c);

        if (oflag & FWRITE) {
                if (!auclnt_is_paused(auclnt_output_stream(c))) {
                        triggers |= PCM_ENABLE_OUTPUT;
                }
        }

        if (oflag & FREAD) {
                if (!auclnt_is_paused(auclnt_input_stream(c))) {
                        triggers |= PCM_ENABLE_INPUT;
                }
        }
        *trigp = triggers;

        return (0);
}

static int
sndctl_dsp_settrigger(audio_client_t *c, int *trigp)
{
        int             triggers;
        int             oflag;
        audio_stream_t  *sp;

        oflag = auclnt_get_oflag(c);
        triggers = *trigp;

        if ((oflag & FWRITE) && (triggers & PCM_ENABLE_OUTPUT)) {
                sp = auclnt_output_stream(c);
                auclnt_clear_paused(sp);
                auclnt_start(sp);
        }

        if ((oflag & FREAD) && (triggers & PCM_ENABLE_INPUT)) {
                sp = auclnt_input_stream(c);
                auclnt_clear_paused(sp);
                auclnt_start(sp);
        }

        return (0);
}

struct oss_legacy_volume {
        pid_t           pid;
        uint8_t         ogain;
        uint8_t         igain;
};

static int
oss_legacy_volume_walker(audio_client_t *c, void *arg)
{
        struct oss_legacy_volume         *olv = arg;

        if (auclnt_get_pid(c) == olv->pid) {
                if (olv->ogain <= 100) {
                        auclnt_set_gain(auclnt_output_stream(c), olv->ogain);
                }
                if (olv->igain <= 100) {
                        auclnt_set_gain(auclnt_input_stream(c), olv->igain);
                }
        }
        return (AUDIO_WALK_CONTINUE);
}

static void
oss_set_legacy_volume(audio_client_t *c, uint8_t ogain, uint8_t igain)
{
        struct oss_legacy_volume olv;

        olv.pid = auclnt_get_pid(c);
        olv.ogain = ogain;
        olv.igain = igain;
        auclnt_dev_walk_clients(auclnt_get_dev(c),
            oss_legacy_volume_walker, &olv);
}

static int
sndctl_dsp_getplayvol(audio_client_t *c, int *volp)
{
        int     vol;

        /* convert monophonic soft value to OSS stereo value */
        vol = auclnt_get_gain(auclnt_output_stream(c));
        *volp = vol | (vol << 8);
        return (0);
}

static int
sndctl_dsp_setplayvol(audio_client_t *c, int *volp)
{
        uint8_t         vol;

        vol = *volp & 0xff;
        if (vol > 100) {
                return (EINVAL);
        }

        auclnt_set_gain(auclnt_output_stream(c), vol);
        *volp = (vol | (vol << 8));

        return (0);
}

static int
sndctl_dsp_getrecvol(audio_client_t *c, int *volp)
{
        int     vol;

        vol = auclnt_get_gain(auclnt_input_stream(c));
        *volp = (vol | (vol << 8));
        return (0);
}

static int
sndctl_dsp_setrecvol(audio_client_t *c, int *volp)
{
        uint8_t         vol;

        vol = *volp & 0xff;
        if (vol > 100) {
                return (EINVAL);
        }

        auclnt_set_gain(auclnt_input_stream(c), vol);
        *volp = (vol | (vol << 8));

        return (0);
}

static int
sound_mixer_write_ogain(audio_client_t *c, int *volp)
{
        uint8_t         vol;

        vol = *volp & 0xff;
        if (vol > 100) {
                return (EINVAL);
        }
        oss_set_legacy_volume(c, vol, 255);
        *volp = (vol | (vol << 8));
        return (0);
}

static int
sound_mixer_write_igain(audio_client_t *c, int *volp)
{
        uint8_t         vol;

        vol = *volp & 0xff;
        if (vol > 100) {
                return (EINVAL);
        }
        oss_set_legacy_volume(c, 255, vol);
        *volp = (vol | (vol << 8));
        return (0);
}

static int
sndctl_dsp_readctl(audio_client_t *c, oss_digital_control *ctl)
{
        /* SPDIF: need to add support with spdif */
        _NOTE(ARGUNUSED(c));
        _NOTE(ARGUNUSED(ctl));
        return (ENOTSUP);
}

static int
sndctl_dsp_writectl(audio_client_t *c, oss_digital_control *ctl)
{
        /* SPDIF: need to add support with spdif */
        _NOTE(ARGUNUSED(c));
        _NOTE(ARGUNUSED(ctl));
        return (ENOTSUP);
}

static int
sndctl_dsp_cookedmode(audio_client_t *c, int *rvp)
{
        _NOTE(ARGUNUSED(c));

        /* We are *always* in cooked mode -- at least until we have AC3. */
        if (*rvp == 0) {
                return (ENOTSUP);
        } else {
                return (0);
        }
}

static int
sndctl_dsp_silence(audio_client_t *c)
{
        if (auclnt_get_oflag(c) & FWRITE) {
                audio_stream_t  *sp = auclnt_output_stream(c);
                auclnt_set_paused(sp);
                auclnt_flush(sp);
        }
        return (0);
}

static int
sndctl_dsp_skip(audio_client_t *c)
{
        if (auclnt_get_oflag(c) & FWRITE) {
                audio_stream_t  *sp = auclnt_output_stream(c);
                auclnt_set_paused(sp);
                auclnt_flush(sp);
                auclnt_clear_paused(sp);
        }
        return (0);
}

static int
sndctl_dsp_halt_input(audio_client_t *c)
{
        if (auclnt_get_oflag(c) & FREAD) {
                audio_stream_t  *sp = auclnt_input_stream(c);
                auclnt_set_paused(sp);
                auclnt_flush(sp);
        }
        return (0);
}

static int
sndctl_dsp_halt_output(audio_client_t *c)
{
        if (auclnt_get_oflag(c) & FWRITE) {
                audio_stream_t  *sp = auclnt_output_stream(c);
                auclnt_set_paused(sp);
                auclnt_flush(sp);
        }
        return (0);
}

static int
sndctl_dsp_halt(audio_client_t *c)
{
        (void) sndctl_dsp_halt_input(c);
        (void) sndctl_dsp_halt_output(c);
        return (0);
}

static int
sndctl_dsp_sync(audio_client_t *c)
{
        return (auclnt_drain(c));
}

static int
sndctl_dsp_setfragment(audio_client_t *c, int *fragp)
{
        int     bufsz;
        int     nfrags;
        int     fragsz;

        nfrags = (*fragp) >> 16;
        if ((nfrags >= 0x7fffU) || (nfrags < 2)) {
                /* use infinite setting... no change */
                return (0);
        }

        fragsz = (*fragp) & 0xffff;
        if (fragsz > 16) {
                /* basically too big, so, no change */
                return (0);
        }
        bufsz = (1U << fragsz) * nfrags;

        /*
         * Now we have our desired buffer size, but we have to
         * make sure we have a whole number of fragments >= 2, and
         * less than the maximum.
         */
        bufsz = ((*fragp) >> 16) * (1U << (*fragp));
        if (bufsz >= 65536) {
                return (0);
        }

        /*
         * We set the latency hints in terms of bytes, not fragments.
         */
        auclnt_set_latency(auclnt_output_stream(c), 0, bufsz);
        auclnt_set_latency(auclnt_input_stream(c), 0, bufsz);

        /*
         * According to the OSS API documentation, the values provided
         * are nothing more than a "hint" and not to be relied upon
         * anyway.  And we aren't obligated to report the actual
         * values back!
         */
        return (0);
}

static int
sndctl_dsp_policy(audio_client_t *c, int *policy)
{
        int     hint = *policy;
        if ((hint >= 2) && (hint <= 10)) {
                auclnt_set_latency(auclnt_input_stream(c), hint, 0);
                auclnt_set_latency(auclnt_output_stream(c), hint, 0);
        }
        return (0);
}

/*
 * A word about recsrc, and playtgt ioctls: We don't allow ordinary DSP
 * applications to change port configurations, because these could have a
 * bad effect for other applications.  Instead, these settings have to
 * be changed using the master mixer panel.  In order to make applications
 * happy, we just present a single "default" source/target.
 */
static int
sndctl_dsp_get_recsrc_names(audio_client_t *c, oss_mixer_enuminfo *ei)
{
        _NOTE(ARGUNUSED(c));

        ei->nvalues = 1;
        (void) snprintf(ei->strings, sizeof (ei->strings), "default");
        ei->strindex[0] = 0;

        return (0);
}

static int
sndctl_dsp_get_recsrc(audio_client_t *c, int *srcp)
{
        _NOTE(ARGUNUSED(c));
        *srcp = 0;
        return (0);
}

static int
sndctl_dsp_set_recsrc(audio_client_t *c, int *srcp)
{
        _NOTE(ARGUNUSED(c));
        *srcp = 0;
        return (0);
}

static int
sndctl_dsp_get_playtgt_names(audio_client_t *c, oss_mixer_enuminfo *ei)
{
        _NOTE(ARGUNUSED(c));

        ei->nvalues = 1;
        (void) snprintf(ei->strings, sizeof (ei->strings), "default");
        ei->strindex[0] = 0;

        return (0);
}

static int
sndctl_dsp_get_playtgt(audio_client_t *c, int *tgtp)
{
        _NOTE(ARGUNUSED(c));
        *tgtp = 0;
        return (0);
}

static int
sndctl_dsp_set_playtgt(audio_client_t *c, int *tgtp)
{
        _NOTE(ARGUNUSED(c));
        *tgtp = 0;
        return (0);
}

static int
sndctl_sysinfo(oss_sysinfo *si)
{
        bzero(si, sizeof (*si));
        (void) snprintf(si->product, sizeof (si->product), "SunOS Audio");
        (void) snprintf(si->version, sizeof (si->version), "4.0");
        si->versionnum = OSS_VERSION;
        si->numcards = oss_cnt_devs();
        si->nummixers = si->numcards - 1;
        si->numaudios = si->numcards - 1;
        si->numaudioengines = si->numaudios;
        (void) snprintf(si->license, sizeof (si->license), "CDDL");
        return (0);
}

static int
sndctl_cardinfo(audio_client_t *c, oss_card_info *ci)
{
        audio_dev_t     *d;
        void            *iter;
        const char      *info;
        int             n;
        boolean_t       release;

        if ((n = ci->card) == -1) {
                release = B_FALSE;
                d = auclnt_get_dev(c);
                n = auclnt_get_dev_index(d);
        } else {
                release = B_TRUE;
                d = auclnt_hold_dev_by_index(n);
        }

        bzero(ci, sizeof (*ci));
        ci->card = n;

        if (d == NULL) {
                /*
                 * If device removed (e.g. for DR), then
                 * report a bogus removed entry.
                 */
                (void) snprintf(ci->shortname, sizeof (ci->shortname),
                    "<removed>");
                (void) snprintf(ci->longname, sizeof (ci->longname),
                    "<removed>");
                return (0);
        }

        (void) snprintf(ci->shortname, sizeof (ci->shortname),
            "%s", auclnt_get_dev_name(d));
        (void) snprintf(ci->longname, sizeof (ci->longname),
            "%s (%s)", auclnt_get_dev_description(d),
            auclnt_get_dev_version(d));

        iter = NULL;
        while ((info = auclnt_get_dev_hw_info(d, &iter)) != NULL) {
                (void) strlcat(ci->hw_info, info, sizeof (ci->hw_info));
                (void) strlcat(ci->hw_info, "\n", sizeof (ci->hw_info));
        }

        /*
         * We don't report interrupt counts, ack counts (which are
         * just "read" interrupts, not spurious), or any other flags.
         * Nothing should be using any of this data anyway ... these
         * values were intended for 4Front's debugging purposes.  In
         * Solaris, drivers should use interrupt kstats to report
         * interrupt related statistics.
         */
        if (release)
                auclnt_release_dev(d);
        return (0);
}

static int
audioinfo_walker(audio_engine_t *e, void *a)
{
        oss_audioinfo *si = a;
        int fmt, nchan, rate, cap;

        fmt = auclnt_engine_get_format(e);
        nchan = auclnt_engine_get_channels(e);
        rate = auclnt_engine_get_rate(e);
        cap = auclnt_engine_get_capab(e);

        for (int i = 0; oss_formats[i].fmt != AUDIO_FORMAT_NONE; i++) {
                if (fmt == oss_formats[i].fmt) {
                        if (cap & AUDIO_CLIENT_CAP_PLAY) {
                                si->oformats |= oss_formats[i].oss;
                        }
                        if (cap & AUDIO_CLIENT_CAP_RECORD) {
                                si->iformats |= oss_formats[i].oss;
                        }
                        break;
                }
        }
        si->max_channels = max(nchan, si->max_channels);
        si->max_rate = max(rate, si->max_rate);

        return (AUDIO_WALK_CONTINUE);
}

static int
sndctl_audioinfo(audio_client_t *c, oss_audioinfo *si)
{
        audio_dev_t             *d;
        const char              *name;
        int                     n;
        boolean_t               release;
        unsigned                cap;

        if ((n = si->dev) == -1) {
                release = B_FALSE;
                d = auclnt_get_dev(c);
                n = auclnt_get_dev_index(d);
        } else {
                release = B_TRUE;
                n++;    /* skip pseudo device */
                d = auclnt_hold_dev_by_index(n);
        }

        bzero(si, sizeof (*si));
        si->dev = n - 1;

        if (d == NULL) {
                /* if device not present, forge a false entry */
                si->card_number = n;
                si->mixer_dev = n - 1;
                si->legacy_device = -1;
                si->enabled = 0;
                (void) snprintf(si->name, sizeof (si->name), "<removed>");
                return (0);
        }

        name = auclnt_get_dev_name(d);
        (void) snprintf(si->name, sizeof (si->name), "%s", name);

        si->legacy_device = auclnt_get_dev_number(d);
        si->caps = 0;

        auclnt_dev_walk_engines(d, audioinfo_walker, si);

        cap = auclnt_get_dev_capab(d);

        if (cap & AUDIO_CLIENT_CAP_DUPLEX) {
                si->caps |= PCM_CAP_DUPLEX;
        }
        if (cap & AUDIO_CLIENT_CAP_PLAY) {
                si->caps |= PCM_CAP_OUTPUT;
        }
        if (cap & AUDIO_CLIENT_CAP_RECORD) {
                si->caps |= PCM_CAP_INPUT;
        }

        if (si->caps != 0) {
                /* MMAP: we add PCM_CAP_MMAP when we we support it */
                si->caps |= PCM_CAP_TRIGGER | PCM_CAP_BATCH;
                si->enabled = 1;
                si->rate_source = si->dev;

                /* we can convert and mix PCM formats */
                if (!(cap & AUDIO_CLIENT_CAP_OPAQUE)) {
                        si->min_channels = min(2, si->max_channels);
                        si->min_rate = min(5000, si->max_rate);
                        si->caps |= PCM_CAP_FREERATE | PCM_CAP_MULTI;
                }
                (void) snprintf(si->devnode, sizeof (si->devnode),
                    "/dev/sound/%s:%ddsp",
                    auclnt_get_dev_driver(d), auclnt_get_dev_instance(d));
        } else {
                si->enabled = 0;        /* stops apps from using us directly */
                si->caps = PCM_CAP_VIRTUAL;
                (void) snprintf(si->devnode, sizeof (si->devnode),
                    "/dev/sndstat");
        }

        si->pid = -1;
        (void) snprintf(si->handle, sizeof (si->handle), "%s", name);
        (void) snprintf(si->label, sizeof (si->label), "%s", name);
        si->latency = -1;
        si->card_number = n;
        si->mixer_dev = n - 1;

        if (release)
                auclnt_release_dev(d);

        return (0);
}

static int
sound_mixer_info(audio_client_t *c, mixer_info *mi)
{
        audio_dev_t     *d;
        const char      *name;

        d = auclnt_get_dev(c);

        name = auclnt_get_dev_name(d);
        (void) snprintf(mi->id, sizeof (mi->id), "%s", name);
        (void) snprintf(mi->name, sizeof (mi->name), "%s", name);
        (void) snprintf(mi->handle, sizeof (mi->handle), "%s", name);
        mi->modify_counter = (int)auclnt_dev_get_serial(d);
        mi->card_number = auclnt_get_dev_index(d);
        mi->port_number = 0;
        return (0);
}

static int
sound_mixer_read_devmask(audio_client_t *c, int *devmask)
{
        _NOTE(ARGUNUSED(c));
        *devmask = SOUND_MASK_VOLUME | SOUND_MASK_PCM | SOUND_MASK_IGAIN;
        return (0);
}

static int
sound_mixer_read_recmask(audio_client_t *c, int *recmask)
{
        _NOTE(ARGUNUSED(c));
        *recmask = 0;
        return (0);
}

static int
sound_mixer_read_recsrc(audio_client_t *c, int *recsrc)
{
        _NOTE(ARGUNUSED(c));
        *recsrc = 0;
        return (0);
}

static int
sound_mixer_read_caps(audio_client_t *c, int *caps)
{
        _NOTE(ARGUNUSED(c));
        /* single recording source... sort of */
        *caps = SOUND_CAP_EXCL_INPUT;
        return (0);
}

static int
sndctl_mixerinfo(audio_client_t *c, oss_mixerinfo *mi)
{
        audio_dev_t             *d;
        ossdev_t                *odev;
        const char              *name;
        int                     n;
        boolean_t               release = B_FALSE;

        if ((n = mi->dev) == -1) {
                release = B_FALSE;
                d = auclnt_get_dev(c);
                n = auclnt_get_dev_index(d);
        } else {
                release = B_TRUE;
                n++;
                d = auclnt_hold_dev_by_index(n);
        }

        bzero(mi, sizeof (*mi));
        mi->dev = n - 1;

        if (d == NULL) {
                mi->card_number = n;
                mi->enabled = 0;
                mi->legacy_device = -1;
                (void) snprintf(mi->name, sizeof (mi->name), "<removed>");
                (void) snprintf(mi->id, sizeof (mi->id), "<removed>");
                return (0);
        }

        if ((odev = auclnt_get_dev_minor_data(d, AUDIO_MINOR_DSP)) == NULL) {
                if (release)
                        auclnt_release_dev(d);
                return (EINVAL);
        }

        name = auclnt_get_dev_name(d);
        (void) snprintf(mi->name, sizeof (mi->name), "%s", name);
        (void) snprintf(mi->id, sizeof (mi->id), "%s", name);
        (void) snprintf(mi->handle, sizeof (mi->handle), "%s", name);
        mi->modify_counter = (int)auclnt_dev_get_serial(d);
        mi->card_number = auclnt_get_dev_index(d);
        mi->legacy_device = auclnt_get_dev_number(d);
        if (mi->legacy_device >= 0) {
                (void) snprintf(mi->devnode, sizeof (mi->devnode),
                    "/dev/sound/%s:%dmixer",
                    auclnt_get_dev_driver(d), auclnt_get_dev_instance(d));
                mi->enabled = 1;
        } else {
                /* special nodes use generic sndstat node */
                (void) snprintf(mi->devnode, sizeof (mi->devnode),
                    "/dev/sndstat");
                mi->enabled = 0;
        }
        mi->nrext = odev->d_nctrl;

        if (release)
                auclnt_release_dev(d);

        return (0);
}

static int
sndctl_dsp_getblksize(audio_client_t *c, int *fragsz)
{
        int     oflag = auclnt_get_oflag(c);

        if (oflag & FWRITE)
                *fragsz  = auclnt_get_fragsz(auclnt_output_stream(c));
        else if (oflag & FREAD)
                *fragsz  = auclnt_get_fragsz(auclnt_input_stream(c));

        return (0);
}

static int
sndctl_dsp_getospace(audio_client_t *c, audio_buf_info *bi)
{
        audio_stream_t  *sp;
        unsigned        n;

        if ((auclnt_get_oflag(c) & FWRITE) == 0) {
                return (EACCES);
        }

        sp = auclnt_output_stream(c);
        n = auclnt_get_nframes(sp) - auclnt_get_count(sp);

        bi->fragsize  = auclnt_get_fragsz(sp);
        bi->fragstotal = auclnt_get_nfrags(sp);
        bi->bytes = (n * auclnt_get_framesz(sp));
        bi->fragments = bi->bytes / bi->fragsize;

        return (0);
}

static int
sndctl_dsp_getispace(audio_client_t *c, audio_buf_info *bi)
{
        audio_stream_t  *sp;
        unsigned        n;

        if ((auclnt_get_oflag(c) & FREAD) == 0) {
                return (EACCES);
        }

        sp = auclnt_input_stream(c);
        n = auclnt_get_count(sp);

        bi->fragsize  = auclnt_get_fragsz(sp);
        bi->fragstotal = auclnt_get_nfrags(sp);
        bi->bytes = (n * auclnt_get_framesz(sp));
        bi->fragments = bi->bytes / bi->fragsize;

        return (0);
}

static int
sndctl_dsp_getodelay(audio_client_t *c, int *bytes)
{
        unsigned        framesz;
        unsigned        slen, flen;

        if (auclnt_get_oflag(c) & FWRITE) {
                audio_stream_t  *sp = auclnt_output_stream(c);
                framesz = auclnt_get_framesz(sp);
                auclnt_get_output_qlen(c, &slen, &flen);
                *bytes = (slen + flen) * framesz;
        } else {
                *bytes = 0;
        }
        return (0);
}

static int
sndctl_dsp_current_iptr(audio_client_t *c, oss_count_t *count)
{
        if (auclnt_get_oflag(c) & FREAD) {
                count->samples = auclnt_get_samples(auclnt_input_stream(c));
                count->fifo_samples = 0;        /* not quite accurate */
        } else {
                count->samples = 0;
                count->fifo_samples = 0;
        }
        return (0);
}

static int
sndctl_dsp_current_optr(audio_client_t *c, oss_count_t *count)
{
        unsigned samples, fifo;

        if (auclnt_get_oflag(c) & FWRITE) {
                auclnt_get_output_qlen(c, &samples, &fifo);
                count->samples = samples;
                count->fifo_samples = fifo;
        } else {
                count->samples = 0;
                count->fifo_samples = 0;
        }
        return (0);
}

static int
sndctl_dsp_getoptr(audio_client_t *c, count_info *ci)
{
        audio_stream_t  *sp;
        unsigned        framesz;
        unsigned        fragsz;

        bzero(ci, sizeof (*ci));
        if ((auclnt_get_oflag(c) & FWRITE) == 0) {
                return (0);
        }
        sp = auclnt_output_stream(c);
        framesz = auclnt_get_framesz(sp);
        fragsz = auclnt_get_fragsz(sp);
        ci->blocks = auclnt_get_samples(sp) * framesz / fragsz;
        auclnt_set_samples(sp, 0);
        ci->bytes = auclnt_get_tail(sp) * framesz;
        ci->ptr = auclnt_get_tidx(sp) * framesz;
        return (0);
}

static int
sndctl_dsp_getiptr(audio_client_t *c, count_info *ci)
{
        audio_stream_t  *sp;
        unsigned        framesz;
        unsigned        fragsz;

        bzero(ci, sizeof (*ci));
        if ((auclnt_get_oflag(c) & FREAD) == 0) {
                return (0);
        }
        sp = auclnt_input_stream(c);
        framesz = auclnt_get_framesz(sp);
        fragsz = auclnt_get_fragsz(sp);
        ci->blocks = auclnt_get_samples(sp) * framesz / fragsz;
        auclnt_set_samples(sp, 0);
        ci->bytes = auclnt_get_head(sp) * framesz;
        ci->ptr = auclnt_get_hidx(sp) * framesz;
        return (0);
}

static int
sndctl_dsp_geterror(audio_client_t *c, audio_errinfo *bi)
{
        audio_stream_t  *sp;
        unsigned        fragsz;
        /*
         * Note: The use of this structure is unsafe... different
         * meanings for error codes are used by different implementations,
         * according to the spec.  (Even different versions of the same
         * implementation could have different values.)
         *
         * Rather than try to come up with a reliable solution here, we
         * don't use it.  If you want to report errors, or see the result
         * of errors, use syslog.
         */
        bzero(bi, sizeof (*bi));

        sp = auclnt_output_stream(c);
        fragsz = max(auclnt_get_fragsz(sp), 1);
        bi->play_underruns = (int)((auclnt_get_errors(sp) + (fragsz - 1)) /
            fragsz);
        auclnt_set_errors(sp, 0);

        sp = auclnt_input_stream(c);
        fragsz = max(auclnt_get_fragsz(sp), 1);
        bi->rec_overruns = (int)((auclnt_get_errors(sp) + (fragsz - 1)) /
            fragsz);
        auclnt_set_errors(sp, 0);

        return (0);
}

static int
sndctl_sun_send_number(audio_client_t *c, int *num, cred_t *cr)
{
        audio_dev_t     *dev;
        int             rv;

        if ((rv = drv_priv(cr)) != 0) {
                return (rv);
        }

        dev = auclnt_get_dev(c);
        auclnt_set_dev_number(dev, *num);
        return (0);
}

static int
oss_getversion(int *versp)
{
        *versp = OSS_VERSION;
        return (0);
}

static int
oss_ioctl(audio_client_t *c, int cmd, intptr_t arg, int mode, cred_t *credp,
    int *rvalp)
{
        int     sz;
        void    *data;
        int     rv = 0;

        _NOTE(ARGUNUSED(credp));

        sz = OSSIOC_GETSZ(cmd);

        if ((cmd & (OSSIOC_IN | OSSIOC_OUT)) && sz) {
                if ((data = kmem_zalloc(sz, KM_NOSLEEP)) == NULL) {
                        return (ENOMEM);
                }
        } else {
                sz = 0;
        }

        if (cmd & OSSIOC_IN) {
                if ((rv = ddi_copyin((void *)arg, data, sz, mode)) != 0) {
                        goto done;
                }
        }

        switch (cmd) {
                /*
                 * DSP specific ioctls
                 */
        case SNDCTL_DSP_HALT:
                rv = sndctl_dsp_halt(c);
                break;

        case SNDCTL_DSP_SYNC:
                rv = sndctl_dsp_sync(c);
                break;

        case SNDCTL_DSP_SPEED:
                rv = sndctl_dsp_speed(c, (int *)data);
                break;
        case SNDCTL_DSP_SETFMT:
                rv = sndctl_dsp_setfmt(c, (int *)data);
                break;
        case SNDCTL_DSP_GETFMTS:
                rv = sndctl_dsp_getfmts(c, (int *)data);
                break;
        case SNDCTL_DSP_STEREO:
                rv = sndctl_dsp_stereo(c, (int *)data);
                break;
        case SNDCTL_DSP_CHANNELS:
                rv = sndctl_dsp_channels(c, (int *)data);
                break;
        case SNDCTL_DSP_POST:
                rv = sndctl_dsp_post(c);
                break;
        case SNDCTL_DSP_GETCAPS:
                rv = sndctl_dsp_getcaps(c, (int *)data);
                break;
        case SNDCTL_DSP_GETTRIGGER:
                rv = sndctl_dsp_gettrigger(c, (int *)data);
                break;
        case SNDCTL_DSP_SETTRIGGER:
                rv = sndctl_dsp_settrigger(c, (int *)data);
                break;
        case SNDCTL_DSP_GETPLAYVOL:
        case SOUND_MIXER_READ_VOLUME:   /* legacy mixer on dsp */
        case SOUND_MIXER_READ_PCM:      /* legacy mixer on dsp */
        case SOUND_MIXER_READ_OGAIN:    /* legacy mixer on dsp */
                rv = sndctl_dsp_getplayvol(c, (int *)data);
                break;
        case SOUND_MIXER_WRITE_VOLUME:  /* legacy mixer on dsp */
        case SOUND_MIXER_WRITE_PCM:     /* legacy mixer on dsp */
        case SOUND_MIXER_WRITE_OGAIN:   /* legacy mixer on dsp */
                rv = sound_mixer_write_ogain(c, (int *)data);
                break;
        case SNDCTL_DSP_SETPLAYVOL:
                rv = sndctl_dsp_setplayvol(c, (int *)data);
                break;
        case SNDCTL_DSP_READCTL:
                rv = sndctl_dsp_readctl(c, (oss_digital_control *)data);
                break;
        case SNDCTL_DSP_WRITECTL:
                rv = sndctl_dsp_writectl(c, (oss_digital_control *)data);
                break;
        case SNDCTL_DSP_COOKEDMODE:
                rv = sndctl_dsp_cookedmode(c, (int *)data);
                break;
        case SNDCTL_DSP_SILENCE:
                rv = sndctl_dsp_silence(c);
                break;
        case SNDCTL_DSP_SKIP:
                rv = sndctl_dsp_skip(c);
                break;
        case SNDCTL_DSP_HALT_INPUT:
                rv = sndctl_dsp_halt_input(c);
                break;
        case SNDCTL_DSP_HALT_OUTPUT:
                rv = sndctl_dsp_halt_output(c);
                break;
        case SNDCTL_DSP_GET_RECSRC_NAMES:
                rv = sndctl_dsp_get_recsrc_names(c, (oss_mixer_enuminfo *)data);
                break;
        case SNDCTL_DSP_SETFRAGMENT:
                rv = sndctl_dsp_setfragment(c, (int *)data);
                break;
        case SNDCTL_DSP_GET_RECSRC:
                rv = sndctl_dsp_get_recsrc(c, (int *)data);
                break;
        case SNDCTL_DSP_SET_RECSRC:
                rv = sndctl_dsp_set_recsrc(c, (int *)data);
                break;
        case SNDCTL_DSP_GET_PLAYTGT_NAMES:
                rv = sndctl_dsp_get_playtgt_names(c,
                    (oss_mixer_enuminfo *)data);
                break;
        case SNDCTL_DSP_GET_PLAYTGT:
                rv = sndctl_dsp_get_playtgt(c, (int *)data);
                break;
        case SNDCTL_DSP_SET_PLAYTGT:
                rv = sndctl_dsp_set_playtgt(c, (int *)data);
                break;
        case SNDCTL_DSP_GETRECVOL:
        case SOUND_MIXER_READ_RECGAIN:  /* legacy mixer on dsp */
        case SOUND_MIXER_READ_RECLEV:   /* legacy mixer on dsp */
        case SOUND_MIXER_READ_IGAIN:    /* legacy mixer on dsp */
                rv = sndctl_dsp_getrecvol(c, (int *)data);
                break;
        case SOUND_MIXER_WRITE_RECGAIN: /* legacy mixer on dsp */
        case SOUND_MIXER_WRITE_RECLEV:  /* legacy mixer on dsp */
        case SOUND_MIXER_WRITE_IGAIN:   /* legacy mixer on dsp */
                rv = sound_mixer_write_igain(c, (int *)data);
                break;
        case SNDCTL_DSP_SETRECVOL:
                rv = sndctl_dsp_setrecvol(c, (int *)data);
                break;
        case SNDCTL_DSP_SUBDIVIDE:      /* Ignored */
        case SNDCTL_DSP_SETDUPLEX:      /* Ignored */
        case SNDCTL_DSP_LOW_WATER:      /* Ignored */
        case SNDCTL_DSP_PROFILE:        /* Ignored */
                rv = 0;
                break;
        case SNDCTL_DSP_POLICY:
                rv = sndctl_dsp_policy(c, (int *)data);
                break;
        case SNDCTL_DSP_GETBLKSIZE:
                rv = sndctl_dsp_getblksize(c, (int *)data);
                break;
        case SNDCTL_DSP_GETOSPACE:
                rv = sndctl_dsp_getospace(c, (audio_buf_info *)data);
                break;
        case SNDCTL_DSP_GETISPACE:
                rv = sndctl_dsp_getispace(c, (audio_buf_info *)data);
                break;
        case SNDCTL_DSP_GETODELAY:
                rv = sndctl_dsp_getodelay(c, (int *)data);
                break;
        case SNDCTL_DSP_GETOPTR:
                rv = sndctl_dsp_getoptr(c, (count_info *)data);
                break;
        case SNDCTL_DSP_GETIPTR:
                rv = sndctl_dsp_getiptr(c, (count_info *)data);
                break;
        case SNDCTL_DSP_GETERROR:
                rv = sndctl_dsp_geterror(c, (audio_errinfo *)data);
                break;
        case SNDCTL_DSP_CURRENT_IPTR:
                rv = sndctl_dsp_current_iptr(c, (oss_count_t *)data);
                break;
        case SNDCTL_DSP_CURRENT_OPTR:
                rv = sndctl_dsp_current_optr(c, (oss_count_t *)data);
                break;

                /*
                 * Shared ioctls with /dev/mixer.
                 */
        case OSS_GETVERSION:
                rv = oss_getversion((int *)data);
                break;
        case SNDCTL_CARDINFO:
                rv = sndctl_cardinfo(c, (oss_card_info *)data);
                break;
        case SNDCTL_ENGINEINFO:
        case SNDCTL_AUDIOINFO:
        case SNDCTL_AUDIOINFO_EX:
                rv = sndctl_audioinfo(c, (oss_audioinfo *)data);
                break;
        case SNDCTL_SYSINFO:
                rv = sndctl_sysinfo((oss_sysinfo *)data);
                break;
        case SNDCTL_MIXERINFO:
                rv = sndctl_mixerinfo(c, (oss_mixerinfo *)data);
                break;
        case SOUND_MIXER_INFO:
                rv = sound_mixer_info(c, (mixer_info *)data);
                break;

                /*
                 * These are mixer ioctls that are virtualized for the DSP
                 * device.  They are accessible via either /dev/mixer or
                 * /dev/dsp.
                 */
        case SOUND_MIXER_READ_RECSRC:
        case SOUND_MIXER_WRITE_RECSRC:
                rv = sound_mixer_read_recsrc(c, (int *)data);
                break;

        case SOUND_MIXER_READ_DEVMASK:
        case SOUND_MIXER_READ_STEREODEVS:
                rv = sound_mixer_read_devmask(c, (int *)data);
                break;

        case SOUND_MIXER_READ_RECMASK:
                rv = sound_mixer_read_recmask(c, (int *)data);
                break;

        case SOUND_MIXER_READ_CAPS:
                rv = sound_mixer_read_caps(c, (int *)data);
                break;

                /*
                 * Ioctls we have chosen not to support for now.  Some
                 * of these are of legacy interest only.
                 */
        case SNDCTL_SETSONG:
        case SNDCTL_GETSONG:
        case SNDCTL_DSP_SYNCGROUP:
        case SNDCTL_DSP_SYNCSTART:
        case SNDCTL_DSP_GET_CHNORDER:
        case SNDCTL_DSP_SET_CHNORDER:
        case SNDCTL_DSP_GETIPEAKS:
        case SNDCTL_DSP_GETOPEAKS:
        case SNDCTL_DSP_GETCHANNELMASK:
        case SNDCTL_DSP_BIND_CHANNEL:
        case SNDCTL_DSP_SETSYNCRO:
        case SNDCTL_DSP_NONBLOCK:
        default:
                rv = EINVAL;
                break;
        }

        if ((rv == 0) && (cmd & OSSIOC_OUT)) {
                rv = ddi_copyout(data, (void *)arg, sz, mode);
        }
        if (rv == 0) {
                *rvalp = 0;
        }

done:
        if (sz) {
                kmem_free(data, sz);
        }
        return (rv);
}

static void
oss_output(audio_client_t *c)
{
        auclnt_pollwakeup(c, POLLOUT);
}

static void
oss_input(audio_client_t *c)
{
        auclnt_pollwakeup(c, POLLIN | POLLRDNORM);
}

static int
ossmix_open(audio_client_t *c, int oflag)
{
        int             rv;
        ossclient_t     *sc;
        ossdev_t        *odev;

        _NOTE(ARGUNUSED(oflag));

        if ((rv = auclnt_open(c, 0)) != 0) {
                return (rv);
        }

        if ((sc = kmem_zalloc(sizeof (*sc), KM_NOSLEEP)) == NULL) {
                return (ENOMEM);
        }
        sc->o_ss_sz = 8192;
        if ((sc->o_ss_buf = kmem_zalloc(sc->o_ss_sz, KM_NOSLEEP)) == NULL) {
                kmem_free(sc, sizeof (*sc));
                return (ENOMEM);
        }
        auclnt_set_private(c, sc);

        odev = auclnt_get_minor_data(c, AUDIO_MINOR_DSP);

        /* set a couple of common fields */
        sc->o_client = c;
        sc->o_ossdev = odev;

        return (rv);
}

static void
ossmix_close(audio_client_t *c)
{
        ossclient_t     *sc;

        sc = auclnt_get_private(c);

        kmem_free(sc->o_ss_buf, sc->o_ss_sz);
        kmem_free(sc, sizeof (*sc));

        auclnt_close(c);
}

static int
sndctl_mix_nrext(audio_client_t *c, int *ncp)
{
        audio_dev_t     *d;
        ossdev_t        *odev;

        d = auclnt_get_dev(c);

        if ((*ncp != -1) && (*ncp != (auclnt_get_dev_index(d) - 1))) {
                return (ENXIO);
        }

        if ((odev = auclnt_get_dev_minor_data(d, AUDIO_MINOR_DSP)) == NULL) {
                return (EINVAL);
        }

        *ncp = odev->d_nctrl;

        return (0);
}

static int
sndctl_mix_extinfo(audio_client_t *c, oss_mixext *pext)
{
        audio_dev_t             *d;
        ossdev_t                *odev;
        int                     rv = 0;
        int                     dev;

        d = auclnt_get_dev(c);

        if (((dev = pext->dev) != -1) && (dev != (auclnt_get_dev_index(d) - 1)))
                return (ENXIO);

        if (((odev = auclnt_get_dev_minor_data(d, AUDIO_MINOR_DSP)) == NULL) ||
            (pext->ctrl >= odev->d_nctrl)) {
                return (EINVAL);
        }

        bcopy(&odev->d_exts[pext->ctrl], pext, sizeof (*pext));
        pext->enumbit = 0;
        pext->dev = dev;

        return (rv);
}

static int
sndctl_mix_enuminfo(audio_client_t *c, oss_mixer_enuminfo *ei)
{
        audio_dev_t             *d;
        audio_ctrl_desc_t       desc;
        audio_ctrl_t            *ctrl;
        ossdev_t                *odev;
        uint64_t                mask;
        int                     bit;
        ushort_t                nxt;

        d = auclnt_get_dev(c);

        if ((ei->dev != -1) && (ei->dev != (auclnt_get_dev_index(d) - 1)))
                return (ENXIO);

        if (((odev = auclnt_get_dev_minor_data(d, AUDIO_MINOR_DSP)) == NULL) ||
            (ei->ctrl >= odev->d_nctrl) ||
            (odev->d_exts[ei->ctrl].type != MIXT_ENUM) ||
            ((ctrl = odev->d_ctrls[ei->ctrl]) == NULL) ||
            (auclnt_control_describe(ctrl, &desc) != 0)) {
                return (EINVAL);
        }

        mask = desc.acd_maxvalue;
        bit = 0;
        nxt = 0;
        ei->nvalues = 0;
        bzero(ei->strings, sizeof (ei->strings));
        bzero(ei->strindex, sizeof (ei->strindex));

        while (mask) {
                const char *name = desc.acd_enum[bit];
                nxt = oss_set_enum(ei, nxt, name ? name : "");
                mask >>= 1;
                bit++;
        }

        return (0);
}

static int
sndctl_mix_read(audio_client_t *c, oss_mixer_value *vr)
{
        int                     rv;
        uint64_t                v;
        audio_dev_t             *d;
        audio_ctrl_t            *ctrl;
        ossdev_t                *odev;

        d = auclnt_get_dev(c);

        if ((vr->dev != -1) && (vr->dev != (auclnt_get_dev_index(d) - 1)))
                return (ENXIO);

        if (((odev = auclnt_get_dev_minor_data(d, AUDIO_MINOR_DSP)) == NULL) ||
            (vr->ctrl >= odev->d_nctrl) ||
            ((ctrl = odev->d_ctrls[vr->ctrl]) == NULL)) {
                return (EINVAL);
        }
        if ((rv = auclnt_control_read(ctrl, &v)) == 0) {
                switch (odev->d_exts[vr->ctrl].type) {
                case MIXT_ENUM:
                        /* translate this from an enum style bit mask */
                        vr->value = ddi_ffs((unsigned long)v) - 1;
                        break;
                case MIXT_STEREOSLIDER:
                        vr->value = (int)ddi_swap16(v & 0xffff);
                        break;
                case MIXT_MONOSLIDER:
                        vr->value = (int)(v | (v << 8));
                        break;
                case MIXT_ONOFF:
                        /* this could be simple, or could be part of a multi */
                        if (odev->d_exts[vr->ctrl].enumbit >= 0) {
                                uint64_t mask;
                                mask = 1;
                                mask <<= (odev->d_exts[vr->ctrl].enumbit);
                                vr->value = (v & mask) ? 1 : 0;
                        } else {
                                vr->value = v ? 1 : 0;
                        }
                        break;

                default:
                        vr->value = (int)v;
                        break;
                }
        }

        return (rv);
}

static int
sndctl_mix_write(audio_client_t *c, oss_mixer_value *vr)
{
        int                     rv;
        uint64_t                v;
        audio_dev_t             *d;
        audio_ctrl_t            *ctrl;
        ossdev_t                *odev;

        d = auclnt_get_dev(c);

        if ((vr->dev != -1) && (vr->dev != (auclnt_get_dev_index(d) - 1)))
                return (ENXIO);

        if (((odev = auclnt_get_dev_minor_data(d, AUDIO_MINOR_DSP)) == NULL) ||
            (vr->ctrl >= odev->d_nctrl) ||
            ((ctrl = odev->d_ctrls[vr->ctrl]) == NULL)) {
                return (EINVAL);
        }

        switch (odev->d_exts[vr->ctrl].type) {
        case MIXT_ONOFF:
                /* this could be standalone, or it could be part of a multi */
                if (odev->d_exts[vr->ctrl].enumbit >= 0) {
                        uint64_t mask;
                        if ((rv = auclnt_control_read(ctrl, &v)) != 0) {
                                return (EINVAL);
                        }
                        mask = 1;
                        mask <<= (odev->d_exts[vr->ctrl].enumbit);
                        if (vr->value) {
                                v |= mask;
                        } else {
                                v &= ~mask;
                        }
                } else {
                        v = vr->value;
                }
                break;
        case MIXT_ENUM:
                /* translate this to an enum style bit mask */
                v = 1U << vr->value;
                break;
        case MIXT_MONOSLIDER:
                /* mask off high order bits */
                v = vr->value & 0xff;
                break;
        case MIXT_STEREOSLIDER:
                /* OSS uses reverse byte ordering */
                v = vr->value;
                v = ddi_swap16(vr->value & 0xffff);
                break;
        default:
                v = vr->value;
        }
        rv = auclnt_control_write(ctrl, v);

        return (rv);
}

static int
sndctl_mix_nrmix(audio_client_t *c, int *nmixp)
{
        _NOTE(ARGUNUSED(c));
        *nmixp = oss_cnt_devs() - 1;
        return (0);
}

static int
ossmix_ioctl(audio_client_t *c, int cmd, intptr_t arg, int mode, cred_t *credp,
    int *rvalp)
{
        int     sz;
        void    *data;
        int     rv = 0;

        sz = OSSIOC_GETSZ(cmd);

        if ((cmd & (OSSIOC_IN | OSSIOC_OUT)) && sz) {
                if ((data = kmem_zalloc(sz, KM_NOSLEEP)) == NULL) {
                        return (ENOMEM);
                }
        } else {
                sz = 0;
        }

        if (cmd & OSSIOC_IN) {
                if ((rv = ddi_copyin((void *)arg, data, sz, mode)) != 0) {
                        goto done;
                }
        }

        switch (cmd) {
                /*
                 * Mixer specific ioctls
                 */
        case SNDCTL_MIX_NREXT:
                rv = sndctl_mix_nrext(c, (int *)data);
                break;
        case SNDCTL_MIX_EXTINFO:
                rv = sndctl_mix_extinfo(c, (oss_mixext *)data);
                break;
        case SNDCTL_MIX_ENUMINFO:
                rv = sndctl_mix_enuminfo(c, (oss_mixer_enuminfo *)data);
                break;
        case SNDCTL_MIX_READ:
                rv = sndctl_mix_read(c, (oss_mixer_value *)data);
                break;
        case SNDCTL_MIX_WRITE:
                rv = sndctl_mix_write(c, (oss_mixer_value *)data);
                break;
        case SNDCTL_MIX_NRMIX:
                rv = sndctl_mix_nrmix(c, (int *)data);
                break;

                /*
                 * Legacy ioctls.  These are treated as soft values only,
                 * and do not affect global hardware state.  For use by
                 * legacy DSP applications.
                 */
        case SOUND_MIXER_READ_VOLUME:
        case SOUND_MIXER_READ_PCM:
        case SOUND_MIXER_READ_OGAIN:
                rv = sndctl_dsp_getplayvol(c, (int *)data);
                break;

        case SOUND_MIXER_WRITE_VOLUME:
        case SOUND_MIXER_WRITE_PCM:
        case SOUND_MIXER_WRITE_OGAIN:
                rv = sound_mixer_write_ogain(c, (int *)data);
                break;

        case SOUND_MIXER_READ_RECGAIN:
        case SOUND_MIXER_READ_RECLEV:
        case SOUND_MIXER_READ_IGAIN:
                rv = sndctl_dsp_getrecvol(c, (int *)data);
                break;

        case SOUND_MIXER_WRITE_RECGAIN:
        case SOUND_MIXER_WRITE_RECLEV:
        case SOUND_MIXER_WRITE_IGAIN:
                rv = sound_mixer_write_igain(c, (int *)data);
                break;

        case SOUND_MIXER_READ_RECSRC:
        case SOUND_MIXER_WRITE_RECSRC:
                rv = sound_mixer_read_recsrc(c, (int *)data);
                break;

        case SOUND_MIXER_READ_DEVMASK:
        case SOUND_MIXER_READ_STEREODEVS:
                rv = sound_mixer_read_devmask(c, (int *)data);
                break;

        case SOUND_MIXER_READ_RECMASK:
                rv = sound_mixer_read_recmask(c, (int *)data);
                break;

        case SOUND_MIXER_READ_CAPS:
                rv = sound_mixer_read_caps(c, (int *)data);
                break;

                /*
                 * Common ioctls shared with DSP
                 */
        case OSS_GETVERSION:
                rv = oss_getversion((int *)data);
                break;

        case SNDCTL_CARDINFO:
                rv = sndctl_cardinfo(c, (oss_card_info *)data);
                break;

        case SNDCTL_ENGINEINFO:
        case SNDCTL_AUDIOINFO:
        case SNDCTL_AUDIOINFO_EX:
                rv = sndctl_audioinfo(c, (oss_audioinfo *)data);
                break;

        case SNDCTL_SYSINFO:
                rv = sndctl_sysinfo((oss_sysinfo *)data);
                break;

        case SNDCTL_MIXERINFO:
                rv = sndctl_mixerinfo(c, (oss_mixerinfo *)data);
                break;

        case SOUND_MIXER_INFO:
                rv = sound_mixer_info(c, (mixer_info *)data);
                break;

        case SNDCTL_MIX_DESCRIPTION:    /* NOT SUPPORTED: tooltip */
                rv = EIO;       /* OSS returns EIO for this one */
                break;

                /*
                 * Special implementation-private ioctls.
                 */
        case SNDCTL_SUN_SEND_NUMBER:
                rv = sndctl_sun_send_number(c, (int *)data, credp);
                break;

                /*
                 * Legacy ioctls we don't support.
                 */
        case SOUND_MIXER_WRITE_MONGAIN:
        case SOUND_MIXER_READ_MONGAIN:
        case SOUND_MIXER_READ_BASS:
        case SOUND_MIXER_READ_TREBLE:
        case SOUND_MIXER_READ_SPEAKER:
        case SOUND_MIXER_READ_LINE:
        case SOUND_MIXER_READ_MIC:
        case SOUND_MIXER_READ_CD:
        case SOUND_MIXER_READ_IMIX:
        case SOUND_MIXER_READ_ALTPCM:
        case SOUND_MIXER_READ_SYNTH:
        case SOUND_MIXER_READ_LINE1:
        case SOUND_MIXER_READ_LINE2:
        case SOUND_MIXER_READ_LINE3:
        case SOUND_MIXER_WRITE_BASS:
        case SOUND_MIXER_WRITE_TREBLE:
        case SOUND_MIXER_WRITE_SPEAKER:
        case SOUND_MIXER_WRITE_LINE:
        case SOUND_MIXER_WRITE_MIC:
        case SOUND_MIXER_WRITE_CD:
        case SOUND_MIXER_WRITE_IMIX:
        case SOUND_MIXER_WRITE_ALTPCM:
        case SOUND_MIXER_WRITE_SYNTH:
        case SOUND_MIXER_WRITE_LINE1:
        case SOUND_MIXER_WRITE_LINE2:
        case SOUND_MIXER_WRITE_LINE3:
                /*
                 * Additional ioctls we *could* support, but don't.
                 */
        case SNDCTL_SETSONG:
        case SNDCTL_SETLABEL:
        case SNDCTL_GETSONG:
        case SNDCTL_GETLABEL:
        case SNDCTL_MIDIINFO:
        case SNDCTL_SETNAME:
        default:
                rv = EINVAL;
                break;
        }

        if ((rv == 0) && (cmd & OSSIOC_OUT)) {
                rv = ddi_copyout(data, (void *)arg, sz, mode);
        }
        if (rv == 0) {
                *rvalp = 0;
        }

done:
        if (sz) {
                kmem_free(data, sz);
        }
        return (rv);
}

static void *
oss_dev_init(audio_dev_t *dev)
{
        ossdev_t        *odev;

        odev = kmem_zalloc(sizeof (*odev), KM_SLEEP);
        odev->d_dev = dev;

        mutex_init(&odev->d_mx, NULL, MUTEX_DRIVER, NULL);
        cv_init(&odev->d_cv, NULL, CV_DRIVER, NULL);
        oss_alloc_controls(odev);

        return (odev);
}

static void
oss_dev_fini(void *arg)
{
        ossdev_t        *odev = arg;

        if (odev != NULL) {
                oss_free_controls(odev);
                mutex_destroy(&odev->d_mx);
                cv_destroy(&odev->d_cv);
                kmem_free(odev, sizeof (*odev));
        }
}

static void
sndstat_printf(ossclient_t *oc, const char *fmt, ...)
{
        va_list va;

        va_start(va, fmt);
        (void) vsnprintf(oc->o_ss_buf + oc->o_ss_len,
            oc->o_ss_sz - oc->o_ss_len, fmt, va);
        va_end(va);
        oc->o_ss_len = strlen(oc->o_ss_buf);
}

static int
sndstat_dev_walker(audio_dev_t *d, void *arg)
{
        ossclient_t     *oc = arg;
        const char      *capstr;
        unsigned        cap;

        cap = auclnt_get_dev_capab(d);

        if (cap & AUDIO_CLIENT_CAP_DUPLEX) {
                capstr = "DUPLEX";
        } else if ((cap & AUDIO_CLIENT_CAP_PLAY) &&
            (cap & AUDIO_CLIENT_CAP_RECORD)) {
                capstr = "INPUT,OUTPUT";
        } else if (cap & AUDIO_CLIENT_CAP_PLAY) {
                capstr = "OUTPUT";
        } else if (cap & AUDIO_CLIENT_CAP_RECORD) {
                capstr = "INPUT";
        } else {
                capstr = NULL;
        }

        if (capstr == NULL)
                return (AUDIO_WALK_CONTINUE);

        sndstat_printf(oc, "%d: %s %s, %s (%s)\n",
            auclnt_get_dev_number(d), auclnt_get_dev_name(d),
            auclnt_get_dev_description(d), auclnt_get_dev_version(d), capstr);

        return (AUDIO_WALK_CONTINUE);
}

static int
sndstat_mixer_walker(audio_dev_t *d, void *arg)
{
        ossclient_t     *oc = arg;
        unsigned        cap;
        void            *iter;
        const char      *info;

        cap = auclnt_get_dev_capab(d);

        if ((cap & (AUDIO_CLIENT_CAP_PLAY|AUDIO_CLIENT_CAP_RECORD)) == 0)
                return (AUDIO_WALK_CONTINUE);

        sndstat_printf(oc, "%d: %s %s, %s\n",
            auclnt_get_dev_number(d), auclnt_get_dev_name(d),
            auclnt_get_dev_description(d), auclnt_get_dev_version(d));
        iter = NULL;
        while ((info = auclnt_get_dev_hw_info(d, &iter)) != NULL) {
                sndstat_printf(oc, "\t%s\n", info);
        }
        return (AUDIO_WALK_CONTINUE);
}

static int
ossmix_write(audio_client_t *c, struct uio *uio, cred_t *cr)
{
        /* write on sndstat is a no-op */
        _NOTE(ARGUNUSED(c));
        _NOTE(ARGUNUSED(uio));
        _NOTE(ARGUNUSED(cr));

        return (0);
}

static int
ossmix_read(audio_client_t *c, struct uio *uio, cred_t *cr)
{
        ossclient_t     *oc;
        unsigned        n;
        int             rv;

        _NOTE(ARGUNUSED(cr));

        if (uio->uio_resid == 0) {
                return (0);
        }

        oc = auclnt_get_private(c);

        mutex_enter(&oc->o_ss_lock);

        if (oc->o_ss_off == 0) {

                sndstat_printf(oc, "SunOS Audio Framework\n");

                sndstat_printf(oc, "\nAudio Devices:\n");
                auclnt_walk_devs_by_number(sndstat_dev_walker, oc);

                sndstat_printf(oc, "\nMixers:\n");
                auclnt_walk_devs_by_number(sndstat_mixer_walker, oc);
        }

        /*
         * For simplicity's sake, we implement a non-seekable device.  We could
         * support seekability, but offsets would be rather meaningless between
         * changes.
         */
        n = min(uio->uio_resid, (oc->o_ss_len - oc->o_ss_off));

        rv = uiomove(oc->o_ss_buf + oc->o_ss_off, n, UIO_READ, uio);
        if (rv != 0) {
                n = 0;
        }
        oc->o_ss_off += n;

        if (n == 0) {
                /*
                 * end-of-file reached... clear the sndstat buffer so that
                 * subsequent reads will get the latest data.
                 */
                oc->o_ss_off = oc->o_ss_len = 0;
        }
        mutex_exit(&oc->o_ss_lock);
        return (rv);
}

int
oss_read(audio_client_t *c, struct uio *uio, cred_t *cr)
{
        _NOTE(ARGUNUSED(cr));

        auclnt_clear_paused(auclnt_input_stream(c));

        return (auclnt_read(c, uio));
}

int
oss_write(audio_client_t *c, struct uio *uio, cred_t *cr)
{
        _NOTE(ARGUNUSED(cr));

        auclnt_clear_paused(auclnt_output_stream(c));

        return (auclnt_write(c, uio));
}

int
oss_chpoll(audio_client_t *c, short events, int anyyet, short *reventsp,
    struct pollhead **phpp)
{
        return (auclnt_chpoll(c, events, anyyet, reventsp, phpp));
}

static struct audio_client_ops oss_ops = {
        "sound,dsp",
        oss_dev_init,
        oss_dev_fini,
        oss_open,
        oss_close,
        oss_read,
        oss_write,
        oss_ioctl,
        oss_chpoll,
        NULL,           /* mmap */
        oss_input,
        oss_output,
        NULL,           /* drain */
};

static struct audio_client_ops ossmix_ops = {
        "sound,mixer",
        NULL,
        NULL,
        ossmix_open,
        ossmix_close,
        ossmix_read,
        ossmix_write,
        ossmix_ioctl,
        NULL,   /* chpoll */
        NULL,   /* mmap */
        NULL,   /* input */
        NULL,   /* output */
        NULL,   /* drain */
        NULL,   /* wput */
        NULL,   /* wsrv */
};

/* nearly the same as ossxmix; different minor name helps devfsadm */
static struct audio_client_ops sndstat_ops = {
        "sound,sndstat",
        NULL,   /* dev_init */
        NULL,   /* dev_fini */
        ossmix_open,
        ossmix_close,
        ossmix_read,
        ossmix_write,
        ossmix_ioctl,
        NULL,   /* chpoll */
        NULL,   /* mmap */
        NULL,   /* input */
        NULL,   /* output */
        NULL,   /* drain */
        NULL,   /* wput */
        NULL,   /* wsrv */
};

void
auimpl_oss_init(void)
{
        auclnt_register_ops(AUDIO_MINOR_DSP, &oss_ops);
        auclnt_register_ops(AUDIO_MINOR_MIXER, &ossmix_ops);
        auclnt_register_ops(AUDIO_MINOR_SNDSTAT, &sndstat_ops);
}