godot
godot copied to clipboard
Add AudioEffectHardLimiter as a rework of audio limiter effect
This pull request entirely replaces the limiter that was initially implemented. The reason for this is the old limiter boosted the signal and limited through hard clipping, this introduced harsh distortions, reported in #36631. Because this is essentially a complete replacement of a feature and breaks compatibility, I'll try to be thorough in this pull request.
Setting up the old limiter with a Ceiling dB of -12 dB, and a threshold of 0 dB does not limit the signal, but reduces all samples going through by 12dB. When changing the Threshold dB property, the overall signal strength gets boosted, and when a sample crosses this threshold, it multiplies the incoming signal by ceiling/sample. This results in a sample that is exactly the same value as the ceiling (aka hard clipping). This drastically changes the waveform of the sound coming through and thus introduces distortion. This meant this limiter couldn't be used for limiting, as it would either simply lower the volume, or boost the signal and introduce distortions.
Because not everyone is as familiar with audio processing and audio effects, I'll attempt to give a brief overview over what a limiter aims to do, how it works, and how I've implemented it.
Brief overview of a limiter:
The goal of a limiter is to ensure no sound crosses a threshold, and is generally put on the master bus. This is important to have in any software that allows various audio processing effects like boosting and distorting signals to:
- Avoid distorting the outputted sound when it crosses 0dB.
- Protect the hearing of users against unexpected loud signals.
A limiter should always only affect the volume, and preserve the waveform as much as possible. It works as follows: It makes use of lookahead, which introduces a small amount of latency to the sound, typically 1-2ms. This allows the limiter to smoothly apply a gain reduction over time, before the actual peak is reached, which makes it so the waveform is preserved and undistorted. It stores the maximum gain reduction for all last played samples within a certain sustain time period in a buffer, and will always apply the largest amount of gain reduction present within this buffer. When reducing the amount of gain reduction happening, a smooth release is done moving back to no gain reduction, or whichever minimum gain-reduction amount is present in the sustain buffer.
For further (more in-depth) information on how limiters work I recommend this incredible blog post by Geraint Luff: https://signalsmith-audio.co.uk/writing/2022/limiter/
My implementation:
I have commented the various steps in my code to try to make this easier to read for those unfamiliar with signal processing, but here is a quick extra explanation specifically on the buffers and buckets.
For context: the gain variable in my implementation is used to multiply the final output to get the desired volume reduction. Firstly, three arrays are filled with some initial values: Two for the left- and right-samples so we can delay the sound. The size of these arrays depend on how long the attack time is set. Then a third one based on the attack+sustain time called gain_buckets. To avoid having to check over hundreds/thousands of samples to determine what the current gain reduction should be each sample, we simplify this buffer by comparing ranges of samples and storing a single minimum float in a bucket. To determine what value to store in a bucket, each sample we compare the gain of the current sample to the gain stored in the bucket, we store whichever is smaller. We can then run a small for-loop comparing all the buckets and taking the lowest gain value. This gives us an efficient approximation of the desired sustain functionality (See the peak-hold section in Geraints blog post).
Unlike the old limiter which changed the output volume based on the ceiling dB set, this implementation keeps a constant volume, and only affects the volume when peaks cross the ceiling.
I adjusted some pre-existing values:
-
Ceiling_dB default from -0.1 to -0.3 Limiters are often set slightly below 0 by default because of something called the inter-sample-peak (ISP). Audio samples aren't a complete representation of a signal, and it is possible when converting a digital signal to an analog signal (i.e. converting to the electrical signal for a speakers driver) for a peak to exist in between the samples describing a signal that is louder than the samples around it. This ISP can be a cause for distortion on some hardware. Some limiters implement ISP detection, and some just set a default ceiling value of -0.3dB. This seems to be a widely accepted default ceiling value for limiters across various DAWs and limiter plugins, so I think it is a good idea to follow this standard, and adjust our default value accordingly.
-
Ceiling_dB minimum from -20 to -24 Every 6 decibels describe a halving or doubling of volume, so to me it makes more sense for the minimum of this range to be divisible by 6.
Results and comparisons:
My comparisons and tests were done using a sine wave generated in Ableton, linearly fading from amplitude 1 to 0.
This test-signal unaffected looks like this:
Sound file is test-beep.wav (in the attached zip), check your volume since this beep goes from max volume to 0 volume, and can be quite loud.
We expect anywhere where this signal is louder than the ceiling to be limited to the ceiling, keeping the waveshape/form of the sound the same. The ceiling in these examples for both limiters were set at -6dB, threshold for old limiter at -3. The old limiter on the sound-beep is test-beep-old-limiter.wav This new implementation on the sound-beep is test-beep-new-limiter.wav
Zooming into those limited signals, we can see a drastic difference. The first (old limiter) cuts off the peaks of the signal, turning a signal that used to be a sine wave into something more akin to a square wave, while the second preserves the wave shape, simply scaling it down evenly to decrease the volume.
Edit: Looks like I messed up adding the example audiofiles. godot-limiter-comparison.zip
You have removed properties without adding compatibility code, so that needs to be fixed as well, but this still breaks compatibility by removing or renaming existing properties
Thanks for checking out this pull request so quickly!
I've reduced the amount of comments and fixed their formatting.
I'm not entirely sure how to deal with the deprecation of properties and functions, and haven't been successful in finding documentation or reference on how to deal with this.
So far, I've re-added those functions, properties and bindings that I deleted, and added a deprecated tag to those properties in the .xml file.
Unfortunately, because of the fundamental change in how the limiter works, those deprecated properties are not used anymore:
- threshold_db is not used anymore because gain reduction is driven through look ahead.
- soft_clip_db is not used since we're not boosting any signals anymore, aside from with pre-gain.
- soft_clip_ratio is particularly interesting because it seems that it was never implemented in the process function.
Beyond this, is there anything else I should do in terms of deprecation and compatibility code?
I haven't reviewed and might not be able to to at all but it's possible this might fit better into the codebase as a new effect instead of replacing the old one if its API is substantially different. Replacement would then happen next major version bump.
this might fit better into the codebase as a new effect instead of replacing the old one
That makes sense to me, I'll re-implement it as a separate effect. I'm not sure what I should rename the new limiter so there's a distinction between the two though. I assume I should also update the old-limiters .xml then to mark it as deprecated?
I assume I should also update the old-limiters .xml then to mark it as deprecated?
Correct, if you feel like the new limiter to be a straight upgrade that is.
The new name could be something that can be tinkered about later. Something like AudioStreamLimiterAdvanced, or even better, an adjective to describe what the new implementation does (which you would know best based on the extensive explanation).
I generated some new names for the limiter:
AudioEffectLimiterPreciseAudioEffectLimiterEnhancedAudioEffectLimiterWaveform
Maybe you can use these names for reference.
Edited:
Edited the names.
I've re-implemented the new limiter as a separate audio effect, and marked the original limiter as deprecated, stating it would be replaced. When it comes to naming, I ended up naming it "hard limiter", since that's what this type of limiter is generally described as.
Audacity's manual page on their limiter confirms this, as well as the Wikipedia page on limiters under types, so this seems like an appropriate name.
I let the workflow run just to see if there are any errors for this. Renamed the PR's title to something more meaningful (Ideally it could be the commit's name, too).
Thanks for the reviews! Just committed those suggestions :)
Last nitpick, could you amend the commit message to be more explicit? Reusing the title of this PR would be a good option.
Thanks! And congrats for your first merged Godot contribution :tada:
Thanks! Super stoked about this! :D
Really glad this finally happened!