diff options
| author | Jyri Sarha <jyri.sarha@nokia.com> | 2010-10-15 13:05:15 +0300 | 
|---|---|---|
| committer | Colin Guthrie <cguthrie@mandriva.org> | 2010-10-16 11:53:39 +0100 | 
| commit | 1bea9558297773fd2e2fe4deb43a5adabf90a16e (patch) | |
| tree | 822060b62808b9ea012bd9b5e94a159be9a2b06c /src | |
| parent | 5391daf8dfcae45c6abb74c0a5ff9b7c50e3c000 (diff) | |
alsa: Take syncronized HW volume infra into use for alsa-sink
Signed-off-by: Jyri Sarha <jyri.sarha@nokia.com>
Reviewed-by: Tanu Kaskinen <tanu.kaskinen@digia.com>
Reviewd-by: Colin Guthrie <cguthrie@mandriva.org>
Diffstat (limited to 'src')
| -rw-r--r-- | src/modules/alsa/alsa-mixer.c | 117 | ||||
| -rw-r--r-- | src/modules/alsa/alsa-mixer.h | 9 | ||||
| -rw-r--r-- | src/modules/alsa/alsa-sink.c | 129 | ||||
| -rw-r--r-- | src/modules/alsa/alsa-source.c | 2 | ||||
| -rw-r--r-- | src/modules/alsa/module-alsa-card.c | 4 | ||||
| -rw-r--r-- | src/modules/alsa/module-alsa-sink.c | 10 | 
6 files changed, 246 insertions, 25 deletions
diff --git a/src/modules/alsa/alsa-mixer.c b/src/modules/alsa/alsa-mixer.c index 1033bbea..23b22d00 100644 --- a/src/modules/alsa/alsa-mixer.c +++ b/src/modules/alsa/alsa-mixer.c @@ -243,6 +243,95 @@ int pa_alsa_fdlist_set_mixer(struct pa_alsa_fdlist *fdl, snd_mixer_t *mixer_hand      return 0;  } +struct pa_alsa_mixer_pdata { +    pa_rtpoll *rtpoll; +    pa_rtpoll_item *poll_item; +    snd_mixer_t *mixer; +}; + + +struct pa_alsa_mixer_pdata *pa_alsa_mixer_pdata_new(void) { +    struct pa_alsa_mixer_pdata *pd; + +    pd = pa_xnew0(struct pa_alsa_mixer_pdata, 1); + +    return pd; +} + +void pa_alsa_mixer_pdata_free(struct pa_alsa_mixer_pdata *pd) { +    pa_assert(pd); + +    if (pd->poll_item) { +        pa_rtpoll_item_free(pd->poll_item); +    } + +    pa_xfree(pd); +} + +static int rtpoll_work_cb(pa_rtpoll_item *i) { +    struct pa_alsa_mixer_pdata *pd; +    struct pollfd *p; +    unsigned n_fds; +    unsigned short revents = 0; +    int err; + +    pd = pa_rtpoll_item_get_userdata(i); +    pa_assert_fp(pd); +    pa_assert_fp(i == pd->poll_item); + +    p = pa_rtpoll_item_get_pollfd(i, &n_fds); + +    if ((err = snd_mixer_poll_descriptors_revents(pd->mixer, p, n_fds, &revents)) < 0) { +        pa_log_error("Unable to get poll revent: %s", pa_alsa_strerror(err)); +        pa_rtpoll_item_free(i); +        return -1; +    } + +    if (revents) { +        snd_mixer_handle_events(pd->mixer); +        pa_rtpoll_item_free(i); +        pa_alsa_set_mixer_rtpoll(pd, pd->mixer, pd->rtpoll); +    } + +    return 0; +} + +int pa_alsa_set_mixer_rtpoll(struct pa_alsa_mixer_pdata *pd, snd_mixer_t *mixer, pa_rtpoll *rtp) { +    pa_rtpoll_item *i; +    struct pollfd *p; +    int err, n; + +    pa_assert(pd); +    pa_assert(mixer); +    pa_assert(rtp); + +    if ((n = snd_mixer_poll_descriptors_count(mixer)) < 0) { +        pa_log("snd_mixer_poll_descriptors_count() failed: %s", pa_alsa_strerror(n)); +        return -1; +    } + +    i = pa_rtpoll_item_new(rtp, PA_RTPOLL_LATE, (unsigned) n); + +    p = pa_rtpoll_item_get_pollfd(i, NULL); + +    memset(p, 0, sizeof(struct pollfd) * n); + +    if ((err = snd_mixer_poll_descriptors(mixer, p, (unsigned) n)) < 0) { +        pa_log_error("Unable to get poll descriptors: %s", pa_alsa_strerror(err)); +        pa_rtpoll_item_free(i); +        return -1; +    } + +    pd->rtpoll = rtp; +    pd->poll_item = i; +    pd->mixer = mixer; + +    pa_rtpoll_item_set_userdata(i, pd); +    pa_rtpoll_item_set_work_callback(i, rtpoll_work_cb); + +    return 0; +} +  static int prepare_mixer(snd_mixer_t *mixer, const char *dev) {      int err; @@ -671,7 +760,8 @@ int pa_alsa_path_get_mute(pa_alsa_path *p, snd_mixer_t *m, pa_bool_t *muted) {      return 0;  } -static int element_set_volume(pa_alsa_element *e, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v) { +static int element_set_volume(pa_alsa_element *e, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v, pa_bool_t write_to_hw) { +      snd_mixer_selem_id_t *sid;      pa_cvolume rv;      snd_mixer_elem_t *me; @@ -720,14 +810,26 @@ static int element_set_volume(pa_alsa_element *e, snd_mixer_t *m, const pa_chann                   * if the channel is available, ALSA behaves ver                   * strangely and doesn't fail the call */                  if (snd_mixer_selem_has_playback_channel(me, c)) { -                    if ((r = snd_mixer_selem_set_playback_dB(me, c, value, +1)) >= 0) -                        r = snd_mixer_selem_get_playback_dB(me, c, &value); +                    if (write_to_hw) { +                        if ((r = snd_mixer_selem_set_playback_dB(me, c, value, +1)) >= 0) +                            r = snd_mixer_selem_get_playback_dB(me, c, &value); +                    } else { +                        long alsa_val; +                        if ((r = snd_mixer_selem_ask_playback_dB_vol(me, value, +1, &alsa_val)) >= 0) +                            r = snd_mixer_selem_ask_playback_vol_dB(me, alsa_val, &value); +                    }                  } else                      r = -1;              } else {                  if (snd_mixer_selem_has_capture_channel(me, c)) { -                    if ((r = snd_mixer_selem_set_capture_dB(me, c, value, +1)) >= 0) -                        r = snd_mixer_selem_get_capture_dB(me, c, &value); +                    if (write_to_hw) { +                        if ((r = snd_mixer_selem_set_capture_dB(me, c, value, +1)) >= 0) +                            r = snd_mixer_selem_get_capture_dB(me, c, &value); +                    } else { +                        long alsa_val; +                        if ((r = snd_mixer_selem_ask_capture_dB_vol(me, value, +1, &alsa_val)) >= 0) +                            r = snd_mixer_selem_ask_capture_vol_dB(me, alsa_val, &value); +                    }                  } else                      r = -1;              } @@ -782,7 +884,8 @@ static int element_set_volume(pa_alsa_element *e, snd_mixer_t *m, const pa_chann      return 0;  } -int pa_alsa_path_set_volume(pa_alsa_path *p, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v) { +int pa_alsa_path_set_volume(pa_alsa_path *p, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v, pa_bool_t write_to_hw) { +      pa_alsa_element *e;      pa_cvolume rv; @@ -807,7 +910,7 @@ int pa_alsa_path_set_volume(pa_alsa_path *p, snd_mixer_t *m, const pa_channel_ma          pa_assert(!p->has_dB || e->has_dB);          ev = rv; -        if (element_set_volume(e, m, cm, &ev) < 0) +        if (element_set_volume(e, m, cm, &ev, write_to_hw) < 0)              return -1;          if (!p->has_dB) { diff --git a/src/modules/alsa/alsa-mixer.h b/src/modules/alsa/alsa-mixer.h index a0d4fcbe..7fb408a6 100644 --- a/src/modules/alsa/alsa-mixer.h +++ b/src/modules/alsa/alsa-mixer.h @@ -38,6 +38,7 @@  #include <pulsecore/log.h>  typedef struct pa_alsa_fdlist pa_alsa_fdlist; +typedef struct pa_alsa_mixer_pdata pa_alsa_mixer_pdata;  typedef struct pa_alsa_setting pa_alsa_setting;  typedef struct pa_alsa_option pa_alsa_option;  typedef struct pa_alsa_element pa_alsa_element; @@ -202,7 +203,7 @@ int pa_alsa_path_probe(pa_alsa_path *p, snd_mixer_t *m, pa_bool_t ignore_dB);  void pa_alsa_path_dump(pa_alsa_path *p);  int pa_alsa_path_get_volume(pa_alsa_path *p, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v);  int pa_alsa_path_get_mute(pa_alsa_path *path, snd_mixer_t *m, pa_bool_t *muted); -int pa_alsa_path_set_volume(pa_alsa_path *path, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v); +int pa_alsa_path_set_volume(pa_alsa_path *path, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v, pa_bool_t write_to_hw);  int pa_alsa_path_set_mute(pa_alsa_path *path, snd_mixer_t *m, pa_bool_t muted);  int pa_alsa_path_select(pa_alsa_path *p, snd_mixer_t *m);  void pa_alsa_path_set_callback(pa_alsa_path *p, snd_mixer_t *m, snd_mixer_elem_callback_t cb, void *userdata); @@ -279,6 +280,12 @@ pa_alsa_fdlist *pa_alsa_fdlist_new(void);  void pa_alsa_fdlist_free(pa_alsa_fdlist *fdl);  int pa_alsa_fdlist_set_mixer(pa_alsa_fdlist *fdl, snd_mixer_t *mixer_handle, pa_mainloop_api* m); +/* Alternative for handling alsa mixer events in io-thread. */ + +pa_alsa_mixer_pdata *pa_alsa_mixer_pdata_new(void); +void pa_alsa_mixer_pdata_free(pa_alsa_mixer_pdata *pd); +int pa_alsa_set_mixer_rtpoll(struct pa_alsa_mixer_pdata *pd, snd_mixer_t *mixer, pa_rtpoll *rtp); +  /* Data structure for inclusion in pa_device_port for alsa   * sinks/sources. This contains nothing that needs to be freed   * individually */ diff --git a/src/modules/alsa/alsa-sink.c b/src/modules/alsa/alsa-sink.c index 1108a797..069ee95c 100644 --- a/src/modules/alsa/alsa-sink.c +++ b/src/modules/alsa/alsa-sink.c @@ -101,6 +101,7 @@ struct userdata {      snd_pcm_t *pcm_handle;      pa_alsa_fdlist *mixer_fdl; +    pa_alsa_mixer_pdata *mixer_pd;      snd_mixer_t *mixer_handle;      pa_alsa_path_set *mixer_path_set;      pa_alsa_path *mixer_path; @@ -1124,7 +1125,7 @@ static int sink_set_state_cb(pa_sink *s, pa_sink_state_t new_state) {      return 0;  } -static int mixer_callback(snd_mixer_elem_t *elem, unsigned int mask) { +static int ctl_mixer_callback(snd_mixer_elem_t *elem, unsigned int mask) {      struct userdata *u = snd_mixer_elem_get_callback_private(elem);      pa_assert(u); @@ -1144,6 +1145,24 @@ static int mixer_callback(snd_mixer_elem_t *elem, unsigned int mask) {      return 0;  } +static int io_mixer_callback(snd_mixer_elem_t *elem, unsigned int mask) { +    struct userdata *u = snd_mixer_elem_get_callback_private(elem); + +    pa_assert(u); +    pa_assert(u->mixer_handle); + +    if (mask == SND_CTL_EVENT_MASK_REMOVE) +        return 0; + +    if (u->sink->suspend_cause & PA_SUSPEND_SESSION) +        return 0; + +    if (mask & SND_CTL_EVENT_MASK_VALUE) +        pa_sink_update_volume_and_mute(u->sink); + +    return 0; +} +  static void sink_get_volume_cb(pa_sink *s) {      struct userdata *u = s->userdata;      pa_cvolume r; @@ -1175,6 +1194,7 @@ static void sink_set_volume_cb(pa_sink *s) {      struct userdata *u = s->userdata;      pa_cvolume r;      char t[PA_CVOLUME_SNPRINT_MAX]; +    pa_bool_t write_to_hw = (s->flags & PA_SINK_SYNC_VOLUME) ? FALSE : TRUE;      pa_assert(u);      pa_assert(u->mixer_path); @@ -1183,7 +1203,7 @@ static void sink_set_volume_cb(pa_sink *s) {      /* Shift up by the base volume */      pa_sw_cvolume_divide_scalar(&r, &s->real_volume, s->base_volume); -    if (pa_alsa_path_set_volume(u->mixer_path, u->mixer_handle, &s->channel_map, &r) < 0) +    if (pa_alsa_path_set_volume(u->mixer_path, u->mixer_handle, &s->channel_map, &r, write_to_hw) < 0)          return;      /* Shift down by the base volume, so that 0dB becomes maximum volume */ @@ -1223,6 +1243,33 @@ static void sink_set_volume_cb(pa_sink *s) {      }  } +static void sink_write_volume_cb(pa_sink *s) { +    struct userdata *u = s->userdata; +    pa_cvolume hw_vol = s->thread_info.current_hw_volume; + +    pa_assert(u); +    pa_assert(u->mixer_path); +    pa_assert(u->mixer_handle); +    pa_assert(s->flags & PA_SINK_SYNC_VOLUME); + +    if (pa_alsa_path_set_volume(u->mixer_path, u->mixer_handle, &s->channel_map, &hw_vol, TRUE) < 0) +        pa_log_error("Writing HW volume failed"); +    else { +        pa_cvolume tmp_vol; +        pa_bool_t accurate_enough; +        pa_sw_cvolume_divide(&tmp_vol, &hw_vol, &s->thread_info.current_hw_volume); +        accurate_enough = +            (pa_cvolume_min(&tmp_vol) >= (PA_VOLUME_NORM - VOLUME_ACCURACY)) && +            (pa_cvolume_max(&tmp_vol) <= (PA_VOLUME_NORM + VOLUME_ACCURACY)); +        if (!accurate_enough) { +            char t[PA_CVOLUME_SNPRINT_MAX]; +            pa_log_debug("Written HW volume did not match with the request %s != %s", +                         pa_cvolume_snprint(t, sizeof(t), &s->thread_info.current_hw_volume), +                         pa_cvolume_snprint(t, sizeof(t), &hw_vol)); +        } +    } +} +  static void sink_get_mute_cb(pa_sink *s) {      struct userdata *u = s->userdata;      pa_bool_t b; @@ -1385,6 +1432,7 @@ static void thread_func(void *userdata) {      for (;;) {          int ret; +        pa_usec_t rtpoll_sleep = 0;  #ifdef DEBUG_TIMING          pa_log_debug("Loop"); @@ -1453,20 +1501,32 @@ static void thread_func(void *userdata) {  /*                 pa_log_debug("Waking up in %0.2fms (system clock).", (double) cusec / PA_USEC_PER_MSEC); */                  /* We don't trust the conversion, so we wake up whatever comes first */ -                pa_rtpoll_set_timer_relative(u->rtpoll, PA_MIN(sleep_usec, cusec)); +                rtpoll_sleep = PA_MIN(sleep_usec, cusec);              }              u->after_rewind = FALSE; -        } else if (u->use_tsched) +        } + +        if (u->sink->flags & PA_SINK_SYNC_VOLUME) { +            pa_usec_t volume_sleep; +            pa_sink_volume_change_apply(u->sink, &volume_sleep); +            if (volume_sleep > 0) +                rtpoll_sleep = MIN(volume_sleep, rtpoll_sleep); +        } -            /* OK, we're in an invalid state, let's disable our timers */ +        if (rtpoll_sleep > 0) +            pa_rtpoll_set_timer_relative(u->rtpoll, rtpoll_sleep); +        else              pa_rtpoll_set_timer_disabled(u->rtpoll);          /* Hmm, nothing to do. Let's sleep */          if ((ret = pa_rtpoll_run(u->rtpoll, TRUE)) < 0)              goto fail; +        if (u->sink->flags & PA_SINK_SYNC_VOLUME) +            pa_sink_volume_change_apply(u->sink, NULL); +          if (ret == 0)              goto finish; @@ -1585,7 +1645,9 @@ fail:      }  } -static int setup_mixer(struct userdata *u, pa_bool_t ignore_dB) { +static int setup_mixer(struct userdata *u, pa_bool_t ignore_dB, pa_bool_t sync_volume) { +    int (*mixer_callback)(snd_mixer_elem_t *, unsigned int); +      pa_assert(u);      if (!u->mixer_handle) @@ -1651,8 +1713,17 @@ static int setup_mixer(struct userdata *u, pa_bool_t ignore_dB) {          u->sink->get_volume = sink_get_volume_cb;          u->sink->set_volume = sink_set_volume_cb; +        u->sink->write_volume = sink_write_volume_cb; + +        u->sink->flags |= PA_SINK_HW_VOLUME_CTRL; +        if (u->mixer_path->has_dB) { +            u->sink->flags |= PA_SINK_DECIBEL_VOLUME; +            if (sync_volume) { +                u->sink->flags |= PA_SINK_SYNC_VOLUME; +                pa_log_info("Successfully enabled synchronous volume."); +            } +        } -        u->sink->flags |= PA_SINK_HW_VOLUME_CTRL | (u->mixer_path->has_dB ? PA_SINK_DECIBEL_VOLUME : 0);          pa_log_info("Using hardware volume control. Hardware dB scale %s.", u->mixer_path->has_dB ? "supported" : "not supported");      } @@ -1665,11 +1736,23 @@ static int setup_mixer(struct userdata *u, pa_bool_t ignore_dB) {          pa_log_info("Using hardware mute control.");      } -    u->mixer_fdl = pa_alsa_fdlist_new(); +    if (sync_volume) { +        u->mixer_pd = pa_alsa_mixer_pdata_new(); +        mixer_callback = io_mixer_callback; -    if (pa_alsa_fdlist_set_mixer(u->mixer_fdl, u->mixer_handle, u->core->mainloop) < 0) { -        pa_log("Failed to initialize file descriptor monitoring"); -        return -1; +        if (pa_alsa_set_mixer_rtpoll(u->mixer_pd, u->mixer_handle, u->rtpoll) < 0) { +            pa_log("Failed to initialize file descriptor monitoring"); +            return -1; +        } + +    } else { +        u->mixer_fdl = pa_alsa_fdlist_new(); +        mixer_callback = ctl_mixer_callback; + +        if (pa_alsa_fdlist_set_mixer(u->mixer_fdl, u->mixer_handle, u->core->mainloop) < 0) { +            pa_log("Failed to initialize file descriptor monitoring"); +            return -1; +        }      }      if (u->mixer_path_set) @@ -1689,7 +1772,7 @@ pa_sink *pa_alsa_sink_new(pa_module *m, pa_modargs *ma, const char*driver, pa_ca      uint32_t nfrags, frag_size, buffer_size, tsched_size, tsched_watermark, rewind_safeguard;      snd_pcm_uframes_t period_frames, buffer_frames, tsched_frames;      size_t frame_size; -    pa_bool_t use_mmap = TRUE, b, use_tsched = TRUE, d, ignore_dB = FALSE, namereg_fail = FALSE; +    pa_bool_t use_mmap = TRUE, b, use_tsched = TRUE, d, ignore_dB = FALSE, namereg_fail = FALSE, sync_volume = FALSE;      pa_sink_new_data data;      pa_alsa_profile_set *profile_set = NULL; @@ -1748,6 +1831,11 @@ pa_sink *pa_alsa_sink_new(pa_module *m, pa_modargs *ma, const char*driver, pa_ca          goto fail;      } +    if (pa_modargs_get_value_boolean(ma, "sync_volume", &sync_volume) < 0) { +        pa_log("Failed to parse sync_volume argument."); +        goto fail; +    } +      use_tsched = pa_alsa_may_tsched(use_tsched);      u = pa_xnew0(struct userdata, 1); @@ -1913,6 +2001,18 @@ pa_sink *pa_alsa_sink_new(pa_module *m, pa_modargs *ma, const char*driver, pa_ca          goto fail;      } +    if (pa_modargs_get_value_u32(ma, "sync_volume_safety_margin", +                                 &u->sink->thread_info.volume_change_safety_margin) < 0) { +        pa_log("Failed to parse sync_volume_safety_margin parameter"); +        goto fail; +    } + +    if (pa_modargs_get_value_s32(ma, "sync_volume_extra_delay", +                                 &u->sink->thread_info.volume_change_extra_delay) < 0) { +        pa_log("Failed to parse sync_volume_extra_delay parameter"); +        goto fail; +    } +      u->sink->parent.process_msg = sink_process_msg;      if (u->use_tsched)          u->sink->update_requested_latency = sink_update_requested_latency_cb; @@ -1969,7 +2069,7 @@ pa_sink *pa_alsa_sink_new(pa_module *m, pa_modargs *ma, const char*driver, pa_ca      if (update_sw_params(u) < 0)          goto fail; -    if (setup_mixer(u, ignore_dB) < 0) +    if (setup_mixer(u, ignore_dB, sync_volume) < 0)          goto fail;      pa_alsa_dump(PA_LOG_DEBUG, u->pcm_handle); @@ -2033,6 +2133,9 @@ static void userdata_free(struct userdata *u) {      if (u->memchunk.memblock)          pa_memblock_unref(u->memchunk.memblock); +    if (u->mixer_pd) +        pa_alsa_mixer_pdata_free(u->mixer_pd); +      if (u->alsa_rtpoll_item)          pa_rtpoll_item_free(u->alsa_rtpoll_item); diff --git a/src/modules/alsa/alsa-source.c b/src/modules/alsa/alsa-source.c index 5f126751..97951367 100644 --- a/src/modules/alsa/alsa-source.c +++ b/src/modules/alsa/alsa-source.c @@ -1108,7 +1108,7 @@ static void source_set_volume_cb(pa_source *s) {      /* Shift up by the base volume */      pa_sw_cvolume_divide_scalar(&r, &s->volume, s->base_volume); -    if (pa_alsa_path_set_volume(u->mixer_path, u->mixer_handle, &s->channel_map, &r) < 0) +    if (pa_alsa_path_set_volume(u->mixer_path, u->mixer_handle, &s->channel_map, &r, TRUE) < 0)          return;      /* Shift down by the base volume, so that 0dB becomes maximum volume */ diff --git a/src/modules/alsa/module-alsa-card.c b/src/modules/alsa/module-alsa-card.c index 37b5a174..ebd2f8ae 100644 --- a/src/modules/alsa/module-alsa-card.c +++ b/src/modules/alsa/module-alsa-card.c @@ -64,7 +64,8 @@ PA_MODULE_USAGE(          "tsched_buffer_size=<buffer size when using timer based scheduling> "          "tsched_buffer_watermark=<lower fill watermark> "          "profile=<profile name> " -        "ignore_dB=<ignore dB information from the device?>"); +        "ignore_dB=<ignore dB information from the device?> " +        "sync_volume=<syncronize sw and hw voluchanges in IO-thread?>");  static const char* const valid_modargs[] = {      "name", @@ -86,6 +87,7 @@ static const char* const valid_modargs[] = {      "tsched_buffer_watermark",      "profile",      "ignore_dB", +    "sync_volume",      NULL  }; diff --git a/src/modules/alsa/module-alsa-sink.c b/src/modules/alsa/module-alsa-sink.c index a73274fd..697fab45 100644 --- a/src/modules/alsa/module-alsa-sink.c +++ b/src/modules/alsa/module-alsa-sink.c @@ -54,8 +54,11 @@ PA_MODULE_USAGE(          "tsched_buffer_size=<buffer size when using timer based scheduling> "          "tsched_buffer_watermark=<lower fill watermark> "          "ignore_dB=<ignore dB information from the device?> " -        "control=<name of mixer control>" -        "rewind_safeguard=<number of bytes that cannot be rewound"); +        "control=<name of mixer control> " +        "rewind_safeguard=<number of bytes that cannot be rewound> " +        "sync_volume=<syncronize sw and hw voluchanges in IO-thread?> " +        "sync_volume_safety_margin=<usec adjustment depending on volume direction> " +        "sync_volume_extra_delay=<usec adjustment to HW volume changes>");  static const char* const valid_modargs[] = {      "name", @@ -76,6 +79,9 @@ static const char* const valid_modargs[] = {      "ignore_dB",      "control",      "rewind_safeguard", +    "sync_volume", +    "sync_volume_safety_margin", +    "sync_volume_extra_delay",      NULL  };  | 
