The long awaited revamp of Chirp for Arduino is here, with send and receive capabilities, available to use straight away from the Arduino library manager.

The SDK currently only supports ESP32 modules (and Microsoft MXChip), which are available to buy for as little as $3. At this price and with a built in WiFi module, this is the perfect candidate for IoT projects.

ESP32 development board

TLDR: Skip straight to the working example code here.


Requirements

  • Arduino IDE >= v1.8.6
  • ESP32 development board
  • SPH0645 digital MEMS microphone (or equivalent for input)
  • UDA1334 I2S DAC (or equivalent for output)

Setup

To get started with the ESP32, you will first of all need to install the platform on Arduino IDE. You will also need to sign up for Chirp credentials at developers.chirp.io.

  1. Open Arduino / Preferences and add the following to the Additional Board Manager URLs field in the Arduino IDE.
https://dl.espressif.com/dl/package_esp32_index.json

2. Open the Boards Manager from the Tools / Boards menu, and install the ESP32 platform.

3. Create a Chirp account at developers.chirp.io if you haven't done so already. Here you can get your Chirp application key / secret, and access the arduino config which will configure the audio properties of the Chirp SDK for Arduino.


Chirp SDK

To start the Chirp SDK running you will need to configure the SDK, and set the callbacks (in this example we will just use the received callback). There are other callbacks for state changes, and when data has started to be sent or received. See chirp_connect_callbacks.h for more details.

#include <chirp_connect.h>
#include <chirp_connect_callbacks.h>

chirp_connect_t *connect = NULL;

void 
onReceivedCallback(void *connect, uint8_t *payload, size_t length, uint8_t channel)
{
  if (payload) {
    char *hexString = chirp_connect_as_string(connect, payload, length);
    Serial.printf("Received data = %s\n", hexString);
  } else {
    Serial.println("Decode failed.");
  }
}

void 
setupChirp()
{
  connect = new_chirp_connect(APP_KEY, APP_SECRET);
  chirp_connect_set_config(connect, APP_CONFIG);

  chirp_connect_callback_set_t callbacks = {0};
  callbacks.on_received = onReceivedCallback;
  chirp_connect_set_callbacks(connect, callbacks);
  
  chirp_connect_set_callback_ptr(connect, connect);
  chirp_connect_start(connect);
  chirp_connect_set_volume(connect, 0.5);
}

void
initTask(void *parameter)
{
  setupChirp();  
  // Set up audio drivers ..
  vTaskDelete(NULL);
}

In our set up function we will create a FreeRTOS task to initialise the ChirpSDK and audio drivers. Chirp's DSP functions require a little more room on the stack than the default, so we will dedicate 16kB to this.

void
setup() 
{
  Serial.begin(115200);
  xTaskCreate(initTask, "initTask", 16384, NULL, 1, NULL);
}

Audio Input

Now we need to configure the I2S audio driver to receive audio data from the digital MEMS microphone. This is very simple to do using the esp32-arduino software.

#include <driver/i2s.h>

#define I2SI_DATA         12     // I2S DATA IN on GPIO12
#define I2SI_BCK          14     // I2S BCLK on GPIO14
#define I2SI_LRCL         15     // I2S SELECT on GPIO15

void 
setupAudioInput(int sample_rate) 
{
  const i2s_config_t i2s_config = {
      .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX),
      .sample_rate = sample_rate,
      .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
      .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,
      .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),
      .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
      .dma_buf_count = 4,
      .dma_buf_len = 128,
      .use_apll = true
  };

  const i2s_pin_config_t pin_config = {
      .bck_io_num = I2SI_BCK,
      .ws_io_num = I2SI_LRCL,
      .data_out_num = I2S_PIN_NO_CHANGE,
      .data_in_num = I2SI_DATA
  };

  i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM_0, &pin_config);
  err = i2s_set_sample_rates(I2S_NUM_0, sample_rate);
}

You can retrieve the input sample rate at which the Chirp SDK is configured to run as below.

uint32_t sample_rate = chirp_connect_get_input_sample_rate(connect);

We will create another task to run the processing functions. The ESP32 module has a dual core processor, so we can make use of this for the input and output processing.

#define MIC_CALIBRATION   13625
#define convert(sample) (((int32_t)(sample) >> 14) + MIC_CALIBRATION)

void
initTask(void *parameter)
{
  setupChirp();

  uint32_t output_sample_rate = chirp_connect_get_output_sample_rate(connect);
  setupAudioOutput((int)output_sample_rate);
}

