029 – Floating-Point FPGA Audio Limiter (1)


In this post we start a series in which we will explore the RTL description and simulation of a floating-point Limiter for our FPGA Audio Processor.

Our Audio Processor already includes a delay and a single-band equalizer. The delay belongs to the family of time-based effects, while the equalizer is part of the frequency-based effects. Today start our exploration of a new kind of audio effect: dynamic range controllers (DRC). The most common DRC effects are gates, compressors, expanders, and the subject of today’s post, limiters.

What is a Limiter?

A limiter aims to reduce the dynamic range of the signal above a certain threshold, but to otherwise change the signal as little as possible (ideally not at all!). Whenever the input signal goes above the limiting threshold, the limiter kicks in and applies a gain smaller than one to reduce the amplitude of the samples above the threshold. When the input signal comes back down below the threshold, the limiter stops applying its gain and simply passes the incoming signals through to the output.

The figure below shows the block diagram of a limiter, as described in Udo Zölzer’s DAFX Digital Audio Effects (Second Edition).

Block Diagram of a Limiter. Source DAFX Digital Audio Effects (Second Edition)
Block Diagram of a Limiter. Source DAFX Digital Audio Effects (Second Edition)

RTL Description of the Limiter

We will use the code from the source above as a reference, so our Limiter will be a single-precision floating-point FPGA implementation of the algorithm described in the book’s sample code. The book also references the Dynamic range control of digital audio signals paper from the BBC research engineer G. W. McNally, especially when it comes to the attack and release time filter for the envelope detection and gain smoothing.

Diving into the details of how a limiter works is outside the scope of this post, so I kindly refer you to the sources above if you would like to learn more about that. I’m also choosing not to reproduce the book’s sample code here because I’m guessing it might be problematic from a copyright perspective. This is not ideal, but I hope it will serve as an incentive to get the book, as it is a great resource for digital audio processing.

Our limiter performs the following main tasks:

  1. Calculate the envelope of the incoming signal by extracting the absolute value of each sample and smoothing it over time. The time smoothing uses different coefficients depending on whether the incoming sample is above or below the threshold for limiting. In the former case the release time is used, in the latter the attack time is used (this is carried out in the ‘PEAK AT/RT‘ block in the figure above)
  2. Calculate the required gain by comparing the value of the envelope to the threshold. Here too attack and release time coefficients are used to smooth the gain adjustment (this is carried out in the three remaining blocks of the lower path in the figure above)
  3. Apply the calculated gain to the input sample to generate the output. The calculation of the output samples can be delayed so that the gain can account for future changes in the incoming signals. This delay is also known as lookahead (this is carried out in the upper path in the figure above)

The RTL description of the limiter follows the now familiar pattern of an FSM executing all the calculations by driving the floating-point operation IP cores. As in previous modules, we used a naive description, with each operation being executed sequentially and one instance of the floating-point IP core for each type of operation.

This is the most complex module we have implemented so far, which is evident by the number of states in the FSM. It shows that, for moderately complex algorithms and some parallelism, this approach might result in large, complex state machines which might be difficult to maintain, debug and expand.

The complete source code for the Limiter module is shown below. In this version only the left channel is processed, and all the logic is contained in a single module. This was done as a quick way to verify the basic functionality, stereo support will be added next.

