Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Real time limiter UI #6395

Merged
merged 8 commits into from
May 22, 2024
Merged

Conversation

saintmatthieu
Copy link
Contributor

@saintmatthieu saintmatthieu commented May 7, 2024

Resolves: #6305, #6366

This PR wraps Daniel Rudrich's SimpleCompressor in two new real-time capable built-in effects, namely "Real-time Compressor" and "Real-time Limiter".

The limiter is the same as the compressor but with a reduced set of parameters, ratio and attack time being hard-coded to infinity and 0ms.

The legacy, offline compressor files were renamed, but for now stays found in the same place in the menu (Volume and Compression > Compressor and Volume and Compression > Limiter). Removal from default list is planned in #6319. At that stage the new versions should also find their final place in the menu.

UI still misses the transfer function, which may be added to this PR or as follow-up.

  • I signed CLA
  • The title of the pull request describes an issue it addresses
  • If changes are extensive, then there is a sequence of easily reviewable commits
  • Each commit's message describes its purpose and effects
  • There are no behavior changes unnecessary for the stated purpose of the PR

Recommended:

  • Each commit compiles and runs on my machine without known undesirable changes of behavior

QA:

  • Changing parameters in real time works
  • Latency compensation works (use look-ahead for this to be non-trivial)
  • Effect preview works
    Channel routing tests:
  • Mono track is processed properly
  • Stereo track is processed properly (use a clip with contrasting left/right content)
  • Compression history only appears when used as real-time effect
  • Adding an instance on the real-time effect stack and rendering the track yields output that's bit-exact with applying the effect as destructive effect.

