Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Rewrite ALSA device enumeration and selection code #987

Merged
merged 2 commits into from

7 participants

@anssih
Collaborator

The current ALSA device enumeration code steps through all the hardware
ALSA cards and devices while trying to guess the symbolic "hdmi" and
"iec958" names that will be used with ALSA. It misses any virtual
devices, such as "pulse" or "default". In addition, it didn't have any
handling for "surroundXX", which is needed on many cards to get
multi-channel analog output. Also, it didn't find "iec958" devices that
have no separate hardware device from the analog device, like on USB
sound cards.

Solve those issues by rewriting the ALSA device handling code to use the
ALSA device name hints to determine which devices should be used. This
is the same method "aplay -L" uses.

The code will automatically use the correct front/surround40/surround51/
surround71 device depending on the amount of channels requested. This is
achieved with a magic "@" in the beginning of the device string, which
will be replaced with the wanted base device name ("front",
"surround51", etc.). In addition, the "sysdefault" and "default"
dmix-enabled (allowing shared usage) outputs will be automatically used
when available, for stereo streams that have the same sample rate as the
ALSA dmix mixer has (normally 48000Hz).

The enumeration code will now add a preferred "Default" device, which is
the ALSA default device. This allows XBMC to follow the system-wide
configuration, so that when the user e.g. modifies /etc/asound.conf or
changes environment variables ($ALSA_CARD etc.), XBMC will follow those.
The device name of the "Default" device will be either "default" or "@",
depending on if the default device needs "surroundXX" mangling for
multichannel support (normal analog devices need it, but if the ALSA
default is e.g. the pulseaudio sound server, it is not needed nor
wanted).

Also, additional differentation (device number and/or card id) is now
added to the device names when identical names were found by
enumeration.

Additionally, the AES parameters are now set even when not using
passthrough for S/PDIF and HDMI devices as specified by IEC 60958.
The code will fallback to not setting the parameters since it may
sometimes fail if e.g. using a custom device that does not accept them.

The behavior of setting parameters (channel count, sample rate) is not
altered.

anssih added some commits
@anssih anssih rewritten: ALSA device enumeration and selection code
The current ALSA device enumeration code steps through all the hardware
ALSA cards and devices while trying to guess the symbolic "hdmi" and
"iec958" names that will be used with ALSA. It misses any virtual
devices, such as "pulse" or "default". In addition, it didn't have any
handling for "surroundXX", which is needed on many cards to get
multi-channel analog output. Also, it didn't find "iec958" devices that
have no separate hardware device from the analog device, like on USB
sound cards.

Solve those issues by rewriting the ALSA device handling code to use the
ALSA device name hints to determine which devices should be used. This
is the same method "aplay -L" uses.

The code will automatically use the correct front/surround40/surround51/
surround71 device depending on the amount of channels requested. This is
achieved with a magic "@" in the beginning of the device string, which
will be replaced with the wanted base device name ("front",
"surround51", etc.). In addition, the "sysdefault" and "default"
dmix-enabled (allowing shared usage) outputs will be automatically used
when available, for stereo streams that have the same sample rate as the
ALSA dmix mixer has (normally 48000Hz).

The enumeration code will now add a preferred "Default" device, which is
the ALSA default device. This allows XBMC to follow the system-wide
configuration, so that when the user e.g. modifies /etc/asound.conf or
changes environment variables ($ALSA_CARD etc.), XBMC will follow those.
The device name of the "Default" device will be either "default" or "@",
depending on if the default device needs "surroundXX" mangling for
multichannel support (normal analog devices need it, but if the ALSA
default is e.g. the pulseaudio sound server, it is not needed nor
wanted).

Also, additional differentation (device number and/or card id) is now
added to the device names when identical names were found by
enumeration.

Additionally, the AES parameters are now set even when not using
passthrough for S/PDIF and HDMI devices as specified by IEC 60958.
The code will fallback to not setting the parameters since it may
sometimes fail if e.g. using a custom device that does not accept them.

The behavior of setting parameters (channel count, sample rate) is not
altered.
4c1bcc7
@anssih anssih added: log wrapper for ALSA errors b73081c
@gnif
Collaborator
@dteirney
Collaborator

I've merged in these changes to my HTPC and can now get HDMI working using the standard GUI options, e.g. without needing to use plughw:1,7.

Collaborator

I still don't get HDMI device for passthrough on ATI (fglrx).
aplay -L:

hdmi:CARD=Generic,DEV=0
    HD-Audio Generic, HDMI 0
    HDMI Audio Output

This device is not listed under enumerated ALSA devices, only plughw:CARD=Generic,DEV=3 which I can select as audio device.

Collaborator

@FernetMenta, yes, but I think I know what the issue could be, was waiting for you to come in IRC so that I could get more info so that I could fix it in the best way possible :)

Specifically, could you give the output of "amixer contents -c Generic"?

Collaborator

I am stuck in telephone conferences ....

xbmc@AD02:~$ amixer contents -c Generic
numid=1,iface=CARD,name='HDMI/DP,pcm=3 Jack'
  ; type=BOOLEAN,access=r-------,values=1
  : values=off
numid=2,iface=MIXER,name='IEC958 Playback Con Mask'
  ; type=IEC958,access=r-------,values=1
  : values=[AES0=0x0f AES1=0xff AES2=0x00 AES3=0x00]
numid=3,iface=MIXER,name='IEC958 Playback Pro Mask'
  ; type=IEC958,access=r-------,values=1
  : values=[AES0=0x0f AES1=0x00 AES2=0x00 AES3=0x00]
numid=4,iface=MIXER,name='IEC958 Playback Default'
  ; type=IEC958,access=rw------,values=1
  : values=[AES0=0x04 AES1=0x82 AES2=0x00 AES3=0x02]
numid=5,iface=MIXER,name='IEC958 Playback Switch'
  ; type=BOOLEAN,access=rw------,values=1
  : values=on
numid=6,iface=PCM,name='ELD',device=3
  ; type=BYTES,access=r-------,values=0
  : values=

Collaborator
@anssih
Collaborator

