Unable to decipher I2S ADC data on ESP32 as read from WM8906 Audio Codec

Hi

In using the setup in the title, I’m trying to manipulate the ADC values coming to the ESP from the ADC in the Audio Codec, but I haven’t been able to make sense of the numbers in the array returned (e.g. sBuffer[3]) or find the i2s_read and i2s_write functions in the .cpp file to try to figure it out better. Any help would be greatly appreciated!

Thanks!

Details:

I’m using the ESP32 development board with the WM8906 and things are working great. I’ve run many of the examples, and have settled on Example 8: I2S Passthrough as the baseline for what I want to do: Digital Signal Processing on the audio signal read by the ADC on the CODEC, sent to the ESP32 over I2S, some manipulation, and then sending it back over I2S to the CODEC for output by the DAC.

I have been struggling to figure out how to get the numerical values from the buffer without luck. I thought I understood arrays, but I’m not getting it. I’ve been using the Sparkfun logic analyzer to decode the I2S data, and there I see the equivalent of signed 16 bit integers changing as expected when driven by a sine wave from a function generator (+/- 0.3 volts at 300 Hz, corresponding to about +/-6860).

However, when I try to look at the values in sBufferr from the function call below, I don’t get numbers I can use.

#define bufferLen 64
int16_t sBuffer[bufferLen];

esp_err_t result = i2s_read(I2S_PORT, &sBuffer, bufferLen, &bytesIn, portMAX_DELAY);

or set the buffer values to known values before the call to:

esp_err_t result_w = i2s_write(I2S_PORT, &sBuffer, bytesIn, &bytesOut, portMAX_DELAY);

I’m not having much luck. For example, if I read the buffer once (say in setup) and print the results using

int32_t value;

value=sBuffer[ii];
Serial.println(value,DEC);

Also tried masking out the 16 right most bits and casting to uint16 and int16

uint16_t value_uint16;
int16_t value_int16;

value_uint16=sBuffer[ii]& 0xFFFF;
value_int16=(int16_t)value_uint16;

