staging: bcm2835-audio: Add support for simultanous HDMI and Headphone audio

The firmware for the Raspberry PI already supports simultanous output
of audio through both the HDMI and the Headphone jack.  The current
implementation of ALSA doesn't expose this well to user mode since
the firmware audio is represented as a single card.

A newer approach is taken here and a virtual card is created for each
output(HDMI, Headphones, and Traditional ALSA).  The firmware has
the concept of channels or streams for which the number to use is
passed in the device tree. These streams are allocated to each of the
virtual cards.

As a side effect of this change, since each output is represented
independenly it's now very easy to use PulseAudio to control the
priorities of the outputs.

Testing:

Audacity and VLC were both loaded at the same time.  Each application
was assigned to a different card. With this change I was able to play
different music files at the same time through the HDMI and Headphones
jacks and control the audio independently.

Signed-off-by: Michael Zoran <mzoran@crowfest.net>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
This commit is contained in:
Michael Zoran 2017-03-14 17:01:25 -07:00 committed by Greg Kroah-Hartman
parent f7d51372d7
commit 325b5b6c96
4 changed files with 461 additions and 110 deletions

View file

@ -340,3 +340,89 @@ int snd_bcm2835_new_ctl(struct bcm2835_chip *chip)
}
return 0;
}
static struct snd_kcontrol_new snd_bcm2835_headphones_ctl[] = {
{
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.name = "Headphone Playback Volume",
.index = 0,
.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
SNDRV_CTL_ELEM_ACCESS_TLV_READ,
.private_value = PCM_PLAYBACK_VOLUME,
.info = snd_bcm2835_ctl_info,
.get = snd_bcm2835_ctl_get,
.put = snd_bcm2835_ctl_put,
.count = 1,
.tlv = {.p = snd_bcm2835_db_scale}
},
{
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.name = "Headphone Playback Switch",
.index = 0,
.access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
.private_value = PCM_PLAYBACK_MUTE,
.info = snd_bcm2835_ctl_info,
.get = snd_bcm2835_ctl_get,
.put = snd_bcm2835_ctl_put,
.count = 1,
}
};
int snd_bcm2835_new_headphones_ctl(struct bcm2835_chip *chip)
{
int err;
unsigned int idx;
strcpy(chip->card->mixername, "Broadcom Mixer");
for (idx = 0; idx < ARRAY_SIZE(snd_bcm2835_headphones_ctl); idx++) {
err = snd_ctl_add(chip->card,
snd_ctl_new1(&snd_bcm2835_headphones_ctl[idx],
chip));
if (err)
return err;
}
return 0;
}
static struct snd_kcontrol_new snd_bcm2835_hdmi[] = {
{
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.name = "HDMI Playback Volume",
.index = 0,
.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
SNDRV_CTL_ELEM_ACCESS_TLV_READ,
.private_value = PCM_PLAYBACK_VOLUME,
.info = snd_bcm2835_ctl_info,
.get = snd_bcm2835_ctl_get,
.put = snd_bcm2835_ctl_put,
.count = 1,
.tlv = {.p = snd_bcm2835_db_scale}
},
{
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.name = "HDMI Playback Switch",
.index = 0,
.access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
.private_value = PCM_PLAYBACK_MUTE,
.info = snd_bcm2835_ctl_info,
.get = snd_bcm2835_ctl_get,
.put = snd_bcm2835_ctl_put,
.count = 1,
}
};
int snd_bcm2835_new_hdmi_ctl(struct bcm2835_chip *chip)
{
int err;
unsigned int idx;
strcpy(chip->card->mixername, "Broadcom Mixer");
for (idx = 0; idx < ARRAY_SIZE(snd_bcm2835_hdmi); idx++) {
err = snd_ctl_add(chip->card,
snd_ctl_new1(&snd_bcm2835_hdmi[idx], chip));
if (err)
return err;
}
return 0;
}

View file

