root/usr/src/uts/common/io/audio/drv/audiosolo/audiosolo.c
/*
 * Copyright (c) 1999 Cameron Grant <cg@freebsd.org>
 *
 * 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.
 */

/*
 * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*
 * Copyright (C) 4Front Technologies 1996-2008.
 */

#include <sys/audio/audio_driver.h>
#include <sys/note.h>
#include <sys/pci.h>
#include <sys/stdbool.h>


/*
 * NB: The Solo-1 is a bit schizophrenic compared to most devices.
 * It has two separate DMA engines for PCM data.  The first can do
 * either capture or playback, and supports various Sound Blaster
 * compatibility features.  The second is dedicated to playback.  The
 * two engines have very little in common when it comes to programming
 * them.
 *
 * We configure engine 1 for record, and engine 2 for playback.  Both
 * are configured for 48 kHz stereo 16-bit signed PCM.
 */

/*
 * ESS Solo-1 only implements the low 24-bits on Audio1, and requires
 * 64KB alignment.  For Audio2, it implements the full 32-bit address
 * space, but requires a 1MB address boundary.  Audio1 is used for
 * recording, and Audio2 is used for playback.
 */
static struct ddi_dma_attr dma_attr_audio1 = {
        DMA_ATTR_VERSION,       /* dma_attr_version */
        0x0,                    /* dma_attr_addr_lo */
        0x00ffffffU,            /* dma_attr_addr_hi */
        0xffff,                 /* dma_attr_count_max */
        0x10000,                /* dma_attr_align */
        0x7f,                   /* dma_attr_burstsizes */
        0x4,                    /* dma_attr_minxfer */
        0xffff,                 /* dma_attr_maxxfer */
        0xffff,                 /* dma_attr_seg */
        0x1,                    /* dma_attr_sgllen */
        0x1,                    /* dma_attr_granular */
        0                       /* dma_attr_flags */
};

static struct ddi_dma_attr dma_attr_audio2 = {
        DMA_ATTR_VERSION,       /* dma_attr_version */
        0x0,                    /* dma_attr_addr_lo */
        0xffffffffU,            /* dma_attr_addr_hi */
        0xfff0,                 /* dma_attr_count_max */
        0x100000,               /* dma_attr_align */
        0x7f,                   /* dma_attr_burstsizes */
        0x4,                    /* dma_attr_minxfer */
        0xfff0,                 /* dma_attr_maxxfer */
        0xffff,                 /* dma_attr_seg */
        0x1,                    /* dma_attr_sgllen */
        0x1,                    /* dma_attr_granular */
        0                       /* dma_attr_flags */
};

static ddi_device_acc_attr_t acc_attr = {
        DDI_DEVICE_ATTR_V0,
        DDI_STRUCTURE_LE_ACC,
        DDI_STRICTORDER_ACC
};

static ddi_device_acc_attr_t buf_attr = {
        DDI_DEVICE_ATTR_V0,
        DDI_NEVERSWAP_ACC,
        DDI_STRICTORDER_ACC
};


/*
 * For the sake of simplicity, this driver fixes a few parameters with
 * constants.
 */
#define SOLO_RATE       48000
#define SOLO_FRAGFR     1024
#define SOLO_NFRAGS     2
#define SOLO_NCHAN      2
#define SOLO_SAMPSZ     2
#define SOLO_FRAGSZ     (SOLO_FRAGFR * (SOLO_NCHAN * SOLO_SAMPSZ))
#define SOLO_BUFFR      (SOLO_NFRAGS * SOLO_FRAGFR)
#define SOLO_BUFSZ      (SOLO_NFRAGS * SOLO_FRAGSZ)

#define INPUT_MIC       0
#define INPUT_LINE      1
#define INPUT_CD        2
#define INPUT_AUX       3
#define INPUT_MONO      4
#define INSRCS          0x1f            /* bits 0-4 */

#define DRVNAME         "audiosolo"

static const char *solo_insrcs[] = {
        AUDIO_PORT_MIC,
        AUDIO_PORT_LINEIN,
        AUDIO_PORT_CD,
        AUDIO_PORT_AUX1IN,
        AUDIO_PORT_AUX2IN,      /* this is really mono-in */
        NULL
};

typedef struct solo_regs {
        ddi_acc_handle_t        acch;
        caddr_t                 base;
} solo_regs_t;

typedef struct solo_engine {
        struct solo_dev         *dev;
        audio_engine_t          *engine;
        ddi_dma_handle_t        dmah;
        ddi_acc_handle_t        acch;
        caddr_t                 kaddr;
        uint32_t                paddr;

        bool                    started;
        bool                    trigger;
        uint64_t                count;
        uint16_t                offset;
        int                     syncdir;
        int                     format;
        bool                    swapped;

        void                    (*start)(struct solo_engine *);
        void                    (*stop)(struct solo_engine *);
        void                    (*update)(struct solo_engine *);
} solo_engine_t;

typedef enum {
        CTL_FRONT = 0,
        CTL_VOLUME,
        CTL_MIC,
        CTL_LINE,
        CTL_CD,
        CTL_AUX,
        CTL_MONO,
        CTL_MICBOOST,
        CTL_RECGAIN,
        CTL_RECSRC,
        CTL_MONSRC,
        CTL_SPEAKER,
        CTL_LOOPBACK,
        CTL_NUM,                        /* must be last */
} solo_ctrl_num_t;

typedef struct solo_ctrl {
        struct solo_dev         *dev;
        audio_ctrl_t            *ctrl;
        solo_ctrl_num_t         num;
        uint64_t                val;
} solo_ctrl_t;

typedef struct solo_dev {
        dev_info_t              *dip;
        audio_dev_t             *adev;
        kmutex_t                mutex;
        ddi_intr_handle_t       ihandle;

        bool                    suspended;

        /*
         * Audio engines
         */
        solo_engine_t           rec;
        solo_engine_t           play;
        uint32_t                last_capture;

        /*
         * Controls.
         */
        solo_ctrl_t             ctrls[CTL_NUM];

        /*
         * Mapped registers
         */
        ddi_acc_handle_t        pcih;
        solo_regs_t             io;
        solo_regs_t             sb;
        solo_regs_t             vc;

} solo_dev_t;