While driving the left channel only, I get values like 29989, 117, -2878,118,-2967 … in the first 32 slots of the 64 element long buffer, and zeros for the last 32 which seems promising. But these aren’t the expected values for a relatively low frequency sine wave (300 Hz) or the values I’m capturing from the I2S bus with the logic analyzer.
When I tried filling the buffer before the write command with the integers 0 through 63 (sBuffer[i]= value, every other value shows up on the bus analyzer, which seems encouraging, but I haven’t been able to sort it out.

I’ve looked for the source code for i2s_read and i2s_write, without any luck (only the .h file declarations). If anyone knows where that is, I’d appreciate it.

I’ve attempted some different ways to access this array and done a fair bit of googling. I’m guessing that somehow is more levels of indirect addressing than I am understanding.

My goal is to work through the labs from the Prof Tretter’s University of Maryland old digital signal processing for communications lab course and lab text book which is really great material (https://user.eng.umd.edu/~tretter/). I’ve made some progress on another processor, but I really want to do it with an audio CODEC and this seems like exactly the right thing.

Hi -
Update: So I did some more digging, and the Sparkfun example code refers to another project and provides this info and a link to the website:

This code was created using some modified code from DroneBot Workshop.
Specifically, the I2S configuration setup was super helpful to get I2S working.
This example has a similar I2S config to what we are using here: Microphone to
serial plotter example. Although, here we are doing a full duplex I2S port, in
order to do reads and writes. To see the original Drone Workshop code and
learn more about I2S in general, please visit:
Sound with ESP32 - I2S Protocol | DroneBot Workshop

The code there looks very similar to the Sparkfun example I’m using. And an additional section (see below) that would seem to be exactly what I’m looking: it computes the mean/average value of the values read in, buffer read by buffer read . It seems to access the array the same way I am, but implies that the values of interest are in every 8th element of the input buffer, which I don’t understand.

Here are the results when I ran it again using a 300 Hz +/- 300 mV sine wave into one of the ADC channel. The computed mean/average values don’t make much sense to me. Some examples consequitive values from the serial monitor:
-12287.50
-12287.62
-4096.00
-12288.25
-1.25
4094.13
-8191.50
8191.13

The logic analyzer decode of the I2S signal shows the WM8096 is sending out the 300 Hz sine wave with an amplitude of about +/- 6860, so the average value being sometimes so much larger than that doesn’t seem right.

In the mean/average code:

  1. bufferLen is 64 and dimensions a 16 bit (2 byte) signed integer:
    #define bufferLen 64
    int16_t sBuffer[bufferLen];
  2. bytesin appears to be returning 64 (added a print to see)

So why are they averaging every 8th element of element of sBuffer which is 2*64 bytes long.

What am I missing? Any ideas what else I can try?

p.s. If you try to run the Sparkfun example, it doesn’t compile as is (for me anyway). This change was required in i2s_install (it seems the default isn’t assigned).

.mclk_multiple = i2s_mclk_multiple_t(I2S_MCLK_MULTIPLE_512),   // ***!!! mod made to compile

// .mclk_multiple = i2s_mclk_multiple_t(I2S_MCLK_MULTIPLE_DEFAULT),

I did try different values, and saw no change in the behavior. I am confident the example works correctly, as I get the sine wave input into the ADC back as a nice tone on the DAC played into a speaker.


Here’s the code fragment from the web site that produced the above results.

// Get I2S data and place in data buffer
size_t bytesIn = 0;
esp_err_t result = i2s_read(I2S_PORT, &sBuffer, bufferLen, &bytesIn, portMAX_DELAY);

if (result == ESP_OK)
{
// Read I2S data buffer
int16_t samples_read = bytesIn / 8;
if (samples_read > 0) {
float mean = 0;
for (int16_t i = 0; i < samples_read; ++i) {
mean += (sBuffer[i]);
}
// Average the data reading
mean /= samples_read;
// Print to serial plotter
Serial.println(mean);
}

I2S configuration should match the WM8906 settings for data width, word length, and clock settings.Please check all these parameters.

thanks for responding!

So the thing is, the code is the Sparkfun example, and it is working: A signal put in to the ADC on the WM8906 goes to the ESP32 on I2S, gets sent back to the WM8906 and can be heard on the speaker. The numerical values for the I2S are also seen on the logic analyzer in both directions.

What I can’t seem to do is on the ESP32 look at the contents of the I2S buffer being read in and see the numerical values I’d expect or put numbers into the buffer and see them coming out correctly. I can easily believe there’s a trick I’m missing (like needing to shift the results), but looking at the bit patterns I sure haven’t sorted it out.

There’s a comment in the code about how much time should be available to do something between reads and writes (300 microseconds if the buffer size is left at 64), so I was hoping to do somethings like demonstrate amplitude modulation and demodulation algorithms.

Has anyone else tried to use the data from the buffer?

Thanks

Are you applying negative voltages to the ADC input?

There is far too little information in your posts to guess what you are doing wrong. For example, you need to post ALL your code (using code tags), so people can see how the I2S buffer is declared, being accessed, etc.

Thank you for your response! I’m a big fan. I’ve worked with your 9DOF Magnetometer ICM_20948-AHRS project, and learned a lot from it!

Full code posted below. Some notes:

  1. If #define INLINE_PRINT is commented out, it’s the Sparkfun Example code Example_08_I2S_Passthrough for the WM8906 almost exactly (had to replace I2S_MCLK_MULTIPLE_DEFAULT with I2S_MCLK_MULTIPLE_512 to make it compile).
  2. yes, I’m using a negative voltage. That’s the reason for using the WM8906: it handles audio line input and line output. Two pieces of evidence this is working: 1)audio attached to the ADC comes back as audio out the DAC, and 2) Listening to the clear tone and looking at the PulseView Capture of the I2S traffic in both directions decodes as a 300 Hz, +/-6860 is sine wave when the function generator is attached to one of the channels instead of audio feed (see figure). Of course it’s only a clear tone when not doing the INLINE_PRINT.
  3. when #define INLINE_PRINT is not commented out, it’s not a clear tone of course, but it is an attempt to look at the buffer contents.

Here’s some sample output of the buffer as hex from one run, and decimal as another. Format is array element number (Hex): buffer value (Hex for one run, DEC for the other).

Thanks for taking a look at this.

INLINE_PRINT sbuffer:  0:74DA  1:FFFFFFFF  2:FFFFF48B  3:7FFE  4:FFFFF442  5:7FFF  6:73FD  7:7FFF  8:73BD  9:0  A:7382  B:7FFF  C:FFFFF350  D:FFFFFFFF  E:7325  F:7FFF  10:7301  11:FFFF8000  12:72E3  13:FFFFFFFF  14:FFFFF2CA  15:7FFF  16:72B6  17:FFFFFFFF  18:72A9  19:FFFFFFFF  1A:72A0  1B:FFFF8000  1C:729D  1D:7FFF  1E:72A4  1F:7FFF  20:0  21:0  22:0  23:0  24:0  25:0  26:0  27:0  28:0  29:0  2A:0  2B:0  2C:0  2D:0  2E:0  2F:0  30:0  31:0  32:0  33:0  34:0  35:0  36:0  37:0  38:0  39:0  3A:0  3B:0  3C:0  3D:0  3E:0  3F:0 
INLINE_PRINT sbuffer:  0:7C5C  1:FFFF8000  2:7CEB  3:0  4:FFFFFD7A  5:FFFF8000  6:7E0C  7:7FFF  8:FFFFFE9C  9:FFFF8001  A:7F2E  B:7FFF  C:FFFFFFBE  D:FFFF8000  E:FFFF804E  F:FFFF8000  10:E1  11:7FFF  12:FFFF8173  13:FFFF8000  14:207  15:FFFF8000  16:FFFF8297  17:0  18:FFFF8326  19:FFFF8000  1A:FFFF83B5  1B:FFFF8000  1C:43F  1D:FFFFFFFF  1E:FFFF84C9  1F:FFFF8000  20:0  21:0  22:0  23:0  24:0  25:0  26:0  27:0  28:0  29:0  2A:0  2B:0  2C:0  2D:0  2E:0  2F:0  30:0  31:0  32:0  33:0  34:0  35:0  36:0  37:0  38:0  39:0  3A:0  3B:0  3C:0  3D:0  3E:0  3F:0 
INLINE_PRINT sbuffer:  0:FFFFFFC2  1:7FFF  2:54  3:0  4:E5  5:0  6:175  7:0  8:FFFF8205  9:FFFF8000  A:298  B:0  C:FFFF8326  D:FFFF8000  E:3B6  F:1  10:444  11:0  12:FFFF84CE  13:0  14:556  15:0  16:5DB  17:FFFF8000  18:FFFF865C  19:0  1A:6DC  1B:FFFF8000  1C:FFFF8757  1D:FFFFFFFF  1E:FFFF87CF  1F:FFFFFFFF  20:0  21:0  22:0  23:0  24:0  25:0  26:0  27:0  28:0  29:0  2A:0  2B:0  2C:0  2D:0  2E:0  2F:0  30:0  31:0  32:0  33:0  34:0  35:0  36:0  37:0  38:0  39:0  3A:0  3B:0  3C:0  3D:0  3E:0  3F:0 
INLINE_PRINT sbuffer:  0:325  1:FFFF8000  2:3B2  3:FFFF8000  4:440  5:FFFF8000  6:FFFF84C9  7:FFFF8000  8:553  9:FFFFFFFF  A:FFFF85D9  B:FFFF8000  C:65D  D:0  E:FFFF86DD  F:0  10:75A  11:0  12:7D0  13:FFFFFFFF  14:FFFF8844  15:FFFF8000  16:FFFF88B3  17:0  18:920  19:0  1A:987  1B:7FFF  1C:FFFF89EC  1D:FFFF8000  1E:FFFF8A4A  1F:FFFF8000  20:0  21:0  22:0  23:0  24:0  25:0  26:0  27:0  28:0  29:0  2A:0  2B:0  2C:0  2D:0  2E:0  2F:0  30:0  31:0  32:0  33:0  34:0  35:0  36:0  37:0  38:0  39:0  3A:0  3B:0  3C:0  3D:0  3E:0  3F:0
INLINE_PRINT sbuffer:  0:29395  1:-2  2:-3394  3:32767  4:29359  5:-1  6:-3419  7:-1  8:-3426  9:-2  A:-3423  B:32767  C:-3413  D:-1  E:-3396  F:-32768  10:29393  11:-1  12:-3345  13:-1  14:29457  15:0  16:29493  17:-32768  18:29540  19:32767  1A:29594  1B:32767  1C:-3115  1D:32767  1E:-3051  1F:32767  20:0  21:0  22:0  23:0  24:0  25:0  26:0  27:0  28:0  29:0  2A:0  2B:0  2C:0  2D:0  2E:0  2F:0  30:0  31:0  32:0  33:0  34:0  35:0  36:0  37:0  38:0  39:0  3A:0  3B:0  3C:0  3D:0  3E:0  3F:0 
INLINE_PRINT sbuffer:  0:-3396  1:-32768  2:29394  3:32767  4:29423  5:-1  6:-3311  7:-1  8:-3271  9:-1  A:-3226  B:-1  C:-3174  D:32767  E:-3117  F:-32768  10:29716  11:-1  12:-2983  13:-1  14:-2909  15:0  16:29941  17:32767  18:-2745  19:-1  1A:-2655  1B:32767  1C:-2558  1D:-32768  1E:30311  1F:-1  20:0  21:0  22:0  23:0  24:0  25:0  26:0  27:0  28:0  29:0  2A:0  2B:0  2C:0  2D:0  2E:0  2F:0  30:0  31:0  32:0  33:0  34:0  35:0  36:0  37:0  38:0  39:0  3A:0  3B:0  3C:0  3D:0  3E:0  3F:0 
INLINE_PRINT sbuffer:  0:3020  1:-1  2:-29821  3:-1  4:-29896  5:32767  6:-29980  7:-32768  8:2698  9:-1  A:-30160  B:-32768  C:2513  D:32767  E:-30357  F:-32768  10:-30463  11:32767  12:-30572  13:-1  14:2081  15:-1  16:-30806  17:32767  18:-30927  19:0  1A:1715  1B:0  1C:1587  1D:0  1E:1456  1F:0  20:0  21:0  22:0  23:0  24:0  25:0  26:0  27:0  28:0  29:0  2A:0  2B:0  2C:0  2D:0  2E:0  2F:0  30:0  31:0  32:0  33:0  34:0  35:0  36:0  37:0  38:0  39:0  3A:0  3B:0  3C:0  3D:0  3E:0  3F:0 
INLINE_PRINT sbuffer:  0:43  1:32767  2:-103  3:0  4:-248  5:-32768  6:32375  7:-1  8:-536  9:0  A:32089  B:0  C:31946  D:0  E:-961  F:0  10:31665  11:0  12:31528  13:32767  14:-1376  15:0  16:31258  17:-32768  18:31129  19:32767  1A:31001  1B:0  1C:-1891  1D:-1  1E:-2014  1F:32767  20:0  21:0  22:0  23:0  24:0  25:0  26:0  27:0  28:0  29:0  2A:0  2B:0  2C:0  2D:0  2E:0  2F:0  30:0  31:0  32:0  33:0  34:0  35:0  36:0  37:0  38:0  39:0  3A:0  3B:0  3C:0  3D:0  3E:0  3F:0

and the full sketch:

/******************************************************************************
  Example_08_I2S_Passthrough.ino
  Demonstrates reading I2S audio from the ADC, and passing that back to the DAC.
  
  This example sets up analog audio input (on INPUT1s), ADC/DAC enabled as I2S 
  peripheral, sets volume control, and Headphone output on the WM8960 Codec.

  Audio should be connected to both the left and right "INPUT1" inputs, 
  they are labeled "RIN1" and "LIN1" on the board.

  This example will pass your audio source through the mixers and gain stages of 
  the codec into the ADC. Read the audio from the ADC via I2S. Then send audio 
  immediately back to the DAC via I2S. Then send the output of the DAC to the 
  headphone outs.

  Development platform used:
  SparkFun ESP32 IoT RedBoard v10

  HARDWARE CONNECTIONS

  **********************
  ESP32 ------- CODEC
  **********************
  QWIIC ------- QWIIC       *Note this connects GND/3.3V/SDA/SCL
  GND --------- GND         *optional, but not a bad idea
  5V ---------- VIN         *needed to power codec's onboard AVDD (3.3V vreg)
  4 ----------- DDT         *aka DAC_DATA/I2S_SDO/"serial data out", this carries the I2S audio data from ESP32 to codec DAC
  16 ---------- BCK         *aka BCLK/I2S_SCK/"bit clock", this is the clock for I2S audio, can be controlled via controller or peripheral.
  17 ---------- ADAT        *aka ADC_DATA/I2S_SD/"serial data in", this carries the I2S audio data from codec's ADC to ESP32 I2S bus.
  25 ---------- DLRC        *aka I2S_WS/LRC/"word select"/"left-right-channel", this toggles for left or right channel data.
  25 ---------- ALR         *for this example WS is shared for both the ADC WS (ALR) and the DAC WS (DLRC)

  **********************
  CODEC ------- AUDIO IN
  **********************
  GND --------- TRS INPUT SLEEVE        *ground for line level input
  LINPUT1 ----- TRS INPUT TIP           *left audio
  RINPUT1 ----- TRS INPUT RING1         *right audio

  **********************
  CODEC -------- AUDIO OUT
  **********************
  OUT3 --------- TRS OUTPUT SLEEVE          *buffered "vmid" (aka "HP GND")
  HPL ---------- TRS OUTPUT TIP             *left HP output
  HPR ---------- TRS OUTPUT RING1           *right HP output

  You can now control the volume of the codecs built in headphone amp using this 
  fuction:

  codec.setHeadphoneVolumeDB(6.00); Valid inputs are -74.00 (MUTE) up to +6.00, 
  (1.00dB steps).

  Pete Lewis @ SparkFun Electronics
  October 14th, 2022
  https://github.com/sparkfun/SparkFun_WM8960_Arduino_Library
  
  This code was created using some code by Mike Grusin at SparkFun Electronics
  Included with the LilyPad MP3 example code found here:
  Revision history: version 1.0 2012/07/24 MDG Initial release
  https://github.com/sparkfun/LilyPad_MP3_Player

  This code was created using some modified code from DroneBot Workshop.
  Specifically, the I2S configuration setup was super helpful to get I2S working.
  This example has a similar I2S config to what we are using here: Microphone to 
  serial plotter example. Although, here we are doing a full duplex I2S port, in 
  order to do reads and writes. To see the original Drone Workshop code and 
  learn more about I2S in general, please visit:
  https://dronebotworkshop.com/esp32-i2s/

  Do you like this library? Help support SparkFun. Buy a board!

    SparkFun Audio Codec Breakout - WM8960 (QWIIC)
    https://www.sparkfun.com/products/21250
	
	All functions return 1 if the read/write was successful, and 0
	if there was a communications failure. You can ignore the return value
	if you just don't care anymore.

	For information on the data sent to and received from the CODEC,
	refer to the WM8960 datasheet at:
	https://github.com/sparkfun/SparkFun_Audio_Codec_Breakout_WM8960/blob/main/Documents/WM8960_datasheet_v4.2.pdf
  This code is released under the [MIT License](http://opensource.org/licenses/MIT).
  Please review the LICENSE.md file included with this example. If you have any questions 
  or concerns with licensing, please contact techsupport@sparkfun.com.
  Distributed as-is; no warranty is given.
******************************************************************************/

#include <Wire.h>
#include <SparkFun_WM8960_Arduino_Library.h> 
// Click here to get the library: http://librarymanager/All#SparkFun_WM8960
WM8960 codec;

// Include I2S driver
#include <driver/i2s.h>

// Connections to I2S
#define I2S_WS 25
#define I2S_SD 17
#define I2S_SDO 4
#define I2S_SCK 16

// Use I2S Processor 0
#define I2S_PORT I2S_NUM_0

// Define input buffer length
#define bufferLen 64
int16_t sBuffer[bufferLen];

void setup()
{
  Serial.begin(115200);
  Serial.println("Example 8 - I2S Passthough");

  Wire.begin();

  if (codec.begin() == false) //Begin communication over I2C
  {
    Serial.println("The device did not respond. Please check wiring.");
    while (1); // Freeze
  }
  Serial.println("Device is connected properly.");

  codec_setup();

  // Set up I2S
  i2s_install();
  i2s_setpin();
  i2s_start(I2S_PORT);
}

void loop()
{
  // Get I2S data and place in data buffer
  size_t bytesIn = 0;
  size_t bytesOut = 0;
  esp_err_t result = i2s_read(I2S_PORT, &sBuffer, bufferLen, &bytesIn, portMAX_DELAY);

  if (result == ESP_OK)
  {

#define INLINE_PRINT
#ifdef INLINE_PRINT

  int ii;
  int16_t sBuffer_copy[bufferLen];

// copy buffer just in case it's changing
  for(ii=0;ii<bufferLen;ii++){
    sBuffer_copy[ii]=sBuffer[ii];
  }

// print buffer 
  Serial.print("INLINE_PRINT sbuffer:");
  for(ii=0;ii<bufferLen;ii++){
    Serial.print("  ");
    Serial.print(ii,HEX);
    Serial.print(":");
    Serial.print(sBuffer_copy[ii],HEX);
  }
   Serial.println(" ");
  #endif

    // Send what we just received back to the codec
    esp_err_t result_w = i2s_write(I2S_PORT, &sBuffer, bytesIn, &bytesOut, portMAX_DELAY);

    // If there was an I2S write error, let us know on the serial terminal
    if (result_w != ESP_OK)
    {
      Serial.print("I2S write error.");
    }
  }
  // DelayMicroseconds(300); // Only hear to demonstrate how much time you have 
  // to do things.
  // Do not do much in this main loop, or the audio won't pass through correctly.
  // With default settings (64 samples in buffer), you can spend up to 300 
  // microseconds doing something in between passing each buffer of data
  // You can tweak the buffer length to get more time if you need it.
  // When bufferlength is 64, then you get ~300 microseconds
  // When bufferlength is 128, then you get ~600 microseconds
  // Note, as you increase bufferlength, then you are increasing latency between 
  // ADC input to DAC output.
  // Latency may or may not be desired, depending on the project.
}

void codec_setup()
{
  // General setup needed
  codec.enableVREF();
  codec.enableVMID();

  // Setup signal flow to the ADC

  codec.enableLMIC();
  codec.enableRMIC();

  // Connect from INPUT1 to "n" (aka inverting) inputs of PGAs.
  codec.connectLMN1();
  codec.connectRMN1();

  // Disable mutes on PGA inputs (aka INTPUT1)
  codec.disableLINMUTE();
  codec.disableRINMUTE();

  // Set pga volumes
  codec.setLINVOLDB(0.00); // Valid options are -17.25dB to +30dB (0.75dB steps)
  codec.setRINVOLDB(0.00); // Valid options are -17.25dB to +30dB (0.75dB steps)

  // Set input boosts to get inputs 1 to the boost mixers
  codec.setLMICBOOST(WM8960_MIC_BOOST_GAIN_0DB);
  codec.setRMICBOOST(WM8960_MIC_BOOST_GAIN_0DB);

  // Connect from MIC inputs (aka pga output) to boost mixers
  codec.connectLMIC2B();
  codec.connectRMIC2B();

  // Enable boost mixers
  codec.enableAINL();
  codec.enableAINR();

  // Disconnect LB2LO (booster to output mixer (analog bypass)
  // For this example, we are going to pass audio throught the ADC and DAC
  codec.disableLB2LO();
  codec.disableRB2RO();

  // Connect from DAC outputs to output mixer
  codec.enableLD2LO();
  codec.enableRD2RO();

  // Set gainstage between booster mixer and output mixer
  // For this loopback example, we are going to keep these as low as they go
  codec.setLB2LOVOL(WM8960_OUTPUT_MIXER_GAIN_NEG_21DB); 
  codec.setRB2ROVOL(WM8960_OUTPUT_MIXER_GAIN_NEG_21DB);

  // Enable output mixers
  codec.enableLOMIX();
  codec.enableROMIX();

  // CLOCK STUFF, These settings will get you 44.1KHz sample rate, and class-d 
  // freq at 705.6kHz
  codec.enablePLL(); // Needed for class-d amp clock
  codec.setPLLPRESCALE(WM8960_PLLPRESCALE_DIV_2);
  codec.setSMD(WM8960_PLL_MODE_FRACTIONAL);
  codec.setCLKSEL(WM8960_CLKSEL_PLL);
  codec.setSYSCLKDIV(WM8960_SYSCLK_DIV_BY_2);
  codec.setBCLKDIV(4);
  codec.setDCLKDIV(WM8960_DCLKDIV_16);
  codec.setPLLN(7);
  codec.setPLLK(0x86, 0xC2, 0x26); // PLLK=86C226h
  //codec.setADCDIV(0); // Default is 000 (what we need for 44.1KHz)
  //codec.setDACDIV(0); // Default is 000 (what we need for 44.1KHz)
  codec.setWL(WM8960_WL_16BIT);

  codec.enablePeripheralMode();
  //codec.enableMasterMode();
  //codec.setALRCGPIO(); // Note, should not be changed while ADC is enabled.

  // Enable ADCs and DACs
  codec.enableAdcLeft();
  codec.enableAdcRight();
  codec.enableDacLeft();
  codec.enableDacRight();
  codec.disableDacMute();

  //codec.enableLoopBack(); // Loopback sends ADC data directly into DAC
  codec.disableLoopBack();

  // Default is "soft mute" on, so we must disable mute to make channels active
  codec.disableDacMute(); 

  codec.enableHeadphones();
  codec.enableOUT3MIX(); // Provides VMID as buffer for headphone ground

  Serial.println("Volume set to +0dB");
  codec.setHeadphoneVolumeDB(0.00);

  Serial.println("Codec Setup complete. Listen to left/right INPUT1 on Headphone outputs.");
}

void i2s_install() {
  // Set up I2S Processor configuration
  const i2s_driver_config_t i2s_config = {
    .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_TX),
    .sample_rate = 44100,
    .bits_per_sample = i2s_bits_per_sample_t(16),
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_MSB),
    .intr_alloc_flags = 0,
    .dma_buf_count = 8,
    .dma_buf_len = bufferLen,
    .use_apll = false,
    .tx_desc_auto_clear = false,
    .fixed_mclk = 0,
    // change here to allow the code to compile (I2S_MCLK_MULTIPLE_DEFAULT not defined)
    //.mclk_multiple = i2s_mclk_multiple_t(I2S_MCLK_MULTIPLE_DEFAULT),
    .mclk_multiple = i2s_mclk_multiple_t(I2S_MCLK_MULTIPLE_512), 
    .bits_per_chan = i2s_bits_per_chan_t(I2S_BITS_PER_CHAN_DEFAULT)
  };

  i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
}

