[pulseaudio-discuss] [PATCH]: High-definition audio pass-through support

Kelly Anderson kelly at silka.with-linux.com
Mon Nov 21 16:00:53 PST 2011


On 11/21/2011 03:23 PM, Colin Guthrie wrote:
> 'Twas brillig, and Kelly Anderson at 21/11/11 11:08 did gyre and gimble:
>> On 11/21/2011 03:35 AM, David Henningsson wrote:
>>> On 11/17/2011 10:14 PM, Kelly Anderson wrote:
>>>> Hi,
>>>>
>>>> I finally got around to working out the kinks to pass-through
>>>> high-definition audio
>>>> in Xbmc. Funny how long it took me to get back to it. I was
>>>> re-reading some
>>>> emails on the list from way back in March and one of the emails turned
>>>> me on to
>>>> a proper solution. One thing that I wasn't able to do was to get
>>>> PulseAudio
>>>> to pass-through 8 channel audio when the device's sink was set as
>>>> hdmi-stereo.
>>>>
>>>> So I patched PulseAudio 1.1 for hdmi-surround-71 and lo and behold it
>>>> worked.
>>>> Here's a patch, hopefully we can get this added in the next official
>>>> release
>>>> of PulseAudio. I know that there are quite a few Xbmc users that have
>>>> been anxiously
>>>> waiting for high-definition audio pass-through to work correctly. Of
>>>> course if someone
>>>> has a better solution, that would be fine too.
>>>>
>>>> --- ./src/modules/alsa/mixer/profile-sets/default.conf.orig 2011-10-20
>>>> 06:54:16.000000000 -0600
>>>> +++ ./src/modules/alsa/mixer/profile-sets/default.conf 2011-11-17
>>>> 02:16:00.038900536 -0700
>>>> @@ -173,6 +173,12 @@ channel-map = left,right
>>>> priority = 4
>>>> direction = output
>>>>
>>>> +[Mapping hdmi-surround-71]
>>>> +device-strings = hdmi:%f
>>>> +channel-map =
>>>> front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
>>>>
>>> I've been experimenting with enabling such stuff in Ubuntu, but never
>>> got any positive or negative feedback about it. FYI, I'm attaching
>>> what we're shipping for Nvidia and Intel (as part of the jack
>>> detection stuff) in Ubuntu 11.10.
>>>
>>> And Colin, you're right: this isn't passthrough, this is multichannel
>>> PCM.
>>>
>> It's not specifically pass-through, but high-def (i.e, DTS-HD)
>> pass-through requires an 8 channel mapping.  I've got Xbmc/PulseAudio
>> pass-through working well.  It passes low bit rate data through as 2
>> channel and high bit rate data as 8 channel.  Specifically what I have
>> to do in the Xbmc/PulseAudio code is switch the sink to 8 channels when
>> requiring high bit rates and switch back to 2 channels for standard bit
>> rates.  The last couple of commits pertain to the 2/8 channel switching.
>>
>> git://github.com/cbxbiker61/xbmc.git
>
> I'm still confused at this terminology. 8 channel playback and
> pass-through are very different things.

pass-through requires you to open a pipe at a particular bandwidth that 
matches
the data that you are passing through.  DTS-core and Dolby digital will fit
in a 2-channel at 48K pipe.  DTS-HD variations do not fit in the 
2-channel/48K pipe
and therefore I have to open up a bigger pipe, so my code opens an
8-channel at 192K pipe.

As far as channel mapping goes, pass-through doesn't care.  The "real"
channel mapping is all handled by the receiver.


>
> Is this 8 channel HDMI setup any different to an 8 channel analog one?
> If so using the term passthrough is very wrong.
>
> If my receiver supports DTS-HD natively, then enabling pass-through mode
> is totally possible. A digital profile (even in 2ch mode) will happily
> stitch to taking 8ch DTS encoded data. I'm not specifically sure about
> DTS-HD encoding and whether it can be packaged up in packed format we
> accept for passthrough data, but I'm sure Arun or Pierre can comment
> more about the technical bits.
Pass-through is all about the "bandwidth" requirements.  Opening the device
in 8 channels is also a "trigger" for hbr mode, as per the email I 
linked from
March.

Initially I thought it would be nice if PulseAudio would take care of all of
the details, meaning that all I would have to do was trigger pass-through
mode on and start throwing raw DTS(-HD) packets at it.  PulseAudio
would then zero fill and do the conversions to get the data to the receiver.
The problem with that would be, that I'd have to do it one way for Pulse
and another for Alsa.

As it is now.  My code works equally well with Pulse or Alsa, since I'm 
doing
all of the packet conversions.

