root/sys/dev/pci/azalia_codec.c
/*      $OpenBSD: azalia_codec.c,v 1.189 2022/09/08 01:35:39 jsg Exp $  */
/*      $NetBSD: azalia_codec.c,v 1.8 2006/05/10 11:17:27 kent Exp $    */

/*-
 * Copyright (c) 2005 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by TAMURA Kent
 *
 * 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 NETBSD FOUNDATION, INC. 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 FOUNDATION OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/param.h>
#include <sys/device.h>
#include <sys/malloc.h>
#include <sys/systm.h>
#include <dev/pci/azalia.h>
#include <dev/pci/pcireg.h>
#include <dev/pci/pcidevs.h>

#define XNAME(co)       (((struct device *)co->az)->dv_xname)
#define MIXER_DELTA(n)  (AUDIO_MAX_GAIN / (n))

int     azalia_add_convgroup(codec_t *, convgroupset_t *,
    struct io_pin *, int, nid_t *, int, uint32_t, uint32_t);

int     azalia_mixer_fix_indexes(codec_t *);
int     azalia_mixer_default(codec_t *);
int     azalia_mixer_ensure_capacity(codec_t *, size_t);
u_char  azalia_mixer_from_device_value(const codec_t *, nid_t, int, uint32_t );
uint32_t azalia_mixer_to_device_value(const codec_t *, nid_t, int, u_char);

void    azalia_devinfo_offon(mixer_devinfo_t *);
void    azalia_pin_config_ov(widget_t *, int, int);
void    azalia_ampcap_ov(widget_t *, int, int, int, int, int, int);
int     azalia_gpio_unmute(codec_t *, int);


int
azalia_codec_init_vtbl(codec_t *this)
{
        /**
         * We can refer this->vid and this->subid.
         */
        this->name = NULL;
        this->qrks = AZ_QRK_NONE;
        switch (this->vid) {
        case 0x10134206:
                this->name = "Cirrus Logic CS4206";
                if (this->subid == 0xcb8910de ||        /* APPLE_MBA3_1 */
                    this->subid == 0x72708086 ||        /* APPLE_MBA4_1 */
                    this->subid == 0xcb7910de) {        /* APPLE_MBP5_5 */
                        this->qrks |= AZ_QRK_GPIO_UNMUTE_1 |
                            AZ_QRK_GPIO_UNMUTE_3;
                }
                break;
        case 0x10134208:
                this->name = "Cirrus Logic CS4208";
                if (this->subid == 0x72708086) {        /* APPLE_MBA6_1 */
                        this->qrks |= AZ_QRK_GPIO_UNMUTE_0 |
                            AZ_QRK_GPIO_UNMUTE_1;
                }
                break;
        case 0x10ec0221:
                this->name = "Realtek ALC221";
                this->qrks |= AZ_QRK_WID_CDIN_1C | AZ_QRK_WID_BEEP_1D;
                break;
        case 0x10ec0225:
                this->name = "Realtek ALC225";
                break;
        case 0x10ec0233:
        case 0x10ec0235:
                this->name = "Realtek ALC233";
                break;
        case 0x10ec0236:
                if (PCI_VENDOR(this->subid) == PCI_VENDOR_DELL)
                        this->name = "Realtek ALC3204";
                else
                        this->name = "Realtek ALC236";
                break;
        case 0x10ec0245:
                this->name = "Realtek ALC245";
                break;
        case 0x10ec0255:
                this->name = "Realtek ALC255";
                break;
        case 0x10ec0256:
                this->name = "Realtek ALC256";
                break;
        case 0x10ec0257:
                this->name = "Realtek ALC257";
                break;
        case 0x10ec0260:
                this->name = "Realtek ALC260";
                if (this->subid == 0x008f1025)
                        this->qrks |= AZ_QRK_GPIO_UNMUTE_0;
                break;
        case 0x10ec0262:
                this->name = "Realtek ALC262";
                this->qrks |= AZ_QRK_WID_CDIN_1C | AZ_QRK_WID_BEEP_1D;
                break;
        case 0x10ec0268:
                this->name = "Realtek ALC268";
                this->qrks |= AZ_QRK_WID_CDIN_1C | AZ_QRK_WID_BEEP_1D;
                break;
        case 0x10ec0269:
                this->name = "Realtek ALC269";
                this->qrks |= AZ_QRK_WID_CDIN_1C | AZ_QRK_WID_BEEP_1D;

                /*
                 * Enable dock audio on Thinkpad docks
                 * 0x17aa : 0x21f3 = Thinkpad T430
                 * 0x17aa : 0x21f6 = Thinkpad T530
                 * 0x17aa : 0x21fa = Thinkpad X230
                 * 0x17aa : 0x21fb = Thinkpad T430s
                 * 0x17aa : 0x2203 = Thinkpad X230t
                 * 0x17aa : 0x2208 = Thinkpad T431s
                 */
                if (this->subid == 0x21f317aa ||
                    this->subid == 0x21f617aa ||
                    this->subid == 0x21fa17aa ||
                    this->subid == 0x21fb17aa ||
                    this->subid == 0x220317aa ||
                    this->subid == 0x220817aa)
                        this->qrks |= AZ_QRK_WID_TPDOCK1;
                break;
        case 0x10ec0270:
                this->name = "Realtek ALC270";
                break;
        case 0x10ec0272:
                this->name = "Realtek ALC272";
                break;
        case 0x10ec0275:
                this->name = "Realtek ALC275";
                break;
        case 0x10ec0280:
                this->name = "Realtek ALC280";
                break;
        case 0x10ec0282:
                this->name = "Realtek ALC282";
                this->qrks |= AZ_QRK_WID_CDIN_1C | AZ_QRK_WID_BEEP_1D;
                break;
        case 0x10ec0283:
                this->name = "Realtek ALC283";
                break;
        case 0x10ec0285:
                this->name = "Realtek ALC285";
                if (this->subid == 0x229217aa) {
                        /* Thinkpad X1 Carbon 7 */
                        this->qrks |= AZ_QRK_ROUTE_SPKR2_DAC |
                            AZ_QRK_WID_CLOSE_PCBEEP;
                 } else if (this->subid == 0x22c017aa) {
                        /* Thinkpad X1 Extreme 3 */
                        this->qrks |= AZ_QRK_DOLBY_ATMOS |
                            AZ_QRK_ROUTE_SPKR2_DAC;
                }
                break;
        case 0x10ec0287:
                this->name = "Realtek ALC287";
                break;
        case 0x10ec0292:
                this->name = "Realtek ALC292";
                this->qrks |= AZ_QRK_WID_CDIN_1C | AZ_QRK_WID_BEEP_1D;

                /*
                 * Enable dock audio on Thinkpad docks
                 * 0x17aa : 0x220c = Thinkpad T440s
                 * 0x17aa : 0x220e = Thinkpad T440p
                 * 0x17aa : 0x2210 = Thinkpad T540p
                 * 0x17aa : 0x2212 = Thinkpad T440
                 * 0x17aa : 0x2214 = Thinkpad X240
                 * 0x17aa : 0x2226 = Thinkpad X250
                 * 0x17aa : 0x501e = Thinkpad L440
                 * 0x17aa : 0x5034 = Thinkpad T450
                 * 0x17aa : 0x5036 = Thinkpad T450s
                 * 0x17aa : 0x503c = Thinkpad L450
                 */
                if (this->subid == 0x220c17aa ||
                    this->subid == 0x220e17aa ||
                    this->subid == 0x221017aa ||
                    this->subid == 0x221217aa ||
                    this->subid == 0x221417aa ||
                    this->subid == 0x222617aa ||
                    this->subid == 0x501e17aa ||
                    this->subid == 0x503417aa ||
                    this->subid == 0x503617aa ||
                    this->subid == 0x503c17aa)
                        this->qrks |= AZ_QRK_WID_TPDOCK2;
                break;
        case 0x10ec0293:
                if (PCI_VENDOR(this->subid) == PCI_VENDOR_DELL)
                        this->name = "Realtek ALC3235";
                else
                        this->name = "Realtek ALC293";
                break;
        case 0x10ec0294:
                this->name = "Realtek ALC294";
                break;
        case 0x10ec0295:
                if (PCI_VENDOR(this->subid) == PCI_VENDOR_DELL)
                        this->name = "Realtek ALC3254";
                else
                        this->name = "Realtek ALC295";
                break;
        case 0x10ec0298:
                this->name = "Realtek ALC298";
                if (this->subid == 0x320019e5 ||
                    this->subid == 0x320119e5)          /* Huawei Matebook X */
                        this->qrks |= AZ_QRK_DOLBY_ATMOS;
                break;
        case 0x10ec0299:
                this->name = "Realtek ALC299";
                break;
        case 0x10ec0660:
                this->name = "Realtek ALC660";
                if (this->subid == 0x13391043) {        /* ASUS_G2K */
                        this->qrks |= AZ_QRK_GPIO_UNMUTE_0;
                }
                break;
        case 0x10ec0662:
                this->name = "Realtek ALC662";
                this->qrks |= AZ_QRK_WID_CDIN_1C | AZ_QRK_WID_BEEP_1D;
                break;
        case 0x10ec0663:
                this->name = "Realtek ALC663";
                break;
        case 0x10ec0668:
                if (PCI_VENDOR(this->subid) == PCI_VENDOR_DELL)
                        this->name = "Realtek ALC3661";
                else
                        this->name = "Realtek ALC668";
                break;
        case 0x10ec0671:
                this->name = "Realtek ALC671";
                break;
        case 0x10ec0700:
                this->name = "Realtek ALC700";
                break;
        case 0x10ec0861:
                this->name = "Realtek ALC861";
                break;
        case 0x10ec0880:
                this->name = "Realtek ALC880";
                this->qrks |= AZ_QRK_WID_CDIN_1C | AZ_QRK_WID_BEEP_1D;
                if (this->subid == 0x19931043 ||        /* ASUS_M5200 */
                    this->subid == 0x13231043) {        /* ASUS_A7M */
                        this->qrks |= AZ_QRK_GPIO_UNMUTE_0;
                }
                if (this->subid == 0x203d161f) {        /* MEDION_MD95257 */
                        this->qrks |= AZ_QRK_GPIO_UNMUTE_1;
                }
                break;
        case 0x10ec0882:
                this->name = "Realtek ALC882";
                this->qrks |= AZ_QRK_WID_CDIN_1C | AZ_QRK_WID_BEEP_1D;
                if (this->subid == 0x13c21043 ||        /* ASUS_A7T */
                    this->subid == 0x19711043) {        /* ASUS_W2J */
                        this->qrks |= AZ_QRK_GPIO_UNMUTE_0;
                }
                break;
        case 0x10ec0883:
                this->name = "Realtek ALC883";
                this->qrks |= AZ_QRK_WID_CDIN_1C | AZ_QRK_WID_BEEP_1D;
                if (this->subid == 0x00981025) {        /* ACER_ID */
                        this->qrks |= AZ_QRK_GPIO_UNMUTE_0 |
                            AZ_QRK_GPIO_UNMUTE_1;
                }
                break;
        case 0x10ec0885:
                this->name = "Realtek ALC885";
                this->qrks |= AZ_QRK_WID_CDIN_1C | AZ_QRK_WID_BEEP_1D;
                if (this->subid == 0x00a1106b ||        /* APPLE_MB3 */
                    this->subid == 0xcb7910de ||        /* APPLE_MACMINI3_1 (line-in + hp) */
                    this->subid == 0x00a0106b ||        /* APPLE_MB3_1 */
                    this->subid == 0x00a3106b) {        /* APPLE_MB4 */
                        this->qrks |= AZ_QRK_GPIO_UNMUTE_0;
                }
                if (this->subid == 0x00a1106b ||
                    this->subid == 0xcb7910de ||        /* APPLE_MACMINI3_1 (internal spkr) */
                    this->subid == 0x00a0106b)
                        this->qrks |= AZ_QRK_WID_OVREF50;
                break;
        case 0x10ec0887:
                this->name = "Realtek ALC887";
                break;
        case 0x10ec0888:
                this->name = "Realtek ALC888";
                this->qrks |= AZ_QRK_WID_CDIN_1C | AZ_QRK_WID_BEEP_1D;
                break;
        case 0x10ec0889:
                this->name = "Realtek ALC889";
                break;
        case 0x10ec0892:
                this->name = "Realtek ALC892";
                break;
        case 0x10ec0897:
                this->name = "Realtek ALC897";
                break;
        case 0x10ec0900:
                this->name = "Realtek ALC1150";
                break;
        case 0x10ec0b00:
                this->name = "Realtek ALC1200";
                break;
        case 0x10ec1168:
        case 0x10ec1220:
                this->name = "Realtek ALC1220";
                break;
        case 0x11060398:
        case 0x11061398:
        case 0x11062398:
        case 0x11063398:
        case 0x11064398:
        case 0x11065398:
        case 0x11066398:
        case 0x11067398:
                this->name = "VIA VT1702";
                break;
        case 0x111d7603:
                this->name = "IDT 92HD75B3/4";
                if (PCI_VENDOR(this->subid) == PCI_VENDOR_HP)
                        this->qrks |= AZ_QRK_GPIO_UNMUTE_0;
                break;
        case 0x111d7604:
                this->name = "IDT 92HD83C1X";
                break;
        case 0x111d7605:
                this->name = "IDT 92HD81B1X";
                break;
        case 0x111d7608:
                this->name = "IDT 92HD75B1/2";
                if (PCI_VENDOR(this->subid) == PCI_VENDOR_HP)
                        this->qrks |= AZ_QRK_GPIO_UNMUTE_0;
                break;
        case 0x111d7674:
                this->name = "IDT 92HD73D1";
                break;
        case 0x111d7675:
                this->name = "IDT 92HD73C1";    /* aka 92HDW74C1 */
                if (PCI_VENDOR(this->subid) == PCI_VENDOR_DELL)
                        this->qrks |= AZ_QRK_GPIO_UNMUTE_0;
                break;
        case 0x111d7676:
                this->name = "IDT 92HD73E1";    /* aka 92HDW74E1 */
                break;
        case 0x111d7695:
                this->name = "IDT 92HD95";      /* aka IDT/TSI 92HD95B */
                break;
        case 0x111d76b0:
                this->name = "IDT 92HD71B8";
                break;
        case 0x111d76b2:
                this->name = "IDT 92HD71B7";
                if (PCI_VENDOR(this->subid) == PCI_VENDOR_DELL ||
                    PCI_VENDOR(this->subid) == PCI_VENDOR_HP)
                        this->qrks |= AZ_QRK_GPIO_UNMUTE_0;
                break;
        case 0x111d76b6:
                this->name = "IDT 92HD71B5";
                break;
        case 0x111d76d4:
                this->name = "IDT 92HD83C1C";
                break;
        case 0x111d76d5:
                this->name = "IDT 92HD81B1C";
                break;
        case 0x11d4184a:
                this->name = "Analog Devices AD1884A";
                break;
        case 0x11d41882:
                this->name = "Analog Devices AD1882";
                break;
        case 0x11d41883:
                this->name = "Analog Devices AD1883";
                break;
        case 0x11d41884:
                this->name = "Analog Devices AD1884";
                break;
        case 0x11d4194a:
                this->name = "Analog Devices AD1984A";
                break;
        case 0x11d41981:
                this->name = "Analog Devices AD1981HD";
                this->qrks |= AZ_QRK_WID_AD1981_OAMP;
                break;
        case 0x11d41983:
                this->name = "Analog Devices AD1983";
                break;
        case 0x11d41984:
                this->name = "Analog Devices AD1984";
                break;
        case 0x11d41988:
                this->name = "Analog Devices AD1988A";
                break;
        case 0x11d4198b:
                this->name = "Analog Devices AD1988B";
                break;
        case 0x11d4882a:
                this->name = "Analog Devices AD1882A";
                break;
        case 0x11d4989a:
                this->name = "Analog Devices AD1989A";
                break;
        case 0x11d4989b:
                this->name = "Analog Devices AD1989B";
                break;
        case 0x14f15045:
                this->name = "Conexant CX20549";  /* Venice */
                break;
        case 0x14f15047:
                this->name = "Conexant CX20551";  /* Waikiki */
                break;
        case 0x14f15051:
                this->name = "Conexant CX20561";  /* Hermosa */
                break;
        case 0x14f1506e:
                this->name = "Conexant CX20590";
                /*
                 * Enable dock audio on Thinkpad docks
                 * 0x17aa : 0x20f2 = Thinkpad T400
                 * 0x17aa : 0x215e = Thinkpad T410
                 * 0x17aa : 0x215f = Thinkpad T510
                 * 0x17aa : 0x21ce = Thinkpad T420
                 * 0x17aa : 0x21cf = Thinkpad T520
                 * 0x17aa : 0x21da = Thinkpad X220
                 * 0x17aa : 0x21db = Thinkpad X220t
                 */
                if (this->subid == 0x20f217aa ||
                    this->subid == 0x215e17aa ||
                    this->subid == 0x215f17aa ||
                    this->subid == 0x21ce17aa ||
                    this->subid == 0x21cf17aa ||
                    this->subid == 0x21da17aa ||
                    this->subid == 0x21db17aa)
                        this->qrks |= AZ_QRK_WID_TPDOCK3;
                break;
        case 0x434d4980:
                this->name = "CMedia CMI9880";
                break;
        case 0x83847612:
                this->name = "Sigmatel STAC9230X";
                break;
        case 0x83847613:
                this->name = "Sigmatel STAC9230D";
                break;
        case 0x83847614:
                this->name = "Sigmatel STAC9229X";
                break;
        case 0x83847615:
                this->name = "Sigmatel STAC9229D";
                break;
        case 0x83847616:
                this->name = "Sigmatel STAC9228X";
                if (this->subid == 0x02271028 ||        /* DELL_V1400 */
                    this->subid == 0x01f31028) {        /* DELL_I1400 */
                        this->qrks |= AZ_QRK_GPIO_UNMUTE_2;
                }
                break;
        case 0x83847617:
                this->name = "Sigmatel STAC9228D";
                break;
        case 0x83847618:
                this->name = "Sigmatel STAC9227X";
                break;
        case 0x83847619:
                this->name = "Sigmatel STAC9227D";
                break;
        case 0x83847620:
                this->name = "Sigmatel STAC9274";
                break;
        case 0x83847621:
                this->name = "Sigmatel STAC9274D";
                break;
        case 0x83847626:
                this->name = "Sigmatel STAC9271X";
                break;
        case 0x83847627:
                this->name = "Sigmatel STAC9271D";
                break;
        case 0x83847632:
                this->name = "Sigmatel STAC9202";
                break;
        case 0x83847634:
                this->name = "Sigmatel STAC9250";
                break;
        case 0x83847636:
                this->name = "Sigmatel STAC9251";
                break;
        case 0x83847638:
                this->name = "IDT 92HD700X";
                break;
        case 0x83847639:
                this->name = "IDT 92HD700D";
                break;
        case 0x83847645:
                this->name = "IDT 92HD206X";
                break;
        case 0x83847646:
                this->name = "IDT 92HD206D";
                break;
        case 0x83847661:
                /* FALLTHROUGH */
        case 0x83847662:
                this->name = "Sigmatel STAC9225";
                break;
        case 0x83847680:
                this->name = "Sigmatel STAC9220/1";
                if (this->subid == 0x76808384) {        /* APPLE_ID */
                        this->qrks |= AZ_QRK_GPIO_POL_0 | AZ_QRK_GPIO_UNMUTE_0 |
                             AZ_QRK_GPIO_UNMUTE_1;
                }
                break;
        case 0x83847682:
                /* FALLTHROUGH */
        case 0x83847683:
                this->name = "Sigmatel STAC9221D";      /* aka IDT 92HD202 */
                break;
        case 0x83847690:
                this->name = "Sigmatel STAC9200";       /* aka IDT 92HD001 */
                break;
        case 0x83847691:
                this->name = "Sigmatel STAC9200D";
                break;
        case 0x83847698:
                this->name = "IDT 92HD005";
                break;
        case 0x83847699:
                this->name = "IDT 92HD005D";
                break;
        case 0x838476a0:
                this->name = "Sigmatel STAC9205X";
                if (this->subid == 0x01f91028 ||        /* DELL_D630 */
                    this->subid == 0x02281028) {        /* DELL_V1500 */
                        this->qrks |= AZ_QRK_GPIO_UNMUTE_0;
                }
                break;
        case 0x838476a1:
                this->name = "Sigmatel STAC9205D";
                break;
        case 0x838476a2:
                this->name = "Sigmatel STAC9204X";
                break;
        case 0x838476a3:
                this->name = "Sigmatel STAC9204D";
                break;
        }
        return 0;
}

