all output parts for I2S and IDF5.1 (#19440)

This commit is contained in:
Christian Baars 2023-09-02 22:07:20 +02:00 committed by GitHub
parent f4b3574ed4
commit 4f6afbf849
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 650 additions and 1 deletions

View File

@ -16,7 +16,7 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#if ESP_IDF_VERSION_MAJOR < 5
#if (defined(USE_I2S_AUDIO) || defined(USE_TTGO_WATCH) || defined(USE_M5STACK_CORE2) || defined(ESP32S3_BOX))
/*********************************************************************************************\
* I2S support using an external DAC or a speaker connected to GPIO03 using a transistor
@ -765,3 +765,4 @@ bool Xdrv42(uint32_t function) {
}
#endif // USE_I2S_AUDIO
#endif // ESP_IDF_VERSION_MAJOR < 5

View File

@ -0,0 +1,648 @@
/*
xdrv_42_i2s_audio.ino - Audio dac support for Tasmota
Copyright (C) 2021 Gerhard Mutz and Theo Arends
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 <http://www.gnu.org/licenses/>.
*/
#if ESP_IDF_VERSION_MAJOR >= 5
#if defined(USE_I2S_AUDIO)
#define XDRV_42 42
#include "driver/i2s_std.h"
#include "driver/gpio.h"
#include "AudioFileSourcePROGMEM.h"
#include "AudioFileSourceID3.h"
#include "AudioGeneratorMP3.h"
#include <ESP8266SAM.h>
#include "AudioFileSourceFS.h"
#include "AudioGeneratorTalkie.h"
#include "AudioFileSourceICYStream.h"
#include "AudioFileSourceBuffer.h"
#include "AudioGeneratorAAC.h"
#undef AUDIO_PWR_ON
#undef AUDIO_PWR_OFF
#define AUDIO_PWR_ON
#define AUDIO_PWR_OFF
#define USE_I2S_RTTTL
#define USE_I2S_SAY_TIME
class AudioOutputI2S : public AudioOutput
{
public:
AudioOutputI2S(){
hertz = 44100;
i2sOn = false;
bps = I2S_DATA_BIT_WIDTH_16BIT;
channels = I2S_SLOT_MODE_STEREO;
mono = false;
output_mode = EXTERNAL_I2S;
tx_is_enabled = false;
}
~AudioOutputI2S(){
if(i2sOn){
this->stop();
i2s_del_channel(tx_chan);
}
}
bool SetBitsPerSample(int bits)
{
if ( (bits != 16) && (bits != 8) ) return false;
this->bps = bits;
return true;
}
bool SetChannels(int channels)
{
if ( (channels < 1) || (channels > 2) ) return false;
if (channels == (int)this->channels) return true;
this->channels = channels;
return true;
}
bool SetRate(int hz){
if (hz == (int)this->hertz) return true;
this->hertz = hz;
if(i2sOn){
int result = updateClockConfig();
AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: SetRate: %i - %i"),hz, result);
}
return true;
}
bool SetPinout(){
return this->startI2SChannel();
}
bool begin(){
if(tx_is_enabled) return true;
int result = i2s_channel_enable(tx_chan);
if(result != 0){
AddLog(LOG_LEVEL_INFO,PSTR("I2S: Could not enable i2s_channel: %i"), result);
return false;
}
tx_is_enabled = true;
return true;
}
bool stop(){
i2s_channel_disable(tx_chan);
tx_is_enabled = false;
return true;
}
bool ConsumeSample(int16_t sample[2])
{
if (!tx_is_enabled)
return false;
int16_t ms[2];
ms[0] = sample[0];
ms[1] = sample[1];
MakeSampleStereo16( ms );
if (this->mono) {
// Average the two samples and overwrite
int32_t ttl = ms[LEFTCHANNEL] + ms[RIGHTCHANNEL];
ms[LEFTCHANNEL] = ms[RIGHTCHANNEL] = (ttl>>1) & 0xffff;
}
uint32_t s32;
if (output_mode == INTERNAL_DAC)
{
int16_t l = Amplify(ms[LEFTCHANNEL]) + 0x8000;
int16_t r = Amplify(ms[RIGHTCHANNEL]) + 0x8000;
s32 = (r << 16) | (l & 0xffff);
}
else
{
s32 = ((Amplify(ms[RIGHTCHANNEL])) << 16) | (Amplify(ms[LEFTCHANNEL]) & 0xffff);
}
size_t i2s_bytes_written;
i2s_channel_write(tx_chan, (const void*)&s32, sizeof(uint32_t), &i2s_bytes_written, 0);
return i2s_bytes_written;
}
private:
enum : int { EXTERNAL_I2S = 0, INTERNAL_DAC = 1, INTERNAL_PDM = 2 };
int output_mode;
bool i2sOn;
bool mono;
i2s_chan_handle_t tx_chan;
bool tx_is_enabled;
bool startI2SChannel(){
i2s_chan_config_t tx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
i2s_new_channel(&tx_chan_cfg, &tx_chan, NULL);
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_num_t)Pin(GPIO_I2S_MCLK),
.bclk = (gpio_num_t)Pin(GPIO_I2S_BCLK),
.ws = (gpio_num_t)Pin(GPIO_I2S_WS),
.dout = (gpio_num_t)Pin(GPIO_I2S_DOUT),
.din = (gpio_num_t)Pin(GPIO_I2S_DIN),
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
i2sOn = (i2s_channel_init_std_mode(tx_chan, &tx_std_cfg) == 0);
return i2sOn;
}
int updateClockConfig(){
i2s_channel_disable(tx_chan);
i2s_std_clk_config_t clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(hertz);
#ifdef SOC_I2S_SUPPORTS_APLL
clk_cfg.clk_src = I2S_CLK_SRC_APLL;
#endif
int result = i2s_channel_reconfig_std_clock(tx_chan, &clk_cfg );
if(tx_is_enabled) i2s_channel_enable(tx_chan);
return result;
}
};
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;
struct AUDIO_I2S_t {
uint8_t is2_volume; // should be in settings
AudioGeneratorMP3 *mp3 = nullptr;
AudioFileSourceFS *file;
AudioOutputI2S *out;
AudioFileSourceID3 *id3;
AudioGeneratorMP3 *decoder = NULL;
void *mp3ram = NULL;
// Webradio
AudioFileSourceICYStream *ifile = NULL;
AudioFileSourceBuffer *buff = NULL;
char wr_title[64];
void *preallocateBuffer = NULL;
void *preallocateCodec = NULL;
uint32_t retryms = 0;
TaskHandle_t mp3_task_h;
TaskHandle_t mic_task_h;
uint32_t mic_size;
uint32_t mic_rate;
uint8_t *mic_buff;
char mic_path[32];
uint8_t mic_channels;
File fwp;
uint8_t mic_stop;
int8_t mic_error;
int8_t mic_mclk = -1;
int8_t mic_bclk = -1;
int8_t mic_ws = -1;
int8_t mic_din = -1;
int8_t mic_dout = -1;
uint8_t mic_gain = 1;
bool use_stream = false;
i2s_port_t mic_port;
// SHINE
uint32_t recdur;
uint8_t stream_active;
uint8_t stream_enable;
WiFiClient client;
ESP8266WebServer *MP3Server;
uint8_t mode;
// 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;
extern FS *ufsp;
extern FS *ffsp;
const int preallocateBufferSize = 16*1024;
const int preallocateCodecSize = 29192; // MP3 codec max mem needed
//const int preallocateCodecSize = 85332; // AAC+SBR codec max mem needed
enum : int { APLL_AUTO = -1, APLL_ENABLE = 1, APLL_DISABLE = 0 };
enum : int { EXTERNAL_I2S = 0, INTERNAL_DAC = 1, INTERNAL_PDM = 2 };
void sayTime(int hour, int minutes);
void Cmd_MicRec(void);
void Cmd_wav2mp3(void);
void Cmd_Time(void);
void Rtttl(char *buffer);
void Cmd_I2SRtttl(void);
// void copy_micpars(uint32_t port) {
// audio_i2s.mic_mclk = audio_i2s.mclk;
// audio_i2s.mic_bclk = audio_i2s.bclk;
// audio_i2s.mic_ws = audio_i2s.ws;
// audio_i2s.mic_dout = audio_i2s.dout;
// audio_i2s.mic_din = audio_i2s.din;
// audio_i2s.mic_port = (i2s_port_t)port;
// }
int32_t I2S_Init_0(void) {
if(Pin(GPIO_I2S_BCLK) == -1 || Pin(GPIO_I2S_WS) == -1 || Pin(GPIO_I2S_DOUT) == -1){
return -1;
}
audio_i2s.out = new AudioOutputI2S;
bool result = audio_i2s.out->SetPinout();
if (result){
AddLog(LOG_LEVEL_INFO, PSTR("I2S: init pins bclk=%d, ws=%d, dout=%d, mclk=%d, din=%d"), Pin(GPIO_I2S_BCLK) , Pin(GPIO_I2S_WS), Pin(GPIO_I2S_DOUT), Pin(GPIO_I2S_MCLK), Pin(GPIO_I2S_DIN));
}
else{
return -1;
}
// if (audio_i2s.mic_port != 0) {
// AddLog(LOG_LEVEL_INFO, PSTR("Init audio I2S mic: port=%d, bclk=%d, ws=%d, din=%d"), audio_i2s.mic_port, audio_i2s.mic_bclk, audio_i2s.mic_ws, audio_i2s.mic_din);
// }
// audio_i2s.mode = MODE_SPK;
return 0;
}
void I2S_Init(void) {
if (I2S_Init_0() != 0) {
return;
}
audio_i2s.is2_volume = 10;
audio_i2s.out->SetGain(((float)audio_i2s.is2_volume / 100.0) * 4.0);
audio_i2s.out->begin();
audio_i2s.out->stop();
audio_i2s.mp3ram = nullptr;
if (UsePSRAM()) {
audio_i2s.mp3ram = heap_caps_malloc(preallocateCodecSize, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
}
if (UsePSRAM()) {
audio_i2s.preallocateBuffer = heap_caps_malloc(preallocateBufferSize, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
audio_i2s.preallocateCodec = heap_caps_malloc(preallocateCodecSize, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
} else {
audio_i2s.preallocateBuffer = malloc(preallocateBufferSize);
audio_i2s.preallocateCodec = malloc(preallocateCodecSize);
}
if (!audio_i2s.preallocateBuffer || !audio_i2s.preallocateCodec) {
//Serial.printf_P(PSTR("FATAL ERROR: Unable to preallocate %d bytes for app\n"), preallocateBufferSize+preallocateCodecSize);
}
// audio_i2s.mic_channels = MIC_CHANNELS;
// audio_i2s.mic_rate = MICSRATE;
}
void mp3_task(void *arg) {
while (1) {
while (audio_i2s.mp3->isRunning()) {
if (!audio_i2s.mp3->loop()) {
audio_i2s.mp3->stop();
mp3_delete();
audio_i2s.out->stop();
if (audio_i2s.mp3_task_h) {
vTaskDelete(audio_i2s.mp3_task_h);
audio_i2s.mp3_task_h = 0;
}
//mp3_task_h=nullptr;
}
vTaskDelay(pdMS_TO_TICKS(1));
}
}
}
void MDCallback(void *cbData, const char *type, bool isUnicode, const char *str) {
const char *ptr = reinterpret_cast<const char *>(cbData);
(void) isUnicode; // Punt this ball for now
(void) ptr;
if (strstr_P(type, PSTR("Title"))) {
strncpy(audio_i2s.wr_title, str, sizeof(audio_i2s.wr_title));
audio_i2s.wr_title[sizeof(audio_i2s.wr_title)-1] = 0;
//AddLog(LOG_LEVEL_INFO,PSTR("WR-Title: %s"),wr_title);
} else {
// Who knows what to do? Not me!
}
}
void StatusCallback(void *cbData, int code, const char *string) {
const char *ptr = reinterpret_cast<const char *>(cbData);
(void) code;
(void) ptr;
//strncpy_P(status, string, sizeof(status)-1);
//status[sizeof(status)-1] = 0;
}
void Webradio(const char *url) {
if (audio_i2s.decoder || audio_i2s.mp3) return;
if (!audio_i2s.out) return;
AUDIO_PWR_ON
audio_i2s.ifile = new AudioFileSourceICYStream(url);
audio_i2s.ifile->RegisterMetadataCB(MDCallback, NULL);
audio_i2s.buff = new AudioFileSourceBuffer(audio_i2s.ifile, audio_i2s.preallocateBuffer, preallocateBufferSize);
audio_i2s.buff->RegisterStatusCB(StatusCallback, NULL);
audio_i2s.decoder = new AudioGeneratorMP3(audio_i2s.preallocateCodec, preallocateCodecSize);
audio_i2s.decoder->RegisterStatusCB(StatusCallback, NULL);
audio_i2s.decoder->begin(audio_i2s.buff, audio_i2s.out);
if (!audio_i2s.decoder->isRunning()) {
// Serial.printf_P(PSTR("Can't connect to URL"));
StopPlaying();
// strcpy_P(status, PSTR("Unable to connect to URL"));
audio_i2s.retryms = millis() + 2000;
}
xTaskCreatePinnedToCore(mp3_task2, "MP3-2", 8192, NULL, 3, &audio_i2s.mp3_task_h, 1);
}
void mp3_task2(void *arg){
while (1) {
if (audio_i2s.decoder && audio_i2s.decoder->isRunning()) {
if (!audio_i2s.decoder->loop()) {
StopPlaying();
//retryms = millis() + 2000;
}
vTaskDelay(pdMS_TO_TICKS(1));
}
}
}
void StopPlaying() {
if (audio_i2s.mp3_task_h) {
vTaskDelete(audio_i2s.mp3_task_h);
audio_i2s.mp3_task_h = nullptr;
}
if (audio_i2s.decoder) {
audio_i2s.decoder->stop();
delete audio_i2s.decoder;
audio_i2s.decoder = NULL;
}
if (audio_i2s.buff) {
audio_i2s.buff->close();
delete audio_i2s.buff;
audio_i2s.buff = NULL;
}
if (audio_i2s.ifile) {
audio_i2s.ifile->close();
delete audio_i2s.ifile;
audio_i2s.ifile = NULL;
}
AUDIO_PWR_OFF
}
void Cmd_WebRadio(void) {
if (!audio_i2s.out) return;
if (audio_i2s.decoder) {
StopPlaying();
}
if (XdrvMailbox.data_len > 0) {
Webradio(XdrvMailbox.data);
ResponseCmndChar(XdrvMailbox.data);
} else {
ResponseCmndChar_P(PSTR("Stopped"));
}
}
#ifdef USE_WEBSERVER
const char HTTP_WEBRADIO[] PROGMEM =
"{s}" "I2S_WR-Title" "{m}%s{e}";
void I2S_WR_Show(bool json) {
if (audio_i2s.decoder) {
if (json) {
ResponseAppend_P(PSTR(",\"WebRadio\":{\"Title\":\"%s\"}"), audio_i2s.wr_title);
} else {
WSContentSend_PD(HTTP_WEBRADIO,audio_i2s.wr_title);
}
}
}
#endif // USE_WEBSERVER
void Play_mp3(const char *path) {
if (audio_i2s.decoder || audio_i2s.mp3) return;
if (!audio_i2s.out) return;
FS *mp3fsp = ufsp;
if (!strncmp(path, "/ffs", 4)) {
path += 4;
mp3fsp = ffsp;
}
if (!mp3fsp->exists(path)) {
AddLog(LOG_LEVEL_INFO,PSTR("MP3-Title not found: %s"),path);
return;
}
AUDIO_PWR_ON
audio_i2s.file = new AudioFileSourceFS(*mp3fsp, path);
audio_i2s.id3 = new AudioFileSourceID3(audio_i2s.file);
if (audio_i2s.mp3ram) {
audio_i2s.mp3 = new AudioGeneratorMP3(audio_i2s.mp3ram, preallocateCodecSize);
} else {
audio_i2s.mp3 = new AudioGeneratorMP3();
}
audio_i2s.mp3->begin(audio_i2s.id3, audio_i2s.out);
// Always use a task
xTaskCreatePinnedToCore(mp3_task, "MP3", 8192, NULL, 3, &audio_i2s.mp3_task_h, 1);
}
void mp3_delete(void) {
delete audio_i2s.file;
delete audio_i2s.id3;
delete audio_i2s.mp3;
audio_i2s.mp3=nullptr;
AUDIO_PWR_OFF
}
void Say(char *text) {
if (!audio_i2s.out) return;
AUDIO_PWR_ON
ESP8266SAM *sam = new ESP8266SAM;
sam->Say(audio_i2s.out, text);
delete sam;
audio_i2s.out->stop();
AUDIO_PWR_OFF
}
const char kI2SAudio_Commands[] PROGMEM = "I2S|"
"Say|Gain|Time|Rtttl|Play|WR"
#if defined(USE_SHINE) && ( (defined(USE_I2S_AUDIO) && defined(USE_I2S_MIC)) || defined(USE_M5STACK_CORE2) || defined(ESP32S3_BOX) )
"|REC"
"|MGain"
#if defined(USE_SHINE) && defined(MP3_MIC_STREAM)
"|STREAM"
#endif // MP3_MIC_STREAM
#ifdef I2S_BRIDGE
"|BRIDGE"
#endif // I2S_BRIDGE
#endif // USE_SHINE
;
void (* const I2SAudio_Command[])(void) PROGMEM = {
&Cmd_Say, &Cmd_Gain,&Cmd_Time,&Cmd_I2SRtttl,&Cmd_Play,&Cmd_WebRadio
#if defined(USE_SHINE) && ( (defined(USE_I2S_AUDIO) && defined(USE_I2S_MIC)) || defined(USE_M5STACK_CORE2) || defined(ESP32S3_BOX) )
,&Cmd_MicRec
,&Cmd_MicGain
#if defined(USE_SHINE) && defined(MP3_MIC_STREAM)
,&Cmd_MP3Stream
#endif // MP3_MIC_STREAM
#ifdef I2S_BRIDGE
,&Cmd_I2SBridge
#endif // I2S_BRIDGE
#endif // USE_SHINE
};
void Cmd_Play(void) {
if (XdrvMailbox.data_len > 0) {
Play_mp3(XdrvMailbox.data);
}
ResponseCmndChar(XdrvMailbox.data);
}
void Cmd_Gain(void) {
if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 100)) {
if (audio_i2s.out) {
audio_i2s.is2_volume=XdrvMailbox.payload;
audio_i2s.out->SetGain(((float)(audio_i2s.is2_volume-2)/100.0)*4.0);
}
}
ResponseCmndNumber(audio_i2s.is2_volume);
}
void Cmd_Say(void) {
if (XdrvMailbox.data_len > 0) {
Say(XdrvMailbox.data);
}
ResponseCmndChar(XdrvMailbox.data);
}
void Cmd_I2SRtttl(void) {
if (XdrvMailbox.data_len > 0) {
Rtttl(XdrvMailbox.data);
}
ResponseCmndChar(XdrvMailbox.data);
}
/*********************************************************************************************\
* Interface
\*********************************************************************************************/
void i2s_mp3_loop(void);
void i2s_mp3_init(void);
void MP3ShowStream(void);
bool Xdrv42(uint32_t function) {
bool result = false;
switch (function) {
case FUNC_COMMAND:
result = DecodeCommand(kI2SAudio_Commands, I2SAudio_Command);
break;
case FUNC_INIT:
I2S_Init();
break;
case FUNC_WEB_ADD_MAIN_BUTTON:
//MP3ShowStream();
break;
case FUNC_LOOP:
#if defined(USE_SHINE) && defined(MP3_MIC_STREAM)
i2s_mp3_loop();
#endif
#if defined(I2S_BRIDGE) && ( (defined(USE_I2S_AUDIO) && defined(USE_I2S_MIC)) || defined(USE_M5STACK_CORE2) || defined(ESP32S3_BOX) )
i2s_bridge_loop();
#endif
break;
case FUNC_WEB_ADD_HANDLER:
#if defined(USE_SHINE) && defined(MP3_MIC_STREAM)
audio_i2s.stream_enable = 1;
i2s_mp3_init(1);
#endif
#if defined(I2S_BRIDGE) && ( (defined(USE_I2S_AUDIO) && defined(USE_I2S_MIC)) || defined(USE_M5STACK_CORE2) || defined(ESP32S3_BOX) )
I2SBridgeInit();
#endif
break;
#ifdef USE_WEBSERVER
#ifdef USE_I2S_WEBRADIO
case FUNC_WEB_SENSOR:
I2S_WR_Show(false);
break;
#endif // USE_I2S_WEBRADIO
#endif // USE_WEBSERVER
#ifdef USE_I2S_WEBRADIO
case FUNC_JSON_APPEND:
I2S_WR_Show(true);
break;
#endif // USE_I2S_WEBRADIO
}
return result;
}
#endif // USE_I2S_AUDIO
#endif //ESP_IDF_VERSION_MAJOR >= 5