/*
 *  Copyright (C) 2010-2018 Team Kodi
 *  This file is part of Kodi - https://kodi.tv
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  See LICENSES/README.md for more information.
 */

#include "AESinkALSA.h"

#include "ServiceBroker.h"
#include "cores/AudioEngine/AESinkFactory.h"
#include "cores/AudioEngine/Sinks/alsa/ALSADeviceMonitor.h"
#ifndef HAVE_X11
#include "cores/AudioEngine/Sinks/alsa/ALSAHControlMonitor.h"
#endif
#include "cores/AudioEngine/Utils/AEELDParser.h"
#include "cores/AudioEngine/Utils/AEUtil.h"
#include "platform/Platform.h"
#include "utils/XTimeUtils.h"
#include "utils/log.h"

#include <algorithm>
#include <limits.h>
#include <set>
#include <sstream>
#include <stdint.h>
#include <string>

#include <sys/utsname.h>

using namespace std::chrono_literals;

#define ALSA_OPTIONS (SND_PCM_NO_AUTO_FORMAT | SND_PCM_NO_AUTO_CHANNELS | SND_PCM_NO_AUTO_RESAMPLE)

#define ALSA_MAX_CHANNELS 16
static enum AEChannel LegacyALSAChannelMap[ALSA_MAX_CHANNELS + 1] = {
  AE_CH_FL      , AE_CH_FR      , AE_CH_BL      , AE_CH_BR      , AE_CH_FC      , AE_CH_LFE     , AE_CH_SL      , AE_CH_SR      ,
  AE_CH_UNKNOWN1, AE_CH_UNKNOWN2, AE_CH_UNKNOWN3, AE_CH_UNKNOWN4, AE_CH_UNKNOWN5, AE_CH_UNKNOWN6, AE_CH_UNKNOWN7, AE_CH_UNKNOWN8, /* for p16v devices */
  AE_CH_NULL
};

static enum AEChannel LegacyALSAChannelMap51Wide[ALSA_MAX_CHANNELS + 1] = {
  AE_CH_FL      , AE_CH_FR      , AE_CH_SL      , AE_CH_SR      , AE_CH_FC      , AE_CH_LFE     , AE_CH_BL      , AE_CH_BR      ,
  AE_CH_UNKNOWN1, AE_CH_UNKNOWN2, AE_CH_UNKNOWN3, AE_CH_UNKNOWN4, AE_CH_UNKNOWN5, AE_CH_UNKNOWN6, AE_CH_UNKNOWN7, AE_CH_UNKNOWN8, /* for p16v devices */
  AE_CH_NULL
};

static enum AEChannel ALSAChannelMapPassthrough[ALSA_MAX_CHANNELS + 1] = {
  AE_CH_RAW     , AE_CH_RAW     , AE_CH_RAW     , AE_CH_RAW     , AE_CH_RAW     , AE_CH_RAW     , AE_CH_RAW      , AE_CH_RAW      ,
  AE_CH_UNKNOWN1, AE_CH_UNKNOWN2, AE_CH_UNKNOWN3, AE_CH_UNKNOWN4, AE_CH_UNKNOWN5, AE_CH_UNKNOWN6, AE_CH_UNKNOWN7, AE_CH_UNKNOWN8, /* for p16v devices */
  AE_CH_NULL
};

static unsigned int ALSASampleRateList[] =
{
  5512,
  8000,
  11025,
  16000,
  22050,
  32000,
  44100,
  48000,
  64000,
  88200,
  96000,
  176400,
  192000,
  384000,
  0
};

namespace
{
struct SndConfigDeleter
{
  void operator()(snd_config_t* p) { snd_config_delete(p); }
};

inline std::unique_ptr<snd_config_t, SndConfigDeleter> SndConfigCopy(snd_config_t* original)
{
  snd_config_t* config;
  snd_config_copy(&config, original);
  return std::unique_ptr<snd_config_t, SndConfigDeleter>(config, SndConfigDeleter());
}
} // namespace

CAESinkALSA::CAESinkALSA() :
  m_pcm(NULL)
{
  /* ensure that ALSA has been initialized */
  if (!snd_config)
    snd_config_update();
}

CAESinkALSA::~CAESinkALSA()
{
  Deinitialize();
}

void CAESinkALSA::Register()
{
  AE::AESinkRegEntry entry;
  entry.sinkName = "ALSA";
  entry.createFunc = CAESinkALSA::Create;
  entry.enumerateFunc = CAESinkALSA::EnumerateDevicesEx;
  entry.cleanupFunc = CAESinkALSA::Cleanup;
  AE::CAESinkFactory::RegisterSink(entry);
}

std::unique_ptr<IAESink> CAESinkALSA::Create(std::string& device, AEAudioFormat& desiredFormat)
{
  auto sink = std::make_unique<CAESinkALSA>();
  if (sink->Initialize(desiredFormat, device))
    return sink;

  return {};
}

inline CAEChannelInfo CAESinkALSA::GetChannelLayoutRaw(const AEAudioFormat& format)
{
  unsigned int count = 0;

  switch (format.m_streamInfo.m_type)
  {
    case CAEStreamInfo::STREAM_TYPE_DTSHD_MA:
    case CAEStreamInfo::STREAM_TYPE_TRUEHD:
      count = 8;
      break;
    case CAEStreamInfo::STREAM_TYPE_DTSHD_CORE:
    case CAEStreamInfo::STREAM_TYPE_DTS_512:
    case CAEStreamInfo::STREAM_TYPE_DTS_1024:
    case CAEStreamInfo::STREAM_TYPE_DTS_2048:
    case CAEStreamInfo::STREAM_TYPE_AC3:
    case CAEStreamInfo::STREAM_TYPE_EAC3:
    case CAEStreamInfo::STREAM_TYPE_DTSHD:
      count = 2;
      break;
    default:
      count = 0;
      break;
  }

  CAEChannelInfo info;
  for (unsigned int i = 0; i < count; ++i)
    info += ALSAChannelMapPassthrough[i];

  return info;
}

inline CAEChannelInfo CAESinkALSA::GetChannelLayoutLegacy(const AEAudioFormat& format, unsigned int minChannels, unsigned int maxChannels)
{
  enum AEChannel* channelMap = LegacyALSAChannelMap;
  unsigned int count = 0;

  if (format.m_dataFormat == AE_FMT_RAW)
    return GetChannelLayoutRaw(format);

  // According to CEA-861-D only RL and RR are known. In case of a format having SL and SR channels
  // but no BR BL channels, we use the wide map in order to open only the num of channels really
  // needed.
  if (format.m_channelLayout.HasChannel(AE_CH_SL) && !format.m_channelLayout.HasChannel(AE_CH_BL))
  {
    channelMap = LegacyALSAChannelMap51Wide;
  }
  for (unsigned int c = 0; c < 8; ++c)
  {
    for (unsigned int i = 0; i < format.m_channelLayout.Count(); ++i)
    {
      if (format.m_channelLayout[i] == channelMap[c])
      {
        count = c + 1;
        break;
      }
    }
  }
  count = std::max(count, minChannels);
  count = std::min(count, maxChannels);

  CAEChannelInfo info;
  for (unsigned int i = 0; i < count; ++i)
    info += channelMap[i];

  return info;
}