/* ----------------------------------------------------------------
 * functions for generic codecs
 * ---------------------------------------------------------------- */

int
azalia_widget_enabled(const codec_t *this, nid_t nid)
{
        if (!VALID_WIDGET_NID(nid, this) || !this->w[nid].enable)
                return 0;
        return 1;
}

int
azalia_init_dacgroup(codec_t *this)
{
        this->dacs.ngroups = 0;
        if (this->na_dacs > 0)
                azalia_add_convgroup(this, &this->dacs,
                    this->opins, this->nopins,
                    this->a_dacs, this->na_dacs,
                    COP_AWTYPE_AUDIO_OUTPUT, 0);
        if (this->na_dacs_d > 0)
                azalia_add_convgroup(this, &this->dacs,
                    this->opins_d, this->nopins_d,
                    this->a_dacs_d, this->na_dacs_d,
                    COP_AWTYPE_AUDIO_OUTPUT, COP_AWCAP_DIGITAL);
        this->dacs.cur = 0;

        this->adcs.ngroups = 0;
        if (this->na_adcs > 0)
                azalia_add_convgroup(this, &this->adcs,
                    this->ipins, this->nipins,
                    this->a_adcs, this->na_adcs,
                    COP_AWTYPE_AUDIO_INPUT, 0);
        if (this->na_adcs_d > 0)
                azalia_add_convgroup(this, &this->adcs,
                    this->ipins_d, this->nipins_d,
                    this->a_adcs_d, this->na_adcs_d,
                    COP_AWTYPE_AUDIO_INPUT, COP_AWCAP_DIGITAL);
        this->adcs.cur = 0;

        return 0;
}

int
azalia_add_convgroup(codec_t *this, convgroupset_t *group,
    struct io_pin *pins, int npins, nid_t *all_convs, int nall_convs,
    uint32_t type, uint32_t digital)
{
        nid_t convs[HDA_MAX_CHANNELS];
        int nconvs;
        nid_t conv;
        int i, j, k;

        nconvs = 0;

        /* default pin connections */
        for (i = 0; i < npins; i++) {
                conv = pins[i].conv;
                if (conv < 0)
                        continue;
                for (j = 0; j < nconvs; j++) {
                        if (convs[j] == conv)
                                break;
                }
                if (j < nconvs)
                        continue;
                convs[nconvs++] = conv;
                if (nconvs >= nall_convs) {
                        goto done;
                }
        }
        /* non-default connections */
        for (i = 0; i < npins; i++) {
                for (j = 0; j < nall_convs; j++) {
                        conv = all_convs[j];
                        for (k = 0; k < nconvs; k++) {
                                if (convs[k] == conv)
                                        break;
                        }
                        if (k < nconvs)
                                continue;
                        if (type == COP_AWTYPE_AUDIO_OUTPUT) {
                                k = azalia_codec_fnode(this, conv,
                                    pins[i].nid, 0);
                                if (k < 0)
                                        continue;
                        } else {
                                if (!azalia_widget_enabled(this, conv))
                                        continue;
                                k = azalia_codec_fnode(this, pins[i].nid,
                                    conv, 0);
                                if (k < 0)
                                        continue;
                        }
                        convs[nconvs++] = conv;
                        if (nconvs >= nall_convs) {
                                goto done;
                        }
                }
        }
        /* Make sure the speaker dac is part of the analog output convgroup
         * or it won't get connected by azalia_codec_connect_stream().
         */
        if (type == COP_AWTYPE_AUDIO_OUTPUT && !digital &&
            nconvs < nall_convs && this->spkr_dac != -1) {
                for (i = 0; i < nconvs; i++)
                        if (convs[i] == this->spkr_dac)
                                break;
                if (i == nconvs)
                        convs[nconvs++] = this->spkr_dac;
        }
done:
        for (i = 0; i < nconvs; i++)
                group->groups[group->ngroups].conv[i] = convs[i];
        if (nconvs > 0) {
                group->groups[group->ngroups].nconv = i;
                group->ngroups++;
        }

        /* Disable converters that aren't in a convgroup. */
        for (i = 0; i < nall_convs; i++) {
                conv = all_convs[i];
                for (j = 0; j < nconvs; j++)
                        if (convs[j] == conv)
                                break;
                if (j == nconvs)
                        this->w[conv].enable = 0;
        }

        return 0;
}

int
azalia_codec_fnode(codec_t *this, nid_t node, int index, int depth)
{
        const widget_t *w;
        int i, ret;

        w = &this->w[index];
        if (w->nid == node) {
                return index;
        }
        /* back at the beginning or a bad end */
        if (depth > 0 &&
            (w->type == COP_AWTYPE_PIN_COMPLEX ||
            w->type == COP_AWTYPE_BEEP_GENERATOR ||
            w->type == COP_AWTYPE_AUDIO_OUTPUT ||
            w->type == COP_AWTYPE_AUDIO_INPUT))
                return -1;
        if (++depth >= 10)
                return -1;
        for (i = 0; i < w->nconnections; i++) {
                if (!azalia_widget_enabled(this, w->connections[i]))
                        continue;
                ret = azalia_codec_fnode(this, node, w->connections[i], depth);
                if (ret >= 0)
                        return ret;
        }
        return -1;
}

