From 795a194d6552a6f799cad87883976cf083723d8e Mon Sep 17 00:00:00 2001 From: s-hadinger <49731213+s-hadinger@users.noreply.github.com> Date: Mon, 2 Oct 2023 09:18:53 +0200 Subject: [PATCH] Audio prepare for Arduino3 (#19637) --- .../berry_tasmota/src/be_i2s_audio_lib.c | 40 ++ .../xdrv_42_0_i2s_0_config_idf51.ino | 168 +++++ .../xdrv_42_0_i2s_0_lib_idf51.ino | 603 ++++++++++++++++++ .../xdrv_42_0_i2s__lib_idf51.ino | 428 ------------- .../xdrv_42_0_i2s_audio_idf51.ino | 461 +++++++------ .../xdrv_52_3_berry_audio.ino | 108 +++- .../xdrv_52_3_berry_udp.ino | 2 +- 7 files changed, 1189 insertions(+), 621 deletions(-) create mode 100644 tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s_0_config_idf51.ino create mode 100644 tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s_0_lib_idf51.ino delete mode 100644 tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s__lib_idf51.ino 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 081f56868..3a3a8759d 100644 --- a/lib/libesp32/berry_tasmota/src/be_i2s_audio_lib.c +++ b/lib/libesp32/berry_tasmota/src/be_i2s_audio_lib.c @@ -97,6 +97,35 @@ BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_init, "+.p", ""); extern void* be_audio_input_i2s_deinit(void* instance); BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_deinit, "", "."); +// AudioInputI2S.begin() -> bool +extern int be_audio_input_i2s_begin(bvm *vm, void* in); +BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_begin, "b", "@."); + +// AudioInputI2S.stop() -> bool +extern int be_audio_input_i2s_stop(void* in); +BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_stop, "b", "."); + +extern int be_audio_input_i2s_read_bytes(bvm *vm); + +// AudioInputI2S.set_gain(gain:real) -> bool +extern int be_audio_input_set_gain(void* in, float gain); +BE_FUNC_CTYPE_DECLARE(be_audio_input_set_gain, "b", ".f"); + +// AudioInputI2S.get_rate() -> int +extern int be_audio_input_i2s_get_rate(void* in); +BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_get_rate, "i", "."); + +// AudioInputI2S.get_bits_per_sample() -> int +extern int be_audio_input_i2s_get_bits_per_sample(void* in); +BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_get_bits_per_sample, "i", "."); + +// AudioInputI2S.get_channels() -> int +extern int be_audio_input_i2s_get_channels(void* in); +BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_get_channels, "i", "."); + +// AudioInputI2S.get_gain() -> real +extern float be_audio_input_i2s_get_gain(void* in); +BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_get_gain, "f", "."); #include "be_fixed_be_class_AudioOutputI2S.h" #include "be_fixed_be_class_AudioGenerator.h" @@ -162,6 +191,17 @@ class be_class_AudioInputI2S (scope: global, name: AudioInputI2S, strings: weak) .p, var init, ctype_func(be_audio_input_i2s_init) deinit, ctype_func(be_audio_input_i2s_deinit) + + begin, ctype_func(be_audio_input_i2s_begin) + stop, ctype_func(be_audio_input_i2s_stop) + read_bytes, func(be_audio_input_i2s_read_bytes) + + get_rate, ctype_func(be_audio_input_i2s_get_rate) + get_bits_per_sample, ctype_func(be_audio_input_i2s_get_bits_per_sample) + get_channels, ctype_func(be_audio_input_i2s_get_channels) + get_gain, ctype_func(be_audio_input_i2s_get_gain) + + set_gain, ctype_func(be_audio_input_set_gain) } @const_object_info_end */ diff --git a/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s_0_config_idf51.ino b/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s_0_config_idf51.ino new file mode 100644 index 000000000..f71bb3b15 --- /dev/null +++ b/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s_0_config_idf51.ino @@ -0,0 +1,168 @@ +/* + xdrv_42_0_i2s_0_config_idf51.ino - Simplified Audio library, general configuration + + Copyright (C) 2021 Gerhard Mutz, Theo Arends, Staars, Stephan Hadinger + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#if defined(ESP32) && ESP_IDF_VERSION_MAJOR >= 5 +#ifdef USE_I2S_AUDIO + +#include "driver/i2s_std.h" +#include "driver/i2s_pdm.h" +#include "driver/gpio.h" +#include "soc/soc_caps.h" + +#include "AudioFileSourcePROGMEM.h" +#include "AudioFileSourceID3.h" +#include "AudioGeneratorMP3.h" + +#include +#include "AudioFileSourceFS.h" +#include "AudioGeneratorTalkie.h" +#include "AudioFileSourceICYStream.h" +#include "AudioFileSourceBuffer.h" +#include "AudioGeneratorAAC.h" + +#include +#include + +/*********************************************************************************************\ + * Driver Settings in memory +\*********************************************************************************************/ + +// I2S communication mode +enum : int8_t { + I2S_MODE_STD = 0, // I2S mode standard + I2S_MODE_PDM = 1, // I2S mode PDM + I2S_MODE_TDM = 2, // I2S mode TDM + I2S_MODE_DAC = 3, // Using internal DAC - only available on ESP32 +}; + +// I2S slot mask (left, right, both) +enum : int8_t { + I2S_SLOT_NOCHANGE = 0, // don't change default + I2S_SLOT_LEFT = 1, // left + I2S_SLOT_RIGHT = 2, // right + I2S_SLOT_BOTH = 3, // both +}; + +// I2S slot configuration +enum : int8_t { + I2S_SLOT_MSB = 0, // MSB + I2S_SLOT_PCM = 1, // PCM + I2S_SLOT_PHILIPS = 2, // Philips +}; + +#define I2S_SLOTS 2 + +typedef struct{ + struct{ + uint8_t version = 0; // 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 + bool tx = 0; // B02 - depends on GPIO setting + bool rx = 0; // B03 - depends on GPIO setting + bool exclusive = 0; // B04 - depends on GPIO setting, if WS is shared between 2 ports, drivers needs to be reinstalled before being used (Yuck... but we don't have a choice) + + 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 + } sys; + struct { + uint32_t sample_rate = 16000; // B00-03 + uint8_t gain = 10; // B04 - was `volume` + uint8_t mode = I2S_MODE_STD; // 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 = 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 + } 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 + } rx; +} tI2SSettings; + +typedef union { + uint8_t data; + struct { + uint8_t master : 1; + uint8_t enabled : 1; + uint8_t swap_mic : 1; + uint8_t mode : 2; + }; +} BRIDGE_MODE; + +class TasmotaI2S; + +struct AUDIO_I2S_t { + tI2SSettings *Settings; + + i2s_chan_handle_t rx_handle = nullptr; + + AudioGeneratorMP3 *mp3 = nullptr; + AudioFileSourceFS *file = nullptr; + + TasmotaI2S *out = nullptr; // instance used for I2S output, or `nullptr` if none + TasmotaI2S *in = nullptr; // instance used for I2S input, or `nullptr` if none (it can be the same as `out` in case of full duplex) + + AudioFileSourceID3 *id3 = nullptr; + AudioGeneratorMP3 *decoder = NULL; + void *mp3ram = NULL; + + TaskHandle_t mp3_task_handle; + TaskHandle_t mic_task_handle; + + char mic_path[32]; + uint8_t mic_stop; + int8_t mic_error; + bool use_stream = false; + + +// SHINE + uint32_t recdur; + uint8_t stream_active; + uint8_t stream_enable; + WiFiClient client; + ESP8266WebServer *MP3Server; + +// I2S_BRIDGE + BRIDGE_MODE bridge_mode; + WiFiUDP i2s_bridge_udp; + WiFiUDP i2s_bridgec_udp; + IPAddress i2s_bridge_ip; + TaskHandle_t i2s_bridge_h; + int8_t ptt_pin = -1; + +} audio_i2s; + +#endif // USE_I2S_AUDIO +#endif // defined(ESP32) && ESP_IDF_VERSION_MAJOR >= 5 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 new file mode 100644 index 000000000..633048776 --- /dev/null +++ b/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s_0_lib_idf51.ino @@ -0,0 +1,603 @@ +/* + xdrv_42_0_i2s_0_lib_idf51.ino - Simplified Audio library, core class + + Copyright (C) 2021 Gerhard Mutz, Theo Arends, Staars, Stephan Hadinger + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#if defined(ESP32) && ESP_IDF_VERSION_MAJOR >= 5 +#ifdef USE_I2S_AUDIO + +/*********************************************************************************************\ + * Driver Settings in memory +\*********************************************************************************************/ + +/*********************************************************************************************\ + * Reminder of esp_err codes + * + * #define ESP_OK 0 - esp_err_t value indicating success (no error) + * #define ESP_FAIL -1 - Generic esp_err_t code indicating failure + * + * #define ESP_ERR_NO_MEM 0x101 257 - Out of memory + * #define ESP_ERR_INVALID_ARG 0x102 258 - Invalid argument + * #define ESP_ERR_INVALID_STATE 0x103 259 - Invalid state + * #define ESP_ERR_INVALID_SIZE 0x104 260 - Invalid size + * #define ESP_ERR_NOT_FOUND 0x105 261 - Requested resource not found + * #define ESP_ERR_NOT_SUPPORTED 0x106 262 - Operation or feature not supported + * #define ESP_ERR_TIMEOUT 0x107 263 - Operation timed out + * #define ESP_ERR_INVALID_RESPONSE 0x108 264 - Received response was invalid + * #define ESP_ERR_INVALID_CRC 0x109 265 - CRC or checksum was invalid + * #define ESP_ERR_INVALID_VERSION 0x10A 266 - Version was invalid + * #define ESP_ERR_INVALID_MAC 0x10B 267 - MAC address was invalid + * #define ESP_ERR_NOT_FINISHED 0x10C 268 - Operation has not fully completed +\*********************************************************************************************/ + +/*********************************************************************************************\ + * This is the central class to acccess I2S in (rx) or out (tx) + * + * It inherits from AudioOutput so it can be used as output instance for ESP8266Audio library + * + * It also supports microphone input + * +\*********************************************************************************************/ + +class TasmotaI2S : public AudioOutput +{ +public: + + // Constructor takes no parameter, everything is configured from template and config file + TasmotaI2S() { + // set some defaults + hertz = 16000; + bps = I2S_DATA_BIT_WIDTH_16BIT; + channels = 2; + gainF2P6 = 32; // equivalent of 0.5 + } + + ~TasmotaI2S() { + this->stop(); + } + + // Settings + void setPinout(int32_t bclk, int32_t ws, int32_t dout, int32_t mclk, int32_t din, + bool mclk_inv = false, bool bclk_inv = false, bool ws_inv = false, bool apll = false); + + void setSlotConfig(i2s_port_t i2s_port, uint8_t tx_slot_config, uint8_t rx_slot_config, + uint8_t tx_slot_mask, uint8_t rx_slot_mask) { + _i2s_port = i2s_port; + _tx_slot_config = tx_slot_config; + _rx_slot_config = rx_slot_config; + } + void setRxFreq(uint16_t freq) { _rx_freq = freq; } + + // ------------------------------------------------------------------------------------------ + // Setters for configuration parameters + // + // TODO: not sure we still need them since all this should be set at initialiation + // ------------------------------------------------------------------------------------------ + virtual bool SetBitsPerSample(int bits) { + AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: SetBitsPerSample: %i"), bits); + if ( (bits != 16) && (bits != 8) ) { return false; } + this->bps = bits; + return true; + } + + virtual bool SetChannels(int channels) { + AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: SetChannels: %i"), channels); + if ((channels < 1) || (channels > 2)) { return false; } + if (channels == (int)this->channels) { return true; } + this->channels = channels; + return true; + } + + virtual bool SetRate(int hz) { + AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: SetRate: %i was %i on=%i"), hz, this->hertz, _i2s_on); + if (hz == (int) this->hertz) { return true; } + this->hertz = hz; + if (_i2s_on) { + int result = updateClockConfig(); + } + return true; + } + + virtual bool SetGain(float f) { + AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: SetGain: %_f"), &f); + return AudioOutput::SetGain(f); + } + + // ------------------------------------------------------------------------------------------ + // Getters + inline bool isDuplex(void) const { return _tx_configured && _rx_configured; } + + inline bool getExclusive(void) const {return _exclusive; } + inline uint8_t getTxMode(void) const { return _tx_mode; } + inline uint8_t getTxChannels(void) const { return channels; } + inline bool getTxRunning(void) const { return _tx_running; } + inline i2s_chan_handle_t getTxHandle(void) const { return _tx_handle; } + + inline uint8_t getRxMode(void) const { return _rx_mode; } + 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; } + + // ------------------------------------------------------------------------------------------ + // Setters + inline void setExclusive(bool exclusive) { _exclusive = exclusive; } + inline void setTxMode(uint8_t mode) { _tx_mode = mode; } + inline void setTxChannels(uint8_t channels) { SetChannels(channels); } + inline void setTxRunning(bool running) { _tx_running = running; } + 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; } + + // ------------------------------------------------------------------------------------------ + // AudioOutput has inconsistent case for methods, provide consistent setters for super class + inline void setRate(int hz) { SetRate(hz); } + inline void setBitsPerSample(int bits) { SetBitsPerSample(bits); } + inline void setChannels(int chan) { SetChannels(chan); } + inline void setGain(float f) { SetGain(f); } + + bool begin(void); + bool stop(void); + bool ConsumeSample(int16_t sample[2]); + bool startI2SChannel(bool tx, bool rx); + int updateClockConfig(void); + + // The following is now the preferred function + // and allows to send multiple samples at once + // + // Max 128 samples, it is clipped otherwise + // Returns: the number of samples actually consumed + // or -1 if an error occured + // + // The call is non blocking and does not wait + int32_t consumeSamples(int16_t *samples, size_t count); + + // ------------------------------------------------------------------------------------------ + // Microphone related methods + uint32_t I2sMicInit(void); + void I2sMicDeinit(void); + +protected: + void loadSettings(void); // load all settings from Settings file and template - the goal is to have zero touch + +protected: + + bool _i2s_on = false; // is I2S audio active + bool _exclusive = false; // in exclusive mode, stopping this instance needs to uninstall driver, and reinstall for next use + i2s_port_t _i2s_port = I2S_NUM_AUTO; // I2S port, I2S_NUM_0/I2S_NUM_1/I2S_NUM_AUTO + + // local copy of useful settings for audio + // TX + bool _tx_configured = false; // true = configured, false = not configured + uint8_t _tx_mode = I2S_MODE_STD; // I2S_MODE_STD / I2S_MODE_PDM / I2S_MODE_TDM / I2S_MODE_DAC + uint8_t _tx_slot_mask = I2S_SLOT_NOCHANGE; + bool _tx_running = false; // true = enabled, false = disabled + // uint8_t _tx_channels = 2; // number of channels, 1 = mono, 2 = stereo -- `channels` + i2s_chan_handle_t _tx_handle = nullptr; // I2S channel handle, automatically computed + uint8_t _tx_slot_config = I2S_SLOT_MSB;// I2S slot configuration + + // RX + bool _rx_configured = false; // true = configured, false = not configured + uint8_t _rx_mode = I2S_MODE_STD; // I2S_MODE_STD / I2S_MODE_PDM / I2S_MODE_TDM / I2S_MODE_DAC + uint8_t _rx_slot_mask = I2S_SLOT_NOCHANGE; + bool _rx_running = false; // true = enabled, false = disabled + uint8_t _rx_channels = 2; // number of channels, 1 = mono, 2 = stereo + i2s_chan_handle_t _rx_handle = nullptr; // I2S channel handle, automatically computed + uint8_t _rx_slot_config = I2S_SLOT_MSB;// I2S slot configuration + uint16_t _rx_freq = 16000; // I2S Rx sampling frequency in Hz + + // 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 + gpio_num_t _gpio_ws = GPIO_NUM_NC; // GPIO for word select + gpio_num_t _gpio_dout = GPIO_NUM_NC; // GPIO for data out + gpio_num_t _gpio_din = GPIO_NUM_NC; // GPIO for data in + bool _gpio_mclk_inv = false; // invert master clock + bool _gpio_bclk_inv = false; // invert bit clock + bool _gpio_ws_inv = false; // invert word select + bool _apll = false; // use APLL instead of PLL +}; + + +// ------------------------------------------------------------------------------------------ +// Methods +// ------------------------------------------------------------------------------------------ + +void TasmotaI2S::setPinout(int32_t bclk, int32_t ws, int32_t dout, int32_t mclk, int32_t din, + bool mclk_inv, bool bclk_inv, bool ws_inv, bool apll) { + _gpio_mclk = (gpio_num_t) mclk; + _gpio_bclk = (gpio_num_t) bclk; + _gpio_ws = (gpio_num_t) ws; + _gpio_dout = (gpio_num_t) dout; + _gpio_din = (gpio_num_t) din; + _gpio_mclk_inv = mclk_inv; + _gpio_bclk_inv = bclk_inv; + _gpio_ws_inv = ws_inv; + _apll = apll; + + _tx_configured = (_gpio_dout != GPIO_NUM_NC); + _rx_configured = (_gpio_din != GPIO_NUM_NC); + + AddLog(LOG_LEVEL_DEBUG, "I2S: setPinout: gpios[%i,%i,%i,%i,%i] inv[%i,%i,%i] apll:%i _tx_configured:%i _rx_configured:%i", + _gpio_mclk, _gpio_bclk, _gpio_ws, _gpio_dout, _gpio_din, + _gpio_mclk_inv, _gpio_bclk_inv, _gpio_ws_inv, _apll, + _tx_configured, _rx_configured); +} + +bool TasmotaI2S::begin() { + AddLog(LOG_LEVEL_DEBUG, "I2S: begin _tx_running:%i _i2s_on:%i", _tx_running, _i2s_on); + if (_tx_running) { return true; } + // if (!_i2s_on) { + // if ((!_rx_configured || !_tx_configured) && _rx_configured) { // not duplex -- TODO ? + // this->startI2SChannel(); + // } + // } + int result = i2s_channel_enable(_tx_handle); + if (result != 0){ + AddLog(LOG_LEVEL_INFO, "I2S: Could not enable i2s_channel: %i", result); + return false; + } + _tx_running = true; + AddLog(LOG_LEVEL_DEBUG, "I2S: begin _tx_running succeeded"); + return true; +} + +bool TasmotaI2S::stop() { + i2s_channel_disable(_tx_handle); + if ((!_rx_configured || !_tx_configured) && _rx_configured) { // not duplex -- TODO ? + i2s_del_channel(_tx_handle); + _i2s_on = false; + AddLog(LOG_LEVEL_INFO, "I2S: stop: I2S channel disabled"); + } + _tx_running = false; + return true; +} + +bool TasmotaI2S::ConsumeSample(int16_t sample[2]) { + return consumeSamples(sample, 1); +} + +int32_t TasmotaI2S::consumeSamples(int16_t *samples, size_t count) { + if (!_tx_running) { return -1; } + if (count == 0) { return 0; } + if (count > 128) { count = 128; } + + int16_t ms[count*2]; + for (int32_t i = 0; i < count; i++) { + int16_t left = samples[i*2 + LEFTCHANNEL]; + int16_t right = samples[i*2 + RIGHTCHANNEL]; + + if (channels == 1) { // if mono, average the two samples + // Average the two samples and overwrite + int32_t ttl = left + right; + left = right = (ttl>>1) & 0xffff; + } + + if (bps == 8) { + left = (((int16_t)(left & 0xff)) - 128) << 8; + right = (((int16_t)(right & 0xff)) - 128) << 8; + } + + if (_tx_mode == I2S_MODE_DAC) { + left = Amplify(left) + 0x8000; + right = Amplify(right) + 0x8000; + } else { + left = Amplify(left); + right = Amplify(right); + } + + ms[i*2 + LEFTCHANNEL] = left; + ms[i*2 + RIGHTCHANNEL] = right; + } + + // AddLog(LOG_LEVEL_DEBUG, "I2S: consumeSamples: left=%i right=%i", ms[0], ms[1]); + + size_t i2s_bytes_written; + esp_err_t err = i2s_channel_write(_tx_handle, ms, sizeof(ms), &i2s_bytes_written, 0); + if (err && err != ESP_ERR_TIMEOUT) { + AddLog(LOG_LEVEL_INFO, "I2S: Could not write samples (count=%i): %i", count, err); + return -1; + } + return i2s_bytes_written; +} + +// Initialize I2S channel +// return `true` if successful, `false` otherwise +bool TasmotaI2S::startI2SChannel(bool tx, bool rx) { + if (!tx) { _tx_configured = false; } + if (!rx) { _rx_configured = false; } + if (!_tx_configured && !_rx_configured) { return false; } // nothing configured + + esp_err_t err = ESP_OK; + gpio_num_t _DIN = I2S_GPIO_UNUSED; // no input pin by default + + if (_tx_configured) { + // default dma_desc_num = 6 (DMA buffers), dma_frame_num = 240 (frames per buffer) + i2s_chan_config_t tx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_1, I2S_ROLE_MASTER); + + AddLog(LOG_LEVEL_DEBUG, "I2S: tx_chan_cfg id:%i role:%i dma_desc_num:%i dma_frame_num:%i auto_clear:%i", + tx_chan_cfg.id, tx_chan_cfg.role, tx_chan_cfg.dma_desc_num, tx_chan_cfg.dma_frame_num, tx_chan_cfg.auto_clear); + + if (_tx_configured && _rx_configured) { + _DIN = (gpio_num_t)_gpio_din; + err = i2s_new_channel(&tx_chan_cfg, &_tx_handle, &audio_i2s.out->_rx_handle); + } else{ + err = i2s_new_channel(&tx_chan_cfg, &_tx_handle, NULL); + } + + AddLog(LOG_LEVEL_DEBUG, "I2S: i2s_new_channel err:%i", err); + + // by default we configure for MSB 2 slots `I2S_STD_MSB_SLOT_DEFAULT_CONFIG` + i2s_std_config_t tx_std_cfg = { + .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(hertz), + .slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t)bps, (i2s_slot_mode_t)channels), + .gpio_cfg = { + .mclk = _gpio_mclk, + .bclk = _gpio_bclk, + .ws = _gpio_ws, + .dout = _gpio_dout, + .din = _DIN, + .invert_flags = { + .mclk_inv = _gpio_mclk_inv, + .bclk_inv = _gpio_bclk_inv, + .ws_inv = _gpio_ws_inv, + } + } + }; + + // change configuration if we are using PCM or PHILIPS + if (_tx_slot_config == I2S_SLOT_PCM) { // PCM + tx_std_cfg.slot_cfg = I2S_STD_PCM_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t)bps, (i2s_slot_mode_t)channels); + } else if (_tx_slot_config == I2S_SLOT_PHILIPS) { // PHILIPS + tx_std_cfg.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t)bps, (i2s_slot_mode_t)channels); + } + if (_tx_slot_mask != I2S_SLOT_NOCHANGE) { tx_std_cfg.slot_cfg.slot_mask = (i2s_std_slot_mask_t)_tx_slot_mask; } + + // AddLog(LOG_LEVEL_INFO, ">>>: I2S tx_chan_cfg=%*_H", sizeof(tx_chan_cfg), &tx_chan_cfg); + // AddLog(LOG_LEVEL_INFO, ">>>: I2S tx_std_cfg=%*_H", sizeof(tx_std_cfg), &tx_std_cfg); + + err = i2s_channel_init_std_mode(_tx_handle, &tx_std_cfg); + AddLog(LOG_LEVEL_DEBUG, "I2S: TX channel bits:%i channels:%i hertz:%i initialized err=0x%04X", bps, channels, hertz, err); + if (err != ERR_OK) { + _i2s_on = false; + return false; + } + + _i2s_on = true; + if (_rx_configured) { // full duplex mode + err = i2s_channel_init_std_mode(audio_i2s.out->_rx_handle, &tx_std_cfg); + AddLog(LOG_LEVEL_DEBUG, "I2S: i2s_channel_init_std_mode err:%i", err); + AddLog(LOG_LEVEL_DEBUG, "I2S: RX channel added in full duplex mode"); + } + } // if (tx) + + // configure Rx Microphone + if (_rx_configured && !_tx_configured) { // if Microphone and not duplex + gpio_num_t clk_gpio; + + 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 + } + + 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_t)_rx_channels), + .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, _rx_channels, err); + _i2s_on = true; + } + 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); + _i2s_on = true; + } + break; + default: + AddLog(LOG_LEVEL_INFO, "I2S: invalid rx mode=%i", _rx_mode); + } + } + return true; +} + +int TasmotaI2S::updateClockConfig(void) { + i2s_channel_disable(_tx_handle); + i2s_std_clk_config_t clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(hertz); +#ifdef SOC_I2S_SUPPORTS_APLL + if (_apll) { + clk_cfg.clk_src = I2S_CLK_SRC_APLL; + } +#endif + int result = i2s_channel_reconfig_std_clock(_tx_handle, &clk_cfg ); + if (_tx_running) { i2s_channel_enable(_tx_handle); } + AddLog(LOG_LEVEL_DEBUG, "I2S: Updating clock config"); + return result; +} + +/*********************************************************************************************\ + * microphone related functions +\*********************************************************************************************/ + +uint32_t TasmotaI2S::I2sMicInit(void) { + if (!_rx_configured) { return 0; } // nothing configured + + esp_err_t err = ESP_OK; + gpio_num_t clk_gpio; + + 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); + _rx_running = true; + } + return err; +} + + +void TasmotaI2S::I2sMicDeinit(void) { + esp_err_t err = ESP_OK; + gpio_num_t clk_gpio; + + AddLog(LOG_LEVEL_DEBUG, "I2S: mic deinit rx_running:%i rx_handle:%p", _rx_running, _rx_handle); + if (!_rx_handle) { return; } + + if (!_tx_configured || !_rx_configured) { // if duplex mode, there is no mic channel - TODO check this + int err = i2s_channel_disable(_rx_handle); + i2s_del_channel(_rx_handle); + _rx_handle = nullptr; + _rx_running = false; + AddLog(LOG_LEVEL_DEBUG, "I2S: RX channel disable: %i", err); + } +} + +#endif // USE_I2S_AUDIO +#endif // defined(ESP32) && ESP_IDF_VERSION_MAJOR >= 5 diff --git a/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s__lib_idf51.ino b/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s__lib_idf51.ino deleted file mode 100644 index 3102b3d92..000000000 --- a/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s__lib_idf51.ino +++ /dev/null @@ -1,428 +0,0 @@ -/* - xdrv_42_0_i2s__lib_idf51.ino - Simplified Audio library - - Copyright (C) 2021 Gerhard Mutz, Theo Arends, Staars, Stephan Hadinger - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -#if defined(ESP32) && ESP_IDF_VERSION_MAJOR >= 5 -#ifdef USE_I2S_AUDIO - -#include "driver/i2s_std.h" -#include "driver/i2s_pdm.h" -#include "driver/gpio.h" - -#include "AudioFileSourcePROGMEM.h" -#include "AudioFileSourceID3.h" -#include "AudioGeneratorMP3.h" - -#include -#include "AudioFileSourceFS.h" -#include "AudioGeneratorTalkie.h" -#include "AudioFileSourceICYStream.h" -#include "AudioFileSourceBuffer.h" -#include "AudioGeneratorAAC.h" - -#include -#include - -/*********************************************************************************************\ - * Driver Settings in memory -\*********************************************************************************************/ - -// I2S communication mode -enum : int8_t { - I2S_MODE_STD = 0, // I2S mode standard - I2S_MODE_PDM = 1, // I2S mode PDM - I2S_MODE_TDM = 2, // I2S mode TDM - I2S_MODE_DAC = 3, // Using internal DAC - only available on ESP32 -}; - -// I2S slot mask (left, right, both) -enum : int8_t { - I2S_SLOT_LEFT = 1, // left - I2S_SLOT_RIGHT = 2, // right - I2S_SLOT_BOTH = 3, // both -}; - -// I2S slot configuration -enum : int8_t { - I2S_SLOT_MSB = 0, // MSB - I2S_SLOT_PCM = 1, // PCM - I2S_SLOT_PHILIPS = 2, // Philips -}; - -typedef struct{ - struct{ - uint8_t version = 0; // 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 - bool tx = 0; // B02 - depends on GPIO setting - bool rx = 0; // B03 - depends on GPIO setting - uint32_t spare01; // B04-07 - } sys; - struct { - uint8_t mode = I2S_MODE_STD; // B00 - I2S mode standard, PDM, TDM, DAC - bool apll = 1; // B01 - will be ignored on unsupported SOC's - uint8_t channels = 2; // B02 - 1 = mono, 2 = stereo - uint8_t codec = 0; // B03 - S3 box only, unused for now - uint8_t slot_config = I2S_SLOT_MSB;// B04 - slot configuration MSB = 0, PCM = 1, PHILIPS = 2 - uint8_t volume = 10; // B05 - bool mclk_inv = 0; // B06 - invert mclk - bool bclk_inv = 0; // B07 - invert bclk - bool ws_inv = 0; // B08 - invert ws - uint8_t spare[7]; // B09-0F - } tx; - struct { - uint32_t sample_rate = 16000; // B00-03 - uint8_t gain = 30; // B04 - uint8_t mode = I2S_MODE_STD; // B05 - I2S mode standard, PDM, TDM, DAC - uint8_t slot_mask = I2S_SLOT_LEFT;// B06 - slot mask - uint8_t slot_mode = 0; // B07 - mono/stereo - 1 is added for both - uint8_t codec = 0; // B08 - unused for now - uint8_t mp3_encoder = 1; // B09 - will be ignored without PS-RAM - uint8_t spare[6]; // B0A-0F - } rx; -} tI2SSettings; - -typedef union { - uint8_t data; - struct { - uint8_t master : 1; - uint8_t enabled : 1; - uint8_t swap_mic : 1; - uint8_t mode : 2; - }; -} BRIDGE_MODE; - -class TasmotaAudioOutputI2S; - -struct AUDIO_I2S_t { - tI2SSettings *Settings; - - i2s_chan_handle_t rx_handle = nullptr; - - AudioGeneratorMP3 *mp3 = nullptr; - AudioFileSourceFS *file = nullptr; - - TasmotaAudioOutputI2S *out = nullptr; - - AudioFileSourceID3 *id3 = nullptr; - AudioGeneratorMP3 *decoder = NULL; - void *mp3ram = NULL; - - TaskHandle_t mp3_task_handle; - TaskHandle_t mic_task_handle; - - char mic_path[32]; - uint8_t mic_stop; - int8_t mic_error; - bool use_stream = false; - - -// SHINE - uint32_t recdur; - uint8_t stream_active; - uint8_t stream_enable; - WiFiClient client; - ESP8266WebServer *MP3Server; - -// I2S_BRIDGE - BRIDGE_MODE bridge_mode; - WiFiUDP i2s_bridge_udp; - WiFiUDP i2s_bridgec_udp; - IPAddress i2s_bridge_ip; - TaskHandle_t i2s_bridge_h; - int8_t ptt_pin = -1; - -} audio_i2s; - -/*********************************************************************************************\ - * Class for outputting sound as endpoint for ESP8266Audio library -\*********************************************************************************************/ - -class TasmotaAudioOutputI2S : public AudioOutput -{ -public: - - // Constructor takes no parameter, everything is configured from template and config file - TasmotaAudioOutputI2S() { - loadSettings(); - } - - ~TasmotaAudioOutputI2S() { - this->stop(); - } - - // ------------------------------------------------------------------------------------------ - // Setters for configuration parameters - // - // TODO: not sure we still need them since all this should be set at initialiation - // ------------------------------------------------------------------------------------------ - virtual bool SetBitsPerSample(int bits) { - AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: SetBitsPerSample: %i"), bits); - if ( (bits != 16) && (bits != 8) ) { return false; } - this->bps = bits; - return true; - } - - virtual bool SetChannels(int channels) { - AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: SetChannels: %i"), channels); - if ((channels < 1) || (channels > 2)) { return false; } - if (channels == (int)this->channels) { return true; } - this->channels = channels; - return true; - } - - virtual bool SetRate(int hz) { - AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: SetRate: %i was %i on=%i"), hz, this->hertz, _i2s_on); - if (hz == (int) this->hertz) { return true; } - this->hertz = hz; - if (_i2s_on) { - int result = updateClockConfig(); - } - return true; - } - - virtual bool SetGain(float f) { - AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: SetGain: %_f"), &f); - return AudioOutput::SetGain(f); - } - - // ------------------------------------------------------------------------------------------ - // Getters - inline uint8_t getTxMode(void) const { return _tx_mode; } - inline uint8_t getTxChannels(void) const { return _tx_channels; } - inline bool getTxEnabled(void) const { return _tx_enabled; } - - // ------------------------------------------------------------------------------------------ - // Setters - inline void setTxMode(uint8_t mode) { _tx_mode = mode; } - inline void setTxChannels(uint8_t channels) { _tx_channels = channels; } - inline void setTxEnabled(bool enabled) { _tx_enabled = enabled; } - - // ------------------------------------------------------------------------------------------ - // AudioOutput has inconsistent case for methods, provide consistent setters for super class - inline void setRate(int hz) { SetRate(hz); } - inline void setBitsPerSample(int bits) { SetBitsPerSample(bits); } - inline void setChannels(int chan) { SetChannels(chan); } - inline void setGain(float f) { SetGain(f); } - - bool begin(void); - bool stop(void); - bool ConsumeSample(int16_t sample[2]); - bool startI2SChannel(void); - int updateClockConfig(void); - - // The following is now the preferred function - // and allows to send multiple samples at once - // - // Max 128 samples, it is clipped otherwise - // Returns: the number of samples actually consumed - // or -1 if an error occured - // - // The call is non blocking and does not wait - int32_t consumeSamples(int16_t *samples, size_t count); - -protected: - void loadSettings(void); // load all settings from Settings file and template - the goal is to have zero touch - -protected: - - bool _i2s_on = false; // is I2S audio active - // local copy of useful settings for audio - // TX - uint8_t _tx_mode = I2S_MODE_STD; // EXTERNAL_I2S = 0, INTERNAL_DAC = 1, INTERNAL_PDM = 2 - bool _tx_enabled = false; // true = enabled, false = disabled - uint8_t _tx_channels = 2; // true = mono, false = stereo - i2s_chan_handle_t _tx_chan; // I2S channel handle, automatically computed - - // 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 - gpio_num_t _gpio_ws = GPIO_NUM_NC; // GPIO for word select - gpio_num_t _gpio_dout = GPIO_NUM_NC; // GPIO for data out - gpio_num_t _gpio_din = GPIO_NUM_NC; // GPIO for data in - bool _gpio_mclk_inv = false; // invert master clock - bool _gpio_bclk_inv = false; // invert bit clock - bool _gpio_ws_inv = false; // invert word select - -}; - - -// ------------------------------------------------------------------------------------------ -// Methods -// ------------------------------------------------------------------------------------------ -void TasmotaAudioOutputI2S::loadSettings(void) { - hertz = 16000; - _i2s_on = false; - bps = I2S_DATA_BIT_WIDTH_16BIT; - _tx_channels = audio_i2s.Settings->tx.channels; - if (_tx_channels == 0) { _tx_channels = 2; } // if zero channel default to stereo - if (_tx_channels > 2) { _tx_channels = 2; } // if > 2 channels default to stereo - channels = (_tx_channels == 1) ? I2S_SLOT_MODE_MONO : I2S_SLOT_MODE_STEREO; - _tx_mode = I2S_MODE_STD; - _tx_enabled = false; - - _gpio_mclk = (gpio_num_t) Pin(GPIO_I2S_MCLK); - _gpio_bclk = (gpio_num_t) Pin(GPIO_I2S_BCLK); - _gpio_ws = (gpio_num_t) Pin(GPIO_I2S_WS); - _gpio_dout = (gpio_num_t) Pin(GPIO_I2S_DOUT); - _gpio_din = (gpio_num_t) Pin(GPIO_I2S_DIN); - _gpio_mclk_inv = audio_i2s.Settings->tx.mclk_inv; - _gpio_bclk_inv = audio_i2s.Settings->tx.bclk_inv; - _gpio_ws_inv = audio_i2s.Settings->tx.ws_inv; -} - -bool TasmotaAudioOutputI2S::begin() { - if (_tx_enabled) { return true; } - if (!_i2s_on) { - if (audio_i2s.Settings->sys.duplex == 0 && audio_i2s.Settings->sys.rx == 1) { - this->startI2SChannel(); - } - } - int result = i2s_channel_enable(_tx_chan); - if (result != 0){ - AddLog(LOG_LEVEL_INFO, "I2S: Could not enable i2s_channel: %i", result); - return false; - } - _tx_enabled = true; - return true; -} - -bool TasmotaAudioOutputI2S::stop() { - i2s_channel_disable(_tx_chan); - if (audio_i2s.Settings->sys.duplex == 0 && audio_i2s.Settings->sys.rx == 1) { - i2s_del_channel(_tx_chan); - _i2s_on = false; - AddLog(LOG_LEVEL_INFO, "I2S: stop: I2S channel disabled"); - } - _tx_enabled = false; - return true; -} - -bool TasmotaAudioOutputI2S::ConsumeSample(int16_t sample[2]) { - return consumeSamples(sample, 1); -} - -int32_t TasmotaAudioOutputI2S::consumeSamples(int16_t *samples, size_t count) { - if (!_tx_enabled) { return -1; } - if (count == 0) { return 0; } - if (count > 128) { count = 128; } - - int16_t ms[count*2]; - for (int32_t i = 0; i < count; i++) { - int16_t left = samples[i*2 + LEFTCHANNEL]; - int16_t right = samples[i*2 + RIGHTCHANNEL]; - - if (this->_tx_channels == 1) { // if mono, average the two samples - // Average the two samples and overwrite - int32_t ttl = left + right; - left = right = (ttl>>1) & 0xffff; - } - - if (bps == 8) { - left = (((int16_t)(left & 0xff)) - 128) << 8; - right = (((int16_t)(right & 0xff)) - 128) << 8; - } - - if (_tx_mode == I2S_MODE_DAC) { - left = Amplify(left) + 0x8000; - right = Amplify(right) + 0x8000; - } else { - left = Amplify(left); - right = Amplify(right); - } - - ms[i*2 + LEFTCHANNEL] = left; - ms[i*2 + RIGHTCHANNEL] = right; - } - - size_t i2s_bytes_written; - esp_err_t err = i2s_channel_write(_tx_chan, ms, sizeof(ms), &i2s_bytes_written, 0); - if (err && err != ESP_ERR_TIMEOUT) { - AddLog(LOG_LEVEL_INFO, "I2S: Could not write samples (count=%i): %i", count, err); - return -1; - } - return i2s_bytes_written; -} - -// Initialize I2S channel -// return `true` if successful, `false` otherwise -bool TasmotaAudioOutputI2S::startI2SChannel(void) { - gpio_num_t _DIN = I2S_GPIO_UNUSED; // no input pin by default - - // default dma_desc_num = 6 (DMA buffers), dma_frame_num = 240 (frames per buffer) - i2s_chan_config_t tx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER); - - if (audio_i2s.Settings->sys.duplex == 1) { - _DIN = (gpio_num_t)_gpio_din; - i2s_new_channel(&tx_chan_cfg, &_tx_chan, &audio_i2s.rx_handle); - } else{ - i2s_new_channel(&tx_chan_cfg, &_tx_chan, NULL); - } - - // by default we configure for MSB 2 slots `I2S_STD_MSB_SLOT_DEFAULT_CONFIG` - i2s_std_config_t tx_std_cfg = { - .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(hertz), - .slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t)bps, (i2s_slot_mode_t)channels), - .gpio_cfg = { - .mclk = _gpio_mclk, - .bclk = _gpio_bclk, - .ws = _gpio_ws, - .dout = _gpio_dout, - .din = _DIN, - .invert_flags = { - .mclk_inv = _gpio_mclk_inv, - .bclk_inv = _gpio_bclk_inv, - .ws_inv = _gpio_ws_inv, - } - } - }; - - // change configuration if we are using PCM or PHILIPS - if (audio_i2s.Settings->tx.slot_config == I2S_SLOT_PCM) { // PCM - tx_std_cfg.slot_cfg = I2S_STD_PCM_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t)bps, (i2s_slot_mode_t)channels); - } else if (audio_i2s.Settings->tx.slot_config == I2S_SLOT_PHILIPS) { // PHILIPS - tx_std_cfg.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t)bps, (i2s_slot_mode_t)channels); - } - - _i2s_on = (i2s_channel_init_std_mode(_tx_chan, &tx_std_cfg) == 0); - AddLog(LOG_LEVEL_DEBUG, "I2S: TX channel with %i bits width on %i channels initialized i2s_on=%i", bps, channels, _i2s_on); - - if (audio_i2s.Settings->sys.duplex == 1) { - i2s_channel_init_std_mode(audio_i2s.rx_handle, &tx_std_cfg); - AddLog(LOG_LEVEL_DEBUG, "I2S: RX channel added in full duplex mode"); - } - return _i2s_on; -} - -int TasmotaAudioOutputI2S::updateClockConfig(void) { - i2s_channel_disable(_tx_chan); - i2s_std_clk_config_t clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(hertz); -#ifdef SOC_I2S_SUPPORTS_APLL - if (audio_i2s.Settings->tx.apll == 1) { - clk_cfg.clk_src = I2S_CLK_SRC_APLL; - } -#endif - int result = i2s_channel_reconfig_std_clock(_tx_chan, &clk_cfg ); - if (_tx_enabled) { i2s_channel_enable(_tx_chan); } - AddLog(LOG_LEVEL_DEBUG, "I2S: Updating clock config"); - return result; -} - -#endif // USE_I2S_AUDIO -#endif // defined(ESP32) && ESP_IDF_VERSION_MAJOR >= 5 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 72f5f3e13..0dba6f17c 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 @@ -26,6 +26,7 @@ #define USE_I2S_SAY_TIME #define USE_I2S_RTTTL #define USE_I2S_WEBRADIO +#define USE_I2S_DEBUG // remove before flight // Macros used in audio sub-functions #undef AUDIO_PWR_ON @@ -54,6 +55,9 @@ void I2sWebRadioStopPlaying(void); const char kI2SAudio_Commands[] PROGMEM = "I2S|" "Gain|Play|Rec|MGain|Stop" +#ifdef USE_I2S_DEBUG + "|Mic" // debug only +#endif // USE_I2S_DEBUG #ifdef USE_I2S_WEBRADIO "|WR" #endif // USE_I2S_WEBRADIO @@ -76,6 +80,9 @@ const char kI2SAudio_Commands[] PROGMEM = "I2S|" void (* const I2SAudio_Command[])(void) PROGMEM = { &CmndI2SGain, &CmndI2SPlay, &CmndI2SMicRec, &CmndI2SMicGain, &CmndI2SStop, +#ifdef USE_I2S_DEBUG + &CmndI2SMic, +#endif // USE_I2S_DEBUG #ifdef USE_I2S_WEBRADIO &CmndI2SWebRadio, #endif // USE_I2S_WEBRADIO @@ -100,94 +107,6 @@ void (* const I2SAudio_Command[])(void) PROGMEM = { * microphone related functions \*********************************************************************************************/ -uint32_t I2sMicInit(void) { - esp_err_t err = ESP_OK; - gpio_num_t clk_gpio; - - i2s_slot_mode_t slot_mode = (audio_i2s.Settings->rx.slot_mode == 0) ? I2S_SLOT_MODE_MONO : I2S_SLOT_MODE_STEREO; - AddLog(LOG_LEVEL_DEBUG, "I2S: mic init rx_channels=%i", slot_mode); - - if (audio_i2s.Settings->sys.duplex == 1 && audio_i2s.rx_handle){ - return 0; // no need to en- or disable when in full duplex mode and already initialized - } - - if (audio_i2s.rx_handle == nullptr){ - i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER); - - err = i2s_new_channel(&chan_cfg, NULL, &audio_i2s.rx_handle); - AddLog(LOG_LEVEL_DEBUG, "I2S: mic init i2s_new_channel err=%i", err); - switch (audio_i2s.Settings->rx.mode){ - case I2S_MODE_PDM: - { - clk_gpio = (gpio_num_t)Pin(GPIO_I2S_WS,1); //legacy setting for Core2, might be wrong - if (clk_gpio == -1){ - clk_gpio = (gpio_num_t)Pin(GPIO_I2S_WS); //fallback to other port, might be wrong - } - i2s_pdm_rx_config_t pdm_rx_cfg = { - .clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG(audio_i2s.Settings->rx.sample_rate), - /* 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, slot_mode), - .gpio_cfg = { - .clk = clk_gpio, - .din = (gpio_num_t)Pin(GPIO_I2S_DIN), - .invert_flags = { - .clk_inv = false, - }, - }, - }; - pdm_rx_cfg.slot_cfg.slot_mask = (i2s_pdm_slot_mask_t)audio_i2s.Settings->rx.slot_mask; - err = i2s_channel_init_pdm_rx_mode(audio_i2s.rx_handle, &pdm_rx_cfg); - AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: RX channel in PDM mode, CLK: %i, DIN: %i, 16 bit width, %i channel(s), err code: %u"),clk_gpio, Pin(GPIO_I2S_DIN), slot_mode, err); - } - break; - case I2S_MODE_STD: - { - i2s_std_config_t rx_std_cfg = { - .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(audio_i2s.Settings->rx.sample_rate), - .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, - }, - }, - }; - rx_std_cfg.slot_cfg.slot_mask = (i2s_std_slot_mask_t)audio_i2s.Settings->rx.slot_mask; - i2s_channel_init_std_mode(audio_i2s.rx_handle, &rx_std_cfg); - AddLog(LOG_LEVEL_DEBUG, PSTR("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", audio_i2s.Settings->rx.mode); - } - } - - err = i2s_channel_enable(audio_i2s.rx_handle); - AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: RX channel enable: %i"),err); - return err; -} - - -void I2sMicDeinit(void) { - esp_err_t err = ESP_OK; - gpio_num_t clk_gpio; - - if (!audio_i2s.rx_handle) { return; } - - if (!audio_i2s.Settings->sys.duplex) { // if duplex mode, there is no mic channel - TODO check this - int err = i2s_channel_disable(audio_i2s.rx_handle); - i2s_del_channel(audio_i2s.rx_handle); - audio_i2s.rx_handle = nullptr; - AddLog(LOG_LEVEL_DEBUG, "I2S: RX channel disable: %i", err); - } -} - // micro to mp3 file or stream void I2sMicTask(void *arg){ int8_t error = 0; @@ -226,14 +145,14 @@ void I2sMicTask(void *arg){ shine_set_config_mpeg_defaults(&config.mpeg); - if (audio_i2s.Settings->rx.slot_mode == 0) { + if (audio_i2s.Settings->rx.channels == 1) { config.mpeg.mode = MONO; } else { config.mpeg.mode = STEREO; } config.mpeg.bitr = 128; config.wave.samplerate = audio_i2s.Settings->rx.sample_rate; - config.wave.channels = (channels)(audio_i2s.Settings->rx.slot_mode + 1); + config.wave.channels = (channels)(audio_i2s.Settings->rx.channels); if (shine_check_config(config.wave.samplerate, config.mpeg.bitr) < 0) { error = 3; @@ -247,7 +166,7 @@ void I2sMicTask(void *arg){ } samples_per_pass = shine_samples_per_pass(s); - bytesize = samples_per_pass * 2 * (audio_i2s.Settings->rx.slot_mode + 1); + bytesize = samples_per_pass * 2 * (audio_i2s.Settings->rx.channels); buffer = (int16_t*)malloc(bytesize); if (!buffer) { @@ -310,7 +229,7 @@ exit: audio_i2s.client.stop(); } - I2sMicDeinit(); + audio_i2s.out->I2sMicDeinit(); audio_i2s.mic_stop = 0; audio_i2s.mic_error = error; AddLog(LOG_LEVEL_INFO, PSTR("mp3task result code: %d"), error); @@ -334,7 +253,7 @@ int32_t I2sRecordShine(char *path) { if (audio_i2s.use_stream) { stack = 8000; } - I2sMicInit(); + audio_i2s.out->I2sMicInit(); err = xTaskCreatePinnedToCore(I2sMicTask, "MIC", stack, NULL, 3, &audio_i2s.mic_task_handle, 1); @@ -347,7 +266,8 @@ int32_t I2sRecordShine(char *path) { // error codes enum { I2S_OK = 0, - I2S_ERR_OUTPUT_NOT_CONFIGURE, + I2S_ERR_OUTPUT_NOT_CONFIGURED, + I2S_ERR_INPUT_NOT_CONFIGURED, I2S_ERR_DECODER_IN_USE, I2S_ERR_FILE_NOT_FOUND, }; @@ -369,7 +289,6 @@ void I2SSettingsLoad(const char * config_filename, bool erase) { AddLog(LOG_LEVEL_ERROR, "I2S: ERROR memory allocation failed"); return; } - if (!config_filename) { return; } // if no filename, use defaults #ifndef USE_UFILESYS @@ -406,53 +325,20 @@ void I2SSettingsSave(const char * config_filename) { * Driver init \*********************************************************************************************/ -// -// I2sCheckCfg -// -// Multiple checks -void I2sCheckCfg(void){ - // din and dout must be configured on port 0 for full duplex - bool useDuplexMode = ((Pin(GPIO_I2S_DIN) != -1) && (Pin(GPIO_I2S_DOUT) != -1)); - // AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: DIN %i , DOUT %i"),Pin(GPIO_I2S_DIN),Pin(GPIO_I2S_DOUT) ); - - if (useDuplexMode){ - if (audio_i2s.Settings->rx.mode == I2S_MODE_PDM || audio_i2s.Settings->tx.mode == I2S_MODE_PDM ){ - useDuplexMode = false; - AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: PDM forbids full duplex mode")); - } - audio_i2s.Settings->sys.duplex = useDuplexMode ? 1 : 0; - if(useDuplexMode){ - AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: will try to use full duplex mode")); - } - } - if (Pin(GPIO_I2S_DIN) != -1 || Pin(GPIO_I2S_DIN, 1) != -1){ // micro could be port 0 or 1 - audio_i2s.Settings->sys.rx = 1; - AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: RX config = mode: %i, channels: %i, gain: %i, sample rate: %i"), audio_i2s.Settings->rx.mode, (uint8_t)(audio_i2s.Settings->rx.slot_mode + 1), audio_i2s.Settings->rx.gain, audio_i2s.Settings->rx.sample_rate); - } - else{ - audio_i2s.Settings->sys.rx = 0; - audio_i2s.Settings->rx.mp3_encoder = 0; // do not allocate buffer - } - if (Pin(GPIO_I2S_DOUT) != -1){ // output is only supported on port 0 - audio_i2s.Settings->sys.tx = 1; - } - else{ - audio_i2s.Settings->sys.tx = 0; - } - - AddLog(LOG_LEVEL_INFO, PSTR("I2S: init pins bclk=%d, ws=%d, dout=%d, mclk=%d, din=%d [tx=%i, rx=%i, duplex=%i]"), - Pin(GPIO_I2S_BCLK) , Pin(GPIO_I2S_WS), Pin(GPIO_I2S_DOUT), Pin(GPIO_I2S_MCLK), Pin(GPIO_I2S_DIN), - audio_i2s.Settings->sys.tx, audio_i2s.Settings->sys.tx, audio_i2s.Settings->sys.duplex); - -} - // // I2sInit // // Initialize I2S driver for input and output void I2sInit(void) { + int32_t gpio_din_0 = Pin(GPIO_I2S_DIN, 0); + int32_t gpio_din_1 = Pin(GPIO_I2S_DIN, 1); + int32_t gpio_dout_0 = Pin(GPIO_I2S_DOUT, 0); + int32_t gpio_dout_1 = Pin(GPIO_I2S_DOUT, 1); + int32_t gpio_ws_0 = Pin(GPIO_I2S_WS, 0); + // we need at least one pin configured - if(Pin(GPIO_I2S_DIN) + Pin(GPIO_I2S_DIN,1) + Pin(GPIO_I2S_DOUT) + Pin(GPIO_I2S_DOUT,1) == -4){ + // Note: in case of ESP32 DAC output we may have only WS_0 configured. DAC is only supported on port 0 + if ((gpio_din_0 < 0) && (gpio_din_1 < 0) && (gpio_dout_0 < 0) && (gpio_dout_1 < 0) && (gpio_ws_0 < 0)) { AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: no pin configured")); return; } @@ -460,37 +346,204 @@ void I2sInit(void) { I2SSettingsLoad("/.drvset042", false); // load configuration (no-erase) if (!audio_i2s.Settings) { return; } // fatal error, could not allocate memory for configuration - // check configuration is valid - I2sCheckCfg(); - - // TODO - int result = 0; - if (audio_i2s.Settings->sys.rx == 1 && audio_i2s.Settings->sys.duplex == 0){ - I2sMicDeinit(); + // detect if we need full-duplex on port 0 + bool duplex = false; + if ((gpio_din_0 >= 0) && (gpio_dout_0 >= 0)) { + // conditions are potentially favorable for duplex + if (audio_i2s.Settings->rx.mode == I2S_MODE_PDM || audio_i2s.Settings->tx.mode == I2S_MODE_PDM ){ + AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: PDM forbids full duplex mode, ignoring 'I2S DIN 1'")); + gpio_din_0 = -1; // hence deconfigure DIN_0 which can't be used + } else { + AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: will try to use full duplex mode")); + duplex = true; + } } - if(audio_i2s.Settings->sys.tx == 1){ - audio_i2s.out = new TasmotaAudioOutputI2S; - int err = audio_i2s.out->startI2SChannel() ? 0 : 1; - result += err; + // AddLog(LOG_LEVEL_INFO, PSTR("I2S: init pins bclk=%d, ws=%d, dout=%d, mclk=%d, din=%d"), + // Pin(GPIO_I2S_BCLK, 0) , Pin(GPIO_I2S_WS, 0), Pin(GPIO_I2S_DOUT, 0), Pin(GPIO_I2S_MCLK, 0), Pin(GPIO_I2S_DIN, 0)); + + audio_i2s.Settings->sys.duplex = false; + audio_i2s.Settings->sys.tx = false; + audio_i2s.Settings->sys.rx = false; + audio_i2s.Settings->sys.exclusive = false; + bool exclusive = false; // signals that in/out have a shared GPIO and need to un/install driver before use + + for (uint32_t port = 0; port < SOC_I2S_NUM; port++) { + int32_t bclk = Pin(GPIO_I2S_BCLK, port); + int32_t ws = Pin(GPIO_I2S_WS, port); + int32_t dout = Pin(GPIO_I2S_DOUT, port); + int32_t mclk = Pin(GPIO_I2S_MCLK, port); + int32_t din = Pin(GPIO_I2S_DIN, port); + bool tx = false; // is Tx enabled for this port + bool rx = false; // is Rx enabled for this port + + AddLog(LOG_LEVEL_DEBUG, "I2S: I2S%i bclk=%i, ws=%i, dout=%i, mclk=%i, din=%i", port, bclk, ws, dout, mclk, din); + + // if neither input, nor output, nor DAC/ADC skip (WS could is only needed for DAC but supports only port 0) + if (din < 0 && dout < 0 && (ws < 0 || port !=0)) { continue; } + + const char *err_msg = nullptr; // to save code, we indicate an error with a message configured + bool duplex = (din >= 0) && (dout >= 0); + bool dac_mode = false; + if (din >= 0 || dout >= 0) { + // we have regular I2S configuration + // do multiple checks + // 1. check that WS is configured + if (ws < 0) { + // WS may be shared between both ports, so if it is configured on port 0, we accept it on port 1 + int32_t ws0 = Pin(GPIO_I2S_WS, 0); + if (ws0 >= 0) { + ws = ws0; + AddLog(LOG_LEVEL_DEBUG, "I2S: I2S%i WS is shared, using WS from port 0 (%i)", port, ws); + exclusive = true; + } + if (ws < 0) { + err_msg = "no WS pin configured"; + } + } + // 2. check that DAC mode is not enabled for output(incompatible with DIN/DOUT) + else if (dout >= 0 && audio_i2s.Settings->tx.mode == I2S_MODE_DAC) { + err_msg = "DAC mode is not compatible with DOUT"; + } + // 3. check that ADC mode is not enabled for output (incompatible with DIN/DOUT) + else if (din >= 0 && audio_i2s.Settings->rx.mode == I2S_MODE_DAC) { + err_msg = "ADC mode is not compatible with DIN"; + } + // 4. check that output is not already configured + else if (dout >= 0 && audio_i2s.out) { + err_msg = "output already configured"; + } + // 5. check that input is not already configured + else if (din >= 0 && audio_i2s.in) { + err_msg = "input already configured"; + } + // 6. check that we don't try PDM on port 1 + else if (port != 0 && din >= 0 && audio_i2s.Settings->rx.mode == I2S_MODE_PDM) { + err_msg = "PDM Rx is not supported"; + } + // 7. check that we don't try PDM on port 1 + else if (port != 0 && dout >= 0 && audio_i2s.Settings->tx.mode == I2S_MODE_PDM) { + err_msg = "PDM Tx is not supported"; + } + // 8. check that we don't try full-duplex with PDM in either direction + else if (duplex && (audio_i2s.Settings->rx.mode == I2S_MODE_PDM || audio_i2s.Settings->tx.mode == I2S_MODE_PDM )) { + err_msg = "PDM forbids full duplex mode"; + duplex = false; + din = -1; // deconfigure DIN_0 which can't be used + } + } else { + dac_mode = true; + // no DIN/DOUT, try DAC mode + // 1. Check that tx.mode is I2S_MODE_DAC + if (audio_i2s.Settings->tx.mode != I2S_MODE_DAC) { + err_msg = "DAC mode is not enabled"; + } + } + + // is there any error? + if (err_msg) { + AddLog(LOG_LEVEL_DEBUG, "I2S: Error: %s for I2S%i, skipping", err_msg, port); + continue; // skip this port + } + + tx = (dout >= 0) || dac_mode; + rx = (din >= 0); + + if (duplex) { + AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: will try to use full duplex mode")); + } + + TasmotaI2S * i2s = new TasmotaI2S; + i2s->setPinout(bclk, ws, dout, mclk, din, + audio_i2s.Settings->sys.mclk_inv[0], audio_i2s.Settings->sys.bclk_inv[0], + audio_i2s.Settings->sys.ws_inv[0], audio_i2s.Settings->tx.apll); + i2s->setSlotConfig((i2s_port_t)port, audio_i2s.Settings->tx.slot_config, audio_i2s.Settings->rx.slot_config, + audio_i2s.Settings->tx.slot_mask, audio_i2s.Settings->rx.slot_mask); + if (tx) { + i2s->setTxMode(audio_i2s.Settings->tx.mode); + i2s->setTxChannels(audio_i2s.Settings->tx.channels); + i2s->setRate(audio_i2s.Settings->tx.sample_rate); + } + if (rx) { + i2s->setRxMode(audio_i2s.Settings->rx.mode); + i2s->setRxFreq(audio_i2s.Settings->rx.sample_rate); + i2s->setRxChannels(audio_i2s.Settings->rx.channels); + i2s->setRate(audio_i2s.Settings->rx.sample_rate); + } + + if (i2s->startI2SChannel(tx, rx)) { + // succesful, register handlers + if (tx) { + audio_i2s.out = i2s; + audio_i2s.Settings->sys.tx = true; + } + if (rx) { + audio_i2s.in = i2s; + audio_i2s.Settings->sys.rx = true; + } + if (duplex) { + audio_i2s.Settings->sys.duplex = true; + } + } } + // do we have exclusive mode? + audio_i2s.Settings->sys.exclusive = exclusive; + if (audio_i2s.out) { audio_i2s.out->setExclusive(exclusive); } + if (audio_i2s.in) { audio_i2s.in->setExclusive(exclusive); } + if(audio_i2s.out != nullptr){ - audio_i2s.out->SetGain(((float)audio_i2s.Settings->tx.volume / 100.0) * 4.0); + audio_i2s.out->SetGain(((float)audio_i2s.Settings->tx.gain / 100.0) * 4.0); audio_i2s.out->begin(); audio_i2s.out->stop(); } audio_i2s.mp3ram = nullptr; - if(audio_i2s.Settings->rx.mp3_encoder == 1){ + if (audio_i2s.Settings->rx.mp3_preallocate == 1){ + // if (UsePSRAM()) { AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: will allocate buffer for mp3 encoder")); - if (UsePSRAM()) { - audio_i2s.mp3ram = heap_caps_malloc(preallocateCodecSize, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); - } - else{ - audio_i2s.Settings->rx.mp3_encoder = 0; // no PS-RAM -> no MP3 encoding - } + audio_i2s.mp3ram = special_malloc(preallocateCodecSize); + // } + // else{ + // audio_i2s.Settings->rx.mp3_preallocate = 0; // no PS-RAM -> no MP3 encoding + // } } + AddLog(LOG_LEVEL_DEBUG, "I2S: I2sInit done"); +} + +/*********************************************************************************************\ + * General functions +\*********************************************************************************************/ + +// +// I2SPrepareTx() -> int +// +// Prepare I2S for output, handle exclusive access if necessary +// +// Returns `I2S_OK` if ok to send to output or error code +int32_t I2SPrepareTx(void) { + AddLog(LOG_LEVEL_DEBUG, "I2S: I2SPrepareTx out=%p", audio_i2s.out); + if (!audio_i2s.out) { return I2S_ERR_OUTPUT_NOT_CONFIGURED; } + + if (audio_i2s.Settings->sys.exclusive) { + // TODO - deconfigure input driver + } + return I2S_OK; +} + +// +// I2SPrepareRx() -> int +// +// Prepare I2S for input, handle exclusive access if necessary +// +// Returns `I2S_OK` if ok to record input or error code +int32_t I2SPrepareRx(void) { + if (!audio_i2s.in) return I2S_ERR_OUTPUT_NOT_CONFIGURED; + + if (audio_i2s.Settings->sys.exclusive) { + // TODO - deconfigure input driver + } + return I2S_OK; } /*********************************************************************************************\ @@ -558,7 +611,8 @@ void I2sStopPlaying() { // // Returns I2S_error_t int32_t I2SPlayMp3(const char *path) { - if (!audio_i2s.out) return I2S_ERR_OUTPUT_NOT_CONFIGURE; + int32_t i2s_err = I2S_OK; + if ((i2s_err = I2SPrepareTx()) != I2S_OK) { return i2s_err; } if (audio_i2s.decoder || audio_i2s.mp3) return I2S_ERR_DECODER_IN_USE; // check if the filename starts with '/', if not add it @@ -598,7 +652,7 @@ void mp3_delete(void) { void Say(char *text) { - if (!audio_i2s.out) return; + if (I2SPrepareTx()) { return; } I2SAudioPower(true); @@ -615,12 +669,44 @@ void Say(char *text) { * Commands \*********************************************************************************************/ +void CmndI2SMic(void) { + if (I2SPrepareRx()) { + ResponseCmndChar("I2S Mic not configured"); + return; + } + + esp_err_t err = ESP_OK; + if (audio_i2s.decoder || audio_i2s.mp3) return; + audio_i2s.in->I2sMicInit(); + 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); + } + + // err = xTaskCreatePinnedToCore(I2sMicTask, "MIC", stack, NULL, 3, &audio_i2s.mic_task_handle, 1); + + ResponseCmndDone(); +} + + void CmndI2SStop(void) { + if (!I2SPrepareTx()) { + ResponseCmndChar("I2S output not configured"); + return; + } I2sStopPlaying(); ResponseCmndDone(); } void CmndI2SPlay(void) { + if (I2SPrepareTx()) { + ResponseCmndChar("I2S output not configured"); + return; + } if (XdrvMailbox.data_len > 0) { int32_t err = I2SPlayMp3(XdrvMailbox.data); // display return message @@ -628,7 +714,7 @@ void CmndI2SPlay(void) { case I2S_OK: ResponseCmndDone(); break; - case I2S_ERR_OUTPUT_NOT_CONFIGURE: + case I2S_ERR_OUTPUT_NOT_CONFIGURED: ResponseCmndChar("I2S output not configured"); break; case I2S_ERR_DECODER_IN_USE: @@ -649,14 +735,18 @@ void CmndI2SPlay(void) { void CmndI2SGain(void) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 100)) { if (audio_i2s.out) { - audio_i2s.Settings->tx.volume = XdrvMailbox.payload; - audio_i2s.out->SetGain(((float)(audio_i2s.Settings->tx.volume-2)/100.0)*4.0); + audio_i2s.Settings->tx.gain = XdrvMailbox.payload; + audio_i2s.out->SetGain(((float)(audio_i2s.Settings->tx.gain-2)/100.0)*4.0); } } - ResponseCmndNumber(audio_i2s.Settings->tx.volume); + ResponseCmndNumber(audio_i2s.Settings->tx.gain); } void CmndI2SSay(void) { + if (I2SPrepareTx()) { + ResponseCmndChar("I2S output not configured"); + return; + } if (XdrvMailbox.data_len > 0) { Say(XdrvMailbox.data); } @@ -664,6 +754,10 @@ void CmndI2SSay(void) { } void CmndI2SI2SRtttl(void) { + if (I2SPrepareTx()) { + ResponseCmndChar("I2S output not configured"); + return; + } if (XdrvMailbox.data_len > 0) { Rtttl(XdrvMailbox.data); } @@ -671,37 +765,38 @@ void CmndI2SI2SRtttl(void) { } void CmndI2SMicRec(void) { -if (audio_i2s.Settings->rx.mp3_encoder == 1) { - if (XdrvMailbox.data_len > 0) { - if (!strncmp(XdrvMailbox.data, "-?", 2)) { - Response_P("{\"I2SREC-duration\":%d}", audio_i2s.recdur); - } else { - I2sRecordShine(XdrvMailbox.data); - ResponseCmndChar(XdrvMailbox.data); - } - } else { - if (audio_i2s.mic_task_handle) { - // stop task - audio_i2s.mic_stop = 1; - while (audio_i2s.mic_stop) { - delay(1); + if (audio_i2s.Settings->rx.mp3_preallocate == 1) { + if (XdrvMailbox.data_len > 0) { + if (!strncmp(XdrvMailbox.data, "-?", 2)) { + Response_P("{\"I2SREC-duration\":%d}", audio_i2s.recdur); + } else { + I2sRecordShine(XdrvMailbox.data); + ResponseCmndChar(XdrvMailbox.data); + } + } else { + if (audio_i2s.mic_task_handle) { + // stop task + audio_i2s.mic_stop = 1; + while (audio_i2s.mic_stop) { + delay(1); + } + ResponseCmndChar_P(PSTR("Stopped")); } - ResponseCmndChar_P(PSTR("Stopped")); } } -} -else{ - if(audio_i2s.Settings->sys.rx == 1){ - ResponseCmndChar_P(PSTR("need PSRAM for MP3 recording")); - } else{ - ResponseCmndChar_P(PSTR("no mic configured")); + if (audio_i2s.in){ + ResponseCmndChar_P(PSTR("need PSRAM for MP3 recording")); + } + else{ + ResponseCmndChar_P(PSTR("no mic configured")); + } } } -} // mic gain in factor not percent void CmndI2SMicGain(void) { + // TODO - does nothing for now if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 256)) { audio_i2s.Settings->rx.gain = XdrvMailbox.payload; } 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 3e25ac097..d77a71d9c 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_audio.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_audio.ino @@ -40,21 +40,22 @@ extern "C" { // AudioOutputI2S.init() -> instance void* be_audio_output_i2s_init(void) { - return audio_i2s.out; // return the singleton of TasmotaAudioOutputI2S which is already initialized + return audio_i2s.out; // return the singleton of TasmotaI2S which is already initialized } // AudioOutputI2S.deinit()-> void - void be_audio_output_i2s_deinit(TasmotaAudioOutputI2S * out) { + void be_audio_output_i2s_deinit(TasmotaI2S * out) { out->stop(); } // AudioOutputI2S.begin() -> bool - int be_audio_output_i2s_begin(TasmotaAudioOutputI2S* out) { + int be_audio_output_i2s_begin(TasmotaI2S* out) { + if (I2SPrepareTx()) { return false; } return out->begin(); } // AudioOutputI2S.stop() -> bool - int be_audio_output_i2s_stop(TasmotaAudioOutputI2S* out) { + int be_audio_output_i2s_stop(TasmotaI2S* out) { return out->stop(); } @@ -125,7 +126,7 @@ extern "C" { // AudioOutputI2S() // int i2s_output_i2s_init(bvm *vm) { - TasmotaAudioOutputI2S * audio = new TasmotaAudioOutputI2S(); + TasmotaI2S * audio = new TasmotaI2S(); be_pushcomptr(vm, (void*) audio); be_setmember(vm, 1, ".p"); be_return_nil(vm); @@ -134,7 +135,7 @@ extern "C" { int i2s_output_i2s_deinit(bvm *vm) { int argc = be_top(vm); be_getmember(vm, 1, ".p"); - TasmotaAudioOutputI2S * audio = (TasmotaAudioOutputI2S *) be_tocomptr(vm, -1); + TasmotaI2S * audio = (TasmotaI2S *) be_tocomptr(vm, -1); if (audio) { delete audio; // clear @@ -177,6 +178,7 @@ extern "C" { int i2s_generator_wav_begin(bvm *vm) { int argc = be_top(vm); if (argc > 2) { + if (I2SPrepareTx()) { be_raisef(vm, "internal_error", "I2SPrepareTx() failed"); be_return_nil(vm); } AudioGeneratorWAV * wav = i2s_generator_wav_get(vm); be_getmember(vm, 2, ".p"); AudioFileSource * source = (AudioFileSource*) be_tocomptr(vm, -1); @@ -244,6 +246,7 @@ extern "C" { int i2s_generator_mp3_begin(bvm *vm) { int argc = be_top(vm); if (argc > 2) { + if (I2SPrepareTx()) { be_raisef(vm, "internal_error", "I2SPrepareTx() failed"); be_return_nil(vm); } AudioGeneratorMP3 * mp3 = i2s_generator_mp3_get(vm); be_getmember(vm, 2, ".p"); AudioFileSource * source = (AudioFileSource*) be_tocomptr(vm, -1); @@ -313,12 +316,99 @@ extern "C" { // ---------------------------------------------------------------------- // AudioInputI2S.init() -> instance void* be_audio_input_i2s_init(void) { - return audio_i2s.out; // return the singleton of TasmotaAudioOutputI2S which is already initialized + return audio_i2s.in; // return the singleton of TasmotaI2S which is already initialized } // AudioInputI2S.deinit()-> void - void be_audio_input_i2s_deinit(TasmotaAudioOutputI2S * out) { - out->stop(); + void be_audio_input_i2s_deinit(TasmotaI2S * in) { + in->stop(); + } + + // 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(); + return in->getRxRunning(); + } + + // AudioInputI2S.stop() -> bool + int be_audio_input_i2s_stop(TasmotaI2S* in) { + in->I2sMicDeinit(); + return true; + } + + + // AudioInputI2S.get_rate() -> int + int be_audio_input_i2s_get_rate(TasmotaI2S* in) { + return in->getRxRate(); + } + + // AudioInputI2S.get_bits_per_sample() -> int + int be_audio_input_i2s_get_bits_per_sample(TasmotaI2S* in) { + return in->getRxBitsPerSample(); + } + + // AudioInputI2S.get_channels() -> int + int be_audio_input_i2s_get_channels(TasmotaI2S* in) { + return in->getRxChannels(); + } + + // AudioInputI2S.get_gain() -> real + float be_audio_input_i2s_get_gain(TasmotaI2S* in) { + return in->getRxGain(); + } + + // AudioInputI2S.set_gain(gain:real) -> bool + int be_audio_input_set_gain(TasmotaI2S* in, float gain) { + return true; // TODO + // return in->SetGain(gain); + } + + + // AudioInputI2S.read_bytes() -> bytes() + // + // Reads bytes in the input buffer + // Pre-condition: microphone but be recording (call begin() first) + // + // Returns `nil` if not configured or buffer empty + // 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]; + + 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); } + + // we received some data + if (argc >= 2 && be_isbytes(vm, 2)) { + // we have already a bytes() buffer + be_pushvalue(vm, 2); // push on top + // resize to btr + be_getmember(vm, -1, "resize"); + be_pushvalue(vm, -2); + be_pushint(vm, bytes_read); + be_call(vm, 2); + be_pop(vm, 3); + } else { + be_pushbytes(vm, nullptr, bytes_read); // allocate a buffer of size btr filled with zeros + } + + // get the address of the buffer + be_getmember(vm, -1, "_buffer"); + be_pushvalue(vm, -2); + be_call(vm, 1); + uint8_t * buf = (uint8_t*) be_tocomptr(vm, -2); + be_pop(vm, 2); + + // copy + memmove(buf, buf_audio, bytes_read); + + be_return(vm); /* return code */ } } diff --git a/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_udp.ino b/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_udp.ino index b48d35417..ab80735a7 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_udp.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_udp.ino @@ -136,7 +136,7 @@ extern "C" { be_setmember(vm, 1, "remote_port"); be_pop(vm, 1); - be_return(vm); /* return code */ + be_return(vm); } else { be_return_nil(vm); }