mirror of https://github.com/arendst/Tasmota.git
Audio refactoring, filters and I2SConfig (#19662)
This commit is contained in:
parent
e32c39f21d
commit
473a8ee999
|
@ -127,6 +127,18 @@ BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_get_channels, "i", ".");
|
|||
extern float be_audio_input_i2s_get_gain(void* in);
|
||||
BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_get_gain, "f", ".");
|
||||
|
||||
// AudioInputI2S.get_dc_offset() -> float
|
||||
extern int be_audio_input_i2s_get_dc_offset(void* in);
|
||||
BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_get_dc_offset, "i", ".");
|
||||
|
||||
// AudioInputI2S.get_lowpass_alpha() -> float
|
||||
extern float be_audio_input_i2s_get_lowpass_alpha(void* in);
|
||||
BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_get_lowpass_alpha, "f", ".");
|
||||
|
||||
// AudioInputI2S.set_lowpass_alpha(alpha:float) -> bool
|
||||
extern int be_audio_input_i2s_set_lowpass_alpha(void* in, float alpha);
|
||||
BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_set_lowpass_alpha, "b", ".f");
|
||||
|
||||
#include "be_fixed_be_class_AudioOutputI2S.h"
|
||||
#include "be_fixed_be_class_AudioGenerator.h"
|
||||
#include "be_fixed_be_class_AudioGeneratorWAV.h"
|
||||
|
@ -202,6 +214,10 @@ class be_class_AudioInputI2S (scope: global, name: AudioInputI2S, strings: weak)
|
|||
get_gain, ctype_func(be_audio_input_i2s_get_gain)
|
||||
|
||||
set_gain, ctype_func(be_audio_input_set_gain)
|
||||
get_dc_offset, ctype_func(be_audio_input_i2s_get_dc_offset)
|
||||
|
||||
get_lowpass_alpha, ctype_func(be_audio_input_i2s_get_lowpass_alpha)
|
||||
set_lowpass_alpha, ctype_func(be_audio_input_i2s_set_lowpass_alpha)
|
||||
}
|
||||
|
||||
@const_object_info_end */
|
||||
|
|
|
@ -763,6 +763,11 @@
|
|||
#define D_CMND_PING "Ping"
|
||||
#define D_JSON_PING "Ping"
|
||||
|
||||
// Commands xdrv_42_audio - I2S Audio
|
||||
#define D_PRFX_I2S "I2S"
|
||||
#define D_JSON_I2S_CONFIG "Config"
|
||||
|
||||
|
||||
// Commands xdrv_52_berry.ino - Berry scripting language
|
||||
#define D_PRFX_BR "Br"
|
||||
#define D_CMND_BR_RUN ""
|
||||
|
|
|
@ -67,10 +67,11 @@ enum : int8_t {
|
|||
};
|
||||
|
||||
#define I2S_SLOTS 2
|
||||
#define AUDIO_SETTINGS_VERSION 1
|
||||
|
||||
typedef struct{
|
||||
struct{
|
||||
uint8_t version = 0; // B00
|
||||
uint8_t version = AUDIO_SETTINGS_VERSION; // B00
|
||||
|
||||
// runtime options, will be saved but ignored on setting read
|
||||
bool duplex = 0; // B01 - depends on GPIO setting and SOC caps, DIN and DOUT on same port in GPIO means -> try to use duplex if possible
|
||||
|
@ -81,7 +82,8 @@ typedef struct{
|
|||
bool mclk_inv[I2S_SLOTS] = {0}; // B05-06 - invert mclk
|
||||
bool bclk_inv[I2S_SLOTS] = {0}; // B07-08 - invert bclk
|
||||
bool ws_inv[I2S_SLOTS] = {0}; // B09-0A - invert ws
|
||||
uint8_t spare[5]; // B0B-0F
|
||||
uint8_t mp3_preallocate = 0; // B0B - will be ignored without PS-RAM
|
||||
uint8_t spare[4]; // B0C-0F
|
||||
} sys;
|
||||
struct {
|
||||
uint32_t sample_rate = 16000; // B00-03
|
||||
|
@ -91,26 +93,31 @@ typedef struct{
|
|||
uint8_t slot_config = I2S_SLOT_MSB;// B07 - slot configuration MSB = 0, PCM = 1, PHILIPS = 2
|
||||
uint8_t channels = 2; // B08 - mono/stereo - 1 is added for both
|
||||
bool apll = 1; // B09 - will be ignored on unsupported SOC's
|
||||
// device specific
|
||||
uint8_t mp3_preallocate = 0; // B0A - preallocate MP3 buffer for mp3 playing
|
||||
uint8_t codec = 0; // B0B - S3 box only, unused for now
|
||||
uint8_t spare[4]; // B0C-0F
|
||||
uint8_t spare[6]; // B0A-0F
|
||||
} tx;
|
||||
struct {
|
||||
uint32_t sample_rate = 16000; // B00-03
|
||||
uint8_t gain = 30; // B04
|
||||
uint8_t mode = I2S_MODE_PDM; // B05 - I2S mode standard, PDM, TDM, DAC
|
||||
uint8_t slot_mask = I2S_SLOT_NOCHANGE;// B06 - slot mask
|
||||
uint8_t slot_config = I2S_SLOT_MSB;// B07 - slot configuration MSB = 0, PCM = 1, PHILIPS = 2
|
||||
uint8_t channels = 1; // B08 - mono/stereo - 1 is added for both
|
||||
bool apll = 1; // B09 - will be ignored on unsupported SOC's
|
||||
// device specific
|
||||
uint8_t codec = 0; // B0A - S3 box only, unused for now
|
||||
uint8_t mp3_preallocate = 0; // B0B - will be ignored without PS-RAM
|
||||
uint8_t spare[4]; // B0C-0F
|
||||
uint16_t gain = 30 * 16; // B04-05 - in Q12.4
|
||||
uint8_t mode = I2S_MODE_PDM; // B06 - I2S mode standard, PDM, TDM, DAC
|
||||
uint8_t slot_mask = I2S_SLOT_NOCHANGE;// B07 - slot mask
|
||||
uint8_t slot_config = I2S_SLOT_MSB;// B08 - slot configuration MSB = 0, PCM = 1, PHILIPS = 2
|
||||
uint8_t channels = 1; // B09 - mono/stereo - 1 is added for both
|
||||
// filters
|
||||
uint16_t dc_filter_alpha = 0b0111111011111111; // B0A-B0B - DC_block filter = (1-2^(-7)) Q32:1.31, or `0` if disabled
|
||||
// low pass filter
|
||||
// See: https://en.wikipedia.org/wiki/Low-pass_filter#Simple_infinite_impulse_response_filter
|
||||
// For 3000Hz low pass filter, RC = 0.0000530786
|
||||
// dt = 1/16000 = 0.0000625
|
||||
// alpha = dt / (RC + dt) = 0.540757545
|
||||
// alpha = (b) 0.100010100110111 = 0x4537
|
||||
uint16_t lowpass_alpha = 0b0100010100110111; // B0C-B0D - lowpass filter = 3000Hz for 16000Hz sample rate
|
||||
bool apll = 1; // B0E - will be ignored on unsupported SOC's
|
||||
uint8_t spare[1]; // B0F
|
||||
} rx;
|
||||
} tI2SSettings;
|
||||
|
||||
static_assert(sizeof(tI2SSettings) == 48, "tI2SSettings Size is not correct");
|
||||
|
||||
typedef union {
|
||||
uint8_t data;
|
||||
struct {
|
||||
|
|
|
@ -131,9 +131,11 @@ public:
|
|||
inline uint8_t getRxBitsPerSample(void) const { return 16; } // TODO - hardcoded to 16 bits for recording
|
||||
inline uint16_t getRxRate(void) const { return _rx_freq; }
|
||||
inline uint8_t getRxChannels(void) const { return _rx_channels; }
|
||||
inline float getRxGain(void) const { return 1.0f; } // TODO - hardcoded to 1.0 for recording
|
||||
inline bool getRxRunning(void) const { return _rx_running; }
|
||||
inline i2s_chan_handle_t getRxHandle(void) const { return _rx_handle; }
|
||||
inline float getRxGain(void) const { return ((float)_rx_gain) / 16; }
|
||||
inline int32_t getRxDCOffset(void) const { return _rx_dc_offset; }
|
||||
inline uint16_t getRxLowpassAlpha(void) const { return ((float)_rx_lowpass_alpha / 0x8000); }
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Setters
|
||||
|
@ -144,6 +146,15 @@ public:
|
|||
inline void setRxMode(uint8_t mode) { _rx_mode = mode; }
|
||||
inline void setRxChannels(uint8_t channels) { _rx_channels = channels; }
|
||||
inline void setRxRunning(bool running) { _rx_running = running; }
|
||||
inline void setRxGain(float f) { _rx_gain = (uint16_t)(f * 16); }
|
||||
|
||||
void setRxLowpassAlpha(float f) {
|
||||
if (f < 0) { f = 0; }
|
||||
if (f > 1) { f = 1; }
|
||||
_rx_lowpass_alpha = (uint16_t)(f * 0x8000);
|
||||
_rx_lowpass_one_alpha = 0x8000 - _rx_lowpass_alpha;
|
||||
_rx_lowpass_y_prev = 0;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// AudioOutput has inconsistent case for methods, provide consistent setters for super class
|
||||
|
@ -158,6 +169,8 @@ public:
|
|||
bool startI2SChannel(bool tx, bool rx);
|
||||
int updateClockConfig(void);
|
||||
|
||||
int32_t readMic(uint8_t *buffer, uint32_t size, bool dc_block, bool apply_gain, bool lowpass);
|
||||
|
||||
// The following is now the preferred function
|
||||
// and allows to send multiple samples at once
|
||||
//
|
||||
|
@ -170,11 +183,12 @@ public:
|
|||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Microphone related methods
|
||||
uint32_t I2sMicInit(void);
|
||||
void I2sMicDeinit(void);
|
||||
uint32_t micInit(void);
|
||||
void micDeinit(void);
|
||||
|
||||
protected:
|
||||
void loadSettings(void); // load all settings from Settings file and template - the goal is to have zero touch
|
||||
int16_t dcFilter(int16_t pcm_in);
|
||||
int16_t lowpassFilter(int16_t pcm_in);
|
||||
|
||||
protected:
|
||||
|
||||
|
@ -202,6 +216,9 @@ protected:
|
|||
uint8_t _rx_slot_config = I2S_SLOT_MSB;// I2S slot configuration
|
||||
uint16_t _rx_freq = 16000; // I2S Rx sampling frequency in Hz
|
||||
|
||||
uint16_t _rx_gain = 0x10; // Microphone gain in Q12.4 format (0x10 = 1.0)
|
||||
int16_t _rx_dc_offset = 0x8000; // DC offset for PCM data, or 0x8000 if not set yet
|
||||
|
||||
// GPIOs for I2S
|
||||
gpio_num_t _gpio_mclk = GPIO_NUM_NC; // GPIO for master clock
|
||||
gpio_num_t _gpio_bclk = GPIO_NUM_NC; // GPIO for bit clock
|
||||
|
@ -212,6 +229,15 @@ protected:
|
|||
bool _gpio_bclk_inv = false; // invert bit clock
|
||||
bool _gpio_ws_inv = false; // invert word select
|
||||
bool _apll = false; // use APLL instead of PLL
|
||||
|
||||
// DC blocking filter
|
||||
int16_t _rx_dc_x_prev = 0;
|
||||
int16_t _rx_dc_y_prev = 0;
|
||||
|
||||
// Low-pass filter
|
||||
uint16_t _rx_lowpass_alpha = 0; // alpha parameter for lowpass filter Q1.15
|
||||
uint16_t _rx_lowpass_one_alpha = 0x8000; // 1 - alpha, Q1.15
|
||||
int16_t _rx_lowpass_y_prev = 0;
|
||||
};
|
||||
|
||||
|
||||
|
@ -400,6 +426,9 @@ bool TasmotaI2S::startI2SChannel(bool tx, bool rx) {
|
|||
}
|
||||
|
||||
i2s_chan_config_t rx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
|
||||
// change to 3 buffers of 512 samples
|
||||
rx_chan_cfg.dma_desc_num = 3;
|
||||
rx_chan_cfg.dma_frame_num = 512;
|
||||
|
||||
AddLog(LOG_LEVEL_DEBUG, "I2S: rx_chan_cfg id:%i role:%i dma_desc_num:%i dma_frame_num:%i auto_clear:%i",
|
||||
rx_chan_cfg.id, rx_chan_cfg.role, rx_chan_cfg.dma_desc_num, rx_chan_cfg.dma_frame_num, rx_chan_cfg.auto_clear);
|
||||
|
@ -421,7 +450,7 @@ bool TasmotaI2S::startI2SChannel(bool tx, bool rx) {
|
|||
},
|
||||
},
|
||||
};
|
||||
// if (_rx_slot_mask != I2S_SLOT_NOCHANGE) { rx_pdm_cfg.slot_cfg.slot_mask = (i2s_pdm_slot_mask_t)_rx_slot_mask; }
|
||||
if (_rx_slot_mask != I2S_SLOT_NOCHANGE) { rx_pdm_cfg.slot_cfg.slot_mask = (i2s_pdm_slot_mask_t)_rx_slot_mask; }
|
||||
|
||||
// AddLog(LOG_LEVEL_DEBUG, "I2S: rx_pdm_cfg clk_cfg sample_rate_hz:%i clk_src:%i mclk_multiple:%i dn_sample_mode:%i",
|
||||
// rx_pdm_cfg.clk_cfg.sample_rate_hz, rx_pdm_cfg.clk_cfg.clk_src, rx_pdm_cfg.clk_cfg.mclk_multiple, rx_pdm_cfg.clk_cfg.dn_sample_mode);
|
||||
|
@ -489,7 +518,7 @@ int TasmotaI2S::updateClockConfig(void) {
|
|||
* microphone related functions
|
||||
\*********************************************************************************************/
|
||||
|
||||
uint32_t TasmotaI2S::I2sMicInit(void) {
|
||||
uint32_t TasmotaI2S::micInit(void) {
|
||||
if (!_rx_configured) { return 0; } // nothing configured
|
||||
|
||||
esp_err_t err = ESP_OK;
|
||||
|
@ -498,82 +527,6 @@ uint32_t TasmotaI2S::I2sMicInit(void) {
|
|||
i2s_slot_mode_t slot_mode = (_rx_channels == 1) ? I2S_SLOT_MODE_MONO : I2S_SLOT_MODE_STEREO;
|
||||
AddLog(LOG_LEVEL_DEBUG, "I2S: mic init rx_channels:%i rx_running:%i rx_handle:%p", slot_mode, _rx_running, _rx_handle);
|
||||
|
||||
// if (_tx_configured && _rx_running) { // duplex mode, mic was already initialized
|
||||
// AddLog(LOG_LEVEL_DEBUG, "I2S: mic init exit Rx already enabled");
|
||||
// return 0; // no need to en- or disable when in full duplex mode and already initialized
|
||||
// }
|
||||
|
||||
// if (_rx_handle == nullptr){
|
||||
// i2s_chan_config_t rx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
|
||||
|
||||
// AddLog(LOG_LEVEL_DEBUG, "I2S: rx_chan_cfg id:%i role:%i dma_desc_num:%i dma_frame_num:%i auto_clear:%i",
|
||||
// rx_chan_cfg.id, rx_chan_cfg.role, rx_chan_cfg.dma_desc_num, rx_chan_cfg.dma_frame_num, rx_chan_cfg.auto_clear);
|
||||
|
||||
// err = i2s_new_channel(&rx_chan_cfg, NULL, &_rx_handle);
|
||||
// AddLog(LOG_LEVEL_DEBUG, "I2S: mic init i2s_new_channel err=%i", err);
|
||||
// switch (_rx_mode){
|
||||
// case I2S_MODE_PDM:
|
||||
// {
|
||||
// i2s_pdm_rx_config_t rx_pdm_cfg = {
|
||||
// .clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG(_rx_freq),
|
||||
// /* The default mono slot is the left slot (whose 'select pin' of the PDM microphone is pulled down) */
|
||||
// .slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
|
||||
// // .slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, slot_mode),
|
||||
// .gpio_cfg = {
|
||||
// .clk = _gpio_ws,
|
||||
// .din = _gpio_din,
|
||||
// .invert_flags = {
|
||||
// .clk_inv = _gpio_ws_inv,
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
// if (_rx_slot_mask != I2S_SLOT_NOCHANGE) { rx_pdm_cfg.slot_cfg.slot_mask = (i2s_pdm_slot_mask_t)_rx_slot_mask; }
|
||||
|
||||
// // AddLog(LOG_LEVEL_DEBUG, "I2S: rx_pdm_cfg clk_cfg sample_rate_hz:%i clk_src:%i mclk_multiple:%i dn_sample_mode:%i",
|
||||
// // rx_pdm_cfg.clk_cfg.sample_rate_hz, rx_pdm_cfg.clk_cfg.clk_src, rx_pdm_cfg.clk_cfg.mclk_multiple, rx_pdm_cfg.clk_cfg.dn_sample_mode);
|
||||
|
||||
// // AddLog(LOG_LEVEL_DEBUG, "I2S: rx_pdm_cfg slot_cfg data_bit_width:%i slot_bit_width:%i slot_mode:%i slot_mask:%i",
|
||||
// // rx_pdm_cfg.slot_cfg.data_bit_width, rx_pdm_cfg.slot_cfg.slot_bit_width, rx_pdm_cfg.slot_cfg.slot_mode, rx_pdm_cfg.slot_cfg.slot_mask);
|
||||
|
||||
// // AddLog(LOG_LEVEL_DEBUG, "I2S: rx_pdm_cfg gpio_cfg clk:%i din:%i clk_inv:%i",
|
||||
// // rx_pdm_cfg.gpio_cfg.clk, rx_pdm_cfg.gpio_cfg.din, rx_pdm_cfg.gpio_cfg.invert_flags.clk_inv);
|
||||
|
||||
// err = i2s_channel_init_pdm_rx_mode(_rx_handle, &rx_pdm_cfg);
|
||||
// AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: RX channel in PDM mode, CLK: %i, DIN: %i, 16 bit width, %i channel(s), err code: 0x%04X"),
|
||||
// _gpio_ws, _gpio_din, slot_mode, err);
|
||||
// }
|
||||
// break;
|
||||
// case I2S_MODE_STD:
|
||||
// {
|
||||
// i2s_std_config_t rx_std_cfg = {
|
||||
// .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(_rx_freq),
|
||||
// .slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, slot_mode),
|
||||
// .gpio_cfg = {
|
||||
// .mclk = (gpio_num_t)Pin(GPIO_I2S_MCLK),
|
||||
// .bclk = (gpio_num_t)Pin(GPIO_I2S_BCLK),
|
||||
// .ws = (gpio_num_t)Pin(GPIO_I2S_WS),
|
||||
// .dout = I2S_GPIO_UNUSED,
|
||||
// .din = (gpio_num_t)Pin(GPIO_I2S_DIN),
|
||||
// .invert_flags = {
|
||||
// .mclk_inv = false,
|
||||
// .bclk_inv = false,
|
||||
// .ws_inv = false,
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
// if (_rx_slot_mask != I2S_SLOT_NOCHANGE) { rx_std_cfg.slot_cfg.slot_mask = (i2s_std_slot_mask_t)_rx_slot_mask; }
|
||||
|
||||
// err = i2s_channel_init_std_mode(audio_i2s.rx_handle, &rx_std_cfg);
|
||||
// AddLog(LOG_LEVEL_DEBUG, "I2S: RX i2s_channel_init_std_mode err:%i", err);
|
||||
// AddLog(LOG_LEVEL_DEBUG, "I2S: RX channel in standard mode with 16 bit width on %i channel(s) initialized", slot_mode);
|
||||
// }
|
||||
// break;
|
||||
// default:
|
||||
// AddLog(LOG_LEVEL_INFO, "I2S: invalid rx mode=%i", _rx_mode);
|
||||
// return -1;
|
||||
// }
|
||||
// }
|
||||
|
||||
if (!_rx_running) {
|
||||
err = i2s_channel_enable(_rx_handle);
|
||||
AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: RX channel enable err:0x%04X"),err);
|
||||
|
@ -583,7 +536,7 @@ uint32_t TasmotaI2S::I2sMicInit(void) {
|
|||
}
|
||||
|
||||
|
||||
void TasmotaI2S::I2sMicDeinit(void) {
|
||||
void TasmotaI2S::micDeinit(void) {
|
||||
esp_err_t err = ESP_OK;
|
||||
gpio_num_t clk_gpio;
|
||||
|
||||
|
@ -599,5 +552,97 @@ void TasmotaI2S::I2sMicDeinit(void) {
|
|||
}
|
||||
}
|
||||
|
||||
// Read data into buffer of uint16_t[]
|
||||
//
|
||||
// Returns:
|
||||
// n<0: error occured
|
||||
// 0: no data available
|
||||
// n>0: number of bytes read
|
||||
int32_t TasmotaI2S::readMic(uint8_t *buffer, uint32_t size, bool dc_block, bool apply_gain, bool lowpass) {
|
||||
if (!audio_i2s.in->getRxRunning()) { return -1; }
|
||||
|
||||
size_t btr = 0;
|
||||
esp_err_t err = i2s_channel_read(_rx_handle, buffer, size, &btr, 0 /* do not wait */);
|
||||
if ((err != ESP_OK) && (err != ESP_ERR_TIMEOUT)) { return -err; }
|
||||
if (btr == 0) { return 0; }
|
||||
int16_t *samples = (int16_t*) buffer;
|
||||
size_t samples_count = btr / 2; // 16 bits signed samples
|
||||
|
||||
// Apply DC block
|
||||
if (dc_block) {
|
||||
if (_rx_dc_x_prev == 0 && _rx_dc_y_prev == 0) {
|
||||
int32_t sum_samples = 0;
|
||||
for (uint32_t i=0; i<samples_count; i++) {
|
||||
sum_samples += samples[i];
|
||||
}
|
||||
_rx_dc_offset = sum_samples / samples_count;
|
||||
_rx_dc_x_prev = _rx_dc_offset;
|
||||
_rx_dc_y_prev = 0;
|
||||
}
|
||||
// apply dc_blocking filter
|
||||
for (uint32_t i=0; i<samples_count; i++) {
|
||||
samples[i] = dcFilter(samples[i]);
|
||||
}
|
||||
_rx_dc_offset = _rx_dc_x_prev - _rx_dc_y_prev; // update new offset
|
||||
}
|
||||
|
||||
// apply gain
|
||||
if (apply_gain) {
|
||||
for (uint32_t i=0; i<samples_count; i++) {
|
||||
int32_t val = (((int32_t)samples[i]) * _rx_gain) / 0x10;
|
||||
// clipping if overflow
|
||||
if (val > 0x7FFF) { val = 0x7FFF; }
|
||||
if (val < -0x8000) { val = -0x8000; }
|
||||
samples[i] = val;
|
||||
}
|
||||
}
|
||||
|
||||
// lowpass filter
|
||||
if (lowpass && _rx_lowpass_alpha != 0) {
|
||||
// apply lowpass filter
|
||||
for (uint32_t i=0; i<samples_count; i++) {
|
||||
samples[i] = lowpassFilter(samples[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return btr;
|
||||
}
|
||||
|
||||
// DC Blocking filter
|
||||
// 1 - 2^(-7) = 0.9921875
|
||||
// binary: 0.111111011111111 Q1.15
|
||||
int16_t TasmotaI2S::dcFilter(int16_t pcm_in) {
|
||||
#define A1 32511 // (1-2^(-7)) Q32:1.31
|
||||
int16_t delta_x;
|
||||
int32_t a1_y_prev;
|
||||
int16_t pcm_out;
|
||||
|
||||
delta_x = pcm_in - _rx_dc_x_prev;
|
||||
a1_y_prev = (A1 * (int32_t)_rx_dc_y_prev) / 0x8000;
|
||||
pcm_out = delta_x + (int16_t)a1_y_prev;
|
||||
_rx_dc_x_prev = pcm_in;
|
||||
_rx_dc_y_prev = pcm_out;
|
||||
return pcm_out;
|
||||
}
|
||||
|
||||
// low pass filter
|
||||
// See: https://en.wikipedia.org/wiki/Low-pass_filter#Simple_infinite_impulse_response_filter
|
||||
// For 3000Hz low pass filter, RC = 0.0000530786
|
||||
// dt = 1/16000 = 0.0000625
|
||||
// alpha = dt / (RC + dt) = 0.540757545
|
||||
// alpha = (b) 0.100010100110111 = 0x4537
|
||||
// 1 - alpha = 0x3A9C
|
||||
int16_t TasmotaI2S::lowpassFilter(int16_t pcm_in) {
|
||||
// const int32_t alpha = 0x4537;
|
||||
// const int32_t one_alpha = 0x3A9C;
|
||||
int32_t a1_y_prev;
|
||||
int16_t pcm_out;
|
||||
|
||||
a1_y_prev = (_rx_lowpass_alpha * _rx_lowpass_y_prev) / 0x8000 + (_rx_lowpass_one_alpha * pcm_in) / 0x8000;
|
||||
pcm_out = (int16_t)a1_y_prev;
|
||||
_rx_lowpass_y_prev = pcm_out;
|
||||
return pcm_out;
|
||||
}
|
||||
|
||||
#endif // USE_I2S_AUDIO
|
||||
#endif // defined(ESP32) && ESP_IDF_VERSION_MAJOR >= 5
|
||||
|
|
|
@ -34,6 +34,8 @@
|
|||
#define AUDIO_PWR_ON I2SAudioPower(true);
|
||||
#define AUDIO_PWR_OFF I2SAudioPower(false);
|
||||
|
||||
#define AUDIO_CONFIG_FILENAME "/.drvset042"
|
||||
|
||||
extern FS *ufsp;
|
||||
extern FS *ffsp;
|
||||
|
||||
|
@ -54,7 +56,7 @@ void I2sWebRadioStopPlaying(void);
|
|||
\*********************************************************************************************/
|
||||
|
||||
const char kI2SAudio_Commands[] PROGMEM = "I2S|"
|
||||
"Gain|Play|Rec|MGain|Stop"
|
||||
"Gain|Play|Rec|MGain|Stop|Config"
|
||||
#ifdef USE_I2S_DEBUG
|
||||
"|Mic" // debug only
|
||||
#endif // USE_I2S_DEBUG
|
||||
|
@ -79,7 +81,7 @@ const char kI2SAudio_Commands[] PROGMEM = "I2S|"
|
|||
;
|
||||
|
||||
void (* const I2SAudio_Command[])(void) PROGMEM = {
|
||||
&CmndI2SGain, &CmndI2SPlay, &CmndI2SMicRec, &CmndI2SMicGain, &CmndI2SStop,
|
||||
&CmndI2SGain, &CmndI2SPlay, &CmndI2SMicRec, &CmndI2SMicGain, &CmndI2SStop, &CmndI2SConfig,
|
||||
#ifdef USE_I2S_DEBUG
|
||||
&CmndI2SMic,
|
||||
#endif // USE_I2S_DEBUG
|
||||
|
@ -103,6 +105,141 @@ void (* const I2SAudio_Command[])(void) PROGMEM = {
|
|||
#endif // I2S_BRIDGE
|
||||
};
|
||||
|
||||
|
||||
/*********************************************************************************************\
|
||||
* I2S configuration
|
||||
\*********************************************************************************************/
|
||||
|
||||
void CmndI2SConfig(void) {
|
||||
tI2SSettings * cfg = audio_i2s.Settings;
|
||||
|
||||
// if (zigbee.init_phase) { ResponseCmndChar_P(PSTR(D_ZIGBEE_NOT_STARTED)); return; }
|
||||
TrimSpace(XdrvMailbox.data);
|
||||
if (strlen(XdrvMailbox.data) > 0) {
|
||||
JsonParser parser(XdrvMailbox.data);
|
||||
JsonParserObject root = parser.getRootObject();
|
||||
if (!root) { ResponseCmndChar_P(D_JSON_INVALID_JSON); return; }
|
||||
|
||||
// remove "I2SConfig" primary key if present
|
||||
JsonParserToken config_tk = root[D_PRFX_I2S D_JSON_I2S_CONFIG];
|
||||
if ((bool)config_tk) {
|
||||
root = config_tk.getObject();
|
||||
}
|
||||
|
||||
JsonParserToken sys_tk = root["Sys"];
|
||||
if (sys_tk.isObject()) {
|
||||
JsonParserObject sys = sys_tk.getObject();
|
||||
cfg->sys.mclk_inv[0] = sys.getUInt(PSTR("MclkInv0"), cfg->sys.mclk_inv[0]);
|
||||
cfg->sys.mclk_inv[1] = sys.getUInt(PSTR("MclkInv1"), cfg->sys.mclk_inv[1]);
|
||||
cfg->sys.bclk_inv[0] = sys.getUInt(PSTR("BclkInv0"), cfg->sys.bclk_inv[0]);
|
||||
cfg->sys.bclk_inv[1] = sys.getUInt(PSTR("BclkInv1"), cfg->sys.bclk_inv[1]);
|
||||
cfg->sys.ws_inv[0] = sys.getUInt(PSTR("WsInv0"), cfg->sys.ws_inv[0]);
|
||||
cfg->sys.ws_inv[1] = sys.getUInt(PSTR("WsInv1"), cfg->sys.ws_inv[1]);
|
||||
cfg->sys.mp3_preallocate = sys.getUInt(PSTR("Mp3Preallocate"), cfg->sys.mp3_preallocate);
|
||||
}
|
||||
|
||||
JsonParserToken tx_tk = root["Tx"];
|
||||
if (tx_tk.isObject()) {
|
||||
JsonParserObject tx = tx_tk.getObject();
|
||||
// cfg->tx.sample_rate = tx.getUInt(PSTR("SampleRate"), cfg->tx.sample_rate);
|
||||
cfg->tx.gain = tx.getUInt(PSTR("Gain"), cfg->tx.gain);
|
||||
cfg->tx.mode = tx.getUInt(PSTR("Mode"), cfg->tx.mode);
|
||||
cfg->tx.slot_mask = tx.getUInt(PSTR("SlotMask"), cfg->tx.slot_mask);
|
||||
cfg->tx.slot_config = tx.getUInt(PSTR("SlotConfig"), cfg->tx.slot_config);
|
||||
cfg->tx.channels = tx.getUInt(PSTR("Channels"), cfg->tx.channels);
|
||||
cfg->tx.apll = tx.getUInt(PSTR("APLL"), cfg->tx.apll);
|
||||
}
|
||||
|
||||
JsonParserToken rx_tk = root["Rx"];
|
||||
if (rx_tk.isObject()) {
|
||||
JsonParserObject rx = rx_tk.getObject();
|
||||
cfg->rx.sample_rate = rx.getUInt(PSTR("SampleRate"), cfg->rx.sample_rate);
|
||||
float rx_gain = ((float)cfg->rx.gain) / 16;
|
||||
rx_gain = rx.getFloat(PSTR("Gain"), rx_gain);
|
||||
cfg->rx.gain = (uint16_t)(rx_gain * 16);
|
||||
// cfg->rx.gain = rx.getUInt(PSTR("Gain"), cfg->rx.gain);
|
||||
cfg->rx.mode = rx.getUInt(PSTR("Mode"), cfg->rx.mode);
|
||||
cfg->rx.slot_mask = rx.getUInt(PSTR("SlotMask"), cfg->rx.slot_mask);
|
||||
cfg->rx.slot_config = rx.getUInt(PSTR("SlotConfig"), cfg->rx.slot_config);
|
||||
cfg->rx.channels = rx.getUInt(PSTR("Channels"), cfg->rx.channels);
|
||||
cfg->rx.dc_filter_alpha = rx.getUInt(PSTR("DCFilterAlpha"), cfg->rx.dc_filter_alpha);
|
||||
cfg->rx.lowpass_alpha = rx.getUInt(PSTR("LowpassAlpha"), cfg->rx.lowpass_alpha);
|
||||
cfg->rx.apll = rx.getUInt(PSTR("APLL"), cfg->rx.apll);
|
||||
}
|
||||
I2SSettingsSave(AUDIO_CONFIG_FILENAME);
|
||||
}
|
||||
|
||||
float mic_gain = ((float)cfg->rx.gain) / 16;
|
||||
Response_P("{\"" D_PRFX_I2S D_JSON_I2S_CONFIG "\":{"
|
||||
// Sys
|
||||
"\"Sys\":{"
|
||||
"\"Version\":%d,"
|
||||
"\"Duplex\":%d,"
|
||||
"\"Tx\":%d,"
|
||||
"\"Rx\":%d,"
|
||||
"\"Exclusive\":%d,"
|
||||
"\"MclkInv0\":%d,"
|
||||
"\"MclkInv1\":%d,"
|
||||
"\"BclkInv0\":%d,"
|
||||
"\"BclkInv1\":%d,"
|
||||
"\"WsInv0\":%d,"
|
||||
"\"WsInv1\":%d,"
|
||||
"\"Mp3Preallocate\":%d"
|
||||
"},"
|
||||
"\"Tx\":{"
|
||||
"\"SampleRate\":%d,"
|
||||
"\"Gain\":%d,"
|
||||
"\"Mode\":%d,"
|
||||
"\"SlotMask\":%d,"
|
||||
"\"SlotConfig\":%d,"
|
||||
"\"Channels\":%d,"
|
||||
"\"APLL\":%d"
|
||||
"},"
|
||||
"\"Rx\":{"
|
||||
"\"SampleRate\":%d,"
|
||||
"\"Gain\":%_f,"
|
||||
// "\"Gain\":%d,"
|
||||
"\"Mode\":%d,"
|
||||
"\"SlotMask\":%d,"
|
||||
"\"SlotConfig\":%d,"
|
||||
"\"Channels\":%d,"
|
||||
"\"DCFilterAlpha\":%d,"
|
||||
"\"LowpassAlpha\":%d,"
|
||||
"\"APLL\":%d"
|
||||
"}}}",
|
||||
cfg->sys.version,
|
||||
cfg->sys.duplex,
|
||||
cfg->sys.tx,
|
||||
cfg->sys.rx,
|
||||
cfg->sys.exclusive,
|
||||
cfg->sys.mclk_inv[0],
|
||||
cfg->sys.mclk_inv[1],
|
||||
cfg->sys.bclk_inv[0],
|
||||
cfg->sys.bclk_inv[1],
|
||||
cfg->sys.ws_inv[0],
|
||||
cfg->sys.ws_inv[1],
|
||||
cfg->sys.mp3_preallocate,
|
||||
//
|
||||
cfg->tx.sample_rate,
|
||||
cfg->tx.gain,
|
||||
cfg->tx.mode,
|
||||
cfg->tx.slot_mask,
|
||||
cfg->tx.slot_config,
|
||||
cfg->tx.channels,
|
||||
cfg->tx.apll,
|
||||
//
|
||||
cfg->rx.sample_rate,
|
||||
&mic_gain,
|
||||
// cfg->rx.gain,
|
||||
cfg->rx.mode,
|
||||
cfg->rx.slot_mask,
|
||||
cfg->rx.slot_config,
|
||||
cfg->rx.channels,
|
||||
cfg->rx.dc_filter_alpha,
|
||||
cfg->rx.lowpass_alpha,
|
||||
cfg->rx.apll
|
||||
);
|
||||
}
|
||||
/*********************************************************************************************\
|
||||
* microphone related functions
|
||||
\*********************************************************************************************/
|
||||
|
@ -229,7 +366,7 @@ exit:
|
|||
audio_i2s.client.stop();
|
||||
}
|
||||
|
||||
audio_i2s.out->I2sMicDeinit();
|
||||
audio_i2s.out->micDeinit();
|
||||
audio_i2s.mic_stop = 0;
|
||||
audio_i2s.mic_error = error;
|
||||
AddLog(LOG_LEVEL_INFO, PSTR("mp3task result code: %d"), error);
|
||||
|
@ -253,7 +390,7 @@ int32_t I2sRecordShine(char *path) {
|
|||
if (audio_i2s.use_stream) {
|
||||
stack = 8000;
|
||||
}
|
||||
audio_i2s.out->I2sMicInit();
|
||||
audio_i2s.out->micInit();
|
||||
|
||||
err = xTaskCreatePinnedToCore(I2sMicTask, "MIC", stack, NULL, 3, &audio_i2s.mic_task_handle, 1);
|
||||
|
||||
|
@ -299,6 +436,12 @@ void I2SSettingsLoad(const char * config_filename, bool erase) {
|
|||
}
|
||||
else if (TfsLoadFile(config_filename, (uint8_t*)audio_i2s.Settings, sizeof(tI2SSettings))) {
|
||||
AddLog(LOG_LEVEL_INFO, "I2S: config loaded from file '%s'", config_filename);
|
||||
if ((audio_i2s.Settings->sys.version == 0) || (audio_i2s.Settings->sys.version > AUDIO_SETTINGS_VERSION)) {
|
||||
delete audio_i2s.Settings;
|
||||
audio_i2s.Settings = new tI2SSettings();
|
||||
AddLog(LOG_LEVEL_DEBUG, "I2S: unsuppoted configuration version, use defaults");
|
||||
I2SSettingsSave(config_filename);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// File system not ready: No flash space reserved for file system
|
||||
|
@ -343,7 +486,7 @@ void I2sInit(void) {
|
|||
return;
|
||||
}
|
||||
|
||||
I2SSettingsLoad("/.drvset042", false); // load configuration (no-erase)
|
||||
I2SSettingsLoad(AUDIO_CONFIG_FILENAME, false); // load configuration (no-erase)
|
||||
if (!audio_i2s.Settings) { return; } // fatal error, could not allocate memory for configuration
|
||||
|
||||
// detect if we need full-duplex on port 0
|
||||
|
@ -469,6 +612,7 @@ void I2sInit(void) {
|
|||
i2s->setRxFreq(audio_i2s.Settings->rx.sample_rate);
|
||||
i2s->setRxChannels(audio_i2s.Settings->rx.channels);
|
||||
i2s->setRate(audio_i2s.Settings->rx.sample_rate);
|
||||
i2s->setRxGain(audio_i2s.Settings->rx.gain);
|
||||
}
|
||||
|
||||
if (i2s->startI2SChannel(tx, rx)) {
|
||||
|
@ -499,13 +643,13 @@ void I2sInit(void) {
|
|||
}
|
||||
audio_i2s.mp3ram = nullptr;
|
||||
|
||||
if (audio_i2s.Settings->rx.mp3_preallocate == 1){
|
||||
if (audio_i2s.Settings->sys.mp3_preallocate == 1){
|
||||
// if (UsePSRAM()) {
|
||||
AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: will allocate buffer for mp3 encoder"));
|
||||
audio_i2s.mp3ram = special_malloc(preallocateCodecSize);
|
||||
// }
|
||||
// else{
|
||||
// audio_i2s.Settings->rx.mp3_preallocate = 0; // no PS-RAM -> no MP3 encoding
|
||||
// audio_i2s.Settings->sys.mp3_preallocate = 0; // no PS-RAM -> no MP3 encoding
|
||||
// }
|
||||
}
|
||||
AddLog(LOG_LEVEL_DEBUG, "I2S: I2sInit done");
|
||||
|
@ -677,14 +821,20 @@ void CmndI2SMic(void) {
|
|||
|
||||
esp_err_t err = ESP_OK;
|
||||
if (audio_i2s.decoder || audio_i2s.mp3) return;
|
||||
audio_i2s.in->I2sMicInit();
|
||||
audio_i2s.in->micInit();
|
||||
if (audio_i2s.in->getRxRunning()) {
|
||||
uint8_t buf[128];
|
||||
|
||||
size_t bytes_read = 0;
|
||||
esp_err_t err = i2s_channel_read(audio_i2s.in->getRxHandle(), buf, sizeof(buf), &bytes_read, 0);
|
||||
if (err == ESP_ERR_TIMEOUT) { err = ESP_OK; }
|
||||
AddLog(LOG_LEVEL_INFO, "I2S: Mic (err:%i size:%i) %*_H", err, bytes_read, bytes_read, buf);
|
||||
int32_t btr = audio_i2s.in->readMic(buf, sizeof(buf), true /*dc_block*/, false /*apply_gain*/, true /*lowpass*/);
|
||||
if (btr < 0) {
|
||||
AddLog(LOG_LEVEL_INFO, "I2S: Mic (err:%i)", -btr);
|
||||
ResponseCmndChar("I2S Mic read error");
|
||||
return;
|
||||
}
|
||||
// esp_err_t err = i2s_channel_read(audio_i2s.in->getRxHandle(), buf, sizeof(buf), &bytes_read, 0);
|
||||
// if (err == ESP_ERR_TIMEOUT) { err = ESP_OK; }
|
||||
AddLog(LOG_LEVEL_INFO, "I2S: Mic (%i) %*_H", btr, btr, buf);
|
||||
}
|
||||
|
||||
// err = xTaskCreatePinnedToCore(I2sMicTask, "MIC", stack, NULL, 3, &audio_i2s.mic_task_handle, 1);
|
||||
|
@ -765,7 +915,7 @@ void CmndI2SI2SRtttl(void) {
|
|||
}
|
||||
|
||||
void CmndI2SMicRec(void) {
|
||||
if (audio_i2s.Settings->rx.mp3_preallocate == 1) {
|
||||
if (audio_i2s.Settings->sys.mp3_preallocate == 1) {
|
||||
if (XdrvMailbox.data_len > 0) {
|
||||
if (!strncmp(XdrvMailbox.data, "-?", 2)) {
|
||||
Response_P("{\"I2SREC-duration\":%d}", audio_i2s.recdur);
|
||||
|
|
|
@ -327,13 +327,13 @@ extern "C" {
|
|||
// AudioInputI2S.begin() -> bool
|
||||
int be_audio_input_i2s_begin(bvm *vm, TasmotaI2S* in) {
|
||||
if (I2SPrepareRx()) { be_raisef(vm, "internal_error", "I2SPrepareRx() failed"); be_return_nil(vm); }
|
||||
in->I2sMicInit();
|
||||
in->micInit();
|
||||
return in->getRxRunning();
|
||||
}
|
||||
|
||||
// AudioInputI2S.stop() -> bool
|
||||
int be_audio_input_i2s_stop(TasmotaI2S* in) {
|
||||
in->I2sMicDeinit();
|
||||
in->micDeinit();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -360,10 +360,25 @@ extern "C" {
|
|||
|
||||
// AudioInputI2S.set_gain(gain:real) -> bool
|
||||
int be_audio_input_set_gain(TasmotaI2S* in, float gain) {
|
||||
return true; // TODO
|
||||
// return in->SetGain(gain);
|
||||
in->setRxGain(gain);
|
||||
return true;
|
||||
}
|
||||
|
||||
// AudioInputI2S.get_dc_offset() -> float
|
||||
int be_audio_input_i2s_get_dc_offset(TasmotaI2S* in) {
|
||||
return in->getRxDCOffset();
|
||||
}
|
||||
|
||||
// AudioInputI2S.get_lowpass_alpha() -> float
|
||||
float be_audio_input_i2s_get_lowpass_alpha(TasmotaI2S* in) {
|
||||
return in->getRxLowpassAlpha();
|
||||
}
|
||||
|
||||
// AudioInputI2S.set_lowpass_alpha(alpha:float) -> bool
|
||||
int be_audio_input_i2s_set_lowpass_alpha(TasmotaI2S* in, float alpha) {
|
||||
in->setRxLowpassAlpha(alpha);
|
||||
return true;
|
||||
}
|
||||
|
||||
// AudioInputI2S.read_bytes() -> bytes()
|
||||
//
|
||||
|
@ -374,15 +389,16 @@ extern "C" {
|
|||
// Returns `bytes()` instance with 16-bits audio
|
||||
int be_audio_input_i2s_read_bytes(bvm *vm) {
|
||||
int argc = be_top(vm);
|
||||
if (!audio_i2s.in->getRxRunning()) { be_return_nil(vm); }
|
||||
|
||||
uint16_t buf_audio[256];
|
||||
be_getmember(vm, 1, ".p");
|
||||
TasmotaI2S *in = (TasmotaI2S*) be_tocomptr(vm, -1);
|
||||
be_pop(vm, 1);
|
||||
|
||||
size_t bytes_read = 0;
|
||||
esp_err_t err = i2s_channel_read(audio_i2s.in->getRxHandle(), buf_audio, sizeof(buf_audio), &bytes_read, 0);
|
||||
// AddLog(LOG_LEVEL_DEBUG, "BRY: be_audio_input_i2s_read_bytes - err=%i bytes=%i", err, bytes_read);
|
||||
if ((err != ESP_OK) && (err != ESP_ERR_TIMEOUT)) { be_return_nil(vm); }
|
||||
if (bytes_read == 0) { be_return_nil(vm); }
|
||||
if (!in->getRxRunning()) { be_return_nil(vm); }
|
||||
|
||||
uint16_t buf_audio[512];
|
||||
int32_t btr = in->readMic((uint8_t*)buf_audio, sizeof(buf_audio), true /*dc_block*/, true /*apply_gain*/, true /*lowpass*/);
|
||||
if (btr <= 0) { be_return_nil(vm); }
|
||||
|
||||
// we received some data
|
||||
if (argc >= 2 && be_isbytes(vm, 2)) {
|
||||
|
@ -391,11 +407,11 @@ extern "C" {
|
|||
// resize to btr
|
||||
be_getmember(vm, -1, "resize");
|
||||
be_pushvalue(vm, -2);
|
||||
be_pushint(vm, bytes_read);
|
||||
be_pushint(vm, btr);
|
||||
be_call(vm, 2);
|
||||
be_pop(vm, 3);
|
||||
} else {
|
||||
be_pushbytes(vm, nullptr, bytes_read); // allocate a buffer of size btr filled with zeros
|
||||
be_pushbytes(vm, nullptr, btr); // allocate a buffer of size btr filled with zeros
|
||||
}
|
||||
|
||||
// get the address of the buffer
|
||||
|
@ -406,7 +422,7 @@ extern "C" {
|
|||
be_pop(vm, 2);
|
||||
|
||||
// copy
|
||||
memmove(buf, buf_audio, bytes_read);
|
||||
memmove(buf, buf_audio, btr);
|
||||
|
||||
be_return(vm); /* return code */
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue