diff mbox series

[v4,21/24] paaudio: channel-map option

Message ID 8f650662fd6cc50baaede260581aeb560eed44fb.1568927990.git.DirtY.iCE.hu@gmail.com
State New
Headers show
Series Audio: Mixeng-free 5.1/7.1 audio support | expand

Commit Message

=?UTF-8?B?Wm9sdMOhbiBLxZF2w6Fnw7M=?= Sept. 19, 2019, 9:24 p.m. UTC
Add an option to change the channel map used by pulseaudio.  If not
specified, falls back to an OSS compatible channel map.

Signed-off-by: Kővágó, Zoltán <DirtY.iCE.hu@gmail.com>
---
 audio/paaudio.c | 18 ++++++++++++++----
 qapi/audio.json |  7 +++++--
 qemu-options.hx |  9 +++++++++
 3 files changed, 28 insertions(+), 6 deletions(-)

Comments

Markus Armbruster Sept. 23, 2019, 1:12 p.m. UTC | #1
"Kővágó, Zoltán" <dirty.ice.hu@gmail.com> writes:

> Add an option to change the channel map used by pulseaudio.  If not
> specified, falls back to an OSS compatible channel map.
>
> Signed-off-by: Kővágó, Zoltán <DirtY.iCE.hu@gmail.com>
> ---
>  audio/paaudio.c | 18 ++++++++++++++----
>  qapi/audio.json |  7 +++++--
>  qemu-options.hx |  9 +++++++++
>  3 files changed, 28 insertions(+), 6 deletions(-)
>
> diff --git a/audio/paaudio.c b/audio/paaudio.c
> index d195b1caa8..20402b0718 100644
> --- a/audio/paaudio.c
> +++ b/audio/paaudio.c
> @@ -338,17 +338,27 @@ static pa_stream *qpa_simple_new (
>          pa_stream_direction_t dir,
>          const char *dev,
>          const pa_sample_spec *ss,
> -        const pa_channel_map *map,
> +        const char *map,
>          const pa_buffer_attr *attr,
>          int *rerror)
>  {
>      int r;
>      pa_stream *stream;
>      pa_stream_flags_t flags;
> +    pa_channel_map pa_map;
>  
>      pa_threaded_mainloop_lock(c->mainloop);
>  
> -    stream = pa_stream_new(c->context, name, ss, map);
> +    if (map && !pa_channel_map_parse(&pa_map, map)) {
> +        dolog("Invalid channel map specified: '%s'\n", map);
> +        map = NULL;
> +    }
> +    if (!map) {
> +        pa_channel_map_init_extend(&pa_map, ss->channels,
> +                                   PA_CHANNEL_MAP_OSS);
> +    }
> +
> +    stream = pa_stream_new(c->context, name, ss, &pa_map);
>      if (!stream) {
>          goto fail;
>      }
> @@ -421,7 +431,7 @@ static int qpa_init_out(HWVoiceOut *hw, struct audsettings *as,
>          PA_STREAM_PLAYBACK,
>          ppdo->has_name ? ppdo->name : NULL,
>          &ss,
> -        NULL,                   /* channel map */
> +        ppdo->has_channel_map ? ppdo->channel_map : NULL,
>          &ba,                    /* buffering attributes */
>          &error
>          );
> @@ -470,7 +480,7 @@ static int qpa_init_in(HWVoiceIn *hw, struct audsettings *as, void *drv_opaque)
>          PA_STREAM_RECORD,
>          ppdo->has_name ? ppdo->name : NULL,
>          &ss,
> -        NULL,                   /* channel map */
> +        ppdo->has_channel_map ? ppdo->channel_map : NULL,
>          &ba,                    /* buffering attributes */
>          &error
>          );
> diff --git a/qapi/audio.json b/qapi/audio.json
> index 0535eff794..07003808cb 100644
> --- a/qapi/audio.json
> +++ b/qapi/audio.json
> @@ -214,13 +214,16 @@
>  # @latency: latency you want PulseAudio to achieve in microseconds
>  #           (default 15000)
>  #
> +# @channel-map: channel map to use (default: OSS compatible map, since: 4.2)
> +#
>  # Since: 4.0
>  ##
>  { 'struct': 'AudiodevPaPerDirectionOptions',
>    'base': 'AudiodevPerDirectionOptions',
>    'data': {
> -    '*name': 'str',
> -    '*latency': 'uint32' } }
> +    '*name':        'str',
> +    '*latency':     'uint32',
> +    '*channel-map': 'str' } }
>  
>  ##
>  # @AudiodevPaOptions:
> diff --git a/qemu-options.hx b/qemu-options.hx
> index 395427422a..f3bc342f98 100644
> --- a/qemu-options.hx
> +++ b/qemu-options.hx
> @@ -471,6 +471,7 @@ DEF("audiodev", HAS_ARG, QEMU_OPTION_audiodev,
>      "-audiodev pa,id=id[,prop[=value][,...]]\n"
>      "                server= PulseAudio server address\n"
>      "                in|out.name= source/sink device name\n"
> +    "                in|out.channel-map= channel map to use\n"
>  #endif
>  #ifdef CONFIG_AUDIO_SDL
>      "-audiodev sdl,id=id[,prop[=value][,...]]\n"
> @@ -636,6 +637,14 @@ Sets the PulseAudio @var{server} to connect to.
>  @item in|out.name=@var{sink}
>  Use the specified source/sink for recording/playback.
>  
> +@item in|out.channel-map=@var{map}
> +Use the specified channel map.  The default is an OSS compatible
> +channel map.  Do not forget to escape commas inside the map:

Awkward.

> +
> +@example
> +-audiodev pa,id=example,sink.channel-map=front-left,,front-right
> +@end example

Makes me realize new AudiodevPaPerDirectionOptions member @channel-map
is a list encoded in a string.  QAPI heavily frowns upon encoding stuff
in strings.  Any reason why you can't (or don't want to) make it
['str']?

