ALSA: hda - Provide the proper channel mapping for generic HDMI driver

... instead of the standard fixed channel maps.
The generic HDMI is based on the audio infoframe, and its configuration
can be selected via CA bits.  Thus we need a translation between the
CA index and the verbose channel map list.

Signed-off-by: Takashi Iwai <tiwai@suse.de>
This commit is contained in:
Takashi Iwai 2012-07-31 11:36:00 +02:00
Родитель 9c9a5175e6
Коммит d45e6889ee
1 изменённых файлов: 303 добавлений и 5 удалений

Просмотреть файл

@ -35,6 +35,7 @@
#include <sound/core.h>
#include <sound/jack.h>
#include <sound/asoundef.h>
#include <sound/tlv.h>
#include "hda_codec.h"
#include "hda_local.h"
#include "hda_jack.h"
@ -73,6 +74,8 @@ struct hdmi_spec_per_pin {
struct delayed_work work;
int repoll_count;
bool non_pcm;
bool chmap_set; /* channel-map override by ALSA API? */
unsigned char chmap[8]; /* ALSA API channel-map */
};
struct hdmi_spec {
@ -82,6 +85,7 @@ struct hdmi_spec {
int num_pins;
struct hdmi_spec_per_pin pins[MAX_HDMI_PINS];
struct hda_pcm pcm_rec[MAX_HDMI_PINS];
unsigned int channels_max; /* max over all cvts */
/*
* Non-generic ATI/NVIDIA specific
@ -548,7 +552,7 @@ static void hdmi_debug_channel_mapping(struct hda_codec *codec,
}
static void hdmi_setup_channel_mapping(struct hda_codec *codec,
static void hdmi_std_setup_channel_mapping(struct hda_codec *codec,
hda_nid_t pin_nid,
bool non_pcm,
int ca)
@ -588,6 +592,136 @@ static void hdmi_setup_channel_mapping(struct hda_codec *codec,
hdmi_debug_channel_mapping(codec, pin_nid);
}
struct channel_map_table {
unsigned char map; /* ALSA API channel map position */
unsigned char cea_slot; /* CEA slot value */
int spk_mask; /* speaker position bit mask */
};
static struct channel_map_table map_tables[] = {
{ SNDRV_CHMAP_FL, 0x00, FL },
{ SNDRV_CHMAP_FR, 0x01, FR },
{ SNDRV_CHMAP_RL, 0x04, RL },
{ SNDRV_CHMAP_RR, 0x05, RR },
{ SNDRV_CHMAP_LFE, 0x02, LFE },
{ SNDRV_CHMAP_FC, 0x03, FC },
{ SNDRV_CHMAP_RLC, 0x06, RLC },
{ SNDRV_CHMAP_RRC, 0x07, RRC },
{} /* terminator */
};
/* from ALSA API channel position to speaker bit mask */
static int to_spk_mask(unsigned char c)
{
struct channel_map_table *t = map_tables;
for (; t->map; t++) {
if (t->map == c)
return t->spk_mask;
}
return 0;
}
/* from ALSA API channel position to CEA slot */
static int to_cea_slot(unsigned char c)
{
struct channel_map_table *t = map_tables;
for (; t->map; t++) {
if (t->map == c)
return t->cea_slot;
}
return 0x0f;
}
/* from CEA slot to ALSA API channel position */
static int from_cea_slot(unsigned char c)
{
struct channel_map_table *t = map_tables;
for (; t->map; t++) {
if (t->cea_slot == c)
return t->map;
}
return 0;
}
/* from speaker bit mask to ALSA API channel position */
static int spk_to_chmap(int spk)
{
struct channel_map_table *t = map_tables;
for (; t->map; t++) {
if (t->spk_mask == spk)
return t->map;
}
return 0;
}
/* get the CA index corresponding to the given ALSA API channel map */
static int hdmi_manual_channel_allocation(int chs, unsigned char *map)
{
int i, spks = 0, spk_mask = 0;
for (i = 0; i < chs; i++) {
int mask = to_spk_mask(map[i]);
if (mask) {
spk_mask |= mask;
spks++;
}
}
for (i = 0; i < ARRAY_SIZE(channel_allocations); i++) {
if ((chs == channel_allocations[i].channels ||
spks == channel_allocations[i].channels) &&
(spk_mask & channel_allocations[i].spk_mask) ==
channel_allocations[i].spk_mask)
return channel_allocations[i].ca_index;
}
return -1;
}
/* set up the channel slots for the given ALSA API channel map */
static int hdmi_manual_setup_channel_mapping(struct hda_codec *codec,
hda_nid_t pin_nid,
int chs, unsigned char *map)
{
int i;
for (i = 0; i < 8; i++) {
int val, err;
if (i < chs)
val = to_cea_slot(map[i]);
else
val = 0xf;
val |= (i << 4);
err = snd_hda_codec_write(codec, pin_nid, 0,
AC_VERB_SET_HDMI_CHAN_SLOT, val);
if (err)
return -EINVAL;
}
return 0;
}
/* store ALSA API channel map from the current default map */
static void hdmi_setup_fake_chmap(unsigned char *map, int ca)
{
int i;
for (i = 0; i < 8; i++) {
if (i < channel_allocations[ca].channels)
map[i] = from_cea_slot((hdmi_channel_mapping[ca][i] >> 4) & 0x0f);
else
map[i] = 0;
}
}
static void hdmi_setup_channel_mapping(struct hda_codec *codec,
hda_nid_t pin_nid, bool non_pcm, int ca,
int channels, unsigned char *map)
{
if (!non_pcm && map) {
hdmi_manual_setup_channel_mapping(codec, pin_nid,
channels, map);
} else {
hdmi_std_setup_channel_mapping(codec, pin_nid, non_pcm, ca);
hdmi_setup_fake_chmap(map, ca);
}
}
/*
* Audio InfoFrame routines
@ -726,7 +860,12 @@ static void hdmi_setup_audio_infoframe(struct hda_codec *codec, int pin_idx,
if (!eld->monitor_present)
return;
ca = hdmi_channel_allocation(eld, channels);
if (!non_pcm && per_pin->chmap_set)
ca = hdmi_manual_channel_allocation(channels, per_pin->chmap);
else
ca = hdmi_channel_allocation(eld, channels);
if (ca < 0)
ca = 0;
memset(&ai, 0, sizeof(ai));
if (eld->conn_type == 0) { /* HDMI */
@ -763,7 +902,8 @@ static void hdmi_setup_audio_infoframe(struct hda_codec *codec, int pin_idx,
"pin=%d channels=%d\n",
pin_nid,
channels);
hdmi_setup_channel_mapping(codec, pin_nid, non_pcm, ca);
hdmi_setup_channel_mapping(codec, pin_nid, non_pcm, ca,
channels, per_pin->chmap);
hdmi_stop_infoframe_trans(codec, pin_nid);
hdmi_fill_audio_infoframe(codec, pin_nid,
ai.bytes, sizeof(ai));
@ -772,7 +912,8 @@ static void hdmi_setup_audio_infoframe(struct hda_codec *codec, int pin_idx,
/* For non-pcm audio switch, setup new channel mapping
* accordingly */
if (per_pin->non_pcm != non_pcm)
hdmi_setup_channel_mapping(codec, pin_nid, non_pcm, ca);
hdmi_setup_channel_mapping(codec, pin_nid, non_pcm, ca,
channels, per_pin->chmap);
}
per_pin->non_pcm = non_pcm;
@ -1098,8 +1239,11 @@ static int hdmi_add_cvt(struct hda_codec *codec, hda_nid_t cvt_nid)
per_cvt->cvt_nid = cvt_nid;
per_cvt->channels_min = 2;
if (chans <= 16)
if (chans <= 16) {
per_cvt->channels_max = chans;
if (chans > spec->channels_max)
spec->channels_max = chans;
}
err = snd_hda_query_supported_pcm(codec, cvt_nid,
&per_cvt->rates,
@ -1250,7 +1394,10 @@ static int hdmi_pcm_close(struct hda_pcm_stream *hinfo,
AC_VERB_SET_PIN_WIDGET_CONTROL,
pinctl & ~PIN_OUT);
snd_hda_spdif_ctls_unassign(codec, pin_idx);
per_pin->chmap_set = false;
memset(per_pin->chmap, 0, sizeof(per_pin->chmap));
}
return 0;
}
@ -1261,6 +1408,135 @@ static const struct hda_pcm_ops generic_ops = {
.cleanup = generic_hdmi_playback_pcm_cleanup,
};
/*
* ALSA API channel-map control callbacks
*/
static int hdmi_chmap_ctl_info(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_info *uinfo)
{
struct snd_pcm_chmap *info = snd_kcontrol_chip(kcontrol);
struct hda_codec *codec = info->private_data;
struct hdmi_spec *spec = codec->spec;
uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
uinfo->count = spec->channels_max;
uinfo->value.integer.min = 0;
uinfo->value.integer.max = SNDRV_CHMAP_LAST;
return 0;
}
static int hdmi_chmap_ctl_tlv(struct snd_kcontrol *kcontrol, int op_flag,
unsigned int size, unsigned int __user *tlv)
{
struct snd_pcm_chmap *info = snd_kcontrol_chip(kcontrol);
struct hda_codec *codec = info->private_data;
struct hdmi_spec *spec = codec->spec;
const unsigned int valid_mask =
FL | FR | RL | RR | LFE | FC | RLC | RRC;
unsigned int __user *dst;
int chs, count = 0;
if (size < 8)
return -ENOMEM;
if (put_user(SNDRV_CTL_TLVT_CONTAINER, tlv))
return -EFAULT;
size -= 8;
dst = tlv + 2;
for (chs = 2; chs <= spec->channels_max; chs += 2) {
int i, c;
struct cea_channel_speaker_allocation *cap;
cap = channel_allocations;
for (i = 0; i < ARRAY_SIZE(channel_allocations); i++, cap++) {
int chs_bytes = chs * 4;
if (cap->channels != chs)
continue;
if (cap->spk_mask & ~valid_mask)
continue;
if (size < 8)
return -ENOMEM;
if (put_user(SNDRV_CTL_TLVT_CHMAP_VAR, dst) ||
put_user(chs_bytes, dst + 1))
return -EFAULT;
dst += 2;
size -= 8;
count += 8;
if (size < chs_bytes)
return -ENOMEM;
size -= chs_bytes;
count += chs_bytes;
for (c = 7; c >= 0; c--) {
int spk = cap->speakers[c];
if (!spk)
continue;
if (put_user(spk_to_chmap(spk), dst))
return -EFAULT;
dst++;
}
}
}
if (put_user(count, tlv + 1))
return -EFAULT;
return 0;
}
static int hdmi_chmap_ctl_get(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
struct snd_pcm_chmap *info = snd_kcontrol_chip(kcontrol);
struct hda_codec *codec = info->private_data;
struct hdmi_spec *spec = codec->spec;
int pin_idx = kcontrol->private_value;
struct hdmi_spec_per_pin *per_pin = &spec->pins[pin_idx];
int i;
for (i = 0; i < ARRAY_SIZE(per_pin->chmap); i++)
ucontrol->value.integer.value[i] = per_pin->chmap[i];
return 0;
}
static int hdmi_chmap_ctl_put(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
struct snd_pcm_chmap *info = snd_kcontrol_chip(kcontrol);
struct hda_codec *codec = info->private_data;
struct hdmi_spec *spec = codec->spec;
int pin_idx = kcontrol->private_value;
struct hdmi_spec_per_pin *per_pin = &spec->pins[pin_idx];
unsigned int ctl_idx;
struct snd_pcm_substream *substream;
unsigned char chmap[8];
int i, ca, prepared = 0;
ctl_idx = snd_ctl_get_ioffidx(kcontrol, &ucontrol->id);
substream = snd_pcm_chmap_substream(info, ctl_idx);
if (!substream || !substream->runtime)
return -EBADFD;
switch (substream->runtime->status->state) {
case SNDRV_PCM_STATE_OPEN:
case SNDRV_PCM_STATE_SETUP:
break;
case SNDRV_PCM_STATE_PREPARED:
prepared = 1;
break;
default:
return -EBUSY;
}
memset(chmap, 0, sizeof(chmap));
for (i = 0; i < ARRAY_SIZE(chmap); i++)
chmap[i] = ucontrol->value.integer.value[i];
if (!memcmp(chmap, per_pin->chmap, sizeof(chmap)))
return 0;
ca = hdmi_manual_channel_allocation(ARRAY_SIZE(chmap), chmap);
if (ca < 0)
return -EINVAL;
per_pin->chmap_set = true;
memcpy(per_pin->chmap, chmap, sizeof(chmap));
if (prepared)
hdmi_setup_audio_infoframe(codec, pin_idx, per_pin->non_pcm,
substream);
return 0;
}
static int generic_hdmi_build_pcms(struct hda_codec *codec)
{
struct hdmi_spec *spec = codec->spec;
@ -1273,6 +1549,7 @@ static int generic_hdmi_build_pcms(struct hda_codec *codec)
info = &spec->pcm_rec[pin_idx];
info->name = get_hdmi_pcm_name(pin_idx);
info->pcm_type = HDA_PCM_TYPE_HDMI;
info->own_chmap = true;
pstr = &info->stream[SNDRV_PCM_STREAM_PLAYBACK];
pstr->substreams = 1;
@ -1330,6 +1607,27 @@ static int generic_hdmi_build_controls(struct hda_codec *codec)
hdmi_present_sense(per_pin, 0);
}
/* add channel maps */
for (pin_idx = 0; pin_idx < spec->num_pins; pin_idx++) {
struct snd_pcm_chmap *chmap;
struct snd_kcontrol *kctl;
int i;
err = snd_pcm_add_chmap_ctls(codec->pcm_info[pin_idx].pcm,
SNDRV_PCM_STREAM_PLAYBACK,
NULL, 0, pin_idx, &chmap);
if (err < 0)
return err;
/* override handlers */
chmap->private_data = codec;
kctl = chmap->kctl;
for (i = 0; i < kctl->count; i++)
kctl->vd[i].access |= SNDRV_CTL_ELEM_ACCESS_WRITE;
kctl->info = hdmi_chmap_ctl_info;
kctl->get = hdmi_chmap_ctl_get;
kctl->put = hdmi_chmap_ctl_put;
kctl->tlv.c = hdmi_chmap_ctl_tlv;
}
return 0;
}