diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index b97e965e1..b0b923863 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -66,7 +66,7 @@ void setupModules() #endif #ifdef ARCH_ESP32 // Only run on an esp32 based device. - new AudioModule(); + audioModule = new AudioModule(); new ExternalNotificationModule(); storeForwardModule = new StoreForwardModule(); diff --git a/src/modules/esp32/AudioModule.cpp b/src/modules/esp32/AudioModule.cpp index 7ff46bf10..a47f47bd8 100644 --- a/src/modules/esp32/AudioModule.cpp +++ b/src/modules/esp32/AudioModule.cpp @@ -23,25 +23,36 @@ Basic Usage: 1) Enable the module by setting audio.codec2_enabled to 1. - 2) Set the pins (audio.mic_pin / audio.amp_pin) for your preferred microphone and amplifier GPIO pins. - On tbeam, recommend to use: + 2a) Set the pins for the I2S interface if you want to use digital audio. Recommended on TLora is I2S_WS 13/I2S_SD 15/I2S_SIN 2/I2S_SCK 14 + 2b) Set the pins (audio.mic_pin / audio.amp_pin) for your preferred microphone and amplifier GPIO pins if you want to use analog audio. + This is rather heavy on the CPU and not recommended. + On tbeam, best use: audio.mic_chan 6 (GPIO 34) audio.amp_pin 14 audio.ptt_pin 39 - 3) Set audio.timeout to the amount of time to wait before we consider - your voice stream as "done". - 4) Set audio.bitrate to the desired codec2 rate (CODEC2_3200, CODEC2_2400, CODEC2_1600, CODEC2_1400, CODEC2_1300, CODEC2_1200, CODEC2_700, CODEC2_700B) + 3) Set audio.bitrate to the desired codec2 rate (CODEC2_3200, CODEC2_2400, CODEC2_1600, CODEC2_1400, CODEC2_1300, CODEC2_1200, CODEC2_700, CODEC2_700B) KNOWN PROBLEMS - * Until the module is initilized by the startup sequence, the amp_pin pin is in a floating - radio_state. This may produce a bit of "noise". - * Will not work on NRF and the Linux device targets. + * Until the module is initilized by the startup sequence, the amp_pin is in a floating + state. This may produce a bit of "noise". + * Will not work on NRF and the Linux device targets (yet?). */ #define AMIC 6 #define AAMP 14 #define PTT_PIN 39 +ButterworthFilter hp_filter(240, 8000, ButterworthFilter::ButterworthFilter::Highpass, 1); + +// Use I2S Processor 0 +#define I2S_PORT I2S_NUM_0 + +#define AUDIO_MODULE_RX_BUFFER 128 +#define AUDIO_MODULE_MODE ModuleConfig_AudioConfig_Audio_Baud_CODEC2_700 + +TaskHandle_t codec2HandlerTask; +AudioModule *audioModule; + #ifdef ARCH_ESP32 // ESP32 doesn't use that flag #define YIELD_FROM_ISR(x) portYIELD_FROM_ISR() @@ -49,36 +60,13 @@ #define YIELD_FROM_ISR(x) portYIELD_FROM_ISR(x) #endif -// #define I2S_WS 13 -// #define I2S_SD 15 -// #define I2S_SIN 2 -// #define I2S_SCK 14 - -// Use I2S Processor 0 -#define I2S_PORT I2S_NUM_0 - -#define AUDIO_MODULE_RX_BUFFER 128 -#define AUDIO_MODULE_MODE ModuleConfig_AudioConfig_Audio_Baud_CODEC2_700 - -AudioModule *audioModule; -Codec2Thread *codec2Thread; - -FastAudioFIFO audio_fifo; -uint16_t adc_buffer[ADC_BUFFER_SIZE] = {}; -uint16_t adc_buffer_index = 0; portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED; -int16_t speech[ADC_BUFFER_SIZE] = {}; -volatile RadioState radio_state = RadioState::tx; -adc1_channel_t mic_chan = (adc1_channel_t)0; - -ButterworthFilter hp_filter(240, 8000, ButterworthFilter::ButterworthFilter::Highpass, 1); +hw_timer_t* adcTimer = NULL; //int16_t 1KHz sine test tone int16_t Sine1KHz[8] = { -21210 , -30000, -21210, 0 , 21210 , 30000 , 21210, 0 }; int Sine1KHz_index = 0; -uint8_t rx_raw_audio_value = 127; - int IRAM_ATTR local_adc1_read(int channel) { uint16_t adc_value; #if CONFIG_IDF_TARGET_ESP32S3 @@ -102,8 +90,8 @@ int IRAM_ATTR local_adc1_read(int channel) { IRAM_ATTR void am_onTimer() { portENTER_CRITICAL_ISR(&timerMux); //Enter crital code without interruptions - if ((radio_state == RadioState::tx) && (!moduleConfig.audio.i2s_sd)) { - adc_buffer[adc_buffer_index++] = (16 * local_adc1_read(mic_chan)) - 32768; + if ((audioModule->radio_state == RadioState::tx) && (!moduleConfig.audio.i2s_sd)) { + audioModule->adc_buffer[audioModule->adc_buffer_index++] = (16 * local_adc1_read(audioModule->mic_chan)) - 32768; //If you want to test with a 1KHz tone, comment the line above and descomment the three lines below @@ -111,91 +99,90 @@ IRAM_ATTR void am_onTimer() // if (Sine1KHz_index >= 8) // Sine1KHz_index = 0; - if (adc_buffer_index == ADC_BUFFER_SIZE) { - adc_buffer_index = 0; - DEBUG_MSG("♪♫♪ memcpy\n"); - memcpy((void*)speech, (void*)adc_buffer, 2 * ADC_BUFFER_SIZE); - // Notify codec2 task that the buffer is ready. + if (audioModule->adc_buffer_index == audioModule->adc_buffer_size) { + audioModule->adc_buffer_index = 0; + memcpy((void*)audioModule->speech, (void*)audioModule->adc_buffer, 2 * audioModule->adc_buffer_size); BaseType_t xHigherPriorityTaskWoken = pdFALSE; - DEBUG_MSG("♪♫♪ notifyFromISR\n"); - codec2Thread->notifyFromISR(&xHigherPriorityTaskWoken, RadioState::tx, true); - if (xHigherPriorityTaskWoken) - portYIELD_FROM_ISR(); + vTaskNotifyGiveFromISR(codec2HandlerTask, &xHigherPriorityTaskWoken); + if (xHigherPriorityTaskWoken == pdTRUE) + YIELD_FROM_ISR(xHigherPriorityTaskWoken); } - } else if ((radio_state == RadioState::rx) && (!moduleConfig.audio.i2s_din)) { + } else if ((audioModule->radio_state == RadioState::rx) && (!moduleConfig.audio.i2s_din)) { // ESP32-S3 does not have DAC support #if !defined(CONFIG_IDF_TARGET_ESP32S3) - int16_t v; //Get a value from audio_fifo and convert it to 0 - 255 to play it in the ADC - if (audio_fifo.get(&v)) - rx_raw_audio_value = (uint8_t)((v + 32768) / 256); + if (audioModule->fifo.get(&audioModule->sample)) + audioModule->rx_raw_audio_value = (uint8_t)((audioModule->sample + 32768) / 256); - dacWrite(moduleConfig.audio.amp_pin ? moduleConfig.audio.amp_pin : AAMP, rx_raw_audio_value); + dacWrite(moduleConfig.audio.amp_pin ? moduleConfig.audio.amp_pin : AAMP, audioModule->rx_raw_audio_value); #endif } portEXIT_CRITICAL_ISR(&timerMux); // exit critical code } -Codec2Thread::Codec2Thread() : concurrency::NotifiedWorkerThread("Codec2Thread") { - if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { - DEBUG_MSG("♪♫♪ Setting up codec2 in mode %u", (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); - codec2_state = codec2_create((moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); - codec2_set_lpc_post_filter(codec2_state, 1, 0, 0.8, 0.2); - encode_codec_size = (codec2_bits_per_frame(codec2_state) + 7) / 8; - encode_frame_num = Constants_DATA_PAYLOAD_LEN / encode_codec_size; - encode_frame_size = encode_frame_num * encode_codec_size; // max 237 bytes - DEBUG_MSG(" using %d frames of %d bytes for a total payload length of %d bytes\n", encode_frame_num, encode_codec_size, encode_frame_size); - } else { - DEBUG_MSG("♪♫♪ Codec2 disabled (AudioModule %d, Region %s, permitted %d)\n", moduleConfig.audio.codec2_enabled, myRegion->name, myRegion->audioPermitted); +void run_codec2(void* parameter) +{ + while (true) { + uint32_t tcount = ulTaskNotifyTake(pdFALSE, pdMS_TO_TICKS(10000)); + + if (tcount != 0) { + if (audioModule->radio_state == RadioState::tx) { + + for (int i = 0; i < audioModule->adc_buffer_size; i++) + audioModule->speech[i] = (int16_t)hp_filter.Update((float)audioModule->speech[i]); + + codec2_encode(audioModule->codec2, audioModule->tx_encode_frame + audioModule->tx_encode_frame_index, audioModule->speech); + + //increment the pointer where the encoded frame must be saved + audioModule->tx_encode_frame_index += audioModule->encode_codec_size; + + //If it this is reached we have a ready trasnmission frame + if (audioModule->tx_encode_frame_index == audioModule->encode_frame_size) + { + //Transmit it + DEBUG_MSG("♪♫♪ Sending %d bytes\n", audioModule->encode_frame_size); + audioModule->sendPayload(); + audioModule->tx_encode_frame_index = 0; + } + } + if (audioModule->radio_state == RadioState::rx) { + //Make a cycle to get each codec2 frame from the received frame + for (int i = 0; i < audioModule->rx_encode_frame_index; i += audioModule->encode_codec_size) + { + //Decode the codec2 frame + codec2_decode(audioModule->codec2, audioModule->output_buffer, audioModule->rx_encode_frame + i); + + if (moduleConfig.audio.i2s_din) { + size_t bytesOut = 0; + + i2s_write(I2S_PORT, &audioModule->output_buffer, audioModule->adc_buffer_size, &bytesOut, pdMS_TO_TICKS(40)); + } else { + //Put the decoded audio in the fifo + for (int j = 0; j < audioModule->adc_buffer_size; j++) + audioModule->fifo.put(audioModule->output_buffer[j]); + } + } + } + } } } -AudioModule::AudioModule() : SinglePortModule("AudioModule", PortNum_AUDIO_APP), concurrency::OSThread("AudioModule") { - audio_fifo.init(); - new Codec2Thread(); - //debug - moduleConfig.audio.i2s_ws = 13; - moduleConfig.audio.i2s_sd = 15; - moduleConfig.audio.i2s_din = 2; - moduleConfig.audio.i2s_sck = 14; -} - -void IRAM_ATTR Codec2Thread::onNotify(uint32_t notification) +AudioModule::AudioModule() : SinglePortModule("AudioModule", PortNum_AUDIO_APP), concurrency::OSThread("AudioModule") { - switch (notification) { - case RadioState::tx: - for (int i = 0; i < ADC_BUFFER_SIZE; i++) - speech[i] = (int16_t)hp_filter.Update((float)speech[i]); - - codec2_encode(codec2_state, audioModule->tx_encode_frame + tx_encode_frame_index, speech); - - //increment the pointer where the encoded frame must be saved - tx_encode_frame_index += encode_codec_size; - - //If it this is reached we have a ready trasnmission frame - if (tx_encode_frame_index == encode_frame_size) - { - tx_encode_frame_index = 0; - //Transmit it - audioModule->sendPayload(); - } - break; - case RadioState::rx: - //Make a cycle to get each codec2 frame from the received frame - for (int i = 0; i < encode_frame_size; i += encode_codec_size) - { - //Decode the codec2 frame - codec2_decode(codec2_state, output_buffer, audioModule->rx_encode_frame + i); - - // Add to the audio buffer the 320 samples resulting of the decode of the codec2 frame. - for (int g = 0; g < ADC_BUFFER_SIZE; g++) - audio_fifo.put(output_buffer[g]); - } - break; - default: - assert(0); // We expected to receive a valid notification from the ISR - break; + if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { + fifo.init(); + DEBUG_MSG("♪♫♪ Setting up codec2 in mode %u", (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); + codec2 = codec2_create((moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); + codec2_set_lpc_post_filter(codec2, 1, 0, 0.8, 0.2); + encode_codec_size = (codec2_bits_per_frame(codec2) + 7) / 8; + encode_frame_num = Constants_DATA_PAYLOAD_LEN / encode_codec_size; + encode_frame_size = encode_frame_num * encode_codec_size; // max 237 bytes + adc_buffer_size = codec2_samples_per_frame(codec2); + DEBUG_MSG(" using %d frames of %d bytes for a total payload length of %d bytes\n", encode_frame_num, encode_codec_size, encode_frame_size); + xTaskCreate(&run_codec2, "codec2_task", 30000, NULL, 5, &codec2HandlerTask); + } else { + DEBUG_MSG("♪♫♪ Codec2 disabled (AudioModule %d, Region %s, permitted %d)\n", moduleConfig.audio.codec2_enabled, myRegion->name, myRegion->audioPermitted); } } @@ -214,9 +201,9 @@ int32_t AudioModule::runOnce() .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S), - .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, + .intr_alloc_flags = 0, .dma_buf_count = 8, - .dma_buf_len = ADC_BUFFER_SIZE, // 320 * 2 bytes + .dma_buf_len = adc_buffer_size, // 320 * 2 bytes .use_apll = false, .tx_desc_auto_clear = true, .fixed_mclk = 0 @@ -239,9 +226,20 @@ int32_t AudioModule::runOnce() if(res != ESP_OK) DEBUG_MSG("♪♫♪ Failed to start I2S: %d\n", res); } + + // setup analogue DAC only if we don't use I2S for output. This is not available on ESP32-S3 +#if !defined(CONFIG_IDF_TARGET_ESP32S3) + if (!moduleConfig.audio.i2s_din) + DEBUG_MSG("♪♫♪ Initializing DAC on Pin %u\n", moduleConfig.audio.amp_pin ? moduleConfig.audio.amp_pin : AAMP); +#else + if (!moduleConfig.audio.i2s_din) { + DEBUG_MSG("♪♫♪ ESP32-S3 does not support DAC. Audio Module Disabled.\n"); + return INT32_MAX; + } +#endif if (!moduleConfig.audio.i2s_sd) { - // Set up ADC if we don't have a digital microphone. + // Set up ADC if we don't have a digital microphone. DEBUG_MSG("♪♫♪ Initializing ADC on Channel %u\n", moduleConfig.audio.mic_chan ? moduleConfig.audio.mic_chan : AMIC); mic_chan = moduleConfig.audio.mic_chan ? (adc1_channel_t)(int)moduleConfig.audio.mic_chan : (adc1_channel_t)AMIC; adc1_config_width(ADC_WIDTH_12Bit); @@ -249,8 +247,6 @@ int32_t AudioModule::runOnce() adc1_get_raw(mic_chan); } - radio_state = RadioState::rx; - if ((!moduleConfig.audio.i2s_sd) || (!moduleConfig.audio.i2s_din)) { // Start a timer at 8kHz to sample the ADC and play the audio on the DAC, but only if we have analogue audio to process uint32_t cpufreq = getCpuFrequencyMhz(); @@ -275,11 +271,8 @@ int32_t AudioModule::runOnce() timerAlarmEnable(adcTimer); } - // setup analogue DAC only if we don't use I2S for output. This is not available on ESP32-S3 -#if !defined(CONFIG_IDF_TARGET_ESP32S3) - if (moduleConfig.audio.i2s_din) - DEBUG_MSG("♪♫♪ Initializing DAC on Pin %u\n", moduleConfig.audio.amp_pin ? moduleConfig.audio.amp_pin : AAMP); -#endif + radio_state = RadioState::rx; + // Configure PTT input DEBUG_MSG("♪♫♪ Initializing PTT on Pin %u\n", moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN); pinMode(moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN, INPUT); @@ -294,23 +287,32 @@ int32_t AudioModule::runOnce() } } else { if (radio_state == RadioState::tx) { - DEBUG_MSG("♪♫♪ PTT released, switching to RX\n"); + if (tx_encode_frame_index > 0) { + // Send the incomplete frame + DEBUG_MSG("♪♫♪ Sending %d bytes (incomplete)\n", tx_encode_frame_index); + sendPayload(); + } + DEBUG_MSG("♪♫♪ PTT released, switching to RX\n"); + tx_encode_frame_index = 0; radio_state = RadioState::rx; } } if ((radio_state == RadioState::tx) && moduleConfig.audio.i2s_sd) { // Get I2S data from the microphone and place in data buffer size_t bytesIn = 0; - res = i2s_read(I2S_PORT, &adc_buffer + adc_buffer_index, ADC_BUFFER_SIZE - adc_buffer_index, &bytesIn, pdMS_TO_TICKS(40)); // wait 40ms for audio to arrive. + res = i2s_read(I2S_PORT, adc_buffer + adc_buffer_index, adc_buffer_size - adc_buffer_index, &bytesIn, pdMS_TO_TICKS(40)); // wait 40ms for audio to arrive. if (res == ESP_OK) { adc_buffer_index += bytesIn; - if (adc_buffer_index == ADC_BUFFER_SIZE) { + if (adc_buffer_index == adc_buffer_size) { adc_buffer_index = 0; - DEBUG_MSG("♪♫♪ We have a full buffer, process it\n"); - memcpy((void*)speech, (void*)adc_buffer, 2 * ADC_BUFFER_SIZE); - // Notify codec2 task that the buffer is ready. - codec2Thread->notify(RadioState::tx, true); + memcpy((void*)speech, (void*)adc_buffer, 2 * adc_buffer_size); + // Notify run_codec2 task that the buffer is ready. + radio_state = RadioState::tx; + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + vTaskNotifyGiveFromISR(codec2HandlerTask, &xHigherPriorityTaskWoken); + if (xHigherPriorityTaskWoken == pdTRUE) + YIELD_FROM_ISR(xHigherPriorityTaskWoken); } } } @@ -338,7 +340,7 @@ void AudioModule::sendPayload(NodeNum dest, bool wantReplies) p->want_ack = false; // Audio is shoot&forget. TODO: Is this really suppressing retransmissions? p->priority = MeshPacket_Priority_MAX; // Audio is important, because realtime - p->decoded.payload.size = codec2Thread->get_encode_frame_size(); + p->decoded.payload.size = tx_encode_frame_index; memcpy(p->decoded.payload.bytes, tx_encode_frame, p->decoded.payload.size); service.sendToMesh(p); @@ -349,16 +351,14 @@ ProcessMessage AudioModule::handleReceived(const MeshPacket &mp) if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { auto &p = mp.decoded; if (getFrom(&mp) != nodeDB.getNodeNum()) { - if (p.payload.size == codec2Thread->get_encode_frame_size()) { - memcpy(rx_encode_frame, p.payload.bytes, p.payload.size); - radio_state = RadioState::rx; - BaseType_t xHigherPriorityTaskWoken = pdFALSE; - codec2Thread->notifyFromISR(&xHigherPriorityTaskWoken, RadioState::rx, true); - if (xHigherPriorityTaskWoken) - portYIELD_FROM_ISR(); - } else { - DEBUG_MSG("♪♫♪ Invalid payload size %u != %u\n", p.payload.size, codec2Thread->get_encode_frame_size()); - } + memcpy(rx_encode_frame, p.payload.bytes, p.payload.size); + radio_state = RadioState::rx; + rx_encode_frame_index = p.payload.size; + // Notify run_codec2 task that the buffer is ready. + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + vTaskNotifyGiveFromISR(codec2HandlerTask, &xHigherPriorityTaskWoken); + if (xHigherPriorityTaskWoken == pdTRUE) + YIELD_FROM_ISR(xHigherPriorityTaskWoken); } } diff --git a/src/modules/esp32/AudioModule.h b/src/modules/esp32/AudioModule.h index 47eba55ee..c81c5c310 100644 --- a/src/modules/esp32/AudioModule.h +++ b/src/modules/esp32/AudioModule.h @@ -7,41 +7,36 @@ #include "NodeDB.h" #include #include -#include +// #include #include #include #include #include -#define ADC_BUFFER_SIZE 320 // 40ms of voice in 8KHz sampling frequency +#define ADC_BUFFER_SIZE_MAX 320 -class Codec2Thread : public concurrency::NotifiedWorkerThread -{ - struct CODEC2* codec2_state = NULL; - int16_t output_buffer[ADC_BUFFER_SIZE] = {}; - - public: - Codec2Thread(); - - int get_encode_frame_size() { return encode_frame_size; }; - - protected: - int tx_encode_frame_index = 0; - int encode_codec_size = 0; - int encode_frame_num = 0; - int encode_frame_size = 0; - virtual void onNotify(uint32_t notification) override; -}; +enum RadioState { standby, rx, tx }; class AudioModule : public SinglePortModule, private concurrency::OSThread { - bool firstTime = true; - hw_timer_t* adcTimer = NULL; - uint16_t adc_buffer_index = 0; - public: unsigned char rx_encode_frame[Constants_DATA_PAYLOAD_LEN] = {}; unsigned char tx_encode_frame[Constants_DATA_PAYLOAD_LEN] = {}; + int16_t speech[ADC_BUFFER_SIZE_MAX] = {}; + int16_t output_buffer[ADC_BUFFER_SIZE_MAX] = {}; + uint16_t adc_buffer[ADC_BUFFER_SIZE_MAX] = {}; + int adc_buffer_size = 0; + uint16_t adc_buffer_index = 0; + int tx_encode_frame_index = 0; + int rx_encode_frame_index = 0; + int encode_codec_size = 0; + int encode_frame_size = 0; + volatile RadioState radio_state = RadioState::rx; + FastAudioFIFO fifo; + struct CODEC2* codec2 = NULL; + int16_t sample; + adc1_channel_t mic_chan = (adc1_channel_t)0; + uint8_t rx_raw_audio_value = 127; AudioModule(); @@ -51,9 +46,11 @@ class AudioModule : public SinglePortModule, private concurrency::OSThread void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); protected: - virtual int32_t runOnce() override; + int encode_frame_num = 0; + bool firstTime = true; - // void run_codec2(); + + virtual int32_t runOnce() override; virtual MeshPacket *allocReply() override; @@ -64,15 +61,5 @@ class AudioModule : public SinglePortModule, private concurrency::OSThread }; extern AudioModule *audioModule; -extern Codec2Thread *codec2Thread; -extern uint16_t adc_buffer[ADC_BUFFER_SIZE]; -extern uint16_t adc_buffer_index; -extern portMUX_TYPE timerMux; -extern int16_t speech[ADC_BUFFER_SIZE]; -enum RadioState { standby, rx, tx }; -extern volatile RadioState radio_state; -extern adc1_channel_t mic_chan; - -IRAM_ATTR void am_onTimer(); #endif \ No newline at end of file