> +
>  @end table
>  
>  @item -audiodev sdl,id=@var{id}[,@var{prop}[=@var{value}][,...]]
=?UTF-8?B?Wm9sdMOhbiBLxZF2w6Fnw7M=?= Sept. 24, 2019, 12:43 a.m. UTC | #2
On 2019-09-23 15:12, Markus Armbruster wrote:
> "Kővágó, Zoltán" <dirty.ice.hu@gmail.com> writes:
> 
>> Add an option to change the channel map used by pulseaudio.  If not
>> specified, falls back to an OSS compatible channel map.
>>
>> Signed-off-by: Kővágó, Zoltán <DirtY.iCE.hu@gmail.com>
>> ---
>>   audio/paaudio.c | 18 ++++++++++++++----
>>   qapi/audio.json |  7 +++++--
>>   qemu-options.hx |  9 +++++++++
>>   3 files changed, 28 insertions(+), 6 deletions(-)
>>
>> diff --git a/audio/paaudio.c b/audio/paaudio.c
>> index d195b1caa8..20402b0718 100644
>> --- a/audio/paaudio.c
>> +++ b/audio/paaudio.c
>> @@ -338,17 +338,27 @@ static pa_stream *qpa_simple_new (
>>           pa_stream_direction_t dir,
>>           const char *dev,
>>           const pa_sample_spec *ss,
>> -        const pa_channel_map *map,
>> +        const char *map,
>>           const pa_buffer_attr *attr,
>>           int *rerror)
>>   {
>>       int r;
>>       pa_stream *stream;
>>       pa_stream_flags_t flags;
>> +    pa_channel_map pa_map;
>>   
>>       pa_threaded_mainloop_lock(c->mainloop);
>>   
>> -    stream = pa_stream_new(c->context, name, ss, map);
>> +    if (map && !pa_channel_map_parse(&pa_map, map)) {
>> +        dolog("Invalid channel map specified: '%s'\n", map);
>> +        map = NULL;
>> +    }
>> +    if (!map) {
>> +        pa_channel_map_init_extend(&pa_map, ss->channels,
>> +                                   PA_CHANNEL_MAP_OSS);
>> +    }
>> +
>> +    stream = pa_stream_new(c->context, name, ss, &pa_map);
>>       if (!stream) {
>>           goto fail;
>>       }
>> @@ -421,7 +431,7 @@ static int qpa_init_out(HWVoiceOut *hw, struct audsettings *as,
>>           PA_STREAM_PLAYBACK,
>>           ppdo->has_name ? ppdo->name : NULL,
>>           &ss,
>> -        NULL,                   /* channel map */
>> +        ppdo->has_channel_map ? ppdo->channel_map : NULL,
>>           &ba,                    /* buffering attributes */
>>           &error
>>           );
>> @@ -470,7 +480,7 @@ static int qpa_init_in(HWVoiceIn *hw, struct audsettings *as, void *drv_opaque)
>>           PA_STREAM_RECORD,
>>           ppdo->has_name ? ppdo->name : NULL,
>>           &ss,
>> -        NULL,                   /* channel map */
>> +        ppdo->has_channel_map ? ppdo->channel_map : NULL,
>>           &ba,                    /* buffering attributes */
>>           &error
>>           );
>> diff --git a/qapi/audio.json b/qapi/audio.json
>> index 0535eff794..07003808cb 100644
>> --- a/qapi/audio.json
>> +++ b/qapi/audio.json
>> @@ -214,13 +214,16 @@
>>   # @latency: latency you want PulseAudio to achieve in microseconds
>>   #           (default 15000)
>>   #
>> +# @channel-map: channel map to use (default: OSS compatible map, since: 4.2)
>> +#
>>   # Since: 4.0
>>   ##
>>   { 'struct': 'AudiodevPaPerDirectionOptions',
>>     'base': 'AudiodevPerDirectionOptions',
>>     'data': {
>> -    '*name': 'str',
>> -    '*latency': 'uint32' } }
>> +    '*name':        'str',
>> +    '*latency':     'uint32',
>> +    '*channel-map': 'str' } }
>>   
>>   ##
>>   # @AudiodevPaOptions:
>> diff --git a/qemu-options.hx b/qemu-options.hx
>> index 395427422a..f3bc342f98 100644
>> --- a/qemu-options.hx
>> +++ b/qemu-options.hx
>> @@ -471,6 +471,7 @@ DEF("audiodev", HAS_ARG, QEMU_OPTION_audiodev,
>>       "-audiodev pa,id=id[,prop[=value][,...]]\n"
>>       "                server= PulseAudio server address\n"
>>       "                in|out.name= source/sink device name\n"
>> +    "                in|out.channel-map= channel map to use\n"
>>   #endif
>>   #ifdef CONFIG_AUDIO_SDL
>>       "-audiodev sdl,id=id[,prop[=value][,...]]\n"
>> @@ -636,6 +637,14 @@ Sets the PulseAudio @var{server} to connect to.
>>   @item in|out.name=@var{sink}
>>   Use the specified source/sink for recording/playback.
>>   
>> +@item in|out.channel-map=@var{map}
>> +Use the specified channel map.  The default is an OSS compatible
>> +channel map.  Do not forget to escape commas inside the map:
> 
> Awkward.
> 
>> +
>> +@example
>> +-audiodev pa,id=example,sink.channel-map=front-left,,front-right
>> +@end example
> 
> Makes me realize new AudiodevPaPerDirectionOptions member @channel-map
> is a list encoded in a string.  QAPI heavily frowns upon encoding stuff
> in strings.  Any reason why you can't (or don't want to) make it
> ['str']?