@@ -0,0 +1,143 @@
/*
Copy link
Collaborator

Choose a reason for hiding this comment

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

May be lib-src is better place for this library?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've been considering, yes, but I'll probably be modifying the code in some follow-up work. Is that a good reason?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Up to you. I think it's not a big deal if you modify code added to lib-src, I recall there are already such cases

std::make_shared<ParameterWrapper>(
releaseMs),
} } }
, mSettings { std::move(settings) }
Copy link
Collaborator

Choose a reason for hiding this comment

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

std::move is redundant here, better pass settings parameter by reference

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is it? The given settings object is originally a right value. If I passed by reference in the end there'd be a call to the copy ctor, wouldn't there?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Not the case with POD-types I guess. It will simply create an extra copy.

services,
access,
{ {
mSettings.inCompressionThreshDb,
Copy link
Collaborator

Choose a reason for hiding this comment

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

mSettings is not initialized at this point. While in that particular case nothing bad will happen because CompressorSettings is trivially constructible, I'd still suggested to move initialization into constructor's body.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well spot, thanks.

wxWindow* parent, const DynamicRangeProcessorHistoryUPtr& history,
DynamicRangeProcessorOutputs& outputs, EffectSettingsAccess& access)
{
const CompressorSettings* settings = access.Get().cast<CompressorSettings>();
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't understand why should LimiterSettings inherit CompressorSettings. I really do not see any benefits from doing so, but I see downsides in many if(...) else { ... } and typechecks. I'd suggested to eliminate LimiterSettings type, but instead introduce:

static constexpr DefaultCompressorSettings = CompressorSettings {...};
static constexpr DefaultLimiterSettings = CompressorSettings {...};

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure that this would work - not that I wouldn't like it to!
I think it's because of this guy:
image
The EffectSettingsAccess class initially creates a default-constructed struct. Creating a new type is the only way I found of circumventing that.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hm, didn't think of it. EffectWithSettings<>::MakeSettings is virtual and can be overriden, will that help?

@saintmatthieu saintmatthieu force-pushed the real-time-limiter-ui branch 2 times, most recently from dc3fc0f to 85e0f9b Compare May 14, 2024 15:22
@dozzzzer
Copy link
Contributor

@saintmatthieu

When changing parameters during playback, abrupt volume changes of high amplitude occur:

(Warning: it may get loud)

Screen.Recording.2024-05-14.at.17.54.40.video-converter.com.mp4

@saintmatthieu
Copy link
Contributor Author

@petersampsonaudacity was reporting this here for a chirp and with other parameters, too.
I expect this to happen. We do not have any kind of parameter change smoothing in place, so it does so in steps. In this particular case, you are changine the threshold for the limiter, which is like changing the input gain.

@petersampsonaudacity
Copy link

petersampsonaudacity commented May 15, 2024

Testing on W11 with @saintmatthieu 's latest branch build: audacity-win-3.6.0-alpha-20240514+2d767ac-x64

This shows that with both the new effects the Presets and Settings fail to work:
a) the Factory Presets/Default fails to restore the default settings,
b) whilst the user can set and recall a user preset, these also fail to apply when invoked.

This is technically a regression on 3.5.1 behavior with the old, replaced, effects.

[this](AudioIOEvent evt) {
if (evt.on)
ClearHistory();
, mInitializeProcessingSettingsSubscription { instance.Subscribe(
Copy link
Collaborator

Choose a reason for hiding this comment

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

instance will outlive editor, so that should be safe

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And if it didn't, would there be harm in a dangling subscription ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

No, it stores pointer in weak_ptr, should be safe anyway

// empirically, but with a relatively large audio playback delay. Maybe it
// will be found to lag on lower-latency playbacks. Best would probably be to
// make it playback-delay dependent.
constexpr auto displayDelay = 0.2f;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it possibly depend on ring buffer size (which is usually 0.25ms)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The size of the low-latency playback thread buffer you mean? Yes.

prev = it;
}
if (std::unique_ptr<wxGraphicsContext> gc { wxGraphicsContext::Create(dc) })
DrawHistory<wxPoint2DDouble>(*gc, GetSize(), history, *mSync);
Copy link
Collaborator

Choose a reason for hiding this comment

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

wxPoint2DDouble makes graph move smoother?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, it's more for anti-aliasing. Will comment.

// Process trivially
for (size_t ii = 0; ii < chans; ++ii)
memcpy(outbuf[ii], inbuf[ii], numSamples * sizeof(float));
if (pInstance)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess it will be better to always call RealtimeProcess, but instead effects could implement bypass mode, so that effects could accumulate samples if it's required for processing. For VST's that should be done trivially. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We'd then have to review all implementations and make sure none of the actually does the processing when bypassed. So I think this is safer.

class CompressorInstance final :
public PerTrackEffect::Instance,
public EffectInstanceWithBlockSize,
public Observer::Publisher<std::optional<InitializeProcessingSettings>>
public InitializeProcessingSettingsPublisher,
Copy link
Collaborator

Choose a reason for hiding this comment

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

CompressorInstance could remember it's state after init/suspend/resume/finalize, OnTimer then could check that state. I have no doubt that observers will do the thing, I'm just a bit sceptical about effect instance being a publisher.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The sample rate may change between playback states, and all this time the panel may stay open, so the sample rate update is a must.
I tried other things before, and the observer-publisher pattern seemed like the simplest in the end (although there is no fundamental difference between injecting a callback lambda, I think).

@saintmatthieu
Copy link
Contributor Author

Testing on W11 with @saintmatthieu 's latest branch build: audacity-win-3.6.0-alpha-20240514+2d767ac-x64

This shows that with both the new effects the Presets and Settings fail to work: a) the Factory Presets/Default fails to restore the default settings, b) whilst the user can set and recall a user preset, these also fail to apply when invoked.

This is technically a regression on 3.5.1 behavior with the old, replaced, effects.

@petersampsonaudacity thanks for your report!
On my side it's also buggy, but less so: changing an effect parameter by applying a preset does not update the slider positions. Everything else works, though.
Could you double-check if this is consistent with your experience?
Fix commit pushed.

@petersampsonaudacity
Copy link

petersampsonaudacity commented May 16, 2024

On my side it's also buggy, but less so: changing an effect parameter by applying a preset does not update the slider positions.

@saintmatthieu testing on W11 with your two latest branch builds;
audacity-win-3.6.0-alpha-20240516+b7b392b-x64 and
audacity-win-3.6.0-alpha-20240516+13bcfb5-x64

I confirm that
a) the default slider settings get restored (slider positions) with the use of the Factory >default
b) the user can set a user preset and when recalled it restores the users slider positions

So looks like this works properly now.

Everything else works, though.
Could you double-check if this is consistent with your experience?

I can't really vouch for the actual accuracy in use of compressor or limirter as I have no real experience of using such effects - you might want to approach @SteveDaulton for that.


One thing I do note is that the dialogs for the new replacement effects are narrower than the ones they replace - the upshot being that the sliders are narrower than previously.

This can be mitigated by expanded the dialog rightwards with click&drag on its right edge
BUT I am un-convinced that this will be readily "discoverable" by many users.

@SteveDaulton
Copy link
Member

Am I testing the correct version? I am using Audacity 3.6.0-alpha commit ID: 4e4fdb but the new Limiter does not behave like any other limiter I've ever used.

@saintmatthieu
Copy link
Contributor Author

saintmatthieu commented May 16, 2024

That's the correct version. I'm listening :)

@SteveDaulton
Copy link
Member

An "ideal" (but impossible) limiter prevents the audio from going above a specified threshold level, without affecting audio below that level (other than optional makeup gain), and without introducing distortion.

Easiest to demonstrate with a generated test tone, though it also applies to real-world audio.
Here is a 10 second 2kHz sine tone, which fades up to a peak level of 0dB over 5 seconds, then fades back down to silence:

sine

After limiting to -5dB (chosen to match the new limiter default), I would expect the waveform to look similar to this:

hard-limit-5

Using the new Limiter with default settings, the result looks like this:

new-limiter

which, after normalizing to 0dB looks like this:

new-limiter-normalized

@saintmatthieu
Copy link
Contributor Author

Strange. I get this:
image
That was, like you, with default settings:
image
That looks correct to me.

@SteveDaulton
Copy link
Member

Strange. I get this:

That does not look like a sine wave. I'm guessing it is a square wave, but even so I don't get that result.
(I am using the Ubuntu 22.04 AppImage on Ubuntu 22.04)

@LWinterberg
Copy link
Member

LWinterberg commented May 16, 2024

I cannot reproduce your post-normalization case either.

The way this thing works is somewhat similar to the Waves L2, the Avid Pro Limiter and various other mastering limiters. The idea of these is that you have a ceiling (which we call "make-up target") which already would be a normalized output to whatever you set as your target - assuming material actually hits the threshold. For the case where you don't want any make-up, the target and threshold need to be identical.

@SteveDaulton
Copy link
Member

Also tested the same AppImage in a clean install of Debian in VirtualBox and I get the same result.

@saintmatthieu
Copy link
Contributor Author

Right, it was with a square wave. Now with a sine: image
@SteveDaulton any way you could try the exact same on another OS ? Looks like we have a problem on Linux.

@SteveDaulton
Copy link
Member

Curiously, on one attempt the effect did change the dynamics, but much more than it should have. I have not been able to repeat this.

When I try now, if I set the Threshold and Make-up Target to the same value, the effect does nothing (effectively a no-op).

@saintmatthieu
Copy link
Contributor Author

Thanks for your early report, @SteveDaulton , @dozzzzer has confirmed this. Looking into this.

@dozzzzer
Copy link
Contributor

dozzzzer commented May 17, 2024

(Ubuntu 22.04) I also see a wrong result after applying Limiter with the default parameters to a sine tone with fade in & fade out, although different from @SteveDaulton's one:

image

@saintmatthieu
Copy link
Contributor Author

I was using abs instead of std::abs, which on Linux takes an int argument ...
Fix pushed, please @SteveDaulton @dozzzzer would you give it another shot once the build is ready ?

@dozzzzer
Copy link
Contributor

When resizing the window during playback, the graph drawing stops until the LMB is released:

2024-05-17_11-38-35.mp4

@saintmatthieu
Copy link
Contributor Author

Yeah, I haven't found how to solve this. I don't get updates when resizing is under way (wxWidgets stuff). At best I could get the straight line to disappear. Then it'd look like when you bypass and un-bypass.

@saintmatthieu
Copy link
Contributor Author

@dozzzzer actually that was an easy fix. Please re-test when latest build is ready.

@SteveDaulton
Copy link
Member

Testing with Commit Id: d40c0e I am still seeing the same bug.

@saintmatthieu
Copy link
Contributor Author

I beg your pardon: I messed up force-pushing from different machines. 0d0f371 is the fix commit.

@petersampsonaudacity
Copy link

Testing on W11 with: audacity-win-3.6.0-alpha-20240517+d40c0e6-x64

Like @dozzzzer I get a break when the dilaog is resized until the LMB is released.

But unlike @dozzzzer my break is blank withou the joing line that he has:

image

@saintmatthieu
Copy link
Contributor Author

@petersampsonaudacity yes. The fact that you don't see a line is the improvement brought by 8c2b772. I don't think I can possibly get the data updates during resizing, as this seems to block the main thread, where the updates otherwise happen.

@petersampsonaudacity
Copy link

The fact that you don't see a line is the improvement ...

Yes, I think it's better with the blank rather than the line.

I don't think I can possibly get the data updates during resizing, as this seems to block the main thread, where the updates otherwise happen.

I'm pretty sure folk can live with that, resizing the dialog is not something most users will be doing a lot of.

@SteveDaulton
Copy link
Member

I beg your pardon: I messed up force-pushing from different machines. 0d0f371 is the fix commit.

OK, that one works, though it introduces a lot more distortion than the Nyquist limiter, even with the "Hard Limit" algorithm. The distortion is particularly noticeable on the 3rd harmonic.

@dozzzzer
Copy link
Contributor

During playback, when moving one of the following: Threshold, Attack or Release in the Compressor effect, it produces abrupt volume changes. The Limiter effect doesn't have such a problem anymore. Check this out:

2024-05-17_19-26-49.mp4

I think this is pretty serious, so I'd like this to be fixed within this PR. However, given the circumstances, I'd want to know what you guys think about it and whether we would want to address this in the next patch release @Tantacrul @LWinterberg

Also, @saintmatthieu I understand this isn't going to be an easy fix, is it?

@LWinterberg
Copy link
Member

Oof. Yeah that's pretty bad.

@saintmatthieu
Copy link
Contributor Author

saintmatthieu commented May 17, 2024

Thanks @dozzzzer, it is a bug you caught indeed.
Note that each time you change the release time, the graph shows the actual attenuation goes back to zero before it converges back towards the target, in a time lapse that I suspect equals your look-ahead (120ms).
The mistake I did was to reset the look-ahead state when any parameter gets changed. Fix was easy and is pushed. I hope it does the job.

@dozzzzer
Copy link
Contributor

@saintmatthieu I've discovered that the graph isn't being painted as long as the "Presets & Settings" menu is opened. And then when the menu is closed, a straight line is painted all over the gap. Could you fix that?

image

@DavidBailes
Copy link
Collaborator

@saintmatthieu . In the compressor dialog, the panel containing the transfer function doesn't need to be in the tab order.
cf Autoduck effect, where in Autoduck.h, the class definition for the panel it uses includes the following lines:

bool AcceptsFocus() const override { return false; }
// So that wxPanel is not included in Tab traversal - see wxWidgets bug 15581
bool AcceptsFocusFromKeyboard() const override { return false; }

@saintmatthieu
Copy link
Contributor Author

@saintmatthieu I've discovered that the graph isn't being painted as long as the "Presets & Settings" menu is opened. And then when the menu is closed, a straight line is painted all over the gap. Could you fix that?

image

@dozzzzer thanks. It appears that it's not only when opening that particular menu, but any menu. Also, it doesn't occur on Mac.
Looking into this ...

@saintmatthieu
Copy link
Contributor Author

@dozzzzer just pushed a few commits that alleviate but don't eliminate the problem. What you should now see when closing the menu is a gap, like you do after resizing the graph.
It seems to me that the main thread gets blocked rather easily on Windows. Opening a menu, moving the window, resizing it ... and the data transfer from audio to main thread relies on the main thread being fast, or there is data loss. This explains why the graph isn't deterministic. Maybe there is a more reliable way of transmitting the data, I will have to look into this, but not in this PR anymore.

@saintmatthieu
Copy link
Contributor Author

@saintmatthieu . In the compressor dialog, the panel containing the transfer function doesn't need to be in the tab order. cf Autoduck effect, where in Autoduck.h, the class definition for the panel it uses includes the following lines:

bool AcceptsFocus() const override { return false; }
// So that wxPanel is not included in Tab traversal - see wxWidgets bug 15581
bool AcceptsFocusFromKeyboard() const override { return false; }

@DavidBailes beautiful, thank you!

@saintmatthieu saintmatthieu merged commit 8f5a226 into audacity:master May 22, 2024
9 of 10 checks passed
@petersampsonaudacity
Copy link

petersampsonaudacity commented May 27, 2024

Testing on W10 with @saintmatthieu 's latest master alpha build for 3.6.0: audacity-win-3.6.0-alpha-20240527+5f8ee46-x64-msvc2022

The graph now looks continuous:
a) when the "Presets and Settings" menu is opened
b) when parameter sliders are moved and the LMB is later released

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Real-time-capable limiter
7 participants