5086 lines
212 KiB
C++
5086 lines
212 KiB
C++
#include "../core/options.h"
|
||
#if VS1053_CS==255
|
||
/*
|
||
* Audio.cpp
|
||
*
|
||
* Created on: Oct 26.2018
|
||
*
|
||
* Version 2.0.5g
|
||
* Updated on: Aug 12.2022
|
||
* Author: Wolle (schreibfaul1)
|
||
*
|
||
*/
|
||
#include "AudioEx.h"
|
||
#include "mp3_decoder/mp3_decoder.h"
|
||
#include "aac_decoder/aac_decoder.h"
|
||
#include "flac_decoder/flac_decoder.h"
|
||
#include "../core/config.h"
|
||
|
||
#ifdef SDFATFS_USED
|
||
fs::SDFATFS SD_SDFAT;
|
||
#endif
|
||
#ifndef DMA_BUFCOUNT
|
||
#define DMA_BUFCOUNT 8
|
||
#endif
|
||
#ifndef DMA_BUFLEN
|
||
#define DMA_BUFLEN 512 // (512)
|
||
#endif
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
AudioBuffer::AudioBuffer(size_t maxBlockSize) {
|
||
// if maxBlockSize isn't set use defaultspace (1600 bytes) is enough for aac and mp3 player
|
||
if(maxBlockSize) m_resBuffSizeRAM = maxBlockSize;
|
||
if(maxBlockSize) m_maxBlockSize = maxBlockSize;
|
||
}
|
||
|
||
AudioBuffer::~AudioBuffer() {
|
||
if(m_buffer)
|
||
free(m_buffer);
|
||
m_buffer = NULL;
|
||
}
|
||
|
||
void AudioBuffer::setBufsize(int ram, int psram) {
|
||
if (ram > -1) // -1 == default / no change
|
||
m_buffSizeRAM = ram;
|
||
if (psram > -1)
|
||
m_buffSizePSRAM = psram;
|
||
}
|
||
|
||
size_t AudioBuffer::init() {
|
||
if(m_buffer) free(m_buffer);
|
||
m_buffer = NULL;
|
||
if(psramInit() && m_buffSizePSRAM > 0) {
|
||
// PSRAM found, AudioBuffer will be allocated in PSRAM
|
||
m_f_psram = true;
|
||
m_buffSize = m_buffSizePSRAM;
|
||
m_buffer = (uint8_t*) ps_calloc(m_buffSize, sizeof(uint8_t));
|
||
m_buffSize = m_buffSizePSRAM - m_resBuffSizePSRAM;
|
||
}
|
||
if(m_buffer == NULL) {
|
||
// PSRAM not found, not configured or not enough available
|
||
m_f_psram = false;
|
||
m_buffSize = m_buffSizeRAM;
|
||
m_buffer = (uint8_t*) calloc(m_buffSize, sizeof(uint8_t));
|
||
m_buffSize = m_buffSizeRAM - m_resBuffSizeRAM;
|
||
}
|
||
if(!m_buffer)
|
||
return 0;
|
||
m_f_init = true;
|
||
resetBuffer();
|
||
return m_buffSize;
|
||
}
|
||
|
||
void AudioBuffer::changeMaxBlockSize(uint16_t mbs){
|
||
m_maxBlockSize = mbs;
|
||
return;
|
||
}
|
||
|
||
uint16_t AudioBuffer::getMaxBlockSize(){
|
||
return m_maxBlockSize;
|
||
}
|
||
|
||
size_t AudioBuffer::freeSpace() {
|
||
if(m_readPtr >= m_writePtr) {
|
||
m_freeSpace = (m_readPtr - m_writePtr);
|
||
} else {
|
||
m_freeSpace = (m_endPtr - m_writePtr) + (m_readPtr - m_buffer);
|
||
}
|
||
if(m_f_start)
|
||
m_freeSpace = m_buffSize;
|
||
return m_freeSpace - 1;
|
||
}
|
||
|
||
size_t AudioBuffer::writeSpace() {
|
||
if(m_readPtr >= m_writePtr) {
|
||
m_writeSpace = (m_readPtr - m_writePtr - 1); // readPtr must not be overtaken
|
||
} else {
|
||
if(getReadPos() == 0)
|
||
m_writeSpace = (m_endPtr - m_writePtr - 1);
|
||
else
|
||
m_writeSpace = (m_endPtr - m_writePtr);
|
||
}
|
||
if(m_f_start)
|
||
m_writeSpace = m_buffSize - 1;
|
||
return m_writeSpace;
|
||
}
|
||
|
||
size_t AudioBuffer::bufferFilled() {
|
||
if(m_writePtr >= m_readPtr) {
|
||
m_dataLength = (m_writePtr - m_readPtr);
|
||
} else {
|
||
m_dataLength = (m_endPtr - m_readPtr) + (m_writePtr - m_buffer);
|
||
}
|
||
return m_dataLength;
|
||
}
|
||
|
||
void AudioBuffer::bytesWritten(size_t bw) {
|
||
m_writePtr += bw;
|
||
if(m_writePtr == m_endPtr) {
|
||
m_writePtr = m_buffer;
|
||
}
|
||
if(bw && m_f_start)
|
||
m_f_start = false;
|
||
}
|
||
|
||
void AudioBuffer::bytesWasRead(size_t br) {
|
||
m_readPtr += br;
|
||
if(m_readPtr >= m_endPtr) {
|
||
size_t tmp = m_readPtr - m_endPtr;
|
||
m_readPtr = m_buffer + tmp;
|
||
}
|
||
}
|
||
|
||
uint8_t* AudioBuffer::getWritePtr() {
|
||
return m_writePtr;
|
||
}
|
||
|
||
uint8_t* AudioBuffer::getReadPtr() {
|
||
size_t len = m_endPtr - m_readPtr;
|
||
if(len < m_maxBlockSize) { // be sure the last frame is completed
|
||
memcpy(m_endPtr, m_buffer, m_maxBlockSize - len); // cpy from m_buffer to m_endPtr with len
|
||
}
|
||
return m_readPtr;
|
||
}
|
||
|
||
void AudioBuffer::resetBuffer() {
|
||
m_writePtr = m_buffer;
|
||
m_readPtr = m_buffer;
|
||
m_endPtr = m_buffer + m_buffSize;
|
||
m_f_start = true;
|
||
// memset(m_buffer, 0, m_buffSize); //Clear Inputbuffer
|
||
}
|
||
|
||
uint32_t AudioBuffer::getWritePos() {
|
||
return m_writePtr - m_buffer;
|
||
}
|
||
|
||
uint32_t AudioBuffer::getReadPos() {
|
||
return m_readPtr - m_buffer;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
Audio::Audio(bool internalDAC /* = false */, uint8_t channelEnabled /* = I2S_DAC_CHANNEL_BOTH_EN */, uint8_t i2sPort) {
|
||
|
||
// build-in-DAC works only with ESP32 (ESP32-S3 has no build-in-DAC)
|
||
// build-in-DAC last working Arduino Version: 2.0.0-RC2
|
||
// possible values for channelEnabled are:
|
||
// I2S_DAC_CHANNEL_DISABLE = 0, Disable I2S built-in DAC signals
|
||
// I2S_DAC_CHANNEL_RIGHT_EN = 1, Enable I2S built-in DAC right channel, maps to DAC channel 1 on GPIO25
|
||
// I2S_DAC_CHANNEL_LEFT_EN = 2, Enable I2S built-in DAC left channel, maps to DAC channel 2 on GPIO26
|
||
// I2S_DAC_CHANNEL_BOTH_EN = 0x3, Enable both of the I2S built-in DAC channels.
|
||
// I2S_DAC_CHANNEL_MAX = 0x4, I2S built-in DAC mode max index
|
||
#ifdef AUDIO_LOG
|
||
m_f_Log = true;
|
||
#endif
|
||
mutex_pl = xSemaphoreCreateMutex();
|
||
clientsecure.setInsecure(); // if that can't be resolved update to ESP32 Arduino version 1.0.5-rc05 or higher
|
||
m_f_channelEnabled = channelEnabled;
|
||
m_f_internalDAC = internalDAC;
|
||
//i2s configuration
|
||
m_i2s_num = i2sPort; // i2s port number
|
||
m_i2s_config.sample_rate = 16000;
|
||
m_i2s_config.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT;
|
||
m_i2s_config.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT;
|
||
m_i2s_config.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1; // interrupt priority
|
||
#ifdef OLD_DMABUF_PARAMS
|
||
m_i2s_config.dma_buf_count = 16; // 4×512×16=32768
|
||
#else
|
||
m_i2s_config.dma_buf_count = psramInit()?16:DMA_BUFCOUNT;
|
||
#endif
|
||
m_i2s_config.dma_buf_len = psramInit()?512:DMA_BUFLEN;
|
||
m_i2s_config.use_apll = APLL_DISABLE; // must be disabled in V2.0.1-RC1
|
||
m_i2s_config.tx_desc_auto_clear = true; // new in V1.0.1
|
||
m_i2s_config.fixed_mclk = I2S_PIN_NO_CHANGE;
|
||
|
||
|
||
if (internalDAC) {
|
||
|
||
#ifdef CONFIG_IDF_TARGET_ESP32 // ESP32S3 has no DAC
|
||
|
||
log_i("internal DAC");
|
||
m_i2s_config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN );
|
||
|
||
#if ESP_ARDUINO_VERSION_MAJOR >= 2
|
||
m_i2s_config.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_MSB); // vers >= 2.0.0
|
||
#else
|
||
m_i2s_config.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S_MSB);
|
||
#endif
|
||
|
||
i2s_driver_install((i2s_port_t)m_i2s_num, &m_i2s_config, 0, NULL);
|
||
i2s_set_dac_mode((i2s_dac_mode_t)m_f_channelEnabled);
|
||
if(m_f_channelEnabled != I2S_DAC_CHANNEL_BOTH_EN) {
|
||
m_f_forceMono = true;
|
||
}
|
||
|
||
#endif
|
||
|
||
}
|
||
else {
|
||
m_i2s_config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX);
|
||
|
||
#if ESP_ARDUINO_VERSION_MAJOR >= 2
|
||
m_i2s_config.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S); // Arduino vers. > 2.0.0
|
||
#else
|
||
m_i2s_config.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB);
|
||
#endif
|
||
|
||
i2s_driver_install((i2s_port_t)m_i2s_num, &m_i2s_config, 0, NULL);
|
||
m_f_forceMono = false;
|
||
}
|
||
|
||
i2s_zero_dma_buffer((i2s_port_t) m_i2s_num);
|
||
|
||
for(int i = 0; i <3; i++) {
|
||
m_filter[i].a0 = 1;
|
||
m_filter[i].a1 = 0;
|
||
m_filter[i].a2 = 0;
|
||
m_filter[i].b1 = 0;
|
||
m_filter[i].b2 = 0;
|
||
}
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::setBufsize(int rambuf_sz, int psrambuf_sz) {
|
||
if(InBuff.isInitialized()) {
|
||
log_e("Audio::setBufsize must not be called after audio is initialized");
|
||
return;
|
||
}
|
||
InBuff.setBufsize(rambuf_sz, psrambuf_sz);
|
||
};
|
||
|
||
void Audio::initInBuff() {
|
||
if(!InBuff.isInitialized()) {
|
||
size_t size = InBuff.init();
|
||
if (size > 0) {
|
||
AUDIO_INFO("PSRAM %sfound, inputBufferSize: %u bytes", InBuff.havePSRAM()?"":"not ", size - 1);
|
||
}
|
||
}
|
||
changeMaxBlockSize(1600); // default size mp3 or aac
|
||
}
|
||
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
esp_err_t Audio::I2Sstart(uint8_t i2s_num) {
|
||
// It is not necessary to call this function after i2s_driver_install() (it is started automatically),
|
||
// however it is necessary to call it after i2s_stop()
|
||
return i2s_start((i2s_port_t) i2s_num);
|
||
}
|
||
|
||
esp_err_t Audio::I2Sstop(uint8_t i2s_num) {
|
||
return i2s_stop((i2s_port_t) i2s_num);
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
esp_err_t Audio::i2s_mclk_pin_select(const uint8_t pin) {
|
||
// IDF >= 4.4 use setPinout(BCLK, LRC, DOUT, DIN, MCK) only, i2s_mclk_pin_select() is no longer needed
|
||
|
||
if(pin != 0 && pin != 1 && pin != 3) {
|
||
log_e("Only support GPIO0/GPIO1/GPIO3, gpio_num:%d", pin);
|
||
return ESP_ERR_INVALID_ARG;
|
||
}
|
||
|
||
#ifdef CONFIG_IDF_TARGET_ESP32
|
||
switch(pin){
|
||
case 0:
|
||
PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0_CLK_OUT1);
|
||
WRITE_PERI_REG(PIN_CTRL, 0xFFF0);
|
||
break;
|
||
case 1:
|
||
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0TXD_CLK_OUT3);
|
||
WRITE_PERI_REG(PIN_CTRL, 0xF0F0);
|
||
break;
|
||
case 3:
|
||
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_U0RXD_CLK_OUT2);
|
||
WRITE_PERI_REG(PIN_CTRL, 0xFF00);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
#endif
|
||
|
||
return ESP_OK;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
Audio::~Audio() {
|
||
//I2Sstop(m_i2s_num);
|
||
//InBuff.~AudioBuffer(); #215 the AudioBuffer is automatically destroyed by the destructor
|
||
setDefaults();
|
||
if(m_playlistBuff) {free(m_playlistBuff); m_playlistBuff = NULL;}
|
||
i2s_driver_uninstall((i2s_port_t)m_i2s_num); // #215 free I2S buffer
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::setDefaults() {
|
||
stopSong();
|
||
initInBuff(); // initialize InputBuffer if not already done
|
||
InBuff.resetBuffer();
|
||
MP3Decoder_FreeBuffers();
|
||
FLACDecoder_FreeBuffers();
|
||
AACDecoder_FreeBuffers();
|
||
if(m_playlistBuff) {free(m_playlistBuff); m_playlistBuff = NULL;} // free if stream is not m3u8
|
||
vector_clear_and_shrink(m_playlistURL);
|
||
vector_clear_and_shrink(m_playlistContent);
|
||
m_hashQueue.clear(); m_hashQueue.shrink_to_fit(); // uint32_t vector
|
||
if(config.store.play_mode!=PM_SDCARD){
|
||
client.stop();
|
||
client.flush(); // release memory
|
||
clientsecure.stop();
|
||
clientsecure.flush();
|
||
_client = static_cast<WiFiClient*>(&client); /* default to *something* so that no NULL deref can happen */
|
||
}
|
||
playI2Sremains();
|
||
|
||
AUDIO_INFO("buffers freed, free Heap: %u bytes", ESP.getFreeHeap());
|
||
|
||
m_f_chunked = false; // Assume not chunked
|
||
m_f_firstmetabyte = false;
|
||
m_f_playing = false;
|
||
m_f_ssl = false;
|
||
m_f_swm = true; // Assume no metaint (stream without metadata)
|
||
m_f_tts = false;
|
||
m_f_firstCall = true; // InitSequence for processWebstream and processLokalFile
|
||
m_f_running = false;
|
||
m_f_loop = false; // Set if audio file should loop
|
||
m_f_unsync = false; // set within ID3 tag but not used
|
||
m_f_exthdr = false; // ID3 extended header
|
||
m_f_rtsp = false; // RTSP (m3u8)stream
|
||
m_f_m3u8data = false; // set again in processM3U8entries() if necessary
|
||
m_f_continue = false;
|
||
m_f_ts = false;
|
||
|
||
m_streamType = ST_NONE;
|
||
m_codec = CODEC_NONE;
|
||
m_playlistFormat = FORMAT_NONE;
|
||
m_datamode = AUDIO_NONE;
|
||
m_audioCurrentTime = 0; // Reset playtimer
|
||
m_audioFileDuration = 0;
|
||
m_audioDataStart = 0;
|
||
m_audioDataSize = 0;
|
||
m_avr_bitrate = 0; // the same as m_bitrate if CBR, median if VBR
|
||
m_bitRate = 0; // Bitrate still unknown
|
||
m_bytesNotDecoded = 0; // counts all not decodable bytes
|
||
m_chunkcount = 0; // for chunked streams
|
||
m_contentlength = 0; // If Content-Length is known, count it
|
||
m_curSample = 0;
|
||
m_metaint = 0; // No metaint yet
|
||
m_LFcount = 0; // For end of header detection
|
||
m_controlCounter = 0; // Status within readID3data() and readWaveHeader()
|
||
m_channels = 2; // assume stereo #209
|
||
m_streamTitleHash = 0;
|
||
m_file_size = 0;
|
||
m_ID3Size = 0;
|
||
}
|
||
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::setConnectionTimeout(uint16_t timeout_ms, uint16_t timeout_ms_ssl){
|
||
if(timeout_ms) m_timeout_ms = timeout_ms;
|
||
if(timeout_ms_ssl) m_timeout_ms_ssl = timeout_ms_ssl;
|
||
}
|
||
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::connecttohost(const char* host, const char* user, const char* pwd) {
|
||
// user and pwd for authentification only, can be empty
|
||
|
||
if(host == NULL) {
|
||
AUDIO_INFO("Hostaddress is empty");
|
||
if(audio_error) audio_error("Hostaddress is empty");
|
||
return false;
|
||
}
|
||
|
||
uint16_t lenHost = strlen(host);
|
||
|
||
if(lenHost >= 512 - 10) {
|
||
AUDIO_INFO("Hostaddress is too long");
|
||
if(audio_error) audio_error("Hostaddress is too long");
|
||
return false;
|
||
}
|
||
|
||
int idx = indexOf(host, "http");
|
||
char* l_host = (char*)malloc(lenHost + 10);
|
||
if(idx < 0){strcpy(l_host, "http://"); strcat(l_host, host); } // amend "http;//" if not found
|
||
else {strcpy(l_host, (host + idx));} // trim left if necessary
|
||
|
||
char* h_host = NULL; // pointer of l_host without http:// or https://
|
||
if(startsWith(l_host, "https")) h_host = strdup(l_host + 8);
|
||
else h_host = strdup(l_host + 7);
|
||
|
||
// initializationsequence
|
||
int16_t pos_slash; // position of "/" in hostname
|
||
int16_t pos_colon; // position of ":" in hostname
|
||
int16_t pos_ampersand; // position of "&" in hostname
|
||
uint16_t port = 80; // port number
|
||
|
||
// In the URL there may be an extension, like noisefm.ru:8000/play.m3u&t=.m3u
|
||
pos_slash = indexOf(h_host, "/", 0);
|
||
pos_colon = indexOf(h_host, ":", 0);
|
||
if(isalpha(h_host[pos_colon + 1])) pos_colon = -1; // no portnumber follows
|
||
pos_ampersand = indexOf(h_host, "&", 0);
|
||
|
||
char *hostwoext = NULL; // "skonto.ls.lv:8002" in "skonto.ls.lv:8002/mp3"
|
||
char *extension = NULL; // "/mp3" in "skonto.ls.lv:8002/mp3"
|
||
|
||
if(pos_slash > 1) {
|
||
hostwoext = (char*)malloc(pos_slash + 1);
|
||
memcpy(hostwoext, h_host, pos_slash);
|
||
hostwoext[pos_slash] = '\0';
|
||
uint16_t extLen = urlencode_expected_len(h_host + pos_slash);
|
||
extension = (char *)malloc(extLen + 20);
|
||
memcpy(extension, h_host + pos_slash, extLen);
|
||
urlencode(extension, extLen, true);
|
||
}
|
||
else{ // url has no extension
|
||
hostwoext = strdup(h_host);
|
||
extension = strdup("/");
|
||
}
|
||
|
||
if((pos_colon >= 0) && ((pos_ampersand == -1) or (pos_ampersand > pos_colon))){
|
||
port = atoi(h_host + pos_colon + 1);// Get portnumber as integer
|
||
hostwoext[pos_colon] = '\0';// Host without portnumber
|
||
}
|
||
|
||
AUDIO_INFO("Connect to new host: \"%s\"", l_host);
|
||
setDefaults(); // no need to stop clients if connection is established (default is true)
|
||
|
||
if(startsWith(l_host, "https")) m_f_ssl = true;
|
||
else m_f_ssl = false;
|
||
|
||
// authentification
|
||
uint8_t auth = strlen(user) + strlen(pwd);
|
||
char toEncode[auth + 4];
|
||
toEncode[0] = '\0';
|
||
strcat(toEncode, user);
|
||
strcat(toEncode, ":");
|
||
strcat(toEncode, pwd);
|
||
char authorization[base64_encode_expected_len(strlen(toEncode)) + 1];
|
||
authorization[0] = '\0';
|
||
b64encode((const char*)toEncode, strlen(toEncode), authorization);
|
||
|
||
// AUDIO_INFO("Connect to \"%s\" on port %d, extension \"%s\"", hostwoext, port, extension);
|
||
|
||
char rqh[strlen(h_host) + strlen(authorization) + 200]; // http request header
|
||
rqh[0] = '\0';
|
||
|
||
strcat(rqh, "GET ");
|
||
strcat(rqh, extension);
|
||
strcat(rqh, " HTTP/1.1\r\n");
|
||
strcat(rqh, "Host: ");
|
||
strcat(rqh, hostwoext);
|
||
strcat(rqh, "\r\n");
|
||
strcat(rqh, "Icy-MetaData:1\r\n");
|
||
strcat(rqh, "Authorization: Basic ");
|
||
strcat(rqh, authorization);
|
||
strcat(rqh, "\r\n");
|
||
strcat(rqh, "Accept-Encoding: identity;q=1,*;q=0\r\n");
|
||
strcat(rqh, "User-Agent: Mozilla/5.0\r\n");
|
||
strcat(rqh, "Connection: keep-alive\r\n\r\n");
|
||
|
||
if(ESP_ARDUINO_VERSION_MAJOR == 2 && ESP_ARDUINO_VERSION_MINOR == 0 && ESP_ARDUINO_VERSION_PATCH >= 3 && MAX_AUDIO_SOCKET_TIMEOUT){
|
||
m_timeout_ms_ssl = UINT16_MAX; // bug in v2.0.3 if hostwoext is a IPaddr not a name
|
||
m_timeout_ms = UINT16_MAX; // [WiFiClient.cpp:253] connect(): select returned due to timeout 250 ms for fd 48
|
||
}
|
||
bool res = true; // no need to reconnect if connection exists
|
||
|
||
if(m_f_ssl){ _client = static_cast<WiFiClient*>(&clientsecure); if(port == 80) port = 443;}
|
||
else { _client = static_cast<WiFiClient*>(&client);}
|
||
|
||
uint32_t t = millis();
|
||
if(m_f_Log) AUDIO_INFO("connect to %s on port %d path %s", hostwoext, port, extension);
|
||
res = _client->connect(hostwoext, port, m_f_ssl ? m_timeout_ms_ssl : m_timeout_ms);
|
||
if(res){
|
||
uint32_t dt = millis() - t;
|
||
strcpy(m_lastHost, l_host);
|
||
AUDIO_INFO("%s has been established in %u ms, free Heap: %u bytes",
|
||
m_f_ssl?"SSL":"Connection", dt, ESP.getFreeHeap());
|
||
m_f_running = true;
|
||
}
|
||
|
||
m_expectedCodec = CODEC_NONE;
|
||
m_expectedPlsFmt = FORMAT_NONE;
|
||
|
||
if(res){
|
||
_client->print(rqh);
|
||
if(endsWith(extension, ".mp3")) m_expectedCodec = CODEC_MP3;
|
||
if(endsWith(extension, ".aac")) m_expectedCodec = CODEC_AAC;
|
||
if(endsWith(extension, ".wav")) m_expectedCodec = CODEC_WAV;
|
||
if(endsWith(extension, ".m4a")) m_expectedCodec = CODEC_M4A;
|
||
if(endsWith(extension, ".flac")) m_expectedCodec = CODEC_FLAC;
|
||
if(endsWith(extension, ".asx")) m_expectedPlsFmt = FORMAT_ASX;
|
||
if(endsWith(extension, ".m3u")) m_expectedPlsFmt = FORMAT_M3U;
|
||
if(endsWith(extension, ".m3u8")) m_expectedPlsFmt = FORMAT_M3U8;
|
||
if(endsWith(extension, ".pls")) m_expectedPlsFmt = FORMAT_PLS;
|
||
|
||
setDatamode(HTTP_RESPONSE_HEADER); // Handle header
|
||
m_streamType = ST_WEBSTREAM;
|
||
}
|
||
else{
|
||
AUDIO_INFO("Request %s failed!", l_host);
|
||
AUDIO_ERROR("Request %s failed!", l_host);
|
||
if(audio_showstation) audio_showstation("");
|
||
if(audio_showstreamtitle) audio_showstreamtitle("");
|
||
if(audio_icydescription) audio_icydescription("");
|
||
if(audio_icyurl) audio_icyurl("");
|
||
m_lastHost[0] = 0;
|
||
}
|
||
if(hostwoext) {free(hostwoext); hostwoext = NULL;}
|
||
if(extension) {free(extension); extension = NULL;}
|
||
if(l_host ) {free(l_host); l_host = NULL;}
|
||
if(h_host ) {free(h_host); h_host = NULL;}
|
||
return res;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::httpPrint(const char* host) {
|
||
// user and pwd for authentification only, can be empty
|
||
|
||
if(host == NULL) {
|
||
AUDIO_INFO("Hostaddress is empty");
|
||
return false;
|
||
}
|
||
|
||
char* h_host = NULL; // pointer of l_host without http:// or https://
|
||
if(m_f_ssl) h_host = strdup(host + 8);
|
||
else h_host = strdup(host + 7);
|
||
|
||
int16_t pos_slash; // position of "/" in hostname
|
||
int16_t pos_colon; // position of ":" in hostname
|
||
int16_t pos_ampersand; // position of "&" in hostname
|
||
uint16_t port = 80; // port number
|
||
|
||
// In the URL there may be an extension, like noisefm.ru:8000/play.m3u&t=.m3u
|
||
pos_slash = indexOf(h_host, "/", 0);
|
||
pos_colon = indexOf(h_host, ":", 0);
|
||
if(isalpha(h_host[pos_colon + 1])) pos_colon = -1; // no portnumber follows
|
||
pos_ampersand = indexOf(h_host, "&", 0);
|
||
|
||
char *hostwoext = NULL; // "skonto.ls.lv:8002" in "skonto.ls.lv:8002/mp3"
|
||
char *extension = NULL; // "/mp3" in "skonto.ls.lv:8002/mp3"
|
||
|
||
if(pos_slash > 1) {
|
||
hostwoext = (char*)malloc(pos_slash + 1);
|
||
memcpy(hostwoext, h_host, pos_slash);
|
||
hostwoext[pos_slash] = '\0';
|
||
uint16_t extLen = urlencode_expected_len(h_host + pos_slash);
|
||
extension = (char *)malloc(extLen + 20);
|
||
memcpy(extension, h_host + pos_slash, extLen);
|
||
urlencode(extension, extLen, true);
|
||
}
|
||
else{ // url has no extension
|
||
hostwoext = strdup(h_host);
|
||
extension = strdup("/");
|
||
}
|
||
|
||
if((pos_colon >= 0) && ((pos_ampersand == -1) or (pos_ampersand > pos_colon))){
|
||
port = atoi(h_host + pos_colon + 1);// Get portnumber as integer
|
||
hostwoext[pos_colon] = '\0';// Host without portnumber
|
||
}
|
||
|
||
AUDIO_INFO("new request: \"%s\"", host);
|
||
|
||
char rqh[strlen(h_host) + 200]; // http request header
|
||
rqh[0] = '\0';
|
||
|
||
strcat(rqh, "GET ");
|
||
strcat(rqh, extension);
|
||
strcat(rqh, " HTTP/1.1\r\n");
|
||
strcat(rqh, "Host: ");
|
||
strcat(rqh, hostwoext);
|
||
strcat(rqh, "\r\n");
|
||
strcat(rqh, "Accept-Encoding: identity;q=1,*;q=0\r\n");
|
||
strcat(rqh, "User-Agent: Mozilla/5.0\r\n");
|
||
strcat(rqh, "Connection: keep-alive\r\n\r\n");
|
||
|
||
if(m_f_ssl){ _client = static_cast<WiFiClient*>(&clientsecure); if(port == 80) port = 443;}
|
||
else { _client = static_cast<WiFiClient*>(&client);}
|
||
|
||
if(!_client->connected()){
|
||
AUDIO_INFO("The host has disconnected, reconnecting");
|
||
if(!_client->connect(hostwoext, port)){
|
||
log_e("connection lost");
|
||
stopSong();
|
||
return false;
|
||
}
|
||
}
|
||
_client->print(rqh);
|
||
|
||
if(endsWith(extension, ".mp3")) m_expectedCodec = CODEC_MP3;
|
||
if(endsWith(extension, ".aac")) m_expectedCodec = CODEC_AAC;
|
||
if(endsWith(extension, ".wav")) m_expectedCodec = CODEC_WAV;
|
||
if(endsWith(extension, ".m4a")) m_expectedCodec = CODEC_M4A;
|
||
if(endsWith(extension, ".flac")) m_expectedCodec = CODEC_FLAC;
|
||
if(endsWith(extension, ".asx")) m_expectedPlsFmt = FORMAT_ASX;
|
||
if(endsWith(extension, ".m3u")) m_expectedPlsFmt = FORMAT_M3U;
|
||
if(endsWith(extension, ".m3u8")) m_expectedPlsFmt = FORMAT_M3U8;
|
||
if(endsWith(extension, ".pls")) m_expectedPlsFmt = FORMAT_PLS;
|
||
|
||
setDatamode(HTTP_RESPONSE_HEADER); // Handle header
|
||
m_streamType = ST_WEBSTREAM;
|
||
m_contentlength = 0;
|
||
m_f_chunked = false;
|
||
|
||
if(hostwoext) {free(hostwoext); hostwoext = NULL;}
|
||
if(extension) {free(extension); extension = NULL;}
|
||
if(h_host ) {free(h_host); h_host = NULL;}
|
||
return true;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::setFileLoop(bool input){
|
||
m_f_loop = input;
|
||
return input;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::UTF8toASCII(char* str){
|
||
|
||
#ifdef SDFATFS_USED
|
||
//UTF8->UTF16 (lowbyte)
|
||
const uint8_t ascii[60] = {
|
||
//129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148 // UTF8(C3)
|
||
// Ä Å Æ Ç É Ñ // CHAR
|
||
000, 000, 000, 0xC4, 143, 0xC6,0xC7, 000,0xC9,000, 000, 000, 000, 000, 000, 000, 0xD1, 000, 000, 000, // ASCII (Latin1)
|
||
//149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168
|
||
// Ö Ü ß à ä å æ è
|
||
000, 0xD6,000, 000, 000, 000, 000, 0xDC, 000, 000, 0xDF,0xE0, 000, 000, 000,0xE4,0xE5,0xE6, 000,0xE8,
|
||
//169, 170, 171, 172. 173. 174. 175, 176, 177, 179, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188
|
||
// ê ë ì î ï ñ ò ô ö ù û ü
|
||
000, 0xEA, 0xEB,0xEC, 000,0xEE,0xEB, 000,0xF1,0xF2, 000,0xF4, 000,0xF6, 000, 000,0xF9, 000,0xFB,0xFC};
|
||
#else
|
||
const uint8_t ascii[60] = {
|
||
//129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148 // UTF8(C3)
|
||
// Ä Å Æ Ç É Ñ // CHAR
|
||
000, 000, 000, 142, 143, 146, 128, 000, 144, 000, 000, 000, 000, 000, 000, 000, 165, 000, 000, 000, // ASCII
|
||
//149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168
|
||
// Ö Ü ß à ä å æ è
|
||
000, 153, 000, 000, 000, 000, 000, 154, 000, 000, 225, 133, 000, 000, 000, 132, 134, 145, 000, 138,
|
||
//169, 170, 171, 172. 173. 174. 175, 176, 177, 179, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188
|
||
// ê ë ì î ï ñ ò ô ö ù û ü
|
||
000, 136, 137, 141, 000, 140, 139, 000, 164, 149, 000, 147, 000, 148, 000, 000, 151, 000, 150, 129};
|
||
#endif
|
||
|
||
uint16_t i = 0, j=0, s = 0;
|
||
bool f_C3_seen = false;
|
||
|
||
while(str[i] != 0) { // convert UTF8 to ASCII
|
||
if(str[i] == 195){ // C3
|
||
i++;
|
||
f_C3_seen = true;
|
||
continue;
|
||
}
|
||
str[j] = str[i];
|
||
if(str[j] > 128 && str[j] < 189 && f_C3_seen == true) {
|
||
s = ascii[str[j] - 129];
|
||
if(s != 0) str[j] = s; // found a related ASCII sign
|
||
f_C3_seen = false;
|
||
}
|
||
i++; j++;
|
||
}
|
||
str[j] = 0;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::connecttoSD(const char* path, uint32_t resumeFilePos) {
|
||
return connecttoFS(SD, path, resumeFilePos);
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::connecttoFS(fs::FS &fs, const char* path, uint32_t resumeFilePos) {
|
||
|
||
if(strlen(path)>255) return false;
|
||
|
||
m_resumeFilePos = resumeFilePos;
|
||
char audioName[256];
|
||
setDefaults(); // free buffers an set defaults
|
||
memcpy(audioName, path, strlen(path)+1);
|
||
if(audioName[0] != '/'){
|
||
for(int i = 255; i > 0; i--){
|
||
audioName[i] = audioName[i-1];
|
||
}
|
||
audioName[0] = '/';
|
||
}
|
||
|
||
AUDIO_INFO("Reading file: \"%s\"", audioName); vTaskDelay(2);
|
||
if(audio_beginSDread) audio_beginSDread();
|
||
cardLock(true);
|
||
if(fs.exists(audioName)) {
|
||
audiofile = fs.open(audioName); // #86
|
||
}
|
||
else {
|
||
UTF8toASCII(audioName);
|
||
if(fs.exists(audioName)) {
|
||
audiofile = fs.open(audioName);
|
||
}
|
||
}
|
||
cardLock(false);
|
||
if(!audiofile) {
|
||
if(audio_info) {vTaskDelay(2); audio_info("Failed to open file for reading");}
|
||
return false;
|
||
}
|
||
cardLock(true);
|
||
setDatamode(AUDIO_LOCALFILE);
|
||
m_file_size = audiofile.size();//TEST loop
|
||
cardLock(false);
|
||
char* afn = NULL; // audioFileName
|
||
cardLock(true);
|
||
#ifdef SDFATFS_USED
|
||
audiofile.getName(chbuf, sizeof(chbuf));
|
||
afn = strdup(chbuf);
|
||
#else
|
||
afn = strdup(audiofile.name());
|
||
#endif
|
||
cardLock(false);
|
||
uint8_t dotPos = lastIndexOf(afn, ".");
|
||
for(uint8_t i = dotPos + 1; i < strlen(afn); i++){
|
||
afn[i] = toLowerCase(afn[i]);
|
||
}
|
||
|
||
if(endsWith(afn, ".mp3")) m_codec = CODEC_MP3; // m_codec is by default CODEC_NONE
|
||
if(endsWith(afn, ".m4a")) m_codec = CODEC_M4A;
|
||
if(endsWith(afn, ".aac")) m_codec = CODEC_AAC;
|
||
if(endsWith(afn, ".wav")) m_codec = CODEC_WAV;
|
||
if(endsWith(afn, ".flac")) m_codec = CODEC_FLAC;
|
||
|
||
if(m_codec == CODEC_NONE) {
|
||
AUDIO_INFO("The %s format is not supported", afn + dotPos);
|
||
AUDIO_ERROR("The %s format is not supported", afn + dotPos);
|
||
}
|
||
if(afn) {free(afn); afn = NULL;}
|
||
|
||
bool ret = initializeDecoder();
|
||
if(ret) m_f_running = true;
|
||
else {
|
||
cardLock(true);audiofile.close();cardLock(false);
|
||
}
|
||
return ret;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::connecttospeech(const char* speech, const char* lang){
|
||
|
||
setDefaults();
|
||
char host[] = "translate.google.com.vn";
|
||
char path[] = "/translate_tts";
|
||
|
||
uint16_t speechLen = strlen(speech);
|
||
uint16_t speechBuffLen = speechLen + 300;
|
||
memcpy(m_lastHost, speech, 256);
|
||
char* speechBuff = (char*)malloc(speechBuffLen);
|
||
if(!speechBuff) {log_e("out of memory"); return false;}
|
||
memcpy(speechBuff, speech, speechLen);
|
||
speechBuff[speechLen] = '\0';
|
||
urlencode(speechBuff, speechBuffLen);
|
||
|
||
char resp[strlen(speechBuff) + 200] = "";
|
||
strcat(resp, "GET ");
|
||
strcat(resp, path);
|
||
strcat(resp, "?ie=UTF-8&tl=");
|
||
strcat(resp, lang);
|
||
strcat(resp, "&client=tw-ob&q=");
|
||
strcat(resp, speechBuff);
|
||
strcat(resp, " HTTP/1.1\r\n");
|
||
strcat(resp, "Host: ");
|
||
strcat(resp, host);
|
||
strcat(resp, "\r\n");
|
||
strcat(resp, "User-Agent: Mozilla/5.0 \r\n");
|
||
strcat(resp, "Accept-Encoding: identity\r\n");
|
||
strcat(resp, "Accept: text/html\r\n");
|
||
strcat(resp, "Connection: close\r\n\r\n");
|
||
|
||
if(speechBuff){free(speechBuff); speechBuff = NULL;}
|
||
_client = static_cast<WiFiClient*>(&client);
|
||
if(!_client->connect(host, 80)) {
|
||
log_e("Connection failed");
|
||
return false;
|
||
}
|
||
_client->print(resp);
|
||
|
||
m_streamType = ST_WEBSTREAM;
|
||
m_f_running = true;
|
||
m_f_ssl = false;
|
||
m_f_tts = true;
|
||
setDatamode(HTTP_RESPONSE_HEADER);
|
||
|
||
return true;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::connecttomarytts(const char* speech, const char* lang, const char* voice){
|
||
|
||
//lang: fr, te, ru, en_US, en_GB, sv, lb, tr, de, it
|
||
|
||
//voice: upmc-pierre-hsmm fr male hmm
|
||
// upmc-pierre fr male unitselection general
|
||
// upmc-jessica-hsmm fr female hmm
|
||
// upmc-jessica fr female unitselection general
|
||
// marylux lb female unitselection general
|
||
// istc-lucia-hsmm it female hmm
|
||
// enst-dennys-hsmm fr male hmm
|
||
// enst-camille-hsmm fr female hmm
|
||
// enst-camille fr female unitselection general
|
||
// dfki-spike-hsmm en_GB male hmm
|
||
// dfki-spike en_GB male unitselection general
|
||
// dfki-prudence-hsmm en_GB female hmm
|
||
// dfki-prudence en_GB female unitselection general
|
||
// dfki-poppy-hsmm en_GB female hmm
|
||
// dfki-poppy en_GB female unitselection general
|
||
// dfki-pavoque-styles de male unitselection general
|
||
// dfki-pavoque-neutral-hsmm de male hmm
|
||
// dfki-pavoque-neutral de male unitselection general
|
||
// dfki-ot-hsmm tr male hmm
|
||
// dfki-ot tr male unitselection general
|
||
// dfki-obadiah-hsmm en_GB male hmm
|
||
// dfki-obadiah en_GB male unitselection general
|
||
// cmu-slt-hsmm en_US female hmm
|
||
// cmu-slt en_US female unitselection general
|
||
// cmu-rms-hsmm en_US male hmm
|
||
// cmu-rms en_US male unitselection general
|
||
// cmu-nk-hsmm te female hmm
|
||
// cmu-bdl-hsmm en_US male hmm
|
||
// cmu-bdl en_US male unitselection general
|
||
// bits4 de female unitselection general
|
||
// bits3-hsmm de male hmm
|
||
// bits3 de male unitselection general
|
||
// bits2 de male unitselection general
|
||
// bits1-hsmm de female hmm
|
||
// bits1 de female unitselection general
|
||
|
||
setDefaults();
|
||
char host[] = "mary.dfki.de";
|
||
char path[] = "/process";
|
||
int port = 59125;
|
||
|
||
uint16_t speechLen = strlen(speech);
|
||
uint16_t speechBuffLen = speechLen + 300;
|
||
memcpy(m_lastHost, speech, 256);
|
||
char* speechBuff = (char*)malloc(speechBuffLen);
|
||
if(!speechBuff) {log_e("out of memory"); return false;}
|
||
memcpy(speechBuff, speech, speechLen);
|
||
speechBuff[speechLen] = '\0';
|
||
urlencode(speechBuff, speechBuffLen);
|
||
|
||
char resp[strlen(speechBuff) + 200] = "";
|
||
strcat(resp, "GET ");
|
||
strcat(resp, path);
|
||
strcat(resp, "?INPUT_TEXT=");
|
||
strcat(resp, speechBuff);
|
||
strcat(resp, "&INPUT_TYPE=TEXT");
|
||
strcat(resp, "&OUTPUT_TYPE=AUDIO");
|
||
strcat(resp, "&AUDIO=WAVE_FILE");
|
||
strcat(resp, "&LOCALE=");
|
||
strcat(resp, lang);
|
||
strcat(resp, "&VOICE=");
|
||
strcat(resp, voice);
|
||
strcat(resp, " HTTP/1.1\r\n");
|
||
strcat(resp, "Host: ");
|
||
strcat(resp, host);
|
||
strcat(resp, "\r\n");
|
||
strcat(resp, "User-Agent: Mozilla/5.0 \r\n");
|
||
strcat(resp, "Accept-Encoding: identity\r\n");
|
||
strcat(resp, "Accept: text/html\r\n");
|
||
strcat(resp, "Connection: close\r\n\r\n");
|
||
|
||
if(speechBuff){free(speechBuff); speechBuff = NULL;}
|
||
_client = static_cast<WiFiClient*>(&client);
|
||
if(!_client->connect(host, port)) {
|
||
log_e("Connection failed");
|
||
return false;
|
||
}
|
||
_client->print(resp);
|
||
|
||
m_streamType = ST_WEBSTREAM;
|
||
m_f_running = true;
|
||
m_f_ssl = false;
|
||
m_f_tts = true;
|
||
setDatamode(HTTP_RESPONSE_HEADER);
|
||
|
||
return true;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::urlencode(char* buff, uint16_t buffLen, bool spacesOnly) {
|
||
|
||
uint16_t len = strlen(buff);
|
||
uint8_t* tmpbuff = (uint8_t*)malloc(buffLen);
|
||
if(!tmpbuff) {log_e("out of memory"); return;}
|
||
char c;
|
||
char code0;
|
||
char code1;
|
||
uint16_t j = 0;
|
||
for(int i = 0; i < len; i++) {
|
||
c = buff[i];
|
||
if(isalnum(c)) tmpbuff[j++] = c;
|
||
else if(spacesOnly){
|
||
if(c == ' '){
|
||
tmpbuff[j++] = '%';
|
||
tmpbuff[j++] = '2';
|
||
tmpbuff[j++] = '0';
|
||
}
|
||
else{
|
||
tmpbuff[j++] = c;
|
||
}
|
||
}
|
||
else {
|
||
code1 = (c & 0xf) + '0';
|
||
if((c & 0xf) > 9) code1 = (c & 0xf) - 10 + 'A';
|
||
c = (c >> 4) & 0xf;
|
||
code0 = c + '0';
|
||
if(c > 9) code0 = c - 10 + 'A';
|
||
tmpbuff[j++] = '%';
|
||
tmpbuff[j++] = code0;
|
||
tmpbuff[j++] = code1;
|
||
}
|
||
if(j == buffLen - 1){
|
||
log_e("out of memory");
|
||
break;
|
||
}
|
||
}
|
||
memcpy(buff, tmpbuff, j);
|
||
buff[j] ='\0';
|
||
free(tmpbuff);
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::showID3Tag(const char* tag, const char* value){
|
||
|
||
chbuf[0] = 0;
|
||
// V2.2
|
||
if(!strcmp(tag, "CNT")) sprintf(chbuf, "Play counter: %s", value);
|
||
// if(!strcmp(tag, "COM")) sprintf(chbuf, "Comments: %s", value);
|
||
if(!strcmp(tag, "CRA")) sprintf(chbuf, "Audio encryption: %s", value);
|
||
if(!strcmp(tag, "CRM")) sprintf(chbuf, "Encrypted meta frame: %s", value);
|
||
if(!strcmp(tag, "ETC")) sprintf(chbuf, "Event timing codes: %s", value);
|
||
if(!strcmp(tag, "EQU")) sprintf(chbuf, "Equalization: %s", value);
|
||
if(!strcmp(tag, "IPL")) sprintf(chbuf, "Involved people list: %s", value);
|
||
if(!strcmp(tag, "PIC")) sprintf(chbuf, "Attached picture: %s", value);
|
||
if(!strcmp(tag, "SLT")) sprintf(chbuf, "Synchronized lyric/text: %s", value);
|
||
// if(!strcmp(tag, "TAL")) sprintf(chbuf, "Album/Movie/Show title: %s", value);
|
||
if(!strcmp(tag, "TBP")) sprintf(chbuf, "BPM (Beats Per Minute): %s", value);
|
||
if(!strcmp(tag, "TCM")) sprintf(chbuf, "Composer: %s", value);
|
||
if(!strcmp(tag, "TCO")) sprintf(chbuf, "Content type: %s", value);
|
||
if(!strcmp(tag, "TCR")) sprintf(chbuf, "Copyright message: %s", value);
|
||
if(!strcmp(tag, "TDA")) sprintf(chbuf, "Date: %s", value);
|
||
if(!strcmp(tag, "TDY")) sprintf(chbuf, "Playlist delay: %s", value);
|
||
if(!strcmp(tag, "TEN")) sprintf(chbuf, "Encoded by: %s", value);
|
||
if(!strcmp(tag, "TFT")) sprintf(chbuf, "File type: %s", value);
|
||
if(!strcmp(tag, "TIM")) sprintf(chbuf, "Time: %s", value);
|
||
if(!strcmp(tag, "TKE")) sprintf(chbuf, "Initial key: %s", value);
|
||
if(!strcmp(tag, "TLA")) sprintf(chbuf, "Language(s): %s", value);
|
||
if(!strcmp(tag, "TLE")) sprintf(chbuf, "Length: %s", value);
|
||
if(!strcmp(tag, "TMT")) sprintf(chbuf, "Media type: %s", value);
|
||
if(!strcmp(tag, "TOA")) sprintf(chbuf, "Original artist(s)/performer(s): %s", value);
|
||
if(!strcmp(tag, "TOF")) sprintf(chbuf, "Original filename: %s", value);
|
||
if(!strcmp(tag, "TOL")) sprintf(chbuf, "Original Lyricist(s)/text writer(s): %s", value);
|
||
if(!strcmp(tag, "TOR")) sprintf(chbuf, "Original release year: %s", value);
|
||
if(!strcmp(tag, "TOT")) sprintf(chbuf, "Original album/Movie/Show title: %s", value);
|
||
if(!strcmp(tag, "TP1")) sprintf(chbuf, "Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group: %s", value);
|
||
if(!strcmp(tag, "TP2")) sprintf(chbuf, "Band/Orchestra/Accompaniment: %s", value);
|
||
if(!strcmp(tag, "TP3")) sprintf(chbuf, "Conductor/Performer refinement: %s", value);
|
||
if(!strcmp(tag, "TP4")) sprintf(chbuf, "Interpreted, remixed, or otherwise modified by: %s", value);
|
||
if(!strcmp(tag, "TPA")) sprintf(chbuf, "Part of a set: %s", value);
|
||
if(!strcmp(tag, "TPB")) sprintf(chbuf, "Publisher: %s", value);
|
||
if(!strcmp(tag, "TRC")) sprintf(chbuf, "ISRC (International Standard Recording Code): %s", value);
|
||
if(!strcmp(tag, "TRD")) sprintf(chbuf, "Recording dates: %s", value);
|
||
if(!strcmp(tag, "TRK")) sprintf(chbuf, "Track number/Position in set: %s", value);
|
||
if(!strcmp(tag, "TSI")) sprintf(chbuf, "Size: %s", value);
|
||
if(!strcmp(tag, "TSS")) sprintf(chbuf, "Software/hardware and settings used for encoding: %s", value);
|
||
if(!strcmp(tag, "TT1")) sprintf(chbuf, "Content group description: %s", value);
|
||
if(!strcmp(tag, "TT2")) sprintf(chbuf, "Title/Songname/Content description: %s", value);
|
||
if(!strcmp(tag, "TT3")) sprintf(chbuf, "Subtitle/Description refinement: %s", value);
|
||
if(!strcmp(tag, "TXT")) sprintf(chbuf, "Lyricist/text writer: %s", value);
|
||
if(!strcmp(tag, "TXX")) sprintf(chbuf, "User defined text information frame: %s", value);
|
||
if(!strcmp(tag, "TYE")) sprintf(chbuf, "Year: %s", value);
|
||
if(!strcmp(tag, "UFI")) sprintf(chbuf, "Unique file identifier: %s", value);
|
||
if(!strcmp(tag, "ULT")) sprintf(chbuf, "Unsychronized lyric/text transcription: %s", value);
|
||
if(!strcmp(tag, "WAF")) sprintf(chbuf, "Official audio file webpage: %s", value);
|
||
if(!strcmp(tag, "WAR")) sprintf(chbuf, "Official artist/performer webpage: %s", value);
|
||
if(!strcmp(tag, "WAS")) sprintf(chbuf, "Official audio source webpage: %s", value);
|
||
if(!strcmp(tag, "WCM")) sprintf(chbuf, "Commercial information: %s", value);
|
||
if(!strcmp(tag, "WCP")) sprintf(chbuf, "Copyright/Legal information: %s", value);
|
||
if(!strcmp(tag, "WPB")) sprintf(chbuf, "Publishers official webpage: %s", value);
|
||
if(!strcmp(tag, "WXX")) sprintf(chbuf, "User defined URL link frame: %s", value);
|
||
|
||
// V2.3 V2.4 tags
|
||
// if(!strcmp(tag, "COMM")) sprintf(chbuf, "Comment: %s", value);
|
||
if(!strcmp(tag, "OWNE")) sprintf(chbuf, "Ownership: %s", value);
|
||
// if(!strcmp(tag, "PRIV")) sprintf(chbuf, "Private: %s", value);
|
||
if(!strcmp(tag, "SYLT")) sprintf(chbuf, "SynLyrics: %s", value);
|
||
if(!strcmp(tag, "TALB")) { sprintf(chbuf, "Album: %s", value); if(audio_id3album) audio_id3album(value); }
|
||
if(!strcmp(tag, "TBPM")) sprintf(chbuf, "BeatsPerMinute: %s", value);
|
||
if(!strcmp(tag, "TCMP")) sprintf(chbuf, "Compilation: %s", value);
|
||
if(!strcmp(tag, "TCOM")) sprintf(chbuf, "Composer: %s", value);
|
||
if(!strcmp(tag, "TCON")) sprintf(chbuf, "ContentType: %s", value);
|
||
if(!strcmp(tag, "TCOP")) sprintf(chbuf, "Copyright: %s", value);
|
||
if(!strcmp(tag, "TDAT")) sprintf(chbuf, "Date: %s", value);
|
||
if(!strcmp(tag, "TEXT")) sprintf(chbuf, "Lyricist: %s", value);
|
||
if(!strcmp(tag, "TIME")) sprintf(chbuf, "Time: %s", value);
|
||
if(!strcmp(tag, "TIT1")) sprintf(chbuf, "Grouping: %s", value);
|
||
if(!strcmp(tag, "TIT2")) { sprintf(chbuf, "Title: %s", value); if(audio_id3album) audio_id3album(value); }
|
||
if(!strcmp(tag, "TIT3")) sprintf(chbuf, "Subtitle: %s", value);
|
||
if(!strcmp(tag, "TLAN")) sprintf(chbuf, "Language: %s", value);
|
||
if(!strcmp(tag, "TLEN")) sprintf(chbuf, "Length (ms): %s", value);
|
||
if(!strcmp(tag, "TMED")) sprintf(chbuf, "Media: %s", value);
|
||
if(!strcmp(tag, "TOAL")) sprintf(chbuf, "OriginalAlbum: %s", value);
|
||
if(!strcmp(tag, "TOPE")) sprintf(chbuf, "OriginalArtist: %s", value);
|
||
if(!strcmp(tag, "TORY")) sprintf(chbuf, "OriginalReleaseYear: %s", value);
|
||
if(!strcmp(tag, "TPE1")) { sprintf(chbuf, "Artist: %s", value); if(audio_id3artist) audio_id3artist(value); }
|
||
if(!strcmp(tag, "TPE2")) sprintf(chbuf, "Band: %s", value);
|
||
if(!strcmp(tag, "TPE3")) sprintf(chbuf, "Conductor: %s", value);
|
||
if(!strcmp(tag, "TPE4")) sprintf(chbuf, "InterpretedBy: %s", value);
|
||
if(!strcmp(tag, "TPOS")) sprintf(chbuf, "PartOfSet: %s", value);
|
||
if(!strcmp(tag, "TPUB")) sprintf(chbuf, "Publisher: %s", value);
|
||
if(!strcmp(tag, "TRCK")) sprintf(chbuf, "Track: %s", value);
|
||
if(!strcmp(tag, "TSSE")) sprintf(chbuf, "SettingsForEncoding: %s", value);
|
||
if(!strcmp(tag, "TRDA")) sprintf(chbuf, "RecordingDates: %s", value);
|
||
if(!strcmp(tag, "TXXX")) sprintf(chbuf, "UserDefinedText: %s", value);
|
||
if(!strcmp(tag, "TYER")) sprintf(chbuf, "Year: %s", value);
|
||
if(!strcmp(tag, "USER")) sprintf(chbuf, "TermsOfUse: %s", value);
|
||
if(!strcmp(tag, "USLT")) sprintf(chbuf, "Lyrics: %s", value);
|
||
if(!strcmp(tag, "WOAR")) sprintf(chbuf, "OfficialArtistWebpage: %s", value);
|
||
if(!strcmp(tag, "XDOR")) sprintf(chbuf, "OriginalReleaseTime: %s", value);
|
||
|
||
latinToUTF8(chbuf, sizeof(chbuf));
|
||
if(chbuf[0] != 0) if(audio_id3data) audio_id3data(chbuf);
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::unicode2utf8(char* buff, uint32_t len){
|
||
// converts unicode in UTF-8, buff contains the string to be converted up to len
|
||
// range U+1 ... U+FFFF
|
||
uint8_t* tmpbuff = (uint8_t*)malloc(len * 2);
|
||
if(!tmpbuff) {log_e("out of memory"); return;}
|
||
bool bitorder = false;
|
||
uint16_t j = 0;
|
||
uint16_t k = 0;
|
||
uint16_t m = 0;
|
||
uint8_t uni_h = 0;
|
||
uint8_t uni_l = 0;
|
||
|
||
while(m < len - 1) {
|
||
if((buff[m] == 0xFE) && (buff[m + 1] == 0xFF)) {
|
||
bitorder = true;
|
||
j = m + 2;
|
||
} // LSB/MSB
|
||
if((buff[m] == 0xFF) && (buff[m + 1] == 0xFE)) {
|
||
bitorder = false;
|
||
j = m + 2;
|
||
} // MSB/LSB
|
||
m++;
|
||
} // seek for last bitorder
|
||
m = 0;
|
||
if(j > 0) {
|
||
for(k = j; k < len; k += 2) {
|
||
if(bitorder == true) {
|
||
uni_h = (uint8_t)buff[k];
|
||
uni_l = (uint8_t)buff[k + 1];
|
||
}
|
||
else {
|
||
uni_l = (uint8_t)buff[k];
|
||
uni_h = (uint8_t)buff[k + 1];
|
||
}
|
||
|
||
uint16_t uni_hl = ((uni_h << 8) | uni_l);
|
||
|
||
if (uni_hl < 0X80){
|
||
tmpbuff[m] = uni_l;
|
||
m++;
|
||
}
|
||
else if (uni_hl < 0X800) {
|
||
tmpbuff[m]= ((uni_hl >> 6) | 0XC0);
|
||
m++;
|
||
tmpbuff[m] =((uni_hl & 0X3F) | 0X80);
|
||
m++;
|
||
}
|
||
else {
|
||
tmpbuff[m] = ((uni_hl >> 12) | 0XE0);
|
||
m++;
|
||
tmpbuff[m] = (((uni_hl >> 6) & 0X3F) | 0X80);
|
||
m++;
|
||
tmpbuff[m] = ((uni_hl & 0X3F) | 0X80);
|
||
m++;
|
||
}
|
||
}
|
||
}
|
||
buff[m] = 0;
|
||
memcpy(buff, tmpbuff, m);
|
||
if(tmpbuff){free(tmpbuff); tmpbuff = NULL;}
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::latinToUTF8(char* buff, size_t bufflen){
|
||
// most stations send strings in UTF-8 but a few sends in latin. To standardize this, all latin strings are
|
||
// converted to UTF-8. If UTF-8 is already present, nothing is done and true is returned.
|
||
// A conversion to UTF-8 extends the string. Therefore it is necessary to know the buffer size. If the converted
|
||
// string does not fit into the buffer, false is returned
|
||
// utf8 bytelength: >=0xF0 3 bytes, >=0xE0 2 bytes, >=0xC0 1 byte, e.g. e293ab is ⓫
|
||
|
||
uint16_t pos = 0;
|
||
uint8_t ext_bytes = 0;
|
||
uint16_t len = strlen(buff);
|
||
uint8_t c;
|
||
|
||
while(pos < len){
|
||
c = buff[pos];
|
||
if(c >= 0xC2) { // is UTF8 char
|
||
pos++;
|
||
if(c >= 0xC0 && buff[pos] < 0x80) {ext_bytes++; pos++;}
|
||
if(c >= 0xE0 && buff[pos] < 0x80) {ext_bytes++; pos++;}
|
||
if(c >= 0xF0 && buff[pos] < 0x80) {ext_bytes++; pos++;}
|
||
}
|
||
else pos++;
|
||
}
|
||
if(!ext_bytes) return true; // is UTF-8, do nothing
|
||
|
||
pos = 0;
|
||
|
||
while(buff[pos] != 0){
|
||
len = strlen(buff);
|
||
if(buff[pos] >= 0x80 && buff[pos+1] < 0x80){ // is not UTF8, is latin?
|
||
for(int i = len+1; i > pos; i--){
|
||
buff[i+1] = buff[i];
|
||
}
|
||
uint8_t c = buff[pos];
|
||
buff[pos++] = 0xc0 | ((c >> 6) & 0x1f); // 2+1+5 bits
|
||
buff[pos++] = 0x80 | ((char)c & 0x3f); // 1+1+6 bits
|
||
}
|
||
pos++;
|
||
if(pos > bufflen -3){
|
||
buff[bufflen -1] = '\0';
|
||
return false; // do not overwrite
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
size_t Audio::readAudioHeader(uint32_t bytes){
|
||
size_t bytesReaded = 0;
|
||
if(m_codec == CODEC_WAV){
|
||
int res = read_WAV_Header(InBuff.getReadPtr(), bytes);
|
||
if(res >= 0) bytesReaded = res;
|
||
else{ // error, skip header
|
||
m_controlCounter = 100;
|
||
eofHeader = true;
|
||
}
|
||
}
|
||
if(m_codec == CODEC_MP3){
|
||
int res = read_ID3_Header(InBuff.getReadPtr(), bytes);
|
||
if(res >= 0) bytesReaded = res;
|
||
else{ // error, skip header
|
||
m_controlCounter = 100;
|
||
eofHeader = true;
|
||
}
|
||
}
|
||
if(m_codec == CODEC_M4A){
|
||
int res = read_M4A_Header(InBuff.getReadPtr(), bytes);
|
||
if(res >= 0) bytesReaded = res;
|
||
else{ // error, skip header
|
||
m_controlCounter = 100;
|
||
eofHeader = true;
|
||
}
|
||
}
|
||
if(m_codec == CODEC_AAC){
|
||
// stream only, no header
|
||
m_audioDataSize = getFileSize();
|
||
m_controlCounter = 100;
|
||
eofHeader = true;
|
||
}
|
||
if(m_codec == CODEC_FLAC){
|
||
int res = read_FLAC_Header(InBuff.getReadPtr(), bytes);
|
||
if(res >= 0) bytesReaded = res;
|
||
else{ // error, skip header
|
||
stopSong();
|
||
m_controlCounter = 100;
|
||
eofHeader = true;
|
||
}
|
||
}
|
||
if(!isRunning()){
|
||
log_e("Processing stopped due to invalid audio header");
|
||
return 0;
|
||
}
|
||
return bytesReaded;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
int Audio::read_WAV_Header(uint8_t* data, size_t len) {
|
||
static size_t headerSize;
|
||
static uint32_t cs = 0;
|
||
static uint8_t bts = 0;
|
||
|
||
if(m_controlCounter == 0){
|
||
m_controlCounter ++;
|
||
if((*data != 'R') || (*(data + 1) != 'I') || (*(data + 2) != 'F') || (*(data + 3) != 'F')) {
|
||
AUDIO_INFO("file has no RIFF tag");
|
||
headerSize = 0;
|
||
return -1; //false;
|
||
}
|
||
else{
|
||
headerSize = 4;
|
||
return 4; // ok
|
||
}
|
||
}
|
||
|
||
if(m_controlCounter == 1){
|
||
m_controlCounter ++;
|
||
cs = (uint32_t) (*data + (*(data + 1) << 8) + (*(data + 2) << 16) + (*(data + 3) << 24) - 8);
|
||
headerSize += 4;
|
||
return 4; // ok
|
||
}
|
||
|
||
if(m_controlCounter == 2){
|
||
m_controlCounter ++;
|
||
if((*data != 'W') || (*(data + 1) != 'A') || (*(data + 2) != 'V') || (*(data + 3) != 'E')) {
|
||
AUDIO_INFO("format tag is not WAVE");
|
||
return -1;//false;
|
||
}
|
||
else {
|
||
headerSize += 4;
|
||
return 4;
|
||
}
|
||
}
|
||
|
||
if(m_controlCounter == 3){
|
||
if((*data == 'f') && (*(data + 1) == 'm') && (*(data + 2) == 't')) {
|
||
m_controlCounter ++;
|
||
headerSize += 4;
|
||
return 4;
|
||
}
|
||
else{
|
||
headerSize += 4;
|
||
return 4;
|
||
}
|
||
}
|
||
|
||
if(m_controlCounter == 4){
|
||
m_controlCounter ++;
|
||
cs = (uint32_t) (*data + (*(data + 1) << 8));
|
||
if(cs > 40) return -1; //false, something going wrong
|
||
bts = cs - 16; // bytes to skip if fmt chunk is >16
|
||
headerSize += 4;
|
||
return 4;
|
||
}
|
||
|
||
if(m_controlCounter == 5){
|
||
m_controlCounter ++;
|
||
uint16_t fc = (uint16_t) (*(data + 0) + (*(data + 1) << 8)); // Format code
|
||
uint16_t nic = (uint16_t) (*(data + 2) + (*(data + 3) << 8)); // Number of interleaved channels
|
||
uint32_t sr = (uint32_t) (*(data + 4) + (*(data + 5) << 8) +
|
||
(*(data + 6) << 16) + (*(data + 7) << 24)); // Samplerate
|
||
uint32_t dr = (uint32_t) (*(data + 8) + (*(data + 9) << 8) +
|
||
(*(data + 10) << 16) + (*(data + 11) << 24)); // Datarate
|
||
uint16_t dbs = (uint16_t) (*(data + 12) + (*(data + 13) << 8)); // Data block size
|
||
uint16_t bps = (uint16_t) (*(data + 14) + (*(data + 15) << 8)); // Bits per sample
|
||
|
||
AUDIO_INFO("FormatCode: %u", fc);
|
||
// AUDIO_INFO("Channel: %u", nic);
|
||
// AUDIO_INFO("SampleRate: %u", sr);
|
||
AUDIO_INFO("DataRate: %u", dr);
|
||
AUDIO_INFO("DataBlockSize: %u", dbs);
|
||
AUDIO_INFO("BitsPerSample: %u", bps);
|
||
|
||
if((bps != 8) && (bps != 16)){
|
||
AUDIO_INFO("BitsPerSample is %u, must be 8 or 16" , bps);
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
if((nic != 1) && (nic != 2)){
|
||
AUDIO_INFO("num channels is %u, must be 1 or 2" , nic); audio_info(chbuf);
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
if(fc != 1) {
|
||
AUDIO_INFO("format code is not 1 (PCM)");
|
||
stopSong();
|
||
return -1 ; //false;
|
||
}
|
||
setBitsPerSample(bps);
|
||
setChannels(nic);
|
||
setSampleRate(sr);
|
||
setBitrate(nic * sr * bps);
|
||
// AUDIO_INFO("BitRate: %u", m_bitRate);
|
||
headerSize += 16;
|
||
return 16; // ok
|
||
}
|
||
|
||
if(m_controlCounter == 6){
|
||
m_controlCounter ++;
|
||
headerSize += bts;
|
||
return bts; // skip to data
|
||
}
|
||
|
||
if(m_controlCounter == 7){
|
||
if((*(data + 0) == 'd') && (*(data + 1) == 'a') && (*(data + 2) == 't') && (*(data + 3) == 'a')){
|
||
m_controlCounter ++;
|
||
vTaskDelay(30);
|
||
headerSize += 4;
|
||
return 4;
|
||
}
|
||
else{
|
||
headerSize ++;
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
if(m_controlCounter == 8){
|
||
m_controlCounter ++;
|
||
size_t cs = *(data + 0) + (*(data + 1) << 8) + (*(data + 2) << 16) + (*(data + 3) << 24); //read chunkSize
|
||
headerSize += 4;
|
||
if(getDatamode() == AUDIO_LOCALFILE) m_contentlength = getFileSize();
|
||
if(cs){
|
||
m_audioDataSize = cs - 44;
|
||
}
|
||
else { // sometimes there is nothing here
|
||
if(getDatamode() == AUDIO_LOCALFILE) m_audioDataSize = getFileSize() - headerSize;
|
||
if(m_streamType == ST_WEBFILE) m_audioDataSize = m_contentlength - headerSize;
|
||
}
|
||
AUDIO_INFO("Audio-Length: %u", m_audioDataSize);
|
||
if(audio_progress) audio_progress(headerSize, m_audioDataSize);
|
||
return 4;
|
||
}
|
||
m_controlCounter = 100; // header succesfully read
|
||
eofHeader = true;
|
||
m_audioDataStart = headerSize;
|
||
return 0;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
int Audio::read_FLAC_Header(uint8_t *data, size_t len) {
|
||
static size_t headerSize;
|
||
static size_t retvalue;
|
||
static bool f_lastMetaBlock;
|
||
|
||
if(retvalue) {
|
||
if(retvalue > len) { // if returnvalue > bufferfillsize
|
||
if(len > InBuff.getMaxBlockSize()) len = InBuff.getMaxBlockSize();
|
||
retvalue -= len; // and wait for more bufferdata
|
||
return len;
|
||
}
|
||
else {
|
||
size_t tmp = retvalue;
|
||
retvalue = 0;
|
||
return tmp;
|
||
}
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == FLAC_BEGIN) { // init
|
||
headerSize = 0;
|
||
retvalue = 0;
|
||
m_audioDataStart = 0;
|
||
f_lastMetaBlock = false;
|
||
m_controlCounter = FLAC_MAGIC;
|
||
if(getDatamode() == AUDIO_LOCALFILE){
|
||
m_contentlength = getFileSize();
|
||
AUDIO_INFO("Content-Length: %u", m_contentlength);
|
||
}
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == FLAC_MAGIC) { /* check MAGIC STRING */
|
||
if(specialIndexOf(data, "fLaC", 10) != 0) {
|
||
log_e("Magic String 'fLaC' not found in header");
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
m_controlCounter = FLAC_MBH;
|
||
headerSize = 4;
|
||
retvalue = 4;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == FLAC_MBH) { /* METADATA_BLOCK_HEADER */
|
||
uint8_t blockType = *data;
|
||
if(!f_lastMetaBlock){
|
||
if(blockType & 128) {f_lastMetaBlock = true;}
|
||
blockType &= 127;
|
||
if(blockType == 0) m_controlCounter = FLAC_SINFO;
|
||
if(blockType == 1) m_controlCounter = FLAC_PADDING;
|
||
if(blockType == 2) m_controlCounter = FLAC_APP;
|
||
if(blockType == 3) m_controlCounter = FLAC_SEEK;
|
||
if(blockType == 4) m_controlCounter = FLAC_VORBIS;
|
||
if(blockType == 5) m_controlCounter = FLAC_CUESHEET;
|
||
if(blockType == 6) m_controlCounter = FLAC_PICTURE;
|
||
headerSize += 1;
|
||
retvalue = 1;
|
||
return 0;
|
||
}
|
||
m_controlCounter = FLAC_OKAY;
|
||
eofHeader = true;
|
||
m_audioDataStart = headerSize;
|
||
m_audioDataSize = m_contentlength - m_audioDataStart;
|
||
AUDIO_INFO("Audio-Length: %u", m_audioDataSize);
|
||
if(audio_progress) audio_progress(m_audioDataStart, m_audioDataSize);
|
||
retvalue = 0;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == FLAC_SINFO) { /* Stream info block */
|
||
size_t l = bigEndian(data, 3);
|
||
vTaskDelay(2);
|
||
m_flacMaxBlockSize = bigEndian(data + 5, 2);
|
||
AUDIO_INFO("FLAC maxBlockSize: %u", m_flacMaxBlockSize);
|
||
vTaskDelay(2);
|
||
m_flacMaxFrameSize = bigEndian(data + 10, 3);
|
||
if(m_flacMaxFrameSize){
|
||
AUDIO_INFO("FLAC maxFrameSize: %u", m_flacMaxFrameSize);
|
||
}
|
||
else {
|
||
AUDIO_INFO("FLAC maxFrameSize: N/A");
|
||
}
|
||
if(m_flacMaxFrameSize > InBuff.getMaxBlockSize()) {
|
||
log_e("FLAC maxFrameSize too large!");
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
// InBuff.changeMaxBlockSize(m_flacMaxFrameSize);
|
||
vTaskDelay(2);
|
||
uint32_t nextval = bigEndian(data + 13, 3);
|
||
m_flacSampleRate = nextval >> 4;
|
||
AUDIO_INFO("FLAC sampleRate: %u", m_flacSampleRate);
|
||
vTaskDelay(2);
|
||
m_flacNumChannels = ((nextval & 0x06) >> 1) + 1;
|
||
AUDIO_INFO("FLAC numChannels: %u", m_flacNumChannels);
|
||
vTaskDelay(2);
|
||
uint8_t bps = (nextval & 0x01) << 4;
|
||
bps += (*(data +16) >> 4) + 1;
|
||
m_flacBitsPerSample = bps;
|
||
if((bps != 8) && (bps != 16)){
|
||
log_e("bits per sample must be 8 or 16, is %i", bps);
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
AUDIO_INFO("FLAC bitsPerSample: %u", m_flacBitsPerSample);
|
||
m_flacTotalSamplesInStream = bigEndian(data + 17, 4);
|
||
if(m_flacTotalSamplesInStream){
|
||
AUDIO_INFO("total samples in stream: %u", m_flacTotalSamplesInStream);
|
||
}
|
||
else{
|
||
AUDIO_INFO("total samples in stream: N/A");
|
||
}
|
||
if(bps != 0 && m_flacTotalSamplesInStream) {
|
||
AUDIO_INFO("audio file duration: %u seconds", m_flacTotalSamplesInStream / m_flacSampleRate);
|
||
}
|
||
m_controlCounter = FLAC_MBH; // METADATA_BLOCK_HEADER
|
||
retvalue = l + 3;
|
||
headerSize += retvalue;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == FLAC_PADDING) { /* PADDING */
|
||
size_t l = bigEndian(data, 3);
|
||
m_controlCounter = FLAC_MBH;
|
||
retvalue = l + 3;
|
||
headerSize += retvalue;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == FLAC_APP) { /* APPLICATION */
|
||
size_t l = bigEndian(data, 3);
|
||
m_controlCounter = FLAC_MBH;
|
||
retvalue = l + 3;
|
||
headerSize += retvalue;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == FLAC_SEEK) { /* SEEKTABLE */
|
||
size_t l = bigEndian(data, 3);
|
||
m_controlCounter = FLAC_MBH;
|
||
retvalue = l + 3;
|
||
headerSize += retvalue;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == FLAC_VORBIS) { /* VORBIS COMMENT */ // field names
|
||
const char fn[7][12] = {"TITLE", "VERSION", "ALBUM", "TRACKNUMBER", "ARTIST", "PERFORMER", "GENRE"};
|
||
int offset;
|
||
size_t l = bigEndian(data, 3);
|
||
|
||
for(int i = 0; i < 7; i++){
|
||
offset = specialIndexOf(data, fn[i], len);
|
||
if(offset >= 0){
|
||
sprintf(chbuf, "%s: %s", fn[i], data + offset + strlen(fn[i]) + 1);
|
||
chbuf[strlen(chbuf) - 1] = 0;
|
||
for(int i=0; i<strlen(chbuf);i++){
|
||
if(chbuf[i] == 255) chbuf[i] = 0;
|
||
}
|
||
if(audio_id3data) audio_id3data(chbuf);
|
||
}
|
||
}
|
||
m_controlCounter = FLAC_MBH;
|
||
retvalue = l + 3;
|
||
headerSize += retvalue;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == FLAC_CUESHEET) { /* CUESHEET */
|
||
size_t l = bigEndian(data, 3);
|
||
m_controlCounter = FLAC_MBH;
|
||
retvalue = l + 3;
|
||
headerSize += retvalue;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == FLAC_PICTURE) { /* PICTURE */
|
||
size_t l = bigEndian(data, 3);
|
||
m_controlCounter = FLAC_MBH;
|
||
retvalue = l + 3;
|
||
headerSize += retvalue;
|
||
return 0;
|
||
}
|
||
return 0;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
int Audio::read_ID3_Header(uint8_t *data, size_t len) {
|
||
|
||
static size_t id3Size;
|
||
static size_t headerSize;
|
||
static uint8_t ID3version;
|
||
static int ehsz = 0;
|
||
static char tag[5];
|
||
static char frameid[5];
|
||
static size_t framesize = 0;
|
||
static bool compressed = false;
|
||
static bool APIC_seen = false;
|
||
static size_t APIC_size = 0;
|
||
static uint32_t APIC_pos = 0;
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == 0){ /* read ID3 tag and ID3 header size */
|
||
if(getDatamode() == AUDIO_LOCALFILE){
|
||
ID3version = 0;
|
||
m_contentlength = getFileSize();
|
||
AUDIO_INFO("Content-Length: %u", m_contentlength);
|
||
}
|
||
m_controlCounter ++;
|
||
APIC_seen = false;
|
||
headerSize = 0;
|
||
ehsz = 0;
|
||
if(specialIndexOf(data, "ID3", 4) != 0) { // ID3 not found
|
||
AUDIO_INFO("file has no mp3 tag, skip metadata");
|
||
m_audioDataSize = m_contentlength;
|
||
AUDIO_INFO("Audio-Length: %u", m_audioDataSize);
|
||
if(audio_progress) audio_progress(295903, m_audioDataSize);
|
||
return -1; // error, no ID3 signature found
|
||
}
|
||
ID3version = *(data + 3);
|
||
switch(ID3version){
|
||
case 2:
|
||
m_f_unsync = (*(data + 5) & 0x80);
|
||
m_f_exthdr = false;
|
||
break;
|
||
case 3:
|
||
case 4:
|
||
m_f_unsync = (*(data + 5) & 0x80); // bit7
|
||
m_f_exthdr = (*(data + 5) & 0x40); // bit6 extended header
|
||
break;
|
||
};
|
||
id3Size = bigEndian(data + 6, 4, 7); // ID3v2 size 4 * %0xxxxxxx (shift left seven times!!)
|
||
id3Size += 10;
|
||
|
||
// Every read from now may be unsync'd
|
||
AUDIO_INFO("ID3 framesSize: %i", id3Size);
|
||
|
||
AUDIO_INFO("ID3 version: 2.%i", ID3version);
|
||
|
||
if(ID3version == 2){
|
||
m_controlCounter = 10;
|
||
}
|
||
headerSize = id3Size;
|
||
m_ID3Size = id3Size;
|
||
headerSize -= 10;
|
||
|
||
return 10;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == 1){ // compute extended header size if exists
|
||
m_controlCounter ++;
|
||
if(m_f_exthdr) {
|
||
AUDIO_INFO("ID3 extended header");
|
||
ehsz = bigEndian(data, 4);
|
||
headerSize -= 4;
|
||
ehsz -= 4;
|
||
return 4;
|
||
}
|
||
else{
|
||
AUDIO_INFO("ID3 normal frames");
|
||
return 0;
|
||
}
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == 2){ // skip extended header if exists
|
||
if(ehsz > 256) {
|
||
ehsz -=256;
|
||
headerSize -= 256;
|
||
return 256;} // Throw it away
|
||
else {
|
||
m_controlCounter ++;
|
||
headerSize -= ehsz;
|
||
return ehsz;} // Throw it away
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == 3){ // read a ID3 frame, get the tag
|
||
if(headerSize == 0){
|
||
m_controlCounter = 99;
|
||
return 0;
|
||
}
|
||
m_controlCounter ++;
|
||
frameid[0] = *(data + 0);
|
||
frameid[1] = *(data + 1);
|
||
frameid[2] = *(data + 2);
|
||
frameid[3] = *(data + 3);
|
||
frameid[4] = 0;
|
||
for(uint8_t i = 0; i < 4; i++) tag[i] = frameid[i]; // tag = frameid
|
||
|
||
headerSize -= 4;
|
||
if(frameid[0] == 0 && frameid[1] == 0 && frameid[2] == 0 && frameid[3] == 0) {
|
||
// We're in padding
|
||
m_controlCounter = 98; // all ID3 metadata processed
|
||
}
|
||
return 4;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == 4){ // get the frame size
|
||
m_controlCounter = 6;
|
||
|
||
if(ID3version == 4){
|
||
framesize = bigEndian(data, 4, 7); // << 7
|
||
}
|
||
else {
|
||
framesize = bigEndian(data, 4); // << 8
|
||
}
|
||
headerSize -= 4;
|
||
uint8_t flag = *(data + 4); // skip 1st flag
|
||
(void) flag;
|
||
headerSize--;
|
||
compressed = (*(data + 5)) & 0x80; // Frame is compressed using [#ZLIB zlib] with 4 bytes for 'decompressed
|
||
// size' appended to the frame header.
|
||
headerSize--;
|
||
uint32_t decompsize = 0;
|
||
if(compressed){
|
||
if(m_f_Log) log_i("iscompressed");
|
||
decompsize = bigEndian(data + 6, 4);
|
||
headerSize -= 4;
|
||
(void) decompsize;
|
||
if(m_f_Log) log_i("decompsize=%u", decompsize);
|
||
return 6 + 4;
|
||
}
|
||
return 6;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == 5){ // If the frame is larger than 256 bytes, skip the rest
|
||
if(framesize > 256){
|
||
framesize -= 256;
|
||
headerSize -= 256;
|
||
return 256;
|
||
}
|
||
else {
|
||
m_controlCounter = 3; // check next frame
|
||
headerSize -= framesize;
|
||
return framesize;
|
||
}
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == 6){ // Read the value
|
||
m_controlCounter = 5; // only read 256 bytes
|
||
char value[256];
|
||
char ch = *(data + 0);
|
||
bool isUnicode = (ch==1) ? true : false;
|
||
|
||
if(startsWith(tag, "APIC")) { // a image embedded in file, passing it to external function
|
||
isUnicode = false;
|
||
if(getDatamode() == AUDIO_LOCALFILE){
|
||
APIC_seen = true;
|
||
APIC_pos = id3Size - headerSize;
|
||
APIC_size = framesize;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
size_t fs = framesize;
|
||
if(fs >255) fs = 255;
|
||
for(int i=0; i<fs; i++){
|
||
value[i] = *(data + i);
|
||
}
|
||
framesize -= fs;
|
||
headerSize -= fs;
|
||
value[fs] = 0;
|
||
if(isUnicode && fs > 1) {
|
||
unicode2utf8(value, fs); // convert unicode to utf-8 U+0020...U+07FF
|
||
}
|
||
if(!isUnicode){
|
||
uint16_t j = 0, k = 0;
|
||
j = 0;
|
||
k = 0;
|
||
while(j < fs) {
|
||
if(value[j] == 0x0A) value[j] = 0x20; // replace LF by space
|
||
if(value[j] > 0x1F) {
|
||
value[k] = value[j];
|
||
k++;
|
||
}
|
||
j++;
|
||
} //remove non printables
|
||
if(k>0) value[k] = 0; else value[0] = 0; // new termination
|
||
}
|
||
showID3Tag(tag, value);
|
||
return fs;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
|
||
// -- section V2.2 only , greater Vers above ----
|
||
if(m_controlCounter == 10){ // frames in V2.2, 3bytes identifier, 3bytes size descriptor
|
||
frameid[0] = *(data + 0);
|
||
frameid[1] = *(data + 1);
|
||
frameid[2] = *(data + 2);
|
||
frameid[3] = 0;
|
||
for(uint8_t i = 0; i < 4; i++) tag[i] = frameid[i]; // tag = frameid
|
||
headerSize -= 3;
|
||
size_t len = bigEndian(data + 3, 3);
|
||
headerSize -= 3;
|
||
headerSize -= len;
|
||
char value[256];
|
||
size_t tmp = len;
|
||
if(tmp > 254) tmp = 254;
|
||
memcpy(value, (data + 7), tmp);
|
||
value[tmp+1] = 0;
|
||
chbuf[0] = 0;
|
||
|
||
showID3Tag(tag, value);
|
||
if(len == 0) m_controlCounter = 98;
|
||
|
||
return 3 + 3 + len;
|
||
}
|
||
// -- end section V2.2 -----------
|
||
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == 98){ // skip all ID3 metadata (mostly spaces)
|
||
if(headerSize > 256) {
|
||
headerSize -=256;
|
||
return 256;
|
||
} // Throw it away
|
||
else {
|
||
m_controlCounter = 99;
|
||
return headerSize;
|
||
} // Throw it away
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == 99){ // exist another ID3tag?
|
||
m_audioDataStart += id3Size;
|
||
vTaskDelay(30);
|
||
if((*(data + 0) == 'I') && (*(data + 1) == 'D') && (*(data + 2) == '3')) {
|
||
m_controlCounter = 0;
|
||
return 0;
|
||
}
|
||
else {
|
||
m_controlCounter = 100; // ok
|
||
eofHeader = true;
|
||
m_audioDataSize = m_contentlength - m_audioDataStart;
|
||
AUDIO_INFO("Audio-Length: %u", m_audioDataSize);
|
||
if(audio_progress) audio_progress(m_audioDataStart, m_audioDataSize);
|
||
if(APIC_seen && audio_id3image){
|
||
cardLock(true);
|
||
size_t pos = audiofile.position();
|
||
audio_id3image(audiofile, APIC_pos, APIC_size);
|
||
audiofile.seek(pos); // the filepointer could have been changed by the user, set it back
|
||
cardLock(false);
|
||
}
|
||
return 0;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
int Audio::read_M4A_Header(uint8_t *data, size_t len) {
|
||
/*
|
||
ftyp
|
||
| - moov -> trak -> ... -> mp4a contains raw block parameters
|
||
| L... -> ilst contains artist, composer ....
|
||
free (optional)
|
||
|
|
||
mdat contains the audio data */
|
||
|
||
|
||
static size_t headerSize = 0;
|
||
static size_t retvalue = 0;
|
||
static size_t atomsize = 0;
|
||
static size_t audioDataPos = 0;
|
||
|
||
if(retvalue) {
|
||
if(retvalue > len) { // if returnvalue > bufferfillsize
|
||
if(len > InBuff.getMaxBlockSize()) len = InBuff.getMaxBlockSize();
|
||
retvalue -= len; // and wait for more bufferdata
|
||
return len;
|
||
}
|
||
else {
|
||
size_t tmp = retvalue;
|
||
retvalue = 0;
|
||
return tmp;
|
||
}
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == M4A_BEGIN) { // init
|
||
headerSize = 0;
|
||
retvalue = 0;
|
||
atomsize = 0;
|
||
audioDataPos = 0;
|
||
m_controlCounter = M4A_FTYP;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == M4A_FTYP) { /* check_m4a_file */
|
||
atomsize = bigEndian(data, 4); // length of first atom
|
||
if(specialIndexOf(data, "ftyp", 10) != 4) {
|
||
log_e("atom 'type' not found in header");
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
int m4a = specialIndexOf(data, "M4A ", 20);
|
||
int isom = specialIndexOf(data, "isom", 20);
|
||
|
||
if((m4a !=8) && (isom != 8)){
|
||
log_e("subtype 'MA4 ' or 'isom' expected, but found '%s '", (data + 8));
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
|
||
m_controlCounter = M4A_CHK;
|
||
retvalue = atomsize;
|
||
headerSize = atomsize;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == M4A_CHK) { /* check Tag */
|
||
atomsize = bigEndian(data, 4); // length of this atom
|
||
if(specialIndexOf(data, "moov", 10) == 4) {
|
||
m_controlCounter = M4A_MOOV;
|
||
return 0;
|
||
}
|
||
else if(specialIndexOf(data, "free", 10) == 4) {
|
||
retvalue = atomsize;
|
||
headerSize += atomsize;
|
||
return 0;
|
||
}
|
||
else if(specialIndexOf(data, "mdat", 10) == 4) {
|
||
m_controlCounter = M4A_MDAT;
|
||
return 0;
|
||
}
|
||
else {
|
||
char atomName[5];
|
||
(void)atomName;
|
||
atomName[0] = *data;
|
||
atomName[1] = *(data + 1);
|
||
atomName[2] = *(data + 2);
|
||
atomName[3] = *(data + 3);
|
||
atomName[4] = 0;
|
||
|
||
if(m_f_Log) log_i("atom %s found", atomName);
|
||
|
||
retvalue = atomsize;
|
||
headerSize += atomsize;
|
||
return 0;
|
||
}
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == M4A_MOOV) { // moov
|
||
// we are looking for track and ilst
|
||
if(specialIndexOf(data, "trak", len) > 0){
|
||
int offset = specialIndexOf(data, "trak", len);
|
||
retvalue = offset;
|
||
atomsize -= offset;
|
||
headerSize += offset;
|
||
m_controlCounter = M4A_TRAK;
|
||
return 0;
|
||
}
|
||
if(specialIndexOf(data, "ilst", len) > 0){
|
||
int offset = specialIndexOf(data, "ilst", len);
|
||
retvalue = offset;
|
||
atomsize -= offset;
|
||
headerSize += offset;
|
||
m_controlCounter = M4A_ILST;
|
||
return 0;
|
||
|
||
}
|
||
if (atomsize > len -10){atomsize -= (len -10); headerSize += (len -10); retvalue = (len -10);}
|
||
else {m_controlCounter = M4A_CHK; retvalue = atomsize; headerSize += atomsize;}
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == M4A_TRAK) { // trak
|
||
if(specialIndexOf(data, "esds", len) > 0){
|
||
int esds = specialIndexOf(data, "esds", len); // Packaging/Encapsulation And Setup Data
|
||
uint8_t *pos = data + esds;
|
||
uint8_t len_of_OD = *(pos + 12); // length of this OD (which includes the next 2 tags)
|
||
(void)len_of_OD;
|
||
uint8_t len_of_ESD = *(pos + 20); // length of this Elementary Stream Descriptor
|
||
(void)len_of_ESD;
|
||
uint8_t audioType = *(pos + 21);
|
||
|
||
if (audioType == 0x40) {AUDIO_INFO("AudioType: MPEG4 / Audio");} // ObjectTypeIndication
|
||
else if(audioType == 0x66) {AUDIO_INFO("AudioType: MPEG2 / Audio");}
|
||
else if(audioType == 0x69) {AUDIO_INFO("AudioType: MPEG2 / Audio Part 3");} // Backward Compatible Audio
|
||
else if(audioType == 0x6B) {AUDIO_INFO("AudioType: MPEG1 / Audio");}
|
||
else {AUDIO_INFO("unknown Audio Type %x", audioType);}
|
||
|
||
uint8_t streamType = *(pos + 22);
|
||
streamType = streamType >> 2; // 6 bits
|
||
if(streamType!= 5) { log_e("Streamtype is not audio!"); }
|
||
|
||
uint32_t maxBr = bigEndian(pos + 26, 4); // max bitrate
|
||
AUDIO_INFO("max bitrate: %i", maxBr);
|
||
|
||
uint32_t avrBr = bigEndian(pos + 30, 4); // avg bitrate
|
||
AUDIO_INFO("avr bitrate: %i", avrBr);
|
||
|
||
uint16_t ASC = bigEndian(pos + 39, 2);
|
||
|
||
uint8_t objectType = ASC >> 11; // first 5 bits
|
||
|
||
if (objectType == 1) {AUDIO_INFO("AudioObjectType: AAC Main");} // Audio Object Types
|
||
else if(objectType == 2) {AUDIO_INFO("AudioObjectType: AAC Low Complexity");}
|
||
else if(objectType == 3) {AUDIO_INFO("AudioObjectType: AAC Scalable Sample Rate");}
|
||
else if(objectType == 4) {AUDIO_INFO("AudioObjectType: AAC Long Term Prediction");}
|
||
else if(objectType == 5) {AUDIO_INFO("AudioObjectType: AAC Spectral Band Replication");}
|
||
else if(objectType == 6) {AUDIO_INFO("AudioObjectType: AAC Scalable");}
|
||
else {AUDIO_INFO("unknown Audio Type %x", audioType);}
|
||
|
||
const uint32_t samplingFrequencies[13] = {
|
||
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350
|
||
};
|
||
uint8_t sRate = (ASC & 0x0600) >> 7; // next 4 bits Sampling Frequencies
|
||
AUDIO_INFO("Sampling Frequency: %u",samplingFrequencies[sRate]);
|
||
|
||
uint8_t chConfig = (ASC & 0x78) >> 3; // next 4 bits
|
||
if(chConfig == 0) AUDIO_INFO("Channel Configurations: AOT Specifc Config");
|
||
if(chConfig == 1) AUDIO_INFO("Channel Configurations: front-center");
|
||
if(chConfig == 2) AUDIO_INFO("Channel Configurations: front-left, front-right");
|
||
if(chConfig > 2) { log_e("Channel Configurations with more than 2 channels is not allowed!"); }
|
||
|
||
uint8_t frameLengthFlag = (ASC & 0x04);
|
||
uint8_t dependsOnCoreCoder = (ASC & 0x02);
|
||
(void)dependsOnCoreCoder;
|
||
uint8_t extensionFlag = (ASC & 0x01);
|
||
(void)extensionFlag;
|
||
|
||
if(frameLengthFlag == 0) AUDIO_INFO("AAC FrameLength: 1024 bytes");
|
||
if(frameLengthFlag == 1) AUDIO_INFO("AAC FrameLength: 960 bytes");
|
||
}
|
||
if(specialIndexOf(data, "mp4a", len) > 0){
|
||
int offset = specialIndexOf(data, "mp4a", len);
|
||
int channel = bigEndian(data + offset + 20, 2); // audio parameter must be set before starting
|
||
int bps = bigEndian(data + offset + 22, 2); // the aac decoder. There are RAW blocks only in m4a
|
||
int srate = bigEndian(data + offset + 26, 4); //
|
||
setBitsPerSample(bps);
|
||
setChannels(channel);
|
||
setSampleRate(srate);
|
||
setBitrate(bps * channel * srate);
|
||
AUDIO_INFO("ch; %i, bps: %i, sr: %i", channel, bps, srate);
|
||
if(audioDataPos && getDatamode() == AUDIO_LOCALFILE) {
|
||
m_controlCounter = M4A_AMRDY;
|
||
setFilePos(audioDataPos);
|
||
return 0;
|
||
}
|
||
}
|
||
m_controlCounter = M4A_MOOV;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == M4A_ILST) { // ilst
|
||
const char info[12][6] = { "nam\0", "ART\0", "alb\0", "too\0", "cmt\0", "wrt\0",
|
||
"tmpo\0", "trkn\0","day\0", "cpil\0", "aART\0", "gen\0"};
|
||
int offset;
|
||
for(int i=0; i < 12; i++){
|
||
offset = specialIndexOf(data, info[i], len, true); // seek info[] with '\0'
|
||
if(offset>0) {
|
||
offset += 19; if(*(data + offset) == 0) offset ++;
|
||
char value[256];
|
||
size_t tmp = strlen((const char*)data + offset);
|
||
if(tmp > 254) tmp = 254;
|
||
memcpy(value, (data + offset), tmp);
|
||
value[tmp] = 0;
|
||
chbuf[0] = 0;
|
||
if(i == 0) sprintf(chbuf, "Title: %s", value);
|
||
if(i == 1) sprintf(chbuf, "Artist: %s", value);
|
||
if(i == 2) sprintf(chbuf, "Album: %s", value);
|
||
if(i == 3) sprintf(chbuf, "Encoder: %s", value);
|
||
if(i == 4) sprintf(chbuf, "Comment: %s", value);
|
||
if(i == 5) sprintf(chbuf, "Composer: %s", value);
|
||
if(i == 6) sprintf(chbuf, "BPM: %s", value);
|
||
if(i == 7) sprintf(chbuf, "Track Number: %s", value);
|
||
if(i == 8) sprintf(chbuf, "Year: %s", value);
|
||
if(i == 9) sprintf(chbuf, "Compile: %s", value);
|
||
if(i == 10) sprintf(chbuf, "Album Artist: %s", value);
|
||
if(i == 11) sprintf(chbuf, "Types of: %s", value);
|
||
if(chbuf[0] != 0) {
|
||
if(audio_id3data) audio_id3data(chbuf);
|
||
}
|
||
}
|
||
}
|
||
m_controlCounter = M4A_MOOV;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == M4A_MDAT) { // mdat
|
||
m_audioDataSize = bigEndian(data, 4) -8; // length of this atom - strlen(M4A_MDAT)
|
||
AUDIO_INFO( "Audio-Length: %u",m_audioDataSize);
|
||
if(audio_progress) audio_progress(m_audioDataStart, m_audioDataSize);
|
||
retvalue = 8;
|
||
headerSize += 8;
|
||
m_controlCounter = M4A_AMRDY; // last step before starting the audio
|
||
return 0;
|
||
}
|
||
|
||
if(m_controlCounter == M4A_AMRDY){ // almost ready
|
||
m_audioDataStart = headerSize;
|
||
// m_contentlength = headerSize + m_audioDataSize; // after this mdat atom there may be other atoms
|
||
if(getDatamode() == AUDIO_LOCALFILE){
|
||
AUDIO_INFO("Content-Length: %u", m_contentlength);
|
||
if(audio_progress) audio_progress(m_audioDataStart, m_audioDataSize);
|
||
}
|
||
m_controlCounter = M4A_OKAY; // that's all
|
||
eofHeader = true;
|
||
return 0;
|
||
}
|
||
// this section should never be reached
|
||
log_e("error");
|
||
return 0;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
int Audio::read_OGG_Header(uint8_t *data, size_t len){
|
||
static size_t retvalue = 0;
|
||
static size_t pageLen = 0;
|
||
static bool f_firstPacket = false;
|
||
|
||
if(retvalue) {
|
||
if(retvalue > len) { // if returnvalue > bufferfillsize
|
||
if(len > InBuff.getMaxBlockSize()) len = InBuff.getMaxBlockSize();
|
||
retvalue -= len; // and wait for more bufferdata
|
||
return len;
|
||
}
|
||
else {
|
||
size_t tmp = retvalue;
|
||
retvalue = 0;
|
||
return tmp;
|
||
}
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == OGG_BEGIN) { // init
|
||
retvalue = 0;
|
||
m_audioDataStart = 0;
|
||
f_firstPacket = true;
|
||
m_controlCounter = OGG_MAGIC;
|
||
if(getDatamode() == AUDIO_LOCALFILE){
|
||
m_contentlength = getFileSize();
|
||
AUDIO_INFO("Content-Length: %u", m_contentlength);
|
||
}
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == OGG_MAGIC) { /* check MAGIC STRING */
|
||
if(specialIndexOf(data, "OggS", 10) != 0) {
|
||
log_e("Magic String 'OggS' not found in header");
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
m_controlCounter = OGG_HEADER;
|
||
retvalue = 4;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == OGG_HEADER) { /* check OGG PAGE HEADER */
|
||
uint8_t i = 0;
|
||
uint8_t ssv = *(data + i); // stream_structure_version
|
||
(void)ssv;
|
||
i++;
|
||
uint8_t htf = *(data + i); // header_type_flag
|
||
(void)htf;
|
||
i++;
|
||
uint32_t tmp = bigEndian(data + i, 4); // absolute granule position
|
||
uint64_t agp = (uint64_t) tmp << 32;
|
||
i += 4;
|
||
agp += bigEndian(data + i, 4);
|
||
i += 4;
|
||
uint32_t ssnr = bigEndian(data + i, 4); // stream serial number
|
||
(void)ssnr;
|
||
i += 4;
|
||
uint32_t psnr = bigEndian(data + i, 4); // page sequence no
|
||
(void)psnr;
|
||
i += 4;
|
||
uint32_t pchk = bigEndian(data + i, 4); // page checksum
|
||
(void)pchk;
|
||
i += 4;
|
||
uint8_t psegm = *(data + i);
|
||
i++;
|
||
uint8_t psegmBuff[256];
|
||
pageLen = 0;
|
||
for(uint8_t j = 0; j < psegm; j++){
|
||
psegmBuff[j] = *(data + i);
|
||
pageLen += psegmBuff[j];
|
||
i++;
|
||
}
|
||
retvalue = i;
|
||
if(agp == 0){
|
||
if(f_firstPacket == true){
|
||
f_firstPacket = false;
|
||
m_controlCounter = OGG_FIRST; // ogg first pages
|
||
}
|
||
else{
|
||
retvalue += pageLen;
|
||
m_controlCounter = OGG_MAGIC;
|
||
}
|
||
}
|
||
else{
|
||
if(m_codec == CODEC_OGG_FLAC){
|
||
m_controlCounter = OGG_AMRDY;
|
||
}
|
||
else {
|
||
AUDIO_INFO("unknown format");
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_controlCounter == OGG_FIRST) { /* check OGG FIRST PAGES (has no streaming content) */
|
||
uint8_t i = 0;
|
||
uint8_t obp = *(data + i); // oneBytePacket shold be 0x7F
|
||
(void)obp;
|
||
i++;
|
||
if(specialIndexOf(data + i, "FLAC", 10) == 0){
|
||
}
|
||
else{
|
||
log_e("ogg/flac support only"); // ogg/vorbis or ogg//opus not supported yet
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
i += 4;
|
||
uint8_t major_vers = *(data + i);
|
||
(void)major_vers;
|
||
i++;
|
||
uint8_t minor_vers = *(data + i);
|
||
(void)minor_vers;
|
||
i++;
|
||
uint16_t nonah = bigEndian(data + i, 2); // number of non audio headers (0x00 = unknown)
|
||
(void)nonah;
|
||
i += 2;
|
||
if(specialIndexOf(data + i, "fLaC", 10) == 0){
|
||
m_codec = CODEC_OGG_FLAC;
|
||
}
|
||
i += 4;
|
||
// STREAMINFO metadata block begins
|
||
uint32_t mblen = bigEndian(data + i, 4);
|
||
(void)mblen;
|
||
i += 4; // skip metadata block header + length
|
||
i += 2; // skip minimun block size
|
||
m_flacMaxBlockSize = bigEndian(data + i, 2);
|
||
i += 2;
|
||
vTaskDelay(2);
|
||
AUDIO_INFO("FLAC maxBlockSize: %u", m_flacMaxBlockSize);
|
||
i += 3; // skip minimun frame size
|
||
vTaskDelay(2);
|
||
m_flacMaxFrameSize = bigEndian(data + i, 3);
|
||
i += 3;
|
||
if(m_flacMaxFrameSize){
|
||
AUDIO_INFO("FLAC maxFrameSize: %u", m_flacMaxFrameSize);
|
||
}
|
||
else {
|
||
AUDIO_INFO("FLAC maxFrameSize: N/A");
|
||
}
|
||
if(m_flacMaxFrameSize > InBuff.getMaxBlockSize()) {
|
||
log_e("FLAC maxFrameSize too large!");
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
vTaskDelay(2);
|
||
uint32_t nextval = bigEndian(data + i, 3);
|
||
i += 3;
|
||
m_flacSampleRate = nextval >> 4;
|
||
AUDIO_INFO("FLAC sampleRate: %u", m_flacSampleRate);
|
||
vTaskDelay(2);
|
||
m_flacNumChannels = ((nextval & 0x06) >> 1) + 1;
|
||
AUDIO_INFO("FLAC numChannels: %u", m_flacNumChannels);
|
||
if(m_flacNumChannels != 1 && m_flacNumChannels != 2){
|
||
vTaskDelay(2);
|
||
AUDIO_INFO("numChannels must be 1 or 2");
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
vTaskDelay(2);
|
||
uint8_t bps = (nextval & 0x01) << 4;
|
||
bps += (*(data +i) >> 4) + 1;
|
||
i++;
|
||
m_flacBitsPerSample = bps;
|
||
if((bps != 8) && (bps != 16)){
|
||
log_e("bits per sample must be 8 or 16, is %i", bps);
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
AUDIO_INFO("FLAC bitsPerSample: %u", m_flacBitsPerSample);
|
||
m_flacTotalSamplesInStream = bigEndian(data + i, 4);
|
||
i++;
|
||
if(m_flacTotalSamplesInStream) {
|
||
AUDIO_INFO("total samples in stream: %u", m_flacTotalSamplesInStream);
|
||
}
|
||
else {
|
||
AUDIO_INFO("total samples in stream: N/A");
|
||
}
|
||
if(bps != 0 && m_flacTotalSamplesInStream) {
|
||
AUDIO_INFO("audio file duration: %u seconds", m_flacTotalSamplesInStream / m_flacSampleRate);
|
||
}
|
||
m_controlCounter = OGG_MAGIC;
|
||
retvalue = pageLen;
|
||
return 0;
|
||
}
|
||
if(m_controlCounter == OGG_AMRDY){ // ogg almost ready
|
||
if(!psramFound()){
|
||
AUDIO_INFO("FLAC works only with PSRAM!");
|
||
m_f_running = false; stopSong();
|
||
return -1;
|
||
}
|
||
if(!FLACDecoder_AllocateBuffers()) {m_f_running = false; stopSong(); return -1;}
|
||
InBuff.changeMaxBlockSize(m_frameSizeFLAC);
|
||
AUDIO_INFO("FLACDecoder has been initialized, free Heap: %u bytes", ESP.getFreeHeap());
|
||
|
||
m_controlCounter = OGG_OKAY; // 100
|
||
eofHeader = true;
|
||
retvalue = 0;
|
||
return 0;
|
||
}
|
||
return 0;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
size_t Audio::process_m3u8_ID3_Header(uint8_t* packet){
|
||
uint8_t ID3version;
|
||
size_t id3Size;
|
||
bool m_f_unsync = false, m_f_exthdr = false;
|
||
static uint64_t last_timestamp; // remember the last timestamp
|
||
static uint32_t lastSampleRate;
|
||
uint64_t current_timestamp = 0;
|
||
uint32_t newSampleRate = 0;
|
||
(void)m_f_unsync; (void)last_timestamp; (void)lastSampleRate; (void)current_timestamp; (void)newSampleRate;
|
||
if(specialIndexOf(packet, "ID3", 4) != 0) { // ID3 not found
|
||
if(m_f_Log) log_i("m3u8 file has no mp3 tag");
|
||
return 0; // error, no ID3 signature found
|
||
}
|
||
ID3version = *(packet + 3);
|
||
switch(ID3version){
|
||
case 2:
|
||
m_f_unsync = (*(packet + 5) & 0x80);
|
||
m_f_exthdr = false;
|
||
break;
|
||
case 3:
|
||
case 4:
|
||
m_f_unsync = (*(packet + 5) & 0x80); // bit7
|
||
m_f_exthdr = (*(packet + 5) & 0x40); // bit6 extended header
|
||
break;
|
||
};
|
||
id3Size = bigEndian(&packet[6], 4, 7); // ID3v2 size 4 * %0xxxxxxx (shift left seven times!!)
|
||
id3Size += 10;
|
||
if(m_f_Log) log_i("ID3 framesSize: %i", id3Size);
|
||
if(m_f_Log) log_i("ID3 version: 2.%i", ID3version);
|
||
|
||
if(m_f_exthdr) {
|
||
log_e("ID3 extended header in m3u8 files not supported");
|
||
return 0;
|
||
}
|
||
if(m_f_Log) log_i("ID3 normal frames");
|
||
|
||
if(specialIndexOf(&packet[10], "PRIV", 5) != 0) { // tag PRIV not found
|
||
log_e("tag PRIV in m3u8 Id3 Header not found");
|
||
return 0;
|
||
}
|
||
// if tag PRIV exists assume content is "com.apple.streaming.transportStreamTimestamp"
|
||
// a time stamp is expected in the header.
|
||
|
||
current_timestamp = (double)bigEndian(&packet[69], 4) / 90000; // seconds
|
||
|
||
return id3Size;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
uint32_t Audio::stopSong() {
|
||
uint32_t pos = 0;
|
||
if(m_f_running) {
|
||
m_f_running = false;
|
||
if(getDatamode() == AUDIO_LOCALFILE){
|
||
m_streamType = ST_NONE;
|
||
pos = getFilePos() - inBufferFilled();
|
||
cardLock(true);audiofile.close();cardLock(false);
|
||
AUDIO_INFO("Closing audio file");
|
||
}
|
||
}
|
||
if(audiofile){
|
||
// added this before putting 'm_f_localfile = false' in stopSong(); shoulf never occur....
|
||
cardLock(true);audiofile.close();cardLock(false);
|
||
AUDIO_INFO("Closing audio file");
|
||
log_w("Closing audio file"); // for debug
|
||
}
|
||
memset(m_outBuff, 0, sizeof(m_outBuff)); //Clear OutputBuffer
|
||
i2s_zero_dma_buffer((i2s_port_t) m_i2s_num);
|
||
return pos;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::playI2Sremains() { // returns true if all dma_buffs flushed
|
||
if(!getSampleRate()) setSampleRate(96000);
|
||
if(!getChannels()) setChannels(2);
|
||
if(getBitsPerSample() > 8) memset(m_outBuff, 0, sizeof(m_outBuff)); //Clear OutputBuffer (signed)
|
||
else memset(m_outBuff, 128, sizeof(m_outBuff)); //Clear OutputBuffer (unsigned, PCM 8u)
|
||
|
||
m_validSamples = m_i2s_config.dma_buf_len;
|
||
while(m_validSamples) {
|
||
playChunk();
|
||
}
|
||
return;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::pauseResume() {
|
||
bool retVal = false;
|
||
if(getDatamode() == AUDIO_LOCALFILE || m_streamType == ST_WEBSTREAM) {
|
||
m_f_running = !m_f_running;
|
||
retVal = true;
|
||
if(!m_f_running) {
|
||
memset(m_outBuff, 0, sizeof(m_outBuff)); //Clear OutputBuffer
|
||
i2s_zero_dma_buffer((i2s_port_t) m_i2s_num);
|
||
}
|
||
}
|
||
return retVal;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::playChunk() {
|
||
// If we've got data, try and pump it out..
|
||
int16_t sample[2];
|
||
/* VU Meter ************************************************************************************************************/
|
||
/* По мотивам https://github.com/schreibfaul1/ESP32-audioI2S/pull/170/commits/6cce84217e5bc8f2f8925936affc84576932a29b */
|
||
uint8_t maxl = 0, maxr = 0;
|
||
uint8_t minl = 0xFF, minr = 0xFF;
|
||
/************************************************************************************************************ VU Meter */
|
||
if(getBitsPerSample() == 8) {
|
||
if(getChannels() == 1) {
|
||
while(m_validSamples) {
|
||
uint8_t x = m_outBuff[m_curSample] & 0x00FF;
|
||
uint8_t y = (m_outBuff[m_curSample] & 0xFF00) >> 8;
|
||
sample[LEFTCHANNEL] = x;
|
||
sample[RIGHTCHANNEL] = x;
|
||
if(sample[LEFTCHANNEL] > maxl ) maxl = sample[LEFTCHANNEL];
|
||
if(sample[RIGHTCHANNEL] > maxr ) maxr = sample[RIGHTCHANNEL];
|
||
if(sample[LEFTCHANNEL] < minl ) minl = sample[LEFTCHANNEL];
|
||
if(sample[RIGHTCHANNEL] < minr ) minr = sample[RIGHTCHANNEL];
|
||
while(1) {
|
||
if(playSample(sample)) break;
|
||
} // Can't send?
|
||
sample[LEFTCHANNEL] = y;
|
||
sample[RIGHTCHANNEL] = y;
|
||
if(sample[LEFTCHANNEL] > maxl ) maxl = sample[LEFTCHANNEL];
|
||
if(sample[RIGHTCHANNEL] > maxr ) maxr = sample[RIGHTCHANNEL];
|
||
if(sample[LEFTCHANNEL] < minl ) minl = sample[LEFTCHANNEL];
|
||
if(sample[RIGHTCHANNEL] < minr ) minr = sample[RIGHTCHANNEL];
|
||
while(1) {
|
||
if(playSample(sample)) break;
|
||
} // Can't send?
|
||
m_validSamples--;
|
||
m_curSample++;
|
||
}
|
||
}
|
||
if(getChannels() == 2) {
|
||
while(m_validSamples) {
|
||
uint8_t x = m_outBuff[m_curSample] & 0x00FF;
|
||
uint8_t y = (m_outBuff[m_curSample] & 0xFF00) >> 8;
|
||
if(!m_f_forceMono) { // stereo mode
|
||
sample[LEFTCHANNEL] = x;
|
||
sample[RIGHTCHANNEL] = y;
|
||
}
|
||
else { // force mono
|
||
uint8_t xy = (x + y) / 2;
|
||
sample[LEFTCHANNEL] = xy;
|
||
sample[RIGHTCHANNEL] = xy;
|
||
}
|
||
if(sample[LEFTCHANNEL] > maxl ) maxl = sample[LEFTCHANNEL];
|
||
if(sample[RIGHTCHANNEL] > maxr ) maxr = sample[RIGHTCHANNEL];
|
||
if(sample[LEFTCHANNEL] < minl ) minl = sample[LEFTCHANNEL];
|
||
if(sample[RIGHTCHANNEL] < minr ) minr = sample[RIGHTCHANNEL];
|
||
while(1) {
|
||
if(playSample(sample)) break;
|
||
} // Can't send?
|
||
m_validSamples--;
|
||
m_curSample++;
|
||
}
|
||
}
|
||
vuLeft = maxl - minl;
|
||
vuRight = maxr - minr;
|
||
m_curSample = 0;
|
||
return true;
|
||
}
|
||
if(getBitsPerSample() == 16) {
|
||
if(getChannels() == 1) {
|
||
while(m_validSamples) {
|
||
sample[LEFTCHANNEL] = m_outBuff[m_curSample];
|
||
sample[RIGHTCHANNEL] = m_outBuff[m_curSample];
|
||
if(sample[LEFTCHANNEL] > maxl ) maxl = sample[LEFTCHANNEL];
|
||
if(sample[RIGHTCHANNEL] > maxr ) maxr = sample[RIGHTCHANNEL];
|
||
if(sample[LEFTCHANNEL] < minl ) minl = sample[LEFTCHANNEL];
|
||
if(sample[RIGHTCHANNEL] < minr ) minr = sample[RIGHTCHANNEL];
|
||
if(!playSample(sample)) {
|
||
log_e("can't send");
|
||
return false;
|
||
} // Can't send
|
||
m_validSamples--;
|
||
m_curSample++;
|
||
}
|
||
}
|
||
if(getChannels() == 2) {
|
||
m_curSample = 0;
|
||
while(m_validSamples) {
|
||
if(!m_f_forceMono) { // stereo mode
|
||
sample[LEFTCHANNEL] = m_outBuff[m_curSample * 2];
|
||
sample[RIGHTCHANNEL] = m_outBuff[m_curSample * 2 + 1];
|
||
}
|
||
else { // mono mode, #100
|
||
int16_t xy = (m_outBuff[m_curSample * 2] + m_outBuff[m_curSample * 2 + 1]) / 2;
|
||
sample[LEFTCHANNEL] = xy;
|
||
sample[RIGHTCHANNEL] = xy;
|
||
}
|
||
if(sample[LEFTCHANNEL] > maxl ) maxl = sample[LEFTCHANNEL];
|
||
if(sample[RIGHTCHANNEL] > maxr ) maxr = sample[RIGHTCHANNEL];
|
||
if(sample[LEFTCHANNEL] < minl ) minl = sample[LEFTCHANNEL];
|
||
if(sample[RIGHTCHANNEL] < minr ) minr = sample[RIGHTCHANNEL];
|
||
playSample(sample);
|
||
m_validSamples--;
|
||
m_curSample++;
|
||
}
|
||
}
|
||
vuLeft = maxl - minl;
|
||
vuRight = maxr - minr;
|
||
m_curSample = 0;
|
||
return true;
|
||
}
|
||
log_e("BitsPer Sample must be 8 or 16!");
|
||
m_validSamples = 0;
|
||
stopSong();
|
||
return false;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
|
||
void Audio::cardLock(bool lock){
|
||
#if (TFT_CS!=255) || (SDC_CS!=255)
|
||
if(lock){
|
||
xSemaphoreTake(mutex_pl, portMAX_DELAY);
|
||
}else{
|
||
xSemaphoreGive(mutex_pl);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
void Audio::loop() {
|
||
|
||
if(!m_f_running) return;
|
||
|
||
if(m_playlistFormat != FORMAT_M3U8){ // normal process
|
||
switch(getDatamode()){
|
||
case AUDIO_LOCALFILE:
|
||
processLocalFile();
|
||
break;
|
||
case HTTP_RESPONSE_HEADER:
|
||
parseHttpResponseHeader();
|
||
break;
|
||
case AUDIO_PLAYLISTINIT:
|
||
readPlayListData();
|
||
break;
|
||
case AUDIO_PLAYLISTDATA:
|
||
if(m_playlistFormat == FORMAT_M3U) connecttohost(parsePlaylist_M3U());
|
||
if(m_playlistFormat == FORMAT_PLS) connecttohost(parsePlaylist_PLS());
|
||
if(m_playlistFormat == FORMAT_ASX) connecttohost(parsePlaylist_ASX());
|
||
break;
|
||
case AUDIO_DATA:
|
||
processWebStream();
|
||
break;
|
||
}
|
||
}
|
||
else { // m3u8 datastream only
|
||
static bool f_noNewHost = false;
|
||
static int32_t remaintime, timestamp1, timestamp2; // m3u8 time management
|
||
const char* host;
|
||
|
||
switch(getDatamode()){
|
||
case HTTP_RESPONSE_HEADER:
|
||
playAudioData(); // fill I2S DMA buffer
|
||
parseHttpResponseHeader();
|
||
m_codec = CODEC_AAC;
|
||
break;
|
||
case AUDIO_PLAYLISTINIT:
|
||
readPlayListData();
|
||
break;
|
||
case AUDIO_PLAYLISTDATA:
|
||
host = parsePlaylist_M3U8();
|
||
m_f_m3u8data = true;
|
||
if(host){
|
||
f_noNewHost = false;
|
||
timestamp1 = millis();
|
||
httpPrint(host);
|
||
}
|
||
else {
|
||
f_noNewHost = true;
|
||
timestamp2 = millis() + remaintime;
|
||
setDatamode(AUDIO_DATA); //fake datamode, we have no new audiosequence yet, so let audio run
|
||
}
|
||
break;
|
||
case AUDIO_DATA:
|
||
if(m_f_ts) processWebStreamTS(); // aac or aacp with ts packets
|
||
else processWebStreamHLS(); // aac or aacp normal stream
|
||
if(f_noNewHost){
|
||
m_f_continue = false;
|
||
if(timestamp2 < millis()) {
|
||
httpPrint(m_lastHost);
|
||
remaintime = 1000;
|
||
}
|
||
}
|
||
else{
|
||
if(m_f_continue){ // processWebStream() needs more data
|
||
remaintime = (int32_t)(m_m3u8_targetDuration * 1000) - (millis() - timestamp1);
|
||
// if(m_m3u8_targetDuration < 10) remaintime += 1000;
|
||
m_f_continue = false;
|
||
setDatamode(AUDIO_PLAYLISTDATA);
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
size_t Audio::chunkedDataTransfer(){
|
||
size_t chunksize = 0;
|
||
int b = 0;
|
||
while(true){
|
||
b = _client->read();
|
||
if(b < 0) break;
|
||
if(b == '\n') break;
|
||
if(b < '0') continue;
|
||
// We have received a hexadecimal character. Decode it and add to the result.
|
||
b = toupper(b) - '0'; // Be sure we have uppercase
|
||
if(b > 9) b = b - 7; // Translate A..F to 10..15
|
||
chunksize = (chunksize << 4) + b;
|
||
}
|
||
if(m_f_Log) log_i("chunksize %d", chunksize);
|
||
return chunksize;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::readPlayListData() {
|
||
|
||
if(getDatamode() != AUDIO_PLAYLISTINIT) return false;
|
||
if(_client->available() == 0) return false;
|
||
|
||
uint32_t chunksize = 0;
|
||
if(m_f_chunked) chunksize = chunkedDataTransfer();
|
||
|
||
// reads the content of the playlist and stores it in the vector m_contentlength
|
||
// m_contentlength is a table of pointers to the lines
|
||
char pl[512]; // playlistLine
|
||
uint32_t ctl = 0;
|
||
int lines = 0;
|
||
// delete all memory in m_playlistContent
|
||
if(!psramFound()){log_e("m3u8 playlists requires PSRAM enabled!");}
|
||
vector_clear_and_shrink(m_playlistContent);
|
||
while(true){ // outer while
|
||
|
||
uint32_t ctime = millis();
|
||
uint32_t timeout = 2000; // ms
|
||
|
||
while(true) { // inner while
|
||
uint16_t pos = 0;
|
||
while(_client->available()){ // super inner while :-))
|
||
pl[pos] = _client->read();
|
||
ctl++;
|
||
if(pl[pos] == '\n') {pl[pos] = '\0'; pos++; break;}
|
||
// if(pl[pos] == '&' ) {pl[pos] = '\0'; pos++; break;}
|
||
if(pl[pos] == '\r') {pl[pos] = '\0'; pos++; continue;;}
|
||
pos++;
|
||
if(pos == 511){ pos--; continue;}
|
||
if(pos == 510) {pl[pos] = '\0';}
|
||
if(ctl == chunksize) {pl[pos] = '\0'; break;}
|
||
if(ctl == m_contentlength) {pl[pos] = '\0'; break;}
|
||
}
|
||
if(ctl == chunksize) break;
|
||
if(ctl == m_contentlength) break;
|
||
if(pos) {pl[pos] = '\0'; break;}
|
||
|
||
if(ctime + timeout < millis()) {
|
||
log_e("timeout");
|
||
for(int i = 0; i<m_playlistContent.size(); i++) log_e("pl%i = %s", i, m_playlistContent[i]);
|
||
goto exit;}
|
||
} // inner while
|
||
|
||
if(startsWith(pl, "<!DOCTYPE")) {AUDIO_INFO("url is a webpage!"); goto exit;}
|
||
if(startsWith(pl, "<html")) {AUDIO_INFO("url is a webpage!"); goto exit;}
|
||
if(strlen(pl) > 0) m_playlistContent.push_back(strdup((const char*)pl));
|
||
if(m_playlistContent.size() == 100){
|
||
if(m_f_Log) log_i("the maximum number of lines in the playlist has been reached");
|
||
break;
|
||
}
|
||
// termination conditions
|
||
// 1. The http response header returns a value for contentLength -> read chars until contentLength is reached
|
||
// 2. no contentLength, but Transfer-Encoding:chunked -> compute chunksize and read until chunksize is reached
|
||
// 3. no chunksize and no contentlengt, but Connection: close -> read all available chars
|
||
if(ctl == m_contentlength){while(_client->available()) _client->read(); break;} // read '\n\n' if exists
|
||
if(ctl == chunksize) {while(_client->available()) _client->read(); break;}
|
||
if(!_client->connected() && _client->available() == 0) break;
|
||
|
||
} // outer while
|
||
lines = m_playlistContent.size();
|
||
for (int i = 0; i < lines ; i++) { // print all string in first vector of 'arr'
|
||
if(m_f_Log) log_i("pl=%i \"%s\"", i, m_playlistContent[i]);
|
||
}
|
||
setDatamode(AUDIO_PLAYLISTDATA);
|
||
return true;
|
||
|
||
exit:
|
||
vector_clear_and_shrink(m_playlistContent);
|
||
m_f_running = false;
|
||
setDatamode(AUDIO_NONE);
|
||
return false;
|
||
}
|
||
//----------------------------------------------------------------------------------------------------------------------
|
||
const char* Audio::parsePlaylist_M3U(){
|
||
uint8_t lines = m_playlistContent.size();
|
||
int pos = 0;
|
||
char* host = nullptr;
|
||
|
||
for(int i= 0; i < lines; i++){
|
||
if(indexOf(m_playlistContent[i], "#EXTINF:") >= 0) { // Info?
|
||
pos = indexOf(m_playlistContent[i], ","); // Comma in this line?
|
||
if(pos > 0) {
|
||
// Show artist and title if present in metadata
|
||
AUDIO_INFO(m_playlistContent[i] + pos + 1);
|
||
}
|
||
continue;
|
||
}
|
||
if(startsWith(m_playlistContent[i], "#")) { // Commentline?
|
||
continue;
|
||
}
|
||
|
||
pos = indexOf(m_playlistContent[i], "http://:@", 0); // ":@"?? remove that!
|
||
if(pos >= 0) {
|
||
AUDIO_INFO("Entry in playlist found: %s", (m_playlistContent[i] + pos + 9));
|
||
host = m_playlistContent[i] + pos + 9;
|
||
break;
|
||
}
|
||
// AUDIO_INFO("Entry in playlist found: %s", pl);
|
||
pos = indexOf(m_playlistContent[i], "http", 0); // Search for "http"
|
||
if(pos >= 0) { // Does URL contain "http://"?
|
||
// log_e("%s pos=%i", m_playlistContent[i], pos);
|
||
host = m_playlistContent[i] + pos; // Yes, set new host
|
||
break;
|
||
}
|
||
}
|
||
vector_clear_and_shrink(m_playlistContent);
|
||
return host;
|
||
}
|
||
//----------------------------------------------------------------------------------------------------------------------
|
||
const char* Audio::parsePlaylist_PLS(){
|
||
uint8_t lines = m_playlistContent.size();
|
||
int pos = 0;
|
||
char* host = nullptr;
|
||
|
||
for(int i= 0; i < lines; i++){
|
||
if(i == 0){
|
||
if(strlen(m_playlistContent[0]) == 0) goto exit; // empty line
|
||
if(strcmp(m_playlistContent[0] , "[playlist]") != 0){ // first entry in valid pls
|
||
setDatamode(HTTP_RESPONSE_HEADER); // pls is not valid
|
||
AUDIO_INFO("pls is not valid, switch to HTTP_RESPONSE_HEADER");
|
||
goto exit;
|
||
}
|
||
continue;
|
||
}
|
||
if(startsWith(m_playlistContent[i], "File1")) {
|
||
if(host) continue; // we have already a url
|
||
pos = indexOf(m_playlistContent[i], "http", 0); // File1=http://streamplus30.leonex.de:14840/;
|
||
if(pos >= 0) { // yes, URL contains "http"?
|
||
host = m_playlistContent[i] + pos; // Now we have an URL for a stream in host.
|
||
}
|
||
continue;
|
||
}
|
||
if(startsWith(m_playlistContent[i], "Title1")) { // Title1=Antenne Tirol
|
||
const char* plsStationName = (m_playlistContent[i] + 7);
|
||
if(audio_showstation) audio_showstation(plsStationName);
|
||
AUDIO_INFO("StationName: \"%s\"", plsStationName);
|
||
continue;
|
||
}
|
||
if(startsWith(m_playlistContent[i], "Length1")){
|
||
continue;
|
||
}
|
||
if(indexOf(m_playlistContent[i], "Invalid username") >= 0){ // Unable to access account:
|
||
goto exit; // Invalid username or password
|
||
}
|
||
}
|
||
return host;
|
||
|
||
exit:
|
||
m_f_running = false;
|
||
stopSong();
|
||
vector_clear_and_shrink(m_playlistContent);
|
||
setDatamode(AUDIO_NONE);
|
||
return nullptr;
|
||
}
|
||
//----------------------------------------------------------------------------------------------------------------------
|
||
const char* Audio::parsePlaylist_ASX(){ // Advanced Stream Redirector
|
||
uint8_t lines = m_playlistContent.size();
|
||
bool f_entry = false;
|
||
int pos = 0;
|
||
char* host = nullptr;
|
||
|
||
for(int i= 0; i < lines; i++){
|
||
int p1 = indexOf(m_playlistContent[i], "<", 0);
|
||
int p2 = indexOf(m_playlistContent[i], ">", 1);
|
||
if(p1 >= 0 && p2 > p1){ // #196 set all between "< ...> to lowercase
|
||
for(uint8_t j = p1; j < p2; j++){
|
||
m_playlistContent[i][j] = toLowerCase(m_playlistContent[i][j]);
|
||
}
|
||
}
|
||
if(indexOf(m_playlistContent[i], "<entry>") >= 0) f_entry = true; // found entry tag (returns -1 if not found)
|
||
if(f_entry) {
|
||
if(indexOf(m_playlistContent[i], "ref href") > 0) { // <ref href="http://87.98.217.63:24112/stream" />
|
||
pos = indexOf(m_playlistContent[i], "http", 0);
|
||
if(pos > 0) {
|
||
host = (m_playlistContent[i] + pos); // http://87.98.217.63:24112/stream" />
|
||
int pos1 = indexOf(host, "\"", 0); // http://87.98.217.63:24112/stream
|
||
if(pos1 > 0) host[pos1] = '\0'; // Now we have an URL for a stream in host.
|
||
}
|
||
}
|
||
}
|
||
pos = indexOf(m_playlistContent[i], "<title>", 0);
|
||
if(pos >= 0) {
|
||
char* plsStationName = (m_playlistContent[i] + pos + 7); // remove <Title>
|
||
pos = indexOf(plsStationName, "</", 0);
|
||
if(pos >= 0){
|
||
*(plsStationName +pos) = 0; // remove </Title>
|
||
}
|
||
if(audio_showstation) audio_showstation(plsStationName);
|
||
AUDIO_INFO("StationName: \"%s\"", plsStationName);
|
||
}
|
||
|
||
if(indexOf(m_playlistContent[i], "http") == 0 && !f_entry) { //url only in asx
|
||
host = m_playlistContent[i];
|
||
}
|
||
}
|
||
return host;
|
||
}
|
||
//----------------------------------------------------------------------------------------------------------------------
|
||
const char* Audio::parsePlaylist_M3U8(){
|
||
uint8_t lines = m_playlistContent.size();
|
||
bool f_begin = false;
|
||
uint8_t occurence = 0;
|
||
if(lines){
|
||
for(int i= 0; i < lines; i++){
|
||
if(strlen(m_playlistContent[i]) == 0) continue; // empty line
|
||
if(startsWith(m_playlistContent[i], "#EXTM3U")){ // what we expected
|
||
f_begin = true;
|
||
continue;
|
||
}
|
||
if(!f_begin) continue;
|
||
|
||
// example: redirection
|
||
// #EXTM3U
|
||
// #EXT-X-STREAM-INF:BANDWIDTH=22050,CODECS="mp4a.40.2"
|
||
// http://ample.revma.ihrhls.com/zc7729/63_sdtszizjcjbz02/playlist.m3u8
|
||
if(startsWith(m_playlistContent[i],"#EXT-X-STREAM-INF:")){
|
||
if(occurence > 0) break; // no more than one #EXT-X-STREAM-INF: (can have different BANDWIDTH)
|
||
occurence++;
|
||
if(!endsWith(m_playlistContent[i+1], "m3u8")){ // we have a new m3u8 playlist, skip to next line
|
||
int pos = indexOf(m_playlistContent[i], "CODECS=\"mp4a", 18);
|
||
// 'mp4a.40.01' AAC Main
|
||
// 'mp4a.40.02' AAC LC (Low Complexity)
|
||
// 'mp4a.40.03' AAC SSR (Scalable Sampling Rate) ??
|
||
// 'mp4a.40.03' AAC LTP (Long Term Prediction) ??
|
||
// 'mp4a.40.03' SBR (Spectral Band Replication)
|
||
if(pos < 0){ // not found
|
||
int pos1 = indexOf(m_playlistContent[i], "CODECS=", 18);
|
||
if(pos1 < 0) pos1 = 0;
|
||
log_e("codec %s in m3u8 playlist not supported", m_playlistContent[i] + pos1);
|
||
goto exit;
|
||
}
|
||
}
|
||
i++; // next line
|
||
if(i == lines) continue; // and exit for()
|
||
|
||
char* tmp = nullptr;
|
||
if(!startsWith(m_playlistContent[i], "http")){
|
||
//http://livees.com/prog_index.m3u8 and prog_index48347.aac --> http://livees.com/prog_index48347.aac
|
||
//http://livees.com/prog_index.m3u8 and chunklist022.m3u8 --> http://livees.com/chunklist022.m3u8
|
||
tmp = (char*)malloc(strlen(m_lastHost)+ strlen(m_playlistContent[i]));
|
||
strcpy(tmp, m_lastHost);
|
||
int idx = lastIndexOf(tmp, "/");
|
||
strcpy(tmp + idx + 1, m_playlistContent[i]);
|
||
}
|
||
else{
|
||
tmp = strdup(m_playlistContent[i]);
|
||
}
|
||
if(m_playlistContent[i]){free(m_playlistContent[i]); m_playlistContent[i] = NULL;}
|
||
m_playlistContent[i] = strdup(tmp);
|
||
strcpy(m_lastHost, tmp);
|
||
if(tmp){free(tmp); tmp = NULL;}
|
||
if(m_f_Log) log_i("redirect %s", m_playlistContent[i]);
|
||
return m_playlistContent[i]; // it's a redirection, a new m3u8 playlist
|
||
}
|
||
|
||
// example: audio chunks
|
||
// #EXTM3U
|
||
// #EXT-X-TARGETDURATION:10
|
||
// #EXT-X-MEDIA-SEQUENCE:163374040
|
||
// #EXT-X-DISCONTINUITY
|
||
// #EXTINF:10,title="text=\"Spot Block End\" amgTrackId=\"9876543\"",artist=" ",url="length=\"00:00:00\""
|
||
// http://n3fa-e2.revma.ihrhls.com/zc7729/63_sdtszizjcjbz02/main/163374038.aac
|
||
// #EXTINF:10,title="text=\"Spot Block End\" amgTrackId=\"9876543\"",artist=" ",url="length=\"00:00:00\""
|
||
// http://n3fa-e2.revma.ihrhls.com/zc7729/63_sdtszizjcjbz02/main/163374039.aac
|
||
if(startsWith(m_playlistContent[i], "#EXT-X-MEDIA-SEQUENCE:")){
|
||
// do nothing, because MEDIA-SECUENCE is not set sometimes
|
||
}
|
||
static uint16_t targetDuration = 0;
|
||
if(startsWith(m_playlistContent[i], "#EXT-X-TARGETDURATION:")) {
|
||
targetDuration = atoi(m_playlistContent[i] + 22);
|
||
}
|
||
if(targetDuration) m_m3u8_targetDuration = targetDuration;
|
||
if(m_f_Log) log_i("m_m3u8_targetDuration %d", m_m3u8_targetDuration);
|
||
|
||
if(startsWith(m_playlistContent[i],"#EXTINF")) {
|
||
if(STfromEXTINF(m_playlistContent[i])) showstreamtitle(chbuf);
|
||
i++;
|
||
if(i == lines) continue; // and exit for()
|
||
|
||
char* tmp = nullptr;
|
||
if(!startsWith(m_playlistContent[i], "http")){
|
||
//http://livees.com/prog_index.m3u8 and prog_index48347.aac --> http://livees.com/prog_index48347.aac
|
||
tmp = (char*)malloc(strlen(m_lastHost)+ strlen(m_playlistContent[i]));
|
||
strcpy(tmp, m_lastHost);
|
||
int idx = lastIndexOf(tmp, "/");
|
||
strcpy(tmp + idx + 1, m_playlistContent[i]);
|
||
}
|
||
else{
|
||
tmp = strdup(m_playlistContent[i]);
|
||
}
|
||
|
||
uint32_t hash = simpleHash(tmp);
|
||
if(m_hashQueue.size() == 0){
|
||
m_hashQueue.insert(m_hashQueue.begin(), hash);
|
||
m_playlistURL.insert(m_playlistURL.begin(), strdup(tmp));
|
||
}
|
||
else{
|
||
bool known = false;
|
||
for(int i = 0; i< m_hashQueue.size(); i++){
|
||
if(hash == m_hashQueue[i]){
|
||
if(m_f_Log) log_i("file already known %s", tmp);
|
||
known = true;
|
||
}
|
||
}
|
||
if(!known){
|
||
m_hashQueue.insert(m_hashQueue.begin(), hash);
|
||
m_playlistURL.insert(m_playlistURL.begin(), strdup(tmp));
|
||
}
|
||
}
|
||
|
||
if(m_hashQueue.size() > 20) m_hashQueue.pop_back();
|
||
|
||
if(tmp){free(tmp); tmp = NULL;}
|
||
|
||
if(m_playlistURL.size() == 20){
|
||
ESP_LOGD("", "can't stuff anymore");
|
||
break;
|
||
}
|
||
continue;
|
||
}
|
||
}
|
||
vector_clear_and_shrink(m_playlistContent); //clear after reading everything, m_playlistContent.size is now 0
|
||
}
|
||
|
||
if(m_playlistURL.size() > 0){
|
||
if(m_playlistBuff) {free(m_playlistBuff); m_playlistBuff = NULL;}
|
||
|
||
if(m_playlistURL[m_playlistURL.size() -1]) {
|
||
m_playlistBuff = strdup(m_playlistURL[m_playlistURL.size() -1]);
|
||
free( m_playlistURL[m_playlistURL.size() -1]);
|
||
m_playlistURL[m_playlistURL.size() -1] = NULL;
|
||
m_playlistURL.pop_back();
|
||
m_playlistURL.shrink_to_fit();
|
||
}
|
||
if(m_f_Log) log_i("now playing %s", m_playlistBuff);
|
||
return m_playlistBuff;
|
||
}
|
||
else{
|
||
return NULL;
|
||
}
|
||
exit:
|
||
stopSong();
|
||
return NULL;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::STfromEXTINF(char* str){
|
||
// the result is copied in chbuf!!
|
||
// extraxt StreamTitle from m3u #EXTINF line to icy-format
|
||
// orig: #EXTINF:10,title="text="TitleName",artist="ArtistName"
|
||
// conv: StreamTitle=TitleName - ArtistName
|
||
// orig: #EXTINF:10,title="text=\"Spot Block End\" amgTrackId=\"9876543\"",artist=" ",url="length=\"00:00:00\""
|
||
// conv: StreamTitle=text=\"Spot Block End\" amgTrackId=\"9876543\" -
|
||
|
||
int t1, t2, t3, n0 = 0, n1 = 0, n2 = 0;
|
||
|
||
t1 = indexOf(str, "title", 0);
|
||
if(t1 > 0){
|
||
strcpy(chbuf, "StreamTitle="); n0 = 12;
|
||
t2 = t1 + 7; // title="
|
||
t3 = indexOf(str, "\"", t2);
|
||
while(str[t3 - 1] == '\\'){
|
||
t3 = indexOf(str, "\"", t3 + 1);
|
||
}
|
||
if(t2 < 0 || t2 > t3) return false;
|
||
n1 = t3 - t2;
|
||
strncpy(chbuf + n0, str + t2, n1);
|
||
chbuf[n1] = '\0';
|
||
}
|
||
|
||
t1 = indexOf(str, "artist", 0);
|
||
if(t1 > 0){
|
||
strcpy(chbuf + n0 + n1, " - "); n1 += 3;
|
||
t2 = indexOf(str, "=\"", t1); t2 += 2;
|
||
t3 = indexOf(str, "\"", t2);
|
||
if(t2 < 0 || t2 > t3) return false;
|
||
n2 = t3 - t2;
|
||
strncpy(chbuf + n0 + n1, str + t2, n2);
|
||
chbuf[n0 + n1 + n2] = '\0';
|
||
chbuf[n2] = '\0';
|
||
}
|
||
return true;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::processLocalFile() {
|
||
|
||
if(!(audiofile && m_f_running && getDatamode() == AUDIO_LOCALFILE)) return;
|
||
int bytesDecoded = 0;
|
||
uint32_t bytesCanBeWritten = 0;
|
||
uint32_t bytesCanBeRead = 0;
|
||
int32_t bytesAddedToBuffer = 0;
|
||
static bool f_stream;
|
||
|
||
if(m_f_firstCall) { // runs only one time per connection, prepare for start
|
||
m_f_firstCall = false;
|
||
f_stream = false;
|
||
return;
|
||
}
|
||
|
||
if(!f_stream && m_controlCounter == 100) {
|
||
f_stream = true;
|
||
AUDIO_INFO("stream ready");
|
||
if(m_resumeFilePos){
|
||
if(m_resumeFilePos < m_audioDataStart) m_resumeFilePos = m_audioDataStart;
|
||
if(m_avr_bitrate) m_audioCurrentTime = ((m_resumeFilePos - m_audioDataStart) / m_avr_bitrate) * 8;
|
||
cardLock(true);audiofile.seek(m_resumeFilePos);cardLock(false);
|
||
InBuff.resetBuffer();
|
||
if(m_f_Log) log_i("m_resumeFilePos %i", m_resumeFilePos);
|
||
}
|
||
}
|
||
|
||
bytesCanBeWritten = InBuff.writeSpace();
|
||
//----------------------------------------------------------------------------------------------------
|
||
// some files contain further data after the audio block (e.g. pictures).
|
||
// In that case, the end of the audio block is not the end of the file. An 'eof' has to be forced.
|
||
if((m_controlCounter == 100) && (m_contentlength > 0)) { // fileheader was read
|
||
if(bytesCanBeWritten + getFilePos() >= m_contentlength){
|
||
if(m_contentlength > getFilePos()) bytesCanBeWritten = m_contentlength - getFilePos();
|
||
else bytesCanBeWritten = 0;
|
||
}
|
||
}
|
||
//----------------------------------------------------------------------------------------------------
|
||
|
||
cardLock(true); bytesAddedToBuffer = audiofile.read(InBuff.getWritePtr(), bytesCanBeWritten); cardLock(false);
|
||
if(bytesAddedToBuffer > 0) {
|
||
InBuff.bytesWritten(bytesAddedToBuffer);
|
||
}
|
||
|
||
if(bytesAddedToBuffer == -1) bytesAddedToBuffer = 0; // read error? eof?
|
||
bytesCanBeRead = InBuff.bufferFilled();
|
||
if(bytesCanBeRead > InBuff.getMaxBlockSize()) bytesCanBeRead = InBuff.getMaxBlockSize();
|
||
if(bytesCanBeRead == InBuff.getMaxBlockSize()) { // mp3 or aac frame complete?
|
||
|
||
if(m_controlCounter != 100){
|
||
bytesDecoded = readAudioHeader(bytesCanBeRead);
|
||
}
|
||
else {
|
||
bytesDecoded = sendBytes(InBuff.getReadPtr(), bytesCanBeRead);
|
||
}
|
||
if(bytesDecoded > 0) {InBuff.bytesWasRead(bytesDecoded); return;}
|
||
if(bytesDecoded < 0) { // no syncword found or decode error, try next chunk
|
||
InBuff.bytesWasRead(200); // try next chunk
|
||
m_bytesNotDecoded += 200;
|
||
return;
|
||
}
|
||
return;
|
||
}
|
||
|
||
if(!bytesAddedToBuffer) { // eof
|
||
bytesCanBeRead = InBuff.bufferFilled();
|
||
if(bytesCanBeRead > 200){
|
||
if(bytesCanBeRead > InBuff.getMaxBlockSize()) bytesCanBeRead = InBuff.getMaxBlockSize();
|
||
bytesDecoded = sendBytes(InBuff.getReadPtr(), bytesCanBeRead); // play last chunk(s)
|
||
if(bytesDecoded > 0){
|
||
InBuff.bytesWasRead(bytesDecoded);
|
||
return;
|
||
}
|
||
}
|
||
InBuff.resetBuffer();
|
||
playI2Sremains();
|
||
|
||
if(m_f_loop && f_stream){ //eof
|
||
AUDIO_INFO("loop from: %u to: %u", getFilePos(), m_audioDataStart); //TEST loop
|
||
setFilePos(m_audioDataStart);
|
||
if(m_codec == CODEC_FLAC) FLACDecoderReset();
|
||
/*
|
||
The current time of the loop mode is not reset,
|
||
which will cause the total audio duration to be exceeded.
|
||
For example: current time ====progress bar====> total audio duration
|
||
3:43 ====================> 3:33
|
||
*/
|
||
m_audioCurrentTime = 0;
|
||
return;
|
||
} //TEST loop
|
||
f_stream = false;
|
||
m_streamType = ST_NONE;
|
||
cardLock(true);
|
||
#ifdef SDFATFS_USED
|
||
audiofile.getName(chbuf, sizeof(chbuf));
|
||
char *afn =strdup(chbuf);
|
||
#else
|
||
char *afn =strdup(audiofile.name()); // store temporary the name
|
||
#endif
|
||
cardLock(false);
|
||
stopSong();
|
||
if(m_codec == CODEC_MP3) MP3Decoder_FreeBuffers();
|
||
if(m_codec == CODEC_AAC) AACDecoder_FreeBuffers();
|
||
if(m_codec == CODEC_M4A) AACDecoder_FreeBuffers();
|
||
if(m_codec == CODEC_FLAC) FLACDecoder_FreeBuffers();
|
||
AUDIO_INFO("End of file \"%s\"", afn);
|
||
if(audio_eof_mp3) audio_eof_mp3(afn);
|
||
if(afn) {free(afn); afn = NULL;}
|
||
}
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::processWebStream() {
|
||
|
||
const uint16_t maxFrameSize = InBuff.getMaxBlockSize(); // every mp3/aac frame is not bigger
|
||
uint32_t availableBytes; // available bytes in stream
|
||
static bool f_tmr_1s;
|
||
static bool f_stream; // first audio data received
|
||
static bool f_webFileDataComplete; // all file data received
|
||
static bool f_webFileAudioComplete; // all audio data received
|
||
static int bytesDecoded;
|
||
static uint32_t byteCounter; // count received data
|
||
static uint32_t chunksize; // chunkcount read from stream
|
||
static uint32_t tmr_1s; // timer 1 sec
|
||
static uint32_t loopCnt; // count loops if clientbuffer is empty
|
||
static size_t audioDataCount; // counts the decoded audiodata only
|
||
|
||
// first call, set some values to default - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_f_firstCall) { // runs only ont time per connection, prepare for start
|
||
m_f_firstCall = false;
|
||
f_webFileDataComplete = false;
|
||
f_webFileAudioComplete = false;
|
||
f_stream = false;
|
||
byteCounter = 0;
|
||
chunksize = 0;
|
||
bytesDecoded = 0;
|
||
loopCnt = 0;
|
||
audioDataCount = 0;
|
||
tmr_1s = millis();
|
||
m_t0 = millis();
|
||
m_metacount = m_metaint;
|
||
readMetadata(0, true); // reset all static vars
|
||
}
|
||
|
||
if(getDatamode() != AUDIO_DATA) return; // guard
|
||
|
||
if(m_streamType == ST_WEBFILE){
|
||
|
||
}
|
||
availableBytes = _client->available(); // available from stream
|
||
|
||
// timer, triggers every second - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if((tmr_1s + 1000) < millis()) {
|
||
f_tmr_1s = true; // flag will be set every second for one loop only
|
||
tmr_1s = millis();
|
||
}
|
||
|
||
if(ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE){
|
||
// Here you can see how much data comes in, a summary is displayed in every 10 calls
|
||
static uint8_t i = 0;
|
||
static uint32_t t = 0; (void)t;
|
||
static uint32_t t0 = 0;
|
||
static uint16_t avb[10];
|
||
if(!i) t = millis();
|
||
avb[i] = availableBytes;
|
||
if(!avb[i]){if(!t0) t0 = millis();}
|
||
else{if(t0 && (millis() - t0) > 400) log_v("\033[31m%dms no data received", millis() - t0); t0 = 0;}
|
||
i++;
|
||
if(i == 10) i = 0;
|
||
if(!i){
|
||
log_d("bytes available, 10 polls in %dms %d, %d, %d, %d, %d, %d, %d, %d, %d, %d", millis() - t,
|
||
avb[0], avb[1], avb[2], avb[3], avb[4], avb[5], avb[6], avb[7], avb[8], avb[9]);
|
||
}
|
||
}
|
||
|
||
// if we have chunked data transfer: get the chunksize- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_f_chunked && !m_chunkcount && availableBytes) { // Expecting a new chunkcount?
|
||
int b;
|
||
b = _client->read();
|
||
|
||
if(b == '\r') return;
|
||
if(b == '\n'){
|
||
m_chunkcount = chunksize;
|
||
chunksize = 0;
|
||
if(m_f_tts){
|
||
m_contentlength = m_chunkcount; // tts has one chunk only
|
||
m_streamType = ST_WEBFILE;
|
||
m_f_chunked = false;
|
||
}
|
||
return;
|
||
}
|
||
// We have received a hexadecimal character. Decode it and add to the result.
|
||
b = toupper(b) - '0'; // Be sure we have uppercase
|
||
if(b > 9) b = b - 7; // Translate A..F to 10..15
|
||
chunksize = (chunksize << 4) + b;
|
||
return;
|
||
}
|
||
|
||
// if we have metadata: get them - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(!m_metacount && !m_f_swm){
|
||
int bytes = 0;
|
||
int res = 0;
|
||
if(m_f_chunked) bytes = min(m_chunkcount, availableBytes);
|
||
else bytes = availableBytes;
|
||
res = readMetadata(bytes);
|
||
if(m_f_chunked) m_chunkcount -= res;
|
||
if(!m_metacount) return;
|
||
}
|
||
|
||
// if the buffer is often almost empty issue a warning - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(InBuff.bufferFilled() < maxFrameSize && f_stream && !f_webFileDataComplete){
|
||
static uint8_t cnt_slow = 0;
|
||
cnt_slow ++;
|
||
if(f_tmr_1s) {
|
||
if(cnt_slow > 25 && audio_info) audio_info("slow stream, dropouts are possible");
|
||
f_tmr_1s = false;
|
||
cnt_slow = 0;
|
||
}
|
||
}
|
||
|
||
// if the buffer can't filled for several seconds try a new connection - - - - - - - - - - - - - - - - - - - - - -
|
||
if(f_stream && !availableBytes && !f_webFileAudioComplete){
|
||
loopCnt++;
|
||
if(loopCnt > 200000) { // wait several seconds
|
||
loopCnt = 0;
|
||
AUDIO_INFO("Stream lost -> try new connection");
|
||
connecttohost(m_lastHost);
|
||
return;
|
||
}
|
||
}
|
||
if(availableBytes) loopCnt = 0;
|
||
|
||
// buffer fill routine - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(true) { // statement has no effect
|
||
uint32_t bytesCanBeWritten = InBuff.writeSpace();
|
||
if(!m_f_swm) bytesCanBeWritten = min(m_metacount, bytesCanBeWritten);
|
||
if(m_f_chunked) bytesCanBeWritten = min(m_chunkcount, bytesCanBeWritten);
|
||
|
||
int16_t bytesAddedToBuffer = 0;
|
||
|
||
// Audiobuffer throttle - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_codec == CODEC_AAC || m_codec == CODEC_MP3 || m_codec == CODEC_M4A){
|
||
if(bytesCanBeWritten > maxFrameSize) bytesCanBeWritten = maxFrameSize;
|
||
}
|
||
if(m_codec == CODEC_WAV){
|
||
if(bytesCanBeWritten > maxFrameSize - 500) bytesCanBeWritten = maxFrameSize - 600;
|
||
}
|
||
if(m_codec == CODEC_FLAC){
|
||
if(bytesCanBeWritten > maxFrameSize) bytesCanBeWritten = maxFrameSize;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
|
||
if(m_streamType == ST_WEBFILE){
|
||
// normally there is nothing to do here, if byteCounter == contentLength
|
||
// then the file is completely read, but:
|
||
// m4a files can have more data (e.g. pictures ..) after the audio Block
|
||
// therefore it is bad to read anything else (this can generate noise)
|
||
if(byteCounter + bytesCanBeWritten >= m_contentlength) bytesCanBeWritten = m_contentlength - byteCounter;
|
||
}
|
||
|
||
bytesAddedToBuffer = _client->read(InBuff.getWritePtr(), bytesCanBeWritten);
|
||
|
||
if(bytesAddedToBuffer > 0) {
|
||
if(m_streamType == ST_WEBFILE) byteCounter += bytesAddedToBuffer; // Pull request #42
|
||
if(!m_f_swm) m_metacount -= bytesAddedToBuffer;
|
||
if(m_f_chunked) m_chunkcount -= bytesAddedToBuffer;
|
||
InBuff.bytesWritten(bytesAddedToBuffer);
|
||
}
|
||
|
||
if(InBuff.bufferFilled() > maxFrameSize && !f_stream) { // waiting for buffer filled
|
||
f_stream = true; // ready to play the audio data
|
||
uint16_t filltime = millis() - m_t0;
|
||
|
||
AUDIO_INFO("stream ready");
|
||
AUDIO_INFO("buffer filled in %d ms", filltime);
|
||
}
|
||
if(!f_stream) return;
|
||
}
|
||
|
||
// if we have a webfile, read the file header first - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_streamType == ST_WEBFILE && m_controlCounter != 100 ){ // m3u8call, audiochunk has no header
|
||
if(InBuff.bufferFilled() < maxFrameSize) return;
|
||
if(m_codec == CODEC_WAV){
|
||
int res = read_WAV_Header(InBuff.getReadPtr(), InBuff.bufferFilled());
|
||
if(res >= 0) bytesDecoded = res;
|
||
else{stopSong(); return;}
|
||
}
|
||
if(m_codec == CODEC_MP3){
|
||
int res = read_ID3_Header(InBuff.getReadPtr(), InBuff.bufferFilled());
|
||
if(res >= 0) bytesDecoded = res;
|
||
else{m_controlCounter = 100;} // error, skip header
|
||
}
|
||
if(m_codec == CODEC_M4A){
|
||
int res = read_M4A_Header(InBuff.getReadPtr(), InBuff.bufferFilled());
|
||
if(res >= 0) bytesDecoded = res;
|
||
else{stopSong(); return;}
|
||
}
|
||
if(m_codec == CODEC_FLAC){
|
||
int res = read_FLAC_Header(InBuff.getReadPtr(), InBuff.bufferFilled());
|
||
if(res >= 0) bytesDecoded = res;
|
||
else{stopSong(); return;} // error, skip header
|
||
}
|
||
if(m_codec == CODEC_AAC){ // aac has no header
|
||
if(m_playlistFormat == FORMAT_M3U8){ // except m3u8 stream
|
||
int res = read_ID3_Header(InBuff.getReadPtr(), InBuff.bufferFilled());
|
||
if(res >= 0) bytesDecoded = res;
|
||
else m_controlCounter = 100;
|
||
}
|
||
else{
|
||
m_controlCounter = 100;
|
||
}
|
||
}
|
||
InBuff.bytesWasRead(bytesDecoded);
|
||
return;
|
||
}
|
||
|
||
// end of webfile reached? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(f_webFileAudioComplete){
|
||
if(m_playlistFormat == FORMAT_M3U8) return
|
||
playI2Sremains();
|
||
stopSong(); // Correct close when play known length sound #74 and before callback #11
|
||
if(m_f_tts){
|
||
AUDIO_INFO("End of speech: \"%s\"", m_lastHost);
|
||
if(audio_eof_speech) audio_eof_speech(m_lastHost);
|
||
}
|
||
else{
|
||
AUDIO_INFO("End of webstream: \"%s\"", m_lastHost);
|
||
if(audio_eof_stream) audio_eof_stream(m_lastHost);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// play audio data - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(!f_stream) return; // 1. guard
|
||
bool a = InBuff.bufferFilled() >= maxFrameSize;
|
||
bool b = (m_audioDataSize > 0) && (m_audioDataSize <= audioDataCount + maxFrameSize);
|
||
if(!a && !b) return; // 2. guard fill < frame && last frame(s)
|
||
|
||
size_t data2decode = InBuff.bufferFilled();
|
||
|
||
if(data2decode < maxFrameSize){
|
||
if(m_audioDataSize - audioDataCount < maxFrameSize){
|
||
data2decode = m_audioDataSize - audioDataCount;
|
||
}
|
||
else return;
|
||
}
|
||
else data2decode = maxFrameSize;
|
||
|
||
if(m_streamType == ST_WEBFILE){
|
||
|
||
bytesDecoded = sendBytes(InBuff.getReadPtr(), data2decode);
|
||
if(bytesDecoded > 0) audioDataCount += bytesDecoded;
|
||
|
||
if(byteCounter == m_contentlength){
|
||
if(m_playlistFormat == FORMAT_M3U8){
|
||
byteCounter = 0;
|
||
m_metacount = m_metaint;
|
||
m_f_continue = true;
|
||
return;
|
||
}
|
||
f_webFileDataComplete = true;
|
||
}
|
||
if(m_audioDataSize == audioDataCount && m_controlCounter == 100) f_webFileAudioComplete = true;
|
||
}
|
||
else { // not a webfile
|
||
if(m_controlCounter != 100 && (m_codec == CODEC_OGG || m_codec == CODEC_OGG_FLAC)) { //application/ogg
|
||
int res = read_OGG_Header(InBuff.getReadPtr(), InBuff.bufferFilled());
|
||
if(res >= 0) bytesDecoded = res;
|
||
else { // error, skip header
|
||
stopSong();
|
||
m_controlCounter = 100;
|
||
}
|
||
}
|
||
else{
|
||
bytesDecoded = sendBytes(InBuff.getReadPtr(), data2decode);
|
||
}
|
||
}
|
||
|
||
if(bytesDecoded < 0) { // no syncword found or decode error, try next chunk
|
||
uint8_t next = 200;
|
||
if(InBuff.bufferFilled() < next) next = InBuff.bufferFilled();
|
||
InBuff.getReadPtr();
|
||
InBuff.bytesWasRead(next); // try next chunk
|
||
m_bytesNotDecoded += next;
|
||
if(m_streamType == ST_WEBFILE) audioDataCount += next;
|
||
return;
|
||
}
|
||
else {
|
||
if(bytesDecoded > 0) {InBuff.bytesWasRead(bytesDecoded); return;}
|
||
if(bytesDecoded == 0) return; // syncword at pos0 found
|
||
}
|
||
return;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::processWebStreamTS() {
|
||
|
||
const uint16_t maxFrameSize = InBuff.getMaxBlockSize(); // every mp3/aac frame is not bigger
|
||
uint32_t availableBytes; // available bytes in stream
|
||
static bool f_tmr_1s;
|
||
static bool f_stream; // first audio data received
|
||
static bool f_firstPacket;
|
||
static int bytesDecoded;
|
||
static uint32_t byteCounter; // count received data
|
||
static uint32_t tmr_1s; // timer 1 sec
|
||
static uint32_t loopCnt; // count loops if clientbuffer is empty
|
||
static uint8_t ts_packet[188]; // m3u8 transport stream is 188 bytes long
|
||
uint8_t ts_packetStart = 0;
|
||
uint8_t ts_packetLength = 0;
|
||
static uint8_t ts_packetPtr = 0;
|
||
const uint8_t ts_packetsize = 188;
|
||
static size_t chunkSize = 0;
|
||
(void)bytesDecoded;
|
||
// first call, set some values to default - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_f_firstCall) { // runs only ont time per connection, prepare for start
|
||
f_stream = false;
|
||
f_firstPacket = true;
|
||
byteCounter = 0;
|
||
bytesDecoded = 0;
|
||
chunkSize = 0;
|
||
loopCnt = 0;
|
||
tmr_1s = millis();
|
||
m_t0 = millis();
|
||
ts_packetPtr = 0;
|
||
ts_parsePacket(0, 0, 0); // reset ts routine
|
||
m_controlCounter = 0;
|
||
m_f_firstCall = false;
|
||
}
|
||
|
||
if(getDatamode() != AUDIO_DATA) return; // guard
|
||
|
||
if(InBuff.freeSpace() < maxFrameSize && f_stream){playAudioData(); return;}
|
||
|
||
availableBytes = _client->available();
|
||
if(availableBytes){
|
||
if(m_f_chunked) chunkSize = chunkedDataTransfer();
|
||
int res = _client->read(ts_packet + ts_packetPtr, ts_packetsize - ts_packetPtr);
|
||
if(res > 0){
|
||
ts_packetPtr += res;
|
||
byteCounter += res;
|
||
if(ts_packetPtr < ts_packetsize) return;
|
||
ts_packetPtr = 0;
|
||
if(f_firstPacket){ // search for ID3 Header in the first packet
|
||
f_firstPacket = false;
|
||
uint8_t ID3_HeaderSize = process_m3u8_ID3_Header(ts_packet);
|
||
if(ID3_HeaderSize > ts_packetsize){
|
||
log_e("ID3 Header is too big");
|
||
stopSong();
|
||
return;
|
||
}
|
||
if(ID3_HeaderSize){
|
||
memcpy(ts_packet, &ts_packet[ID3_HeaderSize], ts_packetsize - ID3_HeaderSize);
|
||
ts_packetPtr = ts_packetsize - ID3_HeaderSize;
|
||
return;
|
||
}
|
||
}
|
||
ts_parsePacket(&ts_packet[0], &ts_packetStart, &ts_packetLength);
|
||
|
||
if(ts_packetLength) {
|
||
size_t ws = InBuff.writeSpace();
|
||
if(ws >= ts_packetLength){
|
||
memcpy(InBuff.getWritePtr(), ts_packet + ts_packetStart, ts_packetLength);
|
||
InBuff.bytesWritten(ts_packetLength);
|
||
}
|
||
else{
|
||
memcpy(InBuff.getWritePtr(), ts_packet + ts_packetStart, ws);
|
||
InBuff.bytesWritten(ws);
|
||
memcpy(InBuff.getWritePtr(), &ts_packet[ws + ts_packetStart], ts_packetLength -ws);
|
||
InBuff.bytesWritten(ts_packetLength -ws);
|
||
}
|
||
}
|
||
if(byteCounter == m_contentlength || byteCounter == chunkSize){
|
||
byteCounter = 0;
|
||
m_f_continue = true;
|
||
}
|
||
if(byteCounter > m_contentlength) log_e("byteCounter overflow");
|
||
}
|
||
|
||
}
|
||
|
||
// timer, triggers every second - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if((tmr_1s + 1000) < millis()) {
|
||
f_tmr_1s = true; // flag will be set every second for one loop only
|
||
tmr_1s = millis();
|
||
}
|
||
|
||
// if the buffer is often almost empty issue a warning - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(InBuff.bufferFilled() < maxFrameSize && f_stream){
|
||
static uint8_t cnt_slow = 0;
|
||
cnt_slow ++;
|
||
if(f_tmr_1s) {
|
||
if(cnt_slow > 50 && audio_info) audio_info("slow stream, dropouts are possible");
|
||
f_tmr_1s = false;
|
||
cnt_slow = 0;
|
||
}
|
||
}
|
||
|
||
// if the buffer can't filled for several seconds try a new connection - - - - - - - - - - - - - - - - - - - - - -
|
||
if(f_stream && !availableBytes){
|
||
loopCnt++;
|
||
if(loopCnt > 200000) { // wait several seconds
|
||
loopCnt = 0;
|
||
AUDIO_INFO("Stream lost -> try new connection");
|
||
httpPrint(m_lastHost);
|
||
return;
|
||
}
|
||
}
|
||
if(availableBytes) loopCnt = 0;
|
||
|
||
// buffer fill routine - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(true) { // statement has no effect
|
||
if(InBuff.bufferFilled() > maxFrameSize && !f_stream) { // waiting for buffer filled
|
||
f_stream = true; // ready to play the audio data
|
||
uint16_t filltime = millis() - m_t0;
|
||
if(m_f_Log) AUDIO_INFO("stream ready");
|
||
if(m_f_Log) AUDIO_INFO("buffer filled in %d ms", filltime);
|
||
}
|
||
if(!f_stream) return;
|
||
}
|
||
|
||
// play audio data - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(f_stream){
|
||
static uint8_t cnt = 0;
|
||
cnt++;
|
||
if(cnt == 6){playAudioData(); cnt = 0;}
|
||
}
|
||
return;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::processWebStreamHLS() {
|
||
|
||
const uint16_t maxFrameSize = InBuff.getMaxBlockSize(); // every mp3/aac frame is not bigger
|
||
uint32_t availableBytes; // available bytes in stream
|
||
static bool f_tmr_1s;
|
||
static bool f_stream; // first audio data received
|
||
static int bytesDecoded;
|
||
static uint32_t byteCounter; // count received data
|
||
static size_t chunkSize = 0;
|
||
static uint32_t tmr_1s; // timer 1 sec
|
||
static uint32_t loopCnt; // count loops if clientbuffer is empty
|
||
(void)bytesDecoded;
|
||
// first call, set some values to default - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_f_firstCall) { // runs only ont time per connection, prepare for start
|
||
f_stream = false;
|
||
byteCounter = 0;
|
||
bytesDecoded = 0;
|
||
chunkSize = 0;
|
||
loopCnt = 0;
|
||
tmr_1s = millis();
|
||
m_t0 = millis();
|
||
m_f_firstCall = false;
|
||
}
|
||
|
||
if(getDatamode() != AUDIO_DATA) return; // guard
|
||
|
||
availableBytes = _client->available();
|
||
if(availableBytes){
|
||
if(m_f_chunked) chunkSize = chunkedDataTransfer();
|
||
size_t bytesWasWritten = 0;
|
||
if(InBuff.writeSpace() >= availableBytes){
|
||
bytesWasWritten = _client->read(InBuff.getWritePtr(), availableBytes);
|
||
}
|
||
else{
|
||
bytesWasWritten = _client->read(InBuff.getWritePtr(), InBuff.writeSpace());
|
||
}
|
||
InBuff.bytesWritten(bytesWasWritten);
|
||
byteCounter += bytesWasWritten;
|
||
if(byteCounter == m_contentlength || byteCounter == chunkSize){
|
||
byteCounter = 0;
|
||
m_f_continue = true;
|
||
}
|
||
}
|
||
|
||
// timer, triggers every second - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if((tmr_1s + 1000) < millis()) {
|
||
f_tmr_1s = true; // flag will be set every second for one loop only
|
||
tmr_1s = millis();
|
||
}
|
||
|
||
// if the buffer is often almost empty issue a warning - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(InBuff.bufferFilled() < maxFrameSize && f_stream){
|
||
static uint8_t cnt_slow = 0;
|
||
cnt_slow ++;
|
||
if(f_tmr_1s) {
|
||
if(cnt_slow > 25 && audio_info) audio_info("slow stream, dropouts are possible");
|
||
f_tmr_1s = false;
|
||
cnt_slow = 0;
|
||
}
|
||
}
|
||
|
||
// if the buffer can't filled for several seconds try a new connection - - - - - - - - - - - - - - - - - - - - - -
|
||
if(f_stream && !availableBytes){
|
||
loopCnt++;
|
||
if(loopCnt > 200000) { // wait several seconds
|
||
loopCnt = 0;
|
||
AUDIO_INFO("Stream lost -> try new connection");
|
||
httpPrint(m_lastHost);
|
||
return;
|
||
}
|
||
}
|
||
if(availableBytes) loopCnt = 0;
|
||
|
||
if(InBuff.bufferFilled() > maxFrameSize && !f_stream) { // waiting for buffer filled
|
||
f_stream = true; // ready to play the audio data
|
||
uint16_t filltime = millis() - m_t0;
|
||
if(m_f_Log) AUDIO_INFO("stream ready");
|
||
if(m_f_Log) AUDIO_INFO("buffer filled in %d ms", filltime);
|
||
}
|
||
|
||
// play audio data - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(f_stream) playAudioData();
|
||
return;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::playAudioData(){
|
||
|
||
if(InBuff.bufferFilled() < InBuff.getMaxBlockSize()) return; // guard
|
||
|
||
int bytesDecoded = sendBytes(InBuff.getReadPtr(), InBuff.getMaxBlockSize());
|
||
|
||
if(bytesDecoded < 0) { // no syncword found or decode error, try next chunk
|
||
log_i("err bytesDecoded %i", bytesDecoded);
|
||
uint8_t next = 200;
|
||
if(InBuff.bufferFilled() < next) next = InBuff.bufferFilled();
|
||
InBuff.bytesWasRead(next); // try next chunk
|
||
m_bytesNotDecoded += next;
|
||
}
|
||
else {
|
||
if(bytesDecoded > 0) {InBuff.bytesWasRead(bytesDecoded); return;}
|
||
if(bytesDecoded == 0) return; // syncword at pos0 found
|
||
}
|
||
|
||
return;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::parseHttpResponseHeader() { // this is the response to a GET / request
|
||
|
||
if(getDatamode() != HTTP_RESPONSE_HEADER) return false;
|
||
if(_client->available() == 0) return false;
|
||
|
||
char rhl[512]; // responseHeaderline
|
||
bool ct_seen = false;
|
||
uint32_t ctime = millis();
|
||
uint32_t timeout = 2500; // ms
|
||
|
||
while(true){ // outer while
|
||
uint16_t pos = 0;
|
||
if((millis() - ctime) > timeout) {
|
||
log_e("timeout");
|
||
goto exit;
|
||
}
|
||
while(_client->available()){
|
||
uint8_t b = _client->read();
|
||
if(b == '\n') {
|
||
if(!pos){ // empty line received, is the last line of this responseHeader
|
||
if(ct_seen) goto lastToDo;
|
||
else goto exit;
|
||
}
|
||
break;
|
||
}
|
||
if(b == '\r') rhl[pos] = 0;
|
||
if(b < 0x20) continue;
|
||
rhl[pos] = b;
|
||
pos++;
|
||
if(pos == 511){pos = 510; continue;}
|
||
if(pos == 510){
|
||
rhl[pos] = '\0';
|
||
if(m_f_Log) log_i("responseHeaderline overflow");
|
||
}
|
||
} // inner while
|
||
|
||
if(!pos){vTaskDelay(3); continue;}
|
||
|
||
if(m_f_Log) {log_i("httpResponseHeader: %s", rhl);}
|
||
|
||
int16_t posColon = indexOf(rhl, ":", 0); // lowercase all letters up to the colon
|
||
if(posColon >= 0) {
|
||
for(int i=0; i< posColon; i++) {
|
||
rhl[i] = toLowerCase(rhl[i]);
|
||
}
|
||
}
|
||
|
||
if(startsWith(rhl, "HTTP/")){ // HTTP status error code
|
||
char statusCode[5];
|
||
statusCode[0] = rhl[9];
|
||
statusCode[1] = rhl[10];
|
||
statusCode[2] = rhl[11];
|
||
statusCode[3] = '\0';
|
||
int sc = atoi(statusCode);
|
||
if(sc > 310){ // e.g. HTTP/1.1 301 Moved Permanently
|
||
if(audio_showstreamtitle) audio_showstreamtitle(rhl);
|
||
goto exit;
|
||
}
|
||
}
|
||
|
||
else if(startsWith(rhl, "content-type:")){ // content-type: text/html; charset=UTF-8
|
||
int idx = indexOf(rhl + 13, ";");
|
||
if(idx >0) rhl[13 + idx] = '\0';
|
||
if(parseContentType(rhl + 13)) ct_seen = true;
|
||
else goto exit;
|
||
}
|
||
|
||
else if(startsWith(rhl, "location:")) {
|
||
int pos = indexOf(rhl, "http", 0);
|
||
if(pos >= 0){
|
||
const char* c_host = (rhl + pos);
|
||
if(strcmp(c_host, m_lastHost) != 0) { // prevent a loop
|
||
int pos_slash = indexOf(c_host, "/", 9);
|
||
if(pos_slash > 9){
|
||
if(!strncmp(c_host, m_lastHost, pos_slash)){
|
||
AUDIO_INFO("redirect to new extension at existing host \"%s\"", c_host);
|
||
if(m_playlistFormat == FORMAT_M3U8) {
|
||
strcpy(m_lastHost, c_host);
|
||
m_f_m3u8data = true;
|
||
}
|
||
httpPrint(c_host);
|
||
while(_client->available()) _client->read(); // empty client buffer
|
||
return true;
|
||
}
|
||
}
|
||
AUDIO_INFO("redirect to new host \"%s\"", c_host);
|
||
connecttohost(c_host);
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
else if(startsWith(rhl, "content-encoding:")){
|
||
if(indexOf(rhl, "gzip")){
|
||
AUDIO_INFO("can't extract gzip");
|
||
goto exit;
|
||
}
|
||
}
|
||
|
||
else if(startsWith(rhl, "content-disposition:")) {
|
||
int pos1, pos2; // pos3;
|
||
// e.g we have this headerline: content-disposition: attachment; filename=stream.asx
|
||
// filename is: "stream.asx"
|
||
pos1 = indexOf(rhl, "filename=", 0);
|
||
if(pos1 > 0){
|
||
pos1 += 9;
|
||
if(rhl[pos1] == '\"') pos1++; // remove '\"' around filename if present
|
||
pos2 = strlen(rhl);
|
||
if(rhl[pos2 - 1] == '\"') rhl[pos2 - 1] = '\0';
|
||
}
|
||
AUDIO_INFO("Filename is %s", rhl + pos1);
|
||
}
|
||
|
||
// if(startsWith(rhl, "set-cookie:") ||
|
||
// startsWith(rhl, "pragma:") ||
|
||
// startsWith(rhl, "expires:") ||
|
||
// startsWith(rhl, "cache-control:") ||
|
||
// startsWith(rhl, "icy-pub:") ||
|
||
// startsWith(rhl, "p3p:") ||
|
||
// startsWith(rhl, "accept-ranges:") ){
|
||
// ; // do nothing
|
||
// }
|
||
|
||
else if(startsWith(rhl, "connection:")) {
|
||
if(indexOf(rhl, "close", 0) >= 0) {; /* do nothing */}
|
||
}
|
||
|
||
else if(startsWith(rhl, "icy-genre:")) {
|
||
; // do nothing Ambient, Rock, etc
|
||
}
|
||
|
||
else if(startsWith(rhl, "icy-br:")) {
|
||
const char* c_bitRate = (rhl + 7);
|
||
int32_t br = atoi(c_bitRate); // Found bitrate tag, read the bitrate in Kbit
|
||
br = br * 1000;
|
||
setBitrate(br);
|
||
sprintf(chbuf, "%d", getBitRate());
|
||
if(audio_bitrate) audio_bitrate(chbuf);
|
||
}
|
||
|
||
else if(startsWith(rhl, "icy-metaint:")) {
|
||
const char* c_metaint = (rhl + 12);
|
||
int32_t i_metaint = atoi(c_metaint);
|
||
m_metaint = i_metaint;
|
||
if(m_metaint) m_f_swm = false ; // Multimediastream
|
||
}
|
||
|
||
else if(startsWith(rhl, "icy-name:")) {
|
||
char* c_icyname = (rhl + 9); // Get station name
|
||
trim(c_icyname);
|
||
if(strlen(c_icyname) > 0) {
|
||
if(!m_f_Log) AUDIO_INFO("icy-name: %s", c_icyname);
|
||
if(audio_showstation) audio_showstation(c_icyname);
|
||
}
|
||
}
|
||
|
||
else if(startsWith(rhl, "content-length:")) {
|
||
const char* c_cl = (rhl + 15);
|
||
int32_t i_cl = atoi(c_cl);
|
||
m_contentlength = i_cl;
|
||
m_streamType = ST_WEBFILE; // Stream comes from a fileserver
|
||
if(m_f_Log) AUDIO_INFO("content-length: %i", m_contentlength);
|
||
}
|
||
|
||
else if(startsWith(rhl, "icy-description:")) {
|
||
const char* c_idesc = (rhl + 16);
|
||
while(c_idesc[0] == ' ') c_idesc++;
|
||
latinToUTF8(rhl, sizeof(rhl)); // if already UTF-0 do nothing, otherwise convert to UTF-8
|
||
if(audio_icydescription) audio_icydescription(c_idesc);
|
||
}
|
||
|
||
else if((startsWith(rhl, "transfer-encoding:"))){
|
||
if(endsWith(rhl, "chunked") || endsWith(rhl, "Chunked") ) { // Station provides chunked transfer
|
||
m_f_chunked = true;
|
||
if(!m_f_Log) AUDIO_INFO("chunked data transfer");
|
||
m_chunkcount = 0; // Expect chunkcount in DATA
|
||
}
|
||
}
|
||
|
||
else if(startsWith(rhl, "icy-url:")) {
|
||
char* icyurl = (rhl + 8);
|
||
trim(icyurl);
|
||
if(audio_icyurl) audio_icyurl(icyurl);
|
||
}
|
||
|
||
else if(startsWith(rhl, "www-authenticate:")) {
|
||
AUDIO_INFO("authentification failed, wrong credentials?");
|
||
goto exit;
|
||
}
|
||
else {;}
|
||
} // outer while
|
||
|
||
exit: // termination condition
|
||
if(audio_showstation) audio_showstation("");
|
||
if(audio_icydescription) audio_icydescription("");
|
||
if(audio_icyurl) audio_icyurl("");
|
||
m_lastHost[0] = '\0';
|
||
setDatamode(AUDIO_NONE);
|
||
stopSong();
|
||
return false;
|
||
|
||
lastToDo:
|
||
if(m_codec != CODEC_NONE){
|
||
setDatamode(AUDIO_DATA); // Expecting data now
|
||
if(!initializeDecoder()) return false;
|
||
if(m_f_Log) {log_i("Switch to DATA, metaint is %d", m_metaint);}
|
||
if(m_playlistFormat != FORMAT_M3U8 && audio_lasthost) audio_lasthost(m_lastHost);
|
||
m_controlCounter = 0;
|
||
m_f_firstCall = true;
|
||
}
|
||
else if(m_playlistFormat != FORMAT_NONE){
|
||
setDatamode(AUDIO_PLAYLISTINIT); // playlist expected
|
||
if(m_f_Log) {log_i("now parse playlist");}
|
||
}
|
||
else{
|
||
AUDIO_INFO("unknown content found at: %s", m_lastHost);
|
||
goto exit;
|
||
}
|
||
return true;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio:: initializeDecoder(){
|
||
switch(m_codec){
|
||
case CODEC_MP3:
|
||
if(!MP3Decoder_AllocateBuffers()) goto exit;
|
||
AUDIO_INFO("MP3Decoder has been initialized, free Heap: %u bytes", ESP.getFreeHeap());
|
||
InBuff.changeMaxBlockSize(m_frameSizeMP3);
|
||
break;
|
||
case CODEC_AAC:
|
||
if(!AACDecoder_IsInit()){
|
||
if(!AACDecoder_AllocateBuffers()) goto exit;
|
||
AUDIO_INFO("AACDecoder has been initialized, free Heap: %u bytes", ESP.getFreeHeap());
|
||
InBuff.changeMaxBlockSize(m_frameSizeAAC);
|
||
}
|
||
break;
|
||
case CODEC_M4A:
|
||
if(!AACDecoder_IsInit()){
|
||
if(!AACDecoder_AllocateBuffers()) goto exit;
|
||
AUDIO_INFO("AACDecoder has been initialized, free Heap: %u bytes", ESP.getFreeHeap());
|
||
InBuff.changeMaxBlockSize(m_frameSizeAAC);
|
||
}
|
||
break;
|
||
case CODEC_FLAC:
|
||
if(!psramFound()){
|
||
AUDIO_INFO("FLAC works only with PSRAM!");
|
||
goto exit;
|
||
}
|
||
if(!FLACDecoder_AllocateBuffers()) goto exit;
|
||
InBuff.changeMaxBlockSize(m_frameSizeFLAC);
|
||
AUDIO_INFO("FLACDecoder has been initialized, free Heap: %u bytes", ESP.getFreeHeap());
|
||
break;
|
||
case CODEC_WAV:
|
||
InBuff.changeMaxBlockSize(m_frameSizeWav);
|
||
break;
|
||
case CODEC_OGG:
|
||
m_codec = CODEC_OGG;
|
||
AUDIO_INFO("ogg not supported");
|
||
AUDIO_ERROR("ogg not supported");
|
||
goto exit;
|
||
break;
|
||
default:
|
||
goto exit;
|
||
break;
|
||
}
|
||
return true;
|
||
|
||
exit:
|
||
stopSong();
|
||
return false;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
uint16_t Audio::readMetadata(uint16_t maxBytes, bool first) {
|
||
|
||
static uint16_t pos_ml = 0; // determines the current position in metaline
|
||
static uint16_t metalen = 0;
|
||
uint16_t res = 0;
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(first){
|
||
pos_ml = 0;
|
||
metalen = 0;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(!maxBytes) return 0; // guard
|
||
|
||
if(!metalen) {
|
||
int b = _client->read(); // First byte of metadata?
|
||
metalen = b * 16 ; // New count for metadata including length byte
|
||
if(metalen > 512){
|
||
AUDIO_INFO("Metadata block to long! Skipping all Metadata from now on.");
|
||
m_f_swm = true; // expect stream without metadata
|
||
}
|
||
pos_ml = 0; chbuf[pos_ml] = 0; // Prepare for new line
|
||
res = 1;
|
||
}
|
||
if(!metalen) {m_metacount = m_metaint; return res;}
|
||
|
||
uint16_t a = _client->readBytes(&chbuf[pos_ml], min((uint16_t)(metalen - pos_ml), (uint16_t)(maxBytes -1)));
|
||
res += a;
|
||
pos_ml += a;
|
||
if(pos_ml == metalen) {
|
||
metalen = 0;
|
||
chbuf[pos_ml] = '\0';
|
||
m_metacount = m_metaint;
|
||
if(strlen(chbuf)) { // Any info present?
|
||
// metaline contains artist and song name. For example:
|
||
// "StreamTitle='Don McLean - American Pie';StreamUrl='';"
|
||
// Sometimes it is just other info like:
|
||
// "StreamTitle='60s 03 05 Magic60s';StreamUrl='';"
|
||
// Isolate the StreamTitle, remove leading and trailing quotes if present.
|
||
if(m_f_Log) log_i("metaline %s", chbuf);
|
||
latinToUTF8(chbuf, sizeof(chbuf)); // convert to UTF-8 if necessary
|
||
int pos = indexOf(chbuf, "song_spot", 0); // remove some irrelevant infos
|
||
if(pos > 3) { // e.g. song_spot="T" MediaBaseId="0" itunesTrackId="0"
|
||
chbuf[pos] = 0;
|
||
}
|
||
showstreamtitle(chbuf); // Show artist and title if present in metadata
|
||
}
|
||
pos_ml = 0;
|
||
}
|
||
return res;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::parseContentType(char* ct) {
|
||
|
||
enum : int {CT_NONE, CT_MP3, CT_AAC, CT_M4A, CT_WAV, CT_OGG, CT_FLAC, CT_PLS, CT_M3U, CT_ASX,
|
||
CT_M3U8, CT_TXT, CT_AACP};
|
||
|
||
strlwr(ct);
|
||
trim(ct);
|
||
|
||
m_codec = CODEC_NONE;
|
||
int ct_val = CT_NONE;
|
||
|
||
if(!strcmp(ct, "audio/mpeg")) ct_val = CT_MP3;
|
||
else if(!strcmp(ct, "audio/mpeg3")) ct_val = CT_MP3;
|
||
else if(!strcmp(ct, "audio/x-mpeg")) ct_val = CT_MP3;
|
||
else if(!strcmp(ct, "audio/x-mpeg-3")) ct_val = CT_MP3;
|
||
else if(!strcmp(ct, "audio/mp3")) ct_val = CT_MP3;
|
||
|
||
else if(!strcmp(ct, "audio/aac")) ct_val = CT_AAC;
|
||
else if(!strcmp(ct, "audio/x-aac")) ct_val = CT_AAC;
|
||
else if(!strcmp(ct, "audio/aacp")){ ct_val = CT_AAC; if(m_playlistFormat == FORMAT_M3U8) m_f_ts = true;}
|
||
else if(!strcmp(ct, "video/mp2t")){ ct_val = CT_AAC; m_f_ts = true;} // assume AAC transport stream
|
||
else if(!strcmp(ct, "audio/mp4")) ct_val = CT_M4A;
|
||
else if(!strcmp(ct, "audio/m4a")) ct_val = CT_M4A;
|
||
|
||
else if(!strcmp(ct, "audio/wav")) ct_val = CT_WAV;
|
||
else if(!strcmp(ct, "audio/x-wav")) ct_val = CT_WAV;
|
||
|
||
else if(!strcmp(ct, "audio/flac")) ct_val = CT_FLAC;
|
||
|
||
else if(!strcmp(ct, "audio/scpls")) ct_val = CT_PLS;
|
||
else if(!strcmp(ct, "audio/x-scpls")) ct_val = CT_PLS;
|
||
else if(!strcmp(ct, "application/pls+xml")) ct_val = CT_PLS;
|
||
else if(!strcmp(ct, "audio/mpegurl")) ct_val = CT_M3U;
|
||
else if(!strcmp(ct, "audio/x-mpegurl")) ct_val = CT_M3U;
|
||
else if(!strcmp(ct, "audio/ms-asf")) ct_val = CT_ASX;
|
||
else if(!strcmp(ct, "video/x-ms-asf")) ct_val = CT_ASX;
|
||
|
||
else if(!strcmp(ct, "application/ogg")) ct_val = CT_OGG;
|
||
else if(!strcmp(ct, "application/vnd.apple.mpegurl")) ct_val = CT_M3U8;
|
||
else if(!strcmp(ct, "application/x-mpegurl")) ct_val =CT_M3U8;
|
||
|
||
else if(!strcmp(ct, "application/octet-stream")) ct_val = CT_TXT; // ??? listen.radionomy.com/1oldies before redirection
|
||
else if(!strcmp(ct, "text/html")) ct_val = CT_TXT;
|
||
else if(!strcmp(ct, "text/plain")) ct_val = CT_TXT;
|
||
|
||
else if(ct_val == CT_NONE){
|
||
AUDIO_INFO("ContentType %s not supported", ct);
|
||
AUDIO_ERROR("ContentType %s not supported", ct);
|
||
return false; // nothing valid had been seen
|
||
}
|
||
else {;}
|
||
|
||
switch(ct_val){
|
||
case CT_MP3:
|
||
m_codec = CODEC_MP3;
|
||
if(m_f_Log) { log_i("ContentType %s, format is mp3", ct); } //ok is likely mp3
|
||
if(audio_info) audio_info("format is mp3");
|
||
break;
|
||
case CT_AAC:
|
||
m_codec = CODEC_AAC;
|
||
if(m_f_Log) { log_i("ContentType %s, format is aac", ct); }
|
||
if(audio_info) audio_info("format is aac");
|
||
break;
|
||
case CT_M4A:
|
||
m_codec = CODEC_M4A;
|
||
if(m_f_Log) { log_i("ContentType %s, format is aac", ct); }
|
||
if(audio_info) audio_info("format is aac");
|
||
break;
|
||
case CT_FLAC:
|
||
m_codec = CODEC_FLAC;
|
||
if(m_f_Log) { log_i("ContentType %s, format is flac", ct); }
|
||
if(audio_info) audio_info("format is flac");
|
||
break;
|
||
case CT_WAV:
|
||
m_codec = CODEC_WAV;
|
||
if(m_f_Log) { log_i("ContentType %s, format is wav", ct); }
|
||
if(audio_info) audio_info("format is wav");
|
||
break;
|
||
case CT_OGG:
|
||
m_codec = CODEC_OGG;
|
||
if(m_f_Log) { log_i("ContentType %s found", ct); }
|
||
if(audio_info) audio_info("format is ogg");
|
||
break;
|
||
|
||
case CT_PLS:
|
||
m_playlistFormat = FORMAT_PLS;
|
||
break;
|
||
case CT_M3U:
|
||
m_playlistFormat = FORMAT_M3U;
|
||
break;
|
||
case CT_ASX:
|
||
m_playlistFormat = FORMAT_ASX;
|
||
break;
|
||
case CT_M3U8:
|
||
m_playlistFormat = FORMAT_M3U8;
|
||
break;
|
||
case CT_TXT: // overwrite text/plain
|
||
if(m_expectedCodec == CODEC_AAC){ m_codec = CODEC_AAC; if(m_f_Log) log_i("set ct from M3U8 to AAC");}
|
||
if(m_expectedCodec == CODEC_MP3){ m_codec = CODEC_MP3; if(m_f_Log) log_i("set ct from M3U8 to MP3");}
|
||
|
||
if(m_expectedPlsFmt == FORMAT_ASX){ m_playlistFormat = FORMAT_ASX; if(m_f_Log) log_i("set playlist format to ASX");}
|
||
if(m_expectedPlsFmt == FORMAT_M3U){ m_playlistFormat = FORMAT_M3U; if(m_f_Log) log_i("set playlist format to M3U");}
|
||
if(m_expectedPlsFmt == FORMAT_M3U8){m_playlistFormat = FORMAT_M3U8; if(m_f_Log) log_i("set playlist format to M3U8");}
|
||
if(m_expectedPlsFmt == FORMAT_PLS){ m_playlistFormat = FORMAT_PLS; if(m_f_Log) log_i("set playlist format to PLS");}
|
||
break;
|
||
default:
|
||
AUDIO_INFO("%s, unsupported audio format", ct);
|
||
return false;
|
||
break;
|
||
}
|
||
return true;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::showstreamtitle(const char* ml) {
|
||
// example for ml:
|
||
// StreamTitle='Oliver Frank - Mega Hitmix';StreamUrl='www.radio-welle-woerthersee.at';
|
||
// or adw_ad='true';durationMilliseconds='10135';adId='34254';insertionType='preroll';
|
||
|
||
int16_t idx1, idx2;
|
||
uint16_t i = 0, hash = 0;
|
||
|
||
idx1 = indexOf(ml, "StreamTitle=", 0);
|
||
if(idx1 >= 0){ // Streamtitle found
|
||
idx2 = indexOf(ml, ";", idx1);
|
||
char *sTit;
|
||
if(idx2 >= 0){sTit = strndup(ml + idx1, idx2 + 1); sTit[idx2] = '\0';}
|
||
else sTit = strdup(ml);
|
||
|
||
while(i < strlen(sTit)){hash += sTit[i] * i+1; i++;}
|
||
|
||
if(m_streamTitleHash != hash){
|
||
m_streamTitleHash = hash;
|
||
AUDIO_INFO("%s", sTit);
|
||
uint8_t pos = 12; // remove "StreamTitle="
|
||
if(sTit[pos] == '\'') pos++; // remove leading \'
|
||
if(sTit[strlen(sTit) - 1] == '\'') sTit[strlen(sTit) -1] = '\0'; // remove trailing \'
|
||
if(sTit[pos]==0xEF && sTit[pos+1] == 0xBB && sTit[pos+2] == 0xBF) pos+=3; // remove ZERO WIDTH NO-BREAK SPACE
|
||
if(audio_showstreamtitle) audio_showstreamtitle(sTit + pos);
|
||
}
|
||
if(sTit) {free(sTit); sTit = NULL;}
|
||
}
|
||
m_streamTitleHash = 0;
|
||
idx1 = indexOf(ml, "StreamUrl=", 0);
|
||
idx2 = indexOf(ml, ";", idx1);
|
||
if(idx1 >= 0 && idx2 > idx1){ // StreamURL found
|
||
uint16_t len = idx2 - idx1;
|
||
char *sUrl;
|
||
sUrl = strndup(ml + idx1, len + 1); sUrl[len] = '\0';
|
||
|
||
while(i < strlen(sUrl)){hash += sUrl[i] * i+1; i++;}
|
||
if(m_streamTitleHash != hash){
|
||
m_streamTitleHash = hash;
|
||
AUDIO_INFO("%s", sUrl);
|
||
}
|
||
if(sUrl) {free(sUrl); sUrl = NULL;}
|
||
}
|
||
|
||
idx1 = indexOf(ml, "adw_ad=", 0);
|
||
if(idx1 >= 0){ // Advertisement found
|
||
idx1 = indexOf(ml, "durationMilliseconds=", 0);
|
||
idx2 = indexOf(ml, ";", idx1);
|
||
if(idx1 >= 0 && idx2 > idx1){
|
||
uint16_t len = idx2 - idx1;
|
||
char *sAdv;
|
||
sAdv = strndup(ml + idx1, len + 1); sAdv[len] = '\0';
|
||
AUDIO_INFO("%s", sAdv);
|
||
uint8_t pos = 21; // remove "StreamTitle="
|
||
if(sAdv[pos] == '\'') pos++; // remove leading \'
|
||
if(sAdv[strlen(sAdv) - 1] == '\'') sAdv[strlen(sAdv) -1] = '\0'; // remove trailing \'
|
||
if(audio_commercial) audio_commercial(sAdv + pos);
|
||
if(sAdv){free(sAdv); sAdv = NULL;}
|
||
}
|
||
}
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::showCodecParams(){
|
||
// print Codec Parameter (mp3, aac) in audio_info()
|
||
|
||
AUDIO_INFO("Channels: %i", getChannels());
|
||
AUDIO_INFO("SampleRate: %i", getSampleRate());
|
||
AUDIO_INFO("BitsPerSample: %i", getBitsPerSample());
|
||
if(getBitRate()) {AUDIO_INFO("BitRate: %i", getBitRate());}
|
||
else {AUDIO_INFO("BitRate: N/A");}
|
||
|
||
if(m_codec == CODEC_AAC || m_codec == CODEC_M4A){
|
||
uint8_t answ;
|
||
if((answ = AACGetFormat()) < 4){
|
||
const char hf[4][8] = {"unknown", "ADTS", "ADIF", "RAW"};
|
||
sprintf(chbuf, "AAC HeaderFormat: %s", hf[answ]);
|
||
audio_info(chbuf);
|
||
}
|
||
if(answ == 1){ // ADTS Header
|
||
const char co[2][23] = {"MPEG-4", "MPEG-2"};
|
||
sprintf(chbuf, "AAC Codec: %s", co[AACGetID()]);
|
||
audio_info(chbuf);
|
||
if(AACGetProfile() <5){
|
||
const char pr[4][23] = {"Main", "LowComplexity", "Scalable Sampling Rate", "reserved"};
|
||
sprintf(chbuf, "AAC Profile: %s", pr[answ]);
|
||
audio_info(chbuf);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
int Audio::findNextSync(uint8_t* data, size_t len){
|
||
// Mp3 and aac audio data are divided into frames. At the beginning of each frame there is a sync word.
|
||
// The sync word is 0xFFF. This is followed by information about the structure of the frame.
|
||
// Wav files have no frames
|
||
// Return: 0 the synchronous word was found at position 0
|
||
// > 0 is the offset to the next sync word
|
||
// -1 the sync word was not found within the block with the length len
|
||
|
||
int nextSync;
|
||
static uint32_t swnf = 0;
|
||
if(m_codec == CODEC_WAV) {
|
||
m_f_playing = true; nextSync = 0;
|
||
}
|
||
if(m_codec == CODEC_MP3) {
|
||
nextSync = MP3FindSyncWord(data, len);
|
||
}
|
||
if(m_codec == CODEC_AAC) {
|
||
nextSync = AACFindSyncWord(data, len);
|
||
}
|
||
if(m_codec == CODEC_M4A) {
|
||
AACSetRawBlockParams(0, 2,44100, 1); m_f_playing = true; nextSync = 0;
|
||
}
|
||
if(m_codec == CODEC_FLAC) {
|
||
FLACSetRawBlockParams(m_flacNumChannels, m_flacSampleRate,
|
||
m_flacBitsPerSample, m_flacTotalSamplesInStream, m_audioDataSize);
|
||
nextSync = FLACFindSyncWord(data, len);
|
||
}
|
||
if(m_codec == CODEC_OGG_FLAC) {
|
||
FLACSetRawBlockParams(m_flacNumChannels, m_flacSampleRate,
|
||
m_flacBitsPerSample, m_flacTotalSamplesInStream, m_audioDataSize);
|
||
nextSync = FLACFindSyncWord(data, len);
|
||
}
|
||
if(nextSync == -1) {
|
||
if(audio_info && swnf == 0) audio_info("syncword not found");
|
||
if(m_codec == CODEC_OGG_FLAC){
|
||
nextSync = len;
|
||
}
|
||
else {
|
||
swnf++; // syncword not found counter, can be multimediadata
|
||
}
|
||
}
|
||
if (nextSync == 0){
|
||
if(audio_info && swnf>0){
|
||
sprintf(chbuf, "syncword not found %i times", swnf);
|
||
audio_info(chbuf);
|
||
swnf = 0;
|
||
}
|
||
else {
|
||
if(audio_info) audio_info("syncword found at pos 0");
|
||
}
|
||
}
|
||
if(nextSync > 0){
|
||
AUDIO_INFO("syncword found at pos %i", nextSync);
|
||
}
|
||
return nextSync;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
int Audio::sendBytes(uint8_t* data, size_t len) {
|
||
int bytesLeft;
|
||
static bool f_setDecodeParamsOnce = true;
|
||
int nextSync = 0;
|
||
if(!m_f_playing) {
|
||
f_setDecodeParamsOnce = true;
|
||
nextSync = findNextSync(data, len);
|
||
if(nextSync == 0) { m_f_playing = true;}
|
||
return nextSync;
|
||
}
|
||
// m_f_playing is true at this pos
|
||
bytesLeft = len;
|
||
int ret = 0;
|
||
int bytesDecoded = 0;
|
||
|
||
switch(m_codec){
|
||
case CODEC_WAV: memmove(m_outBuff, data , len); //copy len data in outbuff and set validsamples and bytesdecoded=len
|
||
if(getBitsPerSample() == 16) m_validSamples = len / (2 * getChannels());
|
||
if(getBitsPerSample() == 8 ) m_validSamples = len / 2;
|
||
bytesLeft = 0; break;
|
||
case CODEC_MP3: ret = MP3Decode(data, &bytesLeft, m_outBuff, 0); break;
|
||
case CODEC_AAC: ret = AACDecode(data, &bytesLeft, m_outBuff); break;
|
||
case CODEC_M4A: ret = AACDecode(data, &bytesLeft, m_outBuff); break;
|
||
case CODEC_FLAC: ret = FLACDecode(data, &bytesLeft, m_outBuff); break;
|
||
case CODEC_OGG_FLAC: ret = FLACDecode(data, &bytesLeft, m_outBuff); break; // FLAC webstream wrapped in OGG
|
||
default: {log_e("no valid codec found codec = %d", m_codec); stopSong();}
|
||
}
|
||
|
||
bytesDecoded = len - bytesLeft;
|
||
if(bytesDecoded == 0 && ret == 0){ // unlikely framesize
|
||
if(audio_info) audio_info("framesize is 0, start decoding again");
|
||
m_f_playing = false; // seek for new syncword
|
||
// we're here because there was a wrong sync word
|
||
// so skip two sync bytes and seek for next
|
||
return 1;
|
||
}
|
||
if(ret < 0) { // Error, skip the frame...
|
||
if(m_f_Log) if(m_codec == CODEC_M4A){log_i("begin not found"); return 1;}
|
||
i2s_zero_dma_buffer((i2s_port_t)m_i2s_num);
|
||
if(!getChannels() && (ret == -2)) {
|
||
; // suppress errorcode MAINDATA_UNDERFLOW
|
||
}
|
||
else {
|
||
printDecodeError(ret);
|
||
m_f_playing = false; // seek for new syncword
|
||
}
|
||
if(!bytesDecoded) bytesDecoded = 2;
|
||
return bytesDecoded;
|
||
}
|
||
else{ // ret>=0
|
||
if(f_setDecodeParamsOnce){
|
||
f_setDecodeParamsOnce = false;
|
||
m_PlayingStartTime = millis();
|
||
|
||
if(m_codec == CODEC_MP3){
|
||
setChannels(MP3GetChannels());
|
||
setSampleRate(MP3GetSampRate());
|
||
setBitsPerSample(MP3GetBitsPerSample());
|
||
setBitrate(MP3GetBitrate());
|
||
}
|
||
if(m_codec == CODEC_AAC || m_codec == CODEC_M4A){
|
||
setChannels(AACGetChannels());
|
||
setSampleRate(AACGetSampRate());
|
||
setBitsPerSample(AACGetBitsPerSample());
|
||
setBitrate(AACGetBitrate());
|
||
}
|
||
if(m_codec == CODEC_FLAC || m_codec == CODEC_OGG_FLAC){
|
||
setChannels(FLACGetChannels());
|
||
setSampleRate(FLACGetSampRate());
|
||
setBitsPerSample(FLACGetBitsPerSample());
|
||
setBitrate(FLACGetBitRate());
|
||
}
|
||
showCodecParams();
|
||
}
|
||
if(m_codec == CODEC_MP3){
|
||
m_validSamples = MP3GetOutputSamps() / getChannels();
|
||
}
|
||
if((m_codec == CODEC_AAC) || (m_codec == CODEC_M4A)){
|
||
m_validSamples = AACGetOutputSamps() / getChannels();
|
||
}
|
||
if((m_codec == CODEC_FLAC) || (m_codec == CODEC_OGG_FLAC)){
|
||
m_validSamples = FLACGetOutputSamps() / getChannels();
|
||
}
|
||
}
|
||
compute_audioCurrentTime(bytesDecoded);
|
||
|
||
if(audio_process_extern){
|
||
bool continueI2S = false;
|
||
audio_process_extern(m_outBuff, m_validSamples, &continueI2S);
|
||
if(!continueI2S){
|
||
return bytesDecoded;
|
||
}
|
||
}
|
||
while(m_validSamples) {
|
||
playChunk();
|
||
}
|
||
return bytesDecoded;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::compute_audioCurrentTime(int bd) {
|
||
static uint16_t loop_counter = 0;
|
||
static int old_bitrate = 0;
|
||
static uint64_t sum_bitrate = 0;
|
||
static boolean f_CBR = true; // constant bitrate
|
||
|
||
if(m_codec == CODEC_MP3) {setBitrate(MP3GetBitrate()) ;} // if not CBR, bitrate can be changed
|
||
if(m_codec == CODEC_M4A) {setBitrate(AACGetBitrate()) ;} // if not CBR, bitrate can be changed
|
||
if(m_codec == CODEC_AAC) {setBitrate(AACGetBitrate()) ;} // if not CBR, bitrate can be changed
|
||
if(m_codec == CODEC_FLAC){setBitrate(FLACGetBitRate());} // if not CBR, bitrate can be changed
|
||
if(!getBitRate()) return;
|
||
|
||
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_avr_bitrate == 0) { // first time
|
||
loop_counter = 0;
|
||
old_bitrate = 0;
|
||
sum_bitrate = 0;
|
||
f_CBR = true;
|
||
m_avr_bitrate = getBitRate();
|
||
old_bitrate = getBitRate();
|
||
}
|
||
if(!m_avr_bitrate) return;
|
||
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
|
||
if(loop_counter < 1000) loop_counter ++;
|
||
|
||
if((old_bitrate != getBitRate()) && f_CBR) {
|
||
if(audio_info) audio_info("VBR recognized, audioFileDuration is estimated");
|
||
f_CBR = false; // variable bitrate
|
||
}
|
||
old_bitrate = getBitRate();
|
||
|
||
if(!f_CBR) {
|
||
if(loop_counter > 20 && loop_counter < 200) {
|
||
// if VBR: m_avr_bitrate is average of the first values of m_bitrate
|
||
sum_bitrate += getBitRate();
|
||
m_avr_bitrate = sum_bitrate / (loop_counter - 20);
|
||
if(loop_counter == 199 && m_resumeFilePos){
|
||
m_audioCurrentTime = ((getFilePos() - m_audioDataStart - inBufferFilled()) / m_avr_bitrate) * 8; // #293
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
if(loop_counter == 2){
|
||
m_avr_bitrate = getBitRate();
|
||
if(m_resumeFilePos){ // if connecttoFS() is called with resumeFilePos != 0
|
||
m_audioCurrentTime = ((getFilePos() - m_audioDataStart - inBufferFilled()) / m_avr_bitrate) * 8; // #293
|
||
}
|
||
}
|
||
}
|
||
m_audioCurrentTime += ((float)bd / m_avr_bitrate) * 8;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::printDecodeError(int r) {
|
||
const char *e;
|
||
|
||
if(m_codec == CODEC_MP3){
|
||
switch(r){
|
||
case ERR_MP3_NONE: e = "NONE"; break;
|
||
case ERR_MP3_INDATA_UNDERFLOW: e = "INDATA_UNDERFLOW"; break;
|
||
case ERR_MP3_MAINDATA_UNDERFLOW: e = "MAINDATA_UNDERFLOW"; break;
|
||
case ERR_MP3_FREE_BITRATE_SYNC: e = "FREE_BITRATE_SYNC"; break;
|
||
case ERR_MP3_OUT_OF_MEMORY: e = "OUT_OF_MEMORY"; break;
|
||
case ERR_MP3_NULL_POINTER: e = "NULL_POINTER"; break;
|
||
case ERR_MP3_INVALID_FRAMEHEADER: e = "INVALID_FRAMEHEADER"; break;
|
||
case ERR_MP3_INVALID_SIDEINFO: e = "INVALID_SIDEINFO"; break;
|
||
case ERR_MP3_INVALID_SCALEFACT: e = "INVALID_SCALEFACT"; break;
|
||
case ERR_MP3_INVALID_HUFFCODES: e = "INVALID_HUFFCODES"; break;
|
||
case ERR_MP3_INVALID_DEQUANTIZE: e = "INVALID_DEQUANTIZE"; break;
|
||
case ERR_MP3_INVALID_IMDCT: e = "INVALID_IMDCT"; break;
|
||
case ERR_MP3_INVALID_SUBBAND: e = "INVALID_SUBBAND"; break;
|
||
default: e = "ERR_UNKNOWN";
|
||
}
|
||
AUDIO_INFO("MP3 decode error %d : %s", r, e);
|
||
}
|
||
if(m_codec == CODEC_AAC){
|
||
switch(r){
|
||
case ERR_AAC_NONE: e = "NONE"; break;
|
||
case ERR_AAC_INDATA_UNDERFLOW: e = "INDATA_UNDERFLOW"; break;
|
||
case ERR_AAC_NULL_POINTER: e = "NULL_POINTER"; break;
|
||
case ERR_AAC_INVALID_ADTS_HEADER: e = "INVALID_ADTS_HEADER"; break;
|
||
case ERR_AAC_INVALID_ADIF_HEADER: e = "INVALID_ADIF_HEADER"; break;
|
||
case ERR_AAC_INVALID_FRAME: e = "INVALID_FRAME"; break;
|
||
case ERR_AAC_MPEG4_UNSUPPORTED: e = "MPEG4_UNSUPPORTED"; break;
|
||
case ERR_AAC_CHANNEL_MAP: e = "CHANNEL_MAP"; break;
|
||
case ERR_AAC_SYNTAX_ELEMENT: e = "SYNTAX_ELEMENT"; break;
|
||
case ERR_AAC_DEQUANT: e = "DEQUANT"; break;
|
||
case ERR_AAC_STEREO_PROCESS: e = "STEREO_PROCESS"; break;
|
||
case ERR_AAC_PNS: e = "PNS"; break;
|
||
case ERR_AAC_SHORT_BLOCK_DEINT: e = "SHORT_BLOCK_DEINT"; break;
|
||
case ERR_AAC_TNS: e = "TNS"; break;
|
||
case ERR_AAC_IMDCT: e = "IMDCT"; break;
|
||
case ERR_AAC_SBR_INIT: e = "SBR_INIT"; break;
|
||
case ERR_AAC_SBR_BITSTREAM: e = "SBR_BITSTREAM"; break;
|
||
case ERR_AAC_SBR_DATA: e = "SBR_DATA"; break;
|
||
case ERR_AAC_SBR_PCM_FORMAT: e = "SBR_PCM_FORMAT"; break;
|
||
case ERR_AAC_SBR_NCHANS_TOO_HIGH: e = "SBR_NCHANS_TOO_HIGH"; break;
|
||
case ERR_AAC_SBR_SINGLERATE_UNSUPPORTED: e = "BR_SINGLERATE_UNSUPPORTED"; break;
|
||
case ERR_AAC_NCHANS_TOO_HIGH: e = "NCHANS_TOO_HIGH"; break;
|
||
case ERR_AAC_RAWBLOCK_PARAMS: e = "RAWBLOCK_PARAMS"; break;
|
||
default: e = "ERR_UNKNOWN";
|
||
}
|
||
AUDIO_INFO("AAC decode error %d : %s", r, e);
|
||
}
|
||
if(m_codec == CODEC_FLAC){
|
||
switch(r){
|
||
case ERR_FLAC_NONE: e = "NONE"; break;
|
||
case ERR_FLAC_BLOCKSIZE_TOO_BIG: e = "BLOCKSIZE TOO BIG"; break;
|
||
case ERR_FLAC_RESERVED_BLOCKSIZE_UNSUPPORTED: e = "Reserved Blocksize unsupported"; break;
|
||
case ERR_FLAC_SYNC_CODE_NOT_FOUND: e = "SYNC CODE NOT FOUND"; break;
|
||
case ERR_FLAC_UNKNOWN_CHANNEL_ASSIGNMENT: e = "UNKNOWN CHANNEL ASSIGNMENT"; break;
|
||
case ERR_FLAC_RESERVED_CHANNEL_ASSIGNMENT: e = "RESERVED CHANNEL ASSIGNMENT"; break;
|
||
case ERR_FLAC_RESERVED_SUB_TYPE: e = "RESERVED SUB TYPE"; break;
|
||
case ERR_FLAC_PREORDER_TOO_BIG: e = "PREORDER TOO BIG"; break;
|
||
case ERR_FLAC_RESERVED_RESIDUAL_CODING: e = "RESERVED RESIDUAL CODING"; break;
|
||
case ERR_FLAC_WRONG_RICE_PARTITION_NR: e = "WRONG RICE PARTITION NR"; break;
|
||
case ERR_FLAC_BITS_PER_SAMPLE_TOO_BIG: e = "BITS PER SAMPLE > 16"; break;
|
||
case ERR_FLAG_BITS_PER_SAMPLE_UNKNOWN: e = "BITS PER SAMPLE UNKNOWN"; break;
|
||
default: e = "ERR_UNKNOWN";
|
||
}
|
||
AUDIO_INFO("FLAC decode error %d : %s", r, e);
|
||
}
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::setPinout(uint8_t BCLK, uint8_t LRC, uint8_t DOUT, int8_t DIN, int8_t MCK) {
|
||
|
||
m_pin_config.bck_io_num = BCLK;
|
||
m_pin_config.ws_io_num = LRC; // wclk
|
||
m_pin_config.data_out_num = DOUT;
|
||
m_pin_config.data_in_num = DIN;
|
||
#if(ESP_IDF_VERSION_MAJOR >= 4 && ESP_IDF_VERSION_MINOR >= 4)
|
||
m_pin_config.mck_io_num = MCK;
|
||
#endif
|
||
|
||
const esp_err_t result = i2s_set_pin((i2s_port_t) m_i2s_num, &m_pin_config);
|
||
return (result == ESP_OK);
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
uint32_t Audio::getFileSize() {
|
||
if(!audiofile) return 0;
|
||
cardLock(true);
|
||
uint32_t s = audiofile.size();
|
||
cardLock(false);
|
||
return s;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
uint32_t Audio::getFilePos() {
|
||
if(!audiofile) return 0;
|
||
cardLock(true);
|
||
uint32_t p = audiofile.position();
|
||
cardLock(false);
|
||
return p;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
uint32_t Audio::getAudioDataStartPos() {
|
||
if(!audiofile) return 0;
|
||
return m_audioDataStart;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
uint32_t Audio::getAudioFileDuration() {
|
||
if(getDatamode() == AUDIO_LOCALFILE) {if(!audiofile) return 0;}
|
||
if(m_streamType == ST_WEBFILE) {if(!m_contentlength) return 0;}
|
||
|
||
if (m_avr_bitrate && m_codec == CODEC_MP3) m_audioFileDuration = 8 * (m_audioDataSize / m_avr_bitrate); // #289
|
||
else if(m_avr_bitrate && m_codec == CODEC_WAV) m_audioFileDuration = 8 * (m_audioDataSize / m_avr_bitrate);
|
||
else if(m_avr_bitrate && m_codec == CODEC_M4A) m_audioFileDuration = 8 * (m_audioDataSize / m_avr_bitrate);
|
||
else if(m_avr_bitrate && m_codec == CODEC_AAC) m_audioFileDuration = 8 * (m_audioDataSize / m_avr_bitrate);
|
||
else if( m_codec == CODEC_FLAC) m_audioFileDuration = FLACGetAudioFileDuration();
|
||
else return 0;
|
||
return m_audioFileDuration;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
uint32_t Audio::getAudioCurrentTime() { // return current time in seconds
|
||
return (uint32_t) m_audioCurrentTime;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::setAudioPlayPosition(uint16_t sec){
|
||
// Jump to an absolute position in time within an audio file
|
||
// e.g. setAudioPlayPosition(300) sets the pointer at pos 5 min
|
||
// works only with format mp3 or wav
|
||
if(m_codec == CODEC_M4A) return false;
|
||
if(sec > getAudioFileDuration()) sec = getAudioFileDuration();
|
||
uint32_t filepos = m_audioDataStart + (m_avr_bitrate * sec / 8);
|
||
|
||
return setFilePos(filepos);
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
uint32_t Audio::getTotalPlayingTime() {
|
||
// Is set to zero by a connectToXXX() and starts as soon as the first audio data is available,
|
||
// the time counting is not interrupted by a 'pause / resume' and is not reset by a fileloop
|
||
return millis() - m_PlayingStartTime;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::setTimeOffset(int sec){
|
||
// fast forward or rewind the current position in seconds
|
||
// audiosource must be a mp3, aac or wav file
|
||
|
||
if(!audiofile || !m_avr_bitrate) return false;
|
||
|
||
uint32_t oneSec = m_avr_bitrate / 8; // bytes decoded in one sec
|
||
int32_t offset = oneSec * sec; // bytes to be wind/rewind
|
||
uint32_t startAB = m_audioDataStart; // audioblock begin
|
||
uint32_t endAB = m_audioDataStart + m_audioDataSize; // audioblock end
|
||
|
||
if(m_codec == CODEC_MP3 || m_codec == CODEC_AAC || m_codec == CODEC_WAV || m_codec == CODEC_FLAC){
|
||
int32_t pos = getFilePos();
|
||
pos += offset;
|
||
if(pos < (int32_t)startAB) pos = startAB;
|
||
if(pos >= (int32_t)endAB) pos = endAB;
|
||
setFilePos(pos);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::setFilePos(uint32_t pos) {
|
||
if(!audiofile) return false;
|
||
// if(!m_avr_bitrate) return false;
|
||
if(m_codec == CODEC_M4A) return false;
|
||
m_f_playing = false;
|
||
if(m_codec == CODEC_MP3) MP3Decoder_ClearBuffer();
|
||
if(m_codec == CODEC_WAV) {while((pos % 4) != 0) pos++;} // must be divisible by four
|
||
if(m_codec == CODEC_FLAC) FLACDecoderReset();
|
||
InBuff.resetBuffer();
|
||
if(pos < m_audioDataStart) pos = m_audioDataStart; // issue #96
|
||
if(m_avr_bitrate) m_audioCurrentTime = ((pos-m_audioDataStart) / m_avr_bitrate) * 8; // #96
|
||
cardLock(true);
|
||
uint32_t sk = audiofile.seek(pos);
|
||
cardLock(false);
|
||
return sk;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::audioFileSeek(const float speed) {
|
||
// 0.5 is half speed
|
||
// 1.0 is normal speed
|
||
// 1.5 is one and half speed
|
||
if((speed > 1.5f) || (speed < 0.25f)) return false;
|
||
|
||
uint32_t srate = getSampleRate() * speed;
|
||
i2s_set_sample_rates((i2s_port_t)m_i2s_num, srate);
|
||
return true;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::setSampleRate(uint32_t sampRate) {
|
||
if(!sampRate) sampRate = 16000; // fuse, if there is no value -> set default #209
|
||
i2s_set_sample_rates((i2s_port_t)m_i2s_num, sampRate);
|
||
m_sampleRate = sampRate;
|
||
IIR_calculateCoefficients(m_gain0, m_gain1, m_gain2); // must be recalculated after each samplerate change
|
||
return true;
|
||
}
|
||
uint32_t Audio::getSampleRate(){
|
||
return m_sampleRate;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::setBitsPerSample(int bits) {
|
||
if((bits != 16) && (bits != 8)) return false;
|
||
m_bitsPerSample = bits;
|
||
return true;
|
||
}
|
||
uint8_t Audio::getBitsPerSample(){
|
||
return m_bitsPerSample;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::setChannels(int ch) {
|
||
if((ch < 1) || (ch > 2)) return false;
|
||
m_channels = ch;
|
||
return true;
|
||
}
|
||
uint8_t Audio::getChannels(){
|
||
if (m_channels == 0) { // this should not happen! #209
|
||
m_channels = 2;
|
||
}
|
||
return m_channels;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::setBitrate(int br){
|
||
m_bitRate = br;
|
||
if(br)return true;
|
||
return false;
|
||
}
|
||
uint32_t Audio::getBitRate(bool avg){
|
||
if (avg)
|
||
return m_avr_bitrate;
|
||
return m_bitRate;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::setI2SCommFMT_LSB(bool commFMT) {
|
||
// false: I2S communication format is by default I2S_COMM_FORMAT_I2S_MSB, right->left (AC101, PCM5102A)
|
||
// true: changed to I2S_COMM_FORMAT_I2S_LSB for some DACs (PT8211)
|
||
// Japanese or called LSBJ (Least Significant Bit Justified) format
|
||
|
||
if (commFMT) {
|
||
if(m_f_Log) log_i("commFMT LSB");
|
||
|
||
#if ESP_ARDUINO_VERSION_MAJOR >= 2
|
||
m_i2s_config.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_MSB); // v >= 2.0.0
|
||
#else
|
||
m_i2s_config.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_LSB);
|
||
#endif
|
||
|
||
}
|
||
else {
|
||
if(m_f_Log) log_i("commFMT MSB");
|
||
|
||
#if ESP_ARDUINO_VERSION_MAJOR >= 2
|
||
m_i2s_config.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S); // vers >= 2.0.0
|
||
#else
|
||
m_i2s_config.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB);
|
||
#endif
|
||
|
||
}
|
||
AUDIO_INFO("commFMT = %i", m_i2s_config.communication_format);
|
||
i2s_driver_uninstall((i2s_port_t)m_i2s_num);
|
||
i2s_driver_install ((i2s_port_t)m_i2s_num, &m_i2s_config, 0, NULL);
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::playSample(int16_t sample[2]) {
|
||
|
||
if (getBitsPerSample() == 8) { // Upsample from unsigned 8 bits to signed 16 bits
|
||
sample[LEFTCHANNEL] = ((sample[LEFTCHANNEL] & 0xff) -128) << 8;
|
||
sample[RIGHTCHANNEL] = ((sample[RIGHTCHANNEL] & 0xff) -128) << 8;
|
||
}
|
||
|
||
sample[LEFTCHANNEL] = sample[LEFTCHANNEL] >> 1; // half Vin so we can boost up to 6dB in filters
|
||
sample[RIGHTCHANNEL] = sample[RIGHTCHANNEL] >> 1;
|
||
|
||
// Filterchain, can commented out if not used
|
||
sample = IIR_filterChain0(sample);
|
||
sample = IIR_filterChain1(sample);
|
||
sample = IIR_filterChain2(sample);
|
||
//-------------------------------------------
|
||
|
||
uint32_t s32 = Gain(sample); // vosample2lume;
|
||
|
||
if(m_f_internalDAC) {
|
||
s32 += 0x80008000;
|
||
}
|
||
m_i2s_bytesWritten = 0;
|
||
esp_err_t err = i2s_write((i2s_port_t) m_i2s_num, (const char*) &s32, sizeof(uint32_t), &m_i2s_bytesWritten, 100);
|
||
if(err != ESP_OK) {
|
||
log_e("ESP32 Errorcode %i", err);
|
||
return false;
|
||
}
|
||
if(m_i2s_bytesWritten < 4) {
|
||
log_e("Can't stuff any more in I2S..."); // increase waitingtime or outputbuffer
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::setTone(int8_t gainLowPass, int8_t gainBandPass, int8_t gainHighPass){
|
||
// see https://www.earlevel.com/main/2013/10/13/biquad-calculator-v2/
|
||
// values can be between -40 ... +6 (dB)
|
||
|
||
m_gain0 = gainLowPass;
|
||
m_gain1 = gainBandPass;
|
||
m_gain2 = gainHighPass;
|
||
|
||
IIR_calculateCoefficients(m_gain0, m_gain1, m_gain2);
|
||
|
||
/*
|
||
This will cause a clicking sound when adjusting the EQ.
|
||
Because when the EQ is adjusted, the IIR filter will be cleared and played,
|
||
mixed in the audio data frame, and a click-like sound will be produced.
|
||
*/
|
||
/*
|
||
int16_t tmp[2]; tmp[0] = 0; tmp[1]= 0;
|
||
|
||
IIR_filterChain0(tmp, true ); // flush the filter
|
||
IIR_filterChain1(tmp, true ); // flush the filter
|
||
IIR_filterChain2(tmp, true ); // flush the filter
|
||
*/
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::forceMono(bool m) { // #100 mono option
|
||
m_f_forceMono = m; // false stereo, true mono
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::setBalance(int8_t bal){ // bal -16...16
|
||
if(bal < -16) bal = -16;
|
||
if(bal > 16) bal = 16;
|
||
m_balance = bal;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::setVolume(uint8_t vol) { // vol 22 steps, 0...21
|
||
if(vol > 254) vol = 254;
|
||
m_vol = vol;
|
||
/* if(vol > 21) vol = 21;
|
||
m_vol = volumetable[vol];*/
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
uint8_t Audio::getVolume() {
|
||
return m_vol;
|
||
/*for(uint8_t i = 0; i < 22; i++) {
|
||
if(volumetable[i] == m_vol) return i;
|
||
}
|
||
m_vol = 12; // if m_vol not found in table
|
||
return m_vol;*/
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
uint8_t Audio::getI2sPort() {
|
||
return m_i2s_num;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
int32_t Audio::Gain(int16_t s[2]) {
|
||
int32_t v[2];
|
||
float step = (float)m_vol /254;
|
||
uint8_t l = 0, r = 0;
|
||
|
||
if(m_balance < 0){
|
||
step = step * (float)(abs(m_balance) * 16);
|
||
l = (uint8_t)(step);
|
||
}
|
||
if(m_balance > 0){
|
||
step = step * m_balance * 16;
|
||
r = (uint8_t)(step);
|
||
}
|
||
|
||
v[LEFTCHANNEL] = (s[LEFTCHANNEL] * (m_vol - l)) >> 8;
|
||
v[RIGHTCHANNEL]= (s[RIGHTCHANNEL] * (m_vol - r)) >> 8;
|
||
|
||
return (v[LEFTCHANNEL] << 16) | (v[RIGHTCHANNEL] & 0xffff);
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
uint32_t Audio::inBufferFilled() {
|
||
// current audio input buffer fillsize in bytes
|
||
return InBuff.bufferFilled();
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
uint32_t Audio::inBufferFree() {
|
||
// current audio input buffer free space in bytes
|
||
return InBuff.freeSpace();
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
// *** D i g i t a l b i q u a d r a t i c f i l t e r ***
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
void Audio::IIR_calculateCoefficients(int8_t G0, int8_t G1, int8_t G2){ // Infinite Impulse Response (IIR) filters
|
||
|
||
// G1 - gain low shelf set between -40 ... +6 dB
|
||
// G2 - gain peakEQ set between -40 ... +6 dB
|
||
// G3 - gain high shelf set between -40 ... +6 dB
|
||
// https://www.earlevel.com/main/2012/11/26/biquad-c-source-code/
|
||
|
||
if(getSampleRate() < 1000) return; // fuse
|
||
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
|
||
if(G0 < -40) G0 = -40; // -40dB -> Vin*0.01
|
||
if(G0 > 6) G0 = 6; // +6dB -> Vin*2
|
||
if(G1 < -40) G1 = -40;
|
||
if(G1 > 6) G1 = 6;
|
||
if(G2 < -40) G2 = -40;
|
||
if(G2 > 6) G2 = 6;
|
||
|
||
const float FcLS = 500; // Frequency LowShelf[Hz]
|
||
const float FcPKEQ = 3000; // Frequency PeakEQ[Hz]
|
||
const float FcHS = 6000; // Frequency HighShelf[Hz]
|
||
|
||
float K, norm, Q, Fc, V ;
|
||
|
||
// LOWSHELF
|
||
Fc = (float)FcLS / (float)getSampleRate(); // Cutoff frequency
|
||
K = tanf((float)PI * Fc);
|
||
V = powf(10, fabs(G0) / 20.0);
|
||
|
||
if (G0 >= 0) { // boost
|
||
norm = 1 / (1 + sqrtf(2) * K + K * K);
|
||
m_filter[LOWSHELF].a0 = (1 + sqrtf(2*V) * K + V * K * K) * norm;
|
||
m_filter[LOWSHELF].a1 = 2 * (V * K * K - 1) * norm;
|
||
m_filter[LOWSHELF].a2 = (1 - sqrtf(2*V) * K + V * K * K) * norm;
|
||
m_filter[LOWSHELF].b1 = 2 * (K * K - 1) * norm;
|
||
m_filter[LOWSHELF].b2 = (1 - sqrtf(2) * K + K * K) * norm;
|
||
}
|
||
else { // cut
|
||
norm = 1 / (1 + sqrtf(2*V) * K + V * K * K);
|
||
m_filter[LOWSHELF].a0 = (1 + sqrtf(2) * K + K * K) * norm;
|
||
m_filter[LOWSHELF].a1 = 2 * (K * K - 1) * norm;
|
||
m_filter[LOWSHELF].a2 = (1 - sqrtf(2) * K + K * K) * norm;
|
||
m_filter[LOWSHELF].b1 = 2 * (V * K * K - 1) * norm;
|
||
m_filter[LOWSHELF].b2 = (1 - sqrtf(2*V) * K + V * K * K) * norm;
|
||
}
|
||
|
||
// PEAK EQ
|
||
Fc = (float)FcPKEQ / (float)getSampleRate(); // Cutoff frequency
|
||
K = tanf((float)PI * Fc);
|
||
V = powf(10, fabs(G1) / 20.0);
|
||
Q = 2.5; // Quality factor
|
||
if (G1 >= 0) { // boost
|
||
norm = 1 / (1 + 1/Q * K + K * K);
|
||
m_filter[PEAKEQ].a0 = (1 + V/Q * K + K * K) * norm;
|
||
m_filter[PEAKEQ].a1 = 2 * (K * K - 1) * norm;
|
||
m_filter[PEAKEQ].a2 = (1 - V/Q * K + K * K) * norm;
|
||
m_filter[PEAKEQ].b1 = m_filter[PEAKEQ].a1;
|
||
m_filter[PEAKEQ].b2 = (1 - 1/Q * K + K * K) * norm;
|
||
}
|
||
else { // cut
|
||
norm = 1 / (1 + V/Q * K + K * K);
|
||
m_filter[PEAKEQ].a0 = (1 + 1/Q * K + K * K) * norm;
|
||
m_filter[PEAKEQ].a1 = 2 * (K * K - 1) * norm;
|
||
m_filter[PEAKEQ].a2 = (1 - 1/Q * K + K * K) * norm;
|
||
m_filter[PEAKEQ].b1 = m_filter[PEAKEQ].a1;
|
||
m_filter[PEAKEQ].b2 = (1 - V/Q * K + K * K) * norm;
|
||
}
|
||
|
||
// HIGHSHELF
|
||
Fc = (float)FcHS / (float)getSampleRate(); // Cutoff frequency
|
||
K = tanf((float)PI * Fc);
|
||
V = powf(10, fabs(G2) / 20.0);
|
||
if (G2 >= 0) { // boost
|
||
norm = 1 / (1 + sqrtf(2) * K + K * K);
|
||
m_filter[HIFGSHELF].a0 = (V + sqrtf(2*V) * K + K * K) * norm;
|
||
m_filter[HIFGSHELF].a1 = 2 * (K * K - V) * norm;
|
||
m_filter[HIFGSHELF].a2 = (V - sqrtf(2*V) * K + K * K) * norm;
|
||
m_filter[HIFGSHELF].b1 = 2 * (K * K - 1) * norm;
|
||
m_filter[HIFGSHELF].b2 = (1 - sqrtf(2) * K + K * K) * norm;
|
||
}
|
||
else {
|
||
norm = 1 / (V + sqrtf(2*V) * K + K * K);
|
||
m_filter[HIFGSHELF].a0 = (1 + sqrtf(2) * K + K * K) * norm;
|
||
m_filter[HIFGSHELF].a1 = 2 * (K * K - 1) * norm;
|
||
m_filter[HIFGSHELF].a2 = (1 - sqrtf(2) * K + K * K) * norm;
|
||
m_filter[HIFGSHELF].b1 = 2 * (K * K - V) * norm;
|
||
m_filter[HIFGSHELF].b2 = (V - sqrtf(2*V) * K + K * K) * norm;
|
||
}
|
||
|
||
// log_i("LS a0=%f, a1=%f, a2=%f, b1=%f, b2=%f", m_filter[0].a0, m_filter[0].a1, m_filter[0].a2,
|
||
// m_filter[0].b1, m_filter[0].b2);
|
||
// log_i("EQ a0=%f, a1=%f, a2=%f, b1=%f, b2=%f", m_filter[1].a0, m_filter[1].a1, m_filter[1].a2,
|
||
// m_filter[1].b1, m_filter[1].b2);
|
||
// log_i("HS a0=%f, a1=%f, a2=%f, b1=%f, b2=%f", m_filter[2].a0, m_filter[2].a1, m_filter[2].a2,
|
||
// m_filter[2].b1, m_filter[2].b2);
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
int16_t* Audio::IIR_filterChain0(int16_t iir_in[2], bool clear){ // Infinite Impulse Response (IIR) filters
|
||
|
||
uint8_t z1 = 0, z2 = 1;
|
||
enum: uint8_t {in = 0, out = 1};
|
||
float inSample[2];
|
||
float outSample[2];
|
||
static int16_t iir_out[2];
|
||
|
||
if(clear){
|
||
memset(m_filterBuff, 0, sizeof(m_filterBuff)); // zero IIR filterbuffer
|
||
iir_out[0] = 0;
|
||
iir_out[1] = 0;
|
||
iir_in[0] = 0;
|
||
iir_in[1] = 0;
|
||
}
|
||
|
||
inSample[LEFTCHANNEL] = (float)(iir_in[LEFTCHANNEL]);
|
||
inSample[RIGHTCHANNEL] = (float)(iir_in[RIGHTCHANNEL]);
|
||
|
||
outSample[LEFTCHANNEL] = m_filter[0].a0 * inSample[LEFTCHANNEL]
|
||
+ m_filter[0].a1 * m_filterBuff[0][z1][in] [LEFTCHANNEL]
|
||
+ m_filter[0].a2 * m_filterBuff[0][z2][in] [LEFTCHANNEL]
|
||
- m_filter[0].b1 * m_filterBuff[0][z1][out][LEFTCHANNEL]
|
||
- m_filter[0].b2 * m_filterBuff[0][z2][out][LEFTCHANNEL];
|
||
|
||
m_filterBuff[0][z2][in] [LEFTCHANNEL] = m_filterBuff[0][z1][in][LEFTCHANNEL];
|
||
m_filterBuff[0][z1][in] [LEFTCHANNEL] = inSample[LEFTCHANNEL];
|
||
m_filterBuff[0][z2][out][LEFTCHANNEL] = m_filterBuff[0][z1][out][LEFTCHANNEL];
|
||
m_filterBuff[0][z1][out][LEFTCHANNEL] = outSample[LEFTCHANNEL];
|
||
iir_out[LEFTCHANNEL] = (int16_t)outSample[LEFTCHANNEL];
|
||
|
||
|
||
outSample[RIGHTCHANNEL] = m_filter[0].a0 * inSample[RIGHTCHANNEL]
|
||
+ m_filter[0].a1 * m_filterBuff[0][z1][in] [RIGHTCHANNEL]
|
||
+ m_filter[0].a2 * m_filterBuff[0][z2][in] [RIGHTCHANNEL]
|
||
- m_filter[0].b1 * m_filterBuff[0][z1][out][RIGHTCHANNEL]
|
||
- m_filter[0].b2 * m_filterBuff[0][z2][out][RIGHTCHANNEL];
|
||
|
||
m_filterBuff[0][z2][in] [RIGHTCHANNEL] = m_filterBuff[0][z1][in][RIGHTCHANNEL];
|
||
m_filterBuff[0][z1][in] [RIGHTCHANNEL] = inSample[RIGHTCHANNEL];
|
||
m_filterBuff[0][z2][out][RIGHTCHANNEL] = m_filterBuff[0][z1][out][RIGHTCHANNEL];
|
||
m_filterBuff[0][z1][out][RIGHTCHANNEL] = outSample[RIGHTCHANNEL];
|
||
iir_out[RIGHTCHANNEL] = (int16_t) outSample[RIGHTCHANNEL];
|
||
|
||
return iir_out;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
int16_t* Audio::IIR_filterChain1(int16_t iir_in[2], bool clear){ // Infinite Impulse Response (IIR) filters
|
||
|
||
uint8_t z1 = 0, z2 = 1;
|
||
enum: uint8_t {in = 0, out = 1};
|
||
float inSample[2];
|
||
float outSample[2];
|
||
static int16_t iir_out[2];
|
||
|
||
if(clear){
|
||
memset(m_filterBuff, 0, sizeof(m_filterBuff)); // zero IIR filterbuffer
|
||
iir_out[0] = 0;
|
||
iir_out[1] = 0;
|
||
iir_in[0] = 0;
|
||
iir_in[1] = 0;
|
||
}
|
||
|
||
inSample[LEFTCHANNEL] = (float)(iir_in[LEFTCHANNEL]);
|
||
inSample[RIGHTCHANNEL] = (float)(iir_in[RIGHTCHANNEL]);
|
||
|
||
outSample[LEFTCHANNEL] = m_filter[1].a0 * inSample[LEFTCHANNEL]
|
||
+ m_filter[1].a1 * m_filterBuff[1][z1][in] [LEFTCHANNEL]
|
||
+ m_filter[1].a2 * m_filterBuff[1][z2][in] [LEFTCHANNEL]
|
||
- m_filter[1].b1 * m_filterBuff[1][z1][out][LEFTCHANNEL]
|
||
- m_filter[1].b2 * m_filterBuff[1][z2][out][LEFTCHANNEL];
|
||
|
||
m_filterBuff[1][z2][in] [LEFTCHANNEL] = m_filterBuff[1][z1][in][LEFTCHANNEL];
|
||
m_filterBuff[1][z1][in] [LEFTCHANNEL] = inSample[LEFTCHANNEL];
|
||
m_filterBuff[1][z2][out][LEFTCHANNEL] = m_filterBuff[1][z1][out][LEFTCHANNEL];
|
||
m_filterBuff[1][z1][out][LEFTCHANNEL] = outSample[LEFTCHANNEL];
|
||
iir_out[LEFTCHANNEL] = (int16_t)outSample[LEFTCHANNEL];
|
||
|
||
|
||
outSample[RIGHTCHANNEL] = m_filter[1].a0 * inSample[RIGHTCHANNEL]
|
||
+ m_filter[1].a1 * m_filterBuff[1][z1][in] [RIGHTCHANNEL]
|
||
+ m_filter[1].a2 * m_filterBuff[1][z2][in] [RIGHTCHANNEL]
|
||
- m_filter[1].b1 * m_filterBuff[1][z1][out][RIGHTCHANNEL]
|
||
- m_filter[1].b2 * m_filterBuff[1][z2][out][RIGHTCHANNEL];
|
||
|
||
m_filterBuff[1][z2][in] [RIGHTCHANNEL] = m_filterBuff[1][z1][in][RIGHTCHANNEL];
|
||
m_filterBuff[1][z1][in] [RIGHTCHANNEL] = inSample[RIGHTCHANNEL];
|
||
m_filterBuff[1][z2][out][RIGHTCHANNEL] = m_filterBuff[1][z1][out][RIGHTCHANNEL];
|
||
m_filterBuff[1][z1][out][RIGHTCHANNEL] = outSample[RIGHTCHANNEL];
|
||
iir_out[RIGHTCHANNEL] = (int16_t) outSample[RIGHTCHANNEL];
|
||
|
||
return iir_out;
|
||
}
|
||
//---------------------------------------------------------------------------------------------------------------------
|
||
int16_t* Audio::IIR_filterChain2(int16_t iir_in[2], bool clear){ // Infinite Impulse Response (IIR) filters
|
||
|
||
uint8_t z1 = 0, z2 = 1;
|
||
enum: uint8_t {in = 0, out = 1};
|
||
float inSample[2];
|
||
float outSample[2];
|
||
static int16_t iir_out[2];
|
||
|
||
if(clear){
|
||
memset(m_filterBuff, 0, sizeof(m_filterBuff)); // zero IIR filterbuffer
|
||
iir_out[0] = 0;
|
||
iir_out[1] = 0;
|
||
iir_in[0] = 0;
|
||
iir_in[1] = 0;
|
||
}
|
||
|
||
inSample[LEFTCHANNEL] = (float)(iir_in[LEFTCHANNEL]);
|
||
inSample[RIGHTCHANNEL] = (float)(iir_in[RIGHTCHANNEL]);
|
||
|
||
outSample[LEFTCHANNEL] = m_filter[2].a0 * inSample[LEFTCHANNEL]
|
||
+ m_filter[2].a1 * m_filterBuff[2][z1][in] [LEFTCHANNEL]
|
||
+ m_filter[2].a2 * m_filterBuff[2][z2][in] [LEFTCHANNEL]
|
||
- m_filter[2].b1 * m_filterBuff[2][z1][out][LEFTCHANNEL]
|
||
- m_filter[2].b2 * m_filterBuff[2][z2][out][LEFTCHANNEL];
|
||
|
||
m_filterBuff[2][z2][in] [LEFTCHANNEL] = m_filterBuff[2][z1][in][LEFTCHANNEL];
|
||
m_filterBuff[2][z1][in] [LEFTCHANNEL] = inSample[LEFTCHANNEL];
|
||
m_filterBuff[2][z2][out][LEFTCHANNEL] = m_filterBuff[2][z1][out][LEFTCHANNEL];
|
||
m_filterBuff[2][z1][out][LEFTCHANNEL] = outSample[LEFTCHANNEL];
|
||
iir_out[LEFTCHANNEL] = (int16_t)outSample[LEFTCHANNEL];
|
||
|
||
|
||
outSample[RIGHTCHANNEL] = m_filter[2].a0 * inSample[RIGHTCHANNEL]
|
||
+ m_filter[2].a1 * m_filterBuff[2][z1][in] [RIGHTCHANNEL]
|
||
+ m_filter[2].a2 * m_filterBuff[2][z2][in] [RIGHTCHANNEL]
|
||
- m_filter[2].b1 * m_filterBuff[2][z1][out][RIGHTCHANNEL]
|
||
- m_filter[2].b2 * m_filterBuff[2][z2][out][RIGHTCHANNEL];
|
||
|
||
m_filterBuff[2][z2][in] [RIGHTCHANNEL] = m_filterBuff[2][z1][in][RIGHTCHANNEL];
|
||
m_filterBuff[2][z1][in] [RIGHTCHANNEL] = inSample[RIGHTCHANNEL];
|
||
m_filterBuff[2][z2][out][RIGHTCHANNEL] = m_filterBuff[2][z1][out][RIGHTCHANNEL];
|
||
m_filterBuff[2][z1][out][RIGHTCHANNEL] = outSample[RIGHTCHANNEL];
|
||
iir_out[RIGHTCHANNEL] = (int16_t) outSample[RIGHTCHANNEL];
|
||
|
||
return iir_out;
|
||
}
|
||
//----------------------------------------------------------------------------------------------------------------------
|
||
// AAC - T R A N S P O R T S T R E A M
|
||
//----------------------------------------------------------------------------------------------------------------------
|
||
bool Audio::ts_parsePacket(uint8_t* packet, uint8_t* packetStart, uint8_t* packetLength) {
|
||
|
||
const uint8_t TS_PACKET_SIZE = 188;
|
||
const uint8_t PAYLOAD_SIZE = 184;
|
||
const uint8_t PID_ARRAY_LEN = 4;
|
||
(void)PAYLOAD_SIZE;
|
||
typedef struct{
|
||
int number= 0;
|
||
int pids[PID_ARRAY_LEN];
|
||
} pid_array;
|
||
|
||
static pid_array pidsOfPMT;
|
||
static int PES_DataLength = 0;
|
||
static int pidOfAAC = 0;
|
||
|
||
if(packet == NULL){
|
||
if(m_f_Log) log_i("parseTS reset");
|
||
for(int i = 0; i < PID_ARRAY_LEN; i++) pidsOfPMT.pids[i] = 0;
|
||
PES_DataLength = 0;
|
||
pidOfAAC = 0;
|
||
return true;
|
||
}
|
||
|
||
// --------------------------------------------------------------------------------------------------------
|
||
// 0. Byte SyncByte | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 1 | always bit pattern of 0x47
|
||
//---------------------------------------------------------------------------------------------------------
|
||
// 1. Byte |PUSI|TP| |PID|PID|PID|PID|PID|
|
||
//---------------------------------------------------------------------------------------------------------
|
||
// 2. Byte |PID|PID|PID|PID|PID|PID|PID|PID|
|
||
//---------------------------------------------------------------------------------------------------------
|
||
// 3. Byte |TSC|TSC|AFC|ADC|CC |CC |CC |CC |
|
||
//---------------------------------------------------------------------------------------------------------
|
||
// 4.-187. Byte |Payload data if AFC==01 or 11 |
|
||
//---------------------------------------------------------------------------------------------------------
|
||
|
||
// PUSI Payload unit start indicator, set when this packet contains the first byte of a new payload unit.
|
||
// The first byte of the payload will indicate where this new payload unit starts.
|
||
// TP Transport priority, set when the current packet has a higher priority than other packets with the same PID.
|
||
// PID Packet Identifier, describing the payload data.
|
||
// TSC Transport scrambling control, '00' = Not scrambled.
|
||
// AFC Adaptation field control, 01 – no adaptation field, payload only, 10 – adaptation field only, no payload,
|
||
// 11 – adaptation field followed by payload, 00 – RESERVED for future use
|
||
// CC Continuity counter, Sequence number of payload packets (0x00 to 0x0F) within each stream (except PID 8191)
|
||
|
||
if(packet[0] != 0x47) {
|
||
log_e("ts SyncByte not found, first bytes are %X %X %X %X", packet[0], packet[1], packet[2], packet[3]);
|
||
stopSong();
|
||
return false;
|
||
}
|
||
int PID = (packet[1] & 0x1F) << 8 | (packet[2] & 0xFF);
|
||
if(m_f_Log) log_i("PID: 0x%04X(%d)", PID, PID);
|
||
int PUSI = (packet[1] & 0x40) >> 6;
|
||
if(m_f_Log) log_i("Payload Unit Start Indicator: %d", PUSI);
|
||
int AFC = (packet[3] & 0x30) >> 4;
|
||
if(m_f_Log) log_i("Adaption Field Control: %d", AFC);
|
||
|
||
int AFL = -1;
|
||
if((AFC & 0b10) == 0b10) { // AFC '11' Adaptation Field followed
|
||
AFL = packet[4] & 0xFF; // Adaptation Field Length
|
||
if(m_f_Log) log_i("Adaptation Field Length: %d", AFL);
|
||
}
|
||
int PLS = PUSI ? 5 : 4; // PayLoadStart, Payload Unit Start Indicator
|
||
|
||
if(PID == 0) {
|
||
// Program Association Table (PAT) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if(m_f_Log) log_i("PAT");
|
||
pidsOfPMT.number = 0;
|
||
pidOfAAC = 0;
|
||
int startOfProgramNums = 8;
|
||
int lengthOfPATValue = 4;
|
||
int sectionLength = ((packet[PLS + 1] & 0x0F) << 8) | (packet[PLS + 2] & 0xFF);
|
||
if(m_f_Log) log_i("Section Length: %d", sectionLength);
|
||
int program_number, program_map_PID;
|
||
(void)program_number;
|
||
int indexOfPids = 0;
|
||
for(int i = startOfProgramNums; i <= sectionLength; i += lengthOfPATValue) {
|
||
program_number = ((packet[PLS + i] & 0xFF) << 8) | (packet[PLS + i + 1] & 0xFF);
|
||
program_map_PID = ((packet[PLS + i + 2] & 0x1F) << 8) | (packet[PLS + i + 3] & 0xFF);
|
||
if(m_f_Log) log_i("Program Num: 0x%04X(%d) PMT PID: 0x%04X(%d)", program_number, program_number,
|
||
program_map_PID, program_map_PID);
|
||
pidsOfPMT.pids[indexOfPids++] = program_map_PID;
|
||
}
|
||
pidsOfPMT.number = indexOfPids;
|
||
*packetStart = 0;
|
||
*packetLength = 0;
|
||
return true;
|
||
|
||
}
|
||
else if(PID == pidOfAAC) {
|
||
if(m_f_Log) log_i("AAC");
|
||
uint8_t posOfPacketStart = 4;
|
||
if(AFL >= 0) {posOfPacketStart = 5 + AFL;
|
||
if(m_f_Log) log_i("posOfPacketStart: %d", posOfPacketStart);}
|
||
// Packetized Elementary Stream (PES) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (PES_DataLength > 0) {
|
||
*packetStart = posOfPacketStart;
|
||
*packetLength = TS_PACKET_SIZE - posOfPacketStart;
|
||
PES_DataLength -= TS_PACKET_SIZE - posOfPacketStart;
|
||
return true;
|
||
}
|
||
else{
|
||
int firstByte = packet[posOfPacketStart] & 0xFF;
|
||
int secondByte = packet[posOfPacketStart + 1] & 0xFF;
|
||
int thirdByte = packet[posOfPacketStart + 2] & 0xFF;
|
||
if(m_f_Log) log_i("First 3 bytes: %02X %02X %02X", firstByte, secondByte, thirdByte);
|
||
if(firstByte == 0x00 && secondByte == 0x00 && thirdByte == 0x01) { // Packet start code prefix
|
||
// PES
|
||
uint8_t StreamID = packet[posOfPacketStart + 3] & 0xFF;
|
||
if(StreamID >= 0xC0 && StreamID <= 0xDF) {;} // okay ist audio stream
|
||
if(StreamID >= 0xE0 && StreamID <= 0xEF) {log_e("video stream!"); return false;}
|
||
const uint8_t posOfPacketLengthLatterHalf = 5;
|
||
int PES_PacketLength =
|
||
((packet[posOfPacketStart + 4] & 0xFF) << 8) + (packet[posOfPacketStart + 5] & 0xFF);
|
||
if(m_f_Log) log_i("PES Packet length: %d", PES_PacketLength);
|
||
PES_DataLength = PES_PacketLength;
|
||
int posOfHeaderLength = 8;
|
||
int PESRemainingHeaderLength = packet[posOfPacketStart + posOfHeaderLength] & 0xFF;
|
||
if(m_f_Log) log_i("PES Header length: %d", PESRemainingHeaderLength);
|
||
int startOfData = posOfHeaderLength + PESRemainingHeaderLength + 1;
|
||
if(m_f_Log) log_i("First AAC data byte: %02X", packet[posOfPacketStart + startOfData]);
|
||
*packetStart = posOfPacketStart + startOfData;
|
||
*packetLength = TS_PACKET_SIZE - posOfPacketStart - startOfData;
|
||
PES_DataLength -= (TS_PACKET_SIZE - posOfPacketStart) - (posOfPacketLengthLatterHalf + 1);
|
||
return true;
|
||
}
|
||
}
|
||
*packetStart = 0;
|
||
*packetLength = 0;
|
||
if(m_f_Log) log_e("PES not found");
|
||
return false;
|
||
}
|
||
else if(pidsOfPMT.number) {
|
||
// Program Map Table (PMT) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
for(int i = 0; i < pidsOfPMT.number; i++) {
|
||
if(PID == pidsOfPMT.pids[i]) {
|
||
if(m_f_Log) log_i("PMT");
|
||
int staticLengthOfPMT = 12;
|
||
int sectionLength = ((packet[PLS + 1] & 0x0F) << 8) | (packet[PLS + 2] & 0xFF);
|
||
if(m_f_Log) log_i("Section Length: %d", sectionLength);
|
||
int programInfoLength = ((packet[PLS + 10] & 0x0F) << 8) | (packet[PLS + 11] & 0xFF);
|
||
if(m_f_Log) log_i("Program Info Length: %d", programInfoLength);
|
||
int cursor = staticLengthOfPMT + programInfoLength;
|
||
while(cursor < sectionLength - 1) {
|
||
int streamType = packet[PLS + cursor] & 0xFF;
|
||
int elementaryPID = ((packet[PLS + cursor + 1] & 0x1F) << 8) | (packet[PLS + cursor + 2] & 0xFF);
|
||
if(m_f_Log) log_i("Stream Type: 0x%02X Elementary PID: 0x%04X", streamType, elementaryPID);
|
||
|
||
if(streamType == 0x0F || streamType == 0x11) {
|
||
if(m_f_Log) log_i("AAC PID discover");
|
||
pidOfAAC= elementaryPID;
|
||
}
|
||
int esInfoLength = ((packet[PLS + cursor + 3] & 0x0F) << 8) | (packet[PLS + cursor + 4] & 0xFF);
|
||
if(m_f_Log) log_i("ES Info Length: 0x%04X", esInfoLength);
|
||
cursor += 5 + esInfoLength;
|
||
}
|
||
}
|
||
}
|
||
*packetStart = 0;
|
||
*packetLength = 0;
|
||
return true;
|
||
}
|
||
if(m_f_Log) log_e("invalid ts packet!");
|
||
return false;
|
||
}
|
||
//----------------------------------------------------------------------------------------------------------------------
|
||
#endif // if VS1053_CS==255
|