I2S Audio Test

I had Claude write up an I2S Test for a speaker, specifically the MAX98357A. Here's the program it wrote:

#include <driver/i2s.h>

// I2S pins for ESP32-S3 Feather
#define I2S_BCLK_PIN       8  // Bit clock
#define I2S_LRCLK_PIN      9  // Left/Right clock (Word Select)
#define I2S_DATA_PIN       10  // Data pin

// I2S configuration
#define SAMPLE_RATE     44100  // Audio sample rate in Hz
#define BITS_PER_SAMPLE    16  // Bits per sample
#define CHANNEL_NUM         2  // Number of channels (stereo)

// DMA buffer parameters
#define DMA_BUF_COUNT      8  // Number of DMA buffers
#define DMA_BUF_LEN      256  // Size of each DMA buffer

void setup() {
    Serial.begin(115200);

    // Configure I2S
    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_t)BITS_PER_SAMPLE,
        .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
        .communication_format = I2S_COMM_FORMAT_STAND_I2S,
        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
        .dma_buf_count = DMA_BUF_COUNT,
        .dma_buf_len = DMA_BUF_LEN,
        .use_apll = false,
        .tx_desc_auto_clear = true,
        .fixed_mclk = 0
    };

    // Configure I2S pins
    i2s_pin_config_t pin_config = {
        .mck_io_num = I2S_PIN_NO_CHANGE,
        .bck_io_num = I2S_BCLK_PIN,
        .ws_io_num = I2S_LRCLK_PIN,
        .data_out_num = I2S_DATA_PIN,
        .data_in_num = I2S_PIN_NO_CHANGE
    };

    // Install and start I2S driver
    esp_err_t result = i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
    if (result != ESP_OK) {
        Serial.println("Error installing I2S driver");
        return;
    }

    result = i2s_set_pin(I2S_NUM_0, &pin_config);
    if (result != ESP_OK) {
        Serial.println("Error setting I2S pins");
        return;
    }

    Serial.println("I2S initialized successfully");
}

// Example function to write a sine wave to I2S
void writeSineWave() {
    // Generate a 440 Hz sine wave
    const float frequency = 440.0;  // A4 note
    const float amplitude = 32000;  // Volume (max is 32767 for 16-bit)

    // Buffer for audio samples
    int16_t samples[DMA_BUF_LEN * 2];  // *2 for stereo

    static float phase = 0.0;
    const float phase_increment = 2.0 * PI * frequency / SAMPLE_RATE;

    // Fill the buffer with sine wave samples
    for (int i = 0; i < DMA_BUF_LEN * 2; i += 2) {
        int16_t sample = (int16_t)(amplitude * sin(phase));
        samples[i] = sample;      // Left channel
        samples[i + 1] = sample;  // Right channel

        phase += phase_increment;
        if (phase >= 2.0 * PI) {
            phase -= 2.0 * PI;
        }
    }

    // Write the samples to I2S
    size_t bytes_written;
    i2s_write(I2S_NUM_0, samples, sizeof(samples), &bytes_written, portMAX_DELAY);
}

void loop() {
    // Continuously output the sine wave
    writeSineWave();
}

It worked right away! This program will play a 440 Hz tone.

I then had Claude update the code to read an MP3 file from the SD Card. Here's the functioning code:

#include <Arduino.h>
#include "FS.h"
#include "SD.h"
#include "SPI.h"
#include <driver/i2s.h>
#include "AudioFileSourceSD.h"
#include "AudioFileSourceID3.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"

// Pin definitions
#define I2S_BCLK_PIN       8  // Bit clock
#define I2S_LRCLK_PIN      9  // Left/Right clock (Word Select)
#define I2S_DATA_PIN       10  // Data pin

// Audio objects
AudioFileSourceSD *file;
AudioFileSourceID3 *id3;
AudioGeneratorMP3 *mp3;
AudioOutputI2S *out;

void setup() {
    Serial.begin(115200);
    while (!Serial) {
        delay(10);
    }
    Serial.println("Initializing...");

    // Initialize SD card using the working method
    if (!SD.begin()) {
        Serial.println("Card Mount Failed");
        return;
    }

    uint8_t cardType = SD.cardType();
    if (cardType == CARD_NONE) {
        Serial.println("No SD card attached");
        return;
    }

    Serial.print("SD Card Type: ");
    if (cardType == CARD_MMC) {
        Serial.println("MMC");
    } else if (cardType == CARD_SD) {
        Serial.println("SDSC");
    } else if (cardType == CARD_SDHC) {
        Serial.println("SDHC");
    } else {
        Serial.println("UNKNOWN");
    }

    // Check if test.mp3 exists
    if (!SD.exists("/test.mp3")) {
        Serial.println("Can't find /test.mp3");
        return;
    }
    Serial.println("Found test.mp3");

    // Set up I2S output
    out = new AudioOutputI2S();
    out->SetPinout(I2S_BCLK_PIN, I2S_LRCLK_PIN, I2S_DATA_PIN);
    out->SetGain(0.5); // Set volume (0.0-1.0)

    // Set up MP3 decoder
    file = new AudioFileSourceSD("/test.mp3");
    id3 = new AudioFileSourceID3(file);
    mp3 = new AudioGeneratorMP3();

    Serial.println("Starting MP3...");
    mp3->begin(id3, out);
}

void loop() {
    if (mp3->isRunning()) {
        if (!mp3->loop()) {
            // File is finished playing
            mp3->stop();
            Serial.println("MP3 playback completed");
            delay(1000);

            // Restart playback
            Serial.println("Restarting playback...");
            file->open("/test.mp3");
            mp3->begin(id3, out);
        }
    } else {
        Serial.println("MP3 not running, attempting to restart...");
        delay(1000);
        file->open("/test.mp3");
        mp3->begin(id3, out);
    }
}

This one took a few tries, but next I generated some code to record via a microphone and then play it back over the speaker:

#include <Arduino.h>
#include "FS.h"
#include "SD.h"
#include "SPI.h"
#include <driver/i2s.h>

// Pin Definitions
// Microphone (SPH0645)
#define I2S_MIC_SCK     8
#define I2S_MIC_WS      9
#define I2S_MIC_SD      10

// Speaker
#define I2S_SPKR_BCLK   8
#define I2S_SPKR_LRC    9
#define I2S_SPKR_DIN    10

// Constants for recording
const int RECORD_TIME = 5;  // seconds to record
const int SAMPLE_RATE = 44100;
const int SAMPLE_BITS = 32;
const int WAV_HEADER_SIZE = 44;
const char* RECORD_FILE = "/recording.wav";
const int BINARY_BUFFER_SIZE = 1024;

// Global flag to track I2S state
bool i2s_initialized = false;

// WAV header structure
struct WavHeader {
    char riff[4] = {'R', 'I', 'F', 'F'};
    uint32_t fileSize = 0;
    char wave[4] = {'W', 'A', 'V', 'E'};
    char fmt[4] = {'f', 'm', 't', ' '};
    uint32_t fmtSize = 16;
    uint16_t audioFormat = 1;
    uint16_t numChannels = 1;
    uint32_t sampleRate = SAMPLE_RATE;
    uint32_t byteRate = SAMPLE_RATE * 2;
    uint16_t blockAlign = 2;
    uint16_t bitsPerSample = 16;
    char data[4] = {'d', 'a', 't', 'a'};
    uint32_t dataSize = 0;
};

void cleanup_i2s() {
    if (i2s_initialized) {
        i2s_stop(I2S_NUM_0);
        i2s_driver_uninstall(I2S_NUM_0);
        i2s_initialized = false;
        delay(100);
    }
}

void i2s_mic_init() {
    cleanup_i2s();

    // Configuration for SPH0645
    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_LEFT,
        .communication_format = I2S_COMM_FORMAT_STAND_I2S,
        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
        .dma_buf_count = 4,
        .dma_buf_len = 1024,
        .use_apll = false,
        .tx_desc_auto_clear = false,
        .fixed_mclk = 0
    };

    i2s_pin_config_t pin_config = {
        .mck_io_num = I2S_PIN_NO_CHANGE,
        .bck_io_num = I2S_MIC_SCK,
        .ws_io_num = I2S_MIC_WS,
        .data_out_num = I2S_PIN_NO_CHANGE,
        .data_in_num = I2S_MIC_SD
    };

    esp_err_t err = i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
    if (err != ESP_OK) {
        Serial.printf("Failed to install I2S driver for mic: %d\n", err);
        return;
    }

    err = i2s_set_pin(I2S_NUM_0, &pin_config);
    if (err != ESP_OK) {
        Serial.printf("Failed to set I2S pins for mic: %d\n", err);
        return;
    }

    i2s_initialized = true;
    delay(100);
}