/*
 * Common code for the pcm function
 *
 * solo_cmd write a single byte to the CMD port.
 * solo_cmd1 write a CMD + 1 byte arg
 * ess_get_byte returns a single byte from the DSP data port
 *
 * solo_write is actually solo_cmd1
 * solo_read access ext. regs via solo_cmd(0xc0, reg) followed by solo_get_byte
 */

#define PORT_RD8(port, regno)           \
        ddi_get8(port.acch, (void *)(port.base + (regno)))
#define PORT_RD16(port, regno)          \
        ddi_get16(port.acch, (void *)(port.base + (regno)))
#define PORT_RD32(port, regno)          \
        ddi_get32(port.acch, (void *)(port.base + (regno)))
#define PORT_WR8(port, regno, data)     \
        ddi_put8(port.acch, (void *)(port.base + (regno)), data)
#define PORT_WR16(port, regno, data)    \
        ddi_put16(port.acch, (void *)(port.base + (regno)), data)
#define PORT_WR32(port, regno, data)    \
        ddi_put32(port.acch, (void *)(port.base + (regno)), data)

static bool
solo_dspready(solo_dev_t *dev)
{
        return ((PORT_RD8(dev->sb, 0xc) & 0x80) == 0 ? true : false);
}

static bool
solo_dspwr(solo_dev_t *dev, uint8_t val)
{
        int  i;

        for (i = 0; i < 1000; i++) {
                if (solo_dspready(dev)) {
                        PORT_WR8(dev->sb, 0xc, val);
                        return (true);
                }
                if (i > 10)
                        drv_usecwait((i > 100)? 1000 : 10);
        }
        audio_dev_warn(dev->adev, "solo_dspwr(0x%02x) timed out", val);
        return (false);
}

static bool
solo_cmd(solo_dev_t *dev, uint8_t val)
{
        return (solo_dspwr(dev, val));
}

static void
solo_cmd1(solo_dev_t *dev, uint8_t cmd, uint8_t val)
{
        if (solo_dspwr(dev, cmd)) {
                (void) solo_dspwr(dev, val);
        }
}

static void
solo_setmixer(solo_dev_t *dev, uint8_t port, uint8_t value)
{
        PORT_WR8(dev->sb, 0x4, port); /* Select register */
        drv_usecwait(10);
        PORT_WR8(dev->sb, 0x5, value);
        drv_usecwait(10);
}

static uint8_t
solo_getmixer(solo_dev_t *dev, uint8_t port)
{
        uint8_t val;

        PORT_WR8(dev->sb, 0x4, port); /* Select register */
        drv_usecwait(10);
        val = PORT_RD8(dev->sb, 0x5);
        drv_usecwait(10);

        return (val);
}

static uint8_t
solo_get_byte(solo_dev_t *dev)
{
        for (int i = 1000; i > 0; i--) {
                if (PORT_RD8(dev->sb, 0xc) & 0x40)
                        return (PORT_RD8(dev->sb, 0xa));
                else
                        drv_usecwait(20);
        }
        audio_dev_warn(dev->adev, "timeout waiting to read DSP port");
        return (0xff);
}

static void
solo_write(solo_dev_t *dev, uint8_t reg, uint8_t val)
{
        solo_cmd1(dev, reg, val);
}

static uint8_t
solo_read(solo_dev_t *dev, uint8_t reg)
{
        if (solo_cmd(dev, 0xc0) && solo_cmd(dev, reg)) {
                return (solo_get_byte(dev));
        }
        return (0xff);
}

static bool
solo_reset_dsp(solo_dev_t *dev)
{
        PORT_WR8(dev->sb, 0x6, 3);
        drv_usecwait(100);
        PORT_WR8(dev->sb, 0x6, 0);
        if (solo_get_byte(dev) != 0xAA) {
                audio_dev_warn(dev->adev, "solo_reset_dsp failed");
                return (false); /* Sorry */
        }
        return (true);
}

static uint_t
solo_intr(caddr_t arg1, caddr_t arg2)
{
        solo_dev_t      *dev = (void *)arg1;
        uint8_t         status;
        uint_t          rv = DDI_INTR_UNCLAIMED;

        _NOTE(ARGUNUSED(arg2));

        mutex_enter(&dev->mutex);

        if (dev->suspended) {
                mutex_exit(&dev->mutex);
                return (rv);
        }

        status = PORT_RD8(dev->io, 0x7);
        if (status & 0x20) {
                rv = DDI_INTR_CLAIMED;
                /* ack the interrupt */
                solo_setmixer(dev, 0x7a, solo_getmixer(dev, 0x7a) & ~0x80);
        }

        if (status & 0x10) {
                rv = DDI_INTR_CLAIMED;
                /* ack the interrupt */
                (void) PORT_RD8(dev->sb, 0xe);
        }
        mutex_exit(&dev->mutex);

        return (rv);
}

static uint8_t
solo_mixer_scale(solo_dev_t *dev, solo_ctrl_num_t num)
{
        uint32_t        l, r;
        uint64_t        value = dev->ctrls[num].val;

        l = (value >> 8) & 0xff;
        r = value & 0xff;

        l = (l * 15) / 100;
        r = (r * 15) / 100;
        return ((uint8_t)((l << 4) | (r)));
}

