Skip to content

Commit

Permalink
- Added limiter and compressor limiter tests
Browse files Browse the repository at this point in the history
- Corrected errors in the documentation
  • Loading branch information
djowel committed Sep 3, 2024
1 parent d458886 commit 9eb3174
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 40 deletions.
2 changes: 1 addition & 1 deletion docs/antora/antora-playbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ site:
url: https://www.cycfi.com/
content:
sources:
- url: https://github.com/cycfi/q.git
- url: ../../
branches: [master, v*]
start_path: docs
ui:
Expand Down
53 changes: 37 additions & 16 deletions docs/modules/ROOT/images/dynamic-cascading.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 18 additions & 19 deletions docs/modules/ROOT/images/env-processor.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 6 additions & 4 deletions docs/modules/ROOT/pages/reference/dynamic.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ include::../common.adoc[]

image:comp-transfer.svg[alt="Compressor Transfer Response", width=450, role=right]Audio dynamic range processing DSP refers to the manipulation of an audio signal's dynamic range. The dynamic range of an audio signal is the difference between its loudest and quietest components. Compression, Expansion, Limiting, and Gating are a few common types of dynamic range processing used in audio DSP. Such processing can enhance sound quality, minimize or eliminate noise, and create creative effects.

The dynamic range processors in the Q DSP library are unique in that they do not process audio waveform samples directly. Instead, they operate on the envelope of the audio signal, represented as `decibel`, perform computations in the logarithmic domain, and return a processed envelope, also represented as `decibel`.
The dynamic range processors in the Q DSP library are unique in that they do not process audio waveform samples directly. Instead, they operate on the envelope of the audio signal, represented as `decibel`, perform computations in the logarithmic domain, and return the gain, also represented as `decibel`:

image::env-processor.svg[alt="Envelope Processor", width=500]

Expand All @@ -27,20 +27,22 @@ NOTE: This is a hard knee compressor: a type of compressor in which the compress

=== Key Points

* The code accepts an envelope input, `env`, in decibels and outputs a compressed envelope also in decibels.
* The code accepts an envelope input, `env`, in decibels and outputs the compressed gain also in decibels.
* Processing is done in the logarithmic domain, where addition of two values is equivalent to multiplying their corresponding linear values, and multiplication of two values is equivalent to raising their corresponding linear values to a power.
* The envelope follower, which is responsible for generating the input envelope for the compressor, is implemented outside of the compressor class. The objective is separation of concerns and the flexibility to use various envelope followers.
* Compared to their linear counterparts, performing computations in the logarithmic domain is simpler, more intuitive, and makes the code easier to understand while maintaining the same level of efficiency.
* The compressor example above and all dynamic range processors in the Q DSP library are essentially envelope processors. They receive envelopes, process envelopes, and return envelopes.
* The compressor example above and all dynamic range processors in the Q DSP library are essentially envelope processors. They receive envelopes, process envelopes, and return the gain in decibels.

=== Cascading

Since envelope processors operate on the envelope, perform computations in the logarithmic domain, and return a processed envelope, multiple envelope processors can be cascaded. For instance, you can use a single envelope follower and apply the derived envelope to an expander and then pass the result to a compressor, like in the diagram below:
Since envelope processors operate on the envelope, perform computations in the logarithmic domain, and return the computed gain, multiple envelope processors can logically be cascaded. For instance, you can use a single envelope follower and apply the derived envelope to both an expander and a compressor. Since we are working in the logarithmic domain, cascading is simply summing the outputs of the processors, keeping in mind that summation in the decibel domain is equivalent to multiplication in the linear domain. Example:

image::dynamic-cascading.svg[alt="Cascading Dynamic Processors", width=600]

A specific use-case for cascading an expander and a compressor, like in the example above, is when you want to use the expander as a noise gate, with soft, non-abrupt gating, set at a threshold just above the noise floor. Then, setting the threshold of the compressor to a high value to even out the dynamic range.

NOTE: If there's an overlap, you can use the maximum (or minimum) of the outputs using `std::max` (or `std::min`) instead of adding the outputs.

image:gate-compressor-limiter.svg[alt="Gate Compressor Limiter", width=400, role=right]The compressor and expander in this case work on different regions of the full dynamic range. But there are also useful cases where the overlap of dynamic ranges the processors are working on is advantageous. The compressor-limiter is a good example. Likewise, multiple compressors with variable thresholds and ratios are another example.

By cascading multiple dynamic range processors, we can design efficient multi-function processors like the compressor-limiter with gate (transfer response graph at the right).
70 changes: 70 additions & 0 deletions test/dynamics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,59 @@ void test_agc(in_buffer const& ramp)
compare_golden("curve_agc", 1e-6);
}

///////////////////////////////////////////////////////////////////////////////
void test_limiter(in_buffer const& ramp)
{
out_buffer out;

auto lim = q::compressor{ -6_dB, 1.0/100 };

for (auto i = 0; i != size; ++i)
{
auto pos = i * n_channels;
auto ch1 = pos;
auto s = ramp[i];
out[ch1] = s * lin_float(lim(q::lin_to_db(s)));
}

{
q::wav_writer wav(
"results/curve_limiter.wav", n_channels, sps
);
wav.write(out);
}
compare_golden("curve_limiter", 1e-6);
}

///////////////////////////////////////////////////////////////////////////////
void test_compressor_limiter(in_buffer const& ramp)
{
out_buffer out;

auto comp = q::compressor{ -9_dB, 1.0/2 };
auto lim = q::compressor{ -6_dB, 1.0/50 };

for (auto i = 0; i != size; ++i)
{
auto pos = i * n_channels;
auto ch1 = pos;
auto s = ramp[i];
auto env_db = q::lin_to_db(s);
auto comp_db = comp(env_db);
auto lim_db = lim(env_db);

out[ch1] = s * lin_float(std::min(comp_db, lim_db));
}

{
q::wav_writer wav(
"results/compressor_limiter.wav", n_channels, sps
);
wav.write(out);
}
compare_golden("compressor_limiter", 1e-6);
}

///////////////////////////////////////////////////////////////////////////////
void gen_ramp(in_buffer& ramp)
{
Expand Down Expand Up @@ -199,4 +252,21 @@ TEST_CASE("TEST_agc")
test_agc(ramp);
}

///////////////////////////////////////////////////////////////////////////////
TEST_CASE("TEST_limiter")
{
auto ramp = in_buffer{};
gen_ramp(ramp);
test_limiter(ramp);
}

///////////////////////////////////////////////////////////////////////////////
TEST_CASE("TEST_compressor_limiter")
{
auto ramp = in_buffer{};
gen_ramp(ramp);
test_compressor_limiter(ramp);
}



Binary file added test/golden/compressor_limiter.wav
Binary file not shown.
Binary file added test/golden/curve_limiter.wav
Binary file not shown.

0 comments on commit 9eb3174

Please sign in to comment.