/* * Driver for the i2c/i2s based TA3004 sound chip used * on some Apple hardware. Also known as "snapper". * * Tobias Sargeant * Based upon, tas3001c.c by Christopher C. Chimelis : * * TODO: * ----- * * Enable control over input line 2 (is this connected?) * * Implement sleep support (at least mute everything and * * set gains to minimum during sleep) * * Look into some of Darwin's tweaks regarding the mute * * lines (delays & different behaviour on some HW) * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "dmasound.h" #include "tas_common.h" #include "tas3001c.h" #include "tas_ioctl.h" #define TAS3001C_BIQUAD_FILTER_COUNT 6 #define TAS3001C_BIQUAD_CHANNEL_COUNT 2 #define VOL_DEFAULT (100 * 4 / 5) #define INPUT_DEFAULT (100 * 4 / 5) #define BASS_DEFAULT (100 / 2) #define TREBLE_DEFAULT (100 / 2) struct tas3001c_data_t { struct tas_data_t super; int device_id; int output_id; int speaker_id; struct tas_drce_t drce_state; }; static const union tas_biquad_t tas3001c_eq_unity={ .buf = { 0x100000, 0x000000, 0x000000, 0x000000, 0x000000 } }; static inline unsigned char db_to_regval(short db) { int r=0; r=(db+0x59a0) / 0x60; if (r < 0x91) return 0x91; if (r > 0xef) return 0xef; return r; } static inline short quantize_db(short db) { return db_to_regval(db) * 0x60 - 0x59a0; } static inline int register_width(enum tas3001c_reg_t r) { switch(r) { case TAS3001C_REG_MCR: case TAS3001C_REG_TREBLE: case TAS3001C_REG_BASS: return 1; case TAS3001C_REG_DRC: return 2; case TAS3001C_REG_MIXER1: case TAS3001C_REG_MIXER2: return 3; case TAS3001C_REG_VOLUME: return 6; case TAS3001C_REG_LEFT_BIQUAD0: case TAS3001C_REG_LEFT_BIQUAD1: case TAS3001C_REG_LEFT_BIQUAD2: case TAS3001C_REG_LEFT_BIQUAD3: case TAS3001C_REG_LEFT_BIQUAD4: case TAS3001C_REG_LEFT_BIQUAD5: case TAS3001C_REG_LEFT_BIQUAD6: case TAS3001C_REG_RIGHT_BIQUAD0: case TAS3001C_REG_RIGHT_BIQUAD1: case TAS3001C_REG_RIGHT_BIQUAD2: case TAS3001C_REG_RIGHT_BIQUAD3: case TAS3001C_REG_RIGHT_BIQUAD4: case TAS3001C_REG_RIGHT_BIQUAD5: case TAS3001C_REG_RIGHT_BIQUAD6: return 15; default: return 0; } } static int tas3001c_write_register( struct tas3001c_data_t *self, enum tas3001c_reg_t reg_num, char *data, uint write_mode) { if (reg_num==TAS3001C_REG_MCR || reg_num==TAS3001C_REG_BASS || reg_num==TAS3001C_REG_TREBLE) { return tas_write_byte_register(&self->super, (uint)reg_num, *data, write_mode); } else { return tas_write_register(&self->super, (uint)reg_num, register_width(reg_num), data, write_mode); } } static int tas3001c_sync_register( struct tas3001c_data_t *self, enum tas3001c_reg_t reg_num) { if (reg_num==TAS3001C_REG_MCR || reg_num==TAS3001C_REG_BASS || reg_num==TAS3001C_REG_TREBLE) { return tas_sync_byte_register(&self->super, (uint)reg_num, register_width(reg_num)); } else { return tas_sync_register(&self->super, (uint)reg_num, register_width(reg_num)); } } static int tas3001c_read_register( struct tas3001c_data_t *self, enum tas3001c_reg_t reg_num, char *data, uint write_mode) { return tas_read_register(&self->super, (uint)reg_num, register_width(reg_num), data); } static inline int tas3001c_fast_load(struct tas3001c_data_t *self, int fast) { if (fast) self->super.shadow[TAS3001C_REG_MCR][0] |= 0x80; else self->super.shadow[TAS3001C_REG_MCR][0] &= 0x7f; return tas3001c_sync_register(self,TAS3001C_REG_MCR); } static uint tas3001c_supported_mixers(struct tas3001c_data_t *self) { return SOUND_MASK_VOLUME | SOUND_MASK_PCM | SOUND_MASK_ALTPCM | SOUND_MASK_TREBLE | SOUND_MASK_BASS; } static int tas3001c_mixer_is_stereo(struct tas3001c_data_t *self,int mixer) { switch(mixer) { case SOUND_MIXER_VOLUME: return 1; default: return 0; } } static uint tas3001c_stereo_mixers(struct tas3001c_data_t *self) { uint r=tas3001c_supported_mixers(self); uint i; for (i=1; isuper.mixer[mixer]; return 0; } static int tas3001c_set_mixer_level(struct tas3001c_data_t *self,int mixer,uint level) { int rc; tas_shadow_t *shadow; uint temp; uint offset=0; if (!self) return -1; shadow=self->super.shadow; if (!tas3001c_mixer_is_stereo(self,mixer)) level = tas_mono_to_stereo(level); switch(mixer) { case SOUND_MIXER_VOLUME: temp = tas3001c_gain.master[level&0xff]; shadow[TAS3001C_REG_VOLUME][0] = (temp >> 16) & 0xff; shadow[TAS3001C_REG_VOLUME][1] = (temp >> 8) & 0xff; shadow[TAS3001C_REG_VOLUME][2] = (temp >> 0) & 0xff; temp = tas3001c_gain.master[(level>>8)&0xff]; shadow[TAS3001C_REG_VOLUME][3] = (temp >> 16) & 0xff; shadow[TAS3001C_REG_VOLUME][4] = (temp >> 8) & 0xff; shadow[TAS3001C_REG_VOLUME][5] = (temp >> 0) & 0xff; rc = tas3001c_sync_register(self,TAS3001C_REG_VOLUME); break; case SOUND_MIXER_ALTPCM: /* tas3001c_fast_load(self, 1); */ level = tas_mono_to_stereo(level); temp = tas3001c_gain.mixer[level&0xff]; shadow[TAS3001C_REG_MIXER2][offset+0] = (temp >> 16) & 0xff; shadow[TAS3001C_REG_MIXER2][offset+1] = (temp >> 8) & 0xff; shadow[TAS3001C_REG_MIXER2][offset+2] = (temp >> 0) & 0xff; rc = tas3001c_sync_register(self,TAS3001C_REG_MIXER2); /* tas3001c_fast_load(self, 0); */ break; case SOUND_MIXER_PCM: /* tas3001c_fast_load(self, 1); */ level = tas_mono_to_stereo(level); temp = tas3001c_gain.mixer[level&0xff]; shadow[TAS3001C_REG_MIXER1][offset+0] = (temp >> 16) & 0xff; shadow[TAS3001C_REG_MIXER1][offset+1] = (temp >> 8) & 0xff; shadow[TAS3001C_REG_MIXER1][offset+2] = (temp >> 0) & 0xff; rc = tas3001c_sync_register(self,TAS3001C_REG_MIXER1); /* tas3001c_fast_load(self, 0); */ break; case SOUND_MIXER_TREBLE: temp = tas3001c_gain.treble[level&0xff]; shadow[TAS3001C_REG_TREBLE][0]=temp&0xff; rc = tas3001c_sync_register(self,TAS3001C_REG_TREBLE); break; case SOUND_MIXER_BASS: temp = tas3001c_gain.bass[level&0xff]; shadow[TAS3001C_REG_BASS][0]=temp&0xff; rc = tas3001c_sync_register(self,TAS3001C_REG_BASS); break; default: rc = -1; break; } if (rc < 0) return rc; self->super.mixer[mixer]=level; return 0; } static int tas3001c_leave_sleep(struct tas3001c_data_t *self) { unsigned char mcr = (1<<6)+(2<<4)+(2<<2); if (!self) return -1; /* Make sure something answers on the i2c bus */ if (tas3001c_write_register(self, TAS3001C_REG_MCR, &mcr, WRITE_NORMAL|FORCE_WRITE) < 0) return -1; tas3001c_fast_load(self, 1); (void)tas3001c_sync_register(self,TAS3001C_REG_RIGHT_BIQUAD0); (void)tas3001c_sync_register(self,TAS3001C_REG_RIGHT_BIQUAD1); (void)tas3001c_sync_register(self,TAS3001C_REG_RIGHT_BIQUAD2); (void)tas3001c_sync_register(self,TAS3001C_REG_RIGHT_BIQUAD3); (void)tas3001c_sync_register(self,TAS3001C_REG_RIGHT_BIQUAD4); (void)tas3001c_sync_register(self,TAS3001C_REG_RIGHT_BIQUAD5); (void)tas3001c_sync_register(self,TAS3001C_REG_LEFT_BIQUAD0); (void)tas3001c_sync_register(self,TAS3001C_REG_LEFT_BIQUAD1); (void)tas3001c_sync_register(self,TAS3001C_REG_LEFT_BIQUAD2); (void)tas3001c_sync_register(self,TAS3001C_REG_LEFT_BIQUAD3); (void)tas3001c_sync_register(self,TAS3001C_REG_LEFT_BIQUAD4); (void)tas3001c_sync_register(self,TAS3001C_REG_LEFT_BIQUAD5); tas3001c_fast_load(self, 0); (void)tas3001c_sync_register(self,TAS3001C_REG_BASS); (void)tas3001c_sync_register(self,TAS3001C_REG_TREBLE); (void)tas3001c_sync_register(self,TAS3001C_REG_MIXER1); (void)tas3001c_sync_register(self,TAS3001C_REG_MIXER2); (void)tas3001c_sync_register(self,TAS3001C_REG_VOLUME); return 0; } static int tas3001c_enter_sleep(struct tas3001c_data_t *self) { /* Stub for now, but I have the details on low-power mode */ if (!self) return -1; return 0; } static int tas3001c_sync_biquad( struct tas3001c_data_t *self, u_int channel, u_int filter) { enum tas3001c_reg_t reg; if (channel >= TAS3001C_BIQUAD_CHANNEL_COUNT || filter >= TAS3001C_BIQUAD_FILTER_COUNT) return -EINVAL; reg=( channel ? TAS3001C_REG_RIGHT_BIQUAD0 : TAS3001C_REG_LEFT_BIQUAD0 ) + filter; return tas3001c_sync_register(self,reg); } static int tas3001c_write_biquad_shadow( struct tas3001c_data_t *self, u_int channel, u_int filter, const union tas_biquad_t *biquad) { tas_shadow_t *shadow=self->super.shadow; enum tas3001c_reg_t reg; if (channel >= TAS3001C_BIQUAD_CHANNEL_COUNT || filter >= TAS3001C_BIQUAD_FILTER_COUNT) return -EINVAL; reg=( channel ? TAS3001C_REG_RIGHT_BIQUAD0 : TAS3001C_REG_LEFT_BIQUAD0 ) + filter; SET_4_20(shadow[reg], 0,biquad->coeff.b0); SET_4_20(shadow[reg], 3,biquad->coeff.b1); SET_4_20(shadow[reg], 6,biquad->coeff.b2); SET_4_20(shadow[reg], 9,biquad->coeff.a1); SET_4_20(shadow[reg],12,biquad->coeff.a2); return 0; } static int tas3001c_write_biquad( struct tas3001c_data_t *self, u_int channel, u_int filter, const union tas_biquad_t *biquad) { int rc; rc=tas3001c_write_biquad_shadow(self, channel, filter, biquad); if (rc < 0) return rc; return tas3001c_sync_biquad(self, channel, filter); } static int tas3001c_write_biquad_list( struct tas3001c_data_t *self, u_int filter_count, u_int flags, struct tas_biquad_ctrl_t *biquads) { int i; int rc; if (flags & TAS_BIQUAD_FAST_LOAD) tas3001c_fast_load(self,1); for (i=0; isuper.shadow; enum tas3001c_reg_t reg; if (channel >= TAS3001C_BIQUAD_CHANNEL_COUNT || filter >= TAS3001C_BIQUAD_FILTER_COUNT) return -EINVAL; reg=( channel ? TAS3001C_REG_RIGHT_BIQUAD0 : TAS3001C_REG_LEFT_BIQUAD0 ) + filter; biquad->coeff.b0=GET_4_20(shadow[reg], 0); biquad->coeff.b1=GET_4_20(shadow[reg], 3); biquad->coeff.b2=GET_4_20(shadow[reg], 6); biquad->coeff.a1=GET_4_20(shadow[reg], 9); biquad->coeff.a2=GET_4_20(shadow[reg],12); return 0; } static int tas3001c_eq_rw( struct tas3001c_data_t *self, u_int cmd, u_long arg) { int rc; struct tas_biquad_ctrl_t biquad; void __user *argp = (void __user *)arg; if (copy_from_user(&biquad, argp, sizeof(struct tas_biquad_ctrl_t))) { return -EFAULT; } if (cmd & SIOC_IN) { rc=tas3001c_write_biquad(self, biquad.channel, biquad.filter, &biquad.data); if (rc != 0) return rc; } if (cmd & SIOC_OUT) { rc=tas3001c_read_biquad(self, biquad.channel, biquad.filter, &biquad.data); if (rc != 0) return rc; if (copy_to_user(argp, &biquad, sizeof(struct tas_biquad_ctrl_t))) { return -EFAULT; } } return 0; } static int tas3001c_eq_list_rw( struct tas3001c_data_t *self, u_int cmd, u_long arg) { int rc; int filter_count; int flags; int i,j; char sync_required[2][6]; struct tas_biquad_ctrl_t biquad; struct tas_biquad_ctrl_list_t __user *argp = (void __user *)arg; memset(sync_required,0,sizeof(sync_required)); if (copy_from_user(&filter_count, &argp->filter_count, sizeof(int))) return -EFAULT; if (copy_from_user(&flags, &argp->flags, sizeof(int))) return -EFAULT; if (cmd & SIOC_IN) { } for (i=0; i < filter_count; i++) { if (copy_from_user(&biquad, &argp->biquads[i], sizeof(struct tas_biquad_ctrl_t))) { return -EFAULT; } if (cmd & SIOC_IN) { sync_required[biquad.channel][biquad.filter]=1; rc=tas3001c_write_biquad_shadow(self, biquad.channel, biquad.filter, &biquad.data); if (rc != 0) return rc; } if (cmd & SIOC_OUT) { rc=tas3001c_read_biquad(self, biquad.channel, biquad.filter, &biquad.data); if (rc != 0) return rc; if (copy_to_user(&argp->biquads[i], &biquad, sizeof(struct tas_biquad_ctrl_t))) { return -EFAULT; } } } if (cmd & SIOC_IN) { if (flags & TAS_BIQUAD_FAST_LOAD) tas3001c_fast_load(self,1); for (i=0; i<2; i++) { for (j=0; j<6; j++) { if (sync_required[i][j]) { rc=tas3001c_sync_biquad(self, i, j); if (rc < 0) return rc; } } } if (flags & TAS_BIQUAD_FAST_LOAD) { tas3001c_fast_load(self,0); /* now we need to set up the mixers again, because leaving fast mode resets them. */ (void)tas3001c_sync_register(self,TAS3001C_REG_BASS); (void)tas3001c_sync_register(self,TAS3001C_REG_TREBLE); (void)tas3001c_sync_register(self,TAS3001C_REG_MIXER1); (void)tas3001c_sync_register(self,TAS3001C_REG_MIXER2); (void)tas3001c_sync_register(self,TAS3001C_REG_VOLUME); } } return 0; } static int tas3001c_update_drce( struct tas3001c_data_t *self, int flags, struct tas_drce_t *drce) { tas_shadow_t *shadow; shadow=self->super.shadow; shadow[TAS3001C_REG_DRC][1] = 0xc1; if (flags & TAS_DRCE_THRESHOLD) { self->drce_state.threshold=quantize_db(drce->threshold); shadow[TAS3001C_REG_DRC][2] = db_to_regval(self->drce_state.threshold); } if (flags & TAS_DRCE_ENABLE) { self->drce_state.enable = drce->enable; } if (!self->drce_state.enable) { shadow[TAS3001C_REG_DRC][0] = 0xf0; } #ifdef DEBUG_DRCE printk("DRCE IOCTL: set [ ENABLE:%x THRESH:%x\n", self->drce_state.enable, self->drce_state.threshold); printk("DRCE IOCTL: reg [ %02x %02x ]\n", (unsigned char)shadow[TAS3001C_REG_DRC][0], (unsigned char)shadow[TAS3001C_REG_DRC][1]); #endif return tas3001c_sync_register(self, TAS3001C_REG_DRC); } static int tas3001c_drce_rw( struct tas3001c_data_t *self, u_int cmd, u_long arg) { int rc; struct tas_drce_ctrl_t drce_ctrl; void __user *argp = (void __user *)arg; if (copy_from_user(&drce_ctrl, argp, sizeof(struct tas_drce_ctrl_t))) return -EFAULT; #ifdef DEBUG_DRCE printk("DRCE IOCTL: input [ FLAGS:%x ENABLE:%x THRESH:%x\n", drce_ctrl.flags, drce_ctrl.data.enable, drce_ctrl.data.threshold); #endif if (cmd & SIOC_IN) { rc = tas3001c_update_drce(self, drce_ctrl.flags, &drce_ctrl.data); if (rc < 0) return rc; } if (cmd & SIOC_OUT) { if (drce_ctrl.flags & TAS_DRCE_ENABLE) drce_ctrl.data.enable = self->drce_state.enable; if (drce_ctrl.flags & TAS_DRCE_THRESHOLD) drce_ctrl.data.threshold = self->drce_state.threshold; if (copy_to_user(argp, &drce_ctrl, sizeof(struct tas_drce_ctrl_t))) { return -EFAULT; } } return 0; } static void tas3001c_update_device_parameters(struct tas3001c_data_t *self) { int i,j; if (!self) return; if (self->output_id == TAS_OUTPUT_HEADPHONES) { tas3001c_fast_load(self, 1); for (i=0; idevice_id == self->device_id && (eq->output_id == 0 || eq->output_id == self->output_id) && (eq->speaker_id == 0 || eq->speaker_id == self->speaker_id)) { tas3001c_update_drce(self, TAS_DRCE_ALL, eq->drce); tas3001c_write_biquad_list(self, eq->filter_count, TAS_BIQUAD_FAST_LOAD, eq->biquads); break; } } } static void tas3001c_device_change_handler(void *self) { if (self) tas3001c_update_device_parameters(self); } static struct work_struct device_change; static int tas3001c_output_device_change( struct tas3001c_data_t *self, int device_id, int output_id, int speaker_id) { self->device_id=device_id; self->output_id=output_id; self->speaker_id=speaker_id; schedule_work(&device_change); return 0; } static int tas3001c_device_ioctl( struct tas3001c_data_t *self, u_int cmd, u_long arg) { uint __user *argp = (void __user *)arg; switch (cmd) { case TAS_READ_EQ: case TAS_WRITE_EQ: return tas3001c_eq_rw(self, cmd, arg); case TAS_READ_EQ_LIST: case TAS_WRITE_EQ_LIST: return tas3001c_eq_list_rw(self, cmd, arg); case TAS_READ_EQ_FILTER_COUNT: put_user(TAS3001C_BIQUAD_FILTER_COUNT, argp); return 0; case TAS_READ_EQ_CHANNEL_COUNT: put_user(TAS3001C_BIQUAD_CHANNEL_COUNT, argp); return 0; case TAS_READ_DRCE: case TAS_WRITE_DRCE: return tas3001c_drce_rw(self, cmd, arg); case TAS_READ_DRCE_CAPS: put_user(TAS_DRCE_ENABLE | TAS_DRCE_THRESHOLD, argp); return 0; case TAS_READ_DRCE_MIN: case TAS_READ_DRCE_MAX: { struct tas_drce_ctrl_t drce_ctrl; if (copy_from_user(&drce_ctrl, argp, sizeof(struct tas_drce_ctrl_t))) { return -EFAULT; } if (drce_ctrl.flags & TAS_DRCE_THRESHOLD) { if (cmd == TAS_READ_DRCE_MIN) { drce_ctrl.data.threshold=-36<<8; } else { drce_ctrl.data.threshold=-6<<8; } } if (copy_to_user(argp, &drce_ctrl, sizeof(struct tas_drce_ctrl_t))) { return -EFAULT; } } } return -EINVAL; } static int tas3001c_init_mixer(struct tas3001c_data_t *self) { unsigned char mcr = (1<<6)+(2<<4)+(2<<2); /* Make sure something answers on the i2c bus */ if (tas3001c_write_register(self, TAS3001C_REG_MCR, &mcr, WRITE_NORMAL|FORCE_WRITE) < 0) return -1; tas3001c_fast_load(self, 1); (void)tas3001c_sync_register(self,TAS3001C_REG_RIGHT_BIQUAD0); (void)tas3001c_sync_register(self,TAS3001C_REG_RIGHT_BIQUAD1); (void)tas3001c_sync_register(self,TAS3001C_REG_RIGHT_BIQUAD2); (void)tas3001c_sync_register(self,TAS3001C_REG_RIGHT_BIQUAD3); (void)tas3001c_sync_register(self,TAS3001C_REG_RIGHT_BIQUAD4); (void)tas3001c_sync_register(self,TAS3001C_REG_RIGHT_BIQUAD5); (void)tas3001c_sync_register(self,TAS3001C_REG_RIGHT_BIQUAD6); (void)tas3001c_sync_register(self,TAS3001C_REG_LEFT_BIQUAD0); (void)tas3001c_sync_register(self,TAS3001C_REG_LEFT_BIQUAD1); (void)tas3001c_sync_register(self,TAS3001C_REG_LEFT_BIQUAD2); (void)tas3001c_sync_register(self,TAS3001C_REG_LEFT_BIQUAD3); (void)tas3001c_sync_register(self,TAS3001C_REG_LEFT_BIQUAD4); (void)tas3001c_sync_register(self,TAS3001C_REG_LEFT_BIQUAD5); (void)tas3001c_sync_register(self,TAS3001C_REG_LEFT_BIQUAD6); tas3001c_fast_load(self, 0); tas3001c_set_mixer_level(self, SOUND_MIXER_VOLUME, VOL_DEFAULT<<8 | VOL_DEFAULT); tas3001c_set_mixer_level(self, SOUND_MIXER_PCM, INPUT_DEFAULT<<8 | INPUT_DEFAULT); tas3001c_set_mixer_level(self, SOUND_MIXER_ALTPCM, 0); tas3001c_set_mixer_level(self, SOUND_MIXER_BASS, BASS_DEFAULT); tas3001c_set_mixer_level(self, SOUND_MIXER_TREBLE, TREBLE_DEFAULT); return 0; } static int tas3001c_uninit_mixer(struct tas3001c_data_t *self) { tas3001c_set_mixer_level(self, SOUND_MIXER_VOLUME, 0); tas3001c_set_mixer_level(self, SOUND_MIXER_PCM, 0); tas3001c_set_mixer_level(self, SOUND_MIXER_ALTPCM, 0); tas3001c_set_mixer_level(self, SOUND_MIXER_BASS, 0); tas3001c_set_mixer_level(self, SOUND_MIXER_TREBLE, 0); return 0; } static int tas3001c_init(struct i2c_client *client) { struct tas3001c_data_t *self; size_t sz = sizeof(*self) + (TAS3001C_REG_MAX*sizeof(tas_shadow_t)); int i, j; self = kmalloc(sz, GFP_KERNEL); if (!self) return -ENOMEM; memset(self, 0, sz); self->super.client = client; self->super.shadow = (tas_shadow_t *)(self+1); self->output_id = TAS_OUTPUT_HEADPHONES; dev_set_drvdata(&client->dev, self); for (i = 0; i < TAS3001C_BIQUAD_CHANNEL_COUNT; i++) for (j = 0; j < TAS3001C_BIQUAD_FILTER_COUNT; j++) tas3001c_write_biquad_shadow(self, i, j, &tas3001c_eq_unity); INIT_WORK(&device_change, tas3001c_device_change_handler, self); return 0; } static void tas3001c_uninit(struct tas3001c_data_t *self) { tas3001c_uninit_mixer(self); kfree(self); } struct tas_driver_hooks_t tas3001c_hooks = { .init = (tas_hook_init_t)tas3001c_init, .post_init = (tas_hook_post_init_t)tas3001c_init_mixer, .uninit = (tas_hook_uninit_t)tas3001c_uninit, .get_mixer_level = (tas_hook_get_mixer_level_t)tas3001c_get_mixer_level, .set_mixer_level = (tas_hook_set_mixer_level_t)tas3001c_set_mixer_level, .enter_sleep = (tas_hook_enter_sleep_t)tas3001c_enter_sleep, .leave_sleep = (tas_hook_leave_sleep_t)tas3001c_leave_sleep, .supported_mixers = (tas_hook_supported_mixers_t)tas3001c_supported_mixers, .mixer_is_stereo = (tas_hook_mixer_is_stereo_t)tas3001c_mixer_is_stereo, .stereo_mixers = (tas_hook_stereo_mixers_t)tas3001c_stereo_mixers, .output_device_change = (tas_hook_output_device_change_t)tas3001c_output_device_change, .device_ioctl = (tas_hook_device_ioctl_t)tas3001c_device_ioctl };