static void
solo_configure_mixer(solo_dev_t *dev)
{
        uint32_t v;
        uint32_t mon, rec;

        /*
         * We disable hardware volume control (i.e. async updates to volume).
         * We could in theory support this, but making it work right can be
         * tricky, and we doubt it is widely used.
         */
        solo_setmixer(dev, 0x64, solo_getmixer(dev, 0x64) | 0xc);
        solo_setmixer(dev, 0x66, 0);

        /* master volume has 6 bits per channel, bit 6 indicates mute  */
        /* left */
        v = (dev->ctrls[CTL_FRONT].val >> 8) & 0xff;
        v = v ? (v * 63) / 100 : 64;
        solo_setmixer(dev, 0x60, v & 0xff);

        /* right */
        v = dev->ctrls[CTL_FRONT].val & 0xff;
        v = v ? (v * 63) / 100 : 64;
        solo_setmixer(dev, 0x62, v & 0xff);

        v = solo_mixer_scale(dev, CTL_VOLUME);
        v = v | (v << 4);
        solo_setmixer(dev, 0x7c, v & 0xff);
        solo_setmixer(dev, 0x14, v & 0xff);

        mon = dev->ctrls[CTL_MONSRC].val;
        rec = dev->ctrls[CTL_RECSRC].val;

        /*
         * The Solo-1 has dual stereo mixers (one for input and one for output),
         * with separate volume controls for each.
         */
        v = solo_mixer_scale(dev, CTL_MIC);
        solo_setmixer(dev, 0x68, rec & (1 << INPUT_MIC) ? v : 0);
        solo_setmixer(dev, 0x1a, mon & (1 << INPUT_MIC) ? v : 0);

        v = solo_mixer_scale(dev, CTL_LINE);
        solo_setmixer(dev, 0x6e, rec & (1 << INPUT_LINE) ? v : 0);
        solo_setmixer(dev, 0x3e, mon & (1 << INPUT_LINE) ? v : 0);

        v = solo_mixer_scale(dev, CTL_CD);
        solo_setmixer(dev, 0x6a, rec & (1 << INPUT_CD) ? v : 0);
        solo_setmixer(dev, 0x38, mon & (1 << INPUT_CD) ? v : 0);

        v = solo_mixer_scale(dev, CTL_AUX);
        solo_setmixer(dev, 0x6c, rec & (1 << INPUT_AUX) ? v : 0);
        solo_setmixer(dev, 0x3a, mon & (1 << INPUT_AUX) ? v : 0);

        v = solo_mixer_scale(dev, CTL_MONO);
        v = v | (v << 4);
        solo_setmixer(dev, 0x6f, rec & (1 << INPUT_MONO) ? v : 0);
        solo_setmixer(dev, 0x6d, mon & (1 << INPUT_MONO) ? v : 0);

        if (dev->ctrls[CTL_MICBOOST].val) {
                solo_setmixer(dev, 0x7d, solo_getmixer(dev, 0x7d) | 0x8);
        } else {
                solo_setmixer(dev, 0x7d, solo_getmixer(dev, 0x7d) & ~(0x8));
        }

        v = solo_mixer_scale(dev, CTL_RECGAIN);
        v = v | (v << 4);
        solo_write(dev, 0xb4, v & 0xff);

        v = dev->ctrls[CTL_SPEAKER].val & 0xff;
        v = (v * 7) / 100;
        solo_setmixer(dev, 0x3c, v & 0xff);

        if (dev->ctrls[CTL_LOOPBACK].val) {
                /* record-what-you-hear mode */
                solo_setmixer(dev, 0x1c, 0x3);
        } else {
                /* use record mixer */
                solo_setmixer(dev, 0x1c, 0x5);
        }

}

static int
solo_set_mixsrc(void *arg, uint64_t val)
{
        solo_ctrl_t     *pc = arg;
        solo_dev_t      *dev = pc->dev;

        if ((val & ~INSRCS) != 0)
                return (EINVAL);

        mutex_enter(&dev->mutex);
        pc->val = val;
        solo_configure_mixer(dev);
        mutex_exit(&dev->mutex);
        return (0);
}

static int
solo_set_mono(void *arg, uint64_t val)
{
        solo_ctrl_t     *pc = arg;
        solo_dev_t      *dev = pc->dev;

        val &= 0xff;
        if (val > 100)
                return (EINVAL);

        val = (val & 0xff) | ((val & 0xff) << 8);

        mutex_enter(&dev->mutex);
        pc->val = val;
        solo_configure_mixer(dev);
        mutex_exit(&dev->mutex);
        return (0);
}

static int
solo_set_stereo(void *arg, uint64_t val)
{
        solo_ctrl_t     *pc = arg;
        solo_dev_t      *dev = pc->dev;
        uint8_t         l;
        uint8_t         r;

        l = (val & 0xff00) >> 8;
        r = val & 0xff;

        if ((l > 100) || (r > 100))
                return (EINVAL);

        mutex_enter(&dev->mutex);
        pc->val = val;
        solo_configure_mixer(dev);
        mutex_exit(&dev->mutex);
        return (0);
}

static int
solo_set_bool(void *arg, uint64_t val)
{
        solo_ctrl_t     *pc = arg;
        solo_dev_t      *dev = pc->dev;

        mutex_enter(&dev->mutex);
        pc->val = val;
        solo_configure_mixer(dev);
        mutex_exit(&dev->mutex);
        return (0);
}

static int
solo_get_value(void *arg, uint64_t *val)
{
        solo_ctrl_t     *pc = arg;
        solo_dev_t      *dev = pc->dev;

        mutex_enter(&dev->mutex);
        *val = pc->val;
        mutex_exit(&dev->mutex);
        return (0);
}

#define PLAYCTL (AUDIO_CTRL_FLAG_RW | AUDIO_CTRL_FLAG_PLAY)
#define RECCTL  (AUDIO_CTRL_FLAG_RW | AUDIO_CTRL_FLAG_REC)
#define MONCTL  (AUDIO_CTRL_FLAG_RW | AUDIO_CTRL_FLAG_MONITOR)
#define PCMVOL  (PLAYCTL | AUDIO_CTRL_FLAG_PCMVOL)
#define MAINVOL (PLAYCTL | AUDIO_CTRL_FLAG_MAINVOL)
#define RECVOL  (RECCTL | AUDIO_CTRL_FLAG_RECVOL)

