Echo

This example shows how to use PyGears to implement a hardware module that applies echo audio effect to a continuous audio stream. For a more detailed explanation of PyGears features used in this example, you can checkout a quick introduction to PyGears.

The hardware module is defined in examples/echo/echo.py, and block diagram is given below. You can checkout the functional description of the echo module. In-depth explanation of the PyGears description of the echo model given in hardware description chapter. PyGears takes the Python module description and compiles it to SystemVerilog which is listed below.

_images/echo.png

SystemVerilog Generation

Run the script examples/echo/echo_svgen.py in order to let PyGears generate SystemVerilog files.

cd <pygears_source_dir>/examples/echo
python echo_svgen.py

If you have Vivado installed (you can download a free WebPack version from Xilinx website), the script will automatically try to synthesize the design and display the resource utilization report (displayed also below).

Running Simulation

Run the script examples/echo/plop_test_wav_echo_sim.py to run a simulation of the echo module. The simulation is run with the help of the Verilator tool, so please checkout the installation instructions if you need help with installing the Verilator. Furthermore, if you would like to see plots of the audio waves, you need to install matplotlib. You can run the echo example like this:

cd <pygears_source_dir>/examples/echo
python plop_test_wav_echo_sim.py

Upon starting the script, the following info should be displayed:

Audio file "plop.wav":

    Channels     : 2
    Framerate    : 48000
    Sample width : 2 Bytes
    Sample num   : 165359

-  [INFO]: Running sim with seed: ...
0 /echo [INFO]: Verilating...
0 /echo [WARNING]: Verilator compiled with warnings. Please inspect "..."
0 /echo [INFO]: Verilator VCD dump to "..."
0 /echo [INFO]: Done
0  [INFO]: -------------- Simulation start --------------
165459  [INFO]: ----------- Simulation done ---------------
165459  [INFO]: Elapsed: 31.78
Result length: 165359

Upon completion, the resulting wave will be saved in the file build/plop_echo.wav. If you installed matplotlib, the plots of the original and the resulting audio waves should be displayed.

_images/echo_plot.png

Simulation log will display path to the simulation wave file in standard VCD. Wave can be viewed for an example with an open-source tool GTKWave.

_images/echo_vcd.png

You can now play with the parameters in plop_test_wav_echo_sim.py script. Try changing echo delay and gain settings and check the results.

Stereo Echo

PyGears lets you easily compose gears at any level. To create a stereo echo effect gear, we will instantiate one echo gear for each channel.

_images/stereo_echo.png

In PyGears this can be described as follows:

@gear
def stereo_echo(
        din,  # audio samples
        *,
        feedback_gain,  # feedback gain == echo gain
        sample_rate,  # sample_rate in samples per second
        delay,  # delay in seconds
        precision=15):

    mono_echo = echo(
        feedback_gain=feedback_gain,
        sample_rate=sample_rate,
        delay=delay,
        precision=precision)

    return din | fmap(f=(mono_echo, mono_echo))

The input interface din of the stereo_echo module, needs to carry two samples, one for each channel. This can be represented as a Tuple data type. If the samples are 16 bits wide, the stereo data should be of the type Tuple[Int[16], Int[16]], which can be displayed more succinctly as (i16, 116). Other parameters of the stereo_echo gear have the same meaning as the echo gear parameters.

First, a version of echo gear is created, with some of its parameters supplied/set. This is akin to the partial function application:

mono_echo = echo(
    feedback_gain=feedback_gain,
    sample_rate=sample_rate,
    delay=delay,
    precision=precision)

The mono_echo variable now points to the echo gear, but also carries the information about parameter settings for feedback_gain, sample_rate, delay and precision. Even though the echo function seems to be called, it will not be instantiated at this moment. The reason is that the input interface din was not connected, i.e. it has not been supplied as a parameter. PyGears will instantiate a gear only when all of its input interfaces are supplied.

