Skip to content

Comments

libobs: Implement asynchronous sample rate conversion for audio#6351

Draft
norihiro wants to merge 8 commits intoobsproject:masterfrom
norihiro:async-sample-rate-conversion
Draft

libobs: Implement asynchronous sample rate conversion for audio#6351
norihiro wants to merge 8 commits intoobsproject:masterfrom
norihiro:async-sample-rate-conversion

Conversation

@norihiro
Copy link
Contributor

@norihiro norihiro commented Apr 18, 2022

Description

This PR implements asynchronous sample-rate conversion by utilizing a soft compensation feature in libswresample. A digital approach of asynchronous sample-rate conversion was proposed in 1, a PDF is available here. Also described in 2. The soft compensation in libswresample uses similar technique as 1. In addition, a traditional lag-lead filter is implemented to eliminate errors at steady state.

As described in the description of the PR #6133, next_audio_ts_min should be same as data->timestamp + conv_frames_to_time(sample_rate, in.frames). However, just overwriting next_audio_ts_min cannot compensate insufficient or too many number of samples. Instead, the difference between these values will be fed to the soft compensation.

A lag-lead filter is widely used in a conventional PLL design. In the feedback loop, the lag-lead filter is employeed for these purposes.

  • to eliminate errors at the steady state
  • to suppress jitter induced by fluctuations such as CPU workload and USB transfer.

The unity gain frequency of the feedback loop is set to 1/180 Hz so that the fluctuations faster than 3 minutes will be suppressed
and I believe users won't notice transition at the startup.

This PR also changes built-in plugins to enable the feature by default.

  • linux-pulseaudio
  • decklink (implemented but not tested)
  • aja
  • mac-capture (implemented but not tested)
  • win-wasapi
  • win-dshow
  • linux-alsa
  • linux-jack
  • oss-audio
  • sndio
  • obs-ffmpeg-source and vlc-video (for realtime streaming) It's not easy to implement since the flow is controlled in the thread mp_media_thread.

Note: Though the implementation looks working, I'd like to clean the code to make it more readable.

Motivation and Context

Since most of audio sources are asynchronous,
the number of samples provided from the audio sources is sometimes insufficient or too many,
which causes audio glitch as #4600 and audio/video desync.

Fix #4600.

How Has This Been Tested?

The code is tested on Fedora 34 with MOTU M2 and recording a tone signal.

Also analyzed step response and frequency response of the feedback system with my SPICE model.

obs-async-audio-filter-ac
Frequency response of the open loop transfer function. Gain curve crosses 0 dB at 5.55 mHz (inverse of 3 minutes) and shows suppression for higher frequency components to eliminate fluctuation such as USB and load of the host. At 5.55 mHz, the phase curve shows 60 degree phase margin so that the feedback system is stable.

async-sample-rate-conversion-step-response
Simulated and measured step response of the system. This figure shows step response how the compensation is applied (vc1) and another internal node in the lag-lead filter (vc2), measured with MOTU M2 with Fedora 34. It estimated roughly 4.5 samples are are missing for every second. The implementation gradually started to add samples and reached maximum compensation -5.3 samples per second after 80 seconds from the start, then goes to the steady state compensating 4.5 samples per second. The other node vc2 slowly track vc1 so that it eliminates offset at the steady state. The curves of measured and simulated model are well matched except between 100 seconds to 300 seconds. I think we can rely on the model so that the gain and phase curves are well simulated and conclude the feedback system has enough stability.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

Checklist:

  • My code has been run through clang-format.
  • I have read the contributing document.
  • My code is not on the master branch.
  • The code has been tested.
  • All commit messages are properly formatted and commits squashed where appropriate.
  • I have included updates to all appropriate documentation.

@norihiro norihiro force-pushed the async-sample-rate-conversion branch from 54a2b75 to aa38840 Compare April 18, 2022 16:09
@norihiro norihiro marked this pull request as draft April 18, 2022 16:10
@cg2121 cg2121 added Bug Fix Non-breaking change which fixes an issue Enhancement Improvement to existing functionality labels Apr 18, 2022
@norihiro norihiro force-pushed the async-sample-rate-conversion branch 4 times, most recently from 9eeb15e to ec47a17 Compare April 20, 2022 15:01
@digitalchou
Copy link

Hi, if you need a macos intel tester Ill be glad!! Figthing with this bug till i bought a new Maudio Mtrack DUO 48kHz soundcard, but it sound a little worse than mi UMC404-192kHz one(wich dont exactly match the clock in 48kHz mode and drops a few ms of audio every 2:20 min aprox)
Thanks for your work, your source color minitor is one of the best obs plugins ever!!

@jp9000 jp9000 added the Seeking Testers Build artifacts on CI label Apr 21, 2022
@norihiro norihiro force-pushed the async-sample-rate-conversion branch from f5aec3a to c0392ec Compare April 21, 2022 02:08
@norihiro
Copy link
Contributor Author