inline CAEChannelInfo CAESinkALSA::GetChannelLayout(const AEAudioFormat& format, unsigned int channels)
{
  CAEChannelInfo info;
  std::string alsaMapStr("none");

  if (format.m_dataFormat == AE_FMT_RAW)
  {
    info = GetChannelLayoutRaw(format);
  }
  else
  {
    /* ask for the actual map */
    snd_pcm_chmap_t* actualMap = snd_pcm_get_chmap(m_pcm);
    if (actualMap)
    {
      alsaMapStr = ALSAchmapToString(actualMap);

      info = ALSAchmapToAEChannelMap(actualMap);

      /* "fake" a compatible map if it is more suitable for AE */
      if (!info.ContainsChannels(format.m_channelLayout))
      {
        CAEChannelInfo infoAlternate = GetAlternateLayoutForm(info);
        if (infoAlternate.Count())
        {
          std::vector<CAEChannelInfo> alts;
          alts.push_back(info);
          alts.push_back(infoAlternate);
          if (format.m_channelLayout.BestMatch(alts) == 1)
            info = infoAlternate;
        }
      }

      /* add empty channels as needed (with e.g. FL,FR,LFE in 4ch) */
      while (info.Count() < channels)
        info += AE_CH_UNKNOWN1;

      free(actualMap);
    }
    else
    {
      info = GetChannelLayoutLegacy(format, channels, channels);
    }
  }

  CLog::Log(LOGDEBUG,
            "CAESinkALSA::GetChannelLayout - Input Channel Count: {} Output Channel Count: {}",
            format.m_channelLayout.Count(), info.Count());
  CLog::Log(LOGDEBUG, "CAESinkALSA::GetChannelLayout - Requested Layout: {}",
            std::string(format.m_channelLayout));
  CLog::Log(LOGDEBUG, "CAESinkALSA::GetChannelLayout - Got Layout: {} (ALSA: {})",
            std::string(info), alsaMapStr);

  return info;
}

AEChannel CAESinkALSA::ALSAChannelToAEChannel(unsigned int alsaChannel)
{
  AEChannel aeChannel;
  switch (alsaChannel)
  {
    case SND_CHMAP_FL:   aeChannel = AE_CH_FL; break;
    case SND_CHMAP_FR:   aeChannel = AE_CH_FR; break;
    case SND_CHMAP_FC:   aeChannel = AE_CH_FC; break;
    case SND_CHMAP_LFE:  aeChannel = AE_CH_LFE; break;
    case SND_CHMAP_RL:   aeChannel = AE_CH_BL; break;
    case SND_CHMAP_RR:   aeChannel = AE_CH_BR; break;
    case SND_CHMAP_FLC:  aeChannel = AE_CH_FLOC; break;
    case SND_CHMAP_FRC:  aeChannel = AE_CH_FROC; break;
    case SND_CHMAP_RC:   aeChannel = AE_CH_BC; break;
    case SND_CHMAP_SL:   aeChannel = AE_CH_SL; break;
    case SND_CHMAP_SR:   aeChannel = AE_CH_SR; break;
    case SND_CHMAP_TFL:  aeChannel = AE_CH_TFL; break;
    case SND_CHMAP_TFR:  aeChannel = AE_CH_TFR; break;
    case SND_CHMAP_TFC:  aeChannel = AE_CH_TFC; break;
    case SND_CHMAP_TC:   aeChannel = AE_CH_TC; break;
    case SND_CHMAP_TRL:  aeChannel = AE_CH_TBL; break;
    case SND_CHMAP_TRR:  aeChannel = AE_CH_TBR; break;
    case SND_CHMAP_TRC:  aeChannel = AE_CH_TBC; break;
    case SND_CHMAP_RLC:  aeChannel = AE_CH_BLOC; break;
    case SND_CHMAP_RRC:  aeChannel = AE_CH_BROC; break;
    default:             aeChannel = AE_CH_UNKNOWN1; break;
  }
  return aeChannel;
}

unsigned int CAESinkALSA::AEChannelToALSAChannel(AEChannel aeChannel)
{
  unsigned int alsaChannel;
  switch (aeChannel)
  {
    case AE_CH_FL:    alsaChannel = SND_CHMAP_FL; break;
    case AE_CH_FR:    alsaChannel = SND_CHMAP_FR; break;
    case AE_CH_FC:    alsaChannel = SND_CHMAP_FC; break;
    case AE_CH_LFE:   alsaChannel = SND_CHMAP_LFE; break;
    case AE_CH_BL:    alsaChannel = SND_CHMAP_RL; break;
    case AE_CH_BR:    alsaChannel = SND_CHMAP_RR; break;
    case AE_CH_FLOC:  alsaChannel = SND_CHMAP_FLC; break;
    case AE_CH_FROC:  alsaChannel = SND_CHMAP_FRC; break;
    case AE_CH_BC:    alsaChannel = SND_CHMAP_RC; break;
    case AE_CH_SL:    alsaChannel = SND_CHMAP_SL; break;
    case AE_CH_SR:    alsaChannel = SND_CHMAP_SR; break;
    case AE_CH_TFL:   alsaChannel = SND_CHMAP_TFL; break;
    case AE_CH_TFR:   alsaChannel = SND_CHMAP_TFR; break;
    case AE_CH_TFC:   alsaChannel = SND_CHMAP_TFC; break;
    case AE_CH_TC:    alsaChannel = SND_CHMAP_TC; break;
    case AE_CH_TBL:   alsaChannel = SND_CHMAP_TRL; break;
    case AE_CH_TBR:   alsaChannel = SND_CHMAP_TRR; break;
    case AE_CH_TBC:   alsaChannel = SND_CHMAP_TRC; break;
    case AE_CH_BLOC:  alsaChannel = SND_CHMAP_RLC; break;
    case AE_CH_BROC:  alsaChannel = SND_CHMAP_RRC; break;
    default:          alsaChannel = SND_CHMAP_UNKNOWN; break;
  }
  return alsaChannel;
}

CAEChannelInfo CAESinkALSA::ALSAchmapToAEChannelMap(snd_pcm_chmap_t* alsaMap)
{
  CAEChannelInfo info;

  for (unsigned int i = 0; i < alsaMap->channels; i++)
    info += ALSAChannelToAEChannel(alsaMap->pos[i]);

  return info;
}

snd_pcm_chmap_t* CAESinkALSA::AEChannelMapToALSAchmap(const CAEChannelInfo& info)
{
  int AECount = info.Count();
  snd_pcm_chmap_t* alsaMap = (snd_pcm_chmap_t*)malloc(sizeof(snd_pcm_chmap_t) + AECount * sizeof(int));

  alsaMap->channels = AECount;

  for (int i = 0; i < AECount; i++)
    alsaMap->pos[i] = AEChannelToALSAChannel(info[i]);

  return alsaMap;
}

