libobs: guard against lagging audio sources
masterdf4eb82
fixed a bug that caused source audio timestamps to perpetually lag. However, there is a deeper issue where after we reach max buffering, lagging sources make OBS's entire audio pipeline fall over. These may be corrected by later code, but still cause global audio glitches at best. Persistent problems, as prior todf4eb82
, cause audio to fail entirely. The root cause is that OBS's audio mixing tree cannot deal with timestamps prior to the current audio tick. Intermediate mixing stages assume that the lowest incoming timestamp is the base of the current tick, and mix accordingly. This propagates lagged timestamps up the tree, where at the top level mix_audio will drop the source entirely - which at this point is a transition covering all inputs, thus glitching audio globally. Where extra buffering can cover the slip, the entire mix gets retried and the error corrected, but when the global buffer duration is maxed out, it makes it to the output. The solution is to catch laggy sources immediately after rendering, and drop audio to bring them back in sync, or mark them pending if not enough audio is available. This ensures later mixing stages are not fed with out of sync timestamps. This improves the ignore_audio code to only drop as much audio as needed to bring the source back in sync, and moves its call to immediately after source audio rendering.
parent
3191f5ddb0
commit
31a9dc384d
|
@ -25,6 +25,7 @@ struct ts_info {
|
|||
};
|
||||
|
||||
#define DEBUG_AUDIO 0
|
||||
#define DEBUG_LAGGED_AUDIO 0
|
||||
#define MAX_BUFFERING_TICKS 45
|
||||
|
||||
static void push_audio_tree(obs_source_t *parent, obs_source_t *source, void *p)
|
||||
|
@ -80,20 +81,74 @@ static inline void mix_audio(struct audio_output_data *mixes,
|
|||
}
|
||||
}
|
||||
|
||||
static void ignore_audio(obs_source_t *source, size_t channels,
|
||||
size_t sample_rate)
|
||||
static bool ignore_audio(obs_source_t *source, size_t channels,
|
||||
size_t sample_rate, uint64_t start_ts)
|
||||
{
|
||||
size_t num_floats = source->audio_input_buf[0].size / sizeof(float);
|
||||
const char *name = obs_source_get_name(source);
|
||||
|
||||
if (num_floats) {
|
||||
if (!source->audio_ts && num_floats) {
|
||||
#if DEBUG_LAGGED_AUDIO == 1
|
||||
blog(LOG_DEBUG, "[src: %s] no timestamp, but audio available?",
|
||||
name);
|
||||
#endif
|
||||
for (size_t ch = 0; ch < channels; ch++)
|
||||
circlebuf_pop_front(&source->audio_input_buf[ch], NULL,
|
||||
source->audio_input_buf[ch].size);
|
||||
source->audio_input_buf[0].size);
|
||||
source->last_audio_input_buf_size = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (num_floats) {
|
||||
/* round up the number of samples to drop */
|
||||
size_t drop = util_mul_div64(start_ts - source->audio_ts - 1,
|
||||
sample_rate, 1000000000ULL) +
|
||||
1;
|
||||
if (drop > num_floats)
|
||||
drop = num_floats;
|
||||
|
||||
#if DEBUG_LAGGED_AUDIO == 1
|
||||
blog(LOG_DEBUG,
|
||||
"[src: %s] ignored %" PRIu64 "/%" PRIu64 " samples", name,
|
||||
(uint64_t)drop, (uint64_t)num_floats);
|
||||
#endif
|
||||
for (size_t ch = 0; ch < channels; ch++)
|
||||
circlebuf_pop_front(&source->audio_input_buf[ch], NULL,
|
||||
drop * sizeof(float));
|
||||
|
||||
source->last_audio_input_buf_size = 0;
|
||||
source->audio_ts +=
|
||||
util_mul_div64(num_floats, 1000000000ULL, sample_rate);
|
||||
util_mul_div64(drop, 1000000000ULL, sample_rate);
|
||||
blog(LOG_DEBUG, "[src: %s] ts lag after ignoring: %" PRIu64,
|
||||
name, start_ts - source->audio_ts);
|
||||
|
||||
/* rounding error, adjust */
|
||||
if (source->audio_ts == (start_ts - 1))
|
||||
source->audio_ts = start_ts;
|
||||
|
||||
/* source is back in sync */
|
||||
if (source->audio_ts >= start_ts)
|
||||
return true;
|
||||
} else {
|
||||
#if DEBUG_LAGGED_AUDIO == 1
|
||||
blog(LOG_DEBUG, "[src: %s] no samples to ignore! ts = %" PRIu64,
|
||||
name, source->audio_ts);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (!source->audio_pending || num_floats) {
|
||||
blog(LOG_WARNING,
|
||||
"Source %s audio is lagging (over by %.02f ms) "
|
||||
"at max audio buffering. Restarting source audio.",
|
||||
name, (start_ts - source->audio_ts) / 1000000.);
|
||||
}
|
||||
|
||||
source->audio_pending = true;
|
||||
source->audio_ts = 0;
|
||||
/* tell the timestamp adjustment code in source_output_audio_data to
|
||||
* reset everything, and hopefully fix the timestamps */
|
||||
source->timing_set = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool discard_if_stopped(obs_source_t *source, size_t channels)
|
||||
|
@ -145,6 +200,8 @@ static inline void discard_audio(struct obs_core_audio *audio,
|
|||
{
|
||||
size_t total_floats = AUDIO_OUTPUT_FRAMES;
|
||||
size_t size;
|
||||
/* debug assert only */
|
||||
UNUSED_PARAMETER(audio);
|
||||
|
||||
#if DEBUG_AUDIO == 1
|
||||
bool is_audio_source = source->info.output_flags & OBS_SOURCE_AUDIO;
|
||||
|
@ -180,9 +237,13 @@ static inline void discard_audio(struct obs_core_audio *audio,
|
|||
"start timestamp (%" PRIu64 ")",
|
||||
source->audio_ts, ts->start);
|
||||
}
|
||||
|
||||
/* ignore_audio should have already run and marked this source
|
||||
* pending, unless we *just* added buffering */
|
||||
assert(audio->total_buffering_ticks < MAX_BUFFERING_TICKS ||
|
||||
source->audio_pending || !source->audio_ts ||
|
||||
audio->buffering_wait_ticks);
|
||||
#endif
|
||||
if (audio->total_buffering_ticks == MAX_BUFFERING_TICKS)
|
||||
ignore_audio(source, channels, sample_rate);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -436,6 +497,38 @@ bool audio_callback(void *param, uint64_t start_ts_in, uint64_t end_ts_in,
|
|||
obs_source_t *source = audio->render_order.array[i];
|
||||
obs_source_audio_render(source, mixers, channels, sample_rate,
|
||||
audio_size);
|
||||
|
||||
/* if a source has gone backward in time and we can no
|
||||
* longer buffer, drop some or all of its audio */
|
||||
if (audio->total_buffering_ticks == MAX_BUFFERING_TICKS &&
|
||||
source->audio_ts < ts.start) {
|
||||
if (source->info.audio_render) {
|
||||
blog(LOG_DEBUG,
|
||||
"render audio source %s timestamp has "
|
||||
"gone backwards",
|
||||
obs_source_get_name(source));
|
||||
|
||||
/* just avoid further damage */
|
||||
source->audio_pending = true;
|
||||
#if DEBUG_AUDIO == 1
|
||||
/* this should really be fixed */
|
||||
assert(false);
|
||||
#endif
|
||||
} else {
|
||||
pthread_mutex_lock(&source->audio_buf_mutex);
|
||||
bool rerender = ignore_audio(source, channels,
|
||||
sample_rate,
|
||||
ts.start);
|
||||
pthread_mutex_unlock(&source->audio_buf_mutex);
|
||||
|
||||
/* if we (potentially) recovered, re-render */
|
||||
if (rerender)
|
||||
obs_source_audio_render(source, mixers,
|
||||
channels,
|
||||
sample_rate,
|
||||
audio_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------ */
|
||||
|
|
Loading…
Reference in New Issue