void i2s_setpin() {
  // Set I2S pin configuration
  const i2s_pin_config_t pin_config = {
    .mck_io_num = I2S_PIN_NO_CHANGE,
    .bck_io_num = I2S_SCK,
    .ws_io_num = I2S_WS,
    .data_out_num = I2S_SDO,
    .data_in_num = I2S_SD
  };

  i2s_set_pin(I2S_PORT, &pin_config);
}

Do you mean the SparkFun Audio Codec Breakout - WM8960? Please post a wiring diagram.

In the mean/average code:

Which is not included in the code example most recently posted. I don’t see anything obviously wrong with the sbuffer sample dump.

Documentation for the wm8906 and schematic:

https://learn.sparkfun.com/tutorials/audio-codec-breakout---wm8960-hookup-guide/all#hardware-hookup

The mean calculation: the Sparkfun example code posted above says:


  This code was created using some modified code from DroneBot Workshop.
  Specifically, the I2S configuration setup was super helpful to get I2S working.
  This example has a similar I2S config to what we are using here: Microphone to 
  serial plotter example. Although, here we are doing a full duplex I2S port, in 
  order to do reads and writes. To see the original Drone Workshop code and 
  learn more about I2S in general, please visit:
  https://dronebotworkshop.com/esp32-i2s/

