021 – FPGA Audio Clipper


In this post we will go over the implementation of a floating-point audio clipper for our FPGA Audio Processor.

What is an Audio Clipper?

Clipping is a simple form of dynamic range processing. A clipper has a threshold against which it compares the incoming audio samples. If they exceed this threshold, the clipper sets the outgoing samples to a fixed value, usually the same as or very close to the threshold. If the incoming audio samples do not exceed the threshold, the clipper sends them unchanged to its output. The visual representation of this processing is shown in the figure below.

Audio Clipping. Source: Wikipedia (Clipper (electronics))
Audio Clipping. Source: Wikipedia (Clipper (electronics))

Why (and why not) use a Clipper

There are two common uses for a clipper: as an overflow protection device, and as a creative device. As an overflow protection device, the clipper is used as the last processing element in an audio processing pipeline, where it is guaranteed to keep every sample within the dynamic range of the DAC.

As a creative device, a clipper is often used for adding harmonics to the sound and increasing the perceived loudness of an audio track (those two phenomena go hand in hand). There are more subtle ways to add harmonic content to an audio signal, but a clipper definitely has its place. Furthermore, the harmonics introduced by intentionally clipping a signal can be more controllable and pleasant than simply allowing the samples to overflow.

In most cases however, a clipper is not meant to replace a compressor or a limiter. Those devices have more sophisticated ways of measuring the amplitude of the incoming signal and applying gain reduction in a less ‘aggressive’ manner. We will explore compressors and limiters later on.

Floating-Point Clipper in SystemVerilog

Our clipper uses floating-point greater-than and smaller-than comparators to decide when to manipulate the signal values. the logic goes as follows:

  1. Check if the sample is greater than the positive threshold. If it is, set it to the positive clipping value. If it is not, go to the next step.
  2. Check if the sample is smaller than the negative threshold. If it is, set it to the negative clipping value. If it is not, go to the next step.
  3. Send the audio sample to the output of the clipper without changing its value.

The figure below shows the state diagram for the Clipper FSM.

State Diagram for the Clipper FSM
State Diagram for the Clipper FSM

The complete code for the clipper module is shown below.