Hmm, I don't think it's used too frequently on structs parsed by qapi 
opts visitor. What would be the command line format in that case? 
Something like this?

-audiodev 
pa,id=example,sink.channel-map=front-left,sink.channel-map=front-right

I think it's simply a string because while conceptually it's a string, 
we don't try to interpret it, we just pass the string to 
pa_channel_map_parse.  Of course we could take a list and instead either 
rebuild the string or reimplement half of pa_channel_map_parse by 
manually calling pa_channel_position_from_string.
Oh now that I looked again at the pulseaudio docs, channel-map doesn't 
have to be a list, it can be also a "well-known mapping name".

> 
>> +
>>   @end table
>>   
>>   @item -audiodev sdl,id=@var{id}[,@var{prop}[=@var{value}][,...]]
Markus Armbruster Sept. 25, 2019, 12:17 p.m. UTC | #3
"Zoltán Kővágó" <dirty.ice.hu@gmail.com> writes:

> On 2019-09-23 15:12, Markus Armbruster wrote:
>> "Kővágó, Zoltán" <dirty.ice.hu@gmail.com> writes:
>>
>>> Add an option to change the channel map used by pulseaudio.  If not
>>> specified, falls back to an OSS compatible channel map.
>>>
>>> Signed-off-by: Kővágó, Zoltán <DirtY.iCE.hu@gmail.com>
>>> ---
>>>   audio/paaudio.c | 18 ++++++++++++++----
>>>   qapi/audio.json |  7 +++++--
>>>   qemu-options.hx |  9 +++++++++
>>>   3 files changed, 28 insertions(+), 6 deletions(-)
>>>
>>> diff --git a/audio/paaudio.c b/audio/paaudio.c
>>> index d195b1caa8..20402b0718 100644
>>> --- a/audio/paaudio.c
>>> +++ b/audio/paaudio.c
>>> @@ -338,17 +338,27 @@ static pa_stream *qpa_simple_new (
>>>           pa_stream_direction_t dir,
>>>           const char *dev,
>>>           const pa_sample_spec *ss,
>>> -        const pa_channel_map *map,
>>> +        const char *map,
>>>           const pa_buffer_attr *attr,
>>>           int *rerror)
>>>   {
>>>       int r;
>>>       pa_stream *stream;
>>>       pa_stream_flags_t flags;
>>> +    pa_channel_map pa_map;
>>>         pa_threaded_mainloop_lock(c->mainloop);
>>>   -    stream = pa_stream_new(c->context, name, ss, map);
>>> +    if (map && !pa_channel_map_parse(&pa_map, map)) {
>>> +        dolog("Invalid channel map specified: '%s'\n", map);
>>> +        map = NULL;
>>> +    }
>>> +    if (!map) {
>>> +        pa_channel_map_init_extend(&pa_map, ss->channels,
>>> +                                   PA_CHANNEL_MAP_OSS);
>>> +    }
>>> +
>>> +    stream = pa_stream_new(c->context, name, ss, &pa_map);
>>>       if (!stream) {
>>>           goto fail;
>>>       }
>>> @@ -421,7 +431,7 @@ static int qpa_init_out(HWVoiceOut *hw, struct audsettings *as,
>>>           PA_STREAM_PLAYBACK,
>>>           ppdo->has_name ? ppdo->name : NULL,
>>>           &ss,
>>> -        NULL,                   /* channel map */
>>> +        ppdo->has_channel_map ? ppdo->channel_map : NULL,
>>>           &ba,                    /* buffering attributes */
>>>           &error
>>>           );
>>> @@ -470,7 +480,7 @@ static int qpa_init_in(HWVoiceIn *hw, struct audsettings *as, void *drv_opaque)
>>>           PA_STREAM_RECORD,
>>>           ppdo->has_name ? ppdo->name : NULL,
>>>           &ss,
>>> -        NULL,                   /* channel map */
>>> +        ppdo->has_channel_map ? ppdo->channel_map : NULL,
>>>           &ba,                    /* buffering attributes */
>>>           &error
>>>           );
>>> diff --git a/qapi/audio.json b/qapi/audio.json
>>> index 0535eff794..07003808cb 100644
>>> --- a/qapi/audio.json
>>> +++ b/qapi/audio.json
>>> @@ -214,13 +214,16 @@
>>>   # @latency: latency you want PulseAudio to achieve in microseconds
>>>   #           (default 15000)
>>>   #
>>> +# @channel-map: channel map to use (default: OSS compatible map, since: 4.2)
>>> +#
>>>   # Since: 4.0
>>>   ##
>>>   { 'struct': 'AudiodevPaPerDirectionOptions',
>>>     'base': 'AudiodevPerDirectionOptions',
>>>     'data': {
>>> -    '*name': 'str',
>>> -    '*latency': 'uint32' } }
>>> +    '*name':        'str',
>>> +    '*latency':     'uint32',
>>> +    '*channel-map': 'str' } }
>>>     ##
>>>   # @AudiodevPaOptions:
>>> diff --git a/qemu-options.hx b/qemu-options.hx
>>> index 395427422a..f3bc342f98 100644
>>> --- a/qemu-options.hx
>>> +++ b/qemu-options.hx
>>> @@ -471,6 +471,7 @@ DEF("audiodev", HAS_ARG, QEMU_OPTION_audiodev,
>>>       "-audiodev pa,id=id[,prop[=value][,...]]\n"
>>>       "                server= PulseAudio server address\n"
>>>       "                in|out.name= source/sink device name\n"
>>> +    "                in|out.channel-map= channel map to use\n"
>>>   #endif
>>>   #ifdef CONFIG_AUDIO_SDL
>>>       "-audiodev sdl,id=id[,prop[=value][,...]]\n"
>>> @@ -636,6 +637,14 @@ Sets the PulseAudio @var{server} to connect to.
>>>   @item in|out.name=@var{sink}
>>>   Use the specified source/sink for recording/playback.
>>>   +@item in|out.channel-map=@var{map}
>>> +Use the specified channel map.  The default is an OSS compatible
>>> +channel map.  Do not forget to escape commas inside the map:
>>
>> Awkward.
>>
>>> +
>>> +@example
>>> +-audiodev pa,id=example,sink.channel-map=front-left,,front-right
>>> +@end example
>>
>> Makes me realize new AudiodevPaPerDirectionOptions member @channel-map
>> is a list encoded in a string.  QAPI heavily frowns upon encoding stuff
>> in strings.  Any reason why you can't (or don't want to) make it
>> ['str']?
>
> Hmm, I don't think it's used too frequently on structs parsed by qapi
> opts visitor. What would be the command line format in that case?
> Something like this?
>
> -audiodev
> pa,id=example,sink.channel-map=front-left,sink.channel-map=front-right