static void
solo_alloc_ctrl(solo_dev_t *dev, uint32_t num, uint64_t val)
{
        audio_ctrl_desc_t       desc;
        audio_ctrl_wr_t         fn;
        solo_ctrl_t             *pc;

        bzero(&desc, sizeof (desc));

        pc = &dev->ctrls[num];
        pc->num = num;
        pc->dev = dev;

        switch (num) {
        case CTL_VOLUME:
                desc.acd_name = AUDIO_CTRL_ID_VOLUME;
                desc.acd_type = AUDIO_CTRL_TYPE_MONO;
                desc.acd_minvalue = 0;
                desc.acd_maxvalue = 100;
                desc.acd_flags = PCMVOL;
                fn = solo_set_mono;
                break;

        case CTL_FRONT:
                desc.acd_name = AUDIO_CTRL_ID_LINEOUT;
                desc.acd_type = AUDIO_CTRL_TYPE_STEREO;
                desc.acd_minvalue = 0;
                desc.acd_maxvalue = 100;
                desc.acd_flags = MAINVOL;
                fn = solo_set_stereo;
                break;

        case CTL_SPEAKER:
                desc.acd_name = AUDIO_CTRL_ID_SPEAKER;
                desc.acd_type = AUDIO_CTRL_TYPE_MONO;
                desc.acd_minvalue = 0;
                desc.acd_maxvalue = 100;
                desc.acd_flags = MAINVOL;
                fn = solo_set_mono;
                break;

        case CTL_MIC:
                desc.acd_name = AUDIO_CTRL_ID_MIC;
                desc.acd_type = AUDIO_CTRL_TYPE_STEREO;
                desc.acd_minvalue = 0;
                desc.acd_maxvalue = 100;
                desc.acd_flags = RECVOL;
                fn = solo_set_stereo;
                break;

        case CTL_LINE:
                desc.acd_name = AUDIO_CTRL_ID_LINEIN;
                desc.acd_type = AUDIO_CTRL_TYPE_STEREO;
                desc.acd_minvalue = 0;
                desc.acd_maxvalue = 100;
                desc.acd_flags = RECVOL;
                fn = solo_set_stereo;
                break;

        case CTL_CD:
                desc.acd_name = AUDIO_CTRL_ID_CD;
                desc.acd_type = AUDIO_CTRL_TYPE_STEREO;
                desc.acd_minvalue = 0;
                desc.acd_maxvalue = 100;
                desc.acd_flags = RECVOL;
                fn = solo_set_stereo;
                break;

        case CTL_AUX:
                desc.acd_name = AUDIO_CTRL_ID_AUX1IN;
                desc.acd_type = AUDIO_CTRL_TYPE_STEREO;
                desc.acd_minvalue = 0;
                desc.acd_maxvalue = 100;
                desc.acd_flags = RECVOL;
                fn = solo_set_stereo;
                break;

        case CTL_MONO:
                desc.acd_name = AUDIO_CTRL_ID_AUX2IN;
                desc.acd_type = AUDIO_CTRL_TYPE_MONO;
                desc.acd_minvalue = 0;
                desc.acd_maxvalue = 100;
                desc.acd_flags = RECVOL;
                fn = solo_set_mono;
                break;

        case CTL_RECSRC:
                desc.acd_name = AUDIO_CTRL_ID_RECSRC;
                desc.acd_type = AUDIO_CTRL_TYPE_ENUM;
                desc.acd_minvalue = INSRCS;
                desc.acd_maxvalue = INSRCS;
                desc.acd_flags = RECCTL | AUDIO_CTRL_FLAG_MULTI;
                for (int i = 0; solo_insrcs[i]; i++) {
                        desc.acd_enum[i] = solo_insrcs[i];
                }
                fn = solo_set_mixsrc;
                break;

        case CTL_MONSRC:
                desc.acd_name = AUDIO_CTRL_ID_MONSRC;
                desc.acd_type = AUDIO_CTRL_TYPE_ENUM;
                desc.acd_minvalue = INSRCS;
                desc.acd_maxvalue = INSRCS;
                desc.acd_flags = MONCTL | AUDIO_CTRL_FLAG_MULTI;
                for (int i = 0; solo_insrcs[i]; i++) {
                        desc.acd_enum[i] = solo_insrcs[i];
                }
                fn = solo_set_mixsrc;
                break;

        case CTL_MICBOOST:
                desc.acd_name = AUDIO_CTRL_ID_MICBOOST;
                desc.acd_type = AUDIO_CTRL_TYPE_BOOLEAN;
                desc.acd_minvalue = 0;
                desc.acd_maxvalue = 1;
                desc.acd_flags = RECCTL;
                fn = solo_set_bool;
                break;

        case CTL_LOOPBACK:
                desc.acd_name = AUDIO_CTRL_ID_LOOPBACK;
                desc.acd_type = AUDIO_CTRL_TYPE_BOOLEAN;
                desc.acd_minvalue = 0;
                desc.acd_maxvalue = 1;
                desc.acd_flags = RECCTL;
                fn = solo_set_bool;
                break;

        case CTL_RECGAIN:
                desc.acd_name = AUDIO_CTRL_ID_RECGAIN;
                desc.acd_type = AUDIO_CTRL_TYPE_STEREO;
                desc.acd_minvalue = 0;
                desc.acd_maxvalue = 100;
                desc.acd_flags = RECCTL;
                fn = solo_set_stereo;
                break;
        }

        pc->val = val;
        pc->ctrl = audio_dev_add_control(dev->adev, &desc,
            solo_get_value, fn, pc);
}

static bool
solo_add_controls(solo_dev_t *dev)
{
        solo_alloc_ctrl(dev, CTL_VOLUME, 0x4b);
        solo_alloc_ctrl(dev, CTL_FRONT, 0x5a5a);
        solo_alloc_ctrl(dev, CTL_SPEAKER, 0x4b);
        solo_alloc_ctrl(dev, CTL_MIC, 0x3232);
        solo_alloc_ctrl(dev, CTL_LINE, 0x4b4b);
        solo_alloc_ctrl(dev, CTL_CD, 0x4b4b);
        solo_alloc_ctrl(dev, CTL_AUX, 0);
        solo_alloc_ctrl(dev, CTL_MONO, 0);
        solo_alloc_ctrl(dev, CTL_RECSRC, (1U << INPUT_MIC));
        solo_alloc_ctrl(dev, CTL_MONSRC, 0);
        solo_alloc_ctrl(dev, CTL_RECGAIN, 0x4b4b);
        solo_alloc_ctrl(dev, CTL_MICBOOST, 1);
        solo_alloc_ctrl(dev, CTL_LOOPBACK, 0);

        return (true);
}


