diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 986a375df..d24fa8f26 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -19,6 +19,9 @@ #ifdef ARCH_ESP32 #include "modules/esp32/RangeTestModule.h" #include "modules/esp32/StoreForwardModule.h" +#ifdef USE_SX1280 +#include "modules/esp32/AudioModule.h" +#endif #endif #if defined(ARCH_ESP32) || defined(ARCH_NRF52) #include "modules/ExternalNotificationModule.h" @@ -65,17 +68,16 @@ void setupModules() #endif #ifdef ARCH_ESP32 // Only run on an esp32 based device. - - /* - Maintained by MC Hamster (Jm Casler) jm@casler.org - */ +#ifdef USE_SX1280 + new AudioModule(); +#endif new ExternalNotificationModule(); storeForwardModule = new StoreForwardModule(); new RangeTestModule(); #elif defined(ARCH_NRF52) -new ExternalNotificationModule(); + new ExternalNotificationModule(); #endif // NOTE! This module must be added LAST because it likes to check for replies from other modules and avoid sending extra acks diff --git a/src/modules/esp32/AudioModule.cpp b/src/modules/esp32/AudioModule.cpp new file mode 100644 index 000000000..9328bdd81 --- /dev/null +++ b/src/modules/esp32/AudioModule.cpp @@ -0,0 +1,249 @@ +#include "configuration.h" +#include "AudioModule.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "RTC.h" +#include "Router.h" +#include "FSCommon.h" + +#include + +/* + AudioModule + A interface to send raw codec2 audio data over the mesh network. Based on the example code from the ESP32_codec2 project. + https://github.com/deulis/ESP32_Codec2 + + Codec 2 is a low-bitrate speech audio codec (speech coding) + that is patent free and open source develop by David Grant Rowe. + http://www.rowetel.com/ and https://github.com/drowe67/codec2 + + 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: + audio.mic_chan 7 (GPIO 35) + audio.amp_pin 25 (GPIO 25) + 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) + + KNOWN PROBLEMS + * Until the module is initilized by the startup sequence, the amp_pin pin is in a floating + state. This may produce a bit of "noise". + * Will not work on NRF and the Linux device targets. +*/ + +#define AMIC 7 +#define AAMP 25 +#define PTT_PIN 39 + +#define AUDIO_MODULE_RX_BUFFER 128 +#define AUDIO_MODULE_DATA_MAX Constants_DATA_PAYLOAD_LEN +#define AUDIO_MODULE_MODE 7 // 700B +#define AUDIO_MODULE_ACK 1 + +AudioModule *audioModule; + +#if defined(ARCH_ESP32) && defined(USE_SX1280) +ButterworthFilter hp_filter(240, 8000, ButterworthFilter::ButterworthFilter::Highpass, 1); +#endif + +//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; + +AudioModule::AudioModule() : SinglePortModule("AudioModule", PortNum_AUDIO_APP), concurrency::OSThread("AudioModule") {} + +void AudioModule::run_codec2() +{ +#if defined(ARCH_ESP32) && defined(USE_SX1280) + if (state == State::tx) + { + for (int i = 0; i < ADC_BUFFER_SIZE; i++) + speech[i] = (int16_t)hp_filter.Update((float)speech[i]); + + codec2_encode(codec2_state, tx_encode_frame + tx_encode_frame_index, speech); + + //increment the pointer where the encoded frame must be saved + tx_encode_frame_index += 8; + + //If it is the 5th time then we have a ready trasnmission frame + if (tx_encode_frame_index == ENCODE_FRAME_SIZE) + { + tx_encode_frame_index = 0; + //Transmit it + sendPayload(); + } + } + if (state == State::rx) //Receiving + { + //Make a cycle to get each codec2 frame from the received frame + for (int i = 0; i < ENCODE_FRAME_SIZE; i += 8) + { + //Decode the codec2 frame + codec2_decode(codec2_state, output_buffer, 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]); + } + } + state = State::standby; +#endif +} + +void AudioModule::handleInterrupt() +{ + audioModule->onTimer(); +} + +void AudioModule::onTimer() +{ +#if defined(ARCH_ESP32) && defined(USE_SX1280) + if (state == State::tx) { + adc_buffer[adc_buffer_index++] = (16 * adc1_get_raw(mic_chan)) - 32768; + + //If you want to test with a 1KHz tone, comment the line above and descomment the three lines below + + // adc_buffer[adc_buffer_index++] = Sine1KHz[Sine1KHz_index++]; + // if (Sine1KHz_index >= 8) + // Sine1KHz_index = 0; + + if (adc_buffer_index == ADC_BUFFER_SIZE) { + adc_buffer_index = 0; + memcpy((void*)speech, (void*)adc_buffer, 2 * ADC_BUFFER_SIZE); + audioModule->setIntervalFromNow(0); // process buffer immediately + } + } else if (state == State::rx) { + + int16_t v; + + //Get a value from audio_fifo and convert it to 0 - 255 to play it in the ADC + //If none value is available the DAC will play the last one that was read, that's + //why the rx_raw_audio_value variable is a global one. + if (audio_fifo.get(&v)) + rx_raw_audio_value = (uint8_t)((v + 32768) / 256); + + //Play + dacWrite(moduleConfig.audio.amp_pin ? moduleConfig.audio.amp_pin : AAMP, rx_raw_audio_value); + } +#endif +} + +int32_t AudioModule::runOnce() +{ +#if defined(ARCH_ESP32) && defined(USE_SX1280) + + if (moduleConfig.audio.codec2_enabled) { + + if (firstTime) { + + 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); + adc1_config_channel_atten(mic_chan, ADC_ATTEN_DB_6); + + // Start a timer at 8kHz to sample the ADC and play the audio on the DAC. + uint32_t cpufreq = getCpuFrequencyMhz(); + switch (cpufreq){ + case 160: + adcTimer = timerBegin(3, 1000, true); // 160 MHz / 1000 = 160KHz + break; + case 240: + adcTimer = timerBegin(3, 1500, true); // 240 MHz / 1500 = 160KHz + break; + case 320: + adcTimer = timerBegin(3, 2000, true); // 320 MHz / 2000 = 160KHz + break; + case 80: + default: + adcTimer = timerBegin(3, 500, true); // 80 MHz / 500 = 160KHz + break; + } + timerAttachInterrupt(adcTimer, &AudioModule::handleInterrupt, true); + timerAlarmWrite(adcTimer, 20, true); // Interrupts when counter == 20, 8.000 times a second + timerAlarmEnable(adcTimer); + + DEBUG_MSG("Initializing DAC on Pin %u\n", moduleConfig.audio.amp_pin ? moduleConfig.audio.amp_pin : AAMP); + DEBUG_MSG("Initializing PTT on Pin %u\n", moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN); + + // Configure PTT input + pinMode(moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN, INPUT_PULLUP); + + state = State::rx; + + DEBUG_MSG("Setting up codec2 in mode %u\n", moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE); + + codec2_state = codec2_create(moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE); + codec2_set_lpc_post_filter(codec2_state, 1, 0, 0.8, 0.2); + + firstTime = 0; + } else { + // Check if we have a PTT press + if (digitalRead(moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN) == LOW) { + // PTT pressed, recording + state = State::tx; + } + if (state != State::standby) { + run_codec2(); + } + } + + return 100; + } else { + DEBUG_MSG("Audio Module Disabled\n"); + + return INT32_MAX; + } +#else + return INT32_MAX; +#endif +} + +MeshPacket *AudioModule::allocReply() +{ + + auto reply = allocDataPacket(); // Allocate a packet for sending + + return reply; +} + +void AudioModule::sendPayload(NodeNum dest, bool wantReplies) +{ + MeshPacket *p = allocReply(); + p->to = dest; + p->decoded.want_response = wantReplies; + + p->want_ack = AUDIO_MODULE_ACK; + + p->decoded.payload.size = ENCODE_FRAME_SIZE; + memcpy(p->decoded.payload.bytes, tx_encode_frame, p->decoded.payload.size); + + service.sendToMesh(p); +} + +ProcessMessage AudioModule::handleReceived(const MeshPacket &mp) +{ +#if defined(ARCH_ESP32) && defined(USE_SX1280) + + if (moduleConfig.audio.codec2_enabled) { + auto &p = mp.decoded; + if (getFrom(&mp) != nodeDB.getNodeNum()) { + if (p.payload.size == ENCODE_FRAME_SIZE) { + memcpy(rx_encode_frame, p.payload.bytes, p.payload.size); + state = State::rx; + audioModule->setIntervalFromNow(0); + run_codec2(); + } else { + DEBUG_MSG("Invalid payload size %u != %u\n", p.payload.size, ENCODE_FRAME_SIZE); + } + } + } + +#endif + + return ProcessMessage::CONTINUE; +} diff --git a/src/modules/esp32/AudioModule.h b/src/modules/esp32/AudioModule.h new file mode 100644 index 000000000..fcd5d169e --- /dev/null +++ b/src/modules/esp32/AudioModule.h @@ -0,0 +1,68 @@ +#pragma once + +#include "SinglePortModule.h" +#include "concurrency/OSThread.h" +#include "configuration.h" +#include "NodeDB.h" +#include +#include +#include +#if defined(ARCH_ESP32) && defined(USE_SX1280) +#include +#include +#include +#else +typedef int FastAudioFIFO; +#endif + +#define ADC_BUFFER_SIZE 320 // 40ms of voice in 8KHz sampling frequency +#define ENCODE_FRAME_SIZE 40 // 5 codec2 frames of 8 bytes each + +class AudioModule : public SinglePortModule, private concurrency::OSThread +{ + bool firstTime = 1; + hw_timer_t* adcTimer = NULL; + uint16_t adc_buffer[ADC_BUFFER_SIZE]; + int16_t speech[ADC_BUFFER_SIZE]; + int16_t output_buffer[ADC_BUFFER_SIZE]; + unsigned char rx_encode_frame[ENCODE_FRAME_SIZE]; + unsigned char tx_encode_frame[ENCODE_FRAME_SIZE]; + int tx_encode_frame_index = 0; + FastAudioFIFO audio_fifo; + uint16_t adc_buffer_index = 0; + adc1_channel_t mic_chan = (adc1_channel_t)0; + struct CODEC2* codec2_state; + + enum State + { + standby, rx, tx + }; + volatile State state = State::tx; + + public: + AudioModule(); + + /** + * Send our payload into the mesh + */ + void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); + + protected: + virtual int32_t runOnce() override; + + static void handleInterrupt(); + + void onTimer(); + + void run_codec2(); + + virtual MeshPacket *allocReply() override; + + /** Called to handle a particular incoming message + * @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual ProcessMessage handleReceived(const MeshPacket &mp) override; +}; + +extern AudioModule *audioModule; + diff --git a/variants/tlora_v2_1_18/platformio.ini b/variants/tlora_v2_1_18/platformio.ini index 2cb1c3d2f..4160be5de 100644 --- a/variants/tlora_v2_1_18/platformio.ini +++ b/variants/tlora_v2_1_18/platformio.ini @@ -3,5 +3,6 @@ extends = esp32_base board = ttgo-lora32-v21 lib_deps = ${esp32_base.lib_deps} + caveman99/ESP32 Codec2@^1.0.1 build_flags = ${esp32_base.build_flags} -D TLORA_V2_1_18 -I variants/tlora_v2_1_18 \ No newline at end of file