Let's talk concepts before details.  I'll get back to this below.

> I think it's simply a string because while conceptually it's a string,
> we don't try to interpret it, we just pass the string to
> pa_channel_map_parse.  Of course we could take a list and instead
> either rebuild the string or reimplement half of pa_channel_map_parse
> by manually calling pa_channel_position_from_string.

Precedence: BlockdevOptionsRbd member @auth-client-required.  Under the
hood, this is Ceph configuration option auth-client-required.
Semantically a list of well-known values.  rados_conf_set() takes it
encoded in a string.  We could have plumbed that straight through QAPI
by making @auth-client-required a 'str'.  Instead, we made it
['RbdAuthMode'].  rbd.c has to encode the list in a string for
rados_conf_set().

Why did we bother?  Besides the dogmatic reason "do not encode stuff in
strings in QAPI", there's a pragmatic one: introspection.
@auth-client-required's type ['RbdAuthMode'] tells exactly what the
acceptable values are.  If Ceph ever grows another mode, QEMU support
for it will be visible in introspection: enum RbdAuthMode will have
another value.

Baking the acceptable modes into QEMU means new modes need a QEMU update
to work.  New modes that just work without a QEMU update feel unlikely.
If we're wrong, and a QEMU update is impractical, you can still work
around the limitation with a Ceph configuration file.