snd_pcm_chmap_t* CAESinkALSA::CopyALSAchmap(snd_pcm_chmap_t* alsaMap)
{
  snd_pcm_chmap_t* copyMap = (snd_pcm_chmap_t*)malloc(sizeof(snd_pcm_chmap_t) + alsaMap->channels * sizeof(int));

  copyMap->channels = alsaMap->channels;
  memcpy(copyMap->pos, alsaMap->pos, alsaMap->channels * sizeof(int));

  return copyMap;
}

std::string CAESinkALSA::ALSAchmapToString(snd_pcm_chmap_t* alsaMap)
{
  char buf[128] = {};
  //! @bug ALSA bug - buffer overflow by a factor of 2 is possible
  //! http://mailman.alsa-project.org/pipermail/alsa-devel/2014-December/085815.html
  int err = snd_pcm_chmap_print(alsaMap, sizeof(buf) / 2, buf);
  if (err < 0)
    return "Error";
  return std::string(buf);
}

CAEChannelInfo CAESinkALSA::GetAlternateLayoutForm(const CAEChannelInfo& info)
{
  CAEChannelInfo altLayout;

  /* only handle symmetrical layouts */
  if (info.HasChannel(AE_CH_BL) == info.HasChannel(AE_CH_BR) &&
      info.HasChannel(AE_CH_SL) == info.HasChannel(AE_CH_SR) &&
      info.HasChannel(AE_CH_BLOC) == info.HasChannel(AE_CH_BROC))
  {
    /* CEA-861-D used by HDMI 1.x has 7.1 as back+back-x-of-center, not
     * side+back. Mangle it here. */
    if (info.HasChannel(AE_CH_SL) && info.HasChannel(AE_CH_BL) && !info.HasChannel(AE_CH_BLOC))
    {
      altLayout = info;
      altLayout.ReplaceChannel(AE_CH_BL, AE_CH_BLOC);
      altLayout.ReplaceChannel(AE_CH_BR, AE_CH_BROC);
      altLayout.ReplaceChannel(AE_CH_SL, AE_CH_BL);
      altLayout.ReplaceChannel(AE_CH_SR, AE_CH_BR);
    }
    /* same in reverse */
    else if (!info.HasChannel(AE_CH_SL) && info.HasChannel(AE_CH_BL) && info.HasChannel(AE_CH_BLOC))
    {
      altLayout = info;
      altLayout.ReplaceChannel(AE_CH_BL, AE_CH_SL);
      altLayout.ReplaceChannel(AE_CH_BR, AE_CH_SR);
      altLayout.ReplaceChannel(AE_CH_BLOC, AE_CH_BL);
      altLayout.ReplaceChannel(AE_CH_BROC, AE_CH_BR);
    }
    /* We have side speakers but no back speakers, allow map to back
     * speakers. */
    else if (info.HasChannel(AE_CH_SL) && !info.HasChannel(AE_CH_BL))
    {
      altLayout = info;
      altLayout.ReplaceChannel(AE_CH_SL, AE_CH_BL);
      altLayout.ReplaceChannel(AE_CH_SR, AE_CH_BR);
    }
    /* reverse */
    else if (!info.HasChannel(AE_CH_SL) && info.HasChannel(AE_CH_BL))
    {
      altLayout = info;
      altLayout.ReplaceChannel(AE_CH_BL, AE_CH_SL);
      altLayout.ReplaceChannel(AE_CH_BR, AE_CH_SR);
    }
  }
  return altLayout;
}

snd_pcm_chmap_t* CAESinkALSA::SelectALSAChannelMap(const CAEChannelInfo& info)
{
  snd_pcm_chmap_t* chmap = NULL;
  snd_pcm_chmap_query_t** supportedMaps;

  supportedMaps = snd_pcm_query_chmaps(m_pcm);

  if (!supportedMaps)
    return NULL;

  CAEChannelInfo infoAlternate = GetAlternateLayoutForm(info);

  /* for efficiency, first try to find an exact match, and only then fallback
   * to searching for less perfect matches */
  int i = 0;
  for (snd_pcm_chmap_query_t* supportedMap = supportedMaps[i++];
       supportedMap; supportedMap = supportedMaps[i++])
  {
    if (supportedMap->map.channels == info.Count())
    {
      CAEChannelInfo candidate = ALSAchmapToAEChannelMap(&supportedMap->map);
      const CAEChannelInfo* selectedInfo = &info;

      if (!candidate.ContainsChannels(info) || !info.ContainsChannels(candidate))
      {
        selectedInfo = &infoAlternate;
        if (!candidate.ContainsChannels(infoAlternate) || !infoAlternate.ContainsChannels(candidate))
          continue;
      }

      if (supportedMap->type == SND_CHMAP_TYPE_VAR)
      {
        /* device supports the AE map directly */
        chmap = AEChannelMapToALSAchmap(*selectedInfo);
        break;
      }
      else
      {
        /* device needs 1:1 remapping */
        chmap = CopyALSAchmap(&supportedMap->map);
        break;
      }
    }
  }

  /* if no exact chmap was found, fallback to best-effort */
  if (!chmap)
  {
    CAEChannelInfo allChannels;
    std::vector<CAEChannelInfo> supportedMapsAE;

    /* Convert the ALSA maps to AE maps. */
    int i = 0;
    for (snd_pcm_chmap_query_t* supportedMap = supportedMaps[i++];
        supportedMap; supportedMap = supportedMaps[i++])
      supportedMapsAE.push_back(ALSAchmapToAEChannelMap(&supportedMap->map));

    int score = 0;
    int best = info.BestMatch(supportedMapsAE, &score);

    /* see if we find a better result with the alternate form */
    if (infoAlternate.Count() && score < 0)
    {
      int scoreAlt = 0;
      int bestAlt = infoAlternate.BestMatch(supportedMapsAE, &scoreAlt);
      if (scoreAlt > score)
        best = bestAlt;
    }

    if (best > 0)
      chmap = CopyALSAchmap(&supportedMaps[best]->map);
  }

  if (chmap && CServiceBroker::GetLogging().CanLogComponent(LOGAUDIO))
    CLog::Log(LOGDEBUG, "CAESinkALSA::SelectALSAChannelMap - Selected ALSA map \"{}\"",
              ALSAchmapToString(chmap));

  snd_pcm_free_chmaps(supportedMaps);
  return chmap;
}

