diff --git a/forked-daapd.conf.in b/forked-daapd.conf.in index f3628811..ecdeacbe 100644 --- a/forked-daapd.conf.in +++ b/forked-daapd.conf.in @@ -217,12 +217,23 @@ audio { # If not set, the value for "card" will be used. # mixer_device = "" - # Synchronization - # If your local audio is out of sync with other speakers, e.g. Airplay, - # adjust this value. Negative values correspond to moving local audio - # ahead, positive correspond to delaying it. The unit is milliseconds. - # The offset must be between -1000 and 1000 (+/- 1 sec). + # Enable or disable audio resampling to keep local audio in sync with + # e.g. Airplay. This feature relies on accurate ALSA measurements of + # delay, and some devices don't provide that. If that is the case you + # are better off disabling the feature. +# sync_disable = false + + # Here you can adjust when local audio is started relative to other + # speakers, e.g. Airplay. Negative values correspond to moving local + # audio ahead, positive correspond to delaying it. The unit is + # milliseconds. The offset must be between -1000 and 1000 (+/- 1 sec). # offset_ms = 0 + + # To calculate what and if resampling is required, local audio delay is + # measured each second. After a period the collected measurements are + # used to estimate drift and latency, which determines if corrections + # are required. This setting sets the length of that period in seconds. +# adjust_period_seconds = 100 } # Pipe output diff --git a/src/conffile.c b/src/conffile.c index 19aaae92..42a26040 100644 --- a/src/conffile.c +++ b/src/conffile.c @@ -115,8 +115,10 @@ static cfg_opt_t sec_audio[] = CFG_STR("card", "default", CFGF_NONE), CFG_STR("mixer", NULL, CFGF_NONE), CFG_STR("mixer_device", NULL, CFGF_NONE), + CFG_BOOL("sync_disable", cfg_false, CFGF_NONE), CFG_INT("offset", 0, CFGF_NONE), // deprecated CFG_INT("offset_ms", 0, CFGF_NONE), + CFG_INT("adjust_period_seconds", 100, CFGF_NONE), CFG_END() }; diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c index b5952930..831603d5 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -49,9 +49,6 @@ // if r2 is below this value we won't attempt to correct sync. #define ALSA_MAX_VARIANCE 0.2 -// How many latency calculations we keep in the latency_history buffer -#define ALSA_LATENCY_HISTORY_SIZE 100 - // We correct latency by adjusting the sample rate in steps. However, if the // latency keeps drifting we give up after reaching this step. #define ALSA_RESAMPLE_STEP_MAX 8 @@ -100,7 +97,7 @@ struct alsa_session // Array of latency calculations, where latency_counter tells how many are // currently in the array - double latency_history[ALSA_LATENCY_HISTORY_SIZE]; + double *latency_history; int latency_counter; int sync_resample_step; @@ -123,6 +120,9 @@ struct alsa_session static struct alsa_session *sessions; +static bool alsa_sync_disable; +static int alsa_latency_history_size; + // We will try to play the music with the source quality, but if the card // doesn't support that we resample to the fallback quality static struct media_quality alsa_fallback_quality = { 44100, 16, 2 }; @@ -636,20 +636,20 @@ sync_check(double *drift, double *latency, struct alsa_session *as, snd_pcm_sfra exp_pos = (uint64_t)elapsed * as->quality.sample_rate / 1000; diff = cur_pos - exp_pos; - DPRINTF(E_DBG, L_LAUDIO, "counter %d/%d, stamp %lu:%lu, now %lu:%lu, elapsed is %d ms, cur_pos=%" PRIu64 ", exp_pos=%" PRIu64 ", diff=%d\n", - as->latency_counter, ALSA_LATENCY_HISTORY_SIZE, as->stamp_pts.tv_sec, as->stamp_pts.tv_nsec / 1000000, ts.tv_sec, ts.tv_nsec / 1000000, elapsed, cur_pos, exp_pos, diff); + DPRINTF(E_SPAM, L_LAUDIO, "counter %d/%d, stamp %lu:%lu, now %lu:%lu, elapsed is %d ms, cur_pos=%" PRIu64 ", exp_pos=%" PRIu64 ", diff=%d\n", + as->latency_counter, alsa_latency_history_size, as->stamp_pts.tv_sec, as->stamp_pts.tv_nsec / 1000000, ts.tv_sec, ts.tv_nsec / 1000000, elapsed, cur_pos, exp_pos, diff); // Add the latency to our measurement history as->latency_history[as->latency_counter] = (double)diff; as->latency_counter++; // Haven't collected enough samples for sync evaluation yet, so just return - if (as->latency_counter < ALSA_LATENCY_HISTORY_SIZE) + if (as->latency_counter < alsa_latency_history_size) return ALSA_SYNC_OK; as->latency_counter = 0; - ret = linear_regression(drift, latency, &r2, NULL, as->latency_history, ALSA_LATENCY_HISTORY_SIZE); + ret = linear_regression(drift, latency, &r2, NULL, as->latency_history, alsa_latency_history_size); if (ret < 0) { DPRINTF(E_WARN, L_LAUDIO, "Linear regression of collected latency samples failed\n"); @@ -657,7 +657,7 @@ sync_check(double *drift, double *latency, struct alsa_session *as, snd_pcm_sfra } // Set *latency to the "average" within the period - *latency = (*drift) * ALSA_LATENCY_HISTORY_SIZE / 2 + (*latency); + *latency = (*drift) * alsa_latency_history_size / 2 + (*latency); if (abs(*latency) < ALSA_MAX_LATENCY && abs(*drift) < ALSA_MAX_DRIFT) sync = ALSA_SYNC_OK; // If both latency and drift are within thresholds -> no action @@ -757,7 +757,7 @@ playback_write(struct alsa_session *as, struct output_buffer *obuf) } // Check sync each second (or if this is first write where last_pts is zero) - if (obuf->pts.tv_sec != as->last_pts.tv_sec) + if (!alsa_sync_disable && (obuf->pts.tv_sec != as->last_pts.tv_sec)) { ret = snd_pcm_delay(as->hdl, &delay); if (ret == 0) @@ -874,6 +874,8 @@ alsa_session_make(struct output_device *device, int callback_id) as->offset_ms = 1000 * (as->offset_ms/abs(as->offset_ms)); } + CHECK_NULL(L_LAUDIO, as->latency_history = calloc(alsa_latency_history_size, sizeof(double))); + snd_pcm_status_malloc(&as->pcm_status); ret = device_open(as); @@ -1065,6 +1067,9 @@ alsa_init(void) if (type && (strcasecmp(type, "alsa") != 0)) return -1; + alsa_sync_disable = cfg_getbool(cfg_audio, "sync_disable"); + alsa_latency_history_size = cfg_getint(cfg_audio, "adjust_period_seconds"); + CHECK_NULL(L_LAUDIO, device = calloc(1, sizeof(struct output_device))); device->id = 0;