Which of these considerations other than "do not encode in strings"
apply to PA I can't say.

Let's turn the question around: is there any reason to break the "do not
encode in strings" rule other than "it saves a few lines of code"?

> Oh now that I looked again at the pulseaudio docs, channel-map doesn't
> have to be a list, it can be also a "well-known mapping name".

Unambiguous because the well-known mapping names are not valid channel
position list members.

Semantially, this is a disjoint union of well-known mapping names and
channel position lists.

The tools QAPI provides for disjoint unions are union and alternate
types.  In this case, an alternate of 'str' and ['str'] would do.
Well-known mapping names use the 'str' branch, channel position lists
the ['str'] branch.

>>
>>> +
>>>   @end table
>>>     @item -audiodev
>>> sdl,id=@var{id}[,@var{prop}[=@var{value}][,...]]

I promised you to get back to command line syntax.

The opts visitor uses repeated keys for lists, just like you guessed.
It supports only a subset of the QAPI types, and has weird magic for
integer lists.

-audiodev actually uses the QObject input visitor, which is more general
and somewhat less magical.  Its CLI syntax for lists is even wordier,
though.  I'd expect something like

    -audiodev pa,id=example,sink.channel-map.0=front-left,sink.channel-map.1=front-right

Aside: CLI QAPIfication will have to find a way to accomodate the opts
visitor's syntax and quirks, or break compatibility.
Gerd Hoffmann Sept. 25, 2019, 2:13 p.m. UTC | #4
Hi,