void CAESinkALSA::GetAESParams(const AEAudioFormat& format, std::string& params)
{
  if (m_passthrough)
    params = "AES0=0x06";
  else
    params = "AES0=0x04";

  params += ",AES1=0x82,AES2=0x00";

  if (m_passthrough && format.m_channelLayout.Count() == 8) params += ",AES3=0x09";
  else 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)
{
  m_initDevice = device;
  m_initFormat = format;
  ALSAConfig inconfig, outconfig;
  inconfig.format = format.m_dataFormat;
  inconfig.sampleRate = format.m_sampleRate;

  /*
   * We can't use the better GetChannelLayout() at this point as the device
   * is not opened yet, and we need inconfig.channels to select the correct
   * device... Legacy layouts should be accurate enough for device selection
   * in all cases, though.
   */
  inconfig.channels = GetChannelLayoutLegacy(format, 2, 8).Count();

  /* if we are raw, correct the data format */
  if (format.m_dataFormat == AE_FMT_RAW)
  {
    inconfig.format   = AE_FMT_S16NE;
    m_passthrough     = true;
  }
  else
  {
    m_passthrough   = false;
  }

  if (inconfig.channels == 0)
  {
    CLog::Log(LOGERROR, "CAESinkALSA::Initialize - Unable to open the requested channel layout");
    return false;
  }

  AEDeviceType devType = AEDeviceTypeFromName(device);

  std::string AESParams;
  /* digital interfaces should have AESx set, though in practice most
   * receivers don't care */
  if (m_passthrough || devType == AE_DEVTYPE_HDMI || devType == AE_DEVTYPE_IEC958)
    GetAESParams(format, AESParams);

  CLog::Log(LOGINFO, "CAESinkALSA::Initialize - Attempting to open device \"{}\"", device);

  /* get the sound config */
  std::unique_ptr<snd_config_t, SndConfigDeleter> config = SndConfigCopy(snd_config);

  if (!OpenPCMDevice(device, AESParams, inconfig.channels, &m_pcm, config.get()))
  {
    CLog::Log(LOGERROR, "CAESinkALSA::Initialize - failed to initialize device \"{}\"", device);
    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 \"{}\"", device);

  snd_pcm_chmap_t* selectedChmap = NULL;
  if (!m_passthrough)
  {
    selectedChmap = SelectALSAChannelMap(format.m_channelLayout);
    if (selectedChmap)
    {
      /* update wanted channel count according to the selected map */
      inconfig.channels = selectedChmap->channels;
    }
  }

  if (!InitializeHW(inconfig, outconfig) || !InitializeSW(outconfig))
  {
    free(selectedChmap);
    return false;
  }

  if (selectedChmap)
  {
    /* failure is OK, that likely just means the selected chmap is fixed already */
    snd_pcm_set_chmap(m_pcm, selectedChmap);
    free(selectedChmap);
  }

  // we want it blocking
  snd_pcm_nonblock(m_pcm, 0);
  snd_pcm_prepare (m_pcm);

  if (m_passthrough && inconfig.channels != outconfig.channels)
  {
    CLog::Log(LOGINFO, "CAESinkALSA::Initialize - could not open required number of channels");
    return false;
  }
  // adjust format to the configuration we got
  format.m_channelLayout = GetChannelLayout(format, outconfig.channels);
  // we might end up with an unusable channel layout that contains only UNKNOWN
  // channels, let's do a sanity check.
  if (!format.m_channelLayout.IsLayoutValid())
    return false;

  format.m_sampleRate = outconfig.sampleRate;
  format.m_frames = outconfig.periodSize;
  format.m_frameSize = outconfig.frameSize;
  format.m_dataFormat = outconfig.format;

  m_format              = format;
  m_formatSampleRateMul = 1.0 / (double)m_format.m_sampleRate;

  return true;
}

snd_pcm_format_t CAESinkALSA::AEFormatToALSAFormat(const enum AEDataFormat format)
{
  if (format == AE_FMT_RAW)
    return SND_PCM_FORMAT_S16;

  switch (format)
  {
    case AE_FMT_U8    : return SND_PCM_FORMAT_U8;
    case AE_FMT_S16NE : return SND_PCM_FORMAT_S16;
    case AE_FMT_S16LE : return SND_PCM_FORMAT_S16_LE;
    case AE_FMT_S16BE : return SND_PCM_FORMAT_S16_BE;
    case AE_FMT_S24NE4: return SND_PCM_FORMAT_S24;
#ifdef __BIG_ENDIAN__
    case AE_FMT_S24NE3: return SND_PCM_FORMAT_S24_3BE;
#else
    case AE_FMT_S24NE3: return SND_PCM_FORMAT_S24_3LE;
#endif
    case AE_FMT_S32NE : return SND_PCM_FORMAT_S32;
    case AE_FMT_FLOAT : return SND_PCM_FORMAT_FLOAT;

    default:
      return SND_PCM_FORMAT_UNKNOWN;
  }
}