@ -479,7 +479,7 @@ static struct snd_pcm_ops snd_bcm2835_playback_spdif_ops = {
};
/* create a pcm device */
int snd_bcm2835_new_pcm(struct bcm2835_chip *chip)
int snd_bcm2835_new_pcm(struct bcm2835_chip *chip, u32 numchannels)
{
struct snd_pcm *pcm;
int err;
@ -490,7 +490,7 @@ int snd_bcm2835_new_pcm(struct bcm2835_chip *chip)
audio_error("Interrupted whilst waiting for lock\n");
return -EINTR;
}
err = snd_pcm_new(chip->card, "bcm2835 ALSA", 0, MAX_SUBSTREAMS, 0, &pcm);
err = snd_pcm_new(chip->card, "bcm2835 ALSA", 0, numchannels, 0, &pcm);
if (err < 0)
goto out;
pcm->private_data = chip;
@ -549,3 +549,39 @@ int snd_bcm2835_new_spdif_pcm(struct bcm2835_chip *chip)
return 0;
}
int snd_bcm2835_new_simple_pcm(struct bcm2835_chip *chip,
const char *name,
enum snd_bcm2835_route route,
u32 numchannels)
{
struct snd_pcm *pcm;
int err;
mutex_init(&chip->audio_mutex);
err = snd_pcm_new(chip->card, name, 0, numchannels,
0, &pcm);
if (err)
return err;
pcm->private_data = chip;
strcpy(pcm->name, name);
chip->pcm = pcm;
chip->dest = route;
chip->volume = alsa2chip(0);
chip->mute = CTRL_VOL_UNMUTE;
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK,
&snd_bcm2835_playback_ops);
snd_pcm_lib_preallocate_pages_for_all(
pcm,
SNDRV_DMA_TYPE_CONTINUOUS,
snd_dma_continuous_data(GFP_KERNEL),
snd_bcm2835_playback_hw.buffer_bytes_max,
snd_bcm2835_playback_hw.buffer_bytes_max);
return 0;
}

View file