One thing I'm not sure of is the magic placeholder for the "front/default/surround51/surround71" string in the device name. I've currently used "@", but would something else be more clear maybe, like $foo$ with something interesting as "foo"? I couldn't think think of any descriptive word so I used a non-descriptive "@" :)

This is the thing that will appear in the device names like "@" or "@:CARD=Intel,DEV=0".

@gyunaev
Collaborator

I can confirm that applying this merge fixes the playback on my machine which has both the analog output (which I use) and the HDMI output (which I do not use). Without it the current master doesn't play any sound at all nor it allows selecting the analog playback.

@anssih anssih was assigned
@elupus elupus commented on the diff
xbmc/cores/AudioEngine/Sinks/AESinkALSA.cpp
@@ -144,26 +151,40 @@ bool CAESinkALSA::Initialize(AEAudioFormat &format, std::string &device)
format.m_channelLayout = m_channelLayout;
- /* if passthrough we need the additional AES flags */
- if (m_passthrough)
- GetPassthroughDevice(format, device);
+ std::string AESParams;
+ /* digital interfaces should have AESx set, though in practice most
+ * receivers don't care */
+ if (m_passthrough || device.substr(0, 6) == "iec958" || device.substr(0, 4) == "hdmi")
@elupus Collaborator
elupus added a note

I think some systems expose a aplay -L pcm called spdif, think it shouldn't hurt to add it?

@anssih Collaborator
anssih added a note

Yep, will add in a separate commit.