bool CAESinkALSA::InitializeHW(const ALSAConfig &inconfig, ALSAConfig &outconfig)
{
  snd_pcm_hw_params_t *hw_params;

  snd_pcm_hw_params_alloca(&hw_params);
  memset(hw_params, 0, snd_pcm_hw_params_sizeof());

  snd_pcm_hw_params_any(m_pcm, hw_params);
  snd_pcm_hw_params_set_access(m_pcm, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);

  unsigned int sampleRate   = inconfig.sampleRate;
  snd_pcm_hw_params_set_rate_near    (m_pcm, hw_params, &sampleRate, NULL);

  unsigned int channelCount = inconfig.channels;
  /* select a channel count >=wanted, or otherwise the highest available */
  if (snd_pcm_hw_params_set_channels_min(m_pcm, hw_params, &channelCount) == 0)
    snd_pcm_hw_params_set_channels_first(m_pcm, hw_params, &channelCount);
  else
    snd_pcm_hw_params_set_channels_last(m_pcm, hw_params, &channelCount);

  /* ensure we opened X channels or more */
  if (inconfig.channels > channelCount)
  {
    CLog::Log(LOGINFO, "CAESinkALSA::InitializeHW - Unable to open the required number of channels");
  }

  /* update outconfig */
  outconfig.channels = channelCount;

  snd_pcm_format_t fmt = AEFormatToALSAFormat(inconfig.format);
  outconfig.format = inconfig.format;

  if (fmt == SND_PCM_FORMAT_UNKNOWN)
  {
    /* if we dont support the requested format, fallback to float */
    fmt = SND_PCM_FORMAT_FLOAT;
    outconfig.format = AE_FMT_FLOAT;
  }

  snd_pcm_hw_params_t *hw_params_copy;
  snd_pcm_hw_params_alloca(&hw_params_copy);
  snd_pcm_hw_params_copy(hw_params_copy, hw_params); // copy what we have

  /* try the data format */
  if (snd_pcm_hw_params_set_format(m_pcm, hw_params, fmt) < 0)
  {
    /* if the chosen format is not supported, try each one in descending order */
    CLog::Log(LOGINFO,
              "CAESinkALSA::InitializeHW - Your hardware does not support {}, trying other formats",
              CAEUtil::DataFormatToStr(outconfig.format));
    for (enum AEDataFormat i = AE_FMT_MAX; i > AE_FMT_INVALID; i = (enum AEDataFormat)((int)i - 1))
    {
      if (i == AE_FMT_RAW || i == AE_FMT_MAX)
        continue;

      if (m_passthrough && i != AE_FMT_S16BE && i != AE_FMT_S16LE)
	continue;

      fmt = AEFormatToALSAFormat(i);
      if (fmt == SND_PCM_FORMAT_UNKNOWN)
        continue;

      snd_pcm_hw_params_copy(hw_params, hw_params_copy); // restore from copy
      if (snd_pcm_hw_params_set_format(m_pcm, hw_params, fmt) < 0)
      {
        fmt = SND_PCM_FORMAT_UNKNOWN;
        continue;
      }

      int fmtBits = CAEUtil::DataFormatToBits(i);
      int bits    = snd_pcm_hw_params_get_sbits(hw_params);

      // skip bits check when alsa reports invalid sbits value
      if (bits > 0 && bits != fmtBits)
      {
        /* if we opened in 32bit and only have 24bits, signal it accordingly */
        if (fmt == SND_PCM_FORMAT_S32 && bits == 24)
          i = AE_FMT_S24NE4MSB;
        else if (fmt == SND_PCM_FORMAT_S24 && bits == 24)
          i = AE_FMT_S24NE4;
        else
          continue;
      }

      /* record that the format fell back to X */
      outconfig.format = i;
      CLog::Log(LOGINFO, "CAESinkALSA::InitializeHW - Using data format {}",
                CAEUtil::DataFormatToStr(outconfig.format));
      break;
    }

    /* if we failed to find a valid output format */
    if (fmt == SND_PCM_FORMAT_UNKNOWN)
    {
      CLog::Log(LOGERROR, "CAESinkALSA::InitializeHW - Unable to find a suitable output format");
      return false;
    }
  }

  snd_pcm_uframes_t periodSize, bufferSize;
  snd_pcm_hw_params_get_buffer_size_max(hw_params, &bufferSize);
  snd_pcm_hw_params_get_period_size_max(hw_params, &periodSize, NULL);

  /*
   We want to make sure, that we have max 200 ms Buffer with
   a periodSize of approx 50 ms. Choosing a higher bufferSize
   will cause problems with menu sounds. Buffer will be increased
   after those are fixed.
  */
  periodSize  = std::min(periodSize, (snd_pcm_uframes_t) sampleRate / 20);
  bufferSize  = std::min(bufferSize, (snd_pcm_uframes_t) sampleRate / 5);

  /*
   According to upstream we should set buffer size first - so make sure it is always at least
   4x period size to not get underruns (some systems seem to have issues with only 2 periods)
  */
  periodSize = std::min(periodSize, bufferSize / 4);

  CLog::Log(LOGDEBUG, "CAESinkALSA::InitializeHW - Request: periodSize {}, bufferSize {}",
            periodSize, bufferSize);

  snd_pcm_hw_params_copy(hw_params_copy, hw_params); // copy what we have and is already working

  // Make sure to not initialize too large to not cause underruns
  snd_pcm_uframes_t periodSizeMax = bufferSize / 3;
  if(snd_pcm_hw_params_set_period_size_max(m_pcm, hw_params_copy, &periodSizeMax, NULL) != 0)
  {
    snd_pcm_hw_params_copy(hw_params_copy, hw_params); // restore working copy
    CLog::Log(LOGDEBUG, "CAESinkALSA::InitializeHW - Request: Failed to limit periodSize to {}",
              periodSizeMax);
  }

  // first trying bufferSize, PeriodSize
  // for more info see here:
  // http://mailman.alsa-project.org/pipermail/alsa-devel/2009-September/021069.html
  // the last three tries are done as within pulseaudio

  // backup periodSize and bufferSize first. Restore them after every failed try
  snd_pcm_uframes_t periodSizeTemp, bufferSizeTemp;
  periodSizeTemp = periodSize;
  bufferSizeTemp = bufferSize;
  if (snd_pcm_hw_params_set_buffer_size_near(m_pcm, hw_params_copy, &bufferSize) != 0
    || snd_pcm_hw_params_set_period_size_near(m_pcm, hw_params_copy, &periodSize, NULL) != 0
    || snd_pcm_hw_params(m_pcm, hw_params_copy) != 0)
  {
    bufferSize = bufferSizeTemp;
    periodSize = periodSizeTemp;
    // retry with PeriodSize, bufferSize
    snd_pcm_hw_params_copy(hw_params_copy, hw_params); // restore working copy
    if (snd_pcm_hw_params_set_period_size_near(m_pcm, hw_params_copy, &periodSize, NULL) != 0
      || snd_pcm_hw_params_set_buffer_size_near(m_pcm, hw_params_copy, &bufferSize) != 0
      || snd_pcm_hw_params(m_pcm, hw_params_copy) != 0)
    {
      // try only periodSize
      periodSize = periodSizeTemp;
      snd_pcm_hw_params_copy(hw_params_copy, hw_params); // restore working copy
      if(snd_pcm_hw_params_set_period_size_near(m_pcm, hw_params_copy, &periodSize, NULL) != 0
        || snd_pcm_hw_params(m_pcm, hw_params_copy) != 0)
      {
        // try only BufferSize
        bufferSize = bufferSizeTemp;
        snd_pcm_hw_params_copy(hw_params_copy, hw_params); // restore working copy
        if (snd_pcm_hw_params_set_buffer_size_near(m_pcm, hw_params_copy, &bufferSize) != 0
          || snd_pcm_hw_params(m_pcm, hw_params_copy) != 0)
        {
          // set default that Alsa would choose
          CLog::Log(LOGWARNING, "CAESinkAlsa::InitializeHW - Using default alsa values - set failed");
          if (snd_pcm_hw_params(m_pcm, hw_params) != 0)
          {
            CLog::Log(LOGDEBUG, "CAESinkALSA::InitializeHW - Could not init a valid sink");
            return false;
          }
        }
      }
      // reread values when alsa default was kept
      snd_pcm_get_params(m_pcm, &bufferSize, &periodSize);
    }
  }

  CLog::Log(LOGDEBUG, "CAESinkALSA::InitializeHW - Got: periodSize {}, bufferSize {}", periodSize,
            bufferSize);

  /* set the format parameters */
  outconfig.sampleRate   = sampleRate;

  /* if periodSize is too small Audio Engine might starve */
  m_fragmented = false;
  unsigned int fragments = 1;
  if (periodSize < AE_MIN_PERIODSIZE)
  {
    fragments = std::ceil((double) AE_MIN_PERIODSIZE / periodSize);
    CLog::Log(LOGDEBUG, "Audio Driver reports too low periodSize {} - will use {} fragments",
              (int)periodSize, (int)fragments);
    m_fragmented = true;
  }

  m_originalPeriodSize   = periodSize;
  outconfig.periodSize   = fragments * periodSize;
  outconfig.frameSize    = snd_pcm_frames_to_bytes(m_pcm, 1);

  m_bufferSize = (unsigned int)bufferSize;
  m_timeout    = std::ceil((double)(bufferSize * 1000) / (double)sampleRate);

  CLog::Log(LOGDEBUG, "CAESinkALSA::InitializeHW - Setting timeout to {} ms", m_timeout);

  return true;
}