> > Oh now that I looked again at the pulseaudio docs, channel-map doesn't
> > have to be a list, it can be also a "well-known mapping name".
> 
> Unambiguous because the well-known mapping names are not valid channel
> position list members.

Do we have well-known mapping names for the common use cases?
So maybe just support these?

cheers,
  Gerd
=?UTF-8?B?Wm9sdMOhbiBLxZF2w6Fnw7M=?= Sept. 29, 2019, 6:13 p.m. UTC | #5
On 2019-09-25 16:13, Gerd Hoffmann wrote:
>    Hi,
> 
>>> Oh now that I looked again at the pulseaudio docs, channel-map doesn't
>>> have to be a list, it can be also a "well-known mapping name".
>>
>> Unambiguous because the well-known mapping names are not valid channel
>> position list members.
> 
> Do we have well-known mapping names for the common use cases?
> So maybe just support these?
> 
> cheers,
>    Gerd
> 

It's surprisingly hard to figure out what are these "well-known names", 
I could only find them in the source:
https://github.com/michaelwu/pulseaudio/blob/16e3dccfa88455ebd329de27a98a3d979a8bdc8e/src/pulse/channelmap.c#L538

It doesn't provide much over the "auto detect channel maps from the 
number of channels" pulseaudio already has (the only ambiguous case is 
surround-41 vs surround-50).  IIRC I originally added this feature 
because USB audio and pulseaudio didn't agree about the order of 
channels, for example in 5.1 pa defaults to 
left,right,rear-left,rear-right,front-center,lfe while USB audio expects 
left,right,center,lfe,rear-left,rear-right order. You can't specify this 
with the well-known names.

Regards,
Zoltan
Gerd Hoffmann Sept. 30, 2019, 6:36 a.m. UTC | #6
Hi,

> vs surround-50).  IIRC I originally added this feature because USB audio and
> pulseaudio didn't agree about the order of channels, for example in 5.1 pa
> defaults to left,right,rear-left,rear-right,front-center,lfe while USB audio
> expects left,right,center,lfe,rear-left,rear-right order. You can't specify
> this with the well-known names.

Shouldn't backend and frontend be able to negotiate a channel map
without user invention?

Initially, with only usb-audio supporting 5.1/7.1 we can simply declare
the usb-audio channel map as the one which is used.  When other devices
are added some day (hda-surround?) we might need a ctl function so the
frontend can set the channel map (similar to the way volume is handled
today).

cheers,
  Gerd
diff mbox series

Patch