int
azalia_unsol_event(codec_t *this, int tag)
{
        mixer_ctrl_t mc;
        uint32_t result;
        int i, err, vol, vol2;

        err = 0;
        tag = CORB_UNSOL_TAG(tag);
        switch (tag) {
        case AZ_TAG_SPKR:
                mc.type = AUDIO_MIXER_ENUM;
                vol = 0;
                for (i = 0; !vol && !err && i < this->nsense_pins; i++) {
                        if (!(this->spkr_muters & (1 << i)))
                                continue;
                        err = azalia_comresp(this, this->sense_pins[i],
                            CORB_GET_PIN_WIDGET_CONTROL, 0, &result);
                        if (err || !(result & CORB_PWC_OUTPUT))
                                continue;
                        err = azalia_comresp(this, this->sense_pins[i],
                            CORB_GET_PIN_SENSE, 0, &result);
                        if (!err && (result & CORB_PS_PRESENCE))
                                vol = 1;
                }
                if (err)
                        break;
                this->spkr_muted = vol;
                switch(this->spkr_mute_method) {
                case AZ_SPKR_MUTE_SPKR_MUTE:
                        mc.un.ord = vol;
                        err = azalia_mixer_set(this, this->speaker,
                            MI_TARGET_OUTAMP, &mc);
                        if (!err && this->speaker2 != -1 &&
                            (this->w[this->speaker2].widgetcap & COP_AWCAP_OUTAMP) &&
                            (this->w[this->speaker2].outamp_cap & COP_AMPCAP_MUTE))
                                err = azalia_mixer_set(this, this->speaker2,
                                    MI_TARGET_OUTAMP, &mc);
                        break;
                case AZ_SPKR_MUTE_SPKR_DIR:
                        mc.un.ord = vol ? 0 : 1;
                        err = azalia_mixer_set(this, this->speaker,
                            MI_TARGET_PINDIR, &mc);
                        if (!err && this->speaker2 != -1 &&
                            (this->w[this->speaker2].d.pin.cap & COP_PINCAP_OUTPUT) &&
                            (this->w[this->speaker2].d.pin.cap & COP_PINCAP_INPUT))
                                err = azalia_mixer_set(this, this->speaker2,
                                    MI_TARGET_PINDIR, &mc);
                        break;
                case AZ_SPKR_MUTE_DAC_MUTE:
                        mc.un.ord = vol;
                        err = azalia_mixer_set(this, this->spkr_dac,
                            MI_TARGET_OUTAMP, &mc);
                        break;
                }
                break;

        case AZ_TAG_PLAYVOL:
                if (this->playvols.master == this->audiofunc)
                        return EINVAL;
                err = azalia_comresp(this, this->playvols.master,
                    CORB_GET_VOLUME_KNOB, 0, &result);
                if (err)
                        return err;

                vol = CORB_VKNOB_VOLUME(result) - this->playvols.hw_step;
                vol2 = vol * (AUDIO_MAX_GAIN / this->playvols.hw_nsteps);
                this->playvols.hw_step = CORB_VKNOB_VOLUME(result);

                vol = vol2 + this->playvols.vol_l;
                if (vol < 0)
                        vol = 0;
                else if (vol > AUDIO_MAX_GAIN)
                        vol = AUDIO_MAX_GAIN;
                this->playvols.vol_l = vol;

                vol = vol2 + this->playvols.vol_r;
                if (vol < 0)
                        vol = 0;
                else if (vol > AUDIO_MAX_GAIN)
                        vol = AUDIO_MAX_GAIN;
                this->playvols.vol_r = vol;

                mc.type = AUDIO_MIXER_VALUE;
                mc.un.value.num_channels = 2;
                mc.un.value.level[0] = this->playvols.vol_l;
                mc.un.value.level[1] = this->playvols.vol_r;
                err = azalia_mixer_set(this, this->playvols.master,
                    MI_TARGET_PLAYVOL, &mc);
                break;

        default:
                DPRINTF(("%s: unknown tag %d\n", __func__, tag));
                break;
        }

        return err;
}


/* ----------------------------------------------------------------
 * Generic mixer functions
 * ---------------------------------------------------------------- */

