Delay Lines

class tiliqua.delay_line.DelayLine(*args, src_loc_at=0, **kwargs)

SRAM- or PSRAM-backed audio delay line.

This forms the backbone of many different types of effects - echoes, pitch shifting, chorus, feedback synthesis etc.

Usage

Each DelayLine instance operates in a single-writer, multiple-reader fashion - that is, for each DelayLine, there may be only one stream of samples being written, however from each DelayLine you may create N instances of DelayLineTap, which are submodules of DelayLine used to produce output streams (read operations) on the DelayLine.

For a simple, SRAM-backed delay line, the following is sufficient:

delayln = DelayLine(
    max_delay=8192,
    write_triggers_read=False,
)

From this, you can create some read taps:

tap1 = delayln.add_tap()
tap2 = delayln.add_tap()

The DelayLine instance requires a single incoming stream DelayLine.i, on which incoming samples are taken and written to the backing store.

Each DelayLineTap instance requires both an incoming and outgoing stream, DelayLineTap.i, DelayLineTap.o, where an output sample is only produced some time after the requested delay count has arrived on DelayLineTap.i.

This gives applications the flexibility to read multiple times per write sample (useful for example for fractional delay lines where we want to interpolate between two adjacent samples).

Fixed (simple) delay taps

It can be a bit cumbersome to need to provide each tap with an input stream if you just want some taps with fixed delays.

So, if you want a simple fixed delay tap, you can use the write_triggers_read=True option when creating the DelayLine. Then, you can specify explicit fixed delay taps as follows:

delayln = DelayLine(max_delay=8192, write_triggers_read=True)
tap1    = delayln.add_tap(fixed_delay=5000)
tap2    = delayln.add_tap(fixed_delay=7000)

Note

When used in this mode, tap1 and tap2 will internally have their inputs (sample request streams) hooked up to the write strobe. This means you no longer need to hook up tapX.i and will automatically get a single sample on each tapX.o after every write to delayln.

Backing store

The backing store is a contiguous region of memory where samples are written to a wrapped incrementing index (i.e circular buffer fashion).

The same memory space is shared by all read & write operations, however the way this works is slightly different when comparing SRAM- and PSRAM- backed delay lines. In both cases, all read & write operations go through an arbiter and share the same memory bus.

  • SRAM-backed delay line

    The memory bus is connected directly to an FPGA DPRAM instantiation and does not need to be connected to any external memory bus.

  • PSRAM-backed delay line

    Due to the memory access latency of PSRAM, simply forwarding each read/write access would quickly consume memory bandwidth simply due to the access latency. So, in the PSRAM case, a small cache is inserted between the internal delay line R/W bus and the memory bus exposed by DelayLine.bus (normally hooked up to the PSRAM). The purpose of this cache is to collect as many read & write operations into burstable transactions as possible.

Note

As each delayline contains completely different samples and individually has quite a predictable access pattern, it makes sense to have one cache per DelayLine, rather than one larger shared cache (which would likely perform worse considering area/bandwidth). The important factor is that all writes and reads on the same delayline share the same cache, as the write and read taps have the same working set.

Members:
  • i (In(stream.Signature(ASQ))) – Input stream for writing samples to the delay line.

  • bus (Out(wishbone.Signature)) – Wishbone bus for connecting to external PSRAM (usually through an arbiter). Only present for PSRAM-backed delay lines.

__init__(max_delay, psram_backed=False, addr_width_o=None, base=None, write_triggers_read=True, cache_kwargs=None)
max_delayint

The maximum delay in samples. This exactly corresponds to the memory required in the backing store. Must be a power of 2.

psram_backedbool, optional

If True, the delay line is backed by PSRAM. Otherwise, it is backed by SRAM.

addr_width_oint, optional

Required for PSRAM-backed delay lines. The address width of the external memory bus.

baseint, optional

Required for PSRAM-backed delay lines. The memory slice base address. This is the physical address offset in bytes.

write_triggers_readbool, optional

If True, writing to the delay line triggers a read. This means the DelayLineTap.i stream does not need to be connected

cache_kwargsdict, optional

Relevant only for PSRAM-backed delay lines. Arguments to forward to creation of the internal memory cache.

add_tap(fixed_delay=None)

Add and return a new DelayLineTap to stream samples read from this DelayLine.

Note

Each tap automatically becomes a submodule of the DelayLine instance. That is, you only need to add DelayLine itself to m.submodules.

fixed_delayint

The DelayLineTap.i is automatically set to a fixed delay. Only used when write_triggers_read=True.

class tiliqua.delay_line.DelayLineTap(*args, src_loc_at=0, **kwargs)

A single read tap of a parent DelayLine. See DelayLine top-level comment for information on usage. DelayLineTap should only be created using DelayLine.add_tap().

Members:
  • i (In(unsigned(N))) – Stream of delays requested to be read from the delay line. The unit is in number of samples behind the last written sample to the delay line.

  • o (Out(stream.Signature(ASQ))) – Stream of samples read from the delay line, one per request on DelayLineTap.i.