bool CAESinkALSA::InitializeSW(const ALSAConfig &inconfig)
{
  snd_pcm_sw_params_t *sw_params;
  snd_pcm_uframes_t boundary;

  snd_pcm_sw_params_alloca(&sw_params);
  memset(sw_params, 0, snd_pcm_sw_params_sizeof());

  snd_pcm_sw_params_current              (m_pcm, sw_params);
  snd_pcm_sw_params_set_start_threshold  (m_pcm, sw_params, INT_MAX);
  snd_pcm_sw_params_set_silence_threshold(m_pcm, sw_params, 0);
  snd_pcm_sw_params_get_boundary         (sw_params, &boundary);
  snd_pcm_sw_params_set_silence_size     (m_pcm, sw_params, boundary);
  snd_pcm_sw_params_set_avail_min        (m_pcm, sw_params, inconfig.periodSize);

  if (snd_pcm_sw_params(m_pcm, sw_params) < 0)
  {
    CLog::Log(LOGERROR, "CAESinkALSA::InitializeSW - Failed to set the parameters");
    return false;
  }

  return true;
}

void CAESinkALSA::Deinitialize()
{
  if (m_pcm)
  {
    Stop();
    snd_pcm_close(m_pcm);
    m_pcm = NULL;
  }
}

void CAESinkALSA::Stop()
{
  if (!m_pcm)
    return;
  snd_pcm_drop(m_pcm);
}

void CAESinkALSA::GetDelay(AEDelayStatus& status)
{
  if (!m_pcm)
  {
    status.SetDelay(0);
    return;
  }
  snd_pcm_sframes_t frames = 0;
  snd_pcm_delay(m_pcm, &frames);

  if (frames < 0)
  {
    snd_pcm_forward(m_pcm, -frames);
    frames = 0;
  }

  status.SetDelay((double)frames * m_formatSampleRateMul);
}

double CAESinkALSA::GetCacheTotal()
{
  return (double)m_bufferSize * m_formatSampleRateMul;
}

unsigned int CAESinkALSA::AddPackets(uint8_t **data, unsigned int frames, unsigned int offset)
{
  if (!m_pcm)
  {
    CLog::Log(LOGERROR, "CAESinkALSA - Tried to add packets without a sink");
    return INT_MAX;
  }

  void *buffer = data[0]+offset*m_format.m_frameSize;
  unsigned int amount = 0;
  int64_t data_left = (int64_t) frames;
  int frames_written = 0;

  while (data_left > 0)
  {
    if (m_fragmented)
      amount = std::min((unsigned int) data_left, m_originalPeriodSize);
    else // take care as we can come here a second time if the sink does not eat all data
      amount = (unsigned int) data_left;

    int ret = snd_pcm_writei(m_pcm, buffer, amount);
    if (ret < 0)
    {
      CLog::Log(LOGERROR, "CAESinkALSA - snd_pcm_writei({}) {} - trying to recover", ret,
                snd_strerror(ret));
      ret = snd_pcm_recover(m_pcm, ret, 1);
      if(ret < 0)
      {
        HandleError("snd_pcm_writei(1)", ret);
        ret = snd_pcm_writei(m_pcm, buffer, amount);
        if (ret < 0)
        {
          HandleError("snd_pcm_writei(2)", ret);
          ret = 0;
        }
      }
    }

    if ( ret > 0 && snd_pcm_state(m_pcm) == SND_PCM_STATE_PREPARED)
      snd_pcm_start(m_pcm);

    if (ret <= 0)
      break;

    frames_written += ret;
    data_left -= ret;
    buffer = data[0]+offset*m_format.m_frameSize + frames_written*m_format.m_frameSize;
  }
  return frames_written;
}

void CAESinkALSA::HandleError(const char* name, int err)
{
  switch(err)
  {
    case -EPIPE:
      CLog::Log(LOGERROR, "CAESinkALSA::HandleError({}) - underrun", name);
      if ((err = snd_pcm_prepare(m_pcm)) < 0)
        CLog::Log(LOGERROR, "CAESinkALSA::HandleError({}) - snd_pcm_prepare returned {} ({})", name,
                  err, snd_strerror(err));
      break;

    case -ESTRPIPE:
      CLog::Log(LOGINFO, "CAESinkALSA::HandleError({}) - Resuming after suspend", name);

      /* try to resume the stream */
      while((err = snd_pcm_resume(m_pcm)) == -EAGAIN)
        KODI::TIME::Sleep(1ms);

      /* if the hardware doesn't support resume, prepare the stream */
      if (err == -ENOSYS)
        if ((err = snd_pcm_prepare(m_pcm)) < 0)
          CLog::Log(LOGERROR, "CAESinkALSA::HandleError({}) - snd_pcm_prepare returned {} ({})",
                    name, err, snd_strerror(err));
      break;

    default:
      CLog::Log(LOGERROR, "CAESinkALSA::HandleError({}) - snd_pcm_writei returned {} ({})", name,
                err, snd_strerror(err));
      break;
  }
}

void CAESinkALSA::Drain()
{
  if (!m_pcm)
    return;

  snd_pcm_drain(m_pcm);
  snd_pcm_prepare(m_pcm);
}

void CAESinkALSA::AppendParams(std::string &device, const std::string &params)
{
  /* Note: escaping, e.g. "plug:'something:X=y'" isn't handled,
   * but it is not normally encountered at this point. */

  device += (device.find(':') == std::string::npos) ? ':' : ',';
  device += params;
}

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_close(*pcmp);
    *pcmp = NULL;
  }

  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 \"{}\" for playback", name);
  }

  return err == 0;
}

bool CAESinkALSA::TryDeviceWithParams(const std::string &name, const std::string &params, snd_pcm_t **pcmp, snd_config_t *lconf)
{
  if (!params.empty())
  {
    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);
}