int
azalia_mixer_init(codec_t *this)
{
        /*
         * pin          "<color>%2.2x"
         * audio output "dac%2.2x"
         * audio input  "adc%2.2x"
         * mixer        "mixer%2.2x"
         * selector     "sel%2.2x"
         */
        const widget_t *w, *ww;
        mixer_item_t *m;
        int err, i, j, k, bits;

        this->maxmixers = 10;
        this->nmixers = 0;
        this->mixers = mallocarray(this->maxmixers, sizeof(mixer_item_t),
            M_DEVBUF, M_NOWAIT | M_ZERO);
        if (this->mixers == NULL) {
                printf("%s: out of memory in %s\n", XNAME(this), __func__);
                return ENOMEM;
        }

        /* register classes */
        m = &this->mixers[AZ_CLASS_INPUT];
        m->devinfo.index = AZ_CLASS_INPUT;
        strlcpy(m->devinfo.label.name, AudioCinputs,
            sizeof(m->devinfo.label.name));
        m->devinfo.type = AUDIO_MIXER_CLASS;
        m->devinfo.mixer_class = AZ_CLASS_INPUT;
        m->devinfo.next = AUDIO_MIXER_LAST;
        m->devinfo.prev = AUDIO_MIXER_LAST;
        m->nid = 0;

        m = &this->mixers[AZ_CLASS_OUTPUT];
        m->devinfo.index = AZ_CLASS_OUTPUT;
        strlcpy(m->devinfo.label.name, AudioCoutputs,
            sizeof(m->devinfo.label.name));
        m->devinfo.type = AUDIO_MIXER_CLASS;
        m->devinfo.mixer_class = AZ_CLASS_OUTPUT;
        m->devinfo.next = AUDIO_MIXER_LAST;
        m->devinfo.prev = AUDIO_MIXER_LAST;
        m->nid = 0;

        m = &this->mixers[AZ_CLASS_RECORD];
        m->devinfo.index = AZ_CLASS_RECORD;
        strlcpy(m->devinfo.label.name, AudioCrecord,
            sizeof(m->devinfo.label.name));
        m->devinfo.type = AUDIO_MIXER_CLASS;
        m->devinfo.mixer_class = AZ_CLASS_RECORD;
        m->devinfo.next = AUDIO_MIXER_LAST;
        m->devinfo.prev = AUDIO_MIXER_LAST;
        m->nid = 0;

        this->nmixers = AZ_CLASS_RECORD + 1;

#define MIXER_REG_PROLOG        \
        mixer_devinfo_t *d; \
        err = azalia_mixer_ensure_capacity(this, this->nmixers + 1); \
        if (err) \
                return err; \
        m = &this->mixers[this->nmixers]; \
        d = &m->devinfo; \
        m->nid = i

        FOR_EACH_WIDGET(this, i) {

                w = &this->w[i];
                if (!w->enable)
                        continue;

                /* selector */
                if (w->nconnections > 0 && w->type != COP_AWTYPE_AUDIO_MIXER &&
                    !(w->nconnections == 1 &&
                    azalia_widget_enabled(this, w->connections[0]) &&
                    strcmp(w->name, this->w[w->connections[0]].name) == 0) &&
                    w->nid != this->mic) {
                        MIXER_REG_PROLOG;
                        snprintf(d->label.name, sizeof(d->label.name),
                            "%s_source", w->name);
                        d->type = AUDIO_MIXER_ENUM;
                        if (w->mixer_class >= 0)
                                d->mixer_class = w->mixer_class;
                        else {
                                if (w->type == COP_AWTYPE_AUDIO_SELECTOR)
                                        d->mixer_class = AZ_CLASS_INPUT;
                                else
                                        d->mixer_class = AZ_CLASS_OUTPUT;
                        }
                        m->target = MI_TARGET_CONNLIST;
                        for (j = 0, k = 0; j < w->nconnections && k < 32; j++) {
                                if (!azalia_widget_enabled(this,
                                    w->connections[j]))
                                        continue;
                                d->un.e.member[k].ord = j;
                                strlcpy(d->un.e.member[k].label.name,
                                    this->w[w->connections[j]].name,
                                    MAX_AUDIO_DEV_LEN);
                                k++;
                        }
                        d->un.e.num_mem = k;
                        this->nmixers++;
                }

                /* output mute */
                if (w->widgetcap & COP_AWCAP_OUTAMP &&
                    w->outamp_cap & COP_AMPCAP_MUTE &&
                    w->nid != this->mic) {
                        MIXER_REG_PROLOG;
                        snprintf(d->label.name, sizeof(d->label.name),
                            "%s_mute", w->name);
                        if (w->mixer_class >= 0)
                                d->mixer_class = w->mixer_class;
                        else {
                                if (w->type == COP_AWTYPE_AUDIO_MIXER ||
                                    w->type == COP_AWTYPE_AUDIO_SELECTOR ||
                                    w->type == COP_AWTYPE_PIN_COMPLEX)
                                        d->mixer_class = AZ_CLASS_OUTPUT;
                                else
                                        d->mixer_class = AZ_CLASS_INPUT;
                        }
                        m->target = MI_TARGET_OUTAMP;
                        azalia_devinfo_offon(d);
                        this->nmixers++;
                }

                /* output gain */
                if (w->widgetcap & COP_AWCAP_OUTAMP &&
                    COP_AMPCAP_NUMSTEPS(w->outamp_cap) &&
                    w->nid != this->mic) {
                        MIXER_REG_PROLOG;
                        snprintf(d->label.name, sizeof(d->label.name),
                            "%s", w->name);
                        d->type = AUDIO_MIXER_VALUE;
                        if (w->mixer_class >= 0)
                                d->mixer_class = w->mixer_class;
                        else {
                                if (w->type == COP_AWTYPE_AUDIO_MIXER ||
                                    w->type == COP_AWTYPE_AUDIO_SELECTOR ||
                                    w->type == COP_AWTYPE_PIN_COMPLEX)
                                        d->mixer_class = AZ_CLASS_OUTPUT;
                                else
                                        d->mixer_class = AZ_CLASS_INPUT;
                        }
                        m->target = MI_TARGET_OUTAMP;
                        d->un.v.num_channels = WIDGET_CHANNELS(w);
                        d->un.v.units.name[0] = 0;
                        d->un.v.delta =
                            MIXER_DELTA(COP_AMPCAP_NUMSTEPS(w->outamp_cap));
                        this->nmixers++;
                }

                /* input mute */
                if (w->widgetcap & COP_AWCAP_INAMP &&
                    w->inamp_cap & COP_AMPCAP_MUTE &&
                    w->nid != this->speaker &&
                    w->nid != this->speaker2) {
                        if (w->type != COP_AWTYPE_AUDIO_MIXER) {
                                MIXER_REG_PROLOG;
                                snprintf(d->label.name, sizeof(d->label.name),
                                    "%s_mute", w->name);
                                if (w->mixer_class >= 0)
                                        d->mixer_class = w->mixer_class;
                                else
                                        d->mixer_class = AZ_CLASS_INPUT;
                                m->target = 0;
                                azalia_devinfo_offon(d);
                                this->nmixers++;
                        } else {
                                MIXER_REG_PROLOG;
                                snprintf(d->label.name, sizeof(d->label.name),
                                    "%s_source", w->name);
                                m->target = MI_TARGET_MUTESET;
                                d->type = AUDIO_MIXER_SET;
                                if (w->mixer_class >= 0)
                                        d->mixer_class = w->mixer_class;
                                else
                                        d->mixer_class = AZ_CLASS_INPUT;
                                for (j = 0, k = 0;
                                    j < w->nconnections && k < 32; j++) {
                                        if (!azalia_widget_enabled(this,
                                            w->connections[j]))
                                                continue;
                                        if (w->connections[j] == this->speaker ||
                                            w->connections[j] == this->speaker2)
                                                continue;
                                        d->un.s.member[k].mask = 1 << j;
                                        strlcpy(d->un.s.member[k].label.name,
                                            this->w[w->connections[j]].name,
                                            MAX_AUDIO_DEV_LEN);
                                        k++;
                                }
                                d->un.s.num_mem = k;
                                if (k != 0)
                                        this->nmixers++;
                        }
                }

                /* input gain */
                if (w->widgetcap & COP_AWCAP_INAMP &&
                    COP_AMPCAP_NUMSTEPS(w->inamp_cap) &&
                    w->nid != this->speaker &&
                    w->nid != this->speaker2) {
                        if (w->type != COP_AWTYPE_AUDIO_SELECTOR &&
                            w->type != COP_AWTYPE_AUDIO_MIXER) {
                                MIXER_REG_PROLOG;
                                snprintf(d->label.name, sizeof(d->label.name),
                                    "%s", w->name);
                                d->type = AUDIO_MIXER_VALUE;
                                if (w->mixer_class >= 0)
                                        d->mixer_class = w->mixer_class;
                                else
                                        d->mixer_class = AZ_CLASS_INPUT;
                                m->target = 0;
                                d->un.v.num_channels = WIDGET_CHANNELS(w);
                                d->un.v.units.name[0] = 0;
                                d->un.v.delta =
                                    MIXER_DELTA(COP_AMPCAP_NUMSTEPS(w->inamp_cap));
                                this->nmixers++;
                        } else {
                                for (j = 0; j < w->nconnections; j++) {
                                        if (!azalia_widget_enabled(this,
                                            w->connections[j]))
                                                continue;
                                        if (w->connections[j] == this->speaker ||
                                            w->connections[j] == this->speaker2)
                                                continue;
                                        MIXER_REG_PROLOG;
                                        snprintf(d->label.name,
                                            sizeof(d->label.name), "%s_%s",
                                            w->name,
                                            this->w[w->connections[j]].name);
                                        d->type = AUDIO_MIXER_VALUE;
                                        if (w->mixer_class >= 0)
                                                d->mixer_class = w->mixer_class;
                                        else
                                                d->mixer_class = AZ_CLASS_INPUT;
                                        m->target = j;
                                        d->un.v.num_channels = WIDGET_CHANNELS(w);
                                        d->un.v.units.name[0] = 0;
                                        d->un.v.delta =
                                            MIXER_DELTA(COP_AMPCAP_NUMSTEPS(w->inamp_cap));
                                        this->nmixers++;
                                }
                        }
                }

                /* hardcoded mixer inputs */
                if (w->type == COP_AWTYPE_AUDIO_MIXER &&
                    !(w->widgetcap & COP_AWCAP_INAMP)) {
                        MIXER_REG_PROLOG;
                        snprintf(d->label.name, sizeof(d->label.name),
                            "%s_source", w->name);
                        m->target = MI_TARGET_MIXERSET;
                        d->type = AUDIO_MIXER_SET;
                        if (w->mixer_class >= 0)
                                d->mixer_class = w->mixer_class;
                        else
                                d->mixer_class = AZ_CLASS_INPUT;
                        for (j = 0, k = 0;
                            j < w->nconnections && k < 32; j++) {
                                if (!azalia_widget_enabled(this,
                                    w->connections[j]))
                                        continue;
                                if (w->connections[j] == this->speaker ||
                                    w->connections[j] == this->speaker2)
                                        continue;
                                d->un.s.member[k].mask = 1 << j;
                                strlcpy(d->un.s.member[k].label.name,
                                    this->w[w->connections[j]].name,
                                    MAX_AUDIO_DEV_LEN);
                                k++;
                        }
                        d->un.s.num_mem = k;
                        if (k != 0)
                                this->nmixers++;
                }

                /* pin direction */
                if (w->type == COP_AWTYPE_PIN_COMPLEX &&
                    ((w->d.pin.cap & COP_PINCAP_OUTPUT &&
                    w->d.pin.cap & COP_PINCAP_INPUT) ||
                    COP_PINCAP_VREF(w->d.pin.cap) > 1)) {

                        MIXER_REG_PROLOG;
                        snprintf(d->label.name, sizeof(d->label.name),
                            "%s_dir", w->name);
                        d->type = AUDIO_MIXER_ENUM;
                        d->mixer_class = AZ_CLASS_OUTPUT;
                        m->target = MI_TARGET_PINDIR;

                        k = 0;
                        d->un.e.member[k].ord = 0;
                        strlcpy(d->un.e.member[k].label.name, "none",
                            MAX_AUDIO_DEV_LEN);
                        k++;

                        if (w->d.pin.cap & COP_PINCAP_OUTPUT) {
                                d->un.e.member[k].ord = 1;
                                strlcpy(d->un.e.member[k].label.name,
                                    AudioNoutput, MAX_AUDIO_DEV_LEN);
                                k++;
                        }

                        if (w->d.pin.cap & COP_PINCAP_INPUT) {
                                d->un.e.member[k].ord = 2;
                                strlcpy(d->un.e.member[k].label.name,
                                    AudioNinput, MAX_AUDIO_DEV_LEN);
                                k++;

                                for (j = 0; j < 4; j++) {
                                        if (j == 0) {
                                                bits = (1 << CORB_PWC_VREF_GND);
                                                strlcpy(d->un.e.member[k].label.name,
                                                    AudioNinput "-vr0",
                                                    MAX_AUDIO_DEV_LEN);
                                        } else if (j == 1) {
                                                bits = (1 << CORB_PWC_VREF_50);
                                                strlcpy(d->un.e.member[k].label.name,
                                                    AudioNinput "-vr50",
                                                    MAX_AUDIO_DEV_LEN);
                                        } else if (j == 2) {
                                                bits = (1 << CORB_PWC_VREF_80);
                                                strlcpy(d->un.e.member[k].label.name,
                                                    AudioNinput "-vr80",
                                                    MAX_AUDIO_DEV_LEN);
                                        } else if (j == 3) {
                                                bits = (1 << CORB_PWC_VREF_100);
                                                strlcpy(d->un.e.member[k].label.name,
                                                    AudioNinput "-vr100",
                                                    MAX_AUDIO_DEV_LEN);
                                        }
                                        if ((COP_PINCAP_VREF(w->d.pin.cap) &
                                            bits) == bits) {
                                                d->un.e.member[k].ord = j + 3;
                                                k++;
                                        }
                                }
                        }
                        d->un.e.num_mem = k;
                        this->nmixers++;
                }

                /* pin headphone-boost */
                if (w->type == COP_AWTYPE_PIN_COMPLEX &&
                    w->d.pin.cap & COP_PINCAP_HEADPHONE &&
                    w->nid != this->mic) {
                        MIXER_REG_PROLOG;
                        snprintf(d->label.name, sizeof(d->label.name),
                            "%s_boost", w->name);
                        d->mixer_class = AZ_CLASS_OUTPUT;
                        m->target = MI_TARGET_PINBOOST;
                        azalia_devinfo_offon(d);
                        this->nmixers++;
                }

                if (w->type == COP_AWTYPE_PIN_COMPLEX &&
                    w->d.pin.cap & COP_PINCAP_EAPD) {
                        MIXER_REG_PROLOG;
                        snprintf(d->label.name, sizeof(d->label.name),
                            "%s_eapd", w->name);
                        d->mixer_class = AZ_CLASS_OUTPUT;
                        m->target = MI_TARGET_EAPD;
                        azalia_devinfo_offon(d);
                        this->nmixers++;
                }
        }

        /* sense pins */
        for (i = 0; i < this->nsense_pins; i++) {
                if (!azalia_widget_enabled(this, this->sense_pins[i])) {
                        DPRINTF(("%s: sense pin %2.2x not found\n",
                            __func__, this->sense_pins[i]));
                        continue;
                }

                MIXER_REG_PROLOG;
                m->nid = this->w[this->sense_pins[i]].nid;
                snprintf(d->label.name, sizeof(d->label.name), "%s_sense",
                    this->w[this->sense_pins[i]].name);
                d->type = AUDIO_MIXER_ENUM;
                d->mixer_class = AZ_CLASS_OUTPUT;
                m->target = MI_TARGET_PINSENSE;
                d->un.e.num_mem = 2;
                d->un.e.member[0].ord = 0;
                strlcpy(d->un.e.member[0].label.name, "unplugged",
                    MAX_AUDIO_DEV_LEN);
                d->un.e.member[1].ord = 1;
                strlcpy(d->un.e.member[1].label.name, "plugged",
                    MAX_AUDIO_DEV_LEN);
                this->nmixers++;
        }

        /* spkr mute by jack sense */
        this->spkr_mute_method = AZ_SPKR_MUTE_NONE;
        if (this->speaker != -1 && this->spkr_dac != -1 && this->nsense_pins > 0) {
                w = &this->w[this->speaker];
                if ((w->widgetcap & COP_AWCAP_OUTAMP) &&
                    (w->outamp_cap & COP_AMPCAP_MUTE))
                        this->spkr_mute_method = AZ_SPKR_MUTE_SPKR_MUTE;
                else if ((w->d.pin.cap & COP_PINCAP_OUTPUT) &&
                    (w->d.pin.cap & COP_PINCAP_INPUT))
                        this->spkr_mute_method = AZ_SPKR_MUTE_SPKR_DIR;
                else {
                        w = &this->w[this->spkr_dac];
                        if (w->nid != this->dacs.groups[0].conv[0] &&
                            (w->widgetcap & COP_AWCAP_OUTAMP) &&
                            (w->outamp_cap & COP_AMPCAP_MUTE))
                                this->spkr_mute_method = AZ_SPKR_MUTE_DAC_MUTE;
                }
        }
        if (this->spkr_mute_method != AZ_SPKR_MUTE_NONE) {
                w = &this->w[this->speaker];
                MIXER_REG_PROLOG;
                m->nid = w->nid;
                snprintf(d->label.name, sizeof(d->label.name),
                    "%s_muters", w->name);
                m->target = MI_TARGET_SENSESET;
                d->type = AUDIO_MIXER_SET;
                d->mixer_class = AZ_CLASS_OUTPUT;
                this->spkr_muters = 0;
                for (i = 0, j = 0; i < this->nsense_pins; i++) {
                        ww = &this->w[this->sense_pins[i]];
                        if (!(ww->d.pin.cap & COP_PINCAP_OUTPUT))
                                continue;
                        if (!(ww->widgetcap & COP_AWCAP_UNSOL))
                                continue;
                        d->un.s.member[j].mask = 1 << i;
                        this->spkr_muters |= (1 << i);
                        strlcpy(d->un.s.member[j++].label.name, ww->name,
                            MAX_AUDIO_DEV_LEN);
                }
                d->un.s.num_mem = j;
                if (j != 0)
                        this->nmixers++;
        }

        /* playback volume group */
        if (this->playvols.nslaves > 0) {
                mixer_devinfo_t *d;
                err = azalia_mixer_ensure_capacity(this,
                    this->nmixers + 3);

                /* volume */
                m = &this->mixers[this->nmixers];
                m->nid = this->playvols.master;
                m->target = MI_TARGET_PLAYVOL;
                d = &m->devinfo;
                d->mixer_class = AZ_CLASS_OUTPUT;
                snprintf(d->label.name, sizeof(d->label.name),
                    "%s", AudioNmaster);
                d->type = AUDIO_MIXER_VALUE;
                d->un.v.num_channels = 2;
                d->un.v.delta = 8;
                this->nmixers++;
                d->next = this->nmixers;

                /* mute */
                m = &this->mixers[this->nmixers];
                m->nid = this->playvols.master;
                m->target = MI_TARGET_PLAYVOL;
                d = &m->devinfo;
                d->prev = this->nmixers - 1;
                d->mixer_class = AZ_CLASS_OUTPUT;
                snprintf(d->label.name, sizeof(d->label.name),
                    "%s", AudioNmute);
                azalia_devinfo_offon(d);
                this->nmixers++;
                d->next = this->nmixers;

                /* slaves */
                m = &this->mixers[this->nmixers];
                m->nid = this->playvols.master;
                m->target = MI_TARGET_PLAYVOL;
                d = &m->devinfo;
                d->prev = this->nmixers - 1;
                d->mixer_class = AZ_CLASS_OUTPUT;
                snprintf(d->label.name, sizeof(d->label.name),
                    "%s", "slaves");
                d->type = AUDIO_MIXER_SET;
                for (i = 0, j = 0; i < this->playvols.nslaves; i++) {
                        ww = &this->w[this->playvols.slaves[i]];
                        d->un.s.member[j].mask = (1 << i);
                        strlcpy(d->un.s.member[j++].label.name, ww->name,
                            MAX_AUDIO_DEV_LEN);
                }
                d->un.s.num_mem = j;
                this->nmixers++;
        }

        /* recording volume group */
        if (this->recvols.nslaves > 0) {
                mixer_devinfo_t *d;
                err = azalia_mixer_ensure_capacity(this,
                    this->nmixers + 3);

                /* volume */
                m = &this->mixers[this->nmixers];
                m->nid = this->recvols.master;
                m->target = MI_TARGET_RECVOL;
                d = &m->devinfo;
                d->mixer_class = AZ_CLASS_RECORD;
                snprintf(d->label.name, sizeof(d->label.name),
                    "%s", AudioNvolume);
                d->type = AUDIO_MIXER_VALUE;
                d->un.v.num_channels = 2;
                d->un.v.delta = 8;
                this->nmixers++;
                d->next = this->nmixers;

                /* mute */
                m = &this->mixers[this->nmixers];
                m->nid = this->recvols.master;
                m->target = MI_TARGET_RECVOL;
                d = &m->devinfo;
                d->prev = this->nmixers - 1;
                d->mixer_class = AZ_CLASS_RECORD;
                snprintf(d->label.name, sizeof(d->label.name),
                    "%s", AudioNmute);
                azalia_devinfo_offon(d);
                this->nmixers++;
                d->next = this->nmixers;

                /* slaves */
                m = &this->mixers[this->nmixers];
                m->nid = this->recvols.master;
                m->target = MI_TARGET_RECVOL;
                d = &m->devinfo;
                d->prev = this->nmixers - 1;
                d->mixer_class = AZ_CLASS_RECORD;
                snprintf(d->label.name, sizeof(d->label.name),
                    "%s", "slaves");
                d->type = AUDIO_MIXER_SET;
                for (i = 0, j = 0; i < this->recvols.nslaves; i++) {
                        ww = &this->w[this->recvols.slaves[i]];
                        d->un.s.member[j].mask = (1 << i);
                        strlcpy(d->un.s.member[j++].label.name, ww->name,
                            MAX_AUDIO_DEV_LEN);
                }
                d->un.s.num_mem = j;
                this->nmixers++;
        }

        /* if the codec has more than one DAC group, the first is analog
         * and the second is digital.
         */
        if (this->dacs.ngroups > 1) {
                MIXER_REG_PROLOG;
                strlcpy(d->label.name, AudioNmode, sizeof(d->label.name));
                d->type = AUDIO_MIXER_ENUM;
                d->mixer_class = AZ_CLASS_OUTPUT;
                m->target = MI_TARGET_DAC;
                m->nid = this->audiofunc;
                d->un.e.member[0].ord = 0;
                strlcpy(d->un.e.member[0].label.name, "analog",
                    MAX_AUDIO_DEV_LEN);
                d->un.e.member[1].ord = 1;
                strlcpy(d->un.e.member[1].label.name, "digital",
                    MAX_AUDIO_DEV_LEN);
                d->un.e.num_mem = 2;
                this->nmixers++;
        }

        /* if the codec has more than one ADC group, the first is analog
         * and the second is digital.
         */
        if (this->adcs.ngroups > 1) {
                MIXER_REG_PROLOG;
                strlcpy(d->label.name, AudioNmode, sizeof(d->label.name));
                d->type = AUDIO_MIXER_ENUM;
                d->mixer_class = AZ_CLASS_RECORD;
                m->target = MI_TARGET_ADC;
                m->nid = this->audiofunc;
                d->un.e.member[0].ord = 0;
                strlcpy(d->un.e.member[0].label.name, "analog",
                    MAX_AUDIO_DEV_LEN);
                d->un.e.member[1].ord = 1;
                strlcpy(d->un.e.member[1].label.name, "digital",
                    MAX_AUDIO_DEV_LEN);
                d->un.e.num_mem = 2;
                this->nmixers++;
        }

        azalia_mixer_fix_indexes(this);
        azalia_mixer_default(this);
        return 0;
}

