ATOM-ECHO icon indicating copy to clipboard operation
ATOM-ECHO copied to clipboard

Is full duplex with the mic and speaker possible?

Open joba-1 opened this issue 1 year ago • 0 comments

Hi,

I have several Echos and the examples, especially the record/play, work fine. Now I would like to build intercoms like Echo1 <-> serial or wifi <-> Echo2. It seems this is impossible by hardware design because mic and DAC share same clock pins (shared i2s port?) but have different signal modes (different i2s ports?) Or I just don't get it... In that case please help with a simple (even local mic -> dac) feedback example or fix my attempt below. Thank you!

#include <Arduino.h>

/* Press button to record, release button to playback */

#include <driver/i2s.h>
#include <M5Atom.h>
#include <array>

#define CONFIG_I2S_BCK_PIN     19
#define CONFIG_I2S_LRCK_PIN    33
#define CONFIG_I2S_DATA_PIN    22
#define CONFIG_I2S_DATA_IN_PIN 23

#define SPEAKER_I2S_NUMBER I2S_NUM_1
#define MIC_I2S_NUMBER I2S_NUM_0

#define DATA_SIZE 1024
#define BUFFERS   80


std::array<uint8_t, DATA_SIZE> buffers[BUFFERS];
volatile size_t read_buffer = 0;
size_t write_buffer = 0;


enum intercom_state { TO_IDLE, IDLE, MIC, RELEASE, DELAY, SPEAKER };
const char *intercom_states[] = {"TO_IDLE", "IDLE", "MIC", "RELEASE", "DELAY", "SPEAKER"}; 

class IntercomState { 
    public:
    IntercomState() : state(TO_IDLE) {}
    
    IntercomState &operator=(enum intercom_state new_state) {
        Serial.printf("State %s -> %s\n", intercom_states[state], intercom_states[new_state]);
        state = new_state;
        return *this;
    }

    operator enum intercom_state() const {
        return state;
    }

    private:
    enum intercom_state state; 
};

IntercomState state;


TaskHandle_t micTask;

void micTaskCode( void * parameter ) {
    delay(100);  // let setup() print complete before we do something
    Serial.printf("Mic core: %d\n", xPortGetCoreID());

    for(;;) {
        if (read_buffer < BUFFERS) {
            size_t bytes_read;
            i2s_read(MIC_I2S_NUMBER,
                (char *)buffers[read_buffer].data(), DATA_SIZE,
                &bytes_read, (100 / portTICK_RATE_MS));
            read_buffer++;
        }
        else {
            delay(10);
        }
    }
}


void InstallI2SDriver(i2s_port_t port) {
    esp_err_t err = ESP_OK;

    i2s_config_t i2s_config = {
        .mode        = (i2s_mode_t)(I2S_MODE_MASTER),
        .sample_rate = 16000,
        .bits_per_sample =
            I2S_BITS_PER_SAMPLE_16BIT,  // is fixed at 12bit, stereo, MSB
        .channel_format = I2S_CHANNEL_FMT_ALL_RIGHT,
#if ESP_IDF_VERSION > ESP_IDF_VERSION_VAL(4, 1, 0)
        .communication_format =
            I2S_COMM_FORMAT_STAND_I2S,  // Set the format of the communication.
#else                                   // 设置通讯格式
        .communication_format = I2S_COMM_FORMAT_I2S,
#endif
        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
        .dma_buf_count    = 6,
        .dma_buf_len      = 60,
    };

    // using TX or RX or both seems not to make a difference
    if (port == MIC_I2S_NUMBER) {
        i2s_config.mode =
            (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_TX | I2S_MODE_PDM);
            
    } else {
        i2s_config.mode     = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_TX);
        i2s_config.use_apll = false;
        i2s_config.tx_desc_auto_clear = true;

    }

    err = i2s_driver_install(port, &i2s_config, 0, NULL);

    if (err != ESP_OK) {
        Serial.printf("Install I2S driver %d failed with rc = %d\n", port, err);
    }

    err = i2s_set_clk(port, 16000, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);
    if (err != ESP_OK) {
        Serial.printf("Set I2S clock %d failed with rc = %d\n", port, err);
    }
}