void i2s_speaker_init() {
    cleanup_i2s();

    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_LEFT,
        .communication_format = I2S_COMM_FORMAT_STAND_I2S,
        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
        .dma_buf_count = 8,
        .dma_buf_len = 1024,
        .use_apll = false,
        .tx_desc_auto_clear = true,
        .fixed_mclk = 0
    };

    i2s_pin_config_t pin_config = {
        .mck_io_num = I2S_PIN_NO_CHANGE,
        .bck_io_num = I2S_SPKR_BCLK,
        .ws_io_num = I2S_SPKR_LRC,
        .data_out_num = I2S_SPKR_DIN,
        .data_in_num = I2S_PIN_NO_CHANGE
    };

    esp_err_t err = i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
    if (err != ESP_OK) {
        Serial.printf("Failed to install I2S driver for speaker: %d\n", err);
        return;
    }

    err = i2s_set_pin(I2S_NUM_0, &pin_config);
    if (err != ESP_OK) {
        Serial.printf("Failed to set I2S pins for speaker: %d\n", err);
        return;
    }

    i2s_initialized = true;
    delay(100);
}

void writeWavHeader(File file, size_t dataSize) {
    WavHeader header;
    header.fileSize = dataSize + WAV_HEADER_SIZE - 8;
    header.dataSize = dataSize;
    file.write((const uint8_t*)&header, WAV_HEADER_SIZE);
}

void startRecording() {
    Serial.println("Starting recording...");

    i2s_mic_init();

    if (!i2s_initialized) {
        Serial.println("Failed to initialize I2S for recording");
        return;
    }

    if (SD.exists(RECORD_FILE)) {
        SD.remove(RECORD_FILE);
    }

    File file = SD.open(RECORD_FILE, FILE_WRITE);
    if (!file) {
        Serial.println("Failed to open file for recording");
        return;
    }

    // Write placeholder header
    WavHeader header;
    file.write((const uint8_t*)&header, WAV_HEADER_SIZE);

    size_t bytesWritten = 0;
    unsigned long startTime = millis();
    int32_t samples[BINARY_BUFFER_SIZE/4];

    Serial.println("Recording...");

    while ((millis() - startTime) < (RECORD_TIME * 1000)) {
        size_t bytesRead = 0;
        esp_err_t result = i2s_read(I2S_NUM_0, samples, sizeof(samples), &bytesRead, portMAX_DELAY);

        if (result == ESP_OK && bytesRead > 0) {
            // Process SPH0645 data: 24-bit signed integer in MSB format
            int16_t converted[BINARY_BUFFER_SIZE/8];
            for (int i = 0; i < bytesRead/4; i++) {
                // Convert 24-bit to 16-bit with proper scaling
                converted[i] = (int16_t)(samples[i] >> 14);
            }
            file.write((const uint8_t*)converted, bytesRead/2);
            bytesWritten += bytesRead/2;
        }
    }

    // Update WAV header with final size
    file.seek(0);
    writeWavHeader(file, bytesWritten);
    file.close();

    cleanup_i2s();
    Serial.println("Recording finished!");
}

void playRecording() {
    Serial.println("Playing recording...");

    // Initialize speaker
    i2s_speaker_init();
    if (!i2s_initialized) {
        Serial.println("Failed to initialize I2S for playback");
        return;
    }

    // Open WAV file
    File file = SD.open(RECORD_FILE);
    if (!file) {
        Serial.println("Failed to open file for playback");
        return;
    }

    // Skip WAV header
    file.seek(WAV_HEADER_SIZE);

    // Read and play file
    uint8_t buffer[1024];
    size_t bytes_read;
    Serial.println("Starting playback...");

    while (file.available()) {
        bytes_read = file.read(buffer, sizeof(buffer));
        if (bytes_read > 0) {
            size_t bytes_written = 0;

            // Amplify the signal
            int16_t *samples = (int16_t*)buffer;
            for(int i=0; i < bytes_read/2; i++) {
              samples[i] = samples[i] * 4;  // Multiply by 2-8 for more volume
            }

            esp_err_t result = i2s_write(I2S_NUM_0, buffer, bytes_read, &bytes_written, portMAX_DELAY);
            if (result != ESP_OK) {
                Serial.printf("Failed to write I2S data: %d\n", result);
            }
        }
    }

    file.close();
    cleanup_i2s();
    Serial.println("Playback finished!");
}

void setup() {
    Serial.begin(115200);
    while (!Serial) delay(10);

    Serial.println("Initializing...");

    // Initialize SD card
    if (!SD.begin()) {
        Serial.println("Card Mount Failed");
        return;
    }

    // Make sure I2S is clean at startup
    cleanup_i2s();

    Serial.println("Ready! Send 'R' to start recording.");
}

void loop() {
    if (Serial.available()) {
        char cmd = Serial.read();
        if (cmd == 'R' || cmd == 'r') {
            startRecording();
            delay(500);
            playRecording();
        }
    }
}

And it works great! I'm able to record a short message and play it back over the speaker!