module clipper (
    input   logic               i_clock,
    // Audio Input
    input   logic               i_data_valid,
    input   logic   [31 : 0]    i_data_left,
    input   logic   [31 : 0]    i_data_right,
    // Audio Output
    output  logic               o_data_valid,
    output  logic   [31 : 0]    o_data_left,
    output  logic   [31 : 0]    o_data_right
);

    timeunit 1ns;
    timeprecision 1ps;

    logic           fp_greater_comp_data_in_valid;
    logic [31 : 0]  fp_greater_comp_data_a_in;
    logic           fp_greater_comp_data_out_valid;
    logic [7 : 0]   fp_greater_comp_data_out;
    fp_greater_equal_comp fp_greater_equal_comp_inst (
        .aclk                   (i_clock),
        .s_axis_a_tvalid        (fp_greater_comp_data_in_valid),
        .s_axis_a_tdata         (fp_greater_comp_data_a_in),
        .s_axis_b_tvalid        (fp_greater_comp_data_in_valid),
        .s_axis_b_tdata         (32'h4AF1ADF8),     // 7919356, ~-0.5 dBFS
        .m_axis_result_tvalid   (fp_greater_comp_data_out_valid),
        .m_axis_result_tdata    (fp_greater_comp_data_out)
    );

    logic           fp_less_comp_data_in_valid;
    logic [31 : 0]  fp_less_comp_data_a_in;
    logic           fp_less_comp_data_out_valid;
    logic [7 : 0]   fp_less_comp_data_out;
    fp_less_equal_comp fp_less_equal_comp_inst (
        .aclk                   (i_clock),
        .s_axis_a_tvalid        (fp_less_comp_data_in_valid),
        .s_axis_a_tdata         (fp_less_comp_data_a_in),
        .s_axis_b_tvalid        (fp_less_comp_data_in_valid),
        .s_axis_b_tdata         (32'hCAF1ADF8),     // -7919356, ~-0.5 dBFS
        .m_axis_result_tvalid   (fp_less_comp_data_out_valid),
        .m_axis_result_tdata    (fp_less_comp_data_out)
    );

    // Clipper FSM
    enum logic [2 : 0]  {IDLE,
                        CHECK_LEFT_CHANNEL_POSITIVE,
                        CHECK_LEFT_CHANNEL_NEGATIVE,
                        CHECK_RIGHT_CHANNEL_POSITIVE,
                        CHECK_RIGHT_CHANNEL_NEGATIVE} clipper_fsm_state = IDLE;
    logic [31 : 0] data_left = 'b0;
    logic [31 : 0] data_right = 'b0;

    always_ff @(posedge i_clock) begin : clipper_fsm
        case (clipper_fsm_state)
            IDLE : begin
                o_data_valid <= 1'b0;
                fp_greater_comp_data_in_valid <= 1'b0;
                fp_less_comp_data_in_valid <= 1'b0;
                if (i_data_valid == 1'b1) begin
                    data_left <= i_data_left;
                    data_right <= i_data_right;
                    fp_greater_comp_data_in_valid <= 1'b1;
                    fp_greater_comp_data_a_in <= i_data_left;
                    clipper_fsm_state <= CHECK_LEFT_CHANNEL_POSITIVE;
                end
            end

            CHECK_LEFT_CHANNEL_POSITIVE : begin
                fp_greater_comp_data_in_valid <= 1'b0;
                if (fp_greater_comp_data_out_valid == 1'b1) begin
                    if (fp_greater_comp_data_out[0] == 1'b1) begin
                        o_data_left <= 32'h4AF1ADFA;    // 7919357
                        fp_greater_comp_data_in_valid <= 1'b1;
                        fp_greater_comp_data_a_in <= data_right;
                        clipper_fsm_state <= CHECK_RIGHT_CHANNEL_POSITIVE;
                    end else begin
                        fp_less_comp_data_in_valid <= 1'b1;
                        fp_less_comp_data_a_in <= data_left;
                        clipper_fsm_state <= CHECK_LEFT_CHANNEL_NEGATIVE;
                    end
                end
            end

            CHECK_LEFT_CHANNEL_NEGATIVE : begin
                fp_less_comp_data_in_valid <= 1'b0;
                if (fp_less_comp_data_out_valid == 1'b1) begin
                    if (fp_less_comp_data_out[0] == 1'b1) begin
                        o_data_left <= 32'hCAF1ADFA;    // -7919357
                    end else begin
                        o_data_left <= data_left;
                    end
                    fp_greater_comp_data_in_valid <= 1'b1;
                    fp_greater_comp_data_a_in <= data_right;
                    clipper_fsm_state <= CHECK_RIGHT_CHANNEL_POSITIVE;
                end
            end

            CHECK_RIGHT_CHANNEL_POSITIVE : begin
                fp_greater_comp_data_in_valid <= 1'b0;
                if (fp_greater_comp_data_out_valid == 1'b1) begin
                    if (fp_greater_comp_data_out[0] == 1'b1) begin
                        o_data_right <= 32'h4AF1ADFA;    // 7919357
                        o_data_valid <= 1'b1;
                        clipper_fsm_state <= IDLE;
                    end else begin
                        fp_less_comp_data_in_valid <= 1'b1;
                        fp_less_comp_data_a_in <= data_right;
                        clipper_fsm_state <= CHECK_RIGHT_CHANNEL_NEGATIVE;
                    end
                end
            end

            CHECK_RIGHT_CHANNEL_NEGATIVE : begin
                fp_less_comp_data_in_valid <= 1'b0;
                if (fp_less_comp_data_out_valid == 1'b1) begin
                    if (fp_less_comp_data_out[0] == 1'b1) begin
                        o_data_right <= 32'hCAF1ADFA;    // -7919357
                    end else begin
                        o_data_right <= data_right;
                    end
                    o_data_valid <= 1'b1;
                    clipper_fsm_state <= IDLE;
                end
            end

            default : begin
                clipper_fsm_state <= IDLE;
            end
        endcase
    end
endmodule

Notice how we are comparing the incoming samples against +/-7919356, but clip them to +/-7919357. This guarantees that we will trigger the overflow alarm in our Stereo LED Meter each time we clip the signal.

Clipper Simulation

For the simulation of our clipper module, we will once again use our SystemVerilog WAVE file reader to load a snare hit and use it as an input. However, after converting the samples to floating-point format we multiply them by two to simulate the data values data would engage our clipper. The simulation waveform is shown in Figure 3 below.

Audio Clipper Simulation
Audio Clipper Simulation

The upper analog waveform shows the left input signal, i.e. our snare hit with a 2x gain. The lower analog waveform shows the left output of the clipper. The display in both signals has been scaled to +/-8.388.607, the dynamic range of our audio codec. Before the 1 ms mark the input signal remained within the supported amplitude boundaries, but then we can see that its peak get larger than what could be represented with 24 bits (actually, we can’t see it, as it is out of the range of the display).

We can be absolutely certain that the clipper is being engaged by looking at the floating-point values below the output waveform, they show the right input and right output signals, from top to bottom. There we can see that, while the input keeps changing, the output remains at the +/-7.919.357 clipping value for several samples.

Clipping in Float- to Fixed-Point Conversion

While I was running the simulations for this module, I realized that, for our specific Audio Processor, a clipper might not be needed. Because we perform the float- to fixed-point conversion at the end of our audio-processing pipeline, any value outside of the range of the audio codec will be clipped by the float- to fixed-point converter.

So, did we just waste our time here? Not at all! Having a clipper with an adjustable threshold will always be valuable. On the one hand, we often want to set our clipping value to be somewhat lower than the maximum supported by our codec. And on the other hand, remember that a clipper is also a creative tool, so we must be able to set our threshold and clipping values to whatever we want. We might even use different threshold for each channel or clip the positive samples to a different absolute value than the negative samples.

In the next post we will improve our fixed- to floating- to fixed-point conversion as we introduce a new recurring series in the Blog. Stay tuned!

Cheers,

Isaac


Leave a Reply

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