>
>
> One thing I'll say for sure is that we really do not want applications
> to call the likes of pa_context_set_card_profile_by_name(). It's a very
> bad idea for the application to second guess the routing policy and poke
> too hard at the underlying system. xbmc might be a bit of a special case
> here, but IMO it's still not appropriate. You either have a 8ch setup
> that you use all the time (which should still be able to jump into pass
> through mode if needed) or you don't. It shouldn't switch on-demand.
Switching 2/8 channel modes is a requirement for pass-through to work
with all of the different formats that come into play.

Yes, I realized when I wrote it, that it didn't line up with 
PulseAudio's conceptual model.
That's the rub....PulseAudio doesn't line up properly with the 
conceptual model
necessary to handle the variations of data that need to be passed to the 
reciever
in pass-through mode.

PulseAudio is based on a model that lines up well with analog 
configurations.
I.e.  I have a 5.1 speaker setup, so that's what I want to configure the 
sink
to accept.

When  in pass-through mode, Pulse shouldn't care a bit about channel
mapping because the receiver becomes the ultimate arbiter of what goes
where.  In my receiver's setup, I tell it the physical layout of my 
speakers.
So in my case I have a 5.1 system and my receiver knows that.  If I 
pass-through
a DTS-HD stream that has 7.1 or 5.1 sound, the receiver does the appropriate
thing in both cases.



>
> We may want to write a system that can route things and change profiles
> at the PA end when appropriate (i.e. switch to a 2ch profile when
> playing 2ch streams and switch to 8ch profile when playing 8ch stream)
> but this routing decision is something that should definitely happen at
> the PA end, not in individual apps.
I agree whole-heartedly.  But of course someone has to crack some eggs
to make an omelette.  It'll be easy change my code when Pulse can handle
this more effectively.

>
> I'm happy to discuss the rationale with you on IRC, but it's partially
> covered by my recent comments about routing and priority lists, but
> really goes beyond that. I appreciate your commits will work with your
> setup but it's really not a universally portable solution that could be
> upstreamed IMO.
Pretty much the only thing I was looking for was the 7.1 mapping, within the
confines of PulseAudio's current implementation it works.

>
> That said, I'm very keen for you to keep hacking on XBMC's PA support
> seeing as I use it a lot myself and certainly want it to have a first
> class implementation :D
>
> Col
>
>
>
>
>
>
>

Here's a little python script that parses the hdmi support available
in a particular configuration.  It may be useful to look at it as 
pseudo-code
for maybe something that could be rolled into PulseAudio.  I really
didn't want to write it in C, so I didn't take it that far.  Python
is so appropriate form something like this.

GetHdmiSupport.py:

#! env python

import getopt, glob, os, re, sys

class Card:
     def __init__(self, id):
         self.id = id
         self.codecs = {}

     def getCodecs(self):
         return self.codecs

class Codec:

     def __init__(self, id):
         self.id = id
         self.sads = {}

     def getSads(self):
         return self.sads

     def supportsDts(self):
         return self.supportsCodingType("[0x7]")

     def getDtsRate(self):
         return 48000

     def getDtsChannels(self):
         return 8

     def supportsDtsHd(self):
         return self.supportsCodingType("[0xb]")

     def getDtsHdRate(self):
         return 192000

     def getDtsHdChannels(self):
         return 8

     def supportsAc3(self):
         return self.supportsCodingType("[0x2]")

     def getAc3Rate(self):
         return 48000

     def getAc3Channels(self):
         return 8

     def supportsEAc3(self):
         return self.supportsCodingType("[0xa]")

     def getEAc3Rate(self): # DD+, Dolby Digital Plus
         return 48000

     def getEAc3Channels(self):
         return 8

     def supportsMlp(self): # Dolby TrueHD
         return self.supportsCodingType("[0xc]")

     def getMlpRate(self):
         return 192000

     def getMlpChannels(self):
         return 8

     def supportsCodingType(self, codingType):
         for sadId, sad in self.sads.iteritems():
             if codingType in sad.codingType:
                 return True

         return False

class Sad:
     def __init__(self, id = None, codingType = None, channels = None, 
rates = None, bits = None, maxBitrate = None):
         self.id = id
         self.codingType = codingType
         self.channels = channels
         self.rates = rates
         self.bits = bits
         self.maxBitrate = maxBitrate

class Hdmi:
     _reHdmiCodecs = 
re.compile("/proc/asound/card(?P<CardId>\d+)/codec#(?P<CodecId>\d+)")
     _reMonitorPresent = re.compile("^monitor_present\s*1")
     _reSadCount = re.compile("^sad_count\s*(?P<SadCount>\d+)")

     def __init__(self):
         pass

     def getCards(self):
         cards = dict()

         for cardId, codecId, codecInfo in self.getEldInfo():
             if not cardId in cards.keys():
                 cards[cardId] = Card(cardId)

             card = cards[cardId]

             if not codecId in cards[cardId].codecs.keys():
                 cards[cardId].codecs[codecId] = Codec(codecId)

             sadCount = 0

             for l in codecInfo.split('\n'):
                 m = self._reSadCount.match(l)

                 if m:
                     sadCount = int(m.group("SadCount"))
                     break

             if sadCount:
                 for i in range(0, sadCount):
                     sad = self.parseSad(i, codecInfo)
                     if sad:
                         cards[cardId].codecs[codecId].sads[i] = sad

         return cards

     def parseSad(self, i, codecInfo):
         sad = None
         reSad = re.compile("^sad%d_" % i)

         for l in codecInfo.split('\n'):
             if reSad.match(l):
                 if not sad:
                     sad = Sad(i)
                 v = self.parseSadKey(i, "coding_type", l)
                 if v:
                     sad.codingType = v
                 v = self.parseSadKey(i, "channels", l)
                 if v:
                     sad.channels = v
                 v = self.parseSadKey(i, "rates", l)
                 if v:
                     sad.rates = v
                 v = self.parseSadKey(i, "bits", l)
                 if v:
                     sad.bits = v
                 v = self.parseSadKey(i, "max_bitrate", l)
                 if v:
                     sad.MaxBitrate = v

         return sad

     def parseSadKey(self, i, key, line):
         s = "^sad%d_%s\s*(?P<Value>.*)" % (i, key)
         r = re.compile(s)
         m = r.match(line)

         if m:
             return m.group("Value")

         return None


     def getEldInfo(self):
         eldInfo = []

         for cardId, codecId in self.getHdmiCodecs():
             codecInfo = open("/proc/asound/card%d/eld#%d.0" % (cardId, 
codecId), "r").read()

             for l in codecInfo.split('\n'):
                 if self._reMonitorPresent.match(l):
                     eldInfo.append((cardId, codecId, codecInfo))

         return eldInfo

     def getHdmiCodecs(self):
         hdmiCodecs = []

         for f in glob.glob("/proc/asound/card*/codec#*"):
             m = self._reHdmiCodecs.match(f)

             if m:
                 cardId = int(m.group("CardId"))
                 codecId = int(m.group("CodecId"))

                 for l in open(f, "r").readlines():
                     if "HDMI" in l:
                         hdmiCodecs.append((cardId, codecId))
                         break

         return hdmiCodecs

def usage() :
     path, name = os.path.split(sys.argv[0])
     print ("%s [--dts] [--dts-hd] [--ac3] [--eac3] [--mlp]" % name)

def main(argv) :
     if len(argv) == 1 or argv[1] == "--help" :
         usage()
         sys.exit(0)

     try :
         opts, args = getopt.getopt(argv[1:], "",["dts", "dts-hd", 
"ac3", "eac3", "mlp", "help"])
     except getopt.GetoptError as e :
         print (e)
         usage ()
         sys.exit(2)

     wantDts = False
     wantDtsHd = False
     wantAc3 = False
     wantEAc3 = False
     wantMlp = False

     for o, a in opts:
         if o in ("--dts"):
             wantDts = True
         elif o in ("--dts-hd"):
             wantDtsHd = True
         elif o in ("--ac3"):
             wantAc3 = True
         elif o in ("--eac3"):
             wantEAc3 = True
         elif o in ("--mlp"):
             wantMlp = True

     rc = 0
     h = Hdmi()

     for cardId, card in h.getCards().iteritems():
         for codecId, codec in card.getCodecs().iteritems():
             if wantDts:
                 if codec.supportsDts():
                     print("DTS: %d %d %d %d" % (cardId, codecId, 
codec.getDtsChannels(), codec.getDtsRate()))
                 else:
                     rc = -1
             if wantDtsHd:
                 if codec.supportsDtsHd():
                     print("DTS-HD: %d %d %d %d" % (cardId, codecId, 
codec.getDtsHdChannels(), codec.getDtsHdRate()))
                 else:
                     rc = -1
             if wantAc3:
                 if codec.supportsAc3():
                     print("AC3: %d %d %d %d" % (cardId, codecId, 
codec.getAc3Channels(), codec.getAc3Rate()))
                 else:
                     rc = -1
             if wantEAc3:
                 if codec.supportsEAc3():
                     print("EAC3: %d %d %d %d" % (cardId, codecId, 
codec.getEAc3Channels(), codec.getEAc3Rate()))
                 else:
                     rc = -1
             if wantMlp:
                 if codec.supportsMlp():
                     print("MLP: %d %d %d %d" % (cardId, codecId, 
codec.getMlpChannels(), codec.getMlpRate()))
                 else:
                     rc = -1
     sys.exit(rc)

if __name__ == "__main__":
     main(sys.argv)




More information about the pulseaudio-discuss mailing list