Skip to content

Instantly share code, notes, and snippets.

@Staars
Last active January 14, 2025 13:59
Show Gist options
  • Save Staars/57e7193fe99311cd0e80b83d98b13ece to your computer and use it in GitHub Desktop.
Save Staars/57e7193fe99311cd0e80b83d98b13ece to your computer and use it in GitHub Desktop.
some tests
/*
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 defined(ESP32) && ESP_IDF_VERSION_MAJOR >= 5
#ifdef USE_I2S_AUDIO
class AudioEncoder
{
public:
AudioEncoder() {
inBuffer = nullptr;
outFrame = nullptr;
samplesPerPass = 0;
byteSize = 0;
};
virtual ~AudioEncoder() {};
virtual uint32_t begin(uint32_t samplingRate, uint32_t inputChannels) { return 0; };
virtual size_t getHeader(uint8_t *header) {return 0;}
virtual size_t encode() { return 0; };
virtual size_t stop() { return 0; };
public:
int16_t *inBuffer;
uint8_t *outFrame;
uint32_t samplesPerPass;
uint32_t byteSize;
};
class AudioEncoderShineMP3 : public AudioEncoder
{
public:
AudioEncoderShineMP3() {
AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: AudioEncoderShineMP3"));
};
virtual ~AudioEncoderShineMP3() {
if (s) {shine_close(s);}
if(inBuffer){free(inBuffer); }
};
virtual uint32_t begin(uint32_t samplingRate, uint32_t inputChannels) {
shine_set_config_mpeg_defaults(&config.mpeg);
if (inputChannels == 1) {
config.mpeg.mode = MONO;
} else {
config.mpeg.mode = STEREO;
}
config.wave.samplerate = samplingRate;
config.wave.channels = (channels)inputChannels;
if (shine_check_config(config.wave.samplerate, config.mpeg.bitr) < 0) {return 3;}
s = shine_initialise(&config);
if (!s) {return 4; }
samplesPerPass = shine_samples_per_pass(s);
byteSize = samplesPerPass * 2 * inputChannels;
inBuffer = (int16_t*)malloc(byteSize);
if (!inBuffer) {return 5; }
return 0;
};
virtual size_t stop() {
int written;
outFrame = shine_flush(s, &written);
return written;
}
virtual size_t encode() {
int written;
outFrame = shine_encode_buffer_interleaved(s, inBuffer, &written);
return written;
};
protected:
shine_config_t config;
shine_t s;
};
#include "libopus/opus.h"
class AudioEncoderOpus : public AudioEncoder
{
public:
AudioEncoderOpus() {
AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: AudioEncoderOpus"));
};
virtual ~AudioEncoderOpus() {
if(inBuffer){ free(inBuffer); }
if(outFrame){ free(outFrame); }
opus_encoder_destroy(encoder);
};
virtual uint32_t begin(uint32_t samplingRate, uint32_t inputChannels) {
int error;
encoder = opus_encoder_create(samplingRate, inputChannels, OPUS_APPLICATION_AUDIO, &error);
if (error != 0) { return error; }
opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(complexity));
constexpr uint32_t frameDuration = 20; //ms
samplesPerPass = samplingRate/(1000/frameDuration);
byteSize = samplesPerPass * 2 * inputChannels;
inBuffer = (int16_t*)malloc(byteSize);
if (!inBuffer) {return 5; }
outFrame = (uint8_t*)malloc(maxBytes);
if (!outFrame) {return 5; }
return 0;
};
virtual size_t encode() {
opus_int32 i = opus_encode(encoder, inBuffer, samplesPerPass, outFrame, maxBytes);
return i;
}
virtual size_t getHeader(uint8_t *header) {
header = (uint8_t*)&opusCombinedHeader;
return sizeof(opusCombinedHeader);
}
protected:
OpusEncoder *encoder;
const uint32_t maxBytes = 1296;
const uint32_t complexity = 1; // 1-10 - low to high quality
struct __attribute__((packed)) {
char signatureHead[8] = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'};
uint8_t version = 1;
uint8_t channelCount = 0;
uint16_t preSkip = 3840;
uint32_t sampleRate = 0;
int16_t outputGain = 0;
uint8_t channelMappingFamily = 0;
char signatureTags[8] = {'O', 'p', 'u', 's', 'T', 'a', 'g', 's'};
uint32_t vendorStringLength = 8;
char vendor[8] = "Tasmota";
uint32_t userCommentListLength = 0;
} opusCombinedHeader;
};
// micro to mp3 file or stream
void I2sMicTask_new(void *arg){
int8_t error = 0;
int written;
AudioEncoder *mic_enc;
File rec_file = (File)nullptr;
uint8_t *_header;
size_t _headerSize;
uint16_t bwritten;
uint32_t ctime;
uint32_t gain = audio_i2s.Settings->rx.gain;
uint32_t timeForOneRead;
if(audio_i2s_mp3.encoder_type == MP3_ENCODER){
mic_enc = new AudioEncoderShineMP3;
} else {
mic_enc = new AudioEncoderOpus;
}
if (!audio_i2s_mp3.use_stream) {
rec_file = ufsp->open(audio_i2s_mp3.mic_path, "w");
if (!rec_file) {
error = 1;
goto exit;
}
} else {
if (!audio_i2s_mp3.stream_active) {
error = 2;
audio_i2s_mp3.use_stream = 0;
goto exit;
}
audio_i2s_mp3.client.flush();
audio_i2s_mp3.client.setTimeout(3);
if(audio_i2s_mp3.encoder_type == MP3_ENCODER){
audio_i2s_mp3.client.print("HTTP/1.1 200 OK\r\n"
"Content-Type: audio/mpeg;\r\n\r\n");
} else if (audio_i2s_mp3.encoder_type == OPUS_ENCODER){
audio_i2s_mp3.client.print("HTTP/1.1 200 OK\r\n"
"Content-Type: audio/opus;\r\n\r\n");
}
}
error = mic_enc->begin(audio_i2s.Settings->rx.sample_rate, audio_i2s.Settings->rx.channels);
if(error != 0){
goto exit;
}
AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: mic_enc->begin %u"), error);
_headerSize = mic_enc->getHeader(_header);
if(_headerSize != 0){
if (!audio_i2s_mp3.use_stream) {
rec_file.write(_header, written);
} else {
audio_i2s_mp3.client.write((const char*)_header, written);
}
}
ctime = TasmotaGlobal.uptime;
timeForOneRead = 1000 / ((audio_i2s.Settings->rx.sample_rate / (mic_enc->samplesPerPass * audio_i2s.Settings->rx.channels )));
timeForOneRead -= 1; // be very in time
AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: samples %u, bytesize %u, time: %u"),mic_enc->samplesPerPass, mic_enc->byteSize, timeForOneRead);
while (!audio_i2s_mp3.mic_stop) {
TickType_t xLastWakeTime = xTaskGetTickCount();
size_t bytes_read;
// bytes_read = audio_i2s.in->readMic((uint8_t*)buffer, bytesize, true /*dc_block*/, false /*apply_gain*/, true /*lowpass*/, nullptr /*peak_ptr*/);
i2s_channel_read(audio_i2s.in->getRxHandle(), (void*)mic_enc->inBuffer, mic_enc->byteSize, &bytes_read, pdMS_TO_TICKS(5));
if(bytes_read < mic_enc->byteSize) AddLog(LOG_LEVEL_DEBUG, PSTR("!! %u, %u"), bytes_read, mic_enc->byteSize);
if (gain > 1) {
// set gain the "old way"
int16_t _gain = gain / 16;
for (uint32_t cnt = 0; cnt < bytes_read / 2; cnt++) {
mic_enc->inBuffer[cnt] *= _gain;
}
}
written = mic_enc->encode();
// AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: mic_enc->encode written %u"),written);
if (!audio_i2s_mp3.use_stream) {
bwritten = rec_file.write(mic_enc->outFrame, written);
if (bwritten != written) {
AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: rec file write error"));
break;
}
} else {
audio_i2s_mp3.client.write((const char*)mic_enc->outFrame, written);
if (!audio_i2s_mp3.client.connected()) {
AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: client.connected error"));
break;
}
}
audio_i2s_mp3.recdur = TasmotaGlobal.uptime - ctime;
vTaskDelayUntil( &xLastWakeTime, pdMS_TO_TICKS(timeForOneRead));
}
written = mic_enc->stop();
if (!audio_i2s_mp3.use_stream) {
rec_file.write(mic_enc->outFrame, written);
} else {
audio_i2s_mp3.client.write((const char*)mic_enc->outFrame, written);
}
exit:
delete mic_enc;
if (rec_file) {
rec_file.close();
AddLog(LOG_LEVEL_INFO, PSTR("I2S: rec file closed"));
}
if (audio_i2s_mp3.use_stream) {
audio_i2s_mp3.client.stop();
}
audio_i2s.in->stopRx();
audio_i2s_mp3.mic_stop = 0;
audio_i2s_mp3.mic_error = error;
AddLog(LOG_LEVEL_INFO, PSTR("I2S: record task result code: %d"), error);
audio_i2s_mp3.mic_task_handle = 0;
audio_i2s_mp3.recdur = 0;
audio_i2s_mp3.stream_active = 0;
vTaskDelete(NULL);
}
int32_t I2sRecord(char *path, uint32_t encoder_type) {
esp_err_t err = ESP_OK;
uint32_t stack = 12000;
switch(encoder_type){
case MP3_ENCODER:
switch(audio_i2s.Settings->rx.sample_rate){
case 32000: case 48000: case 44100:
break; // supported
default:
AddLog(LOG_LEVEL_INFO, PSTR("I2S: unsupported sample rate for MP3 encoding: %d"), audio_i2s.Settings->rx.sample_rate);
return -1;
}
AddLog(LOG_LEVEL_INFO, PSTR("I2S: start MP3 encoding: %d"), audio_i2s.Settings->rx.sample_rate);
break;
case OPUS_ENCODER:
switch(audio_i2s.Settings->rx.sample_rate){
case 48000: case 24000: case 16000: case 12000: case 8000:
break;
default:
AddLog(LOG_LEVEL_INFO, PSTR("I2S: unsupported sample rate for OPUS encoding: %d"), audio_i2s.Settings->rx.sample_rate);
return -1;
}
AddLog(LOG_LEVEL_INFO, PSTR("I2S: start OPUS encoding: %d Hz"), audio_i2s.Settings->rx.sample_rate);
stack = 25000;
}
audio_i2s_mp3.encoder_type = encoder_type;
#ifdef USE_I2S_MP3
if (audio_i2s_mp3.decoder) return 0;
#endif
if(!audio_i2s_mp3.use_stream){
strlcpy(audio_i2s_mp3.mic_path, path, sizeof(audio_i2s_mp3.mic_path));
}
audio_i2s_mp3.mic_stop = 0;
audio_i2s.in->startRx();
err = xTaskCreatePinnedToCore(I2sMicTask_new, "MIC", stack, NULL, 3, &audio_i2s_mp3.mic_task_handle, 1);
return err;
}
#endif // USE_I2S_AUDIO
#endif // defined(ESP32) && ESP_IDF_VERSION_MAJOR >= 5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment