From 11aad19800ca8f534ffff5abf1955211b7f15a01 Mon Sep 17 00:00:00 2001 From: s-hadinger <49731213+s-hadinger@users.noreply.github.com> Date: Thu, 5 Oct 2023 21:47:07 +0200 Subject: [PATCH] Audio support for Microphone + Berry (#19677) --- .../berry_tasmota/src/be_i2s_audio_lib.c | 12 ++++ lib/libesp32/berry_tasmota/src/be_light_lib.c | 7 ++ tasmota/berry/audio/audio_slm.be | 71 +++++++++++++++++++ .../xdrv_42_0_i2s_0_lib_idf51.ino | 32 ++++++--- .../xdrv_42_0_i2s_audio_idf51.ino | 2 +- .../xdrv_52_3_berry_audio.ino | 47 +++++++++++- .../xdrv_52_3_berry_light.ino | 5 ++ 7 files changed, 165 insertions(+), 11 deletions(-) create mode 100644 tasmota/berry/audio/audio_slm.be diff --git a/lib/libesp32/berry_tasmota/src/be_i2s_audio_lib.c b/lib/libesp32/berry_tasmota/src/be_i2s_audio_lib.c index 707d98331..b14794832 100644 --- a/lib/libesp32/berry_tasmota/src/be_i2s_audio_lib.c +++ b/lib/libesp32/berry_tasmota/src/be_i2s_audio_lib.c @@ -139,6 +139,14 @@ BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_get_lowpass_alpha, "f", "."); 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"); +// AudioInputI2S.rms_bytes(int16*:bytes) -> int +extern int be_audio_input_i2s_rms_bytes(void *buf, size_t len); +BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_rms_bytes, "i", "(bytes)~"); + +// AudioInputI2S.sqrt_fast(int) -> int +extern int be_audio_input_i2s_sqrt_fast(int in); +BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_sqrt_fast, "i", "i"); + #include "be_fixed_be_class_AudioOutputI2S.h" #include "be_fixed_be_class_AudioGenerator.h" #include "be_fixed_be_class_AudioGeneratorWAV.h" @@ -201,6 +209,7 @@ class be_class_AudioFileSourceFS (scope: global, name: AudioFileSourceFS, super: class be_class_AudioInputI2S (scope: global, name: AudioInputI2S, strings: weak) { .p, var + peak, var init, ctype_func(be_audio_input_i2s_init) deinit, ctype_func(be_audio_input_i2s_deinit) @@ -218,6 +227,9 @@ class be_class_AudioInputI2S (scope: global, name: AudioInputI2S, strings: weak) get_lowpass_alpha, ctype_func(be_audio_input_i2s_get_lowpass_alpha) set_lowpass_alpha, ctype_func(be_audio_input_i2s_set_lowpass_alpha) + + rms_bytes, static_ctype_func(be_audio_input_i2s_rms_bytes) + sqrt_fast, static_ctype_func(be_audio_input_i2s_sqrt_fast) } @const_object_info_end */ diff --git a/lib/libesp32/berry_tasmota/src/be_light_lib.c b/lib/libesp32/berry_tasmota/src/be_light_lib.c index cb0393411..a0153e10c 100644 --- a/lib/libesp32/berry_tasmota/src/be_light_lib.c +++ b/lib/libesp32/berry_tasmota/src/be_light_lib.c @@ -4,6 +4,7 @@ * To use: `import tasmota` *******************************************************************/ #include "be_constobj.h" +#include "be_mapping.h" #ifdef USE_LIGHT extern int l_getlight(bvm *vm); @@ -13,6 +14,10 @@ extern int l_gamma8(bvm *vm); extern int l_gamma10(bvm *vm); extern int l_rev_gamma10(bvm *vm); +// light.set_bri(bri:int) -> nil +extern void l_set_bri(int bri); +BE_FUNC_CTYPE_DECLARE(l_set_bri, "", "i"); + /* @const_object_info_begin module light (scope: global) { get, func(l_getlight) @@ -21,6 +26,8 @@ module light (scope: global) { gamma8, func(l_gamma8) gamma10, func(l_gamma10) reverse_gamma10, func(l_rev_gamma10) + + set_bri, ctype_func(l_set_bri) } @const_object_info_end */ #include "be_fixed_light.h" diff --git a/tasmota/berry/audio/audio_slm.be b/tasmota/berry/audio/audio_slm.be new file mode 100644 index 000000000..f1c2dacc5 --- /dev/null +++ b/tasmota/berry/audio/audio_slm.be @@ -0,0 +1,71 @@ +# This is sample code for audio SLM (Sound Level Meter) +# +# It uses typical cheap mems microphone in PDM mode, +# and uses a DC block filter (high pass), a fixed gain +# and a low pass filter (currently cutting at 3KHz) +# +# add `import audio_slm` in `autoexec.be`` +# +class Audio_SLM + var audio_input, fast_loop_closure + var buffer + var rms + var peak + var noise_gate # ignore below this level + def init() + # tasmota.cmd("SaveData 0") + self.buffer = bytes(1024) # resuse same buffer at each iteration + self.audio_input = AudioInputI2S() + self.audio_input.set_gain(30) + self.rms = 0 + self.peak = 0 + self.noise_gate = 150 + + self.audio_input.begin() + tasmota.delay(300) # wait for 300 ms for mems microphone to stabilize + self.fast_loop_closure = def () self.fast_loop() end + #tasmota.add_fast_loop(self.fast_loop_closure) + tasmota.add_driver(self) + tasmota.set_timer(100, /-> tasmota.add_fast_loop(self.fast_loop_closure)) # delay start by 1 second + end + + def stop() + self.audio_input.stop() + tasmota.remove_fast_loop(self.fast_loop_closure) + tasmota.remove_driver(self) + end + + def fast_loop() + var b = self.audio_input.read_bytes(self.buffer) + if b != nil + import light + var rms = AudioInputI2S.rms_bytes(b) + if rms < self.noise_gate + rms = 0 + end + if rms > self.rms + self.rms = rms + end + + var new_peak = self.audio_input.peak + if new_peak == nil new_peak = 0 end # failsafe + if new_peak > self.peak self.peak = new_peak end + # light.set_bri(rms / 80 #-range 0..255-#, true #-no save-#) + end + end + + #- display sensor value in the web UI -# + def web_sensor() + var msg = format( + "{s}SLM Gain %ix{m}RMS: %i (peak %i%%){e}", + self.audio_input.get_gain(), + self.rms, + self.peak / 32767) + tasmota.web_send(msg) + self.peak = 0 + self.rms = 0 + end +end + +audio_slm = Audio_SLM() +return audio_slm diff --git a/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s_0_lib_idf51.ino b/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s_0_lib_idf51.ino index 34158bb28..5747851ed 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s_0_lib_idf51.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s_0_lib_idf51.ino @@ -169,7 +169,7 @@ 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); + int32_t readMic(uint8_t *buffer, uint32_t size, bool dc_block, bool apply_gain, bool lowpass, uint32_t *peak_ptr); // The following is now the preferred function // and allows to send multiple samples at once @@ -558,7 +558,11 @@ void TasmotaI2S::micDeinit(void) { // 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) { +// peak: peak value of the mic (can go above 32767 to indicate clipping) +int32_t TasmotaI2S::readMic(uint8_t *buffer, uint32_t size, bool dc_block, bool apply_gain, bool lowpass, uint32_t *peak_ptr) { + uint32_t peak = 0; + if (peak_ptr) { *peak_ptr = peak; } + if (!audio_i2s.in->getRxRunning()) { return -1; } size_t btr = 0; @@ -587,14 +591,23 @@ int32_t TasmotaI2S::readMic(uint8_t *buffer, uint32_t size, bool dc_block, bool } // apply gain - if (apply_gain) { - for (uint32_t i=0; i 0x7FFF) { val = 0x7FFF; } - if (val < -0x8000) { val = -0x8000; } - samples[i] = val; + for (uint32_t i=0; i 0x7FFF) { + if (val > peak) { peak = val; } // clipping + val = 0x7FFF; + } else if (val < -0x8000) { + val = -0x8000; + if (-val > peak) { peak = -val; } // clipping + } else { + if ((val > 0) && (val > peak)) { peak = val; } + if ((val < 0) && (-val > peak)) { peak = -val; } + } + samples[i] = val; } // lowpass filter @@ -605,6 +618,7 @@ int32_t TasmotaI2S::readMic(uint8_t *buffer, uint32_t size, bool dc_block, bool } } + if (peak_ptr) { *peak_ptr = peak; } return btr; } diff --git a/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s_audio_idf51.ino b/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s_audio_idf51.ino index df65b9e2d..fefbd5d36 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s_audio_idf51.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s_audio_idf51.ino @@ -826,7 +826,7 @@ void CmndI2SMic(void) { uint8_t buf[128]; size_t bytes_read = 0; - int32_t btr = audio_i2s.in->readMic(buf, sizeof(buf), true /*dc_block*/, false /*apply_gain*/, true /*lowpass*/); + int32_t btr = audio_i2s.in->readMic(buf, sizeof(buf), true /*dc_block*/, false /*apply_gain*/, true /*lowpass*/, nullptr /*peak_ptr*/); if (btr < 0) { AddLog(LOG_LEVEL_INFO, "I2S: Mic (err:%i)", -btr); ResponseCmndChar("I2S Mic read error"); diff --git a/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_audio.ino b/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_audio.ino index c2d034e10..e2085707b 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_audio.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_audio.ino @@ -397,9 +397,15 @@ extern "C" { 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*/); + uint32_t peak; + int32_t btr = in->readMic((uint8_t*)buf_audio, sizeof(buf_audio), true /*dc_block*/, true /*apply_gain*/, true /*lowpass*/, &peak); if (btr <= 0) { be_return_nil(vm); } + // update peak attribute + be_pushint(vm, peak); + be_setmember(vm, 1, "peak"); + be_pop(vm, 1); + // we received some data if (argc >= 2 && be_isbytes(vm, 2)) { // we have already a bytes() buffer @@ -427,6 +433,45 @@ extern "C" { be_return(vm); /* return code */ } + // Fast int sqrt from https://stackoverflow.com/questions/65986056/is-there-a-non-looping-unsigned-32-bit-integer-square-root-function-c + // Time spend is 5-7 microseconds + uint16_t be_isqrt32(uint32_t x) { + int lz = __builtin_clz(x | 1) & 30; + x <<= lz; + uint32_t y = 1 + (x >> 30); + y = (y << 1) + (x >> 27) / y; + y = (y << 3) + (x >> 21) / y; + y = (y << 7) + (x >> 9) / y; + y -= x < (uint32_t)y * y; + return y >> (lz >> 1); + } + + // AudioInputI2S.sqrt_fast(int) -> int + // debug testing - fast sqrt + int be_audio_input_i2s_sqrt_fast(int in) { + uint32_t now = micros(); + int32_t ret = be_isqrt32(in); + uint32_t now_after = micros(); + AddLog(LOG_LEVEL_DEBUG, "sqrtf: %d -> %d (%d us)", in, ret, now_after - now); + return ret; + } + + // AudioInputI2S.rms_bytes(int16*:bytes) -> int + // compute RMS of a bytes buffer + int be_audio_input_i2s_rms_bytes(void *buf, size_t len_bytes) { + size_t len = len_bytes / 2; + if (buf == NULL || len == 0) { return 0; } // failsafe + + int16_t * buf16 = (int16_t*) buf; + uint32_t sum = 0; + for (int i = 0; i < len; i++) { + sum += (buf16[i] * buf16[i]) / 256; + // AddLog(LOG_LEVEL_DEBUG, "I2S: buf16[%i]:%i sum:%i", i, buf16[i], sum); + } + int32_t rms = be_isqrt32(sum / len * 256); + // AddLog(LOG_LEVEL_DEBUG, "I2S: buf:%p rms sum/len:%i sum:%i len:%i ret:%i", buf16, sum/len, sum, len, rms); + return rms; + } } #endif // USE_I2S_AUDIO_BERRY diff --git a/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_light.ino b/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_light.ino index 2b6072f16..b268dba3d 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_light.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_light.ino @@ -320,6 +320,11 @@ extern "C" { } be_raise(vm, kTypeError, nullptr); } + + // light.set_bri(bri:int) -> nil + void l_set_bri(int bri) { + light_controller.changeBri(bri); + } } #endif // USE_LIGHT