void
azalia_devinfo_offon(mixer_devinfo_t *d)
{
        d->type = AUDIO_MIXER_ENUM;
        d->un.e.num_mem = 2;
        d->un.e.member[0].ord = 0;
        strlcpy(d->un.e.member[0].label.name, AudioNoff, MAX_AUDIO_DEV_LEN);
        d->un.e.member[1].ord = 1;
        strlcpy(d->un.e.member[1].label.name, AudioNon, MAX_AUDIO_DEV_LEN);
}

int
azalia_mixer_ensure_capacity(codec_t *this, size_t newsize)
{
        size_t newmax;
        void *newbuf;

        if (this->maxmixers >= newsize)
                return 0;
        newmax = this->maxmixers + 10;
        if (newmax < newsize)
                newmax = newsize;
        newbuf = mallocarray(newmax, sizeof(mixer_item_t), M_DEVBUF,
            M_NOWAIT | M_ZERO);
        if (newbuf == NULL) {
                printf("%s: out of memory in %s\n", XNAME(this), __func__);
                return ENOMEM;
        }
        bcopy(this->mixers, newbuf, this->maxmixers * sizeof(mixer_item_t));
        free(this->mixers, M_DEVBUF, this->maxmixers * sizeof(mixer_item_t));
        this->mixers = newbuf;
        this->maxmixers = newmax;
        return 0;
}

int
azalia_mixer_fix_indexes(codec_t *this)
{
        int i;
        mixer_devinfo_t *d;

        for (i = 0; i < this->nmixers; i++) {
                d = &this->mixers[i].devinfo;
#ifdef DIAGNOSTIC
                if (d->index != 0 && d->index != i)
                        printf("%s: index mismatch %d %d\n", __func__,
                            d->index, i);
#endif
                d->index = i;
                if (d->prev == 0)
                        d->prev = AUDIO_MIXER_LAST;
                if (d->next == 0)
                        d->next = AUDIO_MIXER_LAST;
        }
        return 0;
}

int
azalia_mixer_default(codec_t *this)
{
        widget_t *w;
        mixer_item_t *m;
        mixer_ctrl_t mc;
        int i, j, tgt, cap, err;

        /* unmute all */
        for (i = 0; i < this->nmixers; i++) {
                m = &this->mixers[i];
                if (!IS_MI_TARGET_INAMP(m->target) &&
                    m->target != MI_TARGET_OUTAMP)
                        continue;
                if (m->devinfo.type != AUDIO_MIXER_ENUM)
                        continue;
                bzero(&mc, sizeof(mc));
                mc.dev = i;
                mc.type = AUDIO_MIXER_ENUM;
                azalia_mixer_set(this, m->nid, m->target, &mc);
        }

        /* set unextreme volume */
        for (i = 0; i < this->nmixers; i++) {
                m = &this->mixers[i];
                if (!IS_MI_TARGET_INAMP(m->target) &&
                    m->target != MI_TARGET_OUTAMP)
                        continue;
                if (m->devinfo.type != AUDIO_MIXER_VALUE)
                        continue;
                bzero(&mc, sizeof(mc));
                mc.dev = i;
                mc.type = AUDIO_MIXER_VALUE;
                mc.un.value.num_channels = 1;
                mc.un.value.level[0] = AUDIO_MAX_GAIN / 2;
                if (WIDGET_CHANNELS(&this->w[m->nid]) == 2) {
                        mc.un.value.num_channels = 2;
                        mc.un.value.level[1] = mc.un.value.level[0];
                }
                azalia_mixer_set(this, m->nid, m->target, &mc);
        }

        /* unmute all */
        for (i = 0; i < this->nmixers; i++) {
                m = &this->mixers[i];
                if (m->target != MI_TARGET_MUTESET)
                        continue;
                if (m->devinfo.type != AUDIO_MIXER_SET)
                        continue;
                bzero(&mc, sizeof(mc));
                mc.dev = i;
                mc.type = AUDIO_MIXER_SET;
                if (!azalia_widget_enabled(this, m->nid)) {
                        DPRINTF(("%s: invalid set nid\n", __func__));
                        return EINVAL;
                }
                w = &this->w[m->nid];
                for (j = 0; j < w->nconnections; j++) {
                        if (!azalia_widget_enabled(this, w->connections[j]))
                                continue;
                        if (w->nid == this->input_mixer &&
                            w->connections[j] == this->mic)
                                continue;
                        mc.un.mask |= 1 << j;
                }
                azalia_mixer_set(this, m->nid, m->target, &mc);
        }

        /* make sure default connection is valid */
        for (i = 0; i < this->nmixers; i++) {
                m = &this->mixers[i];
                if (m->target != MI_TARGET_CONNLIST)
                        continue;

                azalia_mixer_get(this, m->nid, m->target, &mc);
                for (j = 0; j < m->devinfo.un.e.num_mem; j++) {
                        if (mc.un.ord == m->devinfo.un.e.member[j].ord)
                                break;
                }
                if (j >= m->devinfo.un.e.num_mem) {
                        bzero(&mc, sizeof(mc));
                        mc.dev = i;
                        mc.type = AUDIO_MIXER_ENUM;
                        mc.un.ord = m->devinfo.un.e.member[0].ord;
                }
                azalia_mixer_set(this, m->nid, m->target, &mc);
        }

        /* get default value for play group master */
        for (i = 0; i < this->playvols.nslaves; i++) {
                if (!(this->playvols.cur & (1 << i)))
                        continue;
                w = &this->w[this->playvols.slaves[i]];
                if (!(COP_AMPCAP_NUMSTEPS(w->outamp_cap)))
                        continue;
                mc.type = AUDIO_MIXER_VALUE;
                tgt = MI_TARGET_OUTAMP;
                azalia_mixer_get(this, w->nid, tgt, &mc);
                this->playvols.vol_l = mc.un.value.level[0];
                this->playvols.vol_r = mc.un.value.level[0];
                break;
        }
        this->playvols.mute = 0;
 
        /* get default value for record group master */
        for (i = 0; i < this->recvols.nslaves; i++) {
                if (!(this->recvols.cur & (1 << i)))
                        continue;
                w = &this->w[this->recvols.slaves[i]];
                mc.type = AUDIO_MIXER_VALUE;
                tgt = MI_TARGET_OUTAMP;
                cap = w->outamp_cap;
                if (w->type == COP_AWTYPE_PIN_COMPLEX ||
                    w->type == COP_AWTYPE_AUDIO_INPUT) {
                        tgt = 0;
                        cap = w->inamp_cap;
                }
                if (!(COP_AMPCAP_NUMSTEPS(cap)))
                        continue;
                azalia_mixer_get(this, w->nid, tgt, &mc);
                this->recvols.vol_l = mc.un.value.level[0];
                this->recvols.vol_r = mc.un.value.level[0];
                break;
        }
        this->recvols.mute = 0;

        err = azalia_codec_enable_unsol(this);
        if (err)
                return(err);

        return 0;
}

int
azalia_codec_enable_unsol(codec_t *this)
{
        widget_t *w;
        uint32_t result;
        int i, err;

        /* jack sense */
        for (i = 0; i < this->nsense_pins; i++) {
                if (this->spkr_muters & (1 << i)) {
                        azalia_comresp(this, this->sense_pins[i],
                            CORB_SET_UNSOLICITED_RESPONSE,
                            CORB_UNSOL_ENABLE | AZ_TAG_SPKR, NULL);
                }
        }
        if (this->spkr_muters != 0)
                azalia_unsol_event(this, AZ_TAG_SPKR);

        /* volume knob */
        if (this->playvols.master != this->audiofunc) {

                w = &this->w[this->playvols.master];
                err = azalia_comresp(this, w->nid, CORB_GET_VOLUME_KNOB,
                    0, &result);
                if (err) {
                        DPRINTF(("%s: get volume knob error\n", __func__));
                        return err;
                }

                /* current level */
                this->playvols.hw_step = CORB_VKNOB_VOLUME(result);
                this->playvols.hw_nsteps = COP_VKCAP_NUMSTEPS(w->d.volume.cap);

                /* indirect mode */
                result &= ~(CORB_VKNOB_DIRECT);
                err = azalia_comresp(this, w->nid, CORB_SET_VOLUME_KNOB,
                    result, NULL);
                if (err) {
                        DPRINTF(("%s: set volume knob error\n", __func__));
                        /* XXX If there was an error setting indirect
                         * mode, do not return an error.  However, do not
                         * enable unsolicited responses either.  Most
                         * likely the volume knob doesn't work right.
                         * Perhaps it's simply not wired/enabled.
                         */
                        return 0;
                }

                /* enable unsolicited responses */
                result = CORB_UNSOL_ENABLE | AZ_TAG_PLAYVOL;
                err = azalia_comresp(this, w->nid,
                    CORB_SET_UNSOLICITED_RESPONSE, result, NULL);
                if (err) {
                        DPRINTF(("%s: set vknob unsol resp error\n", __func__));
                        return err;
                }
        }

        return 0;
}

int
azalia_mixer_delete(codec_t *this)
{
        if (this->mixers != NULL) {
                free(this->mixers, M_DEVBUF, 0);
                this->mixers = NULL;
        }
        return 0;
}

/**
 * @param mc    mc->type must be set by the caller before the call
 */
int
azalia_mixer_get(const codec_t *this, nid_t nid, int target,
    mixer_ctrl_t *mc)
{
        uint32_t result, cap, value;
        nid_t n;
        int i, err;

        if (mc->type == AUDIO_MIXER_CLASS) {
                return(0);
        }

        /* inamp mute */
        else if (IS_MI_TARGET_INAMP(target) && mc->type == AUDIO_MIXER_ENUM) {
                err = azalia_comresp(this, nid, CORB_GET_AMPLIFIER_GAIN_MUTE,
                    CORB_GAGM_INPUT | CORB_GAGM_LEFT |
                    MI_TARGET_INAMP(target), &result);
                if (err)
                        return err;
                mc->un.ord = result & CORB_GAGM_MUTE ? 1 : 0;
        }

        /* inamp gain */
        else if (IS_MI_TARGET_INAMP(target) && mc->type == AUDIO_MIXER_VALUE) {
                err = azalia_comresp(this, nid, CORB_GET_AMPLIFIER_GAIN_MUTE,
                      CORB_GAGM_INPUT | CORB_GAGM_LEFT |
                      MI_TARGET_INAMP(target), &result);
                if (err)
                        return err;
                mc->un.value.level[0] = azalia_mixer_from_device_value(this,
                    nid, target, CORB_GAGM_GAIN(result));
                if (this->w[nid].type == COP_AWTYPE_AUDIO_SELECTOR ||
                    this->w[nid].type == COP_AWTYPE_AUDIO_MIXER) {
                        n = this->w[nid].connections[MI_TARGET_INAMP(target)];
                        if (!azalia_widget_enabled(this, n)) {
                                DPRINTF(("%s: nid %2.2x invalid index %d\n",
                                   __func__, nid,  MI_TARGET_INAMP(target)));
                                n = nid;
                        }
                } else
                        n = nid;
                mc->un.value.num_channels = WIDGET_CHANNELS(&this->w[n]);
                if (mc->un.value.num_channels == 2) {
                        err = azalia_comresp(this, nid,
                            CORB_GET_AMPLIFIER_GAIN_MUTE, CORB_GAGM_INPUT |
                            CORB_GAGM_RIGHT | MI_TARGET_INAMP(target),
                            &result);
                        if (err)
                                return err;
                        mc->un.value.level[1] = azalia_mixer_from_device_value
                            (this, nid, target, CORB_GAGM_GAIN(result));
                }
        }

        /* outamp mute */
        else if (target == MI_TARGET_OUTAMP && mc->type == AUDIO_MIXER_ENUM) {
                err = azalia_comresp(this, nid, CORB_GET_AMPLIFIER_GAIN_MUTE,
                    CORB_GAGM_OUTPUT | CORB_GAGM_LEFT | 0, &result);
                if (err)
                        return err;
                mc->un.ord = result & CORB_GAGM_MUTE ? 1 : 0;
        }

        /* outamp gain */
        else if (target == MI_TARGET_OUTAMP && mc->type == AUDIO_MIXER_VALUE) {
                err = azalia_comresp(this, nid, CORB_GET_AMPLIFIER_GAIN_MUTE,
                      CORB_GAGM_OUTPUT | CORB_GAGM_LEFT | 0, &result);
                if (err)
                        return err;
                mc->un.value.level[0] = azalia_mixer_from_device_value(this,
                    nid, target, CORB_GAGM_GAIN(result));
                mc->un.value.num_channels = WIDGET_CHANNELS(&this->w[nid]);
                if (mc->un.value.num_channels == 2) {
                        err = azalia_comresp(this, nid,
                            CORB_GET_AMPLIFIER_GAIN_MUTE,
                            CORB_GAGM_OUTPUT | CORB_GAGM_RIGHT | 0, &result);
                        if (err)
                                return err;
                        mc->un.value.level[1] = azalia_mixer_from_device_value
                            (this, nid, target, CORB_GAGM_GAIN(result));
                }
        }

        /* selection */
        else if (target == MI_TARGET_CONNLIST) {
                err = azalia_comresp(this, nid,
                    CORB_GET_CONNECTION_SELECT_CONTROL, 0, &result);
                if (err)
                        return err;
                result = CORB_CSC_INDEX(result);
                if (!azalia_widget_enabled(this,
                    this->w[nid].connections[result]))
                        mc->un.ord = -1;
                else
                        mc->un.ord = result;
        }

        /* pin I/O */
        else if (target == MI_TARGET_PINDIR) {
                err = azalia_comresp(this, nid,
                    CORB_GET_PIN_WIDGET_CONTROL, 0, &result);
                if (err)
                        return err;

                value = result;
                if (!(result & (CORB_PWC_INPUT | CORB_PWC_OUTPUT)))
                        mc->un.ord = 0;
                else if (result & CORB_PWC_OUTPUT)
                        mc->un.ord = 1;
                else {
                        cap = COP_PINCAP_VREF(this->w[nid].d.pin.cap);
                        result &= CORB_PWC_VREF_MASK;
                        if (result == CORB_PWC_VREF_GND)
                                mc->un.ord = 3;
                        else if (result == CORB_PWC_VREF_50)
                                mc->un.ord = 4;
                        else if (result == CORB_PWC_VREF_80)
                                mc->un.ord = 5;
                        else if (result == CORB_PWC_VREF_100)
                                mc->un.ord = 6;
                        else
                                mc->un.ord = 2;
                }
        }

        /* pin headphone-boost */
        else if (target == MI_TARGET_PINBOOST) {
                err = azalia_comresp(this, nid,
                    CORB_GET_PIN_WIDGET_CONTROL, 0, &result);
                if (err)
                        return err;
                mc->un.ord = result & CORB_PWC_HEADPHONE ? 1 : 0;
        }

        /* DAC group selection */
        else if (target == MI_TARGET_DAC) {
                mc->un.ord = this->dacs.cur;
        }

        /* ADC selection */
        else if (target == MI_TARGET_ADC) {
                mc->un.ord = this->adcs.cur;
        }

        /* S/PDIF */
        else if (target == MI_TARGET_SPDIF) {
                err = azalia_comresp(this, nid, CORB_GET_DIGITAL_CONTROL,
                    0, &result);
                if (err)
                        return err;
                mc->un.mask = result & 0xff & ~(CORB_DCC_DIGEN | CORB_DCC_NAUDIO);
        } else if (target == MI_TARGET_SPDIF_CC) {
                err = azalia_comresp(this, nid, CORB_GET_DIGITAL_CONTROL,
                    0, &result);
                if (err)
                        return err;
                mc->un.value.num_channels = 1;
                mc->un.value.level[0] = CORB_DCC_CC(result);
        }

        /* EAPD */
        else if (target == MI_TARGET_EAPD) {
                err = azalia_comresp(this, nid, CORB_GET_EAPD_BTL_ENABLE,
                    0, &result);
                if (err)
                        return err;
                mc->un.ord = result & CORB_EAPD_EAPD ? 1 : 0;
        }

        /* sense pin */
        else if (target == MI_TARGET_PINSENSE) {
                err = azalia_comresp(this, nid, CORB_GET_PIN_SENSE,
                    0, &result);
                if (err)
                        return err;
                mc->un.ord = result & CORB_PS_PRESENCE ? 1 : 0;
        }

        /* mute set */
        else if (target == MI_TARGET_MUTESET && mc->type == AUDIO_MIXER_SET) {
                const widget_t *w;

                if (!azalia_widget_enabled(this, nid)) {
                        DPRINTF(("%s: invalid muteset nid\n", XNAME(this)));
                        return EINVAL;
                }
                w = &this->w[nid];
                mc->un.mask = 0;
                for (i = 0; i < w->nconnections; i++) {
                        if (!azalia_widget_enabled(this, w->connections[i]))
                                continue;
                        err = azalia_comresp(this, nid,
                            CORB_GET_AMPLIFIER_GAIN_MUTE,
                            CORB_GAGM_INPUT | CORB_GAGM_LEFT |
                            MI_TARGET_INAMP(i), &result);
                        if (err)
                                return err;
                        mc->un.mask |= (result & CORB_GAGM_MUTE) ? 0 : (1 << i);
                }
        }

        /* mixer set - show all connections */
        else if (target == MI_TARGET_MIXERSET && mc->type == AUDIO_MIXER_SET) {
                const widget_t *w;

                if (!azalia_widget_enabled(this, nid)) {
                        DPRINTF(("%s: invalid mixerset nid\n", XNAME(this)));
                        return EINVAL;
                }
                w = &this->w[nid];
                mc->un.mask = 0;
                for (i = 0; i < w->nconnections; i++) {
                        if (!azalia_widget_enabled(this, w->connections[i]))
                                continue;
                        mc->un.mask |= (1 << i);
                }
        }

        else if (target == MI_TARGET_SENSESET && mc->type == AUDIO_MIXER_SET) {

                if (nid == this->speaker) {
                        mc->un.mask = this->spkr_muters;
                } else {
                        DPRINTF(("%s: invalid senseset nid\n", XNAME(this)));
                        return EINVAL;
                }
        }

        else if (target == MI_TARGET_PLAYVOL) {

                if (mc->type == AUDIO_MIXER_VALUE) {
                        mc->un.value.num_channels = 2;
                        mc->un.value.level[0] = this->playvols.vol_l;
                        mc->un.value.level[1] = this->playvols.vol_r;

                } else if (mc->type == AUDIO_MIXER_ENUM) {
                        mc->un.ord = this->playvols.mute;

                } else if (mc->type == AUDIO_MIXER_SET) {
                        mc->un.mask = this->playvols.cur;

                } else {
                        DPRINTF(("%s: invalid outmaster mixer type\n",
                                XNAME(this)));
                        return EINVAL;
                }
        }

        else if (target == MI_TARGET_RECVOL) {

                if (mc->type == AUDIO_MIXER_VALUE) {
                        mc->un.value.num_channels = 2;
                        mc->un.value.level[0] = this->recvols.vol_l;
                        mc->un.value.level[1] = this->recvols.vol_r;

                } else if (mc->type == AUDIO_MIXER_ENUM) {
                        mc->un.ord = this->recvols.mute;

                } else if (mc->type == AUDIO_MIXER_SET) {
                        mc->un.mask = this->recvols.cur;

                } else {
                        DPRINTF(("%s: invalid inmaster mixer type\n",
                                XNAME(this)));
                        return EINVAL;
                }
        }

        else {
                DPRINTF(("%s: internal error in %s: target=%x\n",
                    XNAME(this), __func__, target));
                return -1;
        }
        return 0;
}