/* utility functions for ESS */
static uint8_t
solo_calcfilter(int spd)
{
        int cutoff;

        cutoff = (spd * 9 * 82) / 20;
        return (256 - (7160000 / cutoff));
}

static void
solo_aud1_update(solo_engine_t *e)
{
        solo_dev_t      *dev = e->dev;
        uint16_t        offset, n;
        uint32_t        ptr;
        uint32_t        count;
        uint32_t        diff;
        int             tries;

        ASSERT(mutex_owned(&dev->mutex));

        /*
         * During recording, this register is known to give back
         * garbage if it's not quiescent while being read.  This hack
         * attempts to work around it.  We also suspend the DMA
         * while we do this, to minimize record distortion.
         */
        if (e->trigger) {
                drv_usecwait(20);
        }
        for (tries = 10; tries; tries--) {
                drv_usecwait(10);
                ptr = PORT_RD32(dev->vc, 0);
                count = PORT_RD16(dev->vc, 4);
                diff = e->paddr + SOLO_BUFSZ - ptr - count;
                if ((diff > 3) || (ptr < e->paddr) ||
                    (ptr >= (e->paddr + SOLO_BUFSZ))) {
                        ptr = dev->last_capture;
                } else {
                        break;
                }
        }
        if (e->trigger) {
                PORT_WR8(dev->vc, 0xf, 0);      /* restart DMA */
        }
        if (!tries) {
                /*
                 * Note, this is a pretty bad situation, because we'll
                 * not have an accurate idea of our position.  But its
                 * better than making a bad alteration.  If we had FMA
                 * for audio devices, this would be a good point to
                 * raise a fault.
                 */
                return;
        }
        dev->last_capture = ptr;
        offset = ptr - e->paddr;
        offset /= (SOLO_NCHAN * SOLO_SAMPSZ);

        n = offset >= e->offset ?
            offset - e->offset :
            offset + SOLO_BUFSZ - e->offset;

        e->offset = offset;
        e->count += n / (SOLO_NCHAN * SOLO_SAMPSZ);
}

static void
solo_aud1_start(solo_engine_t *e)
{
        solo_dev_t      *dev = e->dev;
        int             len;
        uint32_t        v;

        ASSERT(mutex_owned(&dev->mutex));

        e->offset = 0;
        len = SOLO_FRAGSZ / 2;
        len = -len;

        /* sample rate - 48 kHz */
        solo_write(dev, 0xa1, 0xf0);
        /* filter cutoff */
        solo_write(dev, 0xa2, solo_calcfilter(SOLO_RATE));


        /* mono/stereo - bit 0 set, bit 1 clear */
        solo_write(dev, 0xa8, (solo_read(dev, 0xa8) & ~0x03) | 1);

        (void) solo_cmd(dev, 0xd3);     /* turn off DAC1 output */

        /* setup fifo for signed 16-bit stereo */
        solo_write(dev, 0xb7, 0x71);
        solo_write(dev, 0xb7, 0xbc);

        v = solo_mixer_scale(dev, CTL_RECGAIN);
        v = v | (v << 4);
        solo_write(dev, 0xb4, v & 0xff);

        PORT_WR8(dev->vc, 0x8, 0xc4); /* command */
        PORT_WR8(dev->vc, 0xd, 0xff); /* clear DMA */
        PORT_WR8(dev->vc, 0xf, 0x01); /* stop DMA  */

        PORT_WR8(dev->vc, 0xd, 0xff); /* reset */
        PORT_WR8(dev->vc, 0xf, 0x01); /* mask */
        PORT_WR8(dev->vc, 0xb, 0x14); /* mode */

        PORT_WR32(dev->vc, 0x0, e->paddr);
        PORT_WR16(dev->vc, 0x4, SOLO_BUFSZ - 1);

        /* transfer length low, high */
        solo_write(dev, 0xa4, len & 0x00ff);
        solo_write(dev, 0xa5, (len & 0xff00) >> 8);

        /* autoinit, dma dir, go for it */
        solo_write(dev, 0xb8, 0x0f);
        PORT_WR8(dev->vc, 0xf, 0);      /* start DMA */

        dev->last_capture = e->paddr;
        e->trigger = true;
}

static void
solo_aud1_stop(solo_engine_t *e)
{
        solo_dev_t      *dev = e->dev;

        /* NB: We might be in quiesce, without a lock held */
        solo_write(dev, 0xb8, solo_read(dev, 0xb8) & ~0x01);
        e->trigger = false;
}

static void
solo_aud2_update(solo_engine_t *e)
{
        solo_dev_t      *dev = e->dev;
        uint16_t        offset = 0, n;

        ASSERT(mutex_owned(&dev->mutex));

        offset = SOLO_BUFSZ - PORT_RD16(dev->io, 0x4);
        offset /= (SOLO_NCHAN * SOLO_SAMPSZ);

        n = offset >= e->offset ?
            offset - e->offset :
            offset + SOLO_BUFFR - e->offset;

        e->offset = offset;
        e->count += n;
}

static void
solo_aud2_start(solo_engine_t *e)
{
        solo_dev_t      *dev = e->dev;
        int             len;
        uint32_t        v;

        ASSERT(mutex_owned(&dev->mutex));

        e->offset = 0;
        len = SOLO_FRAGSZ / 2;
        len = -len;

        /* program transfer type */
        solo_setmixer(dev, 0x78, 0x10);
        /* sample rate - 48 kHz */
        solo_setmixer(dev, 0x70, 0xf0);
        solo_setmixer(dev, 0x72, solo_calcfilter(SOLO_RATE));
        /* transfer length low & high */
        solo_setmixer(dev, 0x74, len & 0x00ff);
        solo_setmixer(dev, 0x76, (len & 0xff00) >> 8);
        /* enable irq, set signed 16-bit stereo format */
        solo_setmixer(dev, 0x7a, 0x47);

        PORT_WR8(dev->io, 0x6, 0);
        PORT_WR32(dev->io, 0x0, e->paddr);
        PORT_WR16(dev->io, 0x4, SOLO_BUFSZ);

        /* this crazy initialization appears to help with fifo weirdness */
        /* start the engine running */
        solo_setmixer(dev, 0x78, 0x92);
        drv_usecwait(10);
        solo_setmixer(dev, 0x78, 0x93);

        PORT_WR8(dev->io, 0x6, 0x0a); /* autoinit, enable */

        v = solo_mixer_scale(dev, CTL_VOLUME);
        v = v | (v << 4);
        solo_setmixer(dev, 0x7c, v & 0xff);

        e->trigger = true;
}