void
processInputTask(void *parameter)
{
  size_t bytesLength = 0;
  float buffer[BUFFER_SIZE] = {0};
  int32_t ibuffer[BUFFER_SIZE] = {0};

  while (currentState >= CHIRP_CONNECT_STATE_RUNNING) {
    i2s_read(I2S_NUM_0, ibuffer, BUFFER_SIZE * 4, &bytesLength, portMAX_DELAY);
    if (bytesLength) {
      for (int i = 0; i < bytesLength / 4; i++) {
        buffer[i] = (float)CONVERT_INPUT(ibuffer[i]);
      }
      chirp_connect_process_input(connect, buffer, bytesLength / 4);
    }
  }
  vTaskDelete(NULL);
}

Notice the convert macro in the above code - this is to convert the 32bit values from the microphone which are placed directly in memory using DMA, to floating point values which are required by the Chirp SDK.

https://cdn-shop.adafruit.com/product-files/3421/i2S+Datasheet.PDF
The data format from the microphone is 24 bit, 2’s compliment, MSB first. The Data Precision is 18 bits, unused bits are zeros.

The convert  macro casts the values to signed 32 bit integers to convert from 2's compliment, and shifts by 14 to get the 18bits of data in the correct position with the least significant bit in bit 0.

Unfortunately the audio data from the MEMS microphone is not centred exactly about zero, so does require some calibration - see the MIC_CALIBRATION value. I determined this offset using the Serial Plotter functionality of the Arduino IDE. (You may find that the calibration value is different for your microphone).

You can print the audio data to the Serial Plotter just using Serial.printf.

for (int i = 0; i < bytesLength / 4; i++) {
  Serial.printf("%.6f\n", (float)convert(ibuffer[i]));
}

Once your microphone is calibrated, you're received callback should be triggered by playing the sound below.

Audio Output

A similar process is required to set up the I2S audio output.

#include <driver/i2s.h>

#define I2SO_DATA         23     // I2S DATA OUT on GPIO23
#define I2SO_BCK          18     // I2S BCLK on GPIO18
#define I2SO_WSEL         5      // I2S SELECT on GPIO5

void 
setupAudioOutput(int sample_rate) 
{
  const i2s_config_t i2s_config = {
      .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_TX),
      .sample_rate = sample_rate,
      .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
      .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,
      .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_LSB),
      .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
      .dma_buf_count = 8,
      .dma_buf_len = 64,
      .use_apll = true
  };

  const i2s_pin_config_t pin_config = {
      .bck_io_num = I2SO_BCK,
      .ws_io_num = I2SO_WSEL,
      .data_out_num = I2SO_DATA,
      .data_in_num = I2S_PIN_NO_CHANGE
  };

  i2s_driver_install(I2S_NUM_1, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM_1, &pin_config);
  err = i2s_set_sample_rates(I2S_NUM_1, sample_rate);
}
void
processOutputTask(void *parameter)
{
  size_t bytesLength = 0;
  short buffer[BUFFER_SIZE] = {0};
  int32_t ibuffer[BUFFER_SIZE] = {0};

  while (currentState >= CHIRP_CONNECT_STATE_RUNNING) {
    chirp_connect_process_shorts_output(connect, buffer, BUFFER_SIZE);
    for (int i = 0; i < BUFFER_SIZE; i++) {
      ibuffer[i] = (int32_t)buffer[i];
    }
    i2s_write(I2S_NUM_1, ibuffer, BUFFER_SIZE * 4, &bytesLength, portMAX_DELAY);
  }
  vTaskDelete(NULL);
}

The UDA1334 accept 16bits of data per channel by default. The ChirpSDK can also output data as shorts as well as floats, so we make use of this functionality to transfer to the output DMA buffer.

Finally, once the init task has finished we start the processing tasks running. The input processing task is given a higher priority than the output processing task, as it requires more complex DSP to process the input audio.

void
loop()
{
  if (startTasks) {
    xTaskCreate(processInputTask, "processInputTask", 16384, NULL, 5, NULL);
    xTaskCreate(processOutputTask, "processOutputTask", 16384, NULL, 3, NULL);
    startTasks = false;
  }

The example code also shows how you can send a random chirp by pressing the BOOT switch available on the ESP32 board.

if (buttonPressed) {
  size_t payloadLength = 0;
  uint8_t *payload = chirp_connect_random_payload(connect, &payloadLength);
  chirp_connect_send(connect, payload, payloadLength);
  buttonPressed = false;
}

By default the SDK will listen to itself, so you should be able to press the switch and hear a chirp, and see that it is decoded in the terminal.


Chirp for Arduino is still in Beta, so if you find any bugs please create an issue at the GitHub repo.

joe@chirp.io