@ -21,15 +21,70 @@
#include "bcm2835.h"
/* HACKY global pointers needed for successive probes to work : ssp
* But compared against the changes we will have to do in VC audio_ipc code
* to export 8 audio_ipc devices as a single IPC device and then monitor all
* four devices in a thread, this gets things done quickly and should be easier
* to debug if we run into issues
*/
static bool enable_hdmi;
static bool enable_headphones;
static bool enable_compat_alsa = true;
static struct snd_card *g_card;
static struct bcm2835_chip *g_chip;
module_param(enable_hdmi, bool, 0444);
MODULE_PARM_DESC(enable_hdmi, "Enables HDMI virtual audio device");
module_param(enable_headphones, bool, 0444);
MODULE_PARM_DESC(enable_headphones, "Enables Headphones virtual audio device");
module_param(enable_compat_alsa, bool, 0444);
MODULE_PARM_DESC(enable_compat_alsa,
"Enables ALSA compatibility virtual audio device");
static void snd_devm_unregister_child(struct device *dev, void *res)
{
struct device *childdev = *(struct device **)res;
device_unregister(childdev);
}
static int snd_devm_add_child(struct device *dev, struct device *child)
{
struct device **dr;
int ret;
dr = devres_alloc(snd_devm_unregister_child, sizeof(*dr), GFP_KERNEL);
if (!dr)
return -ENOMEM;
ret = device_add(child);
if (ret) {
devres_free(dr);
return ret;
}
*dr = child;
devres_add(dev, dr);
return 0;
}
static struct device *
snd_create_device(struct device *parent,
struct device_driver *driver,
const char *name)
{
struct device *device;
int ret;
device = devm_kzalloc(parent, sizeof(*device), GFP_KERNEL);
if (!device)
return ERR_PTR(-ENOMEM);
device_initialize(device);
device->parent = parent;
device->driver = driver;
dev_set_name(device, "%s", name);
ret = snd_devm_add_child(parent, device);
if (ret)
return ERR_PTR(ret);
return device;
}
static int snd_bcm2835_free(struct bcm2835_chip *chip)
{
@ -49,7 +104,6 @@ static int snd_bcm2835_dev_free(struct snd_device *device)
* (see "Management of Cards and Components")
*/
static int snd_bcm2835_create(struct snd_card *card,
struct platform_device *pdev,
struct bcm2835_chip **rchip)
{
struct bcm2835_chip *chip;
@ -67,7 +121,7 @@ static int snd_bcm2835_create(struct snd_card *card,
chip->card = card;
err = snd_device_new(card, SNDRV_DEV_LOWLEVEL, chip, &ops);
if (err < 0) {
if (err) {
snd_bcm2835_free(chip);
return err;
}
@ -76,13 +130,268 @@ static int snd_bcm2835_create(struct snd_card *card,
return 0;
}
static void snd_devm_card_free(struct device *dev, void *res)
{
struct snd_card *snd_card = *(struct snd_card **)res;
snd_card_free(snd_card);
}
static struct snd_card *snd_devm_card_new(struct device *dev)
{
struct snd_card **dr;
struct snd_card *card;
int ret;
dr = devres_alloc(snd_devm_card_free, sizeof(*dr), GFP_KERNEL);
if (!dr)
return ERR_PTR(-ENOMEM);
ret = snd_card_new(dev, -1, NULL, THIS_MODULE, 0, &card);
if (ret) {
devres_free(dr);
return ERR_PTR(ret);
}
*dr = card;
devres_add(dev, dr);
return card;
}
typedef int (*bcm2835_audio_newpcm_func)(struct bcm2835_chip *chip,
const char *name,
enum snd_bcm2835_route route,
u32 numchannels);
typedef int (*bcm2835_audio_newctl_func)(struct bcm2835_chip *chip);
struct bcm2835_audio_driver {
struct device_driver driver;
const char *shortname;
const char *longname;
int minchannels;
bcm2835_audio_newpcm_func newpcm;
bcm2835_audio_newctl_func newctl;
enum snd_bcm2835_route route;
};
static int bcm2835_audio_alsa_newpcm(struct bcm2835_chip *chip,
const char *name,
enum snd_bcm2835_route route,
u32 numchannels)
{
int err;
err = snd_bcm2835_new_pcm(chip, numchannels - 1);
if (err)
return err;
err = snd_bcm2835_new_spdif_pcm(chip);
if (err)
return err;
return 0;
}
static struct bcm2835_audio_driver bcm2835_audio_alsa = {
.driver = {
.name = "bcm2835_alsa",
.owner = THIS_MODULE,
},
.shortname = "bcm2835 ALSA",
.longname = "bcm2835 ALSA",
.minchannels = 2,
.newpcm = bcm2835_audio_alsa_newpcm,
.newctl = snd_bcm2835_new_ctl,
};
static struct bcm2835_audio_driver bcm2835_audio_hdmi = {
.driver = {
.name = "bcm2835_hdmi",
.owner = THIS_MODULE,
},
.shortname = "bcm2835 HDMI",
.longname = "bcm2835 HDMI",
.minchannels = 1,
.newpcm = snd_bcm2835_new_simple_pcm,
.newctl = snd_bcm2835_new_hdmi_ctl,
.route = AUDIO_DEST_HDMI
};
static struct bcm2835_audio_driver bcm2835_audio_headphones = {
.driver = {
.name = "bcm2835_headphones",
.owner = THIS_MODULE,
},
.shortname = "bcm2835 Headphones",
.longname = "bcm2835 Headphones",
.minchannels = 1,
.newpcm = snd_bcm2835_new_simple_pcm,
.newctl = snd_bcm2835_new_headphones_ctl,
.route = AUDIO_DEST_HEADPHONES
};
struct bcm2835_audio_drivers {
struct bcm2835_audio_driver *audio_driver;
const bool *is_enabled;
};
static struct bcm2835_audio_drivers children_devices[] = {
{
.audio_driver = &bcm2835_audio_alsa,
.is_enabled = &enable_compat_alsa,
},
{
.audio_driver = &bcm2835_audio_hdmi,
.is_enabled = &enable_hdmi,
},
{
.audio_driver = &bcm2835_audio_headphones,
.is_enabled = &enable_headphones,
},
};
static int snd_add_child_device(struct device *device,
struct bcm2835_audio_driver *audio_driver,
u32 numchans)
{
struct snd_card *card;
struct device *child;
struct bcm2835_chip *chip;
int err, i;
child = snd_create_device(device, &audio_driver->driver,
audio_driver->driver.name);
if (IS_ERR(child)) {
dev_err(device,
"Unable to create child device %p, error %ld",
audio_driver->driver.name,
PTR_ERR(child));
return PTR_ERR(child);
}
card = snd_devm_card_new(child);
if (IS_ERR(card)) {
dev_err(child, "Failed to create card");
return PTR_ERR(card);
}
snd_card_set_dev(card, child);
strcpy(card->driver, audio_driver->driver.name);
strcpy(card->shortname, audio_driver->shortname);
strcpy(card->longname, audio_driver->longname);
err = snd_bcm2835_create(card, &chip);
if (err) {
dev_err(child, "Failed to create chip, error %d\n", err);
return err;
}
chip->dev = child;
err = audio_driver->newpcm(chip, audio_driver->shortname,
audio_driver->route,
numchans);
if (err) {
dev_err(child, "Failed to create pcm, error %d\n", err);
return err;
}
err = audio_driver->newctl(chip);
if (err) {
dev_err(child, "Failed to create controls, error %d\n", err);
return err;
}
for (i = 0; i < numchans; i++)
chip->avail_substreams |= (1 << i);
err = snd_card_register(card);
if (err) {
dev_err(child, "Failed to register card, error %d\n", err);
return err;
}
dev_set_drvdata(child, card);
dev_info(child, "card created with %d channels\n", numchans);
return 0;
}
static int snd_add_child_devices(struct device *device, u32 numchans)
{
int i;
int count_devices = 0;
int minchannels = 0;
int extrachannels = 0;
int extrachannels_per_driver = 0;
int extrachannels_remainder = 0;
for (i = 0; i < ARRAY_SIZE(children_devices); i++)
if (*children_devices[i].is_enabled)
count_devices++;
if (!count_devices)
return 0;
for (i = 0; i < ARRAY_SIZE(children_devices); i++)
if (*children_devices[i].is_enabled)
minchannels +=
children_devices[i].audio_driver->minchannels;
if (minchannels < numchans) {
extrachannels = numchans - minchannels;
extrachannels_per_driver = extrachannels / count_devices;
extrachannels_remainder = extrachannels % count_devices;
}
dev_dbg(device, "minchannels %d\n", minchannels);
dev_dbg(device, "extrachannels %d\n", extrachannels);
dev_dbg(device, "extrachannels_per_driver %d\n",
extrachannels_per_driver);
dev_dbg(device, "extrachannels_remainder %d\n",
extrachannels_remainder);
for (i = 0; i < ARRAY_SIZE(children_devices); i++) {
int err;
int numchannels_this_device;
struct bcm2835_audio_driver *audio_driver;
if (!*children_devices[i].is_enabled)
continue;
audio_driver = children_devices[i].audio_driver;
if (audio_driver->minchannels > numchans) {
dev_err(device,
"Out of channels, needed %d but only %d left\n",
audio_driver->minchannels,
numchans);
continue;
}
numchannels_this_device =
audio_driver->minchannels + extrachannels_per_driver +
extrachannels_remainder;
extrachannels_remainder = 0;
numchans -= numchannels_this_device;
err = snd_add_child_device(device, audio_driver,
numchannels_this_device);
if (err)
return err;
}
return 0;
}
static int snd_bcm2835_alsa_probe_dt(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct bcm2835_chip *chip;
struct snd_card *card;
u32 numchans;
int err, i;
int err;
err = of_property_read_u32(dev->of_node, "brcm,pwm-channels",
&numchans);
@ -93,100 +402,14 @@ static int snd_bcm2835_alsa_probe_dt(struct platform_device *pdev)
if (numchans == 0 || numchans > MAX_SUBSTREAMS) {
numchans = MAX_SUBSTREAMS;
dev_warn(dev, "Illegal 'brcm,pwm-channels' value, will use %u\n",
dev_warn(dev,
"Illegal 'brcm,pwm-channels' value, will use %u\n",
numchans);
}
err = snd_card_new(&pdev->dev, -1, NULL, THIS_MODULE, 0, &card);
if (err) {
dev_err(dev, "Failed to create soundcard structure\n");
err = snd_add_child_devices(dev, numchans);
if (err)
return err;
}
snd_card_set_dev(card, dev);
strcpy(card->driver, "bcm2835");
strcpy(card->shortname, "bcm2835 ALSA");
sprintf(card->longname, "%s", card->shortname);
err = snd_bcm2835_create(card, pdev, &chip);
if (err < 0) {
dev_err(dev, "Failed to create bcm2835 chip\n");
goto err_free;
}
err = snd_bcm2835_new_pcm(chip);
if (err < 0) {
dev_err(dev, "Failed to create new bcm2835 pcm device\n");
goto err_free;
}
err = snd_bcm2835_new_spdif_pcm(chip);
if (err < 0) {
dev_err(dev, "Failed to create new bcm2835 spdif pcm device\n");
goto err_free;
}
err = snd_bcm2835_new_ctl(chip);
if (err < 0) {
dev_err(dev, "Failed to create new bcm2835 ctl\n");
goto err_free;
}
for (i = 0; i < numchans; i++) {
chip->avail_substreams |= (1 << i);
chip->pdev[i] = pdev;
}
err = snd_card_register(card);
if (err) {
dev_err(dev, "Failed to register bcm2835 ALSA card\n");
goto err_free;
}
g_card = card;
g_chip = chip;
platform_set_drvdata(pdev, card);
audio_info("bcm2835 ALSA card created with %u channels\n", numchans);
return 0;
err_free:
snd_card_free(card);
return err;
}
static int snd_bcm2835_alsa_remove(struct platform_device *pdev)
{
int idx;
void *drv_data;
drv_data = platform_get_drvdata(pdev);
if (drv_data == (void *)g_card) {
/* This is the card device */
snd_card_free((struct snd_card *)drv_data);
g_card = NULL;
g_chip = NULL;
} else {
idx = (int)(long)drv_data;
if (g_card) {
BUG_ON(!g_chip);
/* We pass chip device numbers in audio ipc devices
* other than the one we registered our card with
*/
idx = (int)(long)drv_data;
BUG_ON(!idx || idx > MAX_SUBSTREAMS);
g_chip->avail_substreams &= ~(1 << idx);
/* There should be atleast one substream registered
* after we are done here, as it wil be removed when
* the *remove* is called for the card device
*/
BUG_ON(!g_chip->avail_substreams);
}
}
platform_set_drvdata(pdev, NULL);
return 0;
}
@ -214,13 +437,12 @@ MODULE_DEVICE_TABLE(of, snd_bcm2835_of_match_table);
static struct platform_driver bcm2835_alsa0_driver = {
.probe = snd_bcm2835_alsa_probe_dt,
.remove = snd_bcm2835_alsa_remove,
#ifdef CONFIG_PM
.suspend = snd_bcm2835_alsa_suspend,
.resume = snd_bcm2835_alsa_resume,
#endif
.driver = {
.name = "bcm2835_AUD0",
.name = "bcm2835_audio",
.owner = THIS_MODULE,
.of_match_table = snd_bcm2835_of_match_table,
},
@ -232,7 +454,7 @@ static int bcm2835_alsa_device_init(void)
retval = platform_driver_register(&bcm2835_alsa0_driver);
if (retval)
pr_err("Error registering bcm2835_alsa0_driver %d .\n", retval);
pr_err("Error registering bcm2835_audio driver %d .\n", retval);
return retval;
}

View file

@ -104,7 +104,7 @@ struct bcm2835_chip {
struct snd_pcm *pcm_spdif;
/* Bitmat for valid reg_base and irq numbers */
unsigned int avail_substreams;
struct platform_device *pdev[MAX_SUBSTREAMS];
struct device *dev;
struct bcm2835_alsa_stream *alsa_stream[MAX_SUBSTREAMS];
int volume;
@ -145,8 +145,15 @@ struct bcm2835_alsa_stream {
};
int snd_bcm2835_new_ctl(struct bcm2835_chip *chip);
int snd_bcm2835_new_pcm(struct bcm2835_chip *chip);
int snd_bcm2835_new_pcm(struct bcm2835_chip *chip, u32 numchannels);
int snd_bcm2835_new_spdif_pcm(struct bcm2835_chip *chip);
int snd_bcm2835_new_simple_pcm(struct bcm2835_chip *chip,
const char *name,
enum snd_bcm2835_route route,
u32 numchannels);
int snd_bcm2835_new_hdmi_ctl(struct bcm2835_chip *chip);
int snd_bcm2835_new_headphones_ctl(struct bcm2835_chip *chip);
int bcm2835_audio_open(struct bcm2835_alsa_stream *alsa_stream);
int bcm2835_audio_close(struct bcm2835_alsa_stream *alsa_stream);