static void
solo_aud2_stop(solo_engine_t *e)
{
        solo_dev_t      *dev = e->dev;

        /* NB: We might be in quiesce, without a lock held */
        PORT_WR8(dev->io, 0x6, 0);
        solo_setmixer(dev, 0x78, solo_getmixer(dev, 0x78) & ~0x03);

        e->trigger = false;
}

/*
 * Audio entry points.
 */
static int
solo_format(void *arg)
{
        solo_engine_t   *e = arg;
        return (e->format);
}

static int
solo_channels(void *arg)
{
        _NOTE(ARGUNUSED(arg));
        return (SOLO_NCHAN);
}

static int
solo_rate(void *arg)
{
        _NOTE(ARGUNUSED(arg));
        return (SOLO_RATE);
}

static void
solo_chinfo(void *arg, int chan, unsigned *offset, unsigned *incr)
{
        solo_engine_t *e = arg;

        if (e->swapped) {
                *offset = !chan;
        } else {
                *offset = chan;
        }
        *incr = 2;
}

static void
solo_sync(void *arg, unsigned nframes)
{
        solo_engine_t *e = arg;

        _NOTE(ARGUNUSED(nframes));

        (void) ddi_dma_sync(e->dmah, 0, 0, e->syncdir);
}


static uint64_t
solo_count(void *arg)
{
        solo_engine_t   *e = arg;
        solo_dev_t      *dev = e->dev;
        uint64_t        count;

        mutex_enter(&dev->mutex);
        e->update(e);
        count = e->count;
        mutex_exit(&dev->mutex);

        return (count);
}

static int
solo_open(void *arg, int f, unsigned *nframes, caddr_t *buf)
{
        solo_engine_t   *e = arg;
        solo_dev_t      *dev = e->dev;

        _NOTE(ARGUNUSED(f));

        *nframes = SOLO_NFRAGS * SOLO_FRAGFR;
        *buf = e->kaddr;

        mutex_enter(&dev->mutex);
        e->started = false;
        e->count = 0;
        mutex_exit(&dev->mutex);

        return (0);
}

void
solo_close(void *arg)
{
        solo_engine_t   *e = arg;
        solo_dev_t      *dev = e->dev;

        mutex_enter(&dev->mutex);
        e->stop(e);
        e->started = false;
        mutex_exit(&dev->mutex);
}


static int
solo_start(void *arg)
{
        solo_engine_t   *e = arg;
        solo_dev_t      *dev = e->dev;

        mutex_enter(&dev->mutex);
        if (!e->started) {
                e->start(e);
                e->started = true;
        }
        mutex_exit(&dev->mutex);

        return (0);
}

static void
solo_stop(void *arg)
{
        solo_engine_t   *e = arg;
        solo_dev_t      *dev = e->dev;

        mutex_enter(&dev->mutex);
        if (e->started) {
                e->stop(e);
                e->started = false;
        }
        mutex_exit(&dev->mutex);

}

static audio_engine_ops_t solo_engine_ops = {
        AUDIO_ENGINE_VERSION,
        solo_open,
        solo_close,
        solo_start,
        solo_stop,
        solo_count,
        solo_format,
        solo_channels,
        solo_rate,
        solo_sync,
        NULL,
        solo_chinfo,
        NULL,
};

static void
solo_release_resources(solo_dev_t *dev)
{
        if (dev->ihandle != NULL) {
                (void) ddi_intr_disable(dev->ihandle);
                (void) ddi_intr_remove_handler(dev->ihandle);
                (void) ddi_intr_free(dev->ihandle);
                mutex_destroy(&dev->mutex);
        }

        if (dev->io.acch != NULL) {
                ddi_regs_map_free(&dev->io.acch);
        }

        if (dev->sb.acch != NULL) {
                ddi_regs_map_free(&dev->sb.acch);
        }

        if (dev->vc.acch != NULL) {
                ddi_regs_map_free(&dev->vc.acch);
        }

        if (dev->pcih != NULL) {
                pci_config_teardown(&dev->pcih);
        }

        /* release play resources */
        if (dev->play.paddr != 0)
                (void) ddi_dma_unbind_handle(dev->play.dmah);
        if (dev->play.acch != NULL)
                ddi_dma_mem_free(&dev->play.acch);
        if (dev->play.dmah != NULL)
                ddi_dma_free_handle(&dev->play.dmah);

        if (dev->play.engine != NULL) {
                audio_dev_remove_engine(dev->adev, dev->play.engine);
                audio_engine_free(dev->play.engine);
        }

        /* release record resources */
        if (dev->rec.paddr != 0)
                (void) ddi_dma_unbind_handle(dev->rec.dmah);
        if (dev->rec.acch != NULL)
                ddi_dma_mem_free(&dev->rec.acch);
        if (dev->rec.dmah != NULL)
                ddi_dma_free_handle(&dev->rec.dmah);

        if (dev->rec.engine != NULL) {
                audio_dev_remove_engine(dev->adev, dev->rec.engine);
                audio_engine_free(dev->rec.engine);
        }

        for (int i = 0; i < CTL_NUM; i++) {
                if (dev->ctrls[i].ctrl != NULL) {
                        audio_dev_del_control(dev->ctrls[i].ctrl);
                }
        }

        if (dev->adev != NULL) {
                audio_dev_free(dev->adev);
        }

        kmem_free(dev, sizeof (*dev));
}