Next, the input interface din is connected to the two echo gears. For this we will rely on fmap to split the data from din into two components, feed each of the components to the individual echo gear, and then combine the result. In more functional terms, fmap applies the echo functions to each item of the din data tuple, i.e. fmap is a polymorphic functor. Checkout a short presentation of useful functors used in PyGears.

return din | fmap(f=(mono_echo, mono_echo))

The output interface of the fmap gear will also be output interface of the stereo_echo gear.

You can run the cosimulation of the stereo design by setting stereo=True in examples/echo/plop_test_wav_echo_sim.py.

Functional description

The echo module operates as follows: audio samples arrive at the echo module input din, echo is added and the resulting samples are output to the module output dout. In PyGears terms this is a single-input, single-output gear (function), with the following declaration:

echo(din: Int['W'], *, feedback_gain, sample_rate, delay, precision=15, sample_width=b'W')

Performs echo audio effect on the continuous input sample stream

Parameters

din – Stream of audio samples

Keyword Arguments
  • feedback_gain (float) – gain of the feedback loop

  • sample_rate (int) – samples per second

  • delay (float) – delay in seconds

  • precision (int) – sample fixed point precision

  • sample_width (int) – sample width in bits

Returns

  • dout - Stream of audio samples with applied echo

As you can see the echo gear has few more parameters besides din: feedback_gain, sample_rate, delay and precision. These are declared after the ‘*’ symbol, which makes them keyword-only arguments in Python, which in turn makes them compile-time parameters in PyGears. These are akin to HDL parameters or generics.

Notice that the din argument has also a type associated with it, namely a template Int['W'] which represents signed integers of arbitrary width. Take a look at the short explanation on how types are used in PyGears. Int type is generic in the number of bits, hence echo gear can work on samples of arbitrary width. The actual width of the input will be set by echo gear parent, i.e the module that instantiates echo:

def mono_echo_sim(seq,
                  sample_rate,
                  sample_width,
                  cosim=True,
                  feedback_gain=0.5,
                  delay=0.250,
                  stereo=True):
    sample_bit_width = 8 * sample_width

    result = []
    drv(t=Int[sample_bit_width], seq=seq) \
        | echo(feedback_gain=feedback_gain,
               sample_rate=sample_rate,
               delay=delay,
               sim_cls=SimVerilated if cosim else None) \
        | collect(result=result, samples_num=len(seq))

    sim(outdir='./build')

    return result

In mono_echo_sim() function, drv gear is used to drive audio samples to the echo gear, which in turn sends the result to the collect gear. In PyGears the connection from drv to echo can be described using pipe ‘|’ operator. The drv gear sends the sequence of audio samples (variable seq), by first converting them to the specified data type: Int[sample_bit_width], where sample_bit_width value is calculated based on the mono_echo_sim() function argument sample_width.

_images/echo_sim.png

At compile time, PyGears will try to match output data type of drv gear: Int[sample_bit_width] to the input data type Int['W'] of the echo gear, and since that their base types (Int) match, deduce the value of the template parameter W = sample_bit_width. This parameter can then be used throughout the gear signature to calculate values of a gear compile-time parameters or fix types of a gear interfaces. In this example, value of the echo gear argument sample_width is set to be exactly equal to W.

Conveniently, echo gear accepts also some floating point arguments, but these then need to be converted in order to be used for parametrizing hardware modules. This is done at the beginning of the function:

    sample_dly_len = sample_rate * delay
    fifo_depth = ceil_pow2(sample_dly_len)
    feedback_gain_fixp = din.dtype(
        float_to_fixp(feedback_gain, precision, sample_width))

Since echo delay is given in seconds, it needs to be calculated in terms of the number of samples: variable sample_dly_len in the code. Then, feedback loop fifo needs to be deep enough to store the delayed samples. Current implementation of the fifo module in PyGears demands its depth to be a power of 2. Hence, function ceil_pow2 is used to calculate the smallest power of 2 that can accommodate selected delay: variable fifo_depth in the code.