The dronebot page has code (see below) that is much the same as the Sparkfun code. In one of my variants, I included their mean calculations. I see that a) the calculation is based on averaging the first 1/8th of the 64 element array and b) the second half of the array is zero. Note that the buffer in both sketches is a signed 16 bit integer array of length 64, and that the value of bytesIn in the Sparkfun example is 64 (I didn’t run the dronebotworkshop example).

So the questions is: How are the numerical values in the array to be converted into values corresponding to the ADC measurements?

Given that the input was a 300 Hz +/- 0.3V sine wave being sampled at 44.1 K Hz into one channel and the other channel not connected, the expectation is that the buffer should have a reasonably slowly varying sequence of numbers corresponding to one channel, and zeros or something for the other. I don’t see how to interpret/convert the non zero values to the equivalent ADC 16 bit integer values. These values are nicely seen in data captured using the Sparkfun logic analyzer and decoded into a file using the Sigrok command line version (see Excel plot below).

/*
  ESP32 I2S Microphone Sample
  esp32-i2s-mic-sample.ino
  Sample sound from I2S microphone, display on Serial Plotter
  Requires INMP441 I2S microphone
 
  DroneBot Workshop 2022
  https://dronebotworkshop.com
*/
 
// Include I2S driver
#include <driver/i2s.h>
 