module limiter # (
    parameter integer SP_FLOATING_POINT_BIT_WIDTH = 32
    ) (
    input   logic                                           i_clock,
    input   logic                                           i_enable,
    // Audio Input
    input   logic                                           i_data_valid,
    input   logic   [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]     i_data_left,
    input   logic   [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]     i_data_right,
    // Controls
    input   logic   [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]     i_linear_threshold,
    // Audio Output
    output  logic                                           o_data_valid,
    output  logic   [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]     o_data_left,
    output  logic   [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]     o_data_right
);

    timeunit 1ns;
    timeprecision 1ps;

    parameter logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0] ATTACK_TIME = 32'h3F666666;    // 0.9
    parameter logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0] RELEASE_TIME = 32'h3E99999A;   // 0.3

    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0] data_left;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0] data_right;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0] xpeak;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0] gain;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0] coefficient;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0] filter;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0] absolute_value;

    logic                                       fp_abs_value_valid_in;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]   fp_abs_value_data_in;
    logic                                       fp_abs_value_valid_out;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]   fp_abs_value_data_out;
    fp_absolute_value fp_absolute_value_inst (
      .s_axis_a_tvalid      (fp_abs_value_valid_in),
      .s_axis_a_tdata       (fp_abs_value_data_in),
      .m_axis_result_tvalid (fp_abs_value_valid_out),
      .m_axis_result_tdata  (fp_abs_value_data_out)
    );

    logic                                       fp_greater_comp_valid_in;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]   fp_greater_comp_data_a_in;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]   fp_greater_comp_data_b_in;
    logic                                       fp_greater_comp_valid_out;
    logic [7 : 0]                               fp_greater_comp_data_out;
    fp_greater_comp fp_greater_comp_inst (
        .aclk                   (i_clock),
        .s_axis_a_tvalid        (fp_greater_comp_valid_in),
        .s_axis_a_tdata         (fp_greater_comp_data_a_in),
        .s_axis_b_tvalid        (fp_greater_comp_valid_in),
        .s_axis_b_tdata         (fp_greater_comp_data_b_in),
        .m_axis_result_tvalid   (fp_greater_comp_valid_out),
        .m_axis_result_tdata    (fp_greater_comp_data_out)
    );

    logic                                       fp_subtractor_valid_in;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]   fp_subtractor_data_a_in;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]   fp_subtractor_data_b_in;
    logic                                       fp_subtractor_valid_out;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]   fp_subtractor_data_out;
    fp_subtractor fp_subtractor_inst (
      .aclk                 (i_clock),
      .s_axis_a_tvalid      (fp_subtractor_valid_in),
      .s_axis_a_tdata       (fp_subtractor_data_a_in),
      .s_axis_b_tvalid      (fp_subtractor_valid_in),
      .s_axis_b_tdata       (fp_subtractor_data_b_in),
      .m_axis_result_tvalid (fp_subtractor_valid_out),
      .m_axis_result_tdata  (fp_subtractor_data_out)
    );

    logic                                       fp_mult_valid_in;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]   fp_mult_data_in_a;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]   fp_mult_data_in_b;
    logic                                       fp_mult_valid_out;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]   fp_mult_data_out;
    fp_multiplier fp_multiplier_inst (
        .aclk                   (i_clock),
        .s_axis_a_tvalid        (fp_mult_valid_in),
        .s_axis_a_tdata         (fp_mult_data_in_a),
        .s_axis_b_tvalid        (fp_mult_valid_in),
        .s_axis_b_tdata         (fp_mult_data_in_b),
        .m_axis_result_tvalid   (fp_mult_valid_out),
        .m_axis_result_tdata    (fp_mult_data_out)
    );

    logic                                       fp_adder_valid_in;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]   fp_adder_data_in_a;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]   fp_adder_data_in_b;
    logic                                       fp_adder_valid_out;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]   fp_adder_data_out;
    fp_adder fp_adder_inst (
        .aclk                   (i_clock),
        .s_axis_a_tvalid        (fp_adder_valid_in),
        .s_axis_a_tdata         (fp_adder_data_in_a),
        .s_axis_b_tvalid        (fp_adder_valid_in),
        .s_axis_b_tdata         (fp_adder_data_in_b),
        .m_axis_result_tvalid   (fp_adder_valid_out),
        .m_axis_result_tdata    (fp_adder_data_out)
    );

    logic                                       fp_divider_valid_in;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]   fp_divider_data_in_a;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]   fp_divider_data_in_b;
    logic                                       fp_divider_valid_out;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0]   fp_divider_data_out;
    fp_divider fp_divider_inst (
        .aclk                   (i_clock),
        .s_axis_a_tvalid        (fp_divider_valid_in),
        .s_axis_a_tdata         (fp_divider_data_in_a),
        .s_axis_b_tvalid        (fp_divider_valid_in),
        .s_axis_b_tdata         (fp_divider_data_in_b),
        .m_axis_result_tvalid   (fp_divider_valid_out),
        .m_axis_result_tdata    (fp_divider_data_out)
    );

    typedef enum logic [5 : 0]  {IDLE,
                                WAIT_SAMPLE,
                                GET_ABS_VALUE,
                                COMPARE_XPEAK,
                                CALC_XPEAK_1,
                                CALC_XPEAK_2,
                                CALC_XPEAK_3,
                                CALC_XPEAK_4,
                                THRESHOLD_XPEAK_RATIO,
                                CALC_FILTER,
                                COMPARE_FILTER,
                                COMPARE_FILTER_GAIN,
                                CALC_GAIN_1,
                                CALC_GAIN_2,
                                CALC_GAIN_3,
                                CALC_GAIN_4,
                                CALC_OUTPUT,
                                DRIVE_OUTPUT} limiter_fsm_state_t;
    limiter_fsm_state_t limiter_fsm_state = IDLE;
    logic [SP_FLOATING_POINT_BIT_WIDTH-1 : 0] aux;

    always_ff @(posedge i_clock) begin
        case (limiter_fsm_state)
            IDLE : begin
                xpeak <= 32'h00000000;
                gain <= 32'h3F800000;
                fp_abs_value_valid_in <= 1'b0;
                fp_greater_comp_valid_in <= 1'b0;
                fp_subtractor_valid_in <= 1'b0;
                fp_mult_valid_in <= 1'b0;
                fp_adder_valid_in <= 1'b0;
                fp_divider_valid_in <= 1'b0;
                o_data_valid <= 1'b0;
                data_left <= 32'h00000000;
                data_right <= 32'h00000000;
                if (i_enable == 1'b1) begin
                    limiter_fsm_state <= WAIT_SAMPLE;
                end
            end

            WAIT_SAMPLE : begin
                o_data_valid <= 1'b0;
                if (i_data_valid == 1'b1) begin
                    data_left <= i_data_left;
                    data_right <= i_data_right;
                    fp_abs_value_data_in <= i_data_left;
                    fp_abs_value_valid_in <= 1'b1;
                    limiter_fsm_state <= GET_ABS_VALUE;
                end
            end

            GET_ABS_VALUE : begin
                fp_abs_value_valid_in <= 1'b0;
                if (fp_abs_value_valid_out == 1'b1) begin
                    absolute_value <= fp_abs_value_data_out;
                    fp_greater_comp_data_a_in <= fp_abs_value_data_out;
                    fp_greater_comp_data_b_in <= xpeak;
                    fp_greater_comp_valid_in <= 1'b1;
                    limiter_fsm_state <= COMPARE_XPEAK;
                end
            end

            COMPARE_XPEAK : begin
                fp_greater_comp_valid_in <= 1'b0;
                if (fp_greater_comp_valid_out == 1'b1) begin
                    if (fp_greater_comp_data_out[0] == 1'b1) begin
                        coefficient <= ATTACK_TIME;
                    end else begin
                        coefficient <= RELEASE_TIME;
                    end
                    limiter_fsm_state <= CALC_XPEAK_1;
                end
            end

            CALC_XPEAK_1 : begin
                fp_subtractor_valid_in <= 1'b1;
                fp_subtractor_data_a_in <= 32'h3F800000;
                fp_subtractor_data_b_in <= coefficient;
                limiter_fsm_state <= CALC_XPEAK_2;
            end

            CALC_XPEAK_2 : begin
                fp_subtractor_valid_in <= 1'b0;
                if (fp_subtractor_valid_out == 1'b1) begin
                    fp_mult_valid_in <= 1'b1;
                    fp_mult_data_in_a <= fp_subtractor_data_out;
                    fp_mult_data_in_b <= xpeak;
                    limiter_fsm_state <= CALC_XPEAK_3;
                end
            end

            CALC_XPEAK_3 : begin
                fp_mult_valid_in <= 1'b0;
                if (fp_mult_valid_out == 1'b1) begin
                    aux <= fp_mult_data_out;
                    fp_mult_valid_in <= 1'b1;
                    fp_mult_data_in_a <= coefficient;
                    fp_mult_data_in_b <= absolute_value;
                    limiter_fsm_state <= CALC_XPEAK_4;
                end
            end

            CALC_XPEAK_4 : begin
                fp_mult_valid_in <= 1'b0;
                if (fp_mult_valid_out == 1'b1) begin
                    fp_adder_valid_in <= 1'b1;
                    fp_adder_data_in_a <= aux;
                    fp_adder_data_in_b <= fp_mult_data_out;
                    limiter_fsm_state <= THRESHOLD_XPEAK_RATIO;
                end
            end

            THRESHOLD_XPEAK_RATIO : begin
                fp_adder_valid_in <= 1'b0;
                if (fp_adder_valid_out) begin
                    xpeak <= fp_adder_data_out;
                    fp_divider_valid_in <= 1'b1;
                    fp_divider_data_in_a <= i_linear_threshold;
                    fp_divider_data_in_b <= fp_adder_data_out;
                    limiter_fsm_state <= CALC_FILTER;
                end
            end

            CALC_FILTER : begin
                fp_divider_valid_in <= 1'b0;
                if (fp_divider_valid_out == 1'b1) begin
                    filter <= fp_divider_data_out;
                    fp_greater_comp_valid_in <= 1'b1;
                    fp_greater_comp_data_a_in <= fp_divider_data_out;
                    fp_greater_comp_data_b_in <= 32'h3F800000;
                    limiter_fsm_state <= COMPARE_FILTER;
                end
            end

            COMPARE_FILTER : begin
                fp_greater_comp_valid_in <= 1'b0;
                if (fp_greater_comp_valid_out == 1'b1) begin
                    if (fp_greater_comp_data_out[0] == 1'b1) begin
                        filter <= 32'h3F800000;
                        fp_greater_comp_data_b_in <= 32'h3F800000;
                    end else begin
                        fp_greater_comp_data_b_in <= filter;
                    end
                    fp_greater_comp_valid_in <= 1'b1;
                    fp_greater_comp_data_a_in <= gain;
                    limiter_fsm_state <= COMPARE_FILTER_GAIN;
                end
            end

            COMPARE_FILTER_GAIN : begin
                fp_greater_comp_valid_in <= 1'b0;
                if (fp_greater_comp_valid_out == 1'b1) begin
                    if (fp_greater_comp_data_out[0] == 1'b1) begin
                        coefficient <= ATTACK_TIME;
                    end else begin
                        coefficient <= RELEASE_TIME;
                    end
                    limiter_fsm_state <= CALC_GAIN_1;
                end
            end

            CALC_GAIN_1 : begin
                fp_subtractor_valid_in <= 1'b1;
                fp_subtractor_data_a_in <= 32'h3F800000;
                fp_subtractor_data_b_in <= coefficient;
                limiter_fsm_state <= CALC_GAIN_2;
            end

            CALC_GAIN_2 : begin
                fp_subtractor_valid_in <= 1'b0;
                if (fp_subtractor_valid_out == 1'b1) begin
                    fp_mult_valid_in <= 1'b1;
                    fp_mult_data_in_a <= fp_subtractor_data_out;
                    fp_mult_data_in_b <= gain;
                    limiter_fsm_state <= CALC_GAIN_3;
                end
            end

            CALC_GAIN_3 : begin
                fp_mult_valid_in <= 1'b0;
                if (fp_mult_valid_out == 1'b1) begin
                    aux <= fp_mult_data_out;
                    fp_mult_valid_in <= 1'b1;
                    fp_mult_data_in_a <= coefficient;
                    fp_mult_data_in_b <= filter;
                    limiter_fsm_state <= CALC_GAIN_4;
                end
            end

            CALC_GAIN_4 : begin
                fp_mult_valid_in <= 1'b0;
                if (fp_mult_valid_out == 1'b1) begin
                    fp_adder_valid_in <= 1'b1;
                    fp_adder_data_in_a <= aux;
                    fp_adder_data_in_b <= fp_mult_data_out;
                    limiter_fsm_state <= CALC_OUTPUT;
                end
            end

            CALC_OUTPUT : begin
                fp_adder_valid_in <= 1'b0;
                if (fp_adder_valid_out == 1'b1) begin
                    gain <= fp_adder_data_out;
                    fp_mult_valid_in <= 1'b1;
                    fp_mult_data_in_a <= fp_adder_data_out;
                    fp_mult_data_in_b <= data_left;
                    limiter_fsm_state <= DRIVE_OUTPUT;
                end
            end

            DRIVE_OUTPUT : begin
                fp_mult_valid_in <= 1'b0;
                if (fp_mult_valid_out == 1'b1) begin
                    o_data_valid <= 1'b1;
                    o_data_left <= fp_mult_data_out;
                    o_data_right <= data_right;
                    limiter_fsm_state <= WAIT_SAMPLE;
                end
            end

            default : begin
                limiter_fsm_state <= IDLE;
            end
        endcase
    end

endmodule

Limiter Simulation

We run the simulation by using our testbench with the SystemVerilog WAVE file reader and the script for standalone simulation with Vivado as a starting point. The testbench can be used as-is, we only need to instantiate the Limiter module. The simulation script must be expanded to include all the floating-point IP cores used by the Limiter. We used our familiar snare hit as the input and run the simulation for 10 ms. The resulting waveforms are shown in figure below.

Results of the Limiter Simulation
Results of the Limiter Simulation

For this simulation the limiting threshold was set to 5.000.000 in the linear scale. the three analog waveforms in Figure 2 are, from top to bottom, the left channel input, the left channel output, and the gain of the Limiter. The latency of our Limiter is 60 cycles.

We can see that, for the first few milliseconds of the simulation, the output of the Limiter is equal to its input, and the gain remains equal to 1. The misleading downward slope in the gain signal at the beginning of the simulation is due to how the Vivado simulator draws ‘analog’ waveforms, the gain actually stays at ‘1’ until the first inflexion point.

At ~1.3 ms, where the marker is located, the input signal reaches its highest absolute value, close to the maximum value that can be represented by a 24-bit signed integer. We can clearly see that by this point the limiter has already kicked in, so the output value is much lower than that. This is also the time where the gain reaches its lowest value, which is exactly the behavior we would like to see in a limiter.

For this simulation we are using fast attack and slow release times, which means that the Limiter kicks in and reduces its gain very quickly after the threshold has been exceeded, but then takes a long time before setting the gain back to unity after the input signal is again below the threshold. Different combinations of attack and release time values will yield different audible results depending on the input signal.

Finally, we see that the highest absolute value of the output signal (5.217.725) is above the threshold we selected (5.000.000). This is not an ideal behavior, but it is expected (and not completely avoidable in our current implementation), even McNally noted this could happen. He suggests using a delay to give the detection logic time to react to the changes in the incoming signal and prevent these peaks from getting through the limiter. We might explore this in a later post.

In the next installment of this series we will add stereo support, discuss the implementation results, and listen to some audio samples to hear our limiter in action. Stay tuned!

Cheers,

Isaac

All files for this post are available in the FPGA Audio Processor repository under this tag.


8 responses to “029 – Floating-Point FPGA Audio Limiter (1)”

  1. Thanks for posting this – it looks very interesting. I opened the project in Vivado 2020.2 and was able to run through implementation, saw timing was met, and generate a bitstream. However, when I tried behavioral simlation, I received the following error:
    [XSIM 43-3225] Cannot find design unit xil_defaultlib.tc_01 in library work located at xsim.dir/work.

    Have you encountered this before, or know of a resolution? Thought I would check in before trying to resolve this myself.
    Thanks
    John

    • Hi John,
      I just cloned a fresh copy of the repository to a different location on my PC, and the simulation runs without issues.
      How are you starting the simulation? It is meant to be run in batch mode, so you would need to go to the simulation folder (fpga_audio_processor\sim\limiter\tc_01), open a terminal windows and type: ‘vivado -mode batch -source sim.tcl’ (without quotes, and assuming your environment variable has been set properly).
      Hope this helps!
      Cheers,
      Isaac

  2. Hi Isaac,
    Yes, I was running the Vivado GUI when I got the simulation error. Switching to batch mode allowed simulation to complete.

    However, when the xsim viewer was launched with the waveform config file and the waveform database file, I can see all the signal names listed in the waveform viewer, but there are no waveforms. I checked my log files and didn’t see any errors, just a warning that I don’t think is related to this issue. Here is my TCL console log from the xsim GUI:

    open_wave_database limiter_tc_01_sim.wdb

    WARNING: [Board 49-91] Board repository path ‘c:/trd_home/vivado/zcu102_base_trd/hw_platform/board’ does not exist, it will not be used to search board files.
    INFO: [IP_Flow 19-234] Refreshing IP repositories
    INFO: [IP_Flow 19-1704] No user IP repositories specified
    INFO: [IP_Flow 19-2313] Loaded Vivado IP repository ‘C:/Xilinx/Vivado/2020.2/data/ip’.
    open_wave_config C:/designs/Biamp/aaron_fp_limiter/fpga_audio_processor-029-floating-point-fpga-audio-limiter-1/sim/limiter/tc_01/limiter_tc_01_sim.wcfg

    Any ideas why the waveforms aren’t showing? Do you think it has to do with the warning about the board repository path?

    Thanks for your assistance,
    John

    • Hi John,
      could you please check that the Vivado Waveform Database File (limiter_tc_01_sim.wdb) is generated correctly? It should be stored directly in the simulation directory and take up about 9 MB.
      If the file is there you should be able to open it by launching the Vivado GUI and, from the start screen (without opening the project) going to ‘Flow -> Open Static Simulation’. Once the database is open you can load the Waveform Configuration File from ‘File -> Simulation Waveform -> Open Configuration…’
      If you can go through the steps above, it means that the simulation ran correctly. I’m not sure why it dosn’t work right away.
      One more thing: if you would like to have the exact same behavior of the Limiter as in the waveform shown in the blog post, you’d have to change the release time to 0.01 (32’h3C23D70A).
      Let me know if this works!
      Cheers,
      Isaac

  3. Not sure what happened with the giant font in the previous comment. It should have appears like this:

    open_wave_database limiter+tc_01_sim.wdb

  4. Apparently the hash (pound sign) symbol in front of the open_wave_database commands causes the fon to change size. Wierd.

  5. HI Isaac,

    I found the issue – in the file fpga_audio_processor-029-floating-point-fpga-audio-limiter-1\sim\common\wave_file_reader.sv, there is a hard-coded directory path on line 52. This is the $fopen command to read the snare.wav file. I changed the path to match my machine, and everything works as expected.
    Thanks!

    • Hi John,
      thanks for working on this – and letting me know what the problem was. The fix will be available in Tag 032.
      Cheers,
      Isaac

Leave a Reply

Your email address will not be published. Required fields are marked *