Thanks for reviewing the code, it is really appreciated :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@elupus elupus commented on the diff
xbmc/cores/AudioEngine/Sinks/AESinkALSA.cpp
((74 lines not shown))
- if (snd_ctl_open_lconf(&ctlhandle, strHwName.c_str(), 0, config) != 0)
+bool CAESinkALSA::OpenPCMDevice(const std::string &name, const std::string &params, int channels, snd_pcm_t **pcmp, snd_config_t *lconf, bool preferDmixStereo)
+{
+ /* Special name denoting surroundXX mangling. This is needed for some
+ * devices for multichannel to work. */
+ if (name == "@" || name.substr(0, 2) == "@:")
@elupus Collaborator
elupus added a note

Is there a reason why it's not just replacing @ with the device name instead of just the first char being that?

@anssih Collaborator
anssih added a note

Can't think of one at least immediately, guess I'll change that.

While on the subject, do you have any better suggestion than using "@"?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@elupus elupus commented on the diff
xbmc/cores/AudioEngine/Sinks/AESinkALSA.cpp
((272 lines not shown))
- /* some HDMI devices (intel) report Digital for HDMI also */
- if (devname.find("Digital") != std::string::npos)
- maybeHDMI = true;
+ for (AEDeviceInfoList::iterator it1 = list.begin(); it1 != list.end(); ++it1)
+ {
+ for (AEDeviceInfoList::iterator it2 = it1+1; it2 != list.end(); ++it2)
+ {
+ if (it1->m_displayName == it2->m_displayName
+ && it1->m_displayNameExtra == it2->m_displayNameExtra)
+ {
+ /* something needs to be done */
+ std::string cardString1;
+ std::string cardString2;
+ GetParamFromName(it1->m_deviceName, "CARD", cardString1);
+ GetParamFromName(it2->m_deviceName, "CARD", cardString2);
@elupus Collaborator
elupus added a note

Cleaner if this would just return the string instead of as a parameter, then these two line things could be combined. But it's just a suggestion, not important.

@anssih Collaborator
anssih added a note

Right, will change in a separate commit (probably after merge).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@anssih anssih merged commit 2de2058 into xbmc:master
@manio

This sample rate comparision leads to problems on my ALSA-only system. I had to remove it otherwise my 44.1kHz music play only on front speakers (CAESinkALSA::Initialize - Opened device "front:CARD=Audigy,DEV=0") while all 48kHz audio play correctly on 5.1 (CAESinkALSA::Initialize - Opened device "sysdefault:CARD=Audigy"). More info in http://trac.xbmc.org/ticket/13381

@tru tru referenced this pull request from a commit in RasPlex/plex-home-theatre
@tru tru Add button for hiding cloud sync servers.
Fixes #987
43582e8
@tru tru referenced this pull request from a commit in plexinc/plex-home-theater-public
@tru tru Add button for hiding cloud sync servers.
Fixes #987
5a82138
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on May 20, 2012
  1. @anssih

    rewritten: ALSA device enumeration and selection code

    anssih authored
    The current ALSA device enumeration code steps through all the hardware
    ALSA cards and devices while trying to guess the symbolic "hdmi" and
    "iec958" names that will be used with ALSA. It misses any virtual
    devices, such as "pulse" or "default". In addition, it didn't have any
    handling for "surroundXX", which is needed on many cards to get
    multi-channel analog output. Also, it didn't find "iec958" devices that
    have no separate hardware device from the analog device, like on USB
    sound cards.
    
    Solve those issues by rewriting the ALSA device handling code to use the
    ALSA device name hints to determine which devices should be used. This
    is the same method "aplay -L" uses.
    
    The code will automatically use the correct front/surround40/surround51/
    surround71 device depending on the amount of channels requested. This is
    achieved with a magic "@" in the beginning of the device string, which
    will be replaced with the wanted base device name ("front",
    "surround51", etc.). In addition, the "sysdefault" and "default"
    dmix-enabled (allowing shared usage) outputs will be automatically used
    when available, for stereo streams that have the same sample rate as the
    ALSA dmix mixer has (normally 48000Hz).
    
    The enumeration code will now add a preferred "Default" device, which is
    the ALSA default device. This allows XBMC to follow the system-wide
    configuration, so that when the user e.g. modifies /etc/asound.conf or
    changes environment variables ($ALSA_CARD etc.), XBMC will follow those.
    The device name of the "Default" device will be either "default" or "@",
    depending on if the default device needs "surroundXX" mangling for
    multichannel support (normal analog devices need it, but if the ALSA
    default is e.g. the pulseaudio sound server, it is not needed nor
    wanted).
    
    Also, additional differentation (device number and/or card id) is now
    added to the device names when identical names were found by
    enumeration.
    
    Additionally, the AES parameters are now set even when not using
    passthrough for S/PDIF and HDMI devices as specified by IEC 60958.
    The code will fallback to not setting the parameters since it may
    sometimes fail if e.g. using a custom device that does not accept them.
    
    The behavior of setting parameters (channel count, sample rate) is not
    altered.
  2. @anssih
This page is out of date. Refresh to see the latest.
View
681 xbmc/cores/AudioEngine/Sinks/AESinkALSA.cpp
@@ -23,6 +23,7 @@
#include <stdint.h>
#include <limits.h>
+#include <set>
#include <sstream>
#include "AESinkALSA.h"
@@ -34,7 +35,7 @@
#include "threads/SingleLock.h"
#include "settings/GUISettings.h"
-#define ALSA_OPTIONS (SND_PCM_NONBLOCK | SND_PCM_NO_AUTO_FORMAT | SND_PCM_NO_AUTO_RESAMPLE)
+#define ALSA_OPTIONS (SND_PCM_NONBLOCK | SND_PCM_NO_AUTO_FORMAT | SND_PCM_NO_AUTO_CHANNELS | SND_PCM_NO_AUTO_RESAMPLE)
#define ALSA_PERIODS 16
#define ALSA_MAX_CHANNELS 16
@@ -105,17 +106,23 @@ inline CAEChannelInfo CAESinkALSA::GetChannelLayout(AEAudioFormat format)
return info;
}
-void CAESinkALSA::GetPassthroughDevice(AEAudioFormat format, std::string& device)
+void CAESinkALSA::GetAESParams(AEAudioFormat format, std::string& params)
{
- device += ",AES0=0x06,AES1=0x82,AES2=0x00";
- if (format.m_sampleRate == 192000) device += ",AES3=0x0e";
- else if (format.m_sampleRate == 176400) device += ",AES3=0x0c";
- else if (format.m_sampleRate == 96000) device += ",AES3=0x0a";
- else if (format.m_sampleRate == 88200) device += ",AES3=0x08";
- else if (format.m_sampleRate == 48000) device += ",AES3=0x02";
- else if (format.m_sampleRate == 44100) device += ",AES3=0x00";
- else if (format.m_sampleRate == 32000) device += ",AES3=0x03";
- else device += ",AES3=0x01";
+ if (m_passthrough)
+ params = "AES0=0x06";
+ else
+ params = "AES0=0x04";
+
+ params += ",AES1=0x82,AES2=0x00";
+
+ if (format.m_sampleRate == 192000) params += ",AES3=0x0e";
+ else if (format.m_sampleRate == 176400) params += ",AES3=0x0c";
+ else if (format.m_sampleRate == 96000) params += ",AES3=0x0a";
+ else if (format.m_sampleRate == 88200) params += ",AES3=0x08";
+ else if (format.m_sampleRate == 48000) params += ",AES3=0x02";
+ else if (format.m_sampleRate == 44100) params += ",AES3=0x00";
+ else if (format.m_sampleRate == 32000) params += ",AES3=0x03";
+ else params += ",AES3=0x01";
}
bool CAESinkALSA::Initialize(AEAudioFormat &format, std::string &device)
@@ -144,26 +151,40 @@ bool CAESinkALSA::Initialize(AEAudioFormat &format, std::string &device)
format.m_channelLayout = m_channelLayout;
- /* if passthrough we need the additional AES flags */
- if (m_passthrough)
- GetPassthroughDevice(format, device);
+ std::string AESParams;
+ /* digital interfaces should have AESx set, though in practice most
+ * receivers don't care */
+ if (m_passthrough || device.substr(0, 6) == "iec958" || device.substr(0, 4) == "hdmi")
@elupus Collaborator
elupus added a note

I think some systems expose a aplay -L pcm called spdif, think it shouldn't hurt to add it?

@anssih Collaborator
anssih added a note

Yep, will add in a separate commit.

Thanks for reviewing the code, it is really appreciated :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ GetAESParams(format, AESParams);
- m_device = device;
- CLog::Log(LOGINFO, "CAESinkALSA::Initialize - Attempting to open device %s", device.c_str());
+ CLog::Log(LOGINFO, "CAESinkALSA::Initialize - Attempting to open device \"%s\"", device.c_str());
/* get the sound config */
snd_config_t *config;
snd_config_copy(&config, snd_config);
- int error;
- error = snd_pcm_open_lconf(&m_pcm, device.c_str(), SND_PCM_STREAM_PLAYBACK, ALSA_OPTIONS, config);
- if (error < 0)
+ snd_config_t *dmixRateConf;
+ long dmixRate;
+
+ if (snd_config_search(config, "defaults.pcm.dmix.rate", &dmixRateConf) < 0
+ || snd_config_get_integer(dmixRateConf, &dmixRate) < 0)
+ dmixRate = 48000; /* assume default */
+
+
+ /* Prefer dmix for non-passthrough stereo when sample rate matches */
+ if (!OpenPCMDevice(device, AESParams, m_channelLayout.Count(), &m_pcm, config, format.m_sampleRate == dmixRate && !m_passthrough))
{
- CLog::Log(LOGERROR, "CAESinkALSA::Initialize - snd_pcm_open_lconf(%d) - %s", error, device.c_str());
+ CLog::Log(LOGERROR, "CAESinkALSA::Initialize - failed to initialize device \"%s\"", device.c_str());
snd_config_delete(config);
return false;
}
+ /* get the actual device name that was used */
+ device = snd_pcm_name(m_pcm);
+ m_device = device;
+
+ CLog::Log(LOGINFO, "CAESinkALSA::Initialize - Opened device \"%s\"", device.c_str());
+
/* free the sound config */
snd_config_delete(config);
@@ -334,7 +355,7 @@ bool CAESinkALSA::InitializeHW(AEAudioFormat &format)
{
CLog::Log(LOGERROR, "CAESinkALSA::InitializeHW - Failed to set the parameters");
return false;
- }
+ }
}
}
}
@@ -525,220 +546,505 @@ void CAESinkALSA::Drain()
snd_pcm_nonblock(m_pcm, 1);
}
-void CAESinkALSA::EnumerateDevicesEx(AEDeviceInfoList &list)
+void CAESinkALSA::AppendParams(std::string &device, const std::string &params)
{
- /* ensure that ALSA has been initialized */
- if(!snd_config)
- snd_config_update();
+ /* Note: escaping, e.g. "plug:'something:X=y'" isn't handled,
+ * but it is not normally encountered at this point. */
- snd_ctl_t *ctlhandle;
- snd_pcm_t *pcmhandle;
+ device += (device.find(':') == std::string::npos) ? ':' : ',';
+ device += params;
+}
- snd_ctl_card_info_t *ctlinfo;
- snd_ctl_card_info_alloca(&ctlinfo);
- memset(ctlinfo, 0, snd_ctl_card_info_sizeof());
+bool CAESinkALSA::TryDevice(const std::string &name, snd_pcm_t **pcmp, snd_config_t *lconf)
+{
+ /* Check if this device was already open (e.g. when checking for supported
+ * channel count in EnumerateDevice()) */
+ if (*pcmp)
+ {
+ if (name == snd_pcm_name(*pcmp))
+ return true;
- snd_pcm_hw_params_t *hwparams;
- snd_pcm_hw_params_alloca(&hwparams);
- memset(hwparams, 0, snd_pcm_hw_params_sizeof());
+ snd_pcm_close(*pcmp);
+ *pcmp = NULL;
+ }
- snd_pcm_info_t *pcminfo;
- snd_pcm_info_alloca(&pcminfo);
- memset(pcminfo, 0, snd_pcm_info_sizeof());
+ int err = snd_pcm_open_lconf(pcmp, name.c_str(), SND_PCM_STREAM_PLAYBACK, ALSA_OPTIONS, lconf);
+ if (err < 0)
+ {
+ CLog::Log(LOGINFO, "CAESinkALSA - Unable to open device \"%s\" for playback", name.c_str());
+ }
- /* get the sound config */
- snd_config_t *config;
- snd_config_copy(&config, snd_config);
+ return err == 0;
+}
- std::string strHwName;
- int n_cards = -1;
- while (snd_card_next(&n_cards) == 0 && n_cards != -1)
+bool CAESinkALSA::TryDeviceWithParams(const std::string &name, const std::string &params, snd_pcm_t **pcmp, snd_config_t *lconf)
+{
+ if (!params.empty())
{
- std::stringstream sstr;
- sstr << "hw:" << n_cards;
- std::string strHwName = sstr.str();
+ std::string nameWithParams = name;
+ AppendParams(nameWithParams, params);
+ if (TryDevice(nameWithParams, pcmp, lconf))
+ return true;
+ }
+
+ /* Try the variant without extra parameters.
+ * Custom devices often do not take the AESx parameters, for example.
+ */
+ return TryDevice(name, pcmp, lconf);
+}
- if (snd_ctl_open_lconf(&ctlhandle, strHwName.c_str(), 0, config) != 0)
+bool CAESinkALSA::OpenPCMDevice(const std::string &name, const std::string &params, int channels, snd_pcm_t **pcmp, snd_config_t *lconf, bool preferDmixStereo)
+{
+ /* Special name denoting surroundXX mangling. This is needed for some
+ * devices for multichannel to work. */
+ if (name == "@" || name.substr(0, 2) == "@:")
@elupus Collaborator
elupus added a note

Is there a reason why it's not just replacing @ with the device name instead of just the first char being that?

@anssih Collaborator
anssih added a note

Can't think of one at least immediately, guess I'll change that.

While on the subject, do you have any better suggestion than using "@"?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ {
+ std::string openName = name.substr(1);
+
+ /* These device names allow alsa-lib to perform special routing if needed
+ * for multichannel to work with the audio hardware.
+ * Fall through in switch() so that devices with more channels are
+ * added as fallback. */
+ switch (channels)
{
- CLog::Log(LOGDEBUG, "CAESinkALSA::EnumerateDevicesEx - Unable to open control for device %s", strHwName.c_str());
- continue;
+ case 3:
+ case 4:
+ if (TryDeviceWithParams("surround40" + openName, params, pcmp, lconf))
+ return true;
+ case 5:
+ case 6:
+ if (TryDeviceWithParams("surround51" + openName, params, pcmp, lconf))
+ return true;
+ case 7:
+ case 8:
+ if (TryDeviceWithParams("surround71" + openName, params, pcmp, lconf))
+ return true;
}
- if (snd_ctl_card_info(ctlhandle, ctlinfo) != 0)
+ /* If preferDmix is false, try non-dmix configuration first.
+ * This allows output with non-48000 sample rate if device is free */
+ if (!preferDmixStereo && TryDeviceWithParams("front" + openName, params, pcmp, lconf))
+ return true;
+
+ /* Try "sysdefault" and "default" (they provide dmix),
+ * unless the selected devices is not DEV=0 of the card, in which case
+ * "sysdefault" and "default" would point to another device.
+ * "sysdefault" is a newish device name that won't be overwritten in case
+ * system configuration redefines "default". "default" is still tried
+ * because "sysdefault" is rather new. */
+ size_t devPos = openName.find(",DEV=");
+ if (devPos == std::string::npos || (devPos + 5 < openName.size() && openName[devPos+5] == '0'))
{
- CLog::Log(LOGDEBUG, "CAESinkALSA::EnumerateDevicesEx - Unable to get card control info for device %s", strHwName.c_str());
- snd_ctl_close(ctlhandle);
- continue;
+ /* "sysdefault" and "default" do not have "DEV=0", drop it */
+ std::string nameWithoutDev = openName;
+ if (devPos != std::string::npos)
+ nameWithoutDev.erase(nameWithoutDev.begin() + devPos, nameWithoutDev.begin() + devPos + 6);
+
+ if (TryDeviceWithParams("sysdefault" + nameWithoutDev, params, pcmp, lconf)
+ || TryDeviceWithParams("default" + nameWithoutDev, params, pcmp, lconf))
+ return true;
}
- snd_hctl_t *hctl;
- if (snd_hctl_open_ctl(&hctl, ctlhandle) != 0)
- hctl = NULL;
- snd_hctl_load(hctl);
+ /* Try non-dmix "front" */
+ if (preferDmixStereo && TryDeviceWithParams("front" + openName, params, pcmp, lconf))
+ return true;
+
+ }
+ else
+ {
+ /* Non-surroundXX device, just add it */
+ if (TryDeviceWithParams(name, params, pcmp, lconf))
+ return true;
+ }
+
+ return false;
+}
+
+void CAESinkALSA::EnumerateDevicesEx(AEDeviceInfoList &list)
+{
+ /* ensure that ALSA has been initialized */
+ snd_lib_error_set_handler(sndLibErrorHandler);
+ if(!snd_config)
+ snd_config_update();
+
+ snd_config_t *config;
+ snd_config_copy(&config, snd_config);
+
+ /* Always enumerate the default device.
+ * Note: If "default" is a stereo device, EnumerateDevice()
+ * will automatically add "@" instead to enable surroundXX mangling.
+ * We don't want to do that if "default" can handle multichannel
+ * itself (e.g. in case of a pulseaudio server). */
+ EnumerateDevice(list, "default", "", config);
- int pcm_index = 0;
- int iec958_index = 0;
- int hdmi_index = 0;
+ void **hints;
- int dev = -1;
- while (snd_ctl_pcm_next_device(ctlhandle, &dev) == 0 && dev != -1)
+ if (snd_device_name_hint(-1, "pcm", &hints) < 0)
+ {
+ CLog::Log(LOGINFO, "CAESinkALSA - Unable to get a list of devices");
+ return;
+ }
+
+ std::string defaultDescription;
+
+ for (void** hint = hints; *hint != NULL; ++hint)
+ {
+ char *io = snd_device_name_get_hint(*hint, "IOID");
+ char *name = snd_device_name_get_hint(*hint, "NAME");
+ char *desc = snd_device_name_get_hint(*hint, "DESC");
+ if ((!io || strcmp(io, "Output") == 0) && name
+ && strcmp(name, "null") != 0)
{
- snd_pcm_info_set_device (pcminfo, dev);
- snd_pcm_info_set_subdevice(pcminfo, 0 );
- snd_pcm_info_set_stream (pcminfo, SND_PCM_STREAM_PLAYBACK);
+ std::string baseName = std::string(name);
+ baseName = baseName.substr(0, baseName.find(':'));
- if (snd_ctl_pcm_info(ctlhandle, pcminfo) < 0)
+ if (strcmp(name, "default") == 0)
{
- CLog::Log(LOGDEBUG, "CAESinkALSA::EnumerateDevicesEx - Skipping device %s,%d as it does not have PCM playback ability", strHwName.c_str(), dev);
- continue;
+ /* added already, but lets get the description if we have one */
+ if (desc)
+ defaultDescription = desc;
}
+ else if (baseName == "front")
+ {
+ /* Enumerate using the surroundXX mangling */
+ /* do not enumerate basic "front", it is already handled
+ * by the default "@" entry added in the very beginning */
+ if (strcmp(name, "front") != 0)
+ EnumerateDevice(list, std::string("@") + (name+5), desc ? desc : name, config);
+ }
+ /* Do not enumerate the sysdefault or surroundXX devices, those are
+ * always accompanied with a "front" device and it is handled above
+ * as "@". The below devices will be automatically used if available
+ * for a "@" device. */
+ else if (baseName != "default"
+ && baseName != "sysdefault"
+ && baseName != "surround40"
+ && baseName != "surround41"
+ && baseName != "surround50"
+ && baseName != "surround51"
+ && baseName != "surround71")
+ {
+ EnumerateDevice(list, name, desc ? desc : name, config);
+ }
+ }
+ free(io);
+ free(name);
+ free(desc);
+ }
+ snd_device_name_free_hint(hints);
+
+ /* set the displayname for default device */
+ if (!list.empty() && list[0].m_deviceName == "default")
+ {
+ /* If we have one from a hint (DESC), use it */
+ if (!defaultDescription.empty())
+ list[0].m_displayName = defaultDescription;
+ /* Otherwise use the discovered name or (unlikely) "Default" */
+ else if (list[0].m_displayName.empty())
+ list[0].m_displayName = "Default";
+ }
- int dev_index;
- sstr.str(std::string());
- CAEDeviceInfo info;
- std::string devname = snd_pcm_info_get_name(pcminfo);
+ /* lets check uniqueness, we may need to append DEV or CARD to DisplayName */
+ /* If even a single device of card/dev X clashes with Y, add suffixes to
+ * all devices of both them, for clarity. */
- bool maybeHDMI = false;
+ /* clashing card names, e.g. "NVidia", "NVidia_2" */
+ std::set<std::string> cardsToAppend;
- /* detect HDMI */
- if (devname.find("HDMI") != std::string::npos)
- {
- info.m_deviceType = AE_DEVTYPE_HDMI;
- dev_index = hdmi_index++;
- sstr << "hdmi";
- }
- else
- {
- /* detect IEC958 */
+ /* clashing basename + cardname combinations, e.g. ("hdmi","Nvidia") */
+ std::set<std::pair<std::string, std::string> > devsToAppend;
- /* some HDMI devices (intel) report Digital for HDMI also */
- if (devname.find("Digital") != std::string::npos)
- maybeHDMI = true;
+ for (AEDeviceInfoList::iterator it1 = list.begin(); it1 != list.end(); ++it1)
+ {
+ for (AEDeviceInfoList::iterator it2 = it1+1; it2 != list.end(); ++it2)
+ {
+ if (it1->m_displayName == it2->m_displayName
+ && it1->m_displayNameExtra == it2->m_displayNameExtra)
+ {
+ /* something needs to be done */
+ std::string cardString1;
+ std::string cardString2;
+ GetParamFromName(it1->m_deviceName, "CARD", cardString1);
+ GetParamFromName(it2->m_deviceName, "CARD", cardString2);
@elupus Collaborator
elupus added a note

Cleaner if this would just return the string instead of as a parameter, then these two line things could be combined. But it's just a suggestion, not important.

@anssih Collaborator
anssih added a note

Right, will change in a separate commit (probably after merge).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
- if (maybeHDMI || devname.find("IEC958" ) != std::string::npos)
+ if (cardString1 != cardString2)
{
- info.m_deviceType = AE_DEVTYPE_IEC958;
- dev_index = iec958_index; /* dont increment, it might be HDMI */
- sstr << "iec958";
+ /* card name differs, add identifiers to all devices */
+ cardsToAppend.insert(cardString1);
+ cardsToAppend.insert(cardString2);
+ continue;
}
- else
+
+ std::string devString1;
+ std::string devString2;
+ GetParamFromName(it1->m_deviceName, "DEV", devString1);
+ GetParamFromName(it2->m_deviceName, "DEV", devString2);
+
+ if (devString1 != devString2)
{
- info.m_deviceType = AE_DEVTYPE_PCM;
- dev_index = pcm_index++;
- sstr << "hw";
+ /* device number differs, add identifiers to all such devices */
+ devsToAppend.insert(std::make_pair(it1->m_deviceName.substr(0, it1->m_deviceName.find(':')), cardString1));
+ devsToAppend.insert(std::make_pair(it2->m_deviceName.substr(0, it2->m_deviceName.find(':')), cardString2));
+ continue;
}
+
+ /* if we got here, the configuration is really weird, just give up */
+ it1->m_displayName = it1->m_deviceName;
+ it2->m_displayName = it2->m_deviceName;
+ }
+ }
+ }
+
+ for (std::set<std::string>::iterator it = cardsToAppend.begin();
+ it != cardsToAppend.end(); ++it)
+ {
+ for (AEDeviceInfoList::iterator itl = list.begin(); itl != list.end(); ++itl)
+ {
+ std::string cardString;
+ GetParamFromName(itl->m_deviceName, "CARD", cardString);
+ if (cardString == *it)
+ /* "HDA NVidia (NVidia)", "HDA NVidia (NVidia_2)", ... */
+ itl->m_displayName += " (" + cardString + ")";
+ }
+ }
+
+ for (std::set<std::pair<std::string, std::string> >::iterator it = devsToAppend.begin();
+ it != devsToAppend.end(); ++it)
+ {
+ for (AEDeviceInfoList::iterator itl = list.begin(); itl != list.end(); ++itl)
+ {
+ std::string baseName = itl->m_deviceName.substr(0, itl->m_deviceName.find(':'));
+ std::string cardString;
+ GetParamFromName(itl->m_deviceName, "CARD", cardString);
+ if (baseName == it->first && cardString == it->second)
+ {
+ std::string devString;
+ GetParamFromName(itl->m_deviceName, "DEV", devString);
+ /* "HDMI #0", "HDMI #1" ... */
+ itl->m_displayNameExtra += " #" + devString;
}
+ }
+ }
+}
- /* build the driver string to pass to ALSA */
- sstr << ":CARD=" << snd_ctl_card_info_get_id(ctlinfo) << ",DEV=" << dev_index;
- info.m_deviceName = sstr.str();
+void CAESinkALSA::GetParamFromName(const std::string &name, const std::string &param, std::string &value)
+{
+ /* name = "hdmi:CARD=x,DEV=y" param = "CARD" => value = "x" */
+ size_t parPos = name.find(param + '=');
+ if (parPos != std::string::npos)
+ {
+ parPos += param.size() + 1;
+ value = name.substr(parPos, name.find_first_of(",'\"", parPos)-parPos);
+ }
+ else
+ {
+ value = "";
+ }
+}
- /* get the friendly display name*/
- info.m_displayName = snd_ctl_card_info_get_name(ctlinfo);
- info.m_displayNameExtra = devname;
+void CAESinkALSA::EnumerateDevice(AEDeviceInfoList &list, const std::string &device, const std::string &description, snd_config_t *config)
+{
+ snd_pcm_t *pcmhandle = NULL;
+ if (!OpenPCMDevice(device, "", ALSA_MAX_CHANNELS, &pcmhandle, config, false))
+ return;
- /* open the device for testing */
- int err = snd_pcm_open_lconf(&pcmhandle, info.m_deviceName.c_str(), SND_PCM_STREAM_PLAYBACK, 0, config);
+ snd_pcm_info_t *pcminfo;
+ snd_pcm_info_alloca(&pcminfo);
+ memset(pcminfo, 0, snd_pcm_info_sizeof());
- /* if open of possible IEC958 failed and it could be HDMI, try as HDMI */
- if (err < 0 && maybeHDMI)
- {
- /* check for HDMI if it failed */
- sstr.str(std::string());
- dev_index = hdmi_index;
+ int err = snd_pcm_info(pcmhandle, pcminfo);
+ if (err < 0)
+ {
+ CLog::Log(LOGINFO, "CAESinkALSA - Unable to get pcm_info for \"%s\"", device.c_str());
+ snd_pcm_close(pcmhandle);
+ }
- sstr << "hdmi";
- sstr << ":CARD=" << snd_ctl_card_info_get_id(ctlinfo) << ",DEV=" << dev_index;
- info.m_deviceName = sstr.str();
- err = snd_pcm_open_lconf(&pcmhandle, info.m_deviceName.c_str(), SND_PCM_STREAM_PLAYBACK, 0, config);
+ int cardNr = snd_pcm_info_get_card(pcminfo);
- /* if it was valid, increment the index and set the type */
- if (err >= 0)
- {
- ++hdmi_index;
- info.m_deviceType = AE_DEVTYPE_HDMI;
- }
- }
+ CAEDeviceInfo info;
+ info.m_deviceName = device;
- /* if it's still IEC958, increment the index */
- if (info.m_deviceType == AE_DEVTYPE_IEC958)
- ++iec958_index;
+ bool isHDMI = (device.substr(0, 4) == "hdmi");
+ bool isSPDIF = (device.substr(0, 6) == "iec958");
- /* final error check */
- if (err < 0)
- {
- CLog::Log(LOGINFO, "CAESinkALSA::EnumerateDevicesEx - Unable to open %s for capability detection", strHwName.c_str());
- continue;
- }
+ if (isHDMI)
+ info.m_deviceType = AE_DEVTYPE_HDMI;
+ else if (isSPDIF)
+ info.m_deviceType = AE_DEVTYPE_IEC958;
+ else
+ info.m_deviceType = AE_DEVTYPE_PCM;
+
+ if (cardNr >= 0)
+ {
+ /* "HDA NVidia", "HDA Intel", "HDA ATI HDMI", "SB Live! 24-bit External", ... */
+ char *cardName;
+ if (snd_card_get_name(cardNr, &cardName) == 0)
+ info.m_displayName = cardName;
+
+ if (isHDMI && info.m_displayName.size() > 5 &&
+ info.m_displayName.substr(info.m_displayName.size()-5) == " HDMI")
+ {
+ /* We already know this is HDMI, strip it */
+ info.m_displayName.erase(info.m_displayName.size()-5);
+ }
+
+ /* "CONEXANT Analog", "USB Audio", "HDMI 0", "ALC889 Digital" ... */
+ std::string pcminfoName = snd_pcm_info_get_name(pcminfo);
- /* see if we can get ELD for this device */
- if (info.m_deviceType == AE_DEVTYPE_HDMI)
+ /*
+ * Filter "USB Audio", in those cases snd_card_get_name() is more
+ * meaningful already
+ */
+ if (pcminfoName != "USB Audio")
+ info.m_displayNameExtra = pcminfoName;
+
+ if (isHDMI)
+ {
+ /* replace, this was likely "HDMI 0" */
+ info.m_displayNameExtra = "HDMI";
+
+ int dev = snd_pcm_info_get_device(pcminfo);
+
+ if (dev >= 0)
{
- bool badHDMI = false;
- if (hctl && !GetELD(hctl, dev, info, badHDMI))
- CLog::Log(LOGDEBUG, "CAESinkALSA::EnumerateDevicesEx - Unable to obtain ELD information for device %s, make sure you have ALSA >= 1.0.25", info.m_deviceName.c_str());
+ /* lets see if we can get ELD info */
+
+ snd_ctl_t *ctlhandle;
+ std::stringstream sstr;
+ sstr << "hw:" << cardNr;
+ std::string strHwName = sstr.str();
- if (badHDMI)
+ if (snd_ctl_open_lconf(&ctlhandle, strHwName.c_str(), 0, config) == 0)
{
- CLog::Log(LOGDEBUG, "CAESinkALSA::EnumerateDevicesEx - Skipping HDMI device %s as it has no ELD data", info.m_deviceName.c_str());
- snd_pcm_close(pcmhandle);
- continue;
+ snd_hctl_t *hctl;
+ if (snd_hctl_open_ctl(&hctl, ctlhandle) == 0)
+ {
+ snd_hctl_load(hctl);
+ bool badHDMI = false;
+ if (!GetELD(hctl, dev, info, badHDMI))
+ CLog::Log(LOGDEBUG, "CAESinkALSA - Unable to obtain ELD information for device \"%s\" (not supported by device, or kernel older than 3.2)",
+ device.c_str());
+
+ /* snd_hctl_close also closes ctlhandle */
+ snd_hctl_close(hctl);
+
+ if (badHDMI)
+ {
+ /* unconnected HDMI port */
+ CLog::Log(LOGDEBUG, "CAESinkALSA - Skipping HDMI device \"%s\" as it has no ELD data", device.c_str());
+ snd_pcm_close(pcmhandle);
+ return;
+ }
+ }
+ else
+ {
+ snd_ctl_close(ctlhandle);
+ }
}
}
+ }
+ else if (isSPDIF)
+ {
+ /* append instead of replace, pcminfoName is useful for S/PDIF */
+ if (!info.m_displayNameExtra.empty())
+ info.m_displayNameExtra += ' ';
+ info.m_displayNameExtra += "S/PDIF";
+ }
+ else if (info.m_displayNameExtra.empty())
+ {
+ /* for USB audio, it gets a bit confusing as there is
+ * - "SB Live! 24-bit External"
+ * - "SB Live! 24-bit External, S/PDIF"
+ * so add "Analog" qualifier to the first one */
+ info.m_displayNameExtra = "Analog";
+ }
- /* ensure we can get a playback configuration for the device */
- if (snd_pcm_hw_params_any(pcmhandle, hwparams) < 0)
- {
- CLog::Log(LOGINFO, "CAESinkALSA::EnumerateDevicesEx - No playback configurations available for device %s", info.m_deviceName.c_str());
- snd_pcm_close(pcmhandle);
- continue;
- }
-
- /* detect the available sample rates */
- for (unsigned int *rate = ALSASampleRateList; *rate != 0; ++rate)
- if (snd_pcm_hw_params_test_rate(pcmhandle, hwparams, *rate, 0) >= 0)
- info.m_sampleRates.push_back(*rate);
+ /* "default" is a device that will be used for all inputs, while
+ * "@" will be mangled to front/default/surroundXX as necessary */
+ if (device == "@" || device == "default")
+ {
+ /* Make it "Default (whatever)" */
+ info.m_displayName = "Default (" + info.m_displayName + (info.m_displayNameExtra.empty() ? "" : " " + info.m_displayNameExtra + ")");
+ info.m_displayNameExtra = "";
+ }
- /* detect the channels available */
- int channels = 0;
- for (int i = 1; i <= ALSA_MAX_CHANNELS; ++i)
- if (snd_pcm_hw_params_test_channels(pcmhandle, hwparams, i) >= 0)
- channels = i;
+ }
+ else
+ {
+ /* virtual devices: "default", "pulse", ... */
+ /* description can be e.g. "PulseAudio Sound Server" - for hw devices it is
+ * normally uninteresting, like "HDMI Audio Output" or "Default Audio Device",
+ * so we only use it for virtual devices that have no better display name */
+ info.m_displayName = description;
+ }
- CAEChannelInfo alsaChannels;
- for (int i = 0; i < channels; ++i)
- {
- if (!info.m_channels.HasChannel(ALSAChannelMap[i]))
- info.m_channels += ALSAChannelMap[i];
- alsaChannels += ALSAChannelMap[i];
- }
+ snd_pcm_hw_params_t *hwparams;
+ snd_pcm_hw_params_alloca(&hwparams);
+ memset(hwparams, 0, snd_pcm_hw_params_sizeof());
- /* remove the channels from m_channels that we cant use */
- info.m_channels.ResolveChannels(alsaChannels);
+ /* ensure we can get a playback configuration for the device */
+ if (snd_pcm_hw_params_any(pcmhandle, hwparams) < 0)
+ {
+ CLog::Log(LOGINFO, "CAESinkALSA - No playback configurations available for device \"%s\"", device.c_str());
+ snd_pcm_close(pcmhandle);
+ return;
+ }
- /* detect the PCM sample formats that are available */
- for (enum AEDataFormat i = AE_FMT_MAX; i > AE_FMT_INVALID; i = (enum AEDataFormat)((int)i - 1))
- {
- if (AE_IS_RAW(i) || i == AE_FMT_MAX)
- continue;
- snd_pcm_format_t fmt = AEFormatToALSAFormat(i);
- if (fmt == SND_PCM_FORMAT_UNKNOWN)
- continue;
+ /* detect the available sample rates */
+ for (unsigned int *rate = ALSASampleRateList; *rate != 0; ++rate)
+ if (snd_pcm_hw_params_test_rate(pcmhandle, hwparams, *rate, 0) >= 0)
+ info.m_sampleRates.push_back(*rate);
- if (snd_pcm_hw_params_test_format(pcmhandle, hwparams, fmt) >= 0)
- info.m_dataFormats.push_back(i);
- }
+ /* detect the channels available */
+ int channels = 0;
+ for (int i = ALSA_MAX_CHANNELS; i >= 1; --i)
+ {
+ /* Reopen the device if needed on the special "surroundXX" cases */
+ if (info.m_deviceType == AE_DEVTYPE_PCM && (i == 8 || i == 6 || i == 4))
+ OpenPCMDevice(device, "", i, &pcmhandle, config, false);
- snd_pcm_close(pcmhandle);
- list.push_back(info);
+ if (snd_pcm_hw_params_test_channels(pcmhandle, hwparams, i) >= 0)
+ {
+ channels = i;
+ break;
}
+ }
+
+ if (device == "default" && channels == 2)
+ {
+ /* This looks like the ALSA standard default stereo dmix device, we
+ * probably want to use "@" instead to get surroundXX. */
+ snd_pcm_close(pcmhandle);
+ EnumerateDevice(list, "@", description, config);
+ return;
+ }
- /* snd_hctl_close also closes ctlhandle */
- if (hctl)
- snd_hctl_close(hctl);
- else
- snd_ctl_close(ctlhandle);
+ CAEChannelInfo alsaChannels;
+ for (int i = 0; i < channels; ++i)
+ {
+ if (!info.m_channels.HasChannel(ALSAChannelMap[i]))
+ info.m_channels += ALSAChannelMap[i];
+ alsaChannels += ALSAChannelMap[i];
}
+
+ /* remove the channels from m_channels that we cant use */
+ info.m_channels.ResolveChannels(alsaChannels);
+
+ /* detect the PCM sample formats that are available */
+ for (enum AEDataFormat i = AE_FMT_MAX; i > AE_FMT_INVALID; i = (enum AEDataFormat)((int)i - 1))
+ {
+ if (AE_IS_RAW(i) || i == AE_FMT_MAX)
+ continue;
+ snd_pcm_format_t fmt = AEFormatToALSAFormat(i);
+ if (fmt == SND_PCM_FORMAT_UNKNOWN)
+ continue;
+
+ if (snd_pcm_hw_params_test_format(pcmhandle, hwparams, fmt) >= 0)
+ info.m_dataFormats.push_back(i);
+ }
+
+ snd_pcm_close(pcmhandle);
+ list.push_back(info);
}
bool CAESinkALSA::GetELD(snd_hctl_t *hctl, int device, CAEDeviceInfo& info, bool& badHDMI)
@@ -792,4 +1098,19 @@ bool CAESinkALSA::GetELD(snd_hctl_t *hctl, int device, CAEDeviceInfo& info, bool
info.m_deviceType = AE_DEVTYPE_HDMI;
return true;
}
+
+void CAESinkALSA::sndLibErrorHandler(const char *file, int line, const char *function, int err, const char *fmt, ...)
+{
+ va_list arg;
+ va_start(arg, fmt);
+ char *errorStr;
+ if (vasprintf(&errorStr, fmt, arg) >= 0)
+ {
+ CLog::Log(LOGINFO, "CAESinkALSA - ALSA: %s:%d:(%s) %s%s%s",
+ file, line, function, errorStr, err ? ": " : "", err ? snd_strerror(err) : "");
+ free(errorStr);
+ }
+ va_end(arg);
+}
+
#endif
View
11 xbmc/cores/AudioEngine/Sinks/AESinkALSA.h
@@ -54,7 +54,7 @@ class CAESinkALSA : public IAESink
static void EnumerateDevicesEx(AEDeviceInfoList &list);
private:
CAEChannelInfo GetChannelLayout(AEAudioFormat format);
- void GetPassthroughDevice(const AEAudioFormat format, std::string& device);
+ void GetAESParams(const AEAudioFormat format, std::string& params);
void HandleError(const char* name, int err);
std::string m_initDevice;
@@ -73,8 +73,17 @@ class CAESinkALSA : public IAESink
bool InitializeHW(AEAudioFormat &format);
bool InitializeSW(AEAudioFormat &format);
+ static void AppendParams(std::string &device, const std::string &params);
+ static bool TryDevice(const std::string &name, snd_pcm_t **pcmp, snd_config_t *lconf);
+ static bool TryDeviceWithParams(const std::string &name, const std::string &params, snd_pcm_t **pcmp, snd_config_t *lconf);
+ static bool OpenPCMDevice(const std::string &name, const std::string &params, int channels, snd_pcm_t **pcmp, snd_config_t *lconf, bool preferDmixStereo = false);
+
+ static void GetParamFromName(const std::string &name, const std::string &param, std::string &value);
+ static void EnumerateDevice(AEDeviceInfoList &list, const std::string &device, const std::string &description, snd_config_t *config);
static bool SoundDeviceExists(const std::string& device);
static bool GetELD(snd_hctl_t *hctl, int device, CAEDeviceInfo& info, bool& badHDMI);
+
+ static void sndLibErrorHandler(const char *file, int line, const char *function, int err, const char *fmt, ...);
};
#endif
Something went wrong with that request. Please try again.