// Connections to INMP441 I2S microphone
#define I2S_WS 25
#define I2S_SD 33
#define I2S_SCK 32
 
// Use I2S Processor 0
#define I2S_PORT I2S_NUM_0
 
// Define input buffer length
#define bufferLen 64
int16_t sBuffer[bufferLen];
 
void i2s_install() {
  // Set up I2S Processor configuration
  const i2s_config_t i2s_config = {
    .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = 44100,
    .bits_per_sample = i2s_bits_per_sample_t(16),
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S),
    .intr_alloc_flags = 0,
    .dma_buf_count = 8,
    .dma_buf_len = bufferLen,
    .use_apll = false
  };
 
  i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
}
 
void i2s_setpin() {
  // Set I2S pin configuration
  const i2s_pin_config_t pin_config = {
    .bck_io_num = I2S_SCK,
    .ws_io_num = I2S_WS,
    .data_out_num = -1,
    .data_in_num = I2S_SD
  };
 
  i2s_set_pin(I2S_PORT, &pin_config);
}
 
void setup() {
 
  // Set up Serial Monitor
  Serial.begin(115200);
  Serial.println(" ");
 
  delay(1000);
 
  // Set up I2S
  i2s_install();
  i2s_setpin();
  i2s_start(I2S_PORT);
 
 
  delay(500);
}
 
