mirror of
https://github.com/meshtastic/firmware.git
synced 2025-04-23 09:06:02 +00:00
codec2 Audio Support for SX1280 (untested)
This commit is contained in:
parent
ef18b173cd
commit
09ddde177c
@ -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
|
||||
|
249
src/modules/esp32/AudioModule.cpp
Normal file
249
src/modules/esp32/AudioModule.cpp
Normal file
@ -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 <assert.h>
|
||||
|
||||
/*
|
||||
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;
|
||||
}
|
68
src/modules/esp32/AudioModule.h
Normal file
68
src/modules/esp32/AudioModule.h
Normal file
@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include "SinglePortModule.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "configuration.h"
|
||||
#include "NodeDB.h"
|
||||
#include <Arduino.h>
|
||||
#include <driver/adc.h>
|
||||
#include <functional>
|
||||
#if defined(ARCH_ESP32) && defined(USE_SX1280)
|
||||
#include <codec2.h>
|
||||
#include <ButterworthFilter.h>
|
||||
#include <FastAudioFIFO.h>
|
||||
#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;
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user