Feedback loop gain is also given as a floating point number and needs to be converted to its fixed-point representation: variable feedback_gain_fixp. Width of the fixed-point gain is chosen to be equal to the width of the audio samples received at din, which will be available via the sample_width argument. Calculated fixed-point value is then cast to the type of the din interface: feedback_gain_fixp = din.dtype(...).

Hardware description

After the compile-time parameters calculation in the echo function, the description of the actual hardware is given.

    dout = Intf(din.dtype)

    feedback = dout \
        | fifo(depth=fifo_depth, threshold=sample_dly_len, regout=True) \
        | fill_void(fill=din.dtype(0)) \
        | decoupler

    feedback_attenuated = (feedback * feedback_gain_fixp) >> precision

    dout |= (din + feedback_attenuated) | dout.dtype

    return dout
_images/echo.png

The feedback loop, present in the design, cannot be described as a plain gear composition since it forms a cycle. This cycle needs to be cut at one spot, described as the gear composition and then stitched together. In this example, we will cut the cycle after the adder and start by defining the interface dout:

dout = Intf(din.dtype)

At this moment, this interface has no source (producer), which has to be attended to later, when we stitch the cycle. We will now connect dout interface to the FIFO, which we will in turn connect to the Fill Void gear:

feedback = dout \
    | fifo(depth=fifo_depth, threshold=sample_dly_len) \
    | fill_void(fill=din.dtype(0)) \
    | decoupler

The FIFO gear is declared in fifo.py, and its SystemVerilog description is given in fifo.sv. In the echo gear FIFO is used to delay the output audio samples before adding them back to the input stream. Parameters depth=fifo_depth and threshold=sample_dly_len are set using the values whose calculations were described earlier. Parameter threshold tells the FIFO the number of data it needs to contain before it starts outputting them. When threshold=0, the FIFO outputs the data immediately.

The function of the Fill Void gear is to supply the feedback loop with zeros until there are enough samples (sample_dly_len of them) in the FIFO, at which moment the FIFO will start outputting the delayed samples. The definition of the Fill Void gear is given in the same file:

@gear
def fill_void(din, fill):
    return priority_mux(din, fill) \
        | union_collapse

The priority_mux gives priority to the din interface over the fill interface. In other words, if the data is available at the din interface, it will be passed to the output of the priority_mux gear. Otherwise, the data from the fill interface is output. priority_mux also outputs some additional information (i.e. from which input interface the data was passed) which is not needed here, so union_collapse gear is used to filter it out.

Back to the echo function. Finally the output interface of the fill_void gear is assigned to the variable feedback. This variable can now be used to connect this interface to the multiplier:

feedback_attenuated = (feedback * feedback_gain_fixp) >> precision

The output of the multiplier is then connected to the SHR gear, whose output interface is in turn assigned to the variable feedback_attenuated. Adder is then instantiated and din and feedback_attenuated interfaces are connected to it. Multiplication, shifting and addition operations change the bit width of the data, so we need restore the width of the data to the input data width. This is done by the cast to the input data type din.dtype. Finally, we stitch the feedback cycle back, by connecting the output interface of the adder to the previously declared dout interface:

dout |= (din + feedback_attenuated) | din.dtype

At the end of the echo function implementation, we declare which of the interfaces will be output outside of the echo gear:

return dout

Given description of the echo gear is translated by the PyGears into the SystemVerilog module given below:

Generated SystemVerilog