Hi, if you need a macos intel tester Ill be glad!!

I appreciate your comment. You can download a package for macOS by this instruction. Since the code is not signed and notarized, you might need to adjust security settings on your macOS.
With this package, you will see an option Enable Asynchronous Compensation for Audio Input Capture.

@norihiro norihiro force-pushed the async-sample-rate-conversion branch 2 times, most recently from 3821723 to 0a36bd0 Compare April 27, 2022 08:40
@tt2468
Copy link
Member

tt2468 commented Jun 26, 2022

Other than for testing purposes, is there any reason to want to have async compensation be a configurable value in sources? To me it seems like it should always be on unless there is a good reason to want it off.

@norihiro
Copy link
Contributor Author

norihiro commented Jun 26, 2022

Other than for testing purposes, is there any reason to want to have async compensation be a configurable value in sources? To me it seems like it should always be on unless there is a good reason to want it off.

It's a good point.

  • I want to provide the option to revert to the old behavior just in case it works wrong.
  • If async-SRC is enabled, a low-pass filter is conformed to cut off very high frequency. Someone might want to avoid the low-pass filter instead of avoiding glitches.
  • Some audio interfaces support genlock. If a user somehow do genlock to OBS, async-SRC is unnecessary.

Anyway, libobs need to have the switch to have async-SRC. Obviously, media source and VLC source have to disable the async-SRC.

Btw, so far I have one thing I need to work on. When I turn off async-SRC, I guess the audio resampler is still running even though it's unnecessary. Other behavior looks fine for me but I don't know the owner of this project is happy to have this PR. Edited: this is fixed.

@norihiro norihiro force-pushed the async-sample-rate-conversion branch from 0a36bd0 to 3d37efd Compare July 15, 2022 10:28
@norihiro norihiro marked this pull request as ready for review July 16, 2022 15:54
@tt2468
Copy link
Member

tt2468 commented Jul 25, 2022

Other than for testing purposes, is there any reason to want to have async compensation be a configurable value in sources? To me it seems like it should always be on unless there is a good reason to want it off.

It's a good point.

  • I want to provide the option to revert to the old behavior just in case it works wrong.
  • If async-SRC is enabled, a low-pass filter is conformed to cut off very high frequency. Someone might want to avoid the low-pass filter instead of avoiding glitches.
  • Some audio interfaces support genlock. If a user somehow do genlock to OBS, async-SRC is unnecessary.

Anyway, libobs need to have the switch to have async-SRC. Obviously, media source and VLC source have to disable the async-SRC.

Btw, so far I have one thing I need to work on. When I turn off async-SRC, I guess the audio resampler is still running even though it's unnecessary. Other behavior looks fine for me but I don't know the owner of this project is happy to have this PR. Edited: this is fixed.

Seems like a UI chat question. If every source should be able to enable/disable conversion, then perhaps it should not be up to the source itself and instead be configured through the advanced audio dialog. Or, if that's too complicated still for users, we just make more assumptions and handle the rest inside of obs-audio.

@norihiro norihiro force-pushed the async-sample-rate-conversion branch from 3d37efd to 55d57fc Compare July 26, 2022 01:22
@norihiro
Copy link
Contributor Author

The reason I set default to enable is based on my assumption that most users won't have genlock and not care audio. The option should be disabled by professional users who have genlock outside obs for example.

This figure shows how the compensation is working for long period, measured on Fedora 35 with MOTU M2, 48000 Hz. The compensation becomes steady state after around 300 seconds has passed.
a

Looking at the behavior after 300 seconds have passed, I think the compensation is stable enough. Cumulative probability of the compensation value is shown in the figure below. Its standard deviation is 0.097 S/s. 99% points are within 0.50 S/s range. The minimum is -4.86 S/s, the maximum is -4.19 S/s. Giving a 442 Hz tone, pitch variation will be between 441.9966 Hz and 442.0027 Hz. I don't think even well-trained musicians can notice.
b

@norihiro norihiro force-pushed the async-sample-rate-conversion branch from 55d57fc to e78a06f Compare July 26, 2022 03:20
@norihiro norihiro force-pushed the async-sample-rate-conversion branch 3 times, most recently from c906af2 to 4caef71 Compare September 27, 2022 15:58
@WizardCM WizardCM added this to the OBS Studio 29.1 milestone Dec 10, 2022
@norihiro norihiro force-pushed the async-sample-rate-conversion branch from 4caef71 to 57b3fcc Compare December 20, 2022 22:54
@norihiro norihiro force-pushed the async-sample-rate-conversion branch from 3e3bb30 to 2c9f064 Compare April 13, 2024 02:20
norihiro added a commit to norihiro/obs-h8819-source that referenced this pull request Apr 13, 2024
This commit implements a property to enable asynchronous sample rate
conversion (SRC) in the PR obsproject/obs-studio#6351.
This commit relies on the PR so that the implementation is disabled by
default.
Assuming the user of this plugin has enough knowledge for asynchronous
sample rate and genlock, the option is disabled by default.
norihiro added a commit to norihiro/obs-h8819-source that referenced this pull request Apr 22, 2024
This commit implements a property to enable asynchronous sample rate
conversion (SRC) in the PR obsproject/obs-studio#6351.
This commit relies on the PR so that the implementation is disabled by
default.
Assuming the user of this plugin has enough knowledge for asynchronous
sample rate and genlock, the option is disabled by default.
@zbynekdrlik
Copy link

