Page 1 of 1

How to synchronize read/write streams with I2S?

Posted: Wed Apr 12, 2023 3:06 am
by danjulio
I have a bi-directional (ADC/DAC) codec connected to an ESP32 I2S (MCLK, SCLK, LRCLK, DIN, DOUT). I have a dedicated task on CPU 1 that writes data from a circular buffer to the codec and reads data from the codec into another circular buffer. The I2S driver is configured with 2 DMA buffers sized to hold all the data written or read at a time. This works fine without any data loss. IDF is v4.4.4.

The application is for interfacing the Bluetooth HF protocol to a traditional POTS telephone line using a SLIC (Subscriber Line Interface Circuit aka "Hybrid"). Because of the way the SLIC works some of the audio sent to it from the DAC is always reflected back to the ADC so I have to implement a Line Echo Cancellation algorithm (LEC) or the person at the other end of the conversation will hear themselves echoed back a few hundred milliseconds later (over the cellular connection). I'm attempting to use the proven open source OSLEC library which was designed to handle just this case. This is different than an Acoustic Echo Cancellation algorithm used for a speakerphone or headphone. It requires a non-changing relationship in time between the TX data sent to the codec and the RX data received from the codec a little later (the echoed data).

Ideal pseudo-code for the task looks like

Code: Select all

// Sample rate is 8000 Hz so we pick a number of samples equal to one FreeRTOS tick (10 mSec)
#define NUM_SAMPLES 80

int16_t tx_buf[NUM_SAMPLES];
int16_t rx_buf[NUM_SAMPLES];

int bytes_written, bytes_read;   // In the real code these are checked to make sure all data was sent or received

while (true) {
	get_tx_data(NUM_SAMPLES, tx_buf);   // tx_buf holds data to write to DAC (2 bytes per sample: one channel, 16-bit data)
	i2s_write(I2S_NUM_0, (void*) tx_buf, NUM_SAMPLES * 2, &bytes_written, portMAX_DELAY);
	i2s_read(I2S_NUM_0, (void*) rx_buf, NUM_SAMPLES * 2, &bytes_read, portMAX_DELAY);
	
	// Execute the LEC
	for (int i=0; i<NUM_SAMPLES; i++) {
		// Echo cancellation machine works sample by sample
		//   Output is echo cancelled rx input
		//   tx_buf data leads rx_buf data in time by some number of samples (< ~50 or 60) but that relationship must remain fixed
		rx_buf[i] = echo_can_update(tx_buf[i], rx_buf[i]);
	}
	
	put_rx_data(NUM_SAMPLES, rx_buf);
}
However I cannot seem to find a way to establish a fixed relationship between data I load into i2s_write and data I read from I2s_read. In the example above the rx_buf data follows the tx_buf data by one or more NUM_SAMPLES increments *BUT* I cannot find a way to fix that. It can change from run to run (boot to boot).

Does anyone know a mechanism to fix the relationship in time between the TX and RX buffers being handed to the I2S driver? In a way it doesn't matter what that relationship is (within reasonable audio bounds) as I can implement additional buffering on the TX stream to bring the TX and RX data close together when the data is handed to the LEC.