int
azalia_mixer_set(codec_t *this, nid_t nid, int target, const mixer_ctrl_t *mc)
{
        uint32_t result, value;
        int i, err;

        if (mc->type == AUDIO_MIXER_CLASS) {
                return(0);
        }

        /* inamp mute */
        else if (IS_MI_TARGET_INAMP(target) && mc->type == AUDIO_MIXER_ENUM) {
                /* set stereo mute separately to keep each gain value */
                err = azalia_comresp(this, nid, CORB_GET_AMPLIFIER_GAIN_MUTE,
                    CORB_GAGM_INPUT | CORB_GAGM_LEFT |
                    MI_TARGET_INAMP(target), &result);
                if (err)
                        return err;
                value = CORB_AGM_INPUT | CORB_AGM_LEFT |
                    (target << CORB_AGM_INDEX_SHIFT) |
                    CORB_GAGM_GAIN(result);
                if (mc->un.ord)
                        value |= CORB_AGM_MUTE;
                err = azalia_comresp(this, nid, CORB_SET_AMPLIFIER_GAIN_MUTE,
                    value, &result);
                if (err)
                        return err;
                if (WIDGET_CHANNELS(&this->w[nid]) == 2) {
                        err = azalia_comresp(this, nid,
                            CORB_GET_AMPLIFIER_GAIN_MUTE, CORB_GAGM_INPUT |
                            CORB_GAGM_RIGHT | MI_TARGET_INAMP(target),
                            &result);
                        if (err)
                                return err;
                        value = CORB_AGM_INPUT | CORB_AGM_RIGHT |
                            (target << CORB_AGM_INDEX_SHIFT) |
                            CORB_GAGM_GAIN(result);
                        if (mc->un.ord)
                                value |= CORB_AGM_MUTE;
                        err = azalia_comresp(this, nid,
                            CORB_SET_AMPLIFIER_GAIN_MUTE, value, &result);
                        if (err)
                                return err;
                }
        }

        /* inamp gain */
        else if (IS_MI_TARGET_INAMP(target) && mc->type == AUDIO_MIXER_VALUE) {
                if (mc->un.value.num_channels < 1)
                        return EINVAL;
                err = azalia_comresp(this, nid, CORB_GET_AMPLIFIER_GAIN_MUTE,
                      CORB_GAGM_INPUT | CORB_GAGM_LEFT |
                      MI_TARGET_INAMP(target), &result);
                if (err)
                        return err;
                value = azalia_mixer_to_device_value(this, nid, target,
                    mc->un.value.level[0]);
                value = CORB_AGM_INPUT | CORB_AGM_LEFT |
                    (target << CORB_AGM_INDEX_SHIFT) |
                    (result & CORB_GAGM_MUTE ? CORB_AGM_MUTE : 0) |
                    (value & CORB_AGM_GAIN_MASK);
                err = azalia_comresp(this, nid, CORB_SET_AMPLIFIER_GAIN_MUTE,
                    value, &result);
                if (err)
                        return err;
                if (mc->un.value.num_channels >= 2 &&
                    WIDGET_CHANNELS(&this->w[nid]) == 2) {
                        err = azalia_comresp(this, nid,
                              CORB_GET_AMPLIFIER_GAIN_MUTE, CORB_GAGM_INPUT |
                              CORB_GAGM_RIGHT | MI_TARGET_INAMP(target),
                              &result);
                        if (err)
                                return err;
                        value = azalia_mixer_to_device_value(this, nid, target,
                            mc->un.value.level[1]);
                        value = CORB_AGM_INPUT | CORB_AGM_RIGHT |
                            (target << CORB_AGM_INDEX_SHIFT) |
                            (result & CORB_GAGM_MUTE ? CORB_AGM_MUTE : 0) |
                            (value & CORB_AGM_GAIN_MASK);
                        err = azalia_comresp(this, nid,
                            CORB_SET_AMPLIFIER_GAIN_MUTE, value, &result);
                        if (err)
                                return err;
                }
        }

        /* outamp mute */
        else if (target == MI_TARGET_OUTAMP && mc->type == AUDIO_MIXER_ENUM) {
                err = azalia_comresp(this, nid, CORB_GET_AMPLIFIER_GAIN_MUTE,
                    CORB_GAGM_OUTPUT | CORB_GAGM_LEFT, &result);
                if (err)
                        return err;
                value = CORB_AGM_OUTPUT | CORB_AGM_LEFT | CORB_GAGM_GAIN(result);
                if (mc->un.ord)
                        value |= CORB_AGM_MUTE;
                err = azalia_comresp(this, nid, CORB_SET_AMPLIFIER_GAIN_MUTE,
                    value, &result);
                if (err)
                        return err;
                if (WIDGET_CHANNELS(&this->w[nid]) == 2) {
                        err = azalia_comresp(this, nid,
                            CORB_GET_AMPLIFIER_GAIN_MUTE,
                            CORB_GAGM_OUTPUT | CORB_GAGM_RIGHT, &result);
                        if (err)
                                return err;
                        value = CORB_AGM_OUTPUT | CORB_AGM_RIGHT |
                            CORB_GAGM_GAIN(result);
                        if (mc->un.ord)
                                value |= CORB_AGM_MUTE;
                        err = azalia_comresp(this, nid,
                            CORB_SET_AMPLIFIER_GAIN_MUTE, value, &result);
                        if (err)
                                return err;
                }
        }

        /* outamp gain */
        else if (target == MI_TARGET_OUTAMP && mc->type == AUDIO_MIXER_VALUE) {
                if (mc->un.value.num_channels < 1)
                        return EINVAL;
                err = azalia_comresp(this, nid, CORB_GET_AMPLIFIER_GAIN_MUTE,
                      CORB_GAGM_OUTPUT | CORB_GAGM_LEFT, &result);
                if (err)
                        return err;
                value = azalia_mixer_to_device_value(this, nid, target,
                    mc->un.value.level[0]);
                value = CORB_AGM_OUTPUT | CORB_AGM_LEFT |
                    (result & CORB_GAGM_MUTE ? CORB_AGM_MUTE : 0) |
                    (value & CORB_AGM_GAIN_MASK);
                err = azalia_comresp(this, nid, CORB_SET_AMPLIFIER_GAIN_MUTE,
                    value, &result);
                if (err)
                        return err;
                if (mc->un.value.num_channels >= 2 &&
                    WIDGET_CHANNELS(&this->w[nid]) == 2) {
                        err = azalia_comresp(this, nid,
                              CORB_GET_AMPLIFIER_GAIN_MUTE, CORB_GAGM_OUTPUT |
                              CORB_GAGM_RIGHT, &result);
                        if (err)
                                return err;
                        value = azalia_mixer_to_device_value(this, nid, target,
                            mc->un.value.level[1]);
                        value = CORB_AGM_OUTPUT | CORB_AGM_RIGHT |
                            (result & CORB_GAGM_MUTE ? CORB_AGM_MUTE : 0) |
                            (value & CORB_AGM_GAIN_MASK);
                        err = azalia_comresp(this, nid,
                            CORB_SET_AMPLIFIER_GAIN_MUTE, value, &result);
                        if (err)
                                return err;
                }
        }

        /* selection */
        else if (target == MI_TARGET_CONNLIST) {
                if (mc->un.ord < 0 ||
                    mc->un.ord >= this->w[nid].nconnections ||
                    !azalia_widget_enabled(this,
                    this->w[nid].connections[mc->un.ord]))
                        return EINVAL;
                err = azalia_comresp(this, nid,
                    CORB_SET_CONNECTION_SELECT_CONTROL, mc->un.ord, &result);
                if (err)
                        return err;
        }

        /* pin I/O */
        else if (target == MI_TARGET_PINDIR) {

                err = azalia_comresp(this, nid,
                    CORB_GET_PIN_WIDGET_CONTROL, 0, &result);
                if (err)
                        return err;

                value = result;
                value &= ~(CORB_PWC_VREF_MASK);
                if (mc->un.ord == 0) {
                        value &= ~(CORB_PWC_OUTPUT | CORB_PWC_INPUT);
                } else if (mc->un.ord == 1) {
                        value &= ~CORB_PWC_INPUT;
                        value |= CORB_PWC_OUTPUT;
                        if (this->qrks & AZ_QRK_WID_OVREF50)
                                value |= CORB_PWC_VREF_50;
                } else {
                        value &= ~CORB_PWC_OUTPUT;
                        value |= CORB_PWC_INPUT;

                        if (mc->un.ord == 3)
                                value |= CORB_PWC_VREF_GND;
                        if (mc->un.ord == 4)
                                value |= CORB_PWC_VREF_50;
                        if (mc->un.ord == 5)
                                value |= CORB_PWC_VREF_80;
                        if (mc->un.ord == 6)
                                value |= CORB_PWC_VREF_100;
                }
                err = azalia_comresp(this, nid,
                    CORB_SET_PIN_WIDGET_CONTROL, value, &result);
                if (err)
                        return err;

                /* Run the unsolicited response handler for speaker mute
                 * since it depends on pin direction.
                 */
                for (i = 0; i < this->nsense_pins; i++) {
                        if (this->sense_pins[i] == nid)
                                break;
                }
                if (i < this->nsense_pins) {
                        azalia_unsol_event(this, AZ_TAG_SPKR);
                }
        }

        /* pin headphone-boost */
        else if (target == MI_TARGET_PINBOOST) {
                if (mc->un.ord >= 2)
                        return EINVAL;
                err = azalia_comresp(this, nid,
                    CORB_GET_PIN_WIDGET_CONTROL, 0, &result);
                if (err)
                        return err;
                if (mc->un.ord == 0) {
                        result &= ~CORB_PWC_HEADPHONE;
                } else {
                        result |= CORB_PWC_HEADPHONE;
                }
                err = azalia_comresp(this, nid,
                    CORB_SET_PIN_WIDGET_CONTROL, result, &result);
                if (err)
                        return err;
        }

        /* DAC group selection */
        else if (target == MI_TARGET_DAC) {
                if (this->running)
                        return EBUSY;
                if (mc->un.ord >= this->dacs.ngroups)
                        return EINVAL;
                if (mc->un.ord != this->dacs.cur)
                        return azalia_codec_construct_format(this,
                            mc->un.ord, this->adcs.cur);
                else
                        return 0;
        }

        /* ADC selection */
        else if (target == MI_TARGET_ADC) {
                if (this->running)
                        return EBUSY;
                if (mc->un.ord >= this->adcs.ngroups)
                        return EINVAL;
                if (mc->un.ord != this->adcs.cur)
                        return azalia_codec_construct_format(this,
                            this->dacs.cur, mc->un.ord);
                else
                        return 0;
        }

        /* S/PDIF */
        else if (target == MI_TARGET_SPDIF) {
                err = azalia_comresp(this, nid, CORB_GET_DIGITAL_CONTROL,
                    0, &result);
                result &= CORB_DCC_DIGEN | CORB_DCC_NAUDIO;
                result |= mc->un.mask & 0xff & ~CORB_DCC_DIGEN;
                err = azalia_comresp(this, nid, CORB_SET_DIGITAL_CONTROL_L,
                    result, NULL);
                if (err)
                        return err;
        } else if (target == MI_TARGET_SPDIF_CC) {
                if (mc->un.value.num_channels != 1)
                        return EINVAL;
                if (mc->un.value.level[0] > 127)
                        return EINVAL;
                err = azalia_comresp(this, nid, CORB_SET_DIGITAL_CONTROL_H,
                    mc->un.value.level[0], NULL);
                if (err)
                        return err;
        }

        /* EAPD */
        else if (target == MI_TARGET_EAPD) {
                if (mc->un.ord >= 2)
                        return EINVAL;
                err = azalia_comresp(this, nid,
                    CORB_GET_EAPD_BTL_ENABLE, 0, &result);
                if (err)
                        return err;
                result &= 0xff;
                if (mc->un.ord == 0) {
                        result &= ~CORB_EAPD_EAPD;
                } else {
                        result |= CORB_EAPD_EAPD;
                }
                err = azalia_comresp(this, nid,
                    CORB_SET_EAPD_BTL_ENABLE, result, &result);
                if (err)
                        return err;
        }

        else if (target == MI_TARGET_PINSENSE) {
                /* do nothing, control is read only */
        }

        else if (target == MI_TARGET_MUTESET && mc->type == AUDIO_MIXER_SET) {
                const widget_t *w;

                if (!azalia_widget_enabled(this, nid)) {
                        DPRINTF(("%s: invalid muteset nid\n", XNAME(this)));
                        return EINVAL;
                }
                w = &this->w[nid];
                for (i = 0; i < w->nconnections; i++) {
                        if (!azalia_widget_enabled(this, w->connections[i]))
                                continue;

                        /* We have to set stereo mute separately
                         * to keep each gain value.
                         */
                        err = azalia_comresp(this, nid,
                            CORB_GET_AMPLIFIER_GAIN_MUTE,
                            CORB_GAGM_INPUT | CORB_GAGM_LEFT |
                            MI_TARGET_INAMP(i), &result);
                        if (err)
                                return err;
                        value = CORB_AGM_INPUT | CORB_AGM_LEFT |
                            (i << CORB_AGM_INDEX_SHIFT) |
                            CORB_GAGM_GAIN(result);
                        if ((mc->un.mask & (1 << i)) == 0)
                                value |= CORB_AGM_MUTE;
                        err = azalia_comresp(this, nid,
                            CORB_SET_AMPLIFIER_GAIN_MUTE, value, &result);
                        if (err)
                                return err;

                        if (WIDGET_CHANNELS(w) == 2) {
                                err = azalia_comresp(this, nid,
                                    CORB_GET_AMPLIFIER_GAIN_MUTE,
                                    CORB_GAGM_INPUT | CORB_GAGM_RIGHT |
                                    MI_TARGET_INAMP(i), &result);
                                if (err)
                                        return err;
                                value = CORB_AGM_INPUT | CORB_AGM_RIGHT |
                                    (i << CORB_AGM_INDEX_SHIFT) |
                                    CORB_GAGM_GAIN(result);
                                if ((mc->un.mask & (1 << i)) == 0)
                                        value |= CORB_AGM_MUTE;
                                err = azalia_comresp(this, nid,
                                    CORB_SET_AMPLIFIER_GAIN_MUTE,
                                    value, &result);
                                if (err)
                                        return err;
                        }
                }
        }

        else if (target == MI_TARGET_MIXERSET && mc->type == AUDIO_MIXER_SET) {
                /* do nothing, control is read only */
        }

        else if (target == MI_TARGET_SENSESET && mc->type == AUDIO_MIXER_SET) {

                if (nid == this->speaker) {
                        this->spkr_muters = mc->un.mask;
                        azalia_unsol_event(this, AZ_TAG_SPKR);
                } else {
                        DPRINTF(("%s: invalid senseset nid\n", XNAME(this)));
                        return EINVAL;
                }
        }

        else if (target == MI_TARGET_PLAYVOL) {

                const widget_t *w;
                mixer_ctrl_t mc2;

                if (mc->type == AUDIO_MIXER_VALUE) {
                        if (mc->un.value.num_channels != 2)
                                return EINVAL;
                        this->playvols.vol_l = mc->un.value.level[0];
                        this->playvols.vol_r = mc->un.value.level[1];
                        for (i = 0; i < this->playvols.nslaves; i++) {
                                if (!(this->playvols.cur & (1 << i)))
                                        continue;
                                w = &this->w[this->playvols.slaves[i]];
                                if (!(COP_AMPCAP_NUMSTEPS(w->outamp_cap)))
                                        continue;

                                /* don't change volume if muted */
                                if (w->outamp_cap & COP_AMPCAP_MUTE) {
                                        mc2.type = AUDIO_MIXER_ENUM;
                                        azalia_mixer_get(this, w->nid,
                                            MI_TARGET_OUTAMP, &mc2);
                                        if (mc2.un.ord)
                                                continue;
                                }
                                mc2.type = AUDIO_MIXER_VALUE;
                                mc2.un.value.num_channels = WIDGET_CHANNELS(w);
                                mc2.un.value.level[0] = this->playvols.vol_l;
                                mc2.un.value.level[1] = this->playvols.vol_r;
                                err = azalia_mixer_set(this, w->nid,
                                    MI_TARGET_OUTAMP, &mc2);
                                if (err) {
                                        DPRINTF(("%s: out slave %2.2x vol\n",
                                            __func__, w->nid));
                                        return err;
                                }
                        }
                } else if (mc->type == AUDIO_MIXER_ENUM) {
                        if (mc->un.ord != 0 && mc->un.ord != 1)
                                return EINVAL;
                        this->playvols.mute = mc->un.ord;
                        for (i = 0; i < this->playvols.nslaves; i++) {
                                if (!(this->playvols.cur & (1 << i)))
                                        continue;
                                w = &this->w[this->playvols.slaves[i]];
                                if (!(w->outamp_cap & COP_AMPCAP_MUTE))
                                        continue;
                                if (this->spkr_muted == 1 &&
                                    ((this->spkr_mute_method ==
                                    AZ_SPKR_MUTE_SPKR_MUTE &&
                                    (w->nid == this->speaker ||
                                    w->nid == this->speaker2)) ||
                                    (this->spkr_mute_method ==
                                    AZ_SPKR_MUTE_DAC_MUTE &&
                                    w->nid == this->spkr_dac))) {
                                        continue;
                                }
                                mc2.type = AUDIO_MIXER_ENUM;
                                mc2.un.ord = this->playvols.mute;
                                err = azalia_mixer_set(this, w->nid,
                                    MI_TARGET_OUTAMP, &mc2);
                                if (err) {
                                        DPRINTF(("%s: out slave %2.2x mute\n",
                                            __func__, w->nid));
                                        return err;
                                }
                        }

                } else if (mc->type == AUDIO_MIXER_SET) {
                        this->playvols.cur =
                            (mc->un.mask & this->playvols.mask);

                } else {
                        DPRINTF(("%s: invalid output master mixer type\n",
                                XNAME(this)));
                        return EINVAL;
                }
        }

        else if (target == MI_TARGET_RECVOL) {

                const widget_t *w;
                mixer_ctrl_t mc2;
                uint32_t cap;
                int tgt;

                if (mc->type == AUDIO_MIXER_VALUE) {
                        if (mc->un.value.num_channels != 2)
                                return EINVAL;
                        this->recvols.vol_l = mc->un.value.level[0];
                        this->recvols.vol_r = mc->un.value.level[1];
                        for (i = 0; i < this->recvols.nslaves; i++) {
                                if (!(this->recvols.cur & (1 << i)))
                                        continue;
                                w = &this->w[this->recvols.slaves[i]];
                                tgt = MI_TARGET_OUTAMP;
                                cap = w->outamp_cap;
                                if (w->type == COP_AWTYPE_AUDIO_INPUT ||
                                    w->type == COP_AWTYPE_PIN_COMPLEX) {
                                        tgt = 0;
                                        cap = w->inamp_cap;
                                }
                                if (!(COP_AMPCAP_NUMSTEPS(cap)))
                                        continue;
                                mc2.type = AUDIO_MIXER_VALUE;
                                mc2.un.value.num_channels = WIDGET_CHANNELS(w);
                                mc2.un.value.level[0] = this->recvols.vol_l;
                                mc2.un.value.level[1] = this->recvols.vol_r;
                                err = azalia_mixer_set(this, w->nid,
                                    tgt, &mc2);
                                if (err) {
                                        DPRINTF(("%s: in slave %2.2x vol\n",
                                            __func__, w->nid));
                                        return err;
                                }
                        }
                } else if (mc->type == AUDIO_MIXER_ENUM) {
                        if (mc->un.ord != 0 && mc->un.ord != 1)
                                return EINVAL;
                        this->recvols.mute = mc->un.ord;
                        for (i = 0; i < this->recvols.nslaves; i++) {
                                if (!(this->recvols.cur & (1 << i)))
                                        continue;
                                w = &this->w[this->recvols.slaves[i]];
                                tgt = MI_TARGET_OUTAMP;
                                cap = w->outamp_cap;
                                if (w->type == COP_AWTYPE_AUDIO_INPUT ||
                                    w->type == COP_AWTYPE_PIN_COMPLEX) {
                                        tgt = 0;
                                        cap = w->inamp_cap;
                                }
                                if (!(cap & COP_AMPCAP_MUTE))
                                        continue;
                                mc2.type = AUDIO_MIXER_ENUM;
                                mc2.un.ord = this->recvols.mute;
                                err = azalia_mixer_set(this, w->nid,
                                    tgt, &mc2);
                                if (err) {
                                        DPRINTF(("%s: out slave %2.2x mute\n",
                                            __func__, w->nid));
                                        return err;
                                }
                        }

                } else if (mc->type == AUDIO_MIXER_SET) {
                        this->recvols.cur = (mc->un.mask & this->recvols.mask);

                } else {
                        DPRINTF(("%s: invalid input master mixer type\n",
                                XNAME(this)));
                        return EINVAL;
                }
        }

        else {
                DPRINTF(("%s: internal error in %s: target=%x\n",
                    XNAME(this), __func__, target));
                return -1;
        }
        return 0;
}