When we could test it?

@norihiro norihiro force-pushed the async-sample-rate-conversion branch from 2c9f064 to c705ee8 Compare October 7, 2024 16:01
@norihiro
Copy link
Contributor Author

norihiro commented Oct 7, 2024

Rebased and the build flow has passed. You can test it from the summary page.

@moonlightes
Copy link

Hi. Is this change missing in the current version of obs 31? If so, is it expected to be added soon?

@Fenrirthviti
Copy link
Member

This has not been merged yet, so it is not missing from any release.

We prefer not to provide estimations on PR reviews as the project priorities are constantly in flux.

Copy link
Member

@pkviet pkviet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code is very clean and well written.
I appreciated the explanations with reference to the research behind the compensation filter.
As discussed in another PR, FFmpeg has the aresample filter which has both hard and soft compensation. Can you summarize the differences and see if this can suggest improvements to your current implementation ?
Also could this filter help solve the desync issues with all audio outputs (chiefly, decklink and ndi)
(currently there is a low latency audio buffering option which sets oai->max_buffering_ms to 20 ms and triggers :
https://github.com/obsproject/obs-studio/blob/master/libobs/obs.c#L1612
and following)
Sidenote: please rebase the PR

#endif

async_compensation = obs_data_get_bool(settings, "async_compensation");
obs_source_set_async_compensation(data->source, async_compensation);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in theory this is a no-op, if async_compensation value has not changed. So it's OK.
But in general, we store source settings as members of the base data struct, and update it only if its value has changed, so this saves a call. I will readily agree that here it does not make much difference though.
(same remark for other sources where you followed the same pattern)

lag_lead_filter_tick(&rs->compensation_filter, rs->input_freq, in_frames);
double drift = lag_lead_filter_get_drift(&rs->compensation_filter);

ret = swr_set_compensation(rs->context, -(int)(drift * 65536 / rs->input_freq), 65536);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where does that figure come from ?

if (!rs->compensation_filter_configured) {
// Parameters are determined experimentally by SPICE simulation
// to have these characteristics.
// - Unity gain frequency: 1/180 Hz (inverse of 3 minutes)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you experiment with shorter and longer times ? say 1 min so 1/60 Hz and 5 min so 1/300 Hz ?
Regarding this curve:
https://user-images.githubusercontent.com/780600/180902267-e95694da-7aed-46c2-839b-36c5fe097f15.png

how is it modified ?
Is the sample/sec negative compensation due to input lag ? have you tested the reverse condition (through a simulation for instance)
More generally, can you explain why the filter converges to a plateau ? it seems to mean that the input source is intrinsically lagging if the filter must always compensate ?

@norihiro norihiro marked this pull request as draft February 18, 2026 05:23
@norihiro norihiro force-pushed the async-sample-rate-conversion branch from c705ee8 to 6ab3caa Compare February 18, 2026 05:25
An API `audio_resampler_compensate` is added that activates resampling
compensation by calling `swr_set_compensation`.
@norihiro
Copy link
Contributor Author

Rebased to resolve conflicts.

@norihiro norihiro force-pushed the async-sample-rate-conversion branch 2 times, most recently from 7be8ff5 to 13cf845 Compare February 18, 2026 05:43
This commit implements asynchronous sample-rate conversion for audio
source.
Since the audio from most sound devices are asynchronous, it requires to
be compensated so that the audio source provides exactly necessary
amount of samples.
The option is enabled by default.
Also update linux-alsa.
When `use_device_timing` is not set, the audio from the device is
asynchronous. It requires to be compensated to avoid audio glitch.
The option is masked if `use_device_timing` is set.
The option is enabled by default for wasapi_input_capture.
Since the audio from most sound devices are asynchronous, it requires to
be compensated so that the audio source provides exactly necessary
amount of samples.
The option is enabled by default.
Since the audio from the device is asynchronous, it requires to be
compensated so that the audio source provides exactly necessary amount
of samples.
The option is enabled by default.
Since the audio from the device is asynchronous, it requires to be
compensated so that the audio source provides exactly necessary amount
of samples.
The option is enabled by default.
@norihiro norihiro force-pushed the async-sample-rate-conversion branch from 13cf845 to c88796e Compare February 18, 2026 05:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Fix Non-breaking change which fixes an issue Enhancement Improvement to existing functionality New Feature New feature or plugin Seeking Testers Build artifacts on CI UI/UX Anything to do with changes or additions to UI/UX elements.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Random short audio dropouts when recording or streaming