forked from Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1180 lines
27 KiB
1180 lines
27 KiB
// SPDX-License-Identifier: GPL-2.0-only |
|
/* |
|
* Apple Onboard Audio driver -- layout/machine id fabric |
|
* |
|
* Copyright 2006-2008 Johannes Berg <[email protected]> |
|
* |
|
* This fabric module looks for sound codecs based on the |
|
* layout-id or device-id property in the device tree. |
|
*/ |
|
#include <asm/prom.h> |
|
#include <linux/list.h> |
|
#include <linux/module.h> |
|
#include <linux/slab.h> |
|
#include "../aoa.h" |
|
#include "../soundbus/soundbus.h" |
|
|
|
MODULE_AUTHOR("Johannes Berg <[email protected]>"); |
|
MODULE_LICENSE("GPL"); |
|
MODULE_DESCRIPTION("Layout-ID fabric for snd-aoa"); |
|
|
|
#define MAX_CODECS_PER_BUS 2 |
|
|
|
/* These are the connections the layout fabric |
|
* knows about. It doesn't really care about the |
|
* input ones, but I thought I'd separate them |
|
* to give them proper names. The thing is that |
|
* Apple usually will distinguish the active output |
|
* by GPIOs, while the active input is set directly |
|
* on the codec. Hence we here tell the codec what |
|
* we think is connected. This information is hard- |
|
* coded below ... */ |
|
#define CC_SPEAKERS (1<<0) |
|
#define CC_HEADPHONE (1<<1) |
|
#define CC_LINEOUT (1<<2) |
|
#define CC_DIGITALOUT (1<<3) |
|
#define CC_LINEIN (1<<4) |
|
#define CC_MICROPHONE (1<<5) |
|
#define CC_DIGITALIN (1<<6) |
|
/* pretty bogus but users complain... |
|
* This is a flag saying that the LINEOUT |
|
* should be renamed to HEADPHONE. |
|
* be careful with input detection! */ |
|
#define CC_LINEOUT_LABELLED_HEADPHONE (1<<7) |
|
|
|
struct codec_connection { |
|
/* CC_ flags from above */ |
|
int connected; |
|
/* codec dependent bit to be set in the aoa_codec.connected field. |
|
* This intentionally doesn't have any generic flags because the |
|
* fabric has to know the codec anyway and all codecs might have |
|
* different connectors */ |
|
int codec_bit; |
|
}; |
|
|
|
struct codec_connect_info { |
|
char *name; |
|
struct codec_connection *connections; |
|
}; |
|
|
|
#define LAYOUT_FLAG_COMBO_LINEOUT_SPDIF (1<<0) |
|
|
|
struct layout { |
|
unsigned int layout_id, device_id; |
|
struct codec_connect_info codecs[MAX_CODECS_PER_BUS]; |
|
int flags; |
|
|
|
/* if busname is not assigned, we use 'Master' below, |
|
* so that our layout table doesn't need to be filled |
|
* too much. |
|
* We only assign these two if we expect to find more |
|
* than one soundbus, i.e. on those machines with |
|
* multiple layout-ids */ |
|
char *busname; |
|
int pcmid; |
|
}; |
|
|
|
MODULE_ALIAS("sound-layout-36"); |
|
MODULE_ALIAS("sound-layout-41"); |
|
MODULE_ALIAS("sound-layout-45"); |
|
MODULE_ALIAS("sound-layout-47"); |
|
MODULE_ALIAS("sound-layout-48"); |
|
MODULE_ALIAS("sound-layout-49"); |
|
MODULE_ALIAS("sound-layout-50"); |
|
MODULE_ALIAS("sound-layout-51"); |
|
MODULE_ALIAS("sound-layout-56"); |
|
MODULE_ALIAS("sound-layout-57"); |
|
MODULE_ALIAS("sound-layout-58"); |
|
MODULE_ALIAS("sound-layout-60"); |
|
MODULE_ALIAS("sound-layout-61"); |
|
MODULE_ALIAS("sound-layout-62"); |
|
MODULE_ALIAS("sound-layout-64"); |
|
MODULE_ALIAS("sound-layout-65"); |
|
MODULE_ALIAS("sound-layout-66"); |
|
MODULE_ALIAS("sound-layout-67"); |
|
MODULE_ALIAS("sound-layout-68"); |
|
MODULE_ALIAS("sound-layout-69"); |
|
MODULE_ALIAS("sound-layout-70"); |
|
MODULE_ALIAS("sound-layout-72"); |
|
MODULE_ALIAS("sound-layout-76"); |
|
MODULE_ALIAS("sound-layout-80"); |
|
MODULE_ALIAS("sound-layout-82"); |
|
MODULE_ALIAS("sound-layout-84"); |
|
MODULE_ALIAS("sound-layout-86"); |
|
MODULE_ALIAS("sound-layout-90"); |
|
MODULE_ALIAS("sound-layout-92"); |
|
MODULE_ALIAS("sound-layout-94"); |
|
MODULE_ALIAS("sound-layout-96"); |
|
MODULE_ALIAS("sound-layout-98"); |
|
MODULE_ALIAS("sound-layout-100"); |
|
|
|
MODULE_ALIAS("aoa-device-id-14"); |
|
MODULE_ALIAS("aoa-device-id-22"); |
|
MODULE_ALIAS("aoa-device-id-31"); |
|
MODULE_ALIAS("aoa-device-id-35"); |
|
MODULE_ALIAS("aoa-device-id-44"); |
|
|
|
/* onyx with all but microphone connected */ |
|
static struct codec_connection onyx_connections_nomic[] = { |
|
{ |
|
.connected = CC_SPEAKERS | CC_HEADPHONE | CC_LINEOUT, |
|
.codec_bit = 0, |
|
}, |
|
{ |
|
.connected = CC_DIGITALOUT, |
|
.codec_bit = 1, |
|
}, |
|
{ |
|
.connected = CC_LINEIN, |
|
.codec_bit = 2, |
|
}, |
|
{} /* terminate array by .connected == 0 */ |
|
}; |
|
|
|
/* onyx on machines without headphone */ |
|
static struct codec_connection onyx_connections_noheadphones[] = { |
|
{ |
|
.connected = CC_SPEAKERS | CC_LINEOUT | |
|
CC_LINEOUT_LABELLED_HEADPHONE, |
|
.codec_bit = 0, |
|
}, |
|
{ |
|
.connected = CC_DIGITALOUT, |
|
.codec_bit = 1, |
|
}, |
|
/* FIXME: are these correct? probably not for all the machines |
|
* below ... If not this will need separating. */ |
|
{ |
|
.connected = CC_LINEIN, |
|
.codec_bit = 2, |
|
}, |
|
{ |
|
.connected = CC_MICROPHONE, |
|
.codec_bit = 3, |
|
}, |
|
{} /* terminate array by .connected == 0 */ |
|
}; |
|
|
|
/* onyx on machines with real line-out */ |
|
static struct codec_connection onyx_connections_reallineout[] = { |
|
{ |
|
.connected = CC_SPEAKERS | CC_LINEOUT | CC_HEADPHONE, |
|
.codec_bit = 0, |
|
}, |
|
{ |
|
.connected = CC_DIGITALOUT, |
|
.codec_bit = 1, |
|
}, |
|
{ |
|
.connected = CC_LINEIN, |
|
.codec_bit = 2, |
|
}, |
|
{} /* terminate array by .connected == 0 */ |
|
}; |
|
|
|
/* tas on machines without line out */ |
|
static struct codec_connection tas_connections_nolineout[] = { |
|
{ |
|
.connected = CC_SPEAKERS | CC_HEADPHONE, |
|
.codec_bit = 0, |
|
}, |
|
{ |
|
.connected = CC_LINEIN, |
|
.codec_bit = 2, |
|
}, |
|
{ |
|
.connected = CC_MICROPHONE, |
|
.codec_bit = 3, |
|
}, |
|
{} /* terminate array by .connected == 0 */ |
|
}; |
|
|
|
/* tas on machines with neither line out nor line in */ |
|
static struct codec_connection tas_connections_noline[] = { |
|
{ |
|
.connected = CC_SPEAKERS | CC_HEADPHONE, |
|
.codec_bit = 0, |
|
}, |
|
{ |
|
.connected = CC_MICROPHONE, |
|
.codec_bit = 3, |
|
}, |
|
{} /* terminate array by .connected == 0 */ |
|
}; |
|
|
|
/* tas on machines without microphone */ |
|
static struct codec_connection tas_connections_nomic[] = { |
|
{ |
|
.connected = CC_SPEAKERS | CC_HEADPHONE | CC_LINEOUT, |
|
.codec_bit = 0, |
|
}, |
|
{ |
|
.connected = CC_LINEIN, |
|
.codec_bit = 2, |
|
}, |
|
{} /* terminate array by .connected == 0 */ |
|
}; |
|
|
|
/* tas on machines with everything connected */ |
|
static struct codec_connection tas_connections_all[] = { |
|
{ |
|
.connected = CC_SPEAKERS | CC_HEADPHONE | CC_LINEOUT, |
|
.codec_bit = 0, |
|
}, |
|
{ |
|
.connected = CC_LINEIN, |
|
.codec_bit = 2, |
|
}, |
|
{ |
|
.connected = CC_MICROPHONE, |
|
.codec_bit = 3, |
|
}, |
|
{} /* terminate array by .connected == 0 */ |
|
}; |
|
|
|
static struct codec_connection toonie_connections[] = { |
|
{ |
|
.connected = CC_SPEAKERS | CC_HEADPHONE, |
|
.codec_bit = 0, |
|
}, |
|
{} /* terminate array by .connected == 0 */ |
|
}; |
|
|
|
static struct codec_connection topaz_input[] = { |
|
{ |
|
.connected = CC_DIGITALIN, |
|
.codec_bit = 0, |
|
}, |
|
{} /* terminate array by .connected == 0 */ |
|
}; |
|
|
|
static struct codec_connection topaz_output[] = { |
|
{ |
|
.connected = CC_DIGITALOUT, |
|
.codec_bit = 1, |
|
}, |
|
{} /* terminate array by .connected == 0 */ |
|
}; |
|
|
|
static struct codec_connection topaz_inout[] = { |
|
{ |
|
.connected = CC_DIGITALIN, |
|
.codec_bit = 0, |
|
}, |
|
{ |
|
.connected = CC_DIGITALOUT, |
|
.codec_bit = 1, |
|
}, |
|
{} /* terminate array by .connected == 0 */ |
|
}; |
|
|
|
static struct layout layouts[] = { |
|
/* last PowerBooks (15" Oct 2005) */ |
|
{ .layout_id = 82, |
|
.flags = LAYOUT_FLAG_COMBO_LINEOUT_SPDIF, |
|
.codecs[0] = { |
|
.name = "onyx", |
|
.connections = onyx_connections_noheadphones, |
|
}, |
|
.codecs[1] = { |
|
.name = "topaz", |
|
.connections = topaz_input, |
|
}, |
|
}, |
|
/* PowerMac9,1 */ |
|
{ .layout_id = 60, |
|
.codecs[0] = { |
|
.name = "onyx", |
|
.connections = onyx_connections_reallineout, |
|
}, |
|
}, |
|
/* PowerMac9,1 */ |
|
{ .layout_id = 61, |
|
.codecs[0] = { |
|
.name = "topaz", |
|
.connections = topaz_input, |
|
}, |
|
}, |
|
/* PowerBook5,7 */ |
|
{ .layout_id = 64, |
|
.flags = LAYOUT_FLAG_COMBO_LINEOUT_SPDIF, |
|
.codecs[0] = { |
|
.name = "onyx", |
|
.connections = onyx_connections_noheadphones, |
|
}, |
|
}, |
|
/* PowerBook5,7 */ |
|
{ .layout_id = 65, |
|
.codecs[0] = { |
|
.name = "topaz", |
|
.connections = topaz_input, |
|
}, |
|
}, |
|
/* PowerBook5,9 [17" Oct 2005] */ |
|
{ .layout_id = 84, |
|
.flags = LAYOUT_FLAG_COMBO_LINEOUT_SPDIF, |
|
.codecs[0] = { |
|
.name = "onyx", |
|
.connections = onyx_connections_noheadphones, |
|
}, |
|
.codecs[1] = { |
|
.name = "topaz", |
|
.connections = topaz_input, |
|
}, |
|
}, |
|
/* PowerMac8,1 */ |
|
{ .layout_id = 45, |
|
.codecs[0] = { |
|
.name = "onyx", |
|
.connections = onyx_connections_noheadphones, |
|
}, |
|
.codecs[1] = { |
|
.name = "topaz", |
|
.connections = topaz_input, |
|
}, |
|
}, |
|
/* Quad PowerMac (analog in, analog/digital out) */ |
|
{ .layout_id = 68, |
|
.codecs[0] = { |
|
.name = "onyx", |
|
.connections = onyx_connections_nomic, |
|
}, |
|
}, |
|
/* Quad PowerMac (digital in) */ |
|
{ .layout_id = 69, |
|
.codecs[0] = { |
|
.name = "topaz", |
|
.connections = topaz_input, |
|
}, |
|
.busname = "digital in", .pcmid = 1 }, |
|
/* Early 2005 PowerBook (PowerBook 5,6) */ |
|
{ .layout_id = 70, |
|
.codecs[0] = { |
|
.name = "tas", |
|
.connections = tas_connections_nolineout, |
|
}, |
|
}, |
|
/* PowerBook 5,4 */ |
|
{ .layout_id = 51, |
|
.codecs[0] = { |
|
.name = "tas", |
|
.connections = tas_connections_nolineout, |
|
}, |
|
}, |
|
/* PowerBook6,1 */ |
|
{ .device_id = 31, |
|
.codecs[0] = { |
|
.name = "tas", |
|
.connections = tas_connections_nolineout, |
|
}, |
|
}, |
|
/* PowerBook6,5 */ |
|
{ .device_id = 44, |
|
.codecs[0] = { |
|
.name = "tas", |
|
.connections = tas_connections_all, |
|
}, |
|
}, |
|
/* PowerBook6,7 */ |
|
{ .layout_id = 80, |
|
.codecs[0] = { |
|
.name = "tas", |
|
.connections = tas_connections_noline, |
|
}, |
|
}, |
|
/* PowerBook6,8 */ |
|
{ .layout_id = 72, |
|
.codecs[0] = { |
|
.name = "tas", |
|
.connections = tas_connections_nolineout, |
|
}, |
|
}, |
|
/* PowerMac8,2 */ |
|
{ .layout_id = 86, |
|
.codecs[0] = { |
|
.name = "onyx", |
|
.connections = onyx_connections_nomic, |
|
}, |
|
.codecs[1] = { |
|
.name = "topaz", |
|
.connections = topaz_input, |
|
}, |
|
}, |
|
/* PowerBook6,7 */ |
|
{ .layout_id = 92, |
|
.codecs[0] = { |
|
.name = "tas", |
|
.connections = tas_connections_nolineout, |
|
}, |
|
}, |
|
/* PowerMac10,1 (Mac Mini) */ |
|
{ .layout_id = 58, |
|
.codecs[0] = { |
|
.name = "toonie", |
|
.connections = toonie_connections, |
|
}, |
|
}, |
|
{ |
|
.layout_id = 96, |
|
.codecs[0] = { |
|
.name = "onyx", |
|
.connections = onyx_connections_noheadphones, |
|
}, |
|
}, |
|
/* unknown, untested, but this comes from Apple */ |
|
{ .layout_id = 41, |
|
.codecs[0] = { |
|
.name = "tas", |
|
.connections = tas_connections_all, |
|
}, |
|
}, |
|
{ .layout_id = 36, |
|
.codecs[0] = { |
|
.name = "tas", |
|
.connections = tas_connections_nomic, |
|
}, |
|
.codecs[1] = { |
|
.name = "topaz", |
|
.connections = topaz_inout, |
|
}, |
|
}, |
|
{ .layout_id = 47, |
|
.codecs[0] = { |
|
.name = "onyx", |
|
.connections = onyx_connections_noheadphones, |
|
}, |
|
}, |
|
{ .layout_id = 48, |
|
.codecs[0] = { |
|
.name = "topaz", |
|
.connections = topaz_input, |
|
}, |
|
}, |
|
{ .layout_id = 49, |
|
.codecs[0] = { |
|
.name = "onyx", |
|
.connections = onyx_connections_nomic, |
|
}, |
|
}, |
|
{ .layout_id = 50, |
|
.codecs[0] = { |
|
.name = "topaz", |
|
.connections = topaz_input, |
|
}, |
|
}, |
|
{ .layout_id = 56, |
|
.codecs[0] = { |
|
.name = "onyx", |
|
.connections = onyx_connections_noheadphones, |
|
}, |
|
}, |
|
{ .layout_id = 57, |
|
.codecs[0] = { |
|
.name = "topaz", |
|
.connections = topaz_input, |
|
}, |
|
}, |
|
{ .layout_id = 62, |
|
.codecs[0] = { |
|
.name = "onyx", |
|
.connections = onyx_connections_noheadphones, |
|
}, |
|
.codecs[1] = { |
|
.name = "topaz", |
|
.connections = topaz_output, |
|
}, |
|
}, |
|
{ .layout_id = 66, |
|
.codecs[0] = { |
|
.name = "onyx", |
|
.connections = onyx_connections_noheadphones, |
|
}, |
|
}, |
|
{ .layout_id = 67, |
|
.codecs[0] = { |
|
.name = "topaz", |
|
.connections = topaz_input, |
|
}, |
|
}, |
|
{ .layout_id = 76, |
|
.codecs[0] = { |
|
.name = "tas", |
|
.connections = tas_connections_nomic, |
|
}, |
|
.codecs[1] = { |
|
.name = "topaz", |
|
.connections = topaz_inout, |
|
}, |
|
}, |
|
{ .layout_id = 90, |
|
.codecs[0] = { |
|
.name = "tas", |
|
.connections = tas_connections_noline, |
|
}, |
|
}, |
|
{ .layout_id = 94, |
|
.codecs[0] = { |
|
.name = "onyx", |
|
/* but it has an external mic?? how to select? */ |
|
.connections = onyx_connections_noheadphones, |
|
}, |
|
}, |
|
{ .layout_id = 98, |
|
.codecs[0] = { |
|
.name = "toonie", |
|
.connections = toonie_connections, |
|
}, |
|
}, |
|
{ .layout_id = 100, |
|
.codecs[0] = { |
|
.name = "topaz", |
|
.connections = topaz_input, |
|
}, |
|
.codecs[1] = { |
|
.name = "onyx", |
|
.connections = onyx_connections_noheadphones, |
|
}, |
|
}, |
|
/* PowerMac3,4 */ |
|
{ .device_id = 14, |
|
.codecs[0] = { |
|
.name = "tas", |
|
.connections = tas_connections_noline, |
|
}, |
|
}, |
|
/* PowerMac3,6 */ |
|
{ .device_id = 22, |
|
.codecs[0] = { |
|
.name = "tas", |
|
.connections = tas_connections_all, |
|
}, |
|
}, |
|
/* PowerBook5,2 */ |
|
{ .device_id = 35, |
|
.codecs[0] = { |
|
.name = "tas", |
|
.connections = tas_connections_all, |
|
}, |
|
}, |
|
{} |
|
}; |
|
|
|
static struct layout *find_layout_by_id(unsigned int id) |
|
{ |
|
struct layout *l; |
|
|
|
l = layouts; |
|
while (l->codecs[0].name) { |
|
if (l->layout_id == id) |
|
return l; |
|
l++; |
|
} |
|
return NULL; |
|
} |
|
|
|
static struct layout *find_layout_by_device(unsigned int id) |
|
{ |
|
struct layout *l; |
|
|
|
l = layouts; |
|
while (l->codecs[0].name) { |
|
if (l->device_id == id) |
|
return l; |
|
l++; |
|
} |
|
return NULL; |
|
} |
|
|
|
static void use_layout(struct layout *l) |
|
{ |
|
int i; |
|
|
|
for (i=0; i<MAX_CODECS_PER_BUS; i++) { |
|
if (l->codecs[i].name) { |
|
request_module("snd-aoa-codec-%s", l->codecs[i].name); |
|
} |
|
} |
|
/* now we wait for the codecs to call us back */ |
|
} |
|
|
|
struct layout_dev; |
|
|
|
struct layout_dev_ptr { |
|
struct layout_dev *ptr; |
|
}; |
|
|
|
struct layout_dev { |
|
struct list_head list; |
|
struct soundbus_dev *sdev; |
|
struct device_node *sound; |
|
struct aoa_codec *codecs[MAX_CODECS_PER_BUS]; |
|
struct layout *layout; |
|
struct gpio_runtime gpio; |
|
|
|
/* we need these for headphone/lineout detection */ |
|
struct snd_kcontrol *headphone_ctrl; |
|
struct snd_kcontrol *lineout_ctrl; |
|
struct snd_kcontrol *speaker_ctrl; |
|
struct snd_kcontrol *master_ctrl; |
|
struct snd_kcontrol *headphone_detected_ctrl; |
|
struct snd_kcontrol *lineout_detected_ctrl; |
|
|
|
struct layout_dev_ptr selfptr_headphone; |
|
struct layout_dev_ptr selfptr_lineout; |
|
|
|
u32 have_lineout_detect:1, |
|
have_headphone_detect:1, |
|
switch_on_headphone:1, |
|
switch_on_lineout:1; |
|
}; |
|
|
|
static LIST_HEAD(layouts_list); |
|
static int layouts_list_items; |
|
/* this can go away but only if we allow multiple cards, |
|
* make the fabric handle all the card stuff, etc... */ |
|
static struct layout_dev *layout_device; |
|
|
|
#define control_info snd_ctl_boolean_mono_info |
|
|
|
#define AMP_CONTROL(n, description) \ |
|
static int n##_control_get(struct snd_kcontrol *kcontrol, \ |
|
struct snd_ctl_elem_value *ucontrol) \ |
|
{ \ |
|
struct gpio_runtime *gpio = snd_kcontrol_chip(kcontrol); \ |
|
if (gpio->methods && gpio->methods->get_##n) \ |
|
ucontrol->value.integer.value[0] = \ |
|
gpio->methods->get_##n(gpio); \ |
|
return 0; \ |
|
} \ |
|
static int n##_control_put(struct snd_kcontrol *kcontrol, \ |
|
struct snd_ctl_elem_value *ucontrol) \ |
|
{ \ |
|
struct gpio_runtime *gpio = snd_kcontrol_chip(kcontrol); \ |
|
if (gpio->methods && gpio->methods->set_##n) \ |
|
gpio->methods->set_##n(gpio, \ |
|
!!ucontrol->value.integer.value[0]); \ |
|
return 1; \ |
|
} \ |
|
static const struct snd_kcontrol_new n##_ctl = { \ |
|
.iface = SNDRV_CTL_ELEM_IFACE_MIXER, \ |
|
.name = description, \ |
|
.access = SNDRV_CTL_ELEM_ACCESS_READWRITE, \ |
|
.info = control_info, \ |
|
.get = n##_control_get, \ |
|
.put = n##_control_put, \ |
|
} |
|
|
|
AMP_CONTROL(headphone, "Headphone Switch"); |
|
AMP_CONTROL(speakers, "Speakers Switch"); |
|
AMP_CONTROL(lineout, "Line-Out Switch"); |
|
AMP_CONTROL(master, "Master Switch"); |
|
|
|
static int detect_choice_get(struct snd_kcontrol *kcontrol, |
|
struct snd_ctl_elem_value *ucontrol) |
|
{ |
|
struct layout_dev *ldev = snd_kcontrol_chip(kcontrol); |
|
|
|
switch (kcontrol->private_value) { |
|
case 0: |
|
ucontrol->value.integer.value[0] = ldev->switch_on_headphone; |
|
break; |
|
case 1: |
|
ucontrol->value.integer.value[0] = ldev->switch_on_lineout; |
|
break; |
|
default: |
|
return -ENODEV; |
|
} |
|
return 0; |
|
} |
|
|
|
static int detect_choice_put(struct snd_kcontrol *kcontrol, |
|
struct snd_ctl_elem_value *ucontrol) |
|
{ |
|
struct layout_dev *ldev = snd_kcontrol_chip(kcontrol); |
|
|
|
switch (kcontrol->private_value) { |
|
case 0: |
|
ldev->switch_on_headphone = !!ucontrol->value.integer.value[0]; |
|
break; |
|
case 1: |
|
ldev->switch_on_lineout = !!ucontrol->value.integer.value[0]; |
|
break; |
|
default: |
|
return -ENODEV; |
|
} |
|
return 1; |
|
} |
|
|
|
static const struct snd_kcontrol_new headphone_detect_choice = { |
|
.iface = SNDRV_CTL_ELEM_IFACE_MIXER, |
|
.name = "Headphone Detect Autoswitch", |
|
.info = control_info, |
|
.access = SNDRV_CTL_ELEM_ACCESS_READWRITE, |
|
.get = detect_choice_get, |
|
.put = detect_choice_put, |
|
.private_value = 0, |
|
}; |
|
|
|
static const struct snd_kcontrol_new lineout_detect_choice = { |
|
.iface = SNDRV_CTL_ELEM_IFACE_MIXER, |
|
.name = "Line-Out Detect Autoswitch", |
|
.info = control_info, |
|
.access = SNDRV_CTL_ELEM_ACCESS_READWRITE, |
|
.get = detect_choice_get, |
|
.put = detect_choice_put, |
|
.private_value = 1, |
|
}; |
|
|
|
static int detected_get(struct snd_kcontrol *kcontrol, |
|
struct snd_ctl_elem_value *ucontrol) |
|
{ |
|
struct layout_dev *ldev = snd_kcontrol_chip(kcontrol); |
|
int v; |
|
|
|
switch (kcontrol->private_value) { |
|
case 0: |
|
v = ldev->gpio.methods->get_detect(&ldev->gpio, |
|
AOA_NOTIFY_HEADPHONE); |
|
break; |
|
case 1: |
|
v = ldev->gpio.methods->get_detect(&ldev->gpio, |
|
AOA_NOTIFY_LINE_OUT); |
|
break; |
|
default: |
|
return -ENODEV; |
|
} |
|
ucontrol->value.integer.value[0] = v; |
|
return 0; |
|
} |
|
|
|
static const struct snd_kcontrol_new headphone_detected = { |
|
.iface = SNDRV_CTL_ELEM_IFACE_MIXER, |
|
.name = "Headphone Detected", |
|
.info = control_info, |
|
.access = SNDRV_CTL_ELEM_ACCESS_READ, |
|
.get = detected_get, |
|
.private_value = 0, |
|
}; |
|
|
|
static const struct snd_kcontrol_new lineout_detected = { |
|
.iface = SNDRV_CTL_ELEM_IFACE_MIXER, |
|
.name = "Line-Out Detected", |
|
.info = control_info, |
|
.access = SNDRV_CTL_ELEM_ACCESS_READ, |
|
.get = detected_get, |
|
.private_value = 1, |
|
}; |
|
|
|
static int check_codec(struct aoa_codec *codec, |
|
struct layout_dev *ldev, |
|
struct codec_connect_info *cci) |
|
{ |
|
const u32 *ref; |
|
char propname[32]; |
|
struct codec_connection *cc; |
|
|
|
/* if the codec has a 'codec' node, we require a reference */ |
|
if (of_node_name_eq(codec->node, "codec")) { |
|
snprintf(propname, sizeof(propname), |
|
"platform-%s-codec-ref", codec->name); |
|
ref = of_get_property(ldev->sound, propname, NULL); |
|
if (!ref) { |
|
printk(KERN_INFO "snd-aoa-fabric-layout: " |
|
"required property %s not present\n", propname); |
|
return -ENODEV; |
|
} |
|
if (*ref != codec->node->phandle) { |
|
printk(KERN_INFO "snd-aoa-fabric-layout: " |
|
"%s doesn't match!\n", propname); |
|
return -ENODEV; |
|
} |
|
} else { |
|
if (layouts_list_items != 1) { |
|
printk(KERN_INFO "snd-aoa-fabric-layout: " |
|
"more than one soundbus, but no references.\n"); |
|
return -ENODEV; |
|
} |
|
} |
|
codec->soundbus_dev = ldev->sdev; |
|
codec->gpio = &ldev->gpio; |
|
|
|
cc = cci->connections; |
|
if (!cc) |
|
return -EINVAL; |
|
|
|
printk(KERN_INFO "snd-aoa-fabric-layout: can use this codec\n"); |
|
|
|
codec->connected = 0; |
|
codec->fabric_data = cc; |
|
|
|
while (cc->connected) { |
|
codec->connected |= 1<<cc->codec_bit; |
|
cc++; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
static int layout_found_codec(struct aoa_codec *codec) |
|
{ |
|
struct layout_dev *ldev; |
|
int i; |
|
|
|
list_for_each_entry(ldev, &layouts_list, list) { |
|
for (i=0; i<MAX_CODECS_PER_BUS; i++) { |
|
if (!ldev->layout->codecs[i].name) |
|
continue; |
|
if (strcmp(ldev->layout->codecs[i].name, codec->name) == 0) { |
|
if (check_codec(codec, |
|
ldev, |
|
&ldev->layout->codecs[i]) == 0) |
|
return 0; |
|
} |
|
} |
|
} |
|
return -ENODEV; |
|
} |
|
|
|
static void layout_remove_codec(struct aoa_codec *codec) |
|
{ |
|
int i; |
|
/* here remove the codec from the layout dev's |
|
* codec reference */ |
|
|
|
codec->soundbus_dev = NULL; |
|
codec->gpio = NULL; |
|
for (i=0; i<MAX_CODECS_PER_BUS; i++) { |
|
} |
|
} |
|
|
|
static void layout_notify(void *data) |
|
{ |
|
struct layout_dev_ptr *dptr = data; |
|
struct layout_dev *ldev; |
|
int v, update; |
|
struct snd_kcontrol *detected, *c; |
|
struct snd_card *card = aoa_get_card(); |
|
|
|
ldev = dptr->ptr; |
|
if (data == &ldev->selfptr_headphone) { |
|
v = ldev->gpio.methods->get_detect(&ldev->gpio, AOA_NOTIFY_HEADPHONE); |
|
detected = ldev->headphone_detected_ctrl; |
|
update = ldev->switch_on_headphone; |
|
if (update) { |
|
ldev->gpio.methods->set_speakers(&ldev->gpio, !v); |
|
ldev->gpio.methods->set_headphone(&ldev->gpio, v); |
|
ldev->gpio.methods->set_lineout(&ldev->gpio, 0); |
|
} |
|
} else if (data == &ldev->selfptr_lineout) { |
|
v = ldev->gpio.methods->get_detect(&ldev->gpio, AOA_NOTIFY_LINE_OUT); |
|
detected = ldev->lineout_detected_ctrl; |
|
update = ldev->switch_on_lineout; |
|
if (update) { |
|
ldev->gpio.methods->set_speakers(&ldev->gpio, !v); |
|
ldev->gpio.methods->set_headphone(&ldev->gpio, 0); |
|
ldev->gpio.methods->set_lineout(&ldev->gpio, v); |
|
} |
|
} else |
|
return; |
|
|
|
if (detected) |
|
snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, &detected->id); |
|
if (update) { |
|
c = ldev->headphone_ctrl; |
|
if (c) |
|
snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, &c->id); |
|
c = ldev->speaker_ctrl; |
|
if (c) |
|
snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, &c->id); |
|
c = ldev->lineout_ctrl; |
|
if (c) |
|
snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, &c->id); |
|
} |
|
} |
|
|
|
static void layout_attached_codec(struct aoa_codec *codec) |
|
{ |
|
struct codec_connection *cc; |
|
struct snd_kcontrol *ctl; |
|
int headphones, lineout; |
|
struct layout_dev *ldev = layout_device; |
|
|
|
/* need to add this codec to our codec array! */ |
|
|
|
cc = codec->fabric_data; |
|
|
|
headphones = codec->gpio->methods->get_detect(codec->gpio, |
|
AOA_NOTIFY_HEADPHONE); |
|
lineout = codec->gpio->methods->get_detect(codec->gpio, |
|
AOA_NOTIFY_LINE_OUT); |
|
|
|
if (codec->gpio->methods->set_master) { |
|
ctl = snd_ctl_new1(&master_ctl, codec->gpio); |
|
ldev->master_ctrl = ctl; |
|
aoa_snd_ctl_add(ctl); |
|
} |
|
while (cc->connected) { |
|
if (cc->connected & CC_SPEAKERS) { |
|
if (headphones <= 0 && lineout <= 0) |
|
ldev->gpio.methods->set_speakers(codec->gpio, 1); |
|
ctl = snd_ctl_new1(&speakers_ctl, codec->gpio); |
|
ldev->speaker_ctrl = ctl; |
|
aoa_snd_ctl_add(ctl); |
|
} |
|
if (cc->connected & CC_HEADPHONE) { |
|
if (headphones == 1) |
|
ldev->gpio.methods->set_headphone(codec->gpio, 1); |
|
ctl = snd_ctl_new1(&headphone_ctl, codec->gpio); |
|
ldev->headphone_ctrl = ctl; |
|
aoa_snd_ctl_add(ctl); |
|
ldev->have_headphone_detect = |
|
!ldev->gpio.methods |
|
->set_notify(&ldev->gpio, |
|
AOA_NOTIFY_HEADPHONE, |
|
layout_notify, |
|
&ldev->selfptr_headphone); |
|
if (ldev->have_headphone_detect) { |
|
ctl = snd_ctl_new1(&headphone_detect_choice, |
|
ldev); |
|
aoa_snd_ctl_add(ctl); |
|
ctl = snd_ctl_new1(&headphone_detected, |
|
ldev); |
|
ldev->headphone_detected_ctrl = ctl; |
|
aoa_snd_ctl_add(ctl); |
|
} |
|
} |
|
if (cc->connected & CC_LINEOUT) { |
|
if (lineout == 1) |
|
ldev->gpio.methods->set_lineout(codec->gpio, 1); |
|
ctl = snd_ctl_new1(&lineout_ctl, codec->gpio); |
|
if (cc->connected & CC_LINEOUT_LABELLED_HEADPHONE) |
|
strscpy(ctl->id.name, |
|
"Headphone Switch", sizeof(ctl->id.name)); |
|
ldev->lineout_ctrl = ctl; |
|
aoa_snd_ctl_add(ctl); |
|
ldev->have_lineout_detect = |
|
!ldev->gpio.methods |
|
->set_notify(&ldev->gpio, |
|
AOA_NOTIFY_LINE_OUT, |
|
layout_notify, |
|
&ldev->selfptr_lineout); |
|
if (ldev->have_lineout_detect) { |
|
ctl = snd_ctl_new1(&lineout_detect_choice, |
|
ldev); |
|
if (cc->connected & CC_LINEOUT_LABELLED_HEADPHONE) |
|
strscpy(ctl->id.name, |
|
"Headphone Detect Autoswitch", |
|
sizeof(ctl->id.name)); |
|
aoa_snd_ctl_add(ctl); |
|
ctl = snd_ctl_new1(&lineout_detected, |
|
ldev); |
|
if (cc->connected & CC_LINEOUT_LABELLED_HEADPHONE) |
|
strscpy(ctl->id.name, |
|
"Headphone Detected", |
|
sizeof(ctl->id.name)); |
|
ldev->lineout_detected_ctrl = ctl; |
|
aoa_snd_ctl_add(ctl); |
|
} |
|
} |
|
cc++; |
|
} |
|
/* now update initial state */ |
|
if (ldev->have_headphone_detect) |
|
layout_notify(&ldev->selfptr_headphone); |
|
if (ldev->have_lineout_detect) |
|
layout_notify(&ldev->selfptr_lineout); |
|
} |
|
|
|
static struct aoa_fabric layout_fabric = { |
|
.name = "SoundByLayout", |
|
.owner = THIS_MODULE, |
|
.found_codec = layout_found_codec, |
|
.remove_codec = layout_remove_codec, |
|
.attached_codec = layout_attached_codec, |
|
}; |
|
|
|
static int aoa_fabric_layout_probe(struct soundbus_dev *sdev) |
|
{ |
|
struct device_node *sound = NULL; |
|
const unsigned int *id; |
|
struct layout *layout = NULL; |
|
struct layout_dev *ldev = NULL; |
|
int err; |
|
|
|
/* hm, currently we can only have one ... */ |
|
if (layout_device) |
|
return -ENODEV; |
|
|
|
/* by breaking out we keep a reference */ |
|
for_each_child_of_node(sdev->ofdev.dev.of_node, sound) { |
|
if (of_node_is_type(sound, "soundchip")) |
|
break; |
|
} |
|
if (!sound) |
|
return -ENODEV; |
|
|
|
id = of_get_property(sound, "layout-id", NULL); |
|
if (id) { |
|
layout = find_layout_by_id(*id); |
|
} else { |
|
id = of_get_property(sound, "device-id", NULL); |
|
if (id) |
|
layout = find_layout_by_device(*id); |
|
} |
|
|
|
if (!layout) { |
|
printk(KERN_ERR "snd-aoa-fabric-layout: unknown layout\n"); |
|
goto outnodev; |
|
} |
|
|
|
ldev = kzalloc(sizeof(struct layout_dev), GFP_KERNEL); |
|
if (!ldev) |
|
goto outnodev; |
|
|
|
layout_device = ldev; |
|
ldev->sdev = sdev; |
|
ldev->sound = sound; |
|
ldev->layout = layout; |
|
ldev->gpio.node = sound->parent; |
|
switch (layout->layout_id) { |
|
case 0: /* anything with device_id, not layout_id */ |
|
case 41: /* that unknown machine no one seems to have */ |
|
case 51: /* PowerBook5,4 */ |
|
case 58: /* Mac Mini */ |
|
ldev->gpio.methods = ftr_gpio_methods; |
|
printk(KERN_DEBUG |
|
"snd-aoa-fabric-layout: Using direct GPIOs\n"); |
|
break; |
|
default: |
|
ldev->gpio.methods = pmf_gpio_methods; |
|
printk(KERN_DEBUG |
|
"snd-aoa-fabric-layout: Using PMF GPIOs\n"); |
|
} |
|
ldev->selfptr_headphone.ptr = ldev; |
|
ldev->selfptr_lineout.ptr = ldev; |
|
dev_set_drvdata(&sdev->ofdev.dev, ldev); |
|
list_add(&ldev->list, &layouts_list); |
|
layouts_list_items++; |
|
|
|
/* assign these before registering ourselves, so |
|
* callbacks that are done during registration |
|
* already have the values */ |
|
sdev->pcmid = ldev->layout->pcmid; |
|
if (ldev->layout->busname) { |
|
sdev->pcmname = ldev->layout->busname; |
|
} else { |
|
sdev->pcmname = "Master"; |
|
} |
|
|
|
ldev->gpio.methods->init(&ldev->gpio); |
|
|
|
err = aoa_fabric_register(&layout_fabric, &sdev->ofdev.dev); |
|
if (err && err != -EALREADY) { |
|
printk(KERN_INFO "snd-aoa-fabric-layout: can't use," |
|
" another fabric is active!\n"); |
|
goto outlistdel; |
|
} |
|
|
|
use_layout(layout); |
|
ldev->switch_on_headphone = 1; |
|
ldev->switch_on_lineout = 1; |
|
return 0; |
|
outlistdel: |
|
/* we won't be using these then... */ |
|
ldev->gpio.methods->exit(&ldev->gpio); |
|
/* reset if we didn't use it */ |
|
sdev->pcmname = NULL; |
|
sdev->pcmid = -1; |
|
list_del(&ldev->list); |
|
layouts_list_items--; |
|
kfree(ldev); |
|
outnodev: |
|
of_node_put(sound); |
|
layout_device = NULL; |
|
return -ENODEV; |
|
} |
|
|
|
static int aoa_fabric_layout_remove(struct soundbus_dev *sdev) |
|
{ |
|
struct layout_dev *ldev = dev_get_drvdata(&sdev->ofdev.dev); |
|
int i; |
|
|
|
for (i=0; i<MAX_CODECS_PER_BUS; i++) { |
|
if (ldev->codecs[i]) { |
|
aoa_fabric_unlink_codec(ldev->codecs[i]); |
|
} |
|
ldev->codecs[i] = NULL; |
|
} |
|
list_del(&ldev->list); |
|
layouts_list_items--; |
|
of_node_put(ldev->sound); |
|
|
|
ldev->gpio.methods->set_notify(&ldev->gpio, |
|
AOA_NOTIFY_HEADPHONE, |
|
NULL, |
|
NULL); |
|
ldev->gpio.methods->set_notify(&ldev->gpio, |
|
AOA_NOTIFY_LINE_OUT, |
|
NULL, |
|
NULL); |
|
|
|
ldev->gpio.methods->exit(&ldev->gpio); |
|
layout_device = NULL; |
|
kfree(ldev); |
|
sdev->pcmid = -1; |
|
sdev->pcmname = NULL; |
|
return 0; |
|
} |
|
|
|
#ifdef CONFIG_PM_SLEEP |
|
static int aoa_fabric_layout_suspend(struct device *dev) |
|
{ |
|
struct layout_dev *ldev = dev_get_drvdata(dev); |
|
|
|
if (ldev->gpio.methods && ldev->gpio.methods->all_amps_off) |
|
ldev->gpio.methods->all_amps_off(&ldev->gpio); |
|
|
|
return 0; |
|
} |
|
|
|
static int aoa_fabric_layout_resume(struct device *dev) |
|
{ |
|
struct layout_dev *ldev = dev_get_drvdata(dev); |
|
|
|
if (ldev->gpio.methods && ldev->gpio.methods->all_amps_restore) |
|
ldev->gpio.methods->all_amps_restore(&ldev->gpio); |
|
|
|
return 0; |
|
} |
|
|
|
static SIMPLE_DEV_PM_OPS(aoa_fabric_layout_pm_ops, |
|
aoa_fabric_layout_suspend, aoa_fabric_layout_resume); |
|
|
|
#endif |
|
|
|
static struct soundbus_driver aoa_soundbus_driver = { |
|
.name = "snd_aoa_soundbus_drv", |
|
.owner = THIS_MODULE, |
|
.probe = aoa_fabric_layout_probe, |
|
.remove = aoa_fabric_layout_remove, |
|
.driver = { |
|
.owner = THIS_MODULE, |
|
#ifdef CONFIG_PM_SLEEP |
|
.pm = &aoa_fabric_layout_pm_ops, |
|
#endif |
|
} |
|
}; |
|
|
|
static int __init aoa_fabric_layout_init(void) |
|
{ |
|
return soundbus_register_driver(&aoa_soundbus_driver); |
|
} |
|
|
|
static void __exit aoa_fabric_layout_exit(void) |
|
{ |
|
soundbus_unregister_driver(&aoa_soundbus_driver); |
|
aoa_fabric_unregister(&layout_fabric); |
|
} |
|
|
|
module_init(aoa_fabric_layout_init); |
|
module_exit(aoa_fabric_layout_exit);
|
|
|