bool CAESinkALSA::OpenPCMDevice(const std::string &name, const std::string &params, int channels, snd_pcm_t **pcmp, snd_config_t *lconf)
{
 /* Special name denoting surroundXX mangling. This is needed for some
   * devices for multichannel to work. */
  if (name == "@" || name.substr(0, 2) == "@:")
  {
    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)
    {
      case 3:
      case 4:
        if (TryDeviceWithParams("surround40" + openName, params, pcmp, lconf))
          return true;
        [[fallthrough]];
      case 5:
      case 6:
        if (TryDeviceWithParams("surround51" + openName, params, pcmp, lconf))
          return true;
        [[fallthrough]];
      case 7:
      case 8:
        if (TryDeviceWithParams("surround71" + openName, params, pcmp, lconf))
          return true;
    }

    /* Try "sysdefault" and "default" (they provide dmix if needed, and route
     * audio to all extra channels on subdeviced cards),
     * 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'))
    {
      /* "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;
    }

    /* Try "front" (no dmix, no audio in other channels on subdeviced cards) */
    if (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, bool force)
{
#if defined(HAVE_LIBUDEV)
  const auto deviceMonitor = CServiceBroker::GetPlatform().GetService<CALSADeviceMonitor>();
  deviceMonitor->Start();
#endif

  /* ensure that ALSA has been initialized */
  snd_lib_error_set_handler(sndLibErrorHandler);
  if(!snd_config || force)
  {
    if(force)
      snd_config_update_free_global();

    snd_config_update();
  }

  std::unique_ptr<snd_config_t, SndConfigDeleter> config = SndConfigCopy(snd_config);

#if !defined(HAVE_X11)
  const auto controlMonitor = CServiceBroker::GetPlatform().GetService<CALSAHControlMonitor>();
  controlMonitor->Clear();
#endif

  /* 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.get());

  void **hints;

  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)
    {
      std::string baseName = std::string(name);
      baseName = baseName.substr(0, baseName.find(':'));

      if (strcmp(name, "default") == 0)
      {
        /* 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.get());
      }

      /* Do not enumerate "default", it is already enumerated above. */

      /* Do not enumerate the surroundXX devices, those are always accompanied
       * with a "front" device and it is handled above as "@". The below
       * devices plus sysdefault will be automatically used if available
       * for a "@" device.
       * sysdefault devices are enumerated as not all cards have front/surround
       * devices. For cards with front/surround devices the sysdefault
       * entry will be removed in a second pass after enumeration.
       */

      /* Ubuntu has patched their alsa-lib so that "defaults.namehint.extended"
       * defaults to "on" instead of upstream "off", causing lots of unwanted
       * extra devices (many of which are not actually routed properly) to be
       * found by the enumeration process. Skip them as well ("hw", "dmix",
       * "plughw", "dsnoop"). */

      else if (baseName != "default"
            && baseName != "surround40"
            && baseName != "surround41"
            && baseName != "surround50"
            && baseName != "surround51"
            && baseName != "surround71"
            && baseName != "hw"
            && baseName != "dmix"
            && baseName != "plughw"
            && baseName != "dsnoop")
      {
        EnumerateDevice(list, name, desc ? desc : name, config.get());
      }
    }
    free(io);
    free(name);
    free(desc);
  }
  snd_device_name_free_hint(hints);

#if !defined(HAVE_X11)
  controlMonitor->Start();
#endif

  /* 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";
  }

  /* cards with surround entries where sysdefault should be removed */
  std::set<std::string> cardsWithSurround;

  for (AEDeviceInfoList::iterator it1 = list.begin(); it1 != list.end(); ++it1)
  {
    std::string baseName = it1->m_deviceName.substr(0, it1->m_deviceName.find(':'));
    std::string card = GetParamFromName(it1->m_deviceName, "CARD");
    if (baseName == "@" && !card.empty())
      cardsWithSurround.insert(card);
  }

  if (!cardsWithSurround.empty())
  {
    /* remove sysdefault entries where we already have a surround entry */
    AEDeviceInfoList::iterator iter = list.begin();
    while (iter != list.end())
    {
      std::string baseName = iter->m_deviceName.substr(0, iter->m_deviceName.find(':'));
      std::string card = GetParamFromName(iter->m_deviceName, "CARD");
      if (baseName == "sysdefault" && cardsWithSurround.find(card) != cardsWithSurround.end())
        iter = list.erase(iter);
      else
        ++iter;
    }
  }

  /* 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. */

  /* clashing card names, e.g. "NVidia", "NVidia_2" */
  std::set<std::string> cardsToAppend;

  /* clashing basename + cardname combinations, e.g. ("hdmi","Nvidia") */
  std::set<std::pair<std::string, std::string> > devsToAppend;

  for (AEDeviceInfoList::iterator it1 = list.begin(); it1 != list.end(); ++it1)
  {
    bool replaceName = false;

    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 = GetParamFromName(it1->m_deviceName, "CARD");
        std::string cardString2 = GetParamFromName(it2->m_deviceName, "CARD");

        if (cardString1 != cardString2)
        {
          /* card name differs, add identifiers to all devices */
          cardsToAppend.insert(cardString1);
          cardsToAppend.insert(cardString2);
          continue;
        }

        std::string devString1 = GetParamFromName(it1->m_deviceName, "DEV");
        std::string devString2 = GetParamFromName(it2->m_deviceName, "DEV");

        if (devString1 != devString2)
        {
          /* 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 append the whole device string */
        replaceName = true;
        it2->m_displayName += " (" + it2->m_deviceName + ")";
      }
    }

    if (replaceName)
      it1->m_displayName = it1->m_displayName + " (" + it1->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");
      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");
      if (baseName == it->first && cardString == it->second)
      {
        std::string devString = GetParamFromName(itl->m_deviceName, "DEV");
        /* "HDMI #0", "HDMI #1" ... */
        itl->m_displayNameExtra += " #" + devString;
      }
    }
  }
}

AEDeviceType CAESinkALSA::AEDeviceTypeFromName(const std::string &name)
{
  if (name.substr(0, 4) == "hdmi")
    return AE_DEVTYPE_HDMI;
  else if (name.substr(0, 6) == "iec958" || name.substr(0, 5) == "spdif")
    return AE_DEVTYPE_IEC958;

  return AE_DEVTYPE_PCM;
}