static bool
solo_setup_interrupts(solo_dev_t *dev)
{
        int actual;
        uint_t ipri;

        if ((ddi_intr_alloc(dev->dip, &dev->ihandle, DDI_INTR_TYPE_FIXED,
            0, 1, &actual, DDI_INTR_ALLOC_NORMAL) != DDI_SUCCESS) ||
            (actual != 1)) {
                audio_dev_warn(dev->adev, "can't alloc intr handle");
                return (false);
        }

        if (ddi_intr_get_pri(dev->ihandle, &ipri) != DDI_SUCCESS) {
                audio_dev_warn(dev->adev,  "can't determine intr priority");
                (void) ddi_intr_free(dev->ihandle);
                dev->ihandle = NULL;
                return (false);
        }

        if (ddi_intr_add_handler(dev->ihandle, solo_intr, dev,
            NULL) != DDI_SUCCESS) {
                audio_dev_warn(dev->adev, "can't add intr handler");
                (void) ddi_intr_free(dev->ihandle);
                dev->ihandle = NULL;
                return (false);
        }

        mutex_init(&dev->mutex, NULL, MUTEX_DRIVER, DDI_INTR_PRI(ipri));

        return (true);
}

static bool
solo_map_registers(solo_dev_t *dev)
{
        dev_info_t      *dip = dev->dip;

        /* map registers */
        if (ddi_regs_map_setup(dip, 1, &dev->io.base, 0, 0, &acc_attr,
            &dev->io.acch) != DDI_SUCCESS) {
                audio_dev_warn(dev->adev, "can't map IO registers");
                return (false);
        }
        if (ddi_regs_map_setup(dip, 2, &dev->sb.base, 0, 0, &acc_attr,
            &dev->sb.acch) != DDI_SUCCESS) {
                audio_dev_warn(dev->adev, "can't map SB registers");
                return (false);
        }
        if (ddi_regs_map_setup(dip, 3, &dev->vc.base, 0, 0, &acc_attr,
            &dev->vc.acch) != DDI_SUCCESS) {
                audio_dev_warn(dev->adev, "can't map VC registers");
                return (false);
        }

        return (true);
}

#define ESS_PCI_LEGACYCONTROL           0x40
#define ESS_PCI_CONFIG                  0x50
#define ESS_PCI_DDMACONTROL             0x60

static bool
solo_init_hw(solo_dev_t *dev)
{
        uint32_t        data;

        /*
         * Legacy audio register -- disable legacy audio.  We also
         * arrange for 16-bit I/O address decoding.
         */
        /* this version disables the MPU, FM synthesis (Adlib), and Game Port */
        pci_config_put16(dev->pcih, ESS_PCI_LEGACYCONTROL, 0x8041);

        /*
         * Note that Solo-1 uses I/O space for all BARs, and hardwires
         * the upper 32-bits to zero.
         */
        data = pci_config_get32(dev->pcih, PCI_CONF_BASE2);
        data |= 1;
        pci_config_put16(dev->pcih, ESS_PCI_DDMACONTROL, data & 0xffff);

        /*
         * Make sure that legacy IRQ and DRQ are disbled.  We disable most
         * other legacy features too.
         */
        pci_config_put16(dev->pcih, ESS_PCI_CONFIG, 0);

        if (!solo_reset_dsp(dev))
                return (false);

        /* enable extended mode */
        (void) solo_cmd(dev, 0xc6);


        PORT_WR8(dev->io, 0x7, 0x30); /* enable audio irqs */

        /* demand mode, 4 bytes/xfer */
        solo_write(dev, 0xb9, 0x01);

        /*
         * This sets Audio 2 (playback) to use its own independent
         * rate control, and gives us 48 kHz compatible divisors.  It
         * also bypasses the switched capacitor filter.
         */
        solo_setmixer(dev, 0x71, 0x2a);

        /* irq control */
        solo_write(dev, 0xb1, (solo_read(dev, 0xb1) & 0x0f) | 0x50);
        /* drq control */
        solo_write(dev, 0xb2, (solo_read(dev, 0xb2) & 0x0f) | 0x50);

        solo_setmixer(dev, 0, 0); /* reset mixer settings */

        solo_configure_mixer(dev);
        return (true);
}

static bool
solo_alloc_engine(solo_dev_t *dev, int engno)
{
        size_t                  rlen;
        ddi_dma_attr_t          *dattr;
        ddi_dma_cookie_t        c;
        unsigned                ccnt;
        unsigned                caps;
        unsigned                dflags;
        const char              *desc;
        solo_engine_t           *e;

        ASSERT((engno == 1) || (engno = 2));

        switch (engno) {
        case 1: /* record */
                e = &dev->rec;
                desc = "record";
                dattr = &dma_attr_audio1;
                caps = ENGINE_INPUT_CAP;
                dflags = DDI_DMA_READ | DDI_DMA_CONSISTENT;
                e->syncdir = DDI_DMA_SYNC_FORKERNEL;
                e->update = solo_aud1_update;
                e->start = solo_aud1_start;
                e->stop = solo_aud1_stop;
                e->format = AUDIO_FORMAT_S16_BE;
                e->swapped = true;
                break;

        case 2: /* playback */
                e = &dev->play;
                desc = "playback";
                dattr = &dma_attr_audio2;
                caps = ENGINE_OUTPUT_CAP;
                dflags = DDI_DMA_WRITE | DDI_DMA_CONSISTENT;
                e->syncdir = DDI_DMA_SYNC_FORDEV;
                e->update = solo_aud2_update;
                e->start = solo_aud2_start;
                e->stop = solo_aud2_stop;
                e->format = AUDIO_FORMAT_S16_LE;
                e->swapped = false;
                break;

        default:
                audio_dev_warn(dev->adev, "bad engine number!");
                return (false);
        }

        e->dev = dev;

        if (ddi_dma_alloc_handle(dev->dip, dattr, DDI_DMA_SLEEP, NULL,
            &e->dmah) != DDI_SUCCESS) {
                audio_dev_warn(dev->adev, "%s dma handle alloc failed", desc);
                return (false);
        }
        if (ddi_dma_mem_alloc(e->dmah, SOLO_BUFSZ, &buf_attr,
            DDI_DMA_CONSISTENT, DDI_DMA_SLEEP, NULL, &e->kaddr,
            &rlen, &e->acch) != DDI_SUCCESS) {
                audio_dev_warn(dev->adev, "%s dma memory alloc failed", desc);
                return (false);
        }
        /* ensure that the buffer is zeroed out properly */
        bzero(e->kaddr, rlen);
        if (ddi_dma_addr_bind_handle(e->dmah, NULL, e->kaddr, SOLO_BUFSZ,
            dflags, DDI_DMA_SLEEP, NULL, &c, &ccnt) != DDI_DMA_MAPPED) {
                audio_dev_warn(dev->adev, "%s dma binding failed", desc);
                return (false);
        }
        e->paddr = c.dmac_address;

        /*
         * Allocate and configure audio engine.
         */
        e->engine = audio_engine_alloc(&solo_engine_ops, caps);
        if (e->engine == NULL) {
                audio_dev_warn(dev->adev, "record audio_engine_alloc failed");
                return (false);
        }

        audio_engine_set_private(e->engine, e);
        audio_dev_add_engine(dev->adev, e->engine);

        return (true);
}