u_char
azalia_mixer_from_device_value(const codec_t *this, nid_t nid, int target,
    uint32_t dv)
{
        uint32_t steps;
        int max_gain, ctloff;

        if (IS_MI_TARGET_INAMP(target)) {
                steps = COP_AMPCAP_NUMSTEPS(this->w[nid].inamp_cap);
                ctloff = COP_AMPCAP_CTLOFF(this->w[nid].inamp_cap);
        } else if (target == MI_TARGET_OUTAMP) {
                steps = COP_AMPCAP_NUMSTEPS(this->w[nid].outamp_cap);
                ctloff = COP_AMPCAP_CTLOFF(this->w[nid].outamp_cap);
        } else {
                DPRINTF(("%s: unknown target: %d\n", __func__, target));
                steps = 255;
                ctloff = 0;
        }
        dv -= ctloff;
        if (dv <= 0 || steps == 0)
                return(AUDIO_MIN_GAIN);
        max_gain = AUDIO_MAX_GAIN - AUDIO_MAX_GAIN % steps;
        if (dv >= steps)
                return(max_gain);
        return(dv * max_gain / steps);
}

uint32_t
azalia_mixer_to_device_value(const codec_t *this, nid_t nid, int target,
    u_char uv)
{
        uint32_t steps;
        int max_gain, ctloff;

        if (IS_MI_TARGET_INAMP(target)) {
                steps = COP_AMPCAP_NUMSTEPS(this->w[nid].inamp_cap);
                ctloff = COP_AMPCAP_CTLOFF(this->w[nid].inamp_cap);
        } else if (target == MI_TARGET_OUTAMP) {
                steps = COP_AMPCAP_NUMSTEPS(this->w[nid].outamp_cap);
                ctloff = COP_AMPCAP_CTLOFF(this->w[nid].outamp_cap);
        } else {
                DPRINTF(("%s: unknown target: %d\n", __func__, target));
                steps = 255;
                ctloff = 0;
        }
        if (uv <= AUDIO_MIN_GAIN || steps == 0)
                return(ctloff);
        max_gain = AUDIO_MAX_GAIN - AUDIO_MAX_GAIN % steps;
        if (uv >= max_gain)
                return(steps + ctloff);
        return(uv * steps / max_gain + ctloff);
}

int
azalia_gpio_unmute(codec_t *this, int pin)
{
        uint32_t data, mask, dir;

        azalia_comresp(this, this->audiofunc, CORB_GET_GPIO_DATA, 0, &data);
        azalia_comresp(this, this->audiofunc, CORB_GET_GPIO_ENABLE_MASK, 0, &mask);
        azalia_comresp(this, this->audiofunc, CORB_GET_GPIO_DIRECTION, 0, &dir);

        data |= 1 << pin;
        mask |= 1 << pin;
        dir |= 1 << pin;

        azalia_comresp(this, this->audiofunc, CORB_SET_GPIO_ENABLE_MASK, mask, NULL);
        azalia_comresp(this, this->audiofunc, CORB_SET_GPIO_DIRECTION, dir, NULL);
        DELAY(1000);
        azalia_comresp(this, this->audiofunc, CORB_SET_GPIO_DATA, data, NULL);

        return 0;
}

void
azalia_ampcap_ov(widget_t *w, int type, int offset, int steps, int size,
   int ctloff, int mute)
{
        uint32_t cap;

        cap = (offset & 0x7f) | ((steps & 0x7f) << 8) |
            ((size & 0x7f) << 16) | ((ctloff & 0x7f) << 24) |
            (mute ? COP_AMPCAP_MUTE : 0);  

        if (type == COP_OUTPUT_AMPCAP) {
                w->outamp_cap = cap;
        } else if (type == COP_INPUT_AMPCAP) {
                w->inamp_cap = cap;
        }
}

void
azalia_pin_config_ov(widget_t *w, int mask, int val)
{
        int bits, offset;

        switch (mask) {
        case CORB_CD_DEVICE_MASK:
                bits = CORB_CD_DEVICE_BITS;
                offset = CORB_CD_DEVICE_OFFSET;
                break;
        case CORB_CD_PORT_MASK:
                bits = CORB_CD_PORT_BITS;
                offset = CORB_CD_PORT_OFFSET;
                break;
        default:
                return;
        }
        val &= bits;
        w->d.pin.config &= ~(mask);
        w->d.pin.config |= val << offset;
        if (mask == CORB_CD_DEVICE_MASK)
                w->d.pin.device = val;
}

int
azalia_codec_gpio_quirks(codec_t *this)
{
        if (this->qrks & AZ_QRK_GPIO_POL_0) {
                azalia_comresp(this, this->audiofunc,
                    CORB_SET_GPIO_POLARITY, 0, NULL);
        }
        if (this->qrks & AZ_QRK_GPIO_UNMUTE_0) {
                azalia_gpio_unmute(this, 0);
        }
        if (this->qrks & AZ_QRK_GPIO_UNMUTE_1) {
                azalia_gpio_unmute(this, 1);
        }
        if (this->qrks & AZ_QRK_GPIO_UNMUTE_2) {
                azalia_gpio_unmute(this, 2);
        }
        if (this->qrks & AZ_QRK_GPIO_UNMUTE_3) {
                azalia_gpio_unmute(this, 3);
        }

        return(0);
}

int
azalia_codec_widget_quirks(codec_t *this, nid_t nid)
{
        widget_t *w;

        w = &this->w[nid];

        if (this->qrks & AZ_QRK_WID_BEEP_1D &&
            nid == 0x1d && w->enable == 0) {
                azalia_pin_config_ov(w, CORB_CD_DEVICE_MASK, CORB_CD_BEEP);
                azalia_pin_config_ov(w, CORB_CD_PORT_MASK, CORB_CD_FIXED);
                w->widgetcap |= COP_AWCAP_STEREO;
                w->enable = 1;
        }

        if (this->qrks & AZ_QRK_WID_TPDOCK1 &&
            nid == 0x19) {
                /* Thinkpad x230/t430 style dock microphone */
                w->d.pin.config = 0x23a11040;
                w->enable = 1;
        }

        if (this->qrks & AZ_QRK_WID_TPDOCK1 &&
            nid == 0x1b) {
                /* Thinkpad x230/t430 style dock headphone */
                w->d.pin.config = 0x2121103f;
                w->enable = 1;
        }

        if (this->qrks & AZ_QRK_WID_TPDOCK2 &&
            nid == 0x16) {
                /* Thinkpad x240/t440 style dock headphone */
                w->d.pin.config = 0x21211010;
                w->enable = 1;
        }

        if (this->qrks & AZ_QRK_WID_TPDOCK2 &&
            nid == 0x19) {
                /* Thinkpad x240/t440 style dock microphone */
                w->d.pin.config = 0x21a11010;
                w->enable = 1;
        }

        if (this->qrks & AZ_QRK_WID_TPDOCK3 &&
            nid == 0x1a) {
                /* Thinkpad x220/t420 style dock microphone */
                w->d.pin.config = 0x21a190f0;
                w->enable = 1;
        }

        if (this->qrks & AZ_QRK_WID_TPDOCK3 &&
            nid == 0x1c) {
                /* Thinkpad x220/t420 style dock headphone */
                w->d.pin.config = 0x212140ff;
                w->enable = 1;
        }

        if (this->qrks & AZ_QRK_WID_CDIN_1C &&
            nid == 0x1c && w->enable == 0 && w->d.pin.device == CORB_CD_CD) {
                azalia_pin_config_ov(w, CORB_CD_PORT_MASK, CORB_CD_FIXED);
                w->widgetcap |= COP_AWCAP_STEREO;
                w->enable = 1;
        }

        if ((this->qrks & AZ_QRK_WID_AD1981_OAMP) &&
            ((nid == 0x05) || (nid == 0x06) || (nid == 0x07) ||
            (nid == 0x09) || (nid == 0x18))) {
                azalia_ampcap_ov(w, COP_OUTPUT_AMPCAP, 31, 33, 6, 30, 1);
        }

        if ((this->qrks & AZ_QRK_WID_CLOSE_PCBEEP) && (nid == 0x20))  {
                /* Close PC beep passthrough to avoid headphone noise */
                azalia_comresp(this, nid, CORB_SET_COEFFICIENT_INDEX, 0x36,
                    NULL);
                azalia_comresp(this, nid, CORB_SET_PROCESSING_COEFFICIENT,
                    0x57d7, NULL);
        }

        return(0);
}

/* Magic init sequence to make the right speaker work (reverse-engineered) */
void
azalia_codec_init_dolby_atmos(codec_t *this)
{
        static uint16_t atmos_init[] = {
                0x06, 0x73e, 0x00, 0x06, 0x73e, 0x80,
                0x20, 0x500, 0x26, 0x20, 0x4f0, 0x00,
                0x20, 0x500, 0x22, 0x20, 0x400, 0x31,
                0x20, 0x500, 0x23, 0x20, 0x400, 0x0b,
                0x20, 0x500, 0x25, 0x20, 0x400, 0x00,
                0x20, 0x500, 0x26, 0x20, 0x4b0, 0x10,
        };
        static struct {
                unsigned char v23;
                unsigned char v25;
        } atmos_v23_v25[] = {
                { 0x0c, 0x00 }, { 0x0d, 0x00 }, { 0x0e, 0x00 }, { 0x0f, 0x00 },
                { 0x10, 0x00 }, { 0x1a, 0x40 }, { 0x1b, 0x82 }, { 0x1c, 0x00 },
                { 0x1d, 0x00 }, { 0x1e, 0x00 }, { 0x1f, 0x00 }, { 0x20, 0xc2 },
                { 0x21, 0xc8 }, { 0x22, 0x26 }, { 0x23, 0x24 }, { 0x27, 0xff },
                { 0x28, 0xff }, { 0x29, 0xff }, { 0x2a, 0x8f }, { 0x2b, 0x02 },
                { 0x2c, 0x48 }, { 0x2d, 0x34 }, { 0x2e, 0x00 }, { 0x2f, 0x00 },
                { 0x30, 0x00 }, { 0x31, 0x00 }, { 0x32, 0x00 }, { 0x33, 0x00 },
                { 0x34, 0x00 }, { 0x35, 0x01 }, { 0x36, 0x93 }, { 0x37, 0x0c },
                { 0x38, 0x00 }, { 0x39, 0x00 }, { 0x3a, 0xf8 }, { 0x38, 0x80 },
        };
        int i;

        for (i = 0; i < nitems(atmos_init) / 3; i++) {
                if (azalia_comresp(this, atmos_init[i * 3],
                    atmos_init[(i * 3) + 1], atmos_init[(i * 3) + 2], NULL))
                        return;
        }

        for (i = 0; i < nitems(atmos_v23_v25); i++) {
                if (azalia_comresp(this, 0x06, 0x73e, 0x00, NULL))
                        return;
                if (azalia_comresp(this, 0x20, 0x500, 0x26, NULL))
                        return;
                if (azalia_comresp(this, 0x20, 0x4b0, 0x00, NULL))
                        return;
                if (i == 0) {
                        if (azalia_comresp(this, 0x21, 0xf09, 0x00, NULL))
                                return;
                }
                if (i != 20) {
                        if (azalia_comresp(this, 0x06, 0x73e, 0x80, NULL))
                                return;
                }

                if (azalia_comresp(this, 0x20, 0x500, 0x26, NULL))
                        return;
                if (azalia_comresp(this, 0x20, 0x4f0, 0x00, NULL))
                        return;
                if (azalia_comresp(this, 0x20, 0x500, 0x23, NULL))
                        return;

                if (azalia_comresp(this, 0x20, 0x400,
                    atmos_v23_v25[i].v23, NULL))
                        return;

                if (atmos_v23_v25[i].v23 != 0x1e) {
                        if (azalia_comresp(this, 0x20, 0x500, 0x25, NULL))
                                return;
                        if (azalia_comresp(this, 0x20, 0x400,
                            atmos_v23_v25[i].v25, NULL))
                                return;
                }

                if (azalia_comresp(this, 0x20, 0x500, 0x26, NULL))
                        return;
                if (azalia_comresp(this, 0x20, 0x4b0, 0x10, NULL))
                        return;
        }
}