module echo(
    input clk,
    input rst,
    dti.consumer din, // i16 (16)
    dti.producer dout // i16 (16)

);

      dti #(.W_DATA(16)) fifo_s(); // i16 (16)

      dti #(.W_DATA(16)) fill_void_s(); // i16 (16)

      dti #(.W_DATA(16)) const0_s(); // i16 (16)

      dti #(.W_DATA(16)) feedback_s(); // i16 (16)

      dti #(.W_DATA(32)) mul_s(); // i32 (32)

      dti #(.W_DATA(16)) const1_s(); // i16 (16)

      dti #(.W_DATA(32)) feedback_attenuated_s(); // i32 (32)

      dti #(.W_DATA(4)) const2_s(); // u4 (4)

      dti #(.W_DATA(33)) add_s(); // i33 (33)

      dti #(.W_DATA(16)) dout_s(); // i16 (16)
      dti #(.W_DATA(16)) dout_s_bc[1:0](); // i16 (16)
    bc #(
                .SIZE(2'd2)
    )
    bc_dout_s (
        .clk(clk),
        .rst(rst),
        .din(dout_s),
        .dout(dout_s_bc)
    );

    connect connect_dout_s_1 (
        .clk(clk),
        .rst(rst),
        .din(dout_s_bc[1]),
        .dout(dout)
    );


    fifo #(
                .DEPTH(15'd16384),
                .THRESHOLD(14'd12000)
    )
    fifo_i (
        .clk(clk),
        .rst(rst),
        .din(dout_s_bc[0]),
        .dout(fifo_s)
    );


    echo_fill_void fill_void_i (
        .clk(clk),
        .rst(rst),
        .din(fifo_s),
        .fill(const0_s),
        .dout(fill_void_s)
    );


    sustain #(
                .TOUT(5'd16)
    )
    const0_i (
        .clk(clk),
        .rst(rst),
        .dout(const0_s)
    );


    decoupler decoupler_i (
        .clk(clk),
        .rst(rst),
        .din(fill_void_s),
        .dout(feedback_s)
    );


    mul #(
                .DIN0(5'd16),
                .DIN0_SIGNED(1'd1),
                .DIN1(5'd16),
                .DIN1_SIGNED(1'd1)
    )
    mul_i (
        .clk(clk),
        .rst(rst),
        .din0(feedback_s),
        .din1(const1_s),
        .dout(mul_s)
    );


    sustain #(
                .VAL(15'd19660),
                .TOUT(5'd16)
    )
    const1_i (
        .clk(clk),
        .rst(rst),
        .dout(const1_s)
    );


    shr #(
                .SIGNED(1'd1)
    )
    shr_i (
        .clk(clk),
        .rst(rst),
        .din(mul_s),
        .cfg(const2_s),
        .dout(feedback_attenuated_s)
    );


    sustain #(
                .VAL(4'd15),
                .TOUT(3'd4)
    )
    const2_i (
        .clk(clk),
        .rst(rst),
        .dout(const2_s)
    );


    add #(
                .DIN0(5'd16),
                .DIN0_SIGNED(1'd1),
                .DIN1(6'd32),
                .DIN1_SIGNED(1'd1)
    )
    add_i (
        .clk(clk),
        .rst(rst),
        .din0(din),
        .din1(feedback_attenuated_s),
        .dout(add_s)
    );


    echo_cast_dout cast_dout_i (
        .clk(clk),
        .rst(rst),
        .din(add_s),
        .dout(dout_s)
    );



endmodule

Resource Utilization

If you have Vivado tool on your path while running the examples/echo/echo_svgen.py script, the following report will be produced. Some of the echo submodules are missing from the list, since Vivado tried to merged the modules in order to reduce the resource utilization.

Instance

Total LUTs

Logic LUTs

LUTRAMs

SRLs

FFs

RAMB36

RAMB18

DSP48 Blocks

wrap_echo

88

88

0

0

72

8

0

1

  • echo_i

88

88

0

0

72

8

0

1

    • add_i

16

16

0

0

0

0

0

0

    • bc_dout_s

5

5

0

0

2

0

0

0

    • decoupler_i

24

24

0

0

38

0

0

0

    • fifo_i

42

42

0

0

30

8

0

0

    • fill_void_i

1

1

0

0

2

0

0

0

      • priority_mux_i

1

1

0

0

2

0

0

0

    • mul_i

0

0

0

0

0

0

0

1