static int
solo_suspend(solo_dev_t *dev)
{
        audio_dev_suspend(dev->adev);

        mutex_enter(&dev->mutex);
        dev->suspended = true;
        mutex_exit(&dev->mutex);

        return (DDI_SUCCESS);
}

static int
solo_resume(solo_dev_t *dev)
{
        mutex_enter(&dev->mutex);
        if (!solo_init_hw(dev)) {
                /* yikes! */
                audio_dev_warn(dev->adev, "unable to resume audio!");
                audio_dev_warn(dev->adev, "reboot or reload driver to reset");
        }
        dev->suspended = false;
        mutex_exit(&dev->mutex);

        audio_dev_resume(dev->adev);

        return (DDI_SUCCESS);
}

static int
solo_attach(dev_info_t *dip)
{
        solo_dev_t      *dev;
        uint32_t        data;

        dev = kmem_zalloc(sizeof (*dev), KM_SLEEP);
        dev->dip = dip;
        ddi_set_driver_private(dip, dev);

        dev->adev = audio_dev_alloc(dip, 0);
        if (dev->adev == NULL)
                goto no;

        audio_dev_set_description(dev->adev, "ESS Solo-1 PCI AudioDrive");
        audio_dev_set_version(dev->adev, "ES1938");

        if (pci_config_setup(dip, &dev->pcih) != DDI_SUCCESS) {
                audio_dev_warn(NULL, "pci_config_setup failed");
                goto no;
        }

        data = pci_config_get16(dev->pcih, PCI_CONF_COMM);
        data |= PCI_COMM_ME | PCI_COMM_IO;
        pci_config_put16(dev->pcih, PCI_CONF_COMM, data);

        if ((!solo_map_registers(dev)) ||
            (!solo_setup_interrupts(dev)) ||
            (!solo_alloc_engine(dev, 1)) ||
            (!solo_alloc_engine(dev, 2)) ||
            (!solo_add_controls(dev)) ||
            (!solo_init_hw(dev))) {
                goto no;
        }

        if (audio_dev_register(dev->adev) != DDI_SUCCESS) {
                audio_dev_warn(dev->adev,
                    "unable to register with audio framework");
                goto no;
        }

        (void) ddi_intr_enable(dev->ihandle);
        ddi_report_dev(dip);

        return (DDI_SUCCESS);

no:
        solo_release_resources(dev);
        return (DDI_FAILURE);
}

static int
solo_detach(solo_dev_t *dev)
{
        if (audio_dev_unregister(dev->adev) != DDI_SUCCESS) {
                return (DDI_FAILURE);
        }

        solo_release_resources(dev);
        return (DDI_SUCCESS);
}

static int
solo_ddi_attach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
        solo_dev_t *dev;

        switch (cmd) {
        case DDI_ATTACH:
                return (solo_attach(dip));

        case DDI_RESUME:
                if ((dev = ddi_get_driver_private(dip)) == NULL) {
                        return (DDI_FAILURE);
                }
                return (solo_resume(dev));

        default:
                return (DDI_FAILURE);
        }
}

static int
solo_ddi_detach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
        solo_dev_t *dev;

        if ((dev = ddi_get_driver_private(dip)) == NULL) {
                return (DDI_FAILURE);
        }

        switch (cmd) {
        case DDI_DETACH:
                return (solo_detach(dev));

        case DDI_SUSPEND:
                return (solo_suspend(dev));
        default:
                return (DDI_FAILURE);
        }
}

static int
solo_quiesce(dev_info_t *dip)
{
        solo_dev_t *dev;

        dev = ddi_get_driver_private(dip);

        solo_aud1_stop(&dev->rec);
        solo_aud2_stop(&dev->play);

        solo_setmixer(dev, 0, 0);
        PORT_WR8(dev->io, 0x7, 0); /* disable all irqs */
        return (0);
}

struct dev_ops solo_dev_ops = {
        DEVO_REV,               /* rev */
        0,                      /* refcnt */
        NULL,                   /* getinfo */
        nulldev,                /* identify */
        nulldev,                /* probe */
        solo_ddi_attach,        /* attach */
        solo_ddi_detach,        /* detach */
        nodev,                  /* reset */
        NULL,                   /* cb_ops */
        NULL,                   /* bus_ops */
        NULL,                   /* power */
        solo_quiesce,           /* quiesce */
};

static struct modldrv solo_modldrv = {
        &mod_driverops,                 /* drv_modops */
        "ESS Solo-1 Audio",             /* linkinfo */
        &solo_dev_ops,                  /* dev_ops */
};

static struct modlinkage modlinkage = {
        MODREV_1,
        { &solo_modldrv, NULL }
};

int
_init(void)
{
        int     rv;

        audio_init_ops(&solo_dev_ops, DRVNAME);
        if ((rv = mod_install(&modlinkage)) != 0) {
                audio_fini_ops(&solo_dev_ops);
        }
        return (rv);
}

int
_fini(void)
{
        int     rv;

        if ((rv = mod_remove(&modlinkage)) == 0) {
                audio_fini_ops(&solo_dev_ops);
        }
        return (rv);
}

int
_info(struct modinfo *modinfop)
{
        return (mod_info(&modlinkage, modinfop));
}