diff --git a/audio/paaudio.c b/audio/paaudio.c
index d195b1caa8..20402b0718 100644
--- a/audio/paaudio.c
+++ b/audio/paaudio.c
@@ -338,17 +338,27 @@  static pa_stream *qpa_simple_new (
         pa_stream_direction_t dir,
         const char *dev,
         const pa_sample_spec *ss,
-        const pa_channel_map *map,
+        const char *map,
         const pa_buffer_attr *attr,
         int *rerror)
 {
     int r;
     pa_stream *stream;
     pa_stream_flags_t flags;
+    pa_channel_map pa_map;
 
     pa_threaded_mainloop_lock(c->mainloop);
 
-    stream = pa_stream_new(c->context, name, ss, map);
+    if (map && !pa_channel_map_parse(&pa_map, map)) {
+        dolog("Invalid channel map specified: '%s'\n", map);
+        map = NULL;
+    }
+    if (!map) {
+        pa_channel_map_init_extend(&pa_map, ss->channels,
+                                   PA_CHANNEL_MAP_OSS);
+    }
+
+    stream = pa_stream_new(c->context, name, ss, &pa_map);
     if (!stream) {
         goto fail;
     }
@@ -421,7 +431,7 @@  static int qpa_init_out(HWVoiceOut *hw, struct audsettings *as,
         PA_STREAM_PLAYBACK,
         ppdo->has_name ? ppdo->name : NULL,
         &ss,
-        NULL,                   /* channel map */
+        ppdo->has_channel_map ? ppdo->channel_map : NULL,
         &ba,                    /* buffering attributes */
         &error
         );
@@ -470,7 +480,7 @@  static int qpa_init_in(HWVoiceIn *hw, struct audsettings *as, void *drv_opaque)
         PA_STREAM_RECORD,
         ppdo->has_name ? ppdo->name : NULL,
         &ss,
-        NULL,                   /* channel map */
+        ppdo->has_channel_map ? ppdo->channel_map : NULL,
         &ba,                    /* buffering attributes */
         &error
         );
diff --git a/qapi/audio.json b/qapi/audio.json
index 0535eff794..07003808cb 100644
--- a/qapi/audio.json
+++ b/qapi/audio.json
@@ -214,13 +214,16 @@ 
 # @latency: latency you want PulseAudio to achieve in microseconds
 #           (default 15000)
 #
+# @channel-map: channel map to use (default: OSS compatible map, since: 4.2)
+#
 # Since: 4.0
 ##
 { 'struct': 'AudiodevPaPerDirectionOptions',
   'base': 'AudiodevPerDirectionOptions',
   'data': {
-    '*name': 'str',
-    '*latency': 'uint32' } }
+    '*name':        'str',
+    '*latency':     'uint32',
+    '*channel-map': 'str' } }
 
 ##
 # @AudiodevPaOptions:
diff --git a/qemu-options.hx b/qemu-options.hx
index 395427422a..f3bc342f98 100644
--- a/qemu-options.hx
+++ b/qemu-options.hx
@@ -471,6 +471,7 @@  DEF("audiodev", HAS_ARG, QEMU_OPTION_audiodev,
     "-audiodev pa,id=id[,prop[=value][,...]]\n"
     "                server= PulseAudio server address\n"
     "                in|out.name= source/sink device name\n"
+    "                in|out.channel-map= channel map to use\n"
 #endif
 #ifdef CONFIG_AUDIO_SDL
     "-audiodev sdl,id=id[,prop[=value][,...]]\n"
@@ -636,6 +637,14 @@  Sets the PulseAudio @var{server} to connect to.
 @item in|out.name=@var{sink}
 Use the specified source/sink for recording/playback.
 
+@item in|out.channel-map=@var{map}
+Use the specified channel map.  The default is an OSS compatible
+channel map.  Do not forget to escape commas inside the map:
+
+@example
+-audiodev pa,id=example,sink.channel-map=front-left,,front-right
+@end example
+
 @end table
 
 @item -audiodev sdl,id=@var{id}[,@var{prop}[=@var{value}][,...]]