std::string CAESinkALSA::GetParamFromName(const std::string &name, const std::string &param)
{
  /* name = "hdmi:CARD=x,DEV=y" param = "CARD" => return "x" */
  size_t parPos = name.find(param + '=');
  if (parPos != std::string::npos)
  {
    parPos += param.size() + 1;
    return name.substr(parPos, name.find_first_of(",'\"", parPos)-parPos);
  }

  return "";
}

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))
    return;

  snd_pcm_info_t *pcminfo;
  snd_pcm_info_alloca(&pcminfo);
  memset(pcminfo, 0, snd_pcm_info_sizeof());

  int err = snd_pcm_info(pcmhandle, pcminfo);
  if (err < 0)
  {
    CLog::Log(LOGINFO, "CAESinkALSA - Unable to get pcm_info for \"{}\"", device);
    snd_pcm_close(pcmhandle);
  }

  int cardNr = snd_pcm_info_get_card(pcminfo);

  CAEDeviceInfo info;
  info.m_deviceName = device;
  info.m_deviceType = AEDeviceTypeFromName(device);

  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;
      free(cardName);
    }

    if (info.m_deviceType == AE_DEVTYPE_HDMI && 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);

    /*
     * Filter "USB Audio", in those cases snd_card_get_name() is more
     * meaningful already
     */
    if (pcminfoName != "USB Audio")
      info.m_displayNameExtra = pcminfoName;

    if (info.m_deviceType == AE_DEVTYPE_HDMI)
    {
      /* replace, this was likely "HDMI 0" */
      info.m_displayNameExtra = "HDMI";

      int dev = snd_pcm_info_get_device(pcminfo);

      if (dev >= 0)
      {
        /* lets see if we can get ELD info */

        snd_ctl_t *ctlhandle;
        std::stringstream sstr;
        sstr << "hw:" << cardNr;
        std::string strHwName = sstr.str();

        if (snd_ctl_open_lconf(&ctlhandle, strHwName.c_str(), 0, config) == 0)
        {
          snd_hctl_t *hctl;
          if (snd_hctl_open_ctl(&hctl, ctlhandle) == 0)
          {
            snd_hctl_load(hctl);
            bool badHDMI = false;

#if !defined(HAVE_X11)
            /* add ELD to monitoring */
            const auto controlMonitor =
                CServiceBroker::GetPlatform().GetService<CALSAHControlMonitor>();
            controlMonitor->Add(strHwName, SND_CTL_ELEM_IFACE_PCM, dev, "ELD");
#endif

            if (!GetELD(hctl, dev, info, badHDMI))
              CLog::Log(LOGDEBUG,
                        "CAESinkALSA - Unable to obtain ELD information for device \"{}\" (not "
                        "supported by device, or kernel older than 3.2)",
                        device);

            /* snd_hctl_close also closes ctlhandle */
            snd_hctl_close(hctl);

            if (badHDMI)
            {
              /*
               * Warn about disconnected devices, but keep them enabled
               * Detection can go wrong on Intel, Nvidia and on all
               * AMD (fglrx) hardware, so it is not safe to close those
               * handles
               */
              CLog::Log(LOGDEBUG,
                        "CAESinkALSA - HDMI device \"{}\" may be unconnected (no ELD data)",
                        device);
            }
          }
          else
          {
            snd_ctl_close(ctlhandle);
          }
        }
      }
    }
    else if (info.m_deviceType == AE_DEVTYPE_IEC958)
    {
      /* append instead of replace, pcminfoName is useful for S/PDIF */
      if (!info.m_displayNameExtra.empty())
        info.m_displayNameExtra += ' ';
      info.m_displayNameExtra += "S/PDIF";

      info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_AC3);
      info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_CORE);
      info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_1024);
      info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_2048);
      info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_512);
      info.m_dataFormats.push_back(AE_FMT_RAW);
    }
    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";
    }

    /* "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 = "";
    }

  }
  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;
  }

  snd_pcm_hw_params_t *hwparams;
  snd_pcm_hw_params_alloca(&hwparams);
  memset(hwparams, 0, snd_pcm_hw_params_sizeof());

  /* 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 \"{}\"",
              device);
    snd_pcm_close(pcmhandle);
    return;
  }

  /* 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);

  /* 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);

    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;
  }

  CAEChannelInfo alsaChannels;
  snd_pcm_chmap_query_t** alsaMaps = snd_pcm_query_chmaps(pcmhandle);
  bool useEldChannels = (info.m_channels.Count() > 0);
  if (alsaMaps)
  {
    int i = 0;
    for (snd_pcm_chmap_query_t* alsaMap = alsaMaps[i++];
        alsaMap; alsaMap = alsaMaps[i++])
    {
      CAEChannelInfo AEmap = ALSAchmapToAEChannelMap(&alsaMap->map);
      alsaChannels.AddMissingChannels(AEmap);
      if (!useEldChannels)
        info.m_channels.AddMissingChannels(AEmap);
    }
    snd_pcm_free_chmaps(alsaMaps);
  }
  else
  {
    for (int i = 0; i < channels; ++i)
    {
      if (!info.m_channels.HasChannel(LegacyALSAChannelMap[i]))
        info.m_channels += LegacyALSAChannelMap[i];
      alsaChannels += LegacyALSAChannelMap[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 (i == AE_FMT_RAW || 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);
  }

  if (info.m_deviceType == AE_DEVTYPE_HDMI)
  {
    // we don't trust ELD information and push back our supported formats explicitly
    info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_AC3);
    info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD);
    info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_MA);
    info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_CORE);
    info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_1024);
    info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_2048);
    info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_512);
    info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_EAC3);
    info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_TRUEHD);

    // indicate that we can do AE_FMT_RAW
    info.m_dataFormats.push_back(AE_FMT_RAW);
  }

  snd_pcm_close(pcmhandle);
  info.m_wantsIECPassthrough = true;
  list.push_back(info);
}

bool CAESinkALSA::GetELD(snd_hctl_t *hctl, int device, CAEDeviceInfo& info, bool& badHDMI)
{
  badHDMI = false;

  snd_ctl_elem_id_t    *id;
  snd_ctl_elem_info_t  *einfo;
  snd_ctl_elem_value_t *control;
  snd_hctl_elem_t      *elem;

  snd_ctl_elem_id_alloca(&id);
  memset(id, 0, snd_ctl_elem_id_sizeof());

  snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_PCM);
  snd_ctl_elem_id_set_name     (id, "ELD" );
  snd_ctl_elem_id_set_device   (id, device);
  elem = snd_hctl_find_elem(hctl, id);
  if (!elem)
    return false;

  snd_ctl_elem_info_alloca(&einfo);
  memset(einfo, 0, snd_ctl_elem_info_sizeof());

  if (snd_hctl_elem_info(elem, einfo) < 0)
    return false;

  if (!snd_ctl_elem_info_is_readable(einfo))
    return false;

  if (snd_ctl_elem_info_get_type(einfo) != SND_CTL_ELEM_TYPE_BYTES)
    return false;

  snd_ctl_elem_value_alloca(&control);
  memset(control, 0, snd_ctl_elem_value_sizeof());

  if (snd_hctl_elem_read(elem, control) < 0)
    return false;

  int dataLength = snd_ctl_elem_info_get_count(einfo);
  /* if there is no ELD data, then its a bad HDMI device, either nothing attached OR an invalid nVidia HDMI device
   * OR the driver doesn't properly support ELD (notably ATI/AMD, 2012-05) */
  if (!dataLength)
    badHDMI = true;
  else
    CAEELDParser::Parse(
      (const uint8_t*)snd_ctl_elem_value_get_bytes(control),
      dataLength,
      info
    );

  info.m_deviceType = AE_DEVTYPE_HDMI;
  return true;
}

void CAESinkALSA::sndLibErrorHandler(const char *file, int line, const char *function, int err, const char *fmt, ...)
{
  if (!CServiceBroker::GetLogging().CanLogComponent(LOGAUDIO))
    return;

  va_list arg;
  va_start(arg, fmt);
  char *errorStr;
  if (vasprintf(&errorStr, fmt, arg) >= 0)
  {
    CLog::Log(LOGINFO, "CAESinkALSA - ALSA: {}:{}:({}) {}{}{}", file, line, function, errorStr,
              err ? ": " : "", err ? snd_strerror(err) : "");
    free(errorStr);
  }
  va_end(arg);
}

void CAESinkALSA::Cleanup()
{
#if HAVE_LIBUDEV
  const auto deviceMonitor = CServiceBroker::GetPlatform().GetService<CALSADeviceMonitor>();
  deviceMonitor->Stop();
#endif

#if !defined(HAVE_X11)
  const auto controlMonitor = CServiceBroker::GetPlatform().GetService<CALSAHControlMonitor>();
  controlMonitor->Clear();
#endif
}