void loop() {
 
  // False print statements to "lock range" on serial plotter display
  // Change rangelimit value to adjust "sensitivity"
  int rangelimit = 3000;
  Serial.print(rangelimit * -1);
  Serial.print(" ");
  Serial.print(rangelimit);
  Serial.print(" ");
 
  // Get I2S data and place in data buffer
  size_t bytesIn = 0;
  esp_err_t result = i2s_read(I2S_PORT, &sBuffer, bufferLen, &bytesIn, portMAX_DELAY);
 
  if (result == ESP_OK)
  {
    // Read I2S data buffer
    int16_t samples_read = bytesIn / 8;
    if (samples_read > 0) {
      float mean = 0;
      for (int16_t i = 0; i < samples_read; ++i) {
        mean += (sBuffer[i]);
      }
 
      // Average the data reading
      mean /= samples_read;
 
      // Print to serial plotter
      Serial.println(mean);
    }
  }
}

image

Sorry, I don’t understand the question. The data in the array ARE the ADC measurements.

This code should work as you expect.

    // Read I2S data buffer
    int16_t samples_read = bytesIn / 8;
    if (samples_read > 0) {
      float mean = 0;
      for (int16_t i = 0; i < samples_read; ++i) {
        mean += (sBuffer[i]);
      }
      // Average the data reading
      mean /= samples_read;

If you don’t process the incoming data quickly enough, samples will be dropped. The buffer size of 64 bytes in the posted code is way too small. See this post for an example of dropped samples: https://forums.adafruit.com/viewtopic.php?t=209823

Thank you for responding again! Your thoughtful posts certainly give me a lot to think about.

Based on what you said, I went back to the code with the idea of increasing the duration of the timeout value the read function provides. Looking at it (just below) it seems it’s already set to the maximum.

The header (at the bottom of this post) seems to imply that it triggers a one shot read of 64 values into the buffer (subject to time out if the data data doesn’t fill the buffer before the timeout). I am unclear on how the subsequent print statements interfere with capturing of at 64 consecutive set of values (although clearly discontinuous with the previous or subsequent buffers). However, It seems from your replies to my posts that you believe I have the wrong mental model for how the code should be expected to behave. Your clarification would be greatly appreciated.

esp_err_t result = i2s_read(I2S_PORT, &sBuffer, bufferLen, &bytesIn, portMAX_DELAY);

#define portMAX_DELAY ( TickType_t ) 0xffffffffUL`

Given that without the print statement, the audio loop back clearly works with clean audio, it seems the delay is in fact long enough to allow the buffer to be filled the as expected.

Also checked: the value of bytesIn after the call is 64, which seems to imply all 64 were read in.

I’ve been looking at this reference for the function calls, but have been unable to find the source code itself.
https://docs.espressif.com/projects/esp-idf/en/v4.2.3/esp32/api-reference/peripherals/i2s.html#_CPPv48i2s_read10i2s_port_tPv6size_tP6size_t10TickType_t

Reading the in-line comment below in the Sparkfun example, I had reached the conclusion that once we figured out how to interpret the data, that some minimal amount of processing could be performed on the buffer before outputting it in the subsequent write.

Since break points aren’t available in the Arduino ecosystem, the “one shot” print statement is the only way I’ve seen to debug code that has this sort of coupling with hardware. Your wonderful code that works with the Sparkfun 9 DOF magnetometer code is pretty complex. Can you tell us how you develop and debug?

And as to the question I posed: “So the questions is: How are the numerical values in the array to be converted into values corresponding to the ADC measurements?” that you said you don’t understand, it is this:

Given that:

  1. a Sine Wave is injected with a function analyzer, and
  2. I2S data is captured using the Sparkfun Logic analyzer and saved to a file, and
  3. the command line post processor is used to come up with decimal numerical values from the capture, and
  4. the values plotted in Excel (see earlier post) look to be exactly what would be expected (a sine wave of amplitude +/- 6000+)

The question is: How do you find within the buffer some sequence of values that that approximately correspond to these values?

You said: “I don’t see anything obviously wrong with the sbuffer sample dump.” My humble goal is to find something “obviously right” before proceeding with any digital signal processing.

Thank you again for taking the time to look at this and responding! I look forward to the light I’m sure you will shine on this.

Comments in the code about spare throughput and guidance on buffer size.

  // DelayMicroseconds(300); // Only hear to demonstrate how much time you have 
  // to do things.
  // Do not do much in this main loop, or the audio won't pass through correctly.
  // With default settings (64 samples in buffer), you can spend up to 300 
  // microseconds doing something in between passing each buffer of data
  // You can tweak the buffer length to get more time if you need it.
  // When bufferlength is 64, then you get ~300 microseconds
  // When bufferlength is 128, then you get ~600 microseconds
  // Note, as you increase bufferlength, then you are increasing latency between 
  // ADC input to DAC output.
  // Latency may or may not be desired, depending on the project.

header for the i2sread function call

/**
 * @brief Read data from I2S DMA receive buffer
 *
 * @param i2s_num         I2S port number
 *
 * @param dest            Destination address to read into
 *
 * @param size            Size of data in bytes
 *
 * @param[out] bytes_read Number of bytes read, if timeout, bytes read will be less than the size passed in.
 *
 * @param ticks_to_wait   RX buffer wait timeout in RTOS ticks. If this many ticks pass without bytes becoming available in the DMA receive buffer, then the function will return (note that if data is read from the DMA buffer in pieces, the overall operation may still take longer than this timeout.) Pass portMAX_DELAY for no timeout.
 *
 * @note If the built-in ADC mode is enabled, we should call i2s_adc_enable and i2s_adc_disable around the whole reading process,
 *       to prevent the data getting corrupted.
 *
 * @return
 *     - ESP_OK               Success
 *     - ESP_ERR_INVALID_ARG  Parameter error
 */
esp_err_t i2s_read(i2s_port_t i2s_num, void *dest, size_t size, size_t *bytes_read, TickType_t ticks_to_wait);

What would be obviously right to you?

How do you find within the buffer some sequence of values that that approximately correspond to these values?

Collect a long buffer full of measurements at an appropriate sampling rate, then print them out as signed decimal integers, and look through that list for the expected values, rising and falling in sequence expected for a sine wave input.

I’m not familiar with that I2S library and can’t comment on how it should be used.

I should mention that the Adafruit I2S library, or perhaps the I2S hardware, seems to have a built in automatic offset removal, and the first 200 milliseconds or so of measurements (after the “start data collection” signal is given) are unusable, and must be discarded.

Solved! (with high probability)

TL;DR:
It seems that functions i2s_read and i2s_write are interacting with an on-going interrupt process (maybe even using DMA as the ESP32 has it), and not initiating discrete actions like I assumed. To be able to do calculations with the input like I was trying to do, I should have immediately copied the read buffer into another buffer and then examined that data. I haven’t had a chance to go back and test this, and am not sure if I will. However, the write up below (and much better the video in the first link) should make it clear why I think this is the case.

JRemington said it above:

"If you don’t process the incoming data quickly enough, samples will be dropped.”

Bottom line: I’m back on track working through the material in Prof Tretter’s University of Maryland DSP for Communications text book and labs (link above), as well as Sophocles J. Orfanidis wonderful material from his class at Rutgers (links below).

Thank you all.

The rest of the story:
My first step before trying to do anything more with the WM8906 (which I really like!) was getting an STM32 board and the pmod I2S2 codec board, and implemented the very nice solution Phil has here:
[#5] IIR Filters - Audio DSP On STM32 with I2S (24 Bit / 96 kHz) https://www.youtube.com/watch?v=lNBrGOk0XzE.

This example is exactly what I was looking for, as it shows how to insert some digital signal processing between the ADC input and the DAC output. Worth watching even if you aren’t going to do it yourself. Working through the example’s interrupt routine it’s clear that the I2S keeps getting data and the DMA keeps writing it to the buffer. So a copy needs to be made immediately before the buffer is overwritten.

And please: before anyone comments about the approach in this video not being how “real DSP” would be done, please note that a) its circular buffer, one point at a time is a very straight forward way to understand/think about the processing, and 2) his video
[#15] CMSIS DSP Library - Audio DSP On STM32 (24 Bit / 48 kHz) https://www.youtube.com/watch?v=vCcALaGNlyw
goes on to do it with a larger buffer to improve throughput. I am now a big fan of Phil’s and plan to keep working through his videos.

As an aside, since a circular buffer is used with two interrupts (one for each half) there can be some interesting “artifacts” if you put a break point in one interrupt service routine and leave the other running!

The second step was with the WM8906, which is great board! (vastly more capable than the pmod I2S). Someone was kind enough to share their c code performing the I2C register configurations for the WM8906, and I’ve got it running on an STM32 board.

I hope the reference material above is of interest. I’m having a lot of fun with it.