void SetI2SPins(i2s_port_t port) {
    esp_err_t err = ESP_OK;

    i2s_pin_config_t tx_pin_config;

#if (ESP_IDF_VERSION > ESP_IDF_VERSION_VAL(4, 3, 0))
    tx_pin_config.mck_io_num = I2S_PIN_NO_CHANGE;
#endif
    tx_pin_config.bck_io_num   = CONFIG_I2S_BCK_PIN;
    tx_pin_config.ws_io_num    = CONFIG_I2S_LRCK_PIN;

    if (port == MIC_I2S_NUMBER) {
        tx_pin_config.data_out_num = I2S_PIN_NO_CHANGE;
        tx_pin_config.data_in_num  = CONFIG_I2S_DATA_IN_PIN;
    } else {
        tx_pin_config.data_out_num = CONFIG_I2S_DATA_PIN;
        tx_pin_config.data_in_num  = I2S_PIN_NO_CHANGE;
    }

    err = i2s_set_pin(port, &tx_pin_config);
    if (err != ESP_OK) {
        Serial.printf("Set I2S pins %d failed with rc = %d\n", port, err);
    }
}


void setup() {
    M5.begin(true, false, true);
    Serial.printf("Main core: %d\n", xPortGetCoreID());

    i2s_driver_uninstall(MIC_I2S_NUMBER);
    i2s_driver_uninstall(SPEAKER_I2S_NUMBER);

    InstallI2SDriver(SPEAKER_I2S_NUMBER);
    InstallI2SDriver(MIC_I2S_NUMBER);

    SetI2SPins(SPEAKER_I2S_NUMBER);
    SetI2SPins(MIC_I2S_NUMBER);

    xTaskCreatePinnedToCore(
      micTaskCode, /* Function to implement the task */
      "MicTask",   /* Name of the task */
      10000,       /* Stack size in words */
      NULL,        /* Task input parameter */
      0,           /* Priority of the task */
      &micTask,    /* Task handle. */
      0);          /* Core where the task should run */
}


void loop() {
    uint32_t delay_start;

    switch (state) {
        case TO_IDLE:
            M5.dis.drawpix(0, CRGB(0, 0, 255));
            state = IDLE;
            break;
        case IDLE:
            if (M5.Btn.isPressed()) {
                // start recording
                M5.dis.drawpix(0, CRGB(255, 0, 0));
                read_buffer = 0;
                SetI2SPins(MIC_I2S_NUMBER);  // TODO remove for continuous operation
                state = MIC;
            }
            break;
        case MIC:
            if (M5.Btn.isReleased() || read_buffer >= BUFFERS) {
                write_buffer = read_buffer;
                read_buffer = BUFFERS;
                M5.dis.drawpix(0, CRGB(255, 255, 255));
                state = RELEASE;
            }
            break;
        case RELEASE:
            if (M5.Btn.isReleased()) {
                delay_start = millis();

                // amplify low sounds
                int16_t maxAmplitude = 0;
                for (size_t i = 0; i < write_buffer; i++) {
                    int16_t *sample = (int16_t *)buffers[i].data();
                    int16_t *end = (int16_t *)(buffers[i].data() + DATA_SIZE);
                    while (sample < end) {
                        if (*sample > maxAmplitude) maxAmplitude = *sample;
                        double amplitude = abs((double)*sample)/INT16_MAX;
                        amplitude = (log10(amplitude+0.001)+3)/(log10(1+0.001)+3)*INT16_MAX;
                        *sample = *sample < 0 ? (int16_t)-amplitude : (int16_t)amplitude;
                        sample++;
                    }
                }
                Serial.println(maxAmplitude);

                state = DELAY;
            }
            break;
        case DELAY:
            if (millis() - delay_start > 100) {
                M5.dis.drawpix(0, CRGB(0, 255, 0));
                SetI2SPins(SPEAKER_I2S_NUMBER);  // TODO remove for continuous operation
                state = SPEAKER;
            }
            break;
        case SPEAKER:
            for (size_t i = 0; i < write_buffer; i++) {
                size_t bytes_written;
                i2s_write(SPEAKER_I2S_NUMBER, (char *)buffers[i].data(), 
                    DATA_SIZE, &bytes_written, portMAX_DELAY);
            }
            state = TO_IDLE;
            break;
    }
    M5.update();
}

joba-1 avatar Oct 17 '24 22:10 joba-1