7799 lines
398 KiB
C++
7799 lines
398 KiB
C++
|
||
/*****************************************************************************************************************************************************
|
||
audio.cpp
|
||
|
||
Created on: 28.10.2018 */
|
||
char audioI2SVers[] = "\
|
||
Version 3.4.6z ";
|
||
/* Updated on: Jun 26, 2026
|
||
|
||
Author: Wolle (schreibfaul1)
|
||
Audio library for ESP32, ESP32-S3 or ESP32-P4
|
||
Arduino Vers. V3 is mandatory
|
||
PSRAM is mandatory
|
||
external DAC is mandatory
|
||
|
||
*****************************************************************************************************************************************************/
|
||
|
||
#include "Audio.h"
|
||
// #include "aac_decoder/aac_decoder.h"
|
||
// #include "flac_decoder/flac_decoder.h"
|
||
#include "mp3_decoder/mp3_decoder.h"
|
||
// #include "opus_decoder/opus_decoder.h"
|
||
#include "psram_unique_ptr.hpp"
|
||
// #include "vorbis_decoder/vorbis_decoder.h"
|
||
#include "wav_decoder/wav_decoder.h"
|
||
|
||
// constants
|
||
constexpr size_t m_frameSizeWav = 4096;
|
||
constexpr size_t m_frameSizeMP3 = 18000; // more than one icy-metaint
|
||
constexpr size_t m_frameSizeAAC = 18000; // more than one icy-metaint
|
||
constexpr size_t m_frameSizeFLAC = UINT16_MAX; // max ogg size
|
||
constexpr size_t m_frameSizeOPUS = UINT16_MAX; // max ogg size
|
||
constexpr size_t m_frameSizeVORBIS = UINT16_MAX; // OGG length is normally 4080 bytes, but can be reach 64KB in the metadata block
|
||
constexpr size_t m_outbuffSize = 4608 * 2;
|
||
constexpr size_t m_resamplesBuffSize = m_outbuffSize * 8; // SRmin: 6KHz -> SRmax: 48K
|
||
|
||
constexpr size_t AUDIO_STACK_SIZE = 3500;
|
||
|
||
// static allocations for Audio task
|
||
StaticTask_t __attribute__((unused)) xAudioTaskBuffer;
|
||
StackType_t __attribute__((unused)) xAudioStack[AUDIO_STACK_SIZE];
|
||
|
||
// weak default implementation - can be overridden by user
|
||
__attribute__((weak)) void audio_process_i2s(int32_t* outBuff, int16_t validSamples, bool* continueI2S) {
|
||
// Default: do nothing. User can provide their own implementation to process audio data.
|
||
}
|
||
__attribute__((weak)) void audio_process_raw_samples(int32_t* outBuff, int16_t validSamples) {
|
||
// Default: do nothing. User can provide their own implementation to process audio data.
|
||
}
|
||
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
// 📌📌📌 A U D I O B U F F E R 📌📌📌
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
|
||
// AudioBuffer will be allocated in PSRAM
|
||
//
|
||
// m_startPtr m_readPtr m_writePtr m_endPtr
|
||
// | |<------maxAvailableBytes----->|<--------------------- writeSpace ----------------------->|
|
||
// ▼ ▼ ▼ ▼
|
||
// ---------------------------------------------------------------------------------------------------------------
|
||
// | <--m_mainBuffSize--> | <--m_resBuffSize --> |
|
||
// ---------------------------------------------------------------------------------------------------------------
|
||
// |<---freeSpace---->|<------------filled---------->|<-------freeSpace-------->|
|
||
// ▲
|
||
// |
|
||
// m_buffEnd
|
||
// if resBuff is full copy data from resBuff to the beginning
|
||
// if m_readPtr >= m_buff<end set m_readptr to m_startPtr + (m_readPtr - m_buffEnd)
|
||
//
|
||
// m_startPtr m_writePtr m_readPtr m_endPtr
|
||
// | |<-------writeSpace------>|<---------------maxAvailableBytes-------------->|
|
||
// ▼ ▼ ▼ ▼ ▼
|
||
// ---------------------------------------------------------------------------------------------------------------
|
||
// | <--m_mainBuffSize--> | <--m_resBuffSize --> |
|
||
// ---------------------------------------------------------------------------------------------------------------
|
||
// |<------------filled------------->|<-------freeSpace------->|<----filled---->▲
|
||
// |
|
||
// m_buffEnd
|
||
//
|
||
|
||
AudioBuffer::AudioBuffer() {
|
||
m_mainBuffSize = UINT16_MAX * 10;
|
||
m_resBuffSize = UINT16_MAX;
|
||
}
|
||
|
||
AudioBuffer::~AudioBuffer() {
|
||
;
|
||
}
|
||
|
||
size_t AudioBuffer::getBufsize() {
|
||
return m_mainBuffSize;
|
||
}
|
||
|
||
size_t AudioBuffer::init() {
|
||
m_buffer.alloc(m_mainBuffSize + m_resBuffSize, "AudioBuffer");
|
||
m_log.set_name("\nAudiobuffer_Log");
|
||
m_mutex = xSemaphoreCreateBinary();
|
||
xSemaphoreGive(m_mutex);
|
||
m_init = true;
|
||
m_startPtr = m_buffer.get();
|
||
m_endPtr = m_buffer.get() + m_mainBuffSize;
|
||
m_buffEnd = m_endPtr + m_resBuffSize;
|
||
reset();
|
||
return m_mainBuffSize;
|
||
}
|
||
|
||
void AudioBuffer::setMaxBlocksize(uint32_t mbs) {
|
||
m_maxBlockSize = mbs;
|
||
}
|
||
|
||
size_t AudioBuffer::getMaxBlockSize() {
|
||
return m_maxBlockSize;
|
||
}
|
||
//----------------------------------------------------------------------------------------------------------------------------------------------------
|
||
size_t AudioBuffer::freeSpace() {
|
||
if (!m_init) return 0;
|
||
if (m_readPtr == m_writePtr) {
|
||
if (m_isEmpty) { return m_mainBuffSize; }
|
||
if (m_isFull) { return 0; }
|
||
m_log.assignf("[{}:{}]" ANSI_ESC_RED " writePtr == readPtr, writePtr {}, readPtr {}", __FILE__, __LINE__, m_writePtr - m_startPtr, m_readPtr - m_startPtr);
|
||
m_log.println();
|
||
}
|
||
if (m_readPtr < m_writePtr) {
|
||
if (m_writePtr > m_endPtr) {
|
||
return (m_readPtr - m_startPtr);
|
||
} else {
|
||
return (m_endPtr - m_writePtr) + (m_readPtr - m_startPtr);
|
||
}
|
||
}
|
||
return m_readPtr - m_writePtr;
|
||
}
|
||
//----------------------------------------------------------------------------------------------------------------------------------------------------
|
||
size_t AudioBuffer::bufferFilled() {
|
||
if (!m_init) return 0;
|
||
xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||
size_t bufferFilled = 0;
|
||
if (m_readPtr == m_writePtr) {
|
||
if (m_isEmpty) {
|
||
bufferFilled = 0;
|
||
goto end;
|
||
}
|
||
if (m_isFull) {
|
||
bufferFilled = m_mainBuffSize;
|
||
goto end;
|
||
}
|
||
m_log.assignf("[{}:{}]" ANSI_ESC_RED " writePtr == readPtr, writePtr {}, readPtr {}", __FILE__, __LINE__, m_writePtr - m_startPtr, m_readPtr - m_startPtr);
|
||
m_log.println();
|
||
}
|
||
if (m_readPtr < m_writePtr) {
|
||
bufferFilled = (size_t)(m_writePtr - m_readPtr);
|
||
goto end;
|
||
}
|
||
bufferFilled = (m_endPtr - m_readPtr) + (m_writePtr - m_startPtr);
|
||
end:
|
||
xSemaphoreGive(m_mutex);
|
||
return bufferFilled;
|
||
}
|
||
//----------------------------------------------------------------------------------------------------------------------------------------------------
|
||
size_t AudioBuffer::writeSpace() {
|
||
if (!m_init) return 0;
|
||
xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||
m_writeSpace = 0;
|
||
|
||
// Check whether a complete block still fits in at the end
|
||
size_t spaceToEnd = m_buffEnd - m_writePtr;
|
||
|
||
if (m_isFull) {
|
||
m_writeSpace = 0;
|
||
goto end;
|
||
}
|
||
|
||
if (m_isEmpty) {
|
||
m_writeSpace = min(m_maxRet, spaceToEnd);
|
||
goto end;
|
||
}
|
||
|
||
if (spaceToEnd == 0) { // be sure that the resBuff is full
|
||
// Only copy if the read pointer is not in the way
|
||
if (m_readPtr > m_startPtr + m_resBuffSize) {
|
||
memcpy(m_startPtr, m_endPtr, m_resBuffSize);
|
||
m_writePtr = m_startPtr + m_resBuffSize;
|
||
}
|
||
}
|
||
|
||
if (m_writePtr < m_readPtr) {
|
||
if (m_readPtr >= m_endPtr) { // readPtr is in resBuff?
|
||
m_writeSpace = min(m_maxRet, (size_t)(m_endPtr - m_writePtr)); // writePtr does not enter resbuff, wait for copy im readspace
|
||
} else {
|
||
m_writeSpace = min(m_maxRet, (size_t)(m_readPtr - m_writePtr));
|
||
}
|
||
goto end;
|
||
}
|
||
if (m_writePtr > m_readPtr) {
|
||
m_writeSpace = min(m_maxRet, spaceToEnd);
|
||
goto end;
|
||
}
|
||
m_log.assignf("[{}:{}]" ANSI_ESC_RED " writePtr == readPtr, writePtr {}, readPtr {}", __FILE__, __LINE__, m_writePtr - m_startPtr, m_readPtr - m_startPtr);
|
||
m_log.println();
|
||
|
||
end:
|
||
xSemaphoreGive(m_mutex);
|
||
return m_writeSpace;
|
||
}
|
||
//----------------------------------------------------------------------------------------------------------------------------------------------------
|
||
void AudioBuffer::bytesWritten(size_t bw) {
|
||
if (!m_init) return;
|
||
xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||
if (!bw) goto end;
|
||
|
||
if (bw > m_writeSpace) {
|
||
m_log.assignf("[{}:{}]" ANSI_ESC_RED " writeSpace < bw, writeSpace {}, bw {}", __FILE__, __LINE__, m_writeSpace, bw); // bw must not be larger than the queried m_writeSpace
|
||
m_log.println();
|
||
}
|
||
if (m_writePtr < m_readPtr && m_writePtr + bw > m_readPtr) {
|
||
m_log.assignf("[{}:{}]" ANSI_ESC_RED " writePtr overrruns readPtr, writePtr {}, readPtr {}, bw {}", __FILE__, __LINE__, m_writePtr - m_startPtr, m_readPtr - m_startPtr, bw);
|
||
m_log.println();
|
||
m_writePtr = m_readPtr;
|
||
goto end;
|
||
}
|
||
if (m_writePtr + bw > m_buffEnd) {
|
||
m_log.assignf("[{}:{}]" ANSI_ESC_RED " writePtr overrruns buffEnd, writePtr {}, buffEnd {}, bw {}", __FILE__, __LINE__, m_writePtr - m_startPtr, m_buffEnd - m_startPtr, bw);
|
||
m_log.println();
|
||
m_writePtr = m_buffEnd;
|
||
goto end;
|
||
}
|
||
|
||
m_writePtr += bw;
|
||
if (bw) {
|
||
if (m_writePtr == m_readPtr) m_isFull = true;
|
||
m_isEmpty = false;
|
||
}
|
||
|
||
end:
|
||
xSemaphoreGive(m_mutex);
|
||
return;
|
||
}
|
||
//----------------------------------------------------------------------------------------------------------------------------------------------------
|
||
size_t AudioBuffer::readSpace() {
|
||
if (!m_init) return 0;
|
||
xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||
|
||
if (m_readPtr >= m_endPtr && m_writePtr <= m_endPtr) {
|
||
size_t len = m_readPtr - m_endPtr;
|
||
if (m_writePtr > m_startPtr + len) {
|
||
// set new readptr
|
||
m_readPtr = m_startPtr + len;
|
||
}
|
||
}
|
||
|
||
m_readSpace = 0;
|
||
if (m_isEmpty) { goto end; }
|
||
|
||
if (m_isFull) {
|
||
m_readSpace = min(m_maxRet, (size_t)(m_buffEnd - m_readPtr));
|
||
goto end;
|
||
}
|
||
|
||
if (m_readPtr < m_writePtr) {
|
||
m_readSpace = min(m_maxRet, (size_t)(m_writePtr - m_readPtr));
|
||
goto end;
|
||
}
|
||
|
||
if (m_readPtr > m_writePtr) {
|
||
m_readSpace = min(m_maxRet, (size_t)(m_buffEnd - m_readPtr));
|
||
goto end;
|
||
}
|
||
m_log.assignf("[{}:{}]" ANSI_ESC_RED " writePtr == readPtr, writePtr {}, readPtr {}", __FILE__, __LINE__, m_writePtr - m_startPtr, m_readPtr - m_startPtr);
|
||
m_log.println();
|
||
end:
|
||
xSemaphoreGive(m_mutex);
|
||
return m_readSpace;
|
||
}
|
||
//----------------------------------------------------------------------------------------------------------------------------------------------------
|
||
void AudioBuffer::bytesWasRead(size_t br) {
|
||
if (!m_init) return;
|
||
xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||
|
||
if (!br) goto end;
|
||
|
||
if (m_readSpace < br) {
|
||
m_log.assignf("[{}:{}]" ANSI_ESC_RED " readSpace < br, rspc {}, br {}", __FILE__, __LINE__, m_readSpace, br); // br must not be larger than the queried m_readSpace
|
||
m_log.println();
|
||
vTaskDelay(100);
|
||
goto end;
|
||
}
|
||
|
||
if (m_readPtr < m_writePtr && m_readPtr + br > m_writePtr) {
|
||
m_log.assignf("[{}:{}]" ANSI_ESC_RED " readPtr overrruns writePtr, readPtr {}, writePtr {}, br {}", __FILE__, __LINE__, m_readPtr - m_startPtr, m_writePtr - m_startPtr, br);
|
||
m_log.println();
|
||
m_readPtr = m_writePtr;
|
||
vTaskDelay(100);
|
||
goto end;
|
||
}
|
||
if (m_readPtr + br > m_buffEnd) {
|
||
m_log.assignf("[{}:{}]" ANSI_ESC_RED " readPtr overrruns buffEnd, readPtr {}, buffEnd {}, bw {}", __FILE__, __LINE__, m_readPtr - m_startPtr, m_buffEnd - m_startPtr, br);
|
||
m_log.println();
|
||
m_readPtr = m_buffEnd;
|
||
vTaskDelay(100);
|
||
goto end;
|
||
}
|
||
|
||
m_readPtr += br;
|
||
|
||
if (br) {
|
||
if (m_readPtr == m_writePtr) {
|
||
m_isEmpty = true;
|
||
// m_log.assignf("readPtr {}, writePtr {}, br {}", m_readPtr - m_startPtr, m_writePtr - m_startPtr, br); m_log.println();
|
||
}
|
||
m_isFull = false;
|
||
}
|
||
|
||
end:
|
||
xSemaphoreGive(m_mutex);
|
||
return;
|
||
}
|
||
//----------------------------------------------------------------------------------------------------------------------------------------------------
|
||
uint8_t* AudioBuffer::getWritePtr() {
|
||
return m_writePtr;
|
||
}
|
||
|
||
uint8_t* AudioBuffer::getReadPtr() {
|
||
return m_readPtr;
|
||
}
|
||
|
||
void AudioBuffer::reset() {
|
||
m_writePtr = m_buffer.get();
|
||
m_readPtr = m_buffer.get();
|
||
m_isEmpty = true;
|
||
m_isFull = false;
|
||
m_readSpace = 0;
|
||
m_writeSpace = min(m_maxRet, (size_t)(m_buffEnd - m_writePtr));
|
||
}
|
||
|
||
void AudioBuffer::showStatus() {
|
||
m_log.assignf("\nfilled {}, free {}\n", bufferFilled(), freeSpace());
|
||
m_log.appendf("writeSpace {}, readSpace {}\n", writeSpace(), readSpace());
|
||
m_log.appendf("writePtr {}, readPtr {}\n", m_writePtr - m_startPtr, m_readPtr - m_startPtr);
|
||
m_log.appendf("isEmpty {}, isFull {}\n\n", m_isEmpty, m_isFull);
|
||
m_log.print();
|
||
}
|
||
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
// 📌📌📌 A U D I O 📌📌📌
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
Audio::Audio(uint8_t i2sPort) {
|
||
|
||
m_f_I2S_init = false;
|
||
mutex_playAudioData = xSemaphoreCreateMutex();
|
||
mutex_audioTask = xSemaphoreCreateMutex();
|
||
mutex_audioTaskIsDecoding = xSemaphoreCreateMutex();
|
||
|
||
clientsecure.setInsecure();
|
||
m_i2s_items.i2s_num = i2sPort; // i2s port number
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
Audio::~Audio() {
|
||
stopSong();
|
||
setDefaults();
|
||
|
||
i2s_channel_disable(m_i2s_tx_handle);
|
||
i2s_del_channel(m_i2s_tx_handle);
|
||
stopAudioTask();
|
||
vSemaphoreDelete(mutex_playAudioData);
|
||
vSemaphoreDelete(mutex_audioTask);
|
||
vSemaphoreDelete(mutex_audioTaskIsDecoding);
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::destroy_decoder() {
|
||
if (m_decoder && m_decoder->isValid()) {
|
||
info(*this, evt_info, "{}Decoder has been destroyed", m_decoder->whoIsIt());
|
||
m_decoder->reset();
|
||
m_decoder.reset();
|
||
}
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
std::unique_ptr<Decoder> Audio::createDecoder(const std::string& type) {
|
||
destroy_decoder();
|
||
if (type == "MP3") return std::make_unique<MP3Decoder>(*this);
|
||
// if (type == "FLAC") return std::make_unique<FlacDecoder>(*this);
|
||
// if (type == "OPUS") return std::make_unique<OpusDecoder>(*this);
|
||
// if (type == "AAC") return std::make_unique<AACDecoder>(*this);
|
||
// if (type == "VORBIS") return std::make_unique<VorbisDecoder>(*this);
|
||
if (type == "WAV") return std::make_unique<WavDecoder>(*this);
|
||
return nullptr;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::initInBuff() {
|
||
if (!InBuff.isInitialized()) {
|
||
size_t size = InBuff.init();
|
||
if (size > 0) { info(*this, evt_info, "inputBufferSize: {} bytes", size - 1); }
|
||
}
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
esp_err_t Audio::I2Sstart() {
|
||
zeroI2Sbuff();
|
||
esp_err_t err = ESP_FAIL;
|
||
if (!m_f_i2s_channel_enabled) {
|
||
err = i2s_channel_enable(m_i2s_tx_handle);
|
||
if (err == ESP_OK) m_f_i2s_channel_enabled = true;
|
||
}
|
||
return err;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
esp_err_t Audio::I2Sstop() {
|
||
m_outBuff.clear(); // Clear OutputBuffer
|
||
m_resamplesBuff.clear(); // Clear m_resamplesBuff
|
||
esp_err_t err = ESP_FAIL;
|
||
if (m_f_i2s_channel_enabled) err = i2s_channel_disable(m_i2s_tx_handle);
|
||
m_f_i2s_channel_enabled = false;
|
||
return err;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::zeroI2Sbuff() {
|
||
uint8_t buff[2] = {0, 0}; // From IDF V5 there is no longer the zero_dma_buff() function.
|
||
size_t bytes_loaded = 0; // As a replacement, we write a small amount of zeros in the buffer and thus reset the entire buffer.
|
||
i2s_channel_preload_data(m_i2s_tx_handle, buff, 2, &bytes_loaded);
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::setDefaults() {
|
||
stopSong();
|
||
initInBuff(); // initialize InputBuffer if not already done
|
||
|
||
InBuff.reset();
|
||
m_streamTitle.reset();
|
||
m_streamURL.reset();
|
||
m_playlistBuff.reset();
|
||
m_m3u8_host.reset();
|
||
|
||
m_outBuff.clear(); // Clear OutputBuffer
|
||
m_resamplesBuff.clear(); // Clear m_resamplesBuff
|
||
m_syltTimeStamp.clear();
|
||
|
||
vector_clear_and_shrink(m_playlistURL);
|
||
vector_clear_and_shrink(m_playlistContent);
|
||
vector_clear_and_shrink(m_syltLines);
|
||
|
||
client.stop();
|
||
clientsecure.stop();
|
||
m_client = static_cast<NetworkClient*>(&client); /* default to *something* so that no NULL deref can happen */
|
||
|
||
m_f_timeout = false;
|
||
m_f_chunked = false; // Assume not chunked
|
||
m_f_firstmetabyte = false;
|
||
m_f_playing = false;
|
||
m_f_tts = false;
|
||
m_f_firstCall = true; // InitSequence for processWebstream and processLocalFile
|
||
m_cat.firstCall = true; // InitSequence for calculateAudioTime
|
||
m_pplM3U8.firstCall = true; // InitSequence for parsePlaylist_M3U8
|
||
m_f_firstPlayCall = true; // InitSequence for playAudioData
|
||
m_f_firstLoop = true;
|
||
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_f_ogg = false;
|
||
m_f_m4aID3dataAreRead = false;
|
||
m_f_stream = false;
|
||
m_f_decode_ready = false;
|
||
m_f_eof = false;
|
||
m_f_ID3v1TagFound = false;
|
||
m_f_lockInBuffer = false;
|
||
m_f_acceptRanges = false;
|
||
m_f_connectionClose = false;
|
||
m_f_allDataReceived = false;
|
||
|
||
m_codec = CODEC_NONE;
|
||
m_dataMode = AUDIO_NONE;
|
||
m_streamType = ST_NONE;
|
||
m_playlistFormat = FORMAT_NONE;
|
||
m_m3u8Codec = CODEC_AAC;
|
||
|
||
m_validSamples = 0;
|
||
m_audioCurrentTime = 0;
|
||
m_audioFileDuration = 0;
|
||
m_resumeFilePos = -1;
|
||
m_audioDataStart = 0;
|
||
m_audioDataSize = 0;
|
||
m_audioFileSize = 0;
|
||
m_avr_bitrate = 0;
|
||
m_nominal_bitrate = 0;
|
||
m_bytesNotConsumed = 0; // counts all not decodable bytes
|
||
m_chunkcount = 0; // for chunked streams
|
||
m_curSample = 0;
|
||
m_LFcount = 0; // For end of header detection
|
||
m_controlCounter = 0; // Status within readID3data() and readWaveHeader()
|
||
m_channels = 2; // assume stereo #209
|
||
m_ID3Size = 0;
|
||
m_haveNewFilePos = 0;
|
||
m_validSamples = 0;
|
||
m_M4A_chConfig = 0;
|
||
m_M4A_objectType = 0;
|
||
m_M4A_sampleRate = 0;
|
||
m_lastGranulePosition = 0;
|
||
m_validSamples = 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;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
|
||
/*
|
||
Text to speech API provides a speech endpoint based on our TTS (text-to-speech) model.
|
||
More info: https://platform.openai.com/docs/guides/text-to-speech/text-to-speech
|
||
|
||
Request body:
|
||
model (string) [Required] - One of the available TTS models: tts-1 or tts-1-hd
|
||
input (string) [Required] - The text to generate audio for. The maximum length is 4096 characters.
|
||
instructions (string) [Optional] - A description of the desired characteristics of the generated audio.
|
||
voice (string) [Required] - The voice to use when generating the audio. Supported voices are alloy, echo, fable, onyx, nova, and shimmer.
|
||
response_format (string) [Optional] - Defaults to mp3. The format to audio in. Supported formats are mp3, opus, aac, and flac.
|
||
speed (number) [Optional] - Defaults to 1. The speed of the generated audio. Select a value from 0.25 to 4.0. 1.0 is the default.
|
||
|
||
Usage: audio.openai_speech(OPENAI_API_KEY, "tts-1", input, instructions, "shimmer", "mp3", "1");
|
||
*/
|
||
bool Audio::openai_speech(const String& api_key, const String& model, const String& input, const String& instructions, const String& voice, const String& response_format, const String& speed) {
|
||
ps_ptr<char> host;
|
||
host.assign("api.openai.com");
|
||
char path[] = "/v1/audio/speech";
|
||
|
||
if (input == "") {
|
||
AUDIO_LOG_WARN("input text is empty");
|
||
stopSong();
|
||
return false;
|
||
}
|
||
xSemaphoreTakeRecursive(mutex_playAudioData, 0.3 * configTICK_RATE_HZ);
|
||
|
||
setDefaults();
|
||
m_f_ssl = true;
|
||
|
||
m_speechtxt.assign(input.c_str());
|
||
|
||
// Escape special characters in input
|
||
String input_clean = "";
|
||
for (int i = 0; i < input.length(); i++) {
|
||
char c = input.charAt(i);
|
||
if (c == '\"') {
|
||
input_clean += "\\\"";
|
||
} else if (c == '\n') {
|
||
input_clean += "\\n";
|
||
} else if (c == '\r') {
|
||
input_clean += "\\r";
|
||
} else if (c == '\t') {
|
||
input_clean += "\\t";
|
||
} else if (c == '\\') {
|
||
input_clean += "\\\\";
|
||
} else if (c == '\b') {
|
||
input_clean += "\\b";
|
||
} else if (c == '\f') {
|
||
input_clean += "\\f";
|
||
} else {
|
||
input_clean += c;
|
||
}
|
||
}
|
||
|
||
// Escape special characters in instructions
|
||
String instructions_clean = "";
|
||
for (int i = 0; i < instructions.length(); i++) {
|
||
char c = instructions.charAt(i);
|
||
if (c == '\"') {
|
||
instructions_clean += "\\\"";
|
||
} else if (c == '\n') {
|
||
instructions_clean += "\\n";
|
||
} else if (c == '\r') {
|
||
instructions_clean += "\\r";
|
||
} else if (c == '\t') {
|
||
instructions_clean += "\\t";
|
||
} else if (c == '\\') {
|
||
instructions_clean += "\\\\";
|
||
} else if (c == '\b') {
|
||
instructions_clean += "\\b";
|
||
} else if (c == '\f') {
|
||
instructions_clean += "\\f";
|
||
} else {
|
||
instructions_clean += c;
|
||
}
|
||
}
|
||
|
||
String post_body = "{"
|
||
"\"model\": \"" +
|
||
model + "\"," + "\"stream\": true," + // add
|
||
"\"input\": \"" + input_clean + "\"," + "\"instructions\": \"" + instructions_clean + "\"," + "\"voice\": \"" + voice + "\"," + "\"response_format\": \"" + response_format +
|
||
"\"," + "\"speed\": " + speed + "}";
|
||
|
||
String http_request =
|
||
// "POST " + String(path) + " HTTP/1.0\r\n" // UNKNOWN ERROR CODE (0050) - crashing on HTTP/1.1 need to use HTTP/1.0
|
||
"POST " + String(path) + " HTTP/1.1\r\n" + "Host: " + host.get() + "\r\n" + "Authorization: Bearer " + api_key + "\r\n" + "Accept-Encoding: identity;q=1,*;q=0\r\n" +
|
||
"User-Agent: nArija/1.0\r\n" + "Content-Type: application/json; charset=utf-8\r\n" + "Content-Length: " + post_body.length() +
|
||
"\r\n"
|
||
// + "Connection: close\r\n" + "\r\n"
|
||
+ "\r\n" + post_body + "\r\n";
|
||
|
||
bool res = true;
|
||
int port = 443;
|
||
m_client = static_cast<NetworkClientSecure*>(&clientsecure);
|
||
|
||
uint32_t t = millis();
|
||
info(*this, evt_info, "Connect to: \"{}\"", host.get());
|
||
res = m_client->connect(host.get(), port, m_timeout_ms_ssl);
|
||
if (res) {
|
||
uint32_t dt = millis() - t;
|
||
m_lastHost.assign(host.get());
|
||
m_currentHost.clone_from(host);
|
||
info(*this, evt_info, "{} has been established in {} ms", m_f_ssl ? "SSL" : "Connection", dt);
|
||
m_f_running = true;
|
||
}
|
||
|
||
m_expectedCodec = CODEC_NONE;
|
||
m_expectedPlsFmt = FORMAT_NONE;
|
||
|
||
if (res) {
|
||
m_client->print(http_request);
|
||
if (response_format == "mp3") m_expectedCodec = CODEC_MP3;
|
||
if (response_format == "opus") m_expectedCodec = CODEC_OPUS;
|
||
if (response_format == "aac") m_expectedCodec = CODEC_AAC;
|
||
if (response_format == "flac") m_expectedCodec = CODEC_FLAC;
|
||
|
||
m_dataMode = HTTP_RESPONSE_HEADER;
|
||
m_f_tts = true;
|
||
} else {
|
||
AUDIO_LOG_WARN("Request {} failed!", host.get());
|
||
}
|
||
xSemaphoreGiveRecursive(mutex_playAudioData);
|
||
return res;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
audiolib::hwoe_t Audio::dismantle_host(const char* host) {
|
||
if (!host) return {};
|
||
|
||
audiolib::hwoe_t result;
|
||
|
||
const char* p = host;
|
||
|
||
// 🔐 1. SSL check
|
||
if (strncmp(p, "https://", 8) == 0) {
|
||
result.ssl = true;
|
||
p += 8;
|
||
} else if (strncmp(p, "http://", 7) == 0) {
|
||
result.ssl = false;
|
||
p += 7;
|
||
} else { // No valid scheme -> error or acceptance http
|
||
result.ssl = false;
|
||
}
|
||
|
||
// ❓ 2. extract host (from p to ':' or '/' or '?')
|
||
const char* host_start = p;
|
||
const char* port_sep = strchr(p, ':');
|
||
const char* path_sep = strchr(p, '/');
|
||
const char* query_sep = strchr(p, '?');
|
||
|
||
const char* host_end = p + strlen(p); // default: end of string
|
||
if (port_sep && port_sep < host_end) host_end = port_sep;
|
||
if (path_sep && path_sep < host_end) host_end = path_sep;
|
||
if (query_sep && query_sep < host_end) host_end = query_sep;
|
||
result.hwoe.copy_from(host_start, host_end - host_start);
|
||
result.rqh_host.clone_from(result.hwoe);
|
||
|
||
// ❓ 3. extract port
|
||
result.port = result.ssl ? 443 : 80; // default
|
||
if (port_sep && (!path_sep || port_sep < path_sep)) {
|
||
result.port = atoi(port_sep + 1);
|
||
result.rqh_host.appendf(":{}", result.port);
|
||
}
|
||
|
||
// ❓ 4. extract extension (path)
|
||
if (path_sep) {
|
||
const char* path_start = path_sep + 1;
|
||
const char* path_end = query_sep ? query_sep : host + strlen(host);
|
||
result.extension.copy_from(path_start, path_end - path_start);
|
||
}
|
||
|
||
// ❓ 5. extract query string
|
||
if (query_sep) { result.query_string.assign(query_sep + 1); }
|
||
|
||
return result;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::connecttohost(const char* host, const char* user, const char* pwd) { // user and pwd for authentification only, can be empty
|
||
|
||
ps_ptr<char> c_host = host; // copy of host
|
||
ps_ptr<char> c_user = user; // copy of user
|
||
ps_ptr<char> c_pwd = pwd; // copy of password
|
||
|
||
if (!c_host.valid()) {
|
||
AUDIO_LOG_ERROR("Hostaddress is empty");
|
||
stopSong();
|
||
return false;
|
||
}
|
||
|
||
c_host.trim();
|
||
|
||
if (c_host.strlen() < 8) {
|
||
AUDIO_LOG_ERROR("Hostaddress is too short");
|
||
stopSong();
|
||
return false;
|
||
}
|
||
|
||
if (c_host.strlen() > 2048) {
|
||
AUDIO_LOG_ERROR("Hostaddress is too long");
|
||
stopSong();
|
||
return false;
|
||
} // max length in Chrome DevTools
|
||
|
||
const char* user_agent_0 = "Mozilla/5.0 (X11; Linux x86_64) Chrome/146.0.0.0 Safari/537.36";
|
||
const char* user_agent_1 = "VLC/3.0.21 LibVLC/3.0.21 AppleWebKit/537.36 (KHTML, like Gecko)";
|
||
|
||
bool res = false; // return value
|
||
uint16_t port = 0; // port number
|
||
uint16_t authLen = 0; // length of authorization
|
||
uint32_t timestamp = 0; // timeout surveillance
|
||
|
||
ps_ptr<char> hwoe; // host without extension
|
||
ps_ptr<char> rqh_host; // host for request header
|
||
ps_ptr<char> extension; // extension
|
||
ps_ptr<char> query_string; // parameter
|
||
ps_ptr<char> path; // extension + '?' + parameter
|
||
ps_ptr<char> rqh; // request header
|
||
|
||
xSemaphoreTakeRecursive(mutex_playAudioData, 0.3 * configTICK_RATE_HZ);
|
||
|
||
auto dismantledHost = dismantle_host(c_host.get());
|
||
|
||
// https://edge.live.mp3.mdn.newmedia.nacamar.net:8000/ps-charivariwb/livestream.mp3;?user=ps-charivariwb;&pwd=ps-charivariwb-------
|
||
// | | | | |
|
||
// | | | | | (query string)
|
||
// ssl?| |<-----host without extension-------->|port|<----- --extension----------->|<-first parameter->|<-second parameter->.......
|
||
// |
|
||
// |<-----------------------------path------------------------------------>......
|
||
|
||
m_f_ssl = dismantledHost.ssl;
|
||
port = dismantledHost.port;
|
||
hwoe = dismantledHost.hwoe.c_get();
|
||
rqh_host = dismantledHost.rqh_host.c_get();
|
||
extension = dismantledHost.extension.c_get();
|
||
query_string = dismantledHost.query_string.c_get();
|
||
|
||
if (query_string.strlen()) extension.appendf("?{}", query_string);
|
||
path = urlencode(extension.get(), true);
|
||
|
||
// optional basic authorization
|
||
if (c_user.valid() && c_pwd.valid()) authLen = c_user.strlen() + c_pwd.strlen();
|
||
ps_ptr<char> authorization;
|
||
ps_ptr<char> toEncode;
|
||
authorization.calloc(base64_encode_expected_len(authLen + 1) + 1, "authorization");
|
||
if (authLen > 0) {
|
||
toEncode.assignf("{}:{}", c_user, c_pwd);
|
||
b64encode((const char*)toEncode.get(), toEncode.strlen(), authorization.get());
|
||
}
|
||
|
||
setDefaults();
|
||
|
||
rqh.assignf("GET /{}", path);
|
||
rqh.append(" HTTP/1.1\r\n");
|
||
rqh.appendf("Host: {}\r\n", rqh_host);
|
||
rqh.append("Icy-MetaData:1\r\n");
|
||
rqh.append("Pragma: no-cache\r\n");
|
||
rqh.append("Cache-Control: no-cache\r\n");
|
||
rqh.append("Range: bytes=0-\r\n");
|
||
rqh.append("Accept: */*\r\n");
|
||
rqh.appendf("User-Agent: {}\r\n", m_f_alt_user_agent ? user_agent_0 : user_agent_1);
|
||
if (authLen > 0) {
|
||
rqh.append("Authorization: Basic ");
|
||
rqh.append(authorization);
|
||
rqh.append("\r\n");
|
||
}
|
||
rqh.append("Accept-Encoding: identity;q=1,*;q=0\r\n");
|
||
rqh.append("Connection: keep-alive\r\n\r\n");
|
||
|
||
if (m_f_ssl) {
|
||
m_client = static_cast<NetworkClientSecure*>(&clientsecure);
|
||
if (port == 80) port = 443;
|
||
} else {
|
||
m_client = static_cast<NetworkClient*>(&client);
|
||
}
|
||
|
||
timestamp = millis();
|
||
m_client->setTimeout(m_f_ssl ? m_timeout_ms_ssl : m_timeout_ms);
|
||
|
||
info(*this, evt_info, "connect to: \"{}\" on port {} path \"/{}\"", hwoe.get(), port, path.get());
|
||
res = m_client->connect(hwoe.get(), port);
|
||
|
||
m_expectedCodec = CODEC_NONE;
|
||
m_expectedPlsFmt = FORMAT_NONE;
|
||
|
||
if (res) {
|
||
uint32_t dt = millis() - timestamp;
|
||
info(*this, evt_info, "{} has been established in {} ms", m_f_ssl ? "SSL" : "Connection", dt);
|
||
m_f_running = true;
|
||
m_client->print(rqh.get());
|
||
if (extension.ends_with_icase(".mp3")) m_expectedCodec = CODEC_MP3;
|
||
if (extension.ends_with_icase(".aac")) m_expectedCodec = CODEC_AAC;
|
||
if (extension.ends_with_icase(".wav")) m_expectedCodec = CODEC_WAV;
|
||
if (extension.ends_with_icase(".m4a")) m_expectedCodec = CODEC_M4A;
|
||
if (extension.ends_with_icase(".ogg")) m_expectedCodec = CODEC_OGG;
|
||
if (extension.ends_with_icase(".flac")) m_expectedCodec = CODEC_FLAC;
|
||
if (extension.ends_with_icase("-flac")) m_expectedCodec = CODEC_FLAC;
|
||
if (extension.ends_with_icase(".opus")) m_expectedCodec = CODEC_OPUS;
|
||
if (extension.ends_with_icase("/opus")) m_expectedCodec = CODEC_OPUS;
|
||
if (extension.ends_with_icase(".asx")) m_expectedPlsFmt = FORMAT_ASX;
|
||
if (extension.ends_with_icase(".m3u")) m_expectedPlsFmt = FORMAT_M3U;
|
||
if (extension.ends_with_icase(".pls")) m_expectedPlsFmt = FORMAT_PLS;
|
||
if (extension.contains(".m3u8")) m_expectedPlsFmt = FORMAT_M3U8;
|
||
|
||
m_currentHost = c_host;
|
||
m_lastHost = c_host;
|
||
info(*this, evt_lasthost, "{}", m_lastHost.c_get());
|
||
m_dataMode = HTTP_RESPONSE_HEADER; // Handle header
|
||
m_streamType = ST_WEBSTREAM;
|
||
} else {
|
||
AUDIO_LOG_ERROR("Request \"{}\" failed!", c_host.get());
|
||
m_f_running = false;
|
||
}
|
||
xSemaphoreGiveRecursive(mutex_playAudioData);
|
||
return res;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::httpPrint(const char* host) {
|
||
|
||
ps_ptr<char> c_host = host; // copy of host
|
||
|
||
if (!c_host.valid()) {
|
||
AUDIO_LOG_ERROR("Hostaddress is empty");
|
||
stopSong();
|
||
return false;
|
||
}
|
||
|
||
uint16_t port = 0; // port number
|
||
ps_ptr<char> hwoe; // host without extension
|
||
ps_ptr<char> rqh_host; // host in request header
|
||
ps_ptr<char> extension; // extension
|
||
ps_ptr<char> query_string; // parameter
|
||
ps_ptr<char> path; // extension + '?' + parameter
|
||
ps_ptr<char> rqh; // request header
|
||
ps_ptr<char> cur_hwoe; // m_currenthost without extension
|
||
|
||
c_host.trim();
|
||
auto dismantledHost = dismantle_host(c_host.get());
|
||
|
||
// https://edge.live.mp3.mdn.newmedia.nacamar.net:8000/ps-charivariwb/livestream.mp3;?user=ps-charivariwb;&pwd=ps-charivariwb-------
|
||
// | | | | |
|
||
// | | | | | (query string)
|
||
// ssl?| |<-----host without extension-------->|port|<----- --extension----------->|<-first parameter->|<-second parameter->.......
|
||
// |
|
||
// |<-----------------------------path------------------------------------------->
|
||
|
||
m_f_ssl = dismantledHost.ssl;
|
||
port = dismantledHost.port;
|
||
hwoe = dismantledHost.hwoe;
|
||
rqh_host = dismantledHost.rqh_host;
|
||
extension = dismantledHost.extension;
|
||
query_string = dismantledHost.query_string;
|
||
|
||
if (query_string.strlen()) extension.appendf("?{}", query_string);
|
||
path = urlencode(extension.get(), true);
|
||
|
||
if (!m_currentHost.valid()) m_currentHost.assign("");
|
||
auto dismantledLastHost = dismantle_host(m_currentHost.get());
|
||
cur_hwoe = dismantledLastHost.hwoe;
|
||
|
||
bool f_equal = true;
|
||
if (hwoe == cur_hwoe && port == dismantledLastHost.port) {
|
||
f_equal = true;
|
||
} else {
|
||
f_equal = false;
|
||
}
|
||
|
||
rqh.assignf("GET /{}", path);
|
||
rqh.append(" HTTP/1.1\r\n");
|
||
rqh.appendf("Host: {}\r\n", rqh_host);
|
||
rqh.append("Icy-MetaData:1\r\n");
|
||
rqh.append("Accept:*/*\r\n");
|
||
rqh.append("User-Agent: VLC/3.0.21 LibVLC/3.0.21 AppleWebKit/537.36 (KHTML, like Gecko)\r\n");
|
||
rqh.append("Accept-Encoding: identity;q=1,*;q=0\r\n");
|
||
rqh.append("Connection: keep-alive\r\n\r\n");
|
||
|
||
info(*this, evt_info, "next URL: \"{}\"", c_host.get());
|
||
|
||
if (f_equal == false) {
|
||
if (m_client->connected()) m_client->stop();
|
||
}
|
||
if (!m_client->connected()) {
|
||
if (m_f_ssl) {
|
||
m_client = static_cast<NetworkClientSecure*>(&clientsecure);
|
||
if (m_f_ssl && port == 80) port = 443;
|
||
} else {
|
||
m_client = static_cast<NetworkClient*>(&client);
|
||
}
|
||
if (f_equal) info(*this, evt_info, "The host has disconnected, reconnecting");
|
||
|
||
if (!m_client->connect(hwoe.get(), port)) {
|
||
AUDIO_LOG_ERROR("connection lost {}", c_host.c_get());
|
||
stopSong();
|
||
return false;
|
||
}
|
||
}
|
||
m_currentHost = c_host;
|
||
m_client->print(rqh.get());
|
||
|
||
if (extension.ends_with_icase(".mp3"))
|
||
m_expectedCodec = CODEC_MP3;
|
||
else if (extension.ends_with_icase(".aac"))
|
||
m_expectedCodec = CODEC_AAC;
|
||
else if (extension.ends_with_icase(".wav"))
|
||
m_expectedCodec = CODEC_WAV;
|
||
else if (extension.ends_with_icase(".m4a"))
|
||
m_expectedCodec = CODEC_M4A;
|
||
else if (extension.ends_with_icase(".flac"))
|
||
m_expectedCodec = CODEC_FLAC;
|
||
else if (extension.ends_with_icase(".m4s")) { // is MP4-Container
|
||
stopSong();
|
||
AUDIO_LOG_WARN("MP4-Container not supported");
|
||
return false;
|
||
} else
|
||
m_expectedCodec = CODEC_NONE;
|
||
|
||
if (extension.ends_with_icase(".asx"))
|
||
m_expectedPlsFmt = FORMAT_ASX;
|
||
else if (extension.ends_with_icase(".m3u"))
|
||
m_expectedPlsFmt = FORMAT_M3U;
|
||
else if (extension.contains(".m3u8"))
|
||
m_expectedPlsFmt = FORMAT_M3U8;
|
||
else if (extension.ends_with_icase(".pls"))
|
||
m_expectedPlsFmt = FORMAT_PLS;
|
||
else
|
||
m_expectedPlsFmt = FORMAT_NONE;
|
||
|
||
m_audioFileSize = 0;
|
||
m_dataMode = HTTP_RESPONSE_HEADER; // Handle header
|
||
m_streamType = ST_WEBSTREAM;
|
||
m_f_chunked = false;
|
||
AUDIO_LOG_DEBUG("playlistFormat {}, dataMode {}, streamType: {}", plsFmtStr[m_playlistFormat], dataModeStr[m_dataMode], streamTypeStr[m_streamType]);
|
||
return true;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::httpRange(uint32_t seek, uint32_t length) {
|
||
|
||
if (!m_f_running) return false;
|
||
|
||
uint16_t port = 0; // port number
|
||
ps_ptr<char> c_host; // copy of host
|
||
ps_ptr<char> hwoe; // host without extension
|
||
ps_ptr<char> rqh_host; // host in request header
|
||
ps_ptr<char> extension; // extension
|
||
ps_ptr<char> query_string; // parameter
|
||
ps_ptr<char> path; // extension + '?' + parameter
|
||
ps_ptr<char> rqh; // request header
|
||
ps_ptr<char> cur_hwoe; // m_currenthost without extension
|
||
ps_ptr<char> range; // e.g. "Range: bytes=124-"
|
||
|
||
c_host.clone_from(m_currentHost);
|
||
c_host.trim();
|
||
auto dismantledHost = dismantle_host(c_host.get());
|
||
|
||
// https://edge.live.mp3.mdn.newmedia.nacamar.net:8000/ps-charivariwb/livestream.mp3;?user=ps-charivariwb;&pwd=ps-charivariwb-------
|
||
// | | | | |
|
||
// | | | | | (query string)
|
||
// ssl?| |<-----host without extension-------->|port|<----- --extension----------->|<-first parameter->|<-second parameter->.......
|
||
// |
|
||
// |<-----------------------------path------------------------------------------->
|
||
|
||
m_f_ssl = dismantledHost.ssl;
|
||
port = dismantledHost.port;
|
||
if (dismantledHost.hwoe.valid()) hwoe.clone_from(dismantledHost.hwoe);
|
||
if (dismantledHost.rqh_host.valid()) rqh_host.clone_from(dismantledHost.rqh_host);
|
||
if (dismantledHost.extension.valid()) extension.clone_from(dismantledHost.extension);
|
||
if (dismantledHost.query_string.valid()) query_string.clone_from(dismantledHost.query_string);
|
||
|
||
if (extension.valid()) path.assign(extension.get());
|
||
if (query_string.valid()) {
|
||
path.append("?");
|
||
path.append(query_string.get());
|
||
}
|
||
if (!hwoe.valid()) hwoe.assign("");
|
||
if (!extension.valid()) extension.assign("");
|
||
if (!path.valid()) path.assign("");
|
||
|
||
path = urlencode(path.get(), true);
|
||
|
||
if (!m_currentHost.valid()) m_currentHost.assign("");
|
||
auto dismantledLastHost = dismantle_host(m_currentHost.get());
|
||
cur_hwoe.clone_from(dismantledLastHost.hwoe);
|
||
|
||
if (length == UINT32_MAX)
|
||
range.assignf("Range: bytes={}-\r\n", seek);
|
||
else
|
||
range.assignf("Range: bytes={}-{}\r\n", seek, seek + length);
|
||
|
||
rqh.assignf("GET /{} HTTP/1.1\r\n", path.get());
|
||
rqh.appendf("Host: {}\r\n", rqh_host.get());
|
||
rqh.append("Accept: */*\r\n");
|
||
rqh.append("Accept-Encoding: identity;q=1,*;q=0\r\n");
|
||
rqh.append("Cache-Control: no-cache\r\n");
|
||
rqh.append("Connection: keep-alive\r\n");
|
||
rqh.appendf(range.c_get());
|
||
rqh.appendf("Referer: {}\r\n", m_currentHost.c_get());
|
||
rqh.append("Sec-GPC: 1\r\n");
|
||
rqh.append("User-Agent: VLC/3.0.21 LibVLC/3.0.21 AppleWebKit/537.36 (KHTML, like Gecko)\r\n\r\n");
|
||
|
||
if (m_client->connected()) { m_client->stop(); }
|
||
if (m_f_ssl) {
|
||
m_client = static_cast<NetworkClientSecure*>(&clientsecure);
|
||
if (m_f_ssl && port == 80) port = 443;
|
||
} else {
|
||
m_client = static_cast<NetworkClient*>(&client);
|
||
}
|
||
|
||
if (!m_client->connect(hwoe.get(), port)) {
|
||
AUDIO_LOG_ERROR("connection lost {}", c_host.c_get());
|
||
stopSong();
|
||
return false;
|
||
}
|
||
|
||
// AUDIO_LOG_INFO("rqh \n{}", rqh.get());
|
||
|
||
m_client->print(rqh.c_get());
|
||
// m_resumeFilePos = seek; // used in processWebFile()
|
||
m_dataMode = HTTP_RANGE_HEADER;
|
||
return true;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::connecttoFS(fs::FS& fs, const char* path, int32_t fileStartTime) {
|
||
|
||
xSemaphoreTakeRecursive(mutex_playAudioData, 0.3 * configTICK_RATE_HZ);
|
||
ps_ptr<char> c_path;
|
||
ps_ptr<char> audioPath;
|
||
bool res = false;
|
||
m_fileStartTime = fileStartTime;
|
||
m_codec = CODEC_NONE;
|
||
|
||
if (!path) {
|
||
AUDIO_LOG_ERROR("file path is not set");
|
||
goto exit;
|
||
} // guard
|
||
c_path.copy_from(path); // copy from path
|
||
c_path.trim();
|
||
if (!c_path.contains(".")) {
|
||
AUDIO_LOG_ERROR("No file extension found");
|
||
goto exit;
|
||
} // guard
|
||
setDefaults(); // free buffers an set defaults
|
||
|
||
if (c_path.ends_with_icase(".mp3")) m_codec = CODEC_MP3;
|
||
if (c_path.ends_with_icase(".m4a")) m_codec = CODEC_M4A;
|
||
if (c_path.ends_with_icase(".aac")) m_codec = CODEC_AAC;
|
||
if (c_path.ends_with_icase(".wav")) m_codec = CODEC_WAV;
|
||
if (c_path.ends_with_icase(".flac")) m_codec = CODEC_FLAC;
|
||
if (c_path.ends_with_icase(".opus")) m_codec = CODEC_OGG;
|
||
if (c_path.ends_with_icase(".ogg")) m_codec = CODEC_OGG;
|
||
if (c_path.ends_with_icase(".oga")) m_codec = CODEC_OGG;
|
||
|
||
if (m_codec == CODEC_OGG) m_f_ogg = true;
|
||
if (m_codec == CODEC_NONE) { // guard
|
||
int dotPos = c_path.last_index_of('.');
|
||
AUDIO_LOG_WARN("The {} format is not supported", path + dotPos);
|
||
goto exit;
|
||
}
|
||
|
||
if (!c_path.starts_with("/")) c_path.insert("/", 0);
|
||
|
||
if (!fs.exists(c_path.get())) {
|
||
AUDIO_LOG_WARN("file not found: {}", c_path.get());
|
||
goto exit;
|
||
}
|
||
info(*this, evt_info, "Reading file: \"{}\"", c_path.get());
|
||
m_audiofile = fs.open(c_path.get());
|
||
m_dataMode = AUDIO_LOCALFILE;
|
||
m_audioFileSize = m_audiofile.size();
|
||
m_f_running = true;
|
||
res = true;
|
||
|
||
exit:
|
||
xSemaphoreGiveRecursive(mutex_playAudioData);
|
||
return res;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::connecttospeech(const char* speech, const char* lang) {
|
||
xSemaphoreTakeRecursive(mutex_playAudioData, 0.3 * configTICK_RATE_HZ);
|
||
|
||
setDefaults();
|
||
char host[] = "translate.google.com.vn";
|
||
char path[] = "/translate_tts";
|
||
|
||
m_speechtxt.assign(speech); // unique pointer takes care of the memory management
|
||
auto urlStr = urlencode(speech, false); // percent encoding
|
||
|
||
ps_ptr<char> req; // request header
|
||
req.assign("GET ");
|
||
req.append(path);
|
||
req.append("?ie=UTF-8&tl=");
|
||
req.append(lang);
|
||
req.append("&client=tw-ob&q=");
|
||
req.append(urlStr.get());
|
||
req.append(" HTTP/1.1\r\n");
|
||
req.append("Host: ");
|
||
req.append(host);
|
||
req.append("\r\n");
|
||
req.append("User-Agent: Mozilla/5.0 \r\n");
|
||
req.append("Accept-Encoding: identity\r\n");
|
||
req.append("Accept: text/html\r\n");
|
||
req.append("Connection: close\r\n\r\n");
|
||
|
||
m_client = static_cast<NetworkClient*>(&client);
|
||
info(*this, evt_info, "connect to \"{}\"", host);
|
||
if (!m_client->connect(host, 80)) {
|
||
AUDIO_LOG_ERROR("Connection failed");
|
||
xSemaphoreGiveRecursive(mutex_playAudioData);
|
||
return false;
|
||
}
|
||
m_client->print(req.get());
|
||
|
||
m_f_running = true;
|
||
m_f_ssl = false;
|
||
m_f_tts = true;
|
||
m_dataMode = HTTP_RESPONSE_HEADER;
|
||
m_lastHost.assign(host);
|
||
m_currentHost.copy_from(host);
|
||
xSemaphoreGiveRecursive(mutex_playAudioData);
|
||
return true;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::showID3Tag(const char* tag, const char* value) {
|
||
ps_ptr<char> id3tag;
|
||
id3tag.set_name("id3tag");
|
||
// V2.2
|
||
if (!strcmp(tag, "CNT")) id3tag.assignf("Play counter: {}", value);
|
||
if (!strcmp(tag, "COM")) id3tag.assignf("Comments: {}", value);
|
||
if (!strcmp(tag, "CRA")) id3tag.assignf("Audio encryption: {}", value);
|
||
if (!strcmp(tag, "CRM")) id3tag.assignf("Encrypted meta frame: {}", value);
|
||
if (!strcmp(tag, "ETC")) id3tag.assignf("Event timing codes: {}", value);
|
||
if (!strcmp(tag, "EQU")) id3tag.assignf("Equalization: {}", value);
|
||
if (!strcmp(tag, "IPL")) id3tag.assignf("Involved people list: {}", value);
|
||
if (!strcmp(tag, "PIC")) id3tag.assignf("Attached picture: {}", value);
|
||
if (!strcmp(tag, "SLT")) id3tag.assignf("Synchronized lyric/text: {}", value);
|
||
if (!strcmp(tag, "TAL")) id3tag.assignf("Album/Movie/Show title: {}", value);
|
||
if (!strcmp(tag, "TBP")) id3tag.assignf("BPM (Beats Per Minute): {}", value);
|
||
if (!strcmp(tag, "TCM")) id3tag.assignf("Composer: {}", value);
|
||
if (!strcmp(tag, "TCO")) id3tag.assignf("Content type: {}", value);
|
||
if (!strcmp(tag, "TCR")) id3tag.assignf("Copyright message: {}", value);
|
||
if (!strcmp(tag, "TDA")) id3tag.assignf("Date: {}", value);
|
||
if (!strcmp(tag, "TDY")) id3tag.assignf("Playlist delay: {}", value);
|
||
if (!strcmp(tag, "TEN")) id3tag.assignf("Encoded by: {}", value);
|
||
if (!strcmp(tag, "TFT")) id3tag.assignf("File type: {}", value);
|
||
if (!strcmp(tag, "TIM")) id3tag.assignf("Time: {}", value);
|
||
if (!strcmp(tag, "TKE")) id3tag.assignf("Initial key: {}", value);
|
||
if (!strcmp(tag, "TLA")) id3tag.assignf("Language(s): {}", value);
|
||
if (!strcmp(tag, "TLE")) id3tag.assignf("Length: {}", value);
|
||
if (!strcmp(tag, "TMT")) id3tag.assignf("Media type: {}", value);
|
||
if (!strcmp(tag, "TOA")) id3tag.assignf("Original artist(s)/performer(s): {}", value);
|
||
if (!strcmp(tag, "TOF")) id3tag.assignf("Original filename: {}", value);
|
||
if (!strcmp(tag, "TOL")) id3tag.assignf("Original Lyricist(s)/text writer(s): {}", value);
|
||
if (!strcmp(tag, "TOR")) id3tag.assignf("Original release year: {}", value);
|
||
if (!strcmp(tag, "TOT")) id3tag.assignf("Original album/Movie/Show title: {}", value);
|
||
if (!strcmp(tag, "TP1")) id3tag.assignf("Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group: {}", value);
|
||
if (!strcmp(tag, "TP2")) id3tag.assignf("Band/Orchestra/Accompaniment: {}", value);
|
||
if (!strcmp(tag, "TP3")) id3tag.assignf("Conductor/Performer refinement: {}", value);
|
||
if (!strcmp(tag, "TP4")) id3tag.assignf("Interpreted, remixed, or otherwise modified by: {}", value);
|
||
if (!strcmp(tag, "TPA")) id3tag.assignf("Part of a set: {}", value);
|
||
if (!strcmp(tag, "TPB")) id3tag.assignf("Publisher: {}", value);
|
||
if (!strcmp(tag, "TRC")) id3tag.assignf("ISRC (International Standard Recording Code): {}", value);
|
||
if (!strcmp(tag, "TRD")) id3tag.assignf("Recording dates: {}", value);
|
||
if (!strcmp(tag, "TRK")) id3tag.assignf("Track number/Position in set: {}", value);
|
||
if (!strcmp(tag, "TSI")) id3tag.assignf("Size: {}", value);
|
||
if (!strcmp(tag, "TSS")) id3tag.assignf("Software/hardware and settings used for encoding: {}", value);
|
||
if (!strcmp(tag, "TT1")) id3tag.assignf("Content group description: {}", value);
|
||
if (!strcmp(tag, "TT2")) id3tag.assignf("Title/Songname/Content description: {}", value);
|
||
if (!strcmp(tag, "TT3")) id3tag.assignf("Subtitle/Description refinement: {}", value);
|
||
if (!strcmp(tag, "TXT")) id3tag.assignf("Lyricist/text writer: {}", value);
|
||
if (!strcmp(tag, "TXX")) id3tag.assignf("User defined text information frame: {}", value);
|
||
if (!strcmp(tag, "TYE")) id3tag.assignf("Year: {}", value);
|
||
if (!strcmp(tag, "UFI")) id3tag.assignf("Unique file identifier: {}", value);
|
||
if (!strcmp(tag, "ULT")) id3tag.assignf("Unsychronized lyric/text transcription: {}", value);
|
||
if (!strcmp(tag, "WAF")) id3tag.assignf("Official audio file webpage: {}", value);
|
||
if (!strcmp(tag, "WAR")) id3tag.assignf("Official artist/performer webpage: {}", value);
|
||
if (!strcmp(tag, "WAS")) id3tag.assignf("Official audio source webpage: {}", value);
|
||
if (!strcmp(tag, "WCM")) id3tag.assignf("Commercial information: {}", value);
|
||
if (!strcmp(tag, "WCP")) id3tag.assignf("Copyright/Legal information: {}", value);
|
||
if (!strcmp(tag, "WPB")) id3tag.assignf("Publishers official webpage: {}", value);
|
||
if (!strcmp(tag, "WXX")) id3tag.assignf("User defined URL link frame: {}", value);
|
||
|
||
// V2.3 V2.4 tags
|
||
if (!strcmp(tag, "COMM")) id3tag.assignf("Comment: {}", value);
|
||
if (!strcmp(tag, "OWNE")) id3tag.assignf("Ownership: {}", value);
|
||
if (!strcmp(tag, "PRIV")) id3tag.assignf("Private: {}", value);
|
||
if (!strcmp(tag, "SYLT")) id3tag.assignf("SynLyrics: {}", value);
|
||
if (!strcmp(tag, "TALB")) id3tag.assignf("Album: {}", value);
|
||
if (!strcmp(tag, "TBPM")) id3tag.assignf("BeatsPerMinute: {}", value);
|
||
if (!strcmp(tag, "TCAT")) id3tag.assignf("PodcastCategory: {}", value);
|
||
if (!strcmp(tag, "TCMP")) id3tag.assignf("Compilation: {}", value);
|
||
if (!strcmp(tag, "TCOM")) id3tag.assignf("Composer: {}", value);
|
||
if (!strcmp(tag, "TCON")) id3tag.assignf("ContentType: {}", value);
|
||
if (!strcmp(tag, "TCOP")) id3tag.assignf("Copyright: {}", value);
|
||
if (!strcmp(tag, "TDAT")) id3tag.assignf("Date: {}", value);
|
||
if (!strcmp(tag, "TDES")) id3tag.assignf("PodcastDescription: {}", value);
|
||
if (!strcmp(tag, "TDOR")) id3tag.assignf("Original Release Year: {}", value);
|
||
if (!strcmp(tag, "TDRC")) id3tag.assignf("Publication date: {}", value);
|
||
if (!strcmp(tag, "TDRL")) id3tag.assignf("ReleaseTime: {}", value);
|
||
if (!strcmp(tag, "TENC")) id3tag.assignf("Encoded by: {}", value);
|
||
if (!strcmp(tag, "TEXT")) {} // id3tag.assignf("Lyricist: {}", value);
|
||
if (!strcmp(tag, "TGID")) id3tag.assignf("PodcastID: {}", value);
|
||
if (!strcmp(tag, "TIME")) id3tag.assignf("Time: {}", value);
|
||
if (!strcmp(tag, "TIT1")) id3tag.assignf("Grouping: {}", value);
|
||
if (!strcmp(tag, "TIT2")) id3tag.assignf("Title: {}", value);
|
||
if (!strcmp(tag, "TIT3")) id3tag.assignf("Subtitle: {}", value);
|
||
if (!strcmp(tag, "TLAN")) id3tag.assignf("Language: {}", value);
|
||
if (!strcmp(tag, "TLEN")) id3tag.assignf("Length (ms): {}", value);
|
||
if (!strcmp(tag, "TMED")) id3tag.assignf("Media: {}", value);
|
||
if (!strcmp(tag, "TOAL")) id3tag.assignf("OriginalAlbum: {}", value);
|
||
if (!strcmp(tag, "TOPE")) id3tag.assignf("OriginalArtist: {}", value);
|
||
if (!strcmp(tag, "TOLY")) id3tag.assignf("OriginalLyricist: {}", value);
|
||
if (!strcmp(tag, "TORY")) id3tag.assignf("OriginalReleaseYear: {}", value);
|
||
if (!strcmp(tag, "TPE1")) id3tag.assignf("Artist: {}", value);
|
||
if (!strcmp(tag, "TPE2")) id3tag.assignf("Band: {}", value);
|
||
if (!strcmp(tag, "TPE3")) id3tag.assignf("Conductor: {}", value);
|
||
if (!strcmp(tag, "TPE4")) id3tag.assignf("InterpretedBy: {}", value);
|
||
if (!strcmp(tag, "TPOS")) id3tag.assignf("PartOfSet: {}", value);
|
||
if (!strcmp(tag, "TPUB")) id3tag.assignf("Publisher: {}", value);
|
||
if (!strcmp(tag, "TRCK")) id3tag.assignf("Track: {}", value);
|
||
if (!strcmp(tag, "TRSN")) id3tag.assignf("InternetRadioStationName: {}", value);
|
||
if (!strcmp(tag, "TSSE")) id3tag.assignf("SettingsForEncoding: {}", value);
|
||
if (!strcmp(tag, "TRDA")) id3tag.assignf("RecordingDates: {}", value);
|
||
if (!strcmp(tag, "TSO2")) id3tag.assignf("AlbumArtistSortOrder: {}", value);
|
||
if (!strcmp(tag, "TSOA")) id3tag.assignf("AlbumSortOrder: {}", value);
|
||
if (!strcmp(tag, "TSOP")) id3tag.assignf("PerformerSortOrder: {}", value);
|
||
if (!strcmp(tag, "TSOC")) id3tag.assignf("ComposerSortOrder: {}", value);
|
||
if (!strcmp(tag, "TSOT")) id3tag.assignf("TitleSortOrder: {}", value);
|
||
if (!strcmp(tag, "TXXX")) id3tag.assignf("UserDefinedText: {}", value);
|
||
if (!strcmp(tag, "TYER")) id3tag.assignf("Year: {}", value);
|
||
if (!strcmp(tag, "USER")) id3tag.assignf("TermsOfUse: {}", value);
|
||
if (!strcmp(tag, "USLT")) id3tag.assignf("Lyrics: {}", value);
|
||
if (!strcmp(tag, "WCOM")) id3tag.assignf("Commercial URL: {}", value);
|
||
if (!strcmp(tag, "WFED")) id3tag.assignf("Podcast URL: {}", value);
|
||
if (!strcmp(tag, "WOAF")) id3tag.assignf("File URL: {}", value);
|
||
if (!strcmp(tag, "WOAR")) id3tag.assignf("OfficialArtistWebpage: {}", value);
|
||
if (!strcmp(tag, "WOAS")) id3tag.assignf("Source URL: {}", value);
|
||
if (!strcmp(tag, "WORS")) id3tag.assignf("InternetRadioStationURL: {}", value);
|
||
if (!strcmp(tag, "WXXX")) id3tag.assignf("User defined URL link frame: {}", value);
|
||
if (!strcmp(tag, "XDOR")) id3tag.assignf("OriginalReleaseTime: {}", value);
|
||
|
||
if (!id3tag.valid()) {
|
||
AUDIO_LOG_DEBUG("unknown tag: {}", tag);
|
||
return;
|
||
}
|
||
|
||
latinToUTF8(id3tag);
|
||
|
||
if (id3tag.contains("?xml")) {
|
||
showstreamtitle(id3tag.get());
|
||
return;
|
||
}
|
||
|
||
if (id3tag.starts_with("Grouping")) {
|
||
showstreamtitle(id3tag.get());
|
||
return;
|
||
}
|
||
if (id3tag.strlen()) { info(*this, evt_id3data, "{}", id3tag.get()); }
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::latinToUTF8(ps_ptr<char>& buff, bool UTF8check) {
|
||
// 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
|
||
|
||
uint16_t pos = 0;
|
||
uint16_t in = 0;
|
||
uint16_t out = 0;
|
||
bool isUTF8 = true;
|
||
|
||
// We cannot detect if a given string (or byte sequence) is a UTF-8 encoded text as for example each and every series
|
||
// of UTF-8 octets is also a valid (if nonsensical) series of Latin-1 (or some other encoding) octets.
|
||
// However not every series of valid Latin-1 octets are valid UTF-8 series. So you can rule out strings that do not conform
|
||
// to the UTF-8 encoding schema:
|
||
|
||
if (UTF8check) {
|
||
const size_t strLen = buff.strlen();
|
||
while (pos < strLen) {
|
||
if ((uint8_t)buff[pos] <= 0x7F) { // 0xxxxxxx: ASCII
|
||
pos++;
|
||
} else if ((buff[pos] & 0xE0) == 0xC0) {
|
||
// 110xxxxx 10xxxxxx: 2-byte
|
||
if (pos + 1 >= strLen || (uint8_t)buff[pos] < 0xC2 || ((uint8_t)buff[pos + 1] & 0xC0) != 0x80) {
|
||
isUTF8 = false;
|
||
break;
|
||
}
|
||
pos += 2;
|
||
} else if ((buff[pos] & 0xF0) == 0xE0) {
|
||
// 1110xxxx 10xxxxxx 10xxxxxx: 3-byte
|
||
if (pos + 2 >= strLen || ((uint8_t)buff[pos + 1] & 0xC0) != 0x80 || ((uint8_t)buff[pos + 2] & 0xC0) != 0x80) {
|
||
isUTF8 = false;
|
||
break;
|
||
}
|
||
if ((uint8_t)buff[pos] == 0xE0 && (uint8_t)buff[pos + 1] < 0xA0) {
|
||
isUTF8 = false;
|
||
break;
|
||
} // Overlong
|
||
if ((uint8_t)buff[pos] == 0xED && (uint8_t)buff[pos + 1] >= 0xA0) {
|
||
isUTF8 = false;
|
||
break;
|
||
} // UTF-16 surrogate
|
||
pos += 3;
|
||
} else if ((buff[pos] & 0xF8) == 0xF0) {
|
||
// 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx: 4-byte
|
||
if (pos + 3 >= strLen || ((uint8_t)buff[pos + 1] & 0xC0) != 0x80 || ((uint8_t)buff[pos + 2] & 0xC0) != 0x80 || ((uint8_t)buff[pos + 3] & 0xC0) != 0x80) {
|
||
isUTF8 = false;
|
||
break;
|
||
}
|
||
if ((uint8_t)buff[pos] == 0xF0 && (uint8_t)buff[pos + 1] < 0x90) {
|
||
isUTF8 = false;
|
||
break;
|
||
} // Overlong
|
||
if ((uint8_t)buff[pos] > 0xF4 || ((uint8_t)buff[pos] == 0xF4 && (uint8_t)buff[pos + 1] > 0x8F)) {
|
||
isUTF8 = false;
|
||
break;
|
||
} // > U+10FFFF
|
||
pos += 4;
|
||
} else {
|
||
isUTF8 = false;
|
||
break; // Invalid first byte (continuation byte or 0xF8-0xFF)
|
||
}
|
||
}
|
||
if (isUTF8) return; // is UTF-8, do nothing
|
||
}
|
||
ps_ptr<char> iso8859_1;
|
||
iso8859_1.assign(buff.get());
|
||
|
||
// Worst-case: all chars are latin1 > 0x7F became 2 Bytes → max length is twice +1
|
||
std::size_t requiredSize = strlen(iso8859_1.get()) * 2 + 1;
|
||
|
||
if (buff.size() < requiredSize) {
|
||
buff.realloc(requiredSize);
|
||
if (buff.size() < requiredSize) {
|
||
AUDIO_LOG_ERROR("latinToUTF8: realloc failed (need {} bytes, have {})", requiredSize, buff.size());
|
||
return; // keep original string, avoid buffer overflow
|
||
}
|
||
}
|
||
|
||
// coding into UTF-8
|
||
while (iso8859_1[in] != '\0') {
|
||
if (iso8859_1[in] < 0x80) {
|
||
buff[out++] = iso8859_1[in++];
|
||
} else {
|
||
buff[out++] = (0xC0 | (iso8859_1[in] >> 6));
|
||
buff[out++] = (0x80 | (iso8859_1[in] & 0x3F));
|
||
in++;
|
||
}
|
||
}
|
||
buff[out] = '\0';
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::htmlToUTF8(char* str) { // convert HTML to UTF-8
|
||
|
||
typedef struct { // --- EntityMap Definition ---
|
||
const char* name;
|
||
uint32_t codepoint;
|
||
} EntityMap;
|
||
|
||
const EntityMap entities[] = {
|
||
{"amp", 0x0026}, // &
|
||
{"lt", 0x003C}, // <
|
||
{"gt", 0x003E}, // >
|
||
{"quot", 0x0022}, // "
|
||
{"apos", 0x0027}, // '
|
||
{"nbsp", 0x00A0}, // non-breaking space
|
||
{"euro", 0x20AC}, // €
|
||
{"copy", 0x00A9}, // ©
|
||
{"reg", 0x00AE}, // ®
|
||
{"trade", 0x2122}, // ™
|
||
{"hellip", 0x2026}, // …
|
||
{"ndash", 0x2013}, // –
|
||
{"mdash", 0x2014}, // —
|
||
{"sect", 0x00A7}, // §
|
||
{"para", 0x00B6} // ¶
|
||
};
|
||
|
||
// --- EntityMap Lookup ---
|
||
auto find_entity = [&](const char* p, uint32_t* codepoint, int* entity_len) {
|
||
for (size_t i = 0; i < sizeof(entities) / sizeof(entities[0]); i++) {
|
||
const char* name = entities[i].name;
|
||
size_t len = strlen(name);
|
||
if (strncmp(p + 1, name, len) == 0 && p[len + 1] == ';') {
|
||
*codepoint = entities[i].codepoint;
|
||
*entity_len = (int)(len + 2); // &name;
|
||
return 1;
|
||
}
|
||
}
|
||
return 0;
|
||
};
|
||
|
||
auto codepoint_to_utf8 = [&](uint32_t cp, char* dst) { // Convert a Codepoint (Unicode) to UTF-8, writes in DST, there is number of bytes back
|
||
if (cp <= 0x7F) {
|
||
dst[0] = cp;
|
||
return 1;
|
||
} else if (cp <= 0x7FF) {
|
||
dst[0] = 0xC0 | (cp >> 6);
|
||
dst[1] = 0x80 | (cp & 0x3F);
|
||
return 2;
|
||
} else if (cp <= 0xFFFF) {
|
||
dst[0] = 0xE0 | (cp >> 12);
|
||
dst[1] = 0x80 | ((cp >> 6) & 0x3F);
|
||
dst[2] = 0x80 | (cp & 0x3F);
|
||
return 3;
|
||
} else if (cp <= 0x10FFFF) {
|
||
dst[0] = 0xF0 | (cp >> 18);
|
||
dst[1] = 0x80 | ((cp >> 12) & 0x3F);
|
||
dst[2] = 0x80 | ((cp >> 6) & 0x3F);
|
||
dst[3] = 0x80 | (cp & 0x3F);
|
||
return 4;
|
||
}
|
||
return -1; // invalid Codepoint
|
||
};
|
||
|
||
char* p = str;
|
||
while (*p != '\0') {
|
||
if (p[0] == '&') {
|
||
uint32_t cp;
|
||
int consumed;
|
||
if (find_entity(p, &cp, &consumed)) { // looking for entity, such as ©
|
||
char utf8[5] = {0};
|
||
int len = codepoint_to_utf8(cp, utf8);
|
||
if (len > 0) {
|
||
size_t tail_len = strlen(p + consumed);
|
||
memmove(p + len, p + consumed, tail_len + 1);
|
||
memcpy(p, utf8, len);
|
||
p += len;
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
if (p[0] == '&' && p[1] == '#') {
|
||
char* endptr;
|
||
uint32_t codepoint = strtol(p + 2, &endptr, 10);
|
||
|
||
if (*endptr == ';' && codepoint <= 0x10FFFF) {
|
||
char utf8[5] = {0};
|
||
int utf8_len = codepoint_to_utf8(codepoint, utf8);
|
||
if (utf8_len > 0) {
|
||
// size_t entity_len = endptr - p + 1;
|
||
size_t tail_len = strlen(endptr + 1);
|
||
|
||
// Show residual ring to the left
|
||
memmove(p + utf8_len, endptr + 1, tail_len + 1); // +1 because of '\0'
|
||
|
||
// Copy UTF-8 characters
|
||
memcpy(p, utf8, utf8_len);
|
||
|
||
// weiter bei neuem Zeichen
|
||
p += utf8_len;
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
p++;
|
||
}
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
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;
|
||
}
|
||
}
|
||
if (m_codec == CODEC_MP3) {
|
||
int res = read_ID3_Header(InBuff.getReadPtr(), bytes);
|
||
if (res > 0 || m_controlCounter < 100)
|
||
bytesReaded = res;
|
||
else { // error, skip header
|
||
m_controlCounter = 100;
|
||
}
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
if (m_codec == CODEC_AAC) {
|
||
// stream only, no header
|
||
m_audioDataSize = m_audioFileSize;
|
||
m_controlCounter = 100;
|
||
}
|
||
if (m_codec == CODEC_FLAC) {
|
||
int res = read_FLAC_Header(InBuff.getReadPtr(), bytes);
|
||
if (res >= 0)
|
||
bytesReaded = res;
|
||
else { // error
|
||
stopSong();
|
||
}
|
||
}
|
||
if (m_codec == CODEC_OPUS) { m_controlCounter = 100; }
|
||
if (m_codec == CODEC_VORBIS) { m_controlCounter = 100; }
|
||
if (m_codec == CODEC_OGG) { m_controlCounter = 100; }
|
||
if (!isRunning()) {
|
||
AUDIO_LOG_ERROR("Processing stopped due to invalid audio header");
|
||
return 0;
|
||
}
|
||
return bytesReaded;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
int Audio::read_WAV_Header(uint8_t* data, size_t len) {
|
||
|
||
if (m_controlCounter == 0) {
|
||
m_rwh.cs = 0;
|
||
m_rwh.bts = 0;
|
||
m_controlCounter++;
|
||
if ((*data != 'R') || (*(data + 1) != 'I') || (*(data + 2) != 'F') || (*(data + 3) != 'F')) {
|
||
AUDIO_LOG_ERROR("file has no RIFF tag");
|
||
m_rwh.headerSize = 0;
|
||
return -1; // false;
|
||
} else {
|
||
m_rwh.headerSize = 4;
|
||
return 4; // ok
|
||
}
|
||
}
|
||
|
||
if (m_controlCounter == 1) {
|
||
m_controlCounter++;
|
||
m_rwh.cs = (uint32_t)(*data + (*(data + 1) << 8) + (*(data + 2) << 16) + (*(data + 3) << 24) - 8);
|
||
m_rwh.headerSize += 4;
|
||
return 4; // ok
|
||
}
|
||
|
||
if (m_controlCounter == 2) {
|
||
m_controlCounter++;
|
||
if ((*data != 'W') || (*(data + 1) != 'A') || (*(data + 2) != 'V') || (*(data + 3) != 'E')) {
|
||
AUDIO_LOG_ERROR("format tag is not WAVE");
|
||
return -1; // false;
|
||
} else {
|
||
m_rwh.headerSize += 4;
|
||
return 4;
|
||
}
|
||
}
|
||
|
||
if (m_controlCounter == 3) {
|
||
if ((*data == 'f') && (*(data + 1) == 'm') && (*(data + 2) == 't')) {
|
||
m_controlCounter++;
|
||
m_rwh.headerSize += 4;
|
||
return 4;
|
||
} else {
|
||
m_rwh.headerSize += 4;
|
||
return 4;
|
||
}
|
||
}
|
||
|
||
if (m_controlCounter == 4) {
|
||
m_controlCounter++;
|
||
m_rwh.cs = (uint32_t)(*data + (*(data + 1) << 8));
|
||
if (m_rwh.cs > 40) return -1; // false, something going wrong
|
||
m_rwh.bts = m_rwh.cs - 16; // bytes to skip if fmt chunk is >16
|
||
m_rwh.headerSize += 4;
|
||
return 4;
|
||
}
|
||
|
||
if (m_controlCounter == 5) {
|
||
m_controlCounter++;
|
||
uint16_t fc = (uint16_t)(*(data + 0) + (*(data + 1) << 8)); // Format code
|
||
uint16_t ch = (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
|
||
|
||
info(*this, evt_info, "FormatCode: {}", fc);
|
||
// info(*this, evt_info, "Channel: {}", nic);
|
||
// info(*this, evt_info, "SampleRate (Hz): {}", sr);
|
||
info(*this, evt_info, "DataRate: {}", dr);
|
||
info(*this, evt_info, "DataBlockSize: {}", dbs);
|
||
info(*this, evt_info, "BitsPerSample: {}", bps);
|
||
|
||
if ((bps != 8) && (bps != 16) && (bps != 24) && (bps != 32)) {
|
||
info(*this, evt_info, "BitsPerSample is {}, must be 8, 16, 24 or 32", bps);
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
if ((ch != 1) && (ch != 2)) {
|
||
info(*this, evt_info, "num channels is {}, must be 1 or 2", ch);
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
if (fc != 1) {
|
||
AUDIO_LOG_ERROR("format code is not 1 (PCM)");
|
||
stopSong();
|
||
return -1; // false;
|
||
}
|
||
m_decoder->setRawBlockParams(ch, sr, bps, 0, 0);
|
||
m_nominal_bitrate = ch * sr * bps;
|
||
m_rwh.headerSize += 16;
|
||
return 16; // ok
|
||
}
|
||
|
||
if (m_controlCounter == 6) {
|
||
m_controlCounter++;
|
||
m_rwh.headerSize += m_rwh.bts;
|
||
return m_rwh.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);
|
||
m_rwh.headerSize += 4;
|
||
return 4;
|
||
} else {
|
||
m_rwh.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
|
||
m_rwh.headerSize += 4;
|
||
if (cs) {
|
||
m_audioDataSize = cs - 44;
|
||
} else { // sometimes there is nothing here
|
||
m_audioDataSize = m_audioFileSize - m_rwh.headerSize;
|
||
}
|
||
|
||
m_audioFileDuration = m_audioDataSize / (getSampleRate() * getChannels());
|
||
if (getBitsPerSample() == 16) m_audioFileDuration /= 2;
|
||
info(*this, evt_info, "Duration (s): {}", m_audioFileDuration);
|
||
return 4;
|
||
}
|
||
m_controlCounter = 100; // header succesfully read
|
||
m_audioDataStart = m_rwh.headerSize;
|
||
info(*this, evt_info, "Audio-Data-Start: {}", m_audioDataStart);
|
||
info(*this, evt_info, "Audio-Length: {}", m_audioDataSize);
|
||
return 0;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
int Audio::read_FLAC_Header(uint8_t* data, size_t len) {
|
||
|
||
if (m_rflh.retvalue && m_controlCounter != 0) {
|
||
if (m_rflh.retvalue > len) { // if returnvalue > bufferfillsize
|
||
if (len > InBuff.getMaxBlockSize()) len = InBuff.getMaxBlockSize();
|
||
m_rflh.retvalue -= len; // and wait for more bufferdata
|
||
return len;
|
||
} else {
|
||
size_t tmp = m_rflh.retvalue;
|
||
m_rflh.retvalue = 0;
|
||
return tmp;
|
||
}
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == FLAC_BEGIN) { // init
|
||
m_rflh.reset();
|
||
m_controlCounter = FLAC_MAGIC;
|
||
m_rflh.picVec.clear();
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == FLAC_MAGIC) { /* check MAGIC STRING */
|
||
if (specialIndexOf(data, "OggS", 10) == 0) { // is ogg
|
||
m_rflh.headerSize = 0;
|
||
m_rflh.retvalue = 0;
|
||
m_controlCounter = FLAC_OKAY;
|
||
return 0;
|
||
}
|
||
if (specialIndexOf(data, "fLaC", 10) != 0) {
|
||
if (specialIndexOf(data, "ID3", 10) == 0) { // has ID3 before fLaC
|
||
// Synchsafe-Size
|
||
uint32_t size = (data[6] << 21) | (data[7] << 14) | (data[8] << 7) | data[9];
|
||
size += 10; // header + body
|
||
m_rflh.retvalue = size; // skip ID3 header + body
|
||
return 0;
|
||
}
|
||
AUDIO_LOG_ERROR("Magic String 'fLaC' not found in header");
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
|
||
m_controlCounter = FLAC_MBH; // METADATA_BLOCK_HEADER
|
||
m_rflh.headerSize = 4;
|
||
m_rflh.retvalue = 4;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == FLAC_MBH) { /* METADATA_BLOCK_HEADER */
|
||
uint8_t blockType = *data;
|
||
|
||
if (!m_rflh.f_lastMetaBlock) {
|
||
if ((blockType & 127) == 0) m_controlCounter = FLAC_SINFO;
|
||
if ((blockType & 127) == 1) m_controlCounter = FLAC_PADDING;
|
||
if ((blockType & 127) == 2) m_controlCounter = FLAC_APP;
|
||
if ((blockType & 127) == 3) m_controlCounter = FLAC_SEEK;
|
||
if ((blockType & 127) == 4) m_controlCounter = FLAC_VORBIS;
|
||
if ((blockType & 127) == 5) m_controlCounter = FLAC_CUESHEET;
|
||
if ((blockType & 127) == 6) m_controlCounter = FLAC_PICTURE;
|
||
if ((blockType & 128)) { m_rflh.f_lastMetaBlock = true; }
|
||
m_rflh.headerSize += 1;
|
||
m_rflh.retvalue = 1;
|
||
return 0;
|
||
}
|
||
|
||
m_controlCounter = FLAC_OKAY;
|
||
|
||
m_audioDataStart = m_rflh.headerSize;
|
||
m_audioDataSize = m_audioFileSize - m_audioDataStart;
|
||
m_decoder->setRawBlockParams(m_rflh.numChannels, m_rflh.sampleRate, m_rflh.bitsPerSample, m_rflh.totalSamplesInStream, (uint32_t)m_audioDataSize);
|
||
if (m_rflh.picLen) {
|
||
size_t pos = m_audioDataStart;
|
||
info(*this, evt_image, m_rflh.picVec);
|
||
}
|
||
|
||
info(*this, evt_info, "Audio-Data-Start: {}", m_audioDataStart);
|
||
info(*this, evt_info, "Audio-Length: {}", m_audioDataSize);
|
||
if (m_rflh.duration) {
|
||
m_rflh.nominalBitrate = (m_audioDataSize * 8) / m_rflh.duration;
|
||
m_nominal_bitrate = m_rflh.nominalBitrate;
|
||
m_audioFileDuration = m_rflh.duration;
|
||
info(*this, evt_info, "Duration (s): {}", m_rflh.duration);
|
||
}
|
||
m_rflh.retvalue = 0;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == FLAC_SINFO) { /* Stream info block */
|
||
size_t l = bigEndian(data, 3);
|
||
vTaskDelay(2);
|
||
m_rflh.maxBlockSize = bigEndian(data + 5, 2);
|
||
info(*this, evt_info, "FLAC maxBlockSize: {}", m_rflh.maxBlockSize);
|
||
vTaskDelay(2);
|
||
m_rflh.maxFrameSize = bigEndian(data + 10, 3);
|
||
if (m_rflh.maxFrameSize) {
|
||
info(*this, evt_info, "FLAC maxFrameSize: {}", m_rflh.maxFrameSize);
|
||
} else {
|
||
info(*this, evt_info, "FLAC maxFrameSize: N/A");
|
||
}
|
||
if (m_rflh.maxFrameSize > InBuff.getMaxBlockSize()) {
|
||
AUDIO_LOG_ERROR("FLAC maxFrameSize too large!");
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
vTaskDelay(2);
|
||
uint32_t nextval = bigEndian(data + 13, 3);
|
||
m_rflh.sampleRate = nextval >> 4;
|
||
info(*this, evt_info, "FLAC sampleRate (Hz): {}", m_rflh.sampleRate);
|
||
vTaskDelay(2);
|
||
m_rflh.numChannels = ((nextval & 0x06) >> 1) + 1;
|
||
info(*this, evt_info, "FLAC numChannels: {}", m_rflh.numChannels);
|
||
vTaskDelay(2);
|
||
uint8_t bps = (nextval & 0x01) << 4;
|
||
bps += (*(data + 16) >> 4) + 1;
|
||
m_rflh.bitsPerSample = bps;
|
||
if ((bps != 8) && (bps != 16) && (bps != 24)) {
|
||
AUDIO_LOG_ERROR("bits per sample must be 8, 16 or 24, is {}", bps);
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
info(*this, evt_info, "FLAC bitsPerSample: {}", m_rflh.bitsPerSample);
|
||
m_rflh.totalSamplesInStream = bigEndian(data + 17, 4);
|
||
if (m_rflh.totalSamplesInStream) {
|
||
info(*this, evt_info, "total samples in stream: {}", m_rflh.totalSamplesInStream);
|
||
} else {
|
||
info(*this, evt_info, "total samples in stream: N/A");
|
||
}
|
||
if (bps != 0 && m_rflh.totalSamplesInStream && m_rflh.sampleRate) { m_rflh.duration = m_rflh.totalSamplesInStream / m_rflh.sampleRate; }
|
||
m_controlCounter = FLAC_MBH; // METADATA_BLOCK_HEADER
|
||
m_rflh.retvalue = l + 3;
|
||
m_rflh.headerSize += m_rflh.retvalue;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == FLAC_PADDING) { /* PADDING */
|
||
size_t l = bigEndian(data, 3);
|
||
m_controlCounter = FLAC_MBH;
|
||
m_rflh.retvalue = l + 3;
|
||
m_rflh.headerSize += m_rflh.retvalue;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == FLAC_APP) { /* APPLICATION */
|
||
size_t l = bigEndian(data, 3);
|
||
m_controlCounter = FLAC_MBH;
|
||
m_rflh.retvalue = l + 3;
|
||
m_rflh.headerSize += m_rflh.retvalue;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == FLAC_SEEK) { /* SEEKTABLE */
|
||
size_t l = bigEndian(data, 3);
|
||
m_controlCounter = FLAC_MBH;
|
||
m_rflh.retvalue = l + 3;
|
||
m_rflh.headerSize += m_rflh.retvalue;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == FLAC_VORBIS) { /* VORBIS COMMENT */ // field names
|
||
|
||
auto parse_flac_timestamp = [&](const char* s) {
|
||
if (!s || *s != '[') return -1;
|
||
int mm = 0, ss = 0, hh = 0;
|
||
if (sscanf(s, "[%d:%d.%d]", &mm, &ss, &hh) == 3) { return mm * 60000 + ss * 1000 + hh * 10; }
|
||
return -1;
|
||
};
|
||
|
||
ps_ptr<char> vendorString;
|
||
ps_ptr<char> commentString;
|
||
ps_ptr<char> lyricsBuffer;
|
||
size_t vendorLength = bigEndian(data, 3);
|
||
size_t idx = 0;
|
||
data += 3;
|
||
idx += 3;
|
||
size_t vendorStringLength = data[0] + (data[1] << 8) + (data[2] << 16) + (data[3] << 24);
|
||
if (vendorStringLength) {
|
||
data += 4;
|
||
idx += 4;
|
||
}
|
||
if (vendorStringLength > 495) vendorStringLength = 495; // guard
|
||
vendorString.assign((const char*)data, vendorStringLength);
|
||
vendorString.insert("VENDOR_STRING: ", 0);
|
||
info(*this, evt_id3data, "{}", vendorString.get());
|
||
data += vendorStringLength;
|
||
idx += vendorStringLength;
|
||
size_t commentListLength = data[0] + (data[1] << 8) + (data[2] << 16) + (data[3] << 24);
|
||
data += 4;
|
||
idx += 4;
|
||
|
||
for (int i = 0; i < commentListLength; i++) {
|
||
(void)i;
|
||
size_t commentLength = data[0] + (data[1] << 8) + (data[2] << 16) + (data[3] << 24);
|
||
data += 4;
|
||
idx += 4;
|
||
if (commentLength) { // guard
|
||
commentString.assign((const char*)data, commentLength);
|
||
if (commentString.starts_with("LYRICS="))
|
||
lyricsBuffer.clone_from(commentString);
|
||
else
|
||
info(*this, evt_id3data, "{}", commentString.get());
|
||
}
|
||
data += commentLength;
|
||
idx += commentLength;
|
||
if (idx > vendorLength + 3) { AUDIO_LOG_ERROR("VORBIS COMMENT section is too long"); }
|
||
}
|
||
ps_ptr<char> tmp;
|
||
ps_ptr<char> timestamp;
|
||
timestamp.alloc(12, "timestamp");
|
||
if (lyricsBuffer.valid()) {
|
||
lyricsBuffer.remove_prefix("LYRICS=");
|
||
idx = 0;
|
||
while (idx < lyricsBuffer.size()) {
|
||
int pos = lyricsBuffer.index_of('\n', idx);
|
||
if (pos == -1) break;
|
||
int len = pos - idx + 1;
|
||
tmp.copy_from(lyricsBuffer.get() + idx, len);
|
||
idx += len;
|
||
if (tmp.ends_with("\n")) tmp.truncate_at('\n');
|
||
// tmp content e.g.: [00:27.07]Jetzt kommt die Zeit
|
||
// [00:28.84]Auf die ihr euch freut
|
||
timestamp.copy_from(tmp.get(), 10);
|
||
|
||
int ms = parse_flac_timestamp(timestamp.c_get());
|
||
|
||
// timestamp.remove_chars("[]:.");
|
||
// timestamp.append("0"); // to ms 0028840
|
||
tmp.remove_before(10, true);
|
||
if (tmp.strlen() > 0) {
|
||
m_syltLines.push_back(std::move(tmp));
|
||
m_syltTimeStamp.push_back(ms);
|
||
}
|
||
}
|
||
info(*this, evt_info, "audiofile contains synchronized lyrics");
|
||
// for(int i = 0; i < m_syltLines.size(); i++){
|
||
// info(*this, evt_lyrics, "{:07} ms, {}", m_syltTimeStamp[i], m_syltLines[i].c_get());
|
||
// }
|
||
}
|
||
m_controlCounter = FLAC_MBH;
|
||
m_rflh.retvalue = vendorLength + 3;
|
||
m_rflh.headerSize += m_rflh.retvalue;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == FLAC_CUESHEET) { /* CUESHEET */
|
||
size_t l = bigEndian(data, 3);
|
||
m_controlCounter = FLAC_MBH;
|
||
m_rflh.retvalue = l + 3;
|
||
m_rflh.headerSize += m_rflh.retvalue;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == FLAC_PICTURE) { /* PICTURE */
|
||
m_rflh.picLen = bigEndian(data, 3);
|
||
m_rflh.picPos = m_rflh.headerSize + 3;
|
||
m_rflh.picVec.push_back(m_rflh.picPos);
|
||
m_rflh.picVec.push_back(m_rflh.picLen);
|
||
AUDIO_LOG_DEBUG("FLAC PICTURE, pos {}, size {}", m_rflh.picPos, m_rflh.picLen);
|
||
m_controlCounter = FLAC_MBH;
|
||
m_rflh.retvalue = m_rflh.picLen + 3;
|
||
m_rflh.headerSize += m_rflh.retvalue;
|
||
return 0;
|
||
}
|
||
return 0;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
|
||
int Audio::read_ID3_Header(uint8_t* data, size_t len) {
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == MP3_BEGIN) { /* read ID3 tag and ID3 header size */
|
||
m_controlCounter = MP3_ID3HEADER;
|
||
m_ID3Hdr.reset(); // reset all
|
||
m_ID3Hdr.iBuffSize = 4096;
|
||
m_ID3Hdr.iBuff.alloc(m_ID3Hdr.iBuffSize + 10, "m_ID3Hdr.iBuff");
|
||
memset(m_ID3Hdr.tag, 0, sizeof(m_ID3Hdr.tag));
|
||
return 0;
|
||
}
|
||
if (m_controlCounter == MP3_NEXTID3) {
|
||
m_controlCounter = MP3_ID3HEADER;
|
||
// reset specific
|
||
m_ID3Hdr.id3Size = 0;
|
||
// m_ID3Hdr.totalId3Size = 0; // if we have more header, id3_1_size + id3_2_size + ....
|
||
m_ID3Hdr.remainingHeaderBytes = 0;
|
||
m_ID3Hdr.v22_tag_length = 0;
|
||
m_ID3Hdr.ID3version = 0;
|
||
m_ID3Hdr.ehsz = 0;
|
||
m_ID3Hdr.framesize = 0;
|
||
m_ID3Hdr.compressed = false;
|
||
m_ID3Hdr.SYLT.size = 0;
|
||
m_ID3Hdr.SYLT.pos = 0;
|
||
m_ID3Hdr.iBuffSize = 4096;
|
||
m_ID3Hdr.iBuff.alloc(m_ID3Hdr.iBuffSize + 10, "m_ID3Hdr.iBuff");
|
||
memset(m_ID3Hdr.tag, 0, sizeof(m_ID3Hdr.tag));
|
||
return 0;
|
||
}
|
||
|
||
if (m_controlCounter == MP3_ID3HEADER) {
|
||
m_controlCounter = MP3_EXTHEADER;
|
||
int retval = 0;
|
||
if (!m_f_m3u8data) info(*this, evt_info, "File-Size: {}", m_audioFileSize);
|
||
|
||
m_ID3Hdr.remainingHeaderBytes = 0;
|
||
m_ID3Hdr.ehsz = 0;
|
||
if (specialIndexOf(data, "ID3", 4) != 0) { // ID3 not found
|
||
if (!m_f_m3u8data) { info(*this, evt_info, "file has no ID3 tag, skip metadata"); }
|
||
m_audioDataSize = m_audioFileSize;
|
||
// if(!m_f_m3u8data) info(*this, evt_info, "Audio-Length: {}", m_audioDataSize);
|
||
m_controlCounter = MP3_XING; // have xing?
|
||
return 0; // error, no ID3 signature found
|
||
}
|
||
m_ID3Hdr.ID3version = *(data + 3);
|
||
switch (m_ID3Hdr.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;
|
||
};
|
||
m_ID3Hdr.id3Size = bigEndian(data + 6, 4, 7); // ID3v2 size 4 * %0xxxxxxx (shift left seven times!!)
|
||
m_ID3Hdr.id3Size += 10;
|
||
|
||
retval = 10;
|
||
|
||
// Every read from now may be unsync'd
|
||
if (!m_f_m3u8data) info(*this, evt_info, "ID3 framesSize: {}", m_ID3Hdr.id3Size);
|
||
if (!m_f_m3u8data) info(*this, evt_info, "ID3 version: 2.{}", m_ID3Hdr.ID3version);
|
||
|
||
if (m_ID3Hdr.ID3version == 2) { m_controlCounter = MP3_ID3V22; }
|
||
m_ID3Hdr.remainingHeaderBytes = m_ID3Hdr.id3Size;
|
||
m_ID3Size = m_ID3Hdr.id3Size;
|
||
m_ID3Hdr.remainingHeaderBytes -= 10;
|
||
|
||
if (m_f_exthdr) {
|
||
m_ID3Hdr.ehsz = bigEndian(data + 10, 4, 7); // syncSave
|
||
info(*this, evt_info, "ID3 extended header, size: {}", m_ID3Hdr.ehsz);
|
||
m_ID3Hdr.ehsz -= 4;
|
||
m_ID3Hdr.remainingHeaderBytes -= 4;
|
||
m_ID3Hdr.id3Size += 4;
|
||
retval += 4;
|
||
}
|
||
return retval;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == MP3_EXTHEADER) { // skip extended header if exists
|
||
if (m_ID3Hdr.ehsz > len) {
|
||
m_ID3Hdr.ehsz -= len;
|
||
m_ID3Hdr.remainingHeaderBytes -= len;
|
||
return len;
|
||
} // Throw it away
|
||
else {
|
||
m_controlCounter = MP3_ID3FRAME;
|
||
m_ID3Hdr.remainingHeaderBytes -= m_ID3Hdr.ehsz;
|
||
return m_ID3Hdr.ehsz;
|
||
} // Throw it away
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == MP3_ID3FRAME) { // read a ID3 frame, get the tag
|
||
if (m_ID3Hdr.remainingHeaderBytes == 0) {
|
||
m_controlCounter = MP3_XING;
|
||
return 0;
|
||
}
|
||
m_controlCounter = MP3_FRAMESIZE;
|
||
m_ID3Hdr.frameid[0] = *(data + 0);
|
||
m_ID3Hdr.frameid[1] = *(data + 1);
|
||
m_ID3Hdr.frameid[2] = *(data + 2);
|
||
m_ID3Hdr.frameid[3] = *(data + 3);
|
||
m_ID3Hdr.frameid[4] = 0;
|
||
for (uint8_t i = 0; i < 4; i++) m_ID3Hdr.tag[i] = m_ID3Hdr.frameid[i]; // tag = frameid
|
||
if (m_ID3Hdr.frameid[0] == 0 && m_ID3Hdr.frameid[1] == 0 && m_ID3Hdr.frameid[2] == 0 && m_ID3Hdr.frameid[3] == 0) {
|
||
// We're in padding
|
||
m_controlCounter = MP3_LASTFRAMES; // all ID3 metadata processed
|
||
}
|
||
m_ID3Hdr.remainingHeaderBytes -= 4;
|
||
return 4;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == MP3_FRAMESIZE) { // get the frame size
|
||
m_controlCounter = MP3_TAG;
|
||
|
||
if (m_ID3Hdr.ID3version == 4) {
|
||
m_ID3Hdr.framesize = bigEndian(data, 4, 7); // << 7
|
||
} else {
|
||
m_ID3Hdr.framesize = bigEndian(data, 4); // << 8
|
||
}
|
||
|
||
uint8_t frameFlag_0 = *(data + 4);
|
||
uint8_t frameFlag_1 = *(data + 5);
|
||
(void)frameFlag_0;
|
||
m_ID3Hdr.compressed = frameFlag_1 & 0x80; // Frame is compressed using [#ZLIB zlib] with 4 bytes for 'decompressed
|
||
uint32_t decompsize = 0;
|
||
if (m_ID3Hdr.compressed) {
|
||
// AUDIO_LOG_INFO("iscompressed");
|
||
decompsize = bigEndian(data + 6, 4);
|
||
(void)decompsize;
|
||
// AUDIO_LOG_INFO("decompsize={}", decompsize);
|
||
m_ID3Hdr.remainingHeaderBytes -= 6 + 4;
|
||
return 6 + 4;
|
||
}
|
||
m_ID3Hdr.remainingHeaderBytes -= 6;
|
||
return 6;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == MP3_SKIP) { // If the frame is larger than m_ID3Hdr.framesize, skip the rest
|
||
if (m_ID3Hdr.framesize > len) {
|
||
m_ID3Hdr.framesize -= len;
|
||
m_ID3Hdr.remainingHeaderBytes -= len;
|
||
return len;
|
||
} else {
|
||
m_controlCounter = MP3_ID3FRAME; // check next frame
|
||
m_ID3Hdr.remainingHeaderBytes -= m_ID3Hdr.framesize;
|
||
return m_ID3Hdr.framesize;
|
||
}
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == MP3_TAG) { // Read the value
|
||
m_controlCounter = MP3_SKIP; // only read 256 bytes
|
||
|
||
uint8_t textEncodingByte = *(data + 0); // ID3v2 Text-Encoding-Byte
|
||
// $00 – ISO-8859-1 (LATIN-1, Identical to ASCII for values smaller than 0x80).
|
||
// $01 – UCS-2 encoded Unicode with BOM (Byte Order Mark), in ID3v2.2 and ID3v2.3.
|
||
// $02 – UTF-16BE encoded Unicode without BOM (Byte Order Mark) , in ID3v2.4.
|
||
// $03 – UTF-8 encoded Unicode, in ID3v2.4.
|
||
|
||
if (startsWith(m_ID3Hdr.tag, "APIC")) { // a image embedded in file, passing it to external function
|
||
m_ID3Hdr.APIC_vec.push_back(m_ID3Hdr.totalId3Size + m_ID3Hdr.id3Size - m_ID3Hdr.remainingHeaderBytes);
|
||
m_ID3Hdr.APIC_vec.push_back(m_ID3Hdr.framesize);
|
||
AUDIO_LOG_DEBUG("APIC_pos {}, APIC_size {}", m_ID3Hdr.APIC_vec[2 * m_ID3Hdr.numID3Header], m_ID3Hdr.APIC_vec[2 * m_ID3Hdr.numID3Header + 1]);
|
||
return 0;
|
||
}
|
||
|
||
if (startsWith(m_ID3Hdr.tag, "SYLT") || startsWith(m_ID3Hdr.tag, "USLT")) { // any lyrics embedded in file, passing it to external function
|
||
m_controlCounter = MP3_SYLT;
|
||
return 0;
|
||
}
|
||
|
||
if ( // proprietary not standard information
|
||
startsWith(m_ID3Hdr.tag, "PRIV")) {
|
||
; // AUDIO_LOG_ERROR("PRIV");
|
||
return 0;
|
||
}
|
||
if (startsWith(m_ID3Hdr.tag, "TSIZ") || startsWith(m_ID3Hdr.tag, "XTRA")) { // tag with invalid length, skip everything and find the first sync word
|
||
m_ID3Hdr.framesize = 0;
|
||
size_t tmp = m_ID3Hdr.remainingHeaderBytes;
|
||
m_ID3Hdr.remainingHeaderBytes = 0;
|
||
return tmp;
|
||
}
|
||
|
||
if (m_ID3Hdr.framesize == 0) return 0;
|
||
|
||
ps_ptr<char> tmp;
|
||
tmp.set_name("tmp");
|
||
ps_ptr<char> content_descriptor;
|
||
content_descriptor.set_name("content_descriptor");
|
||
size_t fs = m_ID3Hdr.framesize; // fs = size of the frame data field as read from header
|
||
size_t bytesToCopy = fs;
|
||
size_t textDataLength = 0;
|
||
uint16_t idx = 0;
|
||
|
||
if (bytesToCopy >= m_ID3Hdr.iBuffSize) { bytesToCopy = m_ID3Hdr.iBuffSize - 1; } // make sure a zero terminator fits
|
||
if (bytesToCopy > 0) { textDataLength = bytesToCopy - 1; } // Only if there are data that we can shorten
|
||
for (int i = 0; i < textDataLength; i++) {
|
||
m_ID3Hdr.iBuff[i] = *(data + i + 1); // Skipped the first byte (Encoding)
|
||
}
|
||
|
||
if (textEncodingByte == 1 || textEncodingByte == 2) { // is UTF-16LE or UTF-16BE
|
||
m_ID3Hdr.iBuff[textDataLength] = 0; // UTF-16: set double zero terminator
|
||
m_ID3Hdr.iBuff[textDataLength + 1] = 0; // second '\0' for UTF-16
|
||
} else {
|
||
m_ID3Hdr.iBuff[textDataLength] = 0; // only one '\0' for ISO-8859-1 or UTF-8
|
||
}
|
||
|
||
m_ID3Hdr.framesize -= fs;
|
||
|
||
uint16_t dataLength = fs - 1;
|
||
bool isBigEndian = (textEncodingByte == 2);
|
||
|
||
char encodingTab[4][12] = {"ISO-8859-1", "UTF-16", "UTF-16BE", "UTF-8"};
|
||
|
||
if (startsWith(m_ID3Hdr.tag, "COMM")) { // language code
|
||
m_ID3Hdr.lang[0] = m_ID3Hdr.iBuff[0];
|
||
m_ID3Hdr.lang[1] = m_ID3Hdr.iBuff[1];
|
||
m_ID3Hdr.lang[2] = m_ID3Hdr.iBuff[2];
|
||
m_ID3Hdr.lang[3] = '\0';
|
||
|
||
idx = 4;
|
||
if (textEncodingByte == 1 || textEncodingByte == 2) idx++;
|
||
|
||
uint16_t cd_len = 0;
|
||
if (textEncodingByte == 0)
|
||
cd_len = 1 + content_descriptor.copy_from_iso8859_1((const uint8_t*)(m_ID3Hdr.iBuff.get() + idx)); // iso8859_1
|
||
else if (textEncodingByte == 3)
|
||
cd_len = 1 + content_descriptor.copy_from((const char*)(m_ID3Hdr.iBuff.get() + idx)); // utf-8
|
||
else
|
||
cd_len = 2 + content_descriptor.copy_from_utf16((const uint8_t*)(m_ID3Hdr.iBuff.get() + idx), isBigEndian); // utf-16
|
||
if (cd_len > 2) info(*this, evt_info, "Comment: text encoding; {}, language {}, content_descriptor: {}", encodingTab[textEncodingByte], m_ID3Hdr.lang, content_descriptor.c_get());
|
||
|
||
idx += cd_len;
|
||
// AUDIO_LOG_INFO("Tag: {}, Length: {}, Format: {}", m_ID3Hdr.tag, textDataLength, encodingTab[textEncodingByte]);
|
||
} else {
|
||
if (textEncodingByte == 0) {
|
||
tmp.copy_from_iso8859_1((const uint8_t*)m_ID3Hdr.iBuff.get() + idx);
|
||
} // ISO-8859-1
|
||
else if (textEncodingByte == 1 || textEncodingByte == 2) {
|
||
tmp.copy_from_utf16((const uint8_t*)m_ID3Hdr.iBuff.get() + idx, isBigEndian);
|
||
} // UTF-16LE oder UTF-16BE
|
||
else if (textEncodingByte == 3) {
|
||
tmp.copy_from(m_ID3Hdr.iBuff.get() + idx);
|
||
} // UTF-8 copy directly because no conversion is necessary
|
||
}
|
||
showID3Tag(m_ID3Hdr.tag, tmp.c_get());
|
||
m_ID3Hdr.remainingHeaderBytes -= fs;
|
||
return fs;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == MP3_SYLT) { // SYLT
|
||
m_controlCounter = MP3_SKIP;
|
||
if (m_dataMode == AUDIO_LOCALFILE || (m_streamType == ST_WEBFILE)) {
|
||
ps_ptr<char> tmp;
|
||
ps_ptr<char> content_descriptor;
|
||
ps_ptr<char> syltBuff;
|
||
bool isBigEndian = true;
|
||
size_t len = 0;
|
||
int idx = 0;
|
||
m_ID3Hdr.SYLT.pos = m_ID3Hdr.id3Size - m_ID3Hdr.remainingHeaderBytes;
|
||
m_ID3Hdr.SYLT.size = m_ID3Hdr.framesize;
|
||
if (m_ID3Hdr.SYLT.size < len) return 0;
|
||
syltBuff.copy_from((const char*)data, m_ID3Hdr.SYLT.size);
|
||
m_ID3Hdr.SYLT.text_encoding = syltBuff[0]; // 0=ISO-8859-1, 1=UTF-16, 2=UTF-16BE, 3=UTF-8
|
||
if (m_ID3Hdr.SYLT.text_encoding == 1) isBigEndian = false;
|
||
if (m_ID3Hdr.SYLT.text_encoding > 3) {
|
||
AUDIO_LOG_ERROR("unknown text encoding: {}", m_ID3Hdr.SYLT.text_encoding);
|
||
m_ID3Hdr.SYLT.text_encoding = 0;
|
||
}
|
||
char encodingTab[4][12] = {"ISO-8859-1", "UTF-16", "UTF-16BE", "UTF-8"};
|
||
memcpy(m_ID3Hdr.SYLT.lang, syltBuff.get() + 1, 3);
|
||
m_ID3Hdr.SYLT.lang[3] = '\0';
|
||
info(*this, evt_info, "Lyrics: text_encoding: {}, language: {}, size {}", encodingTab[m_ID3Hdr.SYLT.text_encoding], m_ID3Hdr.SYLT.lang, m_ID3Hdr.SYLT.size);
|
||
m_ID3Hdr.SYLT.time_stamp_format = syltBuff[4];
|
||
m_ID3Hdr.SYLT.content_type = syltBuff[5];
|
||
idx = 6;
|
||
uint16_t cd_len = 0;
|
||
if (m_ID3Hdr.SYLT.text_encoding == 0)
|
||
cd_len = 1 + content_descriptor.copy_from_iso8859_1((const uint8_t*)(syltBuff.get() + idx)); // iso8859_1
|
||
else if (m_ID3Hdr.SYLT.text_encoding == 3)
|
||
cd_len = 1 + content_descriptor.copy_from((const char*)(syltBuff.get() + idx)); // utf-8
|
||
else
|
||
cd_len = 2 + content_descriptor.copy_from_utf16((const uint8_t*)(syltBuff.get() + idx), isBigEndian); // utf-16
|
||
if (cd_len > 2) info(*this, evt_info, "Lyrics: content_descriptor: {}", content_descriptor.c_get());
|
||
|
||
idx += cd_len;
|
||
while (idx < m_ID3Hdr.SYLT.size) {
|
||
if (m_ID3Hdr.SYLT.text_encoding == 0)
|
||
idx += 1 + tmp.copy_from_iso8859_1((const uint8_t*)syltBuff.get() + idx); // ISO8859_1
|
||
else if (m_ID3Hdr.SYLT.text_encoding == 3)
|
||
idx += 1 + tmp.copy_from((const char*)syltBuff.get() + idx); // UTF-8
|
||
else
|
||
idx += 2 + tmp.copy_from_utf16((const uint8_t*)(syltBuff.get() + idx), isBigEndian); // UTF-16LE, UTF-16BE
|
||
|
||
if (tmp.starts_with("\n")) tmp.remove_before(1);
|
||
m_syltLines.push_back(std::move(tmp));
|
||
if (idx + 4 > m_ID3Hdr.SYLT.size) break; // no more 4 bytes?
|
||
uint32_t timestamp = bigEndian((uint8_t*)syltBuff.get() + idx, 4);
|
||
m_syltTimeStamp.push_back(timestamp);
|
||
idx += 4;
|
||
}
|
||
info(*this, evt_info, "audiofile contains synchronized lyrics");
|
||
// for(int i = 0; i < m_syltLines.size(); i++){
|
||
// info(*this, evt_lyrics, "{:07} ms, {}", m_syltTimeStamp[i], m_syltLines[i].c_get());
|
||
// }
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
|
||
// --- section V2.2 only , higher Vers above ----
|
||
// see https://mutagen-specs.readthedocs.io/en/latest/id3/id3v2.2.html
|
||
if (m_controlCounter == MP3_ID3V22) { // frames in V2.2, 3bytes identifier, 3bytes size descriptor
|
||
|
||
if (m_ID3Hdr.v22_tag_length > 0) {
|
||
if (m_ID3Hdr.v22_tag_length > len) {
|
||
m_ID3Hdr.v22_tag_length -= len;
|
||
return len;
|
||
} // Throw it away
|
||
else {
|
||
uint32_t t = m_ID3Hdr.v22_tag_length;
|
||
m_ID3Hdr.v22_tag_length = 0;
|
||
return t;
|
||
} // Throw it away
|
||
}
|
||
|
||
if (m_ID3Hdr.remainingHeaderBytes == 0) {
|
||
m_controlCounter = MP3_LASTFRAMES;
|
||
return 0;
|
||
}
|
||
|
||
ps_ptr<char> tag;
|
||
tag.set_name("tag");
|
||
ps_ptr<char> value;
|
||
value.set_name("value");
|
||
if (data[0] == 0) { // we are in padding
|
||
m_controlCounter = MP3_LASTFRAMES;
|
||
uint16_t padding = m_ID3Hdr.remainingHeaderBytes;
|
||
m_ID3Hdr.remainingHeaderBytes = 0;
|
||
return padding;
|
||
}
|
||
tag.copy_from((const char*)data, 3);
|
||
m_ID3Hdr.v22_tag_length = bigEndian(data + 3, 3) + 6;
|
||
value.copy_from((const char*)data + 7, min(m_ID3Hdr.v22_tag_length - 7, (size_t)1024));
|
||
|
||
if (tag.starts_with("PIC")) { // image embedded in header
|
||
size_t pic_len = bigEndian(data + 3, 3);
|
||
uint32_t pic_start = m_ID3Hdr.totalId3Size + m_ID3Hdr.id3Size - m_ID3Hdr.remainingHeaderBytes;
|
||
|
||
m_ID3Hdr.APIC_vec.push_back(pic_start + 6);
|
||
m_ID3Hdr.APIC_vec.push_back(pic_len);
|
||
} else if (startsWith(m_ID3Hdr.tag, "SLT")) { // lyrics embedded in header
|
||
if (m_dataMode == AUDIO_LOCALFILE) {
|
||
|
||
ps_ptr<char> tmp;
|
||
ps_ptr<char> content_descriptor;
|
||
ps_ptr<char> syltBuff;
|
||
bool isBigEndian = true;
|
||
size_t len = 0;
|
||
int idx = 0;
|
||
|
||
m_ID3Hdr.SYLT.pos = m_ID3Hdr.id3Size - m_ID3Hdr.remainingHeaderBytes;
|
||
m_ID3Hdr.SYLT.size = m_ID3Hdr.v22_tag_length;
|
||
if (m_ID3Hdr.SYLT.size < len) return 0;
|
||
syltBuff.copy_from((const char*)data, m_ID3Hdr.SYLT.size);
|
||
m_ID3Hdr.SYLT.text_encoding = syltBuff[0];
|
||
memcpy(m_ID3Hdr.SYLT.lang, syltBuff.get() + 1, 3);
|
||
m_ID3Hdr.SYLT.lang[3] = '\0';
|
||
info(*this, evt_info, "Lyrics: text_encoding: {}, language: {}, size {}",
|
||
m_ID3Hdr.SYLT.text_encoding == 0 ? "ASCII"
|
||
: m_ID3Hdr.SYLT.text_encoding == 3 ? "UTF-8"
|
||
: "?",
|
||
m_ID3Hdr.SYLT.lang, m_ID3Hdr.SYLT.size);
|
||
m_ID3Hdr.SYLT.time_stamp_format = syltBuff[4];
|
||
m_ID3Hdr.SYLT.content_type = syltBuff[5];
|
||
|
||
idx = 6;
|
||
|
||
if (m_ID3Hdr.SYLT.text_encoding == 0 || m_ID3Hdr.SYLT.text_encoding == 3) { // utf-8
|
||
len = content_descriptor.copy_from((const char*)(syltBuff.get() + idx));
|
||
} else { // utf-16
|
||
len = content_descriptor.copy_from_utf16((const uint8_t*)(syltBuff.get() + idx), isBigEndian);
|
||
}
|
||
if (len > 2) info(*this, evt_info, "Lyrics: content_descriptor: {}", content_descriptor.c_get());
|
||
idx += len;
|
||
|
||
while (idx < m_ID3Hdr.SYLT.size) {
|
||
// UTF-16LE, UTF-16BE
|
||
if (m_ID3Hdr.SYLT.text_encoding == 1 || m_ID3Hdr.SYLT.text_encoding == 2) {
|
||
idx += tmp.copy_from_utf16((const uint8_t*)(syltBuff.get() + idx), isBigEndian);
|
||
} else {
|
||
// ISO-8859-1 / UTF-8
|
||
idx += tmp.copy_from((const char*)syltBuff.get() + idx);
|
||
}
|
||
if (tmp.starts_with("\n")) tmp.remove_before(1);
|
||
m_syltLines.push_back(std::move(tmp));
|
||
|
||
if (idx + 4 > m_ID3Hdr.SYLT.size) break; // no more 4 bytes?
|
||
|
||
uint32_t timestamp = bigEndian((uint8_t*)syltBuff.get() + idx, 4);
|
||
m_syltTimeStamp.push_back(timestamp);
|
||
|
||
idx += 4;
|
||
}
|
||
info(*this, evt_info, "audiofile contains synchronized lyrics");
|
||
// for(int i = 0; i < m_syltLines.size(); i++){
|
||
// info(*this, evt_lyrics, "{:07} ms, {}", m_syltTimeStamp[i], m_syltLines[i].c_get());
|
||
// }
|
||
}
|
||
} else {
|
||
showID3Tag(tag.c_get(), value.c_get());
|
||
}
|
||
|
||
m_ID3Hdr.remainingHeaderBytes -= m_ID3Hdr.v22_tag_length;
|
||
|
||
return 0;
|
||
}
|
||
// -- end section V2.2 -----------
|
||
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == MP3_LASTFRAMES) { // skip all ID3 metadata (mostly spaces)
|
||
if (m_ID3Hdr.remainingHeaderBytes > len) {
|
||
m_ID3Hdr.remainingHeaderBytes -= len;
|
||
return len;
|
||
} // Throw it away
|
||
else {
|
||
m_controlCounter = MP3_XING;
|
||
return m_ID3Hdr.remainingHeaderBytes;
|
||
} // Throw it away
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == MP3_XING) { // exist another ID3tag?
|
||
|
||
static const int samplerate_table[4][3] = {
|
||
{11025, 12000, 8000}, // MPEG 2.5
|
||
{0, 0, 0}, // reserved
|
||
{22050, 24000, 16000}, // MPEG 2
|
||
{44100, 48000, 32000} // MPEG 1
|
||
};
|
||
|
||
static const int samples_per_frame[4][4] = {// layer: 0 1 2 3
|
||
/* V2.5 */ {0, 576, 1152, 384},
|
||
/* res. */ {0, 0, 0, 0},
|
||
/* V2 */ {0, 576, 1152, 384},
|
||
/* V1 */ {0, 1152, 1152, 384}};
|
||
|
||
m_audioDataStart += m_ID3Hdr.id3Size;
|
||
m_ID3Hdr.totalId3Size += m_ID3Hdr.id3Size;
|
||
m_ID3Hdr.id3Size = 0;
|
||
|
||
if ((*(data + 0) == 'I') && (*(data + 1) == 'D') && (*(data + 2) == '3')) {
|
||
m_controlCounter = MP3_NEXTID3;
|
||
m_ID3Hdr.numID3Header++;
|
||
return 0;
|
||
} else {
|
||
// padding after ID3 body?
|
||
uint32_t padding_counter = 0;
|
||
while (data[padding_counter] == 0) {
|
||
padding_counter++;
|
||
m_audioDataStart++;
|
||
m_audioDataSize--;
|
||
}
|
||
if (padding_counter) return padding_counter;
|
||
|
||
m_controlCounter = MP3_OKAY; // 100 -> ok
|
||
m_audioDataSize = m_audioFileSize - m_audioDataStart;
|
||
if (!m_f_m3u8data) info(*this, evt_info, "Audio-Data-Start: {}", m_audioDataStart);
|
||
if (!m_f_m3u8data) info(*this, evt_info, "Audio-Length: {}", m_audioDataSize);
|
||
|
||
uint32_t hdr = bigEndian(data, 4);
|
||
if ((hdr & 0xFFE00000) != 0xFFE00000) AUDIO_LOG_ERROR("Syncword not found"); // check sync
|
||
int versionID = (hdr >> 19) & 0x3; // 0=MPEG2.5, 2=MPEG2, 3=MPEG1
|
||
int layerIndex = (hdr >> 17) & 0x3; // 1=Layer3
|
||
int bitrateIdx = (hdr >> 12) & 0xF;
|
||
int samplerateIdx = (hdr >> 10) & 0x3;
|
||
int mode = (hdr >> 6) & 0x3; // 0=stereo,3=mono
|
||
int samplerate = samplerate_table[versionID][samplerateIdx];
|
||
int spf = samples_per_frame[versionID][layerIndex];
|
||
|
||
// Xing or Info Header present?
|
||
int8_t mp3_xing = specialIndexOf(data, "Xing", 50);
|
||
int8_t mp3_info = specialIndexOf(data, "Info", 50);
|
||
|
||
uint8_t xingPos = 0;
|
||
if (mp3_xing > 0) xingPos = mp3_xing;
|
||
if (mp3_info > 0) xingPos = mp3_info;
|
||
|
||
if (xingPos > 0 && layerIndex == 1) { // layer III only
|
||
uint32_t frames = bigEndian(data + xingPos + 8, 4);
|
||
AUDIO_LOG_DEBUG("frames {}", frames);
|
||
uint32_t bytes = bigEndian(data + xingPos + 12, 4);
|
||
AUDIO_LOG_DEBUG("bytes {}", bytes);
|
||
uint32_t duration = frames * spf / samplerate;
|
||
info(*this, evt_info, "Duration (s): {}", duration);
|
||
m_audioFileDuration = duration;
|
||
uint32_t bitrate = bytes * 8 / duration;
|
||
info(*this, evt_info, "Bitrate (b/s): {}", bitrate);
|
||
m_nominal_bitrate = bitrate;
|
||
}
|
||
|
||
if (m_ID3Hdr.APIC_vec.size()) { // if we have a APIC
|
||
info(*this, evt_image, m_ID3Hdr.APIC_vec);
|
||
m_ID3Hdr.APIC_vec.clear();
|
||
AUDIO_LOG_DEBUG("m_ID3Hdr.totalId3Size {}, m_audioDataStart {}", m_ID3Hdr.totalId3Size, m_audioDataStart);
|
||
if (m_ID3Hdr.totalId3Size != m_audioDataStart) audioFileSeek(m_audioDataStart);
|
||
}
|
||
m_ID3Hdr.numID3Header = 0;
|
||
m_ID3Hdr.totalId3Size = 0;
|
||
m_ID3Hdr.iBuff.reset();
|
||
return 0;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
int Audio::read_M4A_Header(uint8_t* data, size_t len) {
|
||
bool atom_struct = false;
|
||
ps_ptr<char> atom_name;
|
||
ps_ptr<char> atom_size;
|
||
uint32_t idx = 0;
|
||
|
||
/*
|
||
ftyp
|
||
| - moov -> trak -> ... -> mp4a contains raw block parameters
|
||
| L... -> ilst contains artist, composer ....
|
||
free (optional) // jump to another atoms at the end of mdat
|
||
|
|
||
mdat contains the audio data */
|
||
|
||
if (m_m4aHdr.retvalue) {
|
||
if (m_m4aHdr.retvalue > UINT16_MAX) {
|
||
int res = audioFileSeek(m_m4aHdr.headerSize);
|
||
if (res >= 0) {
|
||
AUDIO_LOG_INFO("skip {} bytes", m_m4aHdr.retvalue);
|
||
InBuff.reset();
|
||
m_m4aHdr.retvalue = 0;
|
||
return 0;
|
||
}
|
||
}
|
||
if (m_m4aHdr.retvalue > len) {
|
||
m_m4aHdr.retvalue -= len;
|
||
m_m4aHdr.cnt += len;
|
||
return len;
|
||
} else {
|
||
size_t tmp = m_m4aHdr.retvalue;
|
||
m_m4aHdr.retvalue = 0;
|
||
m_m4aHdr.cnt += tmp;
|
||
return tmp;
|
||
}
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == M4A_BEGIN) { // init
|
||
memset(&m_m4aHdr, 0, sizeof(audiolib::m4aHdr_t));
|
||
|
||
atom_size.big_endian(data, 4);
|
||
atom_name.copy_from((const char*)data + 4, 4);
|
||
|
||
if (atom_name.equals("ftyp")) {
|
||
if (atom_struct) {
|
||
AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, (uint32_t)atom_size.to_uint32(16), m_m4aHdr.headerSize + (size_t)atom_size.to_uint32(16));
|
||
m_m4aHdr.sizeof_ftyp = atom_size.to_uint32(16);
|
||
}
|
||
m_controlCounter = M4A_FTYP;
|
||
} else {
|
||
AUDIO_LOG_ERROR("begin: ftyp not found");
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == M4A_FTYP) { /* check_m4a_file */
|
||
|
||
int m4a = specialIndexOf(data, "M4A ", 20);
|
||
int isom = specialIndexOf(data, "isom", 20);
|
||
int mp42 = specialIndexOf(data, "mp42", 20);
|
||
|
||
if ((m4a != 8) && (isom != 8) && (mp42 != 8)) {
|
||
AUDIO_LOG_ERROR("subtype 'MA4 ', 'isom' or 'mp42' expected, but found '{} '", (data + 8));
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
|
||
m_m4aHdr.retvalue += m_m4aHdr.sizeof_ftyp;
|
||
m_m4aHdr.headerSize += m_m4aHdr.sizeof_ftyp;
|
||
m_controlCounter = M4A_CHK;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == M4A_CHK) { /* check Tag */
|
||
atom_size.big_endian(data, 4);
|
||
atom_name.copy_from((const char*)data + 4, 4);
|
||
|
||
if (atom_name.equals("moov")) {
|
||
if (!m_m4aHdr.mdat_seen) m_m4aHdr.progressive = true; // moov before mdat
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.sizeof_moov = atom_size.to_uint32(16) - 8;
|
||
m_m4aHdr.retvalue += 8;
|
||
m_m4aHdr.headerSize += 8;
|
||
m_controlCounter = M4A_MOOV;
|
||
return 0;
|
||
}
|
||
|
||
if (atom_name.equals("mdat")) {
|
||
m_m4aHdr.mdat_seen = true;
|
||
if (!m_m4aHdr.progressive) {
|
||
m_m4aHdr.mdat_startPos = m_m4aHdr.headerSize + 8;
|
||
m_m4aHdr.sizeof_mdat = atom_size.to_uint32(16);
|
||
if (atom_struct) {
|
||
AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.mdat_startPos, m_m4aHdr.sizeof_mdat, m_m4aHdr.mdat_startPos + m_m4aHdr.sizeof_mdat);
|
||
}
|
||
info(*this, evt_info, "Audiofile is non progressive");
|
||
m_m4aHdr.retvalue += m_m4aHdr.sizeof_mdat;
|
||
m_m4aHdr.headerSize += m_m4aHdr.sizeof_mdat;
|
||
return 0;
|
||
stopSong();
|
||
return -1;
|
||
}
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.sizeof_mdat = atom_size.to_uint32(16) - 8;
|
||
m_m4aHdr.retvalue += 8;
|
||
m_m4aHdr.headerSize += 8;
|
||
m_controlCounter = M4A_MDAT;
|
||
return 0;
|
||
}
|
||
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.retvalue += atom_size.to_uint32(16);
|
||
m_m4aHdr.headerSize += atom_size.to_uint32(16);
|
||
m_m4aHdr.sizeof_moov -= atom_size.to_uint32(16);
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == M4A_MOOV) { // moov
|
||
if (atom_struct) { AUDIO_LOG_WARN("moov size remain {}", m_m4aHdr.sizeof_moov); }
|
||
if (m_m4aHdr.sizeof_moov == 0) {
|
||
if (m_m4aHdr.progressive) {
|
||
m_controlCounter = M4A_CHK;
|
||
AUDIO_LOG_DEBUG("goto CHK");
|
||
} else {
|
||
audioFileSeek(0); // non progressive, back to mdat
|
||
InBuff.reset();
|
||
m_m4aHdr.headerSize = m_m4aHdr.mdat_startPos;
|
||
m_m4aHdr.retvalue = m_m4aHdr.mdat_startPos; // set InBuff.getReadPtr to audiodatastart
|
||
if (atom_struct) AUDIO_LOG_WARN("goto MDAT at {}", m_m4aHdr.mdat_startPos + 8);
|
||
m_controlCounter = M4A_MDAT;
|
||
}
|
||
return 0;
|
||
} // go back
|
||
|
||
atom_name.copy_from((const char*)data + 4, 4);
|
||
atom_size.big_endian(data, 4);
|
||
m_m4aHdr.sizeof_moov -= atom_size.to_uint32(16);
|
||
|
||
if (atom_name.equals("trak")) {
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.sizeof_trak = atom_size.to_uint32(16) - 8;
|
||
m_m4aHdr.retvalue += 8;
|
||
m_m4aHdr.headerSize += 8;
|
||
m_controlCounter = M4A_TRAK;
|
||
return 0;
|
||
}
|
||
|
||
if (atom_name.equals("udta")) {
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.sizeof_udta = atom_size.to_uint32(16) - 8;
|
||
m_m4aHdr.retvalue += 8;
|
||
m_m4aHdr.headerSize += 8;
|
||
m_controlCounter = M4A_UDTA;
|
||
return 0;
|
||
}
|
||
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.retvalue += atom_size.to_uint32(16);
|
||
m_m4aHdr.headerSize += atom_size.to_uint32(16);
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == M4A_TRAK) { // trak
|
||
if (atom_struct) { AUDIO_LOG_WARN("trak size remain {}", m_m4aHdr.sizeof_trak); }
|
||
if (m_m4aHdr.sizeof_trak == 0) {
|
||
m_controlCounter = M4A_MOOV;
|
||
return 0;
|
||
} // go back
|
||
|
||
atom_name.copy_from((const char*)data + 4, 4);
|
||
atom_size.big_endian(data, 4);
|
||
m_m4aHdr.sizeof_trak -= atom_size.to_uint32(16);
|
||
|
||
if (atom_name.equals("mdia")) {
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.sizeof_mdia = atom_size.to_uint32(16) - 8;
|
||
m_m4aHdr.retvalue += 8;
|
||
m_m4aHdr.headerSize += 8;
|
||
m_controlCounter = M4A_MDIA;
|
||
return 0;
|
||
}
|
||
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.retvalue += atom_size.to_uint32(16);
|
||
m_m4aHdr.headerSize += atom_size.to_uint32(16);
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == M4A_MDIA) { // mdia
|
||
if (atom_struct) { AUDIO_LOG_WARN("mdia size remain {}", m_m4aHdr.sizeof_mdia); }
|
||
if (m_m4aHdr.sizeof_mdia == 0) {
|
||
m_controlCounter = M4A_TRAK;
|
||
return 0;
|
||
} // go back
|
||
|
||
atom_name.copy_from((const char*)data + 4, 4);
|
||
atom_size.big_endian(data, 4);
|
||
m_m4aHdr.sizeof_mdia -= atom_size.to_uint32(16);
|
||
|
||
if (atom_name.equals("mdhd")) {
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.sizeof_mdhd = atom_size.to_uint32(16) - 8;
|
||
m_m4aHdr.retvalue += 8;
|
||
m_m4aHdr.headerSize += 8;
|
||
m_controlCounter = M4A_MDHD;
|
||
return 0;
|
||
}
|
||
|
||
if (atom_name.equals("minf")) {
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.sizeof_minf = atom_size.to_uint32(16) - 8;
|
||
m_m4aHdr.retvalue += 8;
|
||
m_m4aHdr.headerSize += 8;
|
||
m_controlCounter = M4A_MINF;
|
||
return 0;
|
||
}
|
||
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.retvalue += atom_size.to_uint32(16);
|
||
m_m4aHdr.headerSize += atom_size.to_uint32(16);
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == M4A_MINF) { // minf
|
||
if (atom_struct) { AUDIO_LOG_WARN("minf size remain {}", m_m4aHdr.sizeof_minf); }
|
||
if (m_m4aHdr.sizeof_minf == 0) {
|
||
m_controlCounter = M4A_MDIA;
|
||
return 0;
|
||
} // go back
|
||
|
||
atom_name.copy_from((const char*)data + 4, 4);
|
||
atom_size.big_endian(data, 4);
|
||
m_m4aHdr.sizeof_minf -= atom_size.to_uint32(16);
|
||
|
||
if (atom_name.equals("stbl")) {
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.sizeof_stbl = atom_size.to_uint32(16) - 8;
|
||
m_m4aHdr.retvalue += 8;
|
||
m_m4aHdr.headerSize += 8;
|
||
m_controlCounter = M4A_STBL;
|
||
return 0;
|
||
}
|
||
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.retvalue += atom_size.to_uint32(16);
|
||
m_m4aHdr.headerSize += atom_size.to_uint32(16);
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == M4A_MDHD) { // mdhd
|
||
if (atom_struct) { AUDIO_LOG_WARN("mdhd size remain {}", m_m4aHdr.sizeof_mdhd); }
|
||
if (m_m4aHdr.sizeof_mdhd == 0) {
|
||
m_controlCounter = M4A_MDIA;
|
||
return 0;
|
||
} // go back
|
||
|
||
ps_ptr<char> mdhd_buffer; // read ESDS content in a buffer
|
||
mdhd_buffer.copy_from((char*)data, m_m4aHdr.sizeof_mdhd); // read MDHD content (without header)
|
||
// mdhd_buffer.hex_dump(m_m4aHdr.sizeof_mdhd);
|
||
/* version; 1 Byte offset 0 0 -> 32 bit, 1 -> 64 bit
|
||
flags[3]; 3 Bytes offset 1
|
||
creation_time; 4 Bytes offset 4
|
||
modification_time; 4 Bytes offset 8
|
||
timescale; 4 Bytes offset 12
|
||
duration; 4 Bytes offset 16
|
||
language; 2 Bytes offset 20
|
||
pre_defined; 2 Bytes offset 22 */
|
||
m_m4aHdr.timescale = bigEndian((uint8_t*)mdhd_buffer.get() + 12, 4);
|
||
m_m4aHdr.duration = bigEndian((uint8_t*)mdhd_buffer.get() + 16, 4);
|
||
if (m_m4aHdr.timescale) {
|
||
m_audioFileDuration = m_m4aHdr.duration / m_m4aHdr.timescale;
|
||
info(*this, evt_info, "Duration (s): {}", m_audioFileDuration);
|
||
}
|
||
m_m4aHdr.retvalue += m_m4aHdr.sizeof_mdhd;
|
||
m_m4aHdr.headerSize += m_m4aHdr.sizeof_mdhd;
|
||
m_controlCounter = M4A_MDIA;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == M4A_STBL) { // minf
|
||
if (atom_struct) { AUDIO_LOG_WARN("stbl size remain {}", m_m4aHdr.sizeof_stbl); }
|
||
if (m_m4aHdr.sizeof_stbl == 0) {
|
||
m_controlCounter = M4A_MINF;
|
||
return 0;
|
||
} // go back
|
||
|
||
atom_size.big_endian(data, 4);
|
||
atom_name.copy_from((const char*)data + 4, 4);
|
||
m_m4aHdr.sizeof_stbl -= atom_size.to_uint32(16);
|
||
|
||
if (atom_name.equals("stsd")) {
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
uint8_t header_size = 8;
|
||
m_m4aHdr.sizeof_stsd = atom_size.to_uint32(16) - 8 - header_size;
|
||
m_m4aHdr.retvalue += 8 + header_size;
|
||
m_m4aHdr.headerSize += 8 + header_size;
|
||
m_controlCounter = M4A_STSD;
|
||
return 0;
|
||
}
|
||
|
||
if (atom_name.equals("stsz")) {
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
uint8_t header_size = 8;
|
||
m_m4aHdr.sizeof_stsz = atom_size.to_uint32(16) - 8 - header_size;
|
||
m_m4aHdr.retvalue += 8;
|
||
m_m4aHdr.headerSize += 8;
|
||
m_controlCounter = M4A_STSZ;
|
||
return 0;
|
||
}
|
||
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.retvalue += atom_size.to_uint32(16);
|
||
m_m4aHdr.headerSize += atom_size.to_uint32(16);
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == M4A_STSD) {
|
||
if (atom_struct) { AUDIO_LOG_WARN("stsd size remain {}", m_m4aHdr.sizeof_stsd); }
|
||
if (m_m4aHdr.sizeof_stsd == 0) {
|
||
m_controlCounter = M4A_STBL;
|
||
return 0;
|
||
} // go back
|
||
|
||
atom_size.big_endian(data, 4);
|
||
atom_name.copy_from((const char*)data + 4, 4);
|
||
m_m4aHdr.sizeof_stsd -= atom_size.to_uint32(16);
|
||
|
||
if (atom_name.equals("mp4a")) {
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.sizeof_mp4a = atom_size.to_uint32(16) - 8;
|
||
m_m4aHdr.retvalue += 8;
|
||
m_m4aHdr.headerSize += 8;
|
||
m_controlCounter = M4A_MP4A;
|
||
return 0;
|
||
}
|
||
|
||
if (atom_name.equals("alac")) {
|
||
AUDIO_LOG_ERROR("Apple loseless audio codec (ALAC) not supported");
|
||
stopSong();
|
||
return 0;
|
||
}
|
||
|
||
if (atom_name.equals("ac-3")) {
|
||
AUDIO_LOG_ERROR("Dolby Digital not supported");
|
||
stopSong();
|
||
return 0;
|
||
}
|
||
|
||
if (atom_name.equals("ec-3")) {
|
||
AUDIO_LOG_ERROR("Dolby Digital Plud not supported");
|
||
stopSong();
|
||
return 0;
|
||
}
|
||
|
||
if (atom_name.equals("Opus")) {
|
||
// todo
|
||
stopSong();
|
||
return 0;
|
||
}
|
||
|
||
if (atom_name.equals("mp3 ")) {
|
||
// todo
|
||
stopSong();
|
||
return 0;
|
||
}
|
||
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.retvalue += atom_size.to_uint32(16);
|
||
m_m4aHdr.headerSize += atom_size.to_uint32(16);
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == M4A_CHPL) {
|
||
if (atom_struct) { AUDIO_LOG_WARN("chpl size remain {}", m_m4aHdr.sizeof_chpl); }
|
||
if (m_m4aHdr.sizeof_chpl == 0) {
|
||
m_controlCounter = M4A_UDTA;
|
||
return 0;
|
||
} // go back
|
||
|
||
ps_ptr<char> title_name;
|
||
// data[0] version
|
||
// data[1] 4 flags
|
||
uint32_t nr_of_chapters = bigEndian(data + 5, 4); // Number of chapters
|
||
// data[9] uint64 timestamp
|
||
uint8_t title_length = data[17];
|
||
title_name.copy_from((char*)data + 18, title_length);
|
||
|
||
info(*this, evt_info, "Number of chapters: {}", nr_of_chapters);
|
||
info(*this, evt_info, "Chapter name: {}", title_name.c_get());
|
||
|
||
m_m4aHdr.retvalue += m_m4aHdr.sizeof_chpl;
|
||
m_m4aHdr.headerSize += m_m4aHdr.sizeof_chpl;
|
||
m_m4aHdr.sizeof_chpl = 0; // there are no subatoms
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == M4A_MP4A) {
|
||
if (atom_struct) { AUDIO_LOG_WARN("mp4a size remain {}", m_m4aHdr.sizeof_mp4a); }
|
||
if (m_m4aHdr.sizeof_mp4a == 0) {
|
||
m_controlCounter = M4A_STSD;
|
||
return 0;
|
||
} // go back
|
||
|
||
if (!m_m4aHdr.sample_rate) { // read the header first
|
||
// data[0]...[5] reserved
|
||
// data[6] & [7] Data reference buffer_index
|
||
// data[8]...[15] reserved
|
||
m_m4aHdr.channel_count = data[16] * 256 + data[17];
|
||
AUDIO_LOG_DEBUG("channels {}", m_m4aHdr.channel_count);
|
||
m_m4aHdr.sample_size = data[18] * 256 + data[19];
|
||
AUDIO_LOG_DEBUG("bit per sample {}", m_m4aHdr.sample_size);
|
||
// data[20]...data[23] reserved
|
||
m_m4aHdr.sample_rate = data[24] * 256 + data[25];
|
||
AUDIO_LOG_DEBUG("sample rate {}", m_m4aHdr.sample_rate);
|
||
// data[26]...data[27] sample rate fixed point is 0x00, 0x00
|
||
m_m4aHdr.offset = 28;
|
||
m_m4aHdr.retvalue += m_m4aHdr.offset;
|
||
m_m4aHdr.headerSize += m_m4aHdr.offset;
|
||
m_m4aHdr.sizeof_mp4a -= m_m4aHdr.offset;
|
||
return 0;
|
||
}
|
||
atom_size.big_endian(data, 4);
|
||
atom_name.copy_from((const char*)data + 4, 4);
|
||
m_m4aHdr.sizeof_mp4a -= atom_size.to_uint32(16);
|
||
|
||
if (atom_name.equals("esds")) {
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.sizeof_esds = atom_size.to_uint32(16) - 8;
|
||
m_m4aHdr.retvalue += 8;
|
||
m_m4aHdr.headerSize += 8;
|
||
m_controlCounter = M4A_ESDS;
|
||
return 0;
|
||
}
|
||
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.retvalue += atom_size.to_uint32(16);
|
||
m_m4aHdr.headerSize += atom_size.to_uint32(16);
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == M4A_UDTA) { // udta
|
||
if (atom_struct) { AUDIO_LOG_WARN("udta size remain {}", m_m4aHdr.sizeof_udta); }
|
||
if (m_m4aHdr.sizeof_udta == 0) {
|
||
m_controlCounter = M4A_MOOV;
|
||
return 0;
|
||
} // go back
|
||
|
||
atom_size.big_endian(data, 4);
|
||
atom_name.copy_from((const char*)data + 4, 4);
|
||
m_m4aHdr.sizeof_udta -= atom_size.to_uint32(16);
|
||
|
||
if (atom_name.equals("meta")) {
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.sizeof_meta = atom_size.to_uint32(16) - 8;
|
||
m_m4aHdr.retvalue += 8;
|
||
m_m4aHdr.headerSize += 8;
|
||
m_controlCounter = M4A_META;
|
||
return 0;
|
||
}
|
||
|
||
if (atom_name.equals("chpl")) {
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.sizeof_chpl = atom_size.to_uint32(16) - 8;
|
||
m_m4aHdr.retvalue += 8;
|
||
m_m4aHdr.headerSize += 8;
|
||
m_controlCounter = M4A_CHPL;
|
||
return 0;
|
||
}
|
||
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.retvalue += atom_size.to_uint32(16);
|
||
m_m4aHdr.headerSize += atom_size.to_uint32(16);
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == M4A_META) { // meta
|
||
if (atom_struct) { AUDIO_LOG_WARN("meta size remain {}", m_m4aHdr.sizeof_meta); }
|
||
if (m_m4aHdr.sizeof_meta == 0) {
|
||
m_controlCounter = M4A_UDTA;
|
||
return 0;
|
||
} // go back, 4bytes typ + 4 bytes size
|
||
|
||
if (!m_m4aHdr.version_flags) {
|
||
// 0x00, 0x00, 0x00, 0x94, 0x6D, 0x65, 0x74, 0x61, 0x00, 0x00, 0x00, 0x00,
|
||
// size | m e t a | flags
|
||
m_m4aHdr.version_flags = true;
|
||
m_m4aHdr.sizeof_meta -= 4;
|
||
m_m4aHdr.retvalue += 4;
|
||
m_m4aHdr.headerSize += 4;
|
||
idx += 4;
|
||
}
|
||
atom_size.big_endian(data + idx, 4);
|
||
atom_name.copy_from((const char*)data + 4 + idx, 4);
|
||
m_m4aHdr.sizeof_meta -= atom_size.to_uint32(16);
|
||
|
||
if (atom_name.equals("ilst")) {
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.sizeof_ilst = atom_size.to_uint32(16) - 8;
|
||
m_m4aHdr.retvalue += 8;
|
||
m_m4aHdr.headerSize += 8;
|
||
m_controlCounter = M4A_ILST;
|
||
return 0;
|
||
}
|
||
|
||
if (atom_struct) { AUDIO_LOG_WARN("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), m_m4aHdr.headerSize, atom_size.to_uint32(16), m_m4aHdr.headerSize + atom_size.to_uint32(16)); }
|
||
m_m4aHdr.retvalue += atom_size.to_uint32(16);
|
||
m_m4aHdr.headerSize += atom_size.to_uint32(16);
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == M4A_ESDS) { // Elementary Stream Descriptor
|
||
ps_ptr<char> esds_buffer; // read ESDS content in a buffer
|
||
esds_buffer.copy_from((char*)data, m_m4aHdr.sizeof_esds); // read ESDS content (without header)
|
||
// esds_buffer.hex_dump(m_m4aHdr.sizeof_esds);
|
||
|
||
// search for decoderConfigDescriptor (tag 0x04)
|
||
int32_t dec_config_descriptor_offset = esds_buffer.special_index_of("\x04\x80\x80\x80", 4, (uint32_t)m_m4aHdr.sizeof_esds);
|
||
if (dec_config_descriptor_offset > 0) { // decoderConfigDescriptor found
|
||
uint8_t dec_config_descriptor_length = ((uint8_t*)esds_buffer.get())[dec_config_descriptor_offset + 4]; // Length after Tag + 3 Extended Length Bytes
|
||
m_m4aHdr.objectTypeIndicator = ((uint8_t*)esds_buffer.get())[dec_config_descriptor_offset + 5]; // 0x40 (AAC)
|
||
m_m4aHdr.streamType = ((uint8_t*)esds_buffer.get())[dec_config_descriptor_offset + 6]; // 0x05 (Audio)
|
||
m_m4aHdr.bufferSizeDB = bigEndian((uint8_t*)esds_buffer.get() + dec_config_descriptor_offset + 7, 3); // 24 bit
|
||
m_m4aHdr.maxBitrate = bigEndian((uint8_t*)esds_buffer.get() + dec_config_descriptor_offset + 10, 4); // 32 bit
|
||
m_m4aHdr.nomBitrate = bigEndian((uint8_t*)esds_buffer.get() + dec_config_descriptor_offset + 14, 4); // 32 bit
|
||
// info(*this, evt_info, "maxBitrate {}, nominalBitrate {}", m_m4aHdr.maxBitrate, m_m4aHdr.nomBitrate);
|
||
}
|
||
|
||
// search for decoderspecificinfo (tag 0x05)
|
||
int32_t dec_specific_offset = esds_buffer.special_index_of("\x05\x80\x80\x80", 4, m_m4aHdr.sizeof_esds);
|
||
if (dec_specific_offset >= 0) { // decoderSpecificInfo found
|
||
uint8_t dec_specific_length = ((uint8_t*)esds_buffer.get())[dec_specific_offset + 4]; // Länge nach Tag + 3 Extended Length Bytes
|
||
AUDIO_LOG_DEBUG("DecoderSpecificInfo found at offset {}, length: {}", dec_specific_offset, dec_specific_length);
|
||
|
||
// extract decoderSpecificInfo-data
|
||
ps_ptr<char> dec_specific_data;
|
||
dec_specific_data.alloc(dec_specific_length, "dec_specific_data");
|
||
memcpy(dec_specific_data.get(), (uint8_t*)esds_buffer.get() + dec_specific_offset + 5, dec_specific_length);
|
||
m_m4aHdr.aac_profile = (uint8_t)dec_specific_data.get()[0] >> 3;
|
||
if (m_m4aHdr.aac_profile > 4) {
|
||
AUDIO_LOG_ERROR("unsupported AAC Profile {}", m_m4aHdr.aac_profile);
|
||
stopSong();
|
||
return 0;
|
||
}
|
||
AUDIO_LOG_DEBUG("DecoderSpecificInfo data: 0x{:02X} 0x{:02X}", (uint8_t)dec_specific_data.get()[0], (uint8_t)dec_specific_data.get()[1]);
|
||
char profile[5][33] = {"unknown", "AAC Main", "AAC LC (Low Complexity)", "AAC SSR (Scalable Sample Rate)", "AAC LTP (Long Term Prediction)"};
|
||
info(*this, evt_info, "AAC Profile: {}", profile[m_m4aHdr.aac_profile]);
|
||
info(*this, evt_info, "AAC Channels; {}", m_m4aHdr.channel_count);
|
||
info(*this, evt_info, "AAC Sample Rate: {}", m_m4aHdr.sample_rate);
|
||
info(*this, evt_info, "AAC Bits Per Sample: {}", m_m4aHdr.sample_size);
|
||
m_M4A_chConfig = m_m4aHdr.channel_count;
|
||
m_M4A_sampleRate = m_m4aHdr.sample_rate;
|
||
m_M4A_objectType = m_m4aHdr.aac_profile;
|
||
} else {
|
||
AUDIO_LOG_WARN("No DecoderSpecificInfo found in esds");
|
||
}
|
||
m_m4aHdr.retvalue += m_m4aHdr.sizeof_esds;
|
||
m_m4aHdr.headerSize += m_m4aHdr.sizeof_esds;
|
||
m_controlCounter = M4A_MP4A;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == M4A_ILST) {
|
||
|
||
auto parse_m4a_timestamp = [&](const char* s) {
|
||
if (!s || *s != '[') return -1;
|
||
int mm = 0, ss = 0, hh = 0;
|
||
if (sscanf(s, "[%d:%d.%d]", &mm, &ss, &hh) == 3) { return mm * 60000 + ss * 1000 + hh * 10; }
|
||
return -1;
|
||
};
|
||
|
||
struct TagInfo { // Definition of the Taginfo structure for iTunes-style metadata
|
||
const uint8_t tag[4];
|
||
const char* name;
|
||
const char* descr;
|
||
};
|
||
const TagInfo tags[] = {
|
||
// List of all usual tags
|
||
{{0xA9, 0x6E, 0x61, 0x6D}, "©nam", "Title"}, {{0xA9, 0x41, 0x52, 0x54}, "©ART", "Artist"}, {{0xA9, 0x61, 0x72, 0x74}, "©art", "Artist"},
|
||
{{0xA9, 0x61, 0x6C, 0x62}, "©alb", "Album"}, {{0xA9, 0x74, 0x6F, 0x6F}, "©too", "Encoder"}, {{0xA9, 0x63, 0x6D, 0x74}, "©cmt", "Comment"},
|
||
{{0xA9, 0x77, 0x72, 0x74}, "©wrt", "Composer"}, {{0x74, 0x6D, 0x70, 0x6F}, "tmpo", "Tempo (BPM)"}, {{0x74, 0x72, 0x6B, 0x6E}, "trkn", "Track-Number"},
|
||
{{0xA9, 0x64, 0x61, 0x79}, "©day", "Year"}, {{0x63, 0x70, 0x69, 0x6C}, "cpil", "Compilation-Flag"}, {{0x61, 0x41, 0x52, 0x54}, "aART", "Album Artist"},
|
||
{{0xA9, 0x67, 0x65, 0x6E}, "©gen", "Genre"}, {{0x63, 0x6F, 0x76, 0x72}, "covr", "Cover Art"}, {{0x64, 0x69, 0x73, 0x6B}, "disk", "Disk-Nummer"},
|
||
{{0xA9, 0x6C, 0x79, 0x72}, "©lyr", "Songtext"}, {{0xA9, 0x70, 0x72, 0x74}, "cprt", "Copyright"}, {{0x67, 0x6E, 0x72, 0x65}, "gnre", "Genre-ID"},
|
||
{{0x72, 0x74, 0x6E, 0x67}, "rtng", "Evaluation"}, {{0x70, 0x67, 0x61, 0x70}, "pgap", "Gapless Playback"},
|
||
};
|
||
const size_t tags_count = sizeof(tags) / sizeof(tags[0]); // Number of tags
|
||
|
||
ps_ptr<char> id3tag;
|
||
ps_ptr<char> lyricsBuffer;
|
||
uint32_t pos = m_m4aHdr.headerSize;
|
||
uint32_t consumed = 0;
|
||
while (true) {
|
||
atom_size.big_endian(data + consumed, 4);
|
||
atom_name.copy_from((const char*)data + 4 + consumed, 4);
|
||
uint32_t as = atom_size.to_uint32(16);
|
||
AUDIO_LOG_DEBUG("atom {} @ {}, size: {}, ends @ {}", atom_name.c_get(), pos, as, pos + as);
|
||
m_m4aHdr.ilst_pos += as;
|
||
pos += as;
|
||
consumed += as;
|
||
|
||
// 0x00, 0x00, 0x00, 0x1C, 0xA9, 0x64, 0x61, 0x79, 0x00, 0x00, 0x00, 0x14, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x32, 0x30, 0x32, 0x32,
|
||
// sub atom length | © d a y | sub sub atom length | d a t a | data type 1->UTF-8 | reserved | 2 0 2 2
|
||
|
||
ps_ptr<char> sa; // sub atom
|
||
sa.copy_from((const char*)data + consumed - as, min(as, (uint32_t)1024));
|
||
|
||
char san[5] = {0}; // sub atom name
|
||
char ssan[5] = {0}; // sub sub atom name (should be 'data')
|
||
strncpy(san, &sa[4], 4);
|
||
strncpy(ssan, &sa[12], 4);
|
||
uint32_t ssal = bigEndian((uint8_t*)&sa[8], 4); // sub sub atom length
|
||
uint32_t dty = bigEndian((uint8_t*)&sa[16], 4); // data type 1-UTF8
|
||
if (strncmp(ssan, "data", 4) == 0) {
|
||
for (int i = 0; i < tags_count; i++) {
|
||
if (memcmp(san, tags[i].tag, 4) == 0) {
|
||
id3tag.reset();
|
||
if (dty == 0 && strlen(&sa[24]) > 0) {
|
||
if (strcmp(san, "covr") == 0) {
|
||
m_m4aHdr.picLen = as - 24;
|
||
m_m4aHdr.picPos = pos + 24 - as;
|
||
AUDIO_LOG_DEBUG("cover jpeg start: {}, len {}", m_m4aHdr.picPos, m_m4aHdr.picLen);
|
||
}
|
||
id3tag.assignf("{}: {}", tags[i].descr, &sa[24]); // binary
|
||
} else if (dty == 1 && strlen(&sa[24]) > 0)
|
||
id3tag.assignf("{}: {}", tags[i].descr, &sa[24]); // UTF-8 Text
|
||
else if (dty == 0x0D) {
|
||
m_m4aHdr.picLen = as - 24;
|
||
m_m4aHdr.picPos = pos + 24 - as;
|
||
AUDIO_LOG_DEBUG("cover jpeg start: {}, len {}", m_m4aHdr.picPos, m_m4aHdr.picLen);
|
||
} // jpeg
|
||
else if (dty == 0x0E) {
|
||
m_m4aHdr.picLen = as - 24;
|
||
m_m4aHdr.picPos = pos + 24 - as;
|
||
AUDIO_LOG_WARN("cover png start: {}, len {}", m_m4aHdr.picPos, m_m4aHdr.picLen);
|
||
} // png
|
||
else if (dty == 21) {
|
||
if (memcmp(tags[i].tag, "cpil", 4) == 0) { break; } // Compilation Flag
|
||
if (memcmp(tags[i].tag, "pgap", 4) == 0) { break; } // Gapless Playback
|
||
if (memcmp(tags[i].tag, "rtng", 4) == 0) { break; } // Evaluation
|
||
}
|
||
if (id3tag.valid()) { info(*this, evt_id3data, "{}", id3tag.get()); }
|
||
if (strcmp(tags[i].descr, "Songtext") == 0) { lyricsBuffer.copy_from(&sa[24]); }
|
||
break;
|
||
}
|
||
}
|
||
} else
|
||
AUDIO_LOG_DEBUG("{} tag not supported", san);
|
||
|
||
if (consumed > len + m_m4aHdr.ilst_already_consumed) { // ILST > len can happen
|
||
m_m4aHdr.ilst_already_consumed += len;
|
||
m_m4aHdr.retvalue = consumed;
|
||
m_m4aHdr.headerSize += consumed;
|
||
m_controlCounter = M4A_ILST;
|
||
return 0;
|
||
}
|
||
|
||
if (m_m4aHdr.sizeof_ilst <= m_m4aHdr.ilst_pos) {
|
||
|
||
ps_ptr<char> tmp; // last todo if we have lyrics
|
||
ps_ptr<char> timestamp;
|
||
timestamp.alloc(12, "timestamp");
|
||
if (lyricsBuffer.valid()) {
|
||
lyricsBuffer.remove_prefix("LYRICS=");
|
||
idx = 0;
|
||
while (idx < lyricsBuffer.size()) {
|
||
int pos = lyricsBuffer.index_of('\n', idx);
|
||
if (pos == -1) break;
|
||
int len = pos - idx + 1;
|
||
tmp.copy_from(lyricsBuffer.get() + idx, len);
|
||
idx += len;
|
||
if (tmp.ends_with("\n")) tmp.truncate_at('\n');
|
||
// tmp content e.g.: [00:27.07]Jetzt kommt die Zeit
|
||
// [00:28.84]Auf die ihr euch freut
|
||
timestamp.copy_from(tmp.get(), 10);
|
||
|
||
int ms = parse_m4a_timestamp(timestamp.c_get());
|
||
|
||
// timestamp.remove_chars("[]:.");
|
||
// timestamp.append("0"); // to ms 0028840
|
||
tmp.remove_before(10, true);
|
||
if (tmp.strlen() > 0) {
|
||
m_syltLines.push_back(std::move(tmp));
|
||
m_syltTimeStamp.push_back(ms);
|
||
}
|
||
}
|
||
info(*this, evt_info, "audiofile contains synchronized lyrics");
|
||
// for(int i = 0; i < m_syltLines.size(); i++){
|
||
// info(*this, evt_lyrics, "{:07} ms, {}", m_syltTimeStamp[i], m_syltLines[i].c_get());
|
||
// }
|
||
}
|
||
m_m4aHdr.retvalue = consumed;
|
||
m_m4aHdr.headerSize += consumed;
|
||
m_controlCounter = M4A_META;
|
||
break;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_controlCounter == M4A_STSZ) {
|
||
uint8_t version = data[0];
|
||
(void)version;
|
||
uint32_t flags = bigEndian(data + 1, 3);
|
||
(void)flags;
|
||
uint32_t sample_size = bigEndian(data + 4, 4);
|
||
(void)sample_size;
|
||
m_m4aHdr.stsz_num_entries = bigEndian(data + 8, 4);
|
||
m_m4aHdr.stsz_table_pos = m_m4aHdr.headerSize + 12;
|
||
m_m4aHdr.retvalue += m_m4aHdr.sizeof_stsz + 8;
|
||
m_m4aHdr.headerSize += m_m4aHdr.sizeof_stsz + 8;
|
||
m_controlCounter = M4A_STBL;
|
||
return 0;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
uint8_t extLen = 0;
|
||
if (m_controlCounter == M4A_MDAT) { // mdat
|
||
// ps_ptr<char> hd;
|
||
// hd.copy_from((const char*)data, 30);
|
||
// hd.hex_dump(30);
|
||
m_audioDataSize = m_m4aHdr.sizeof_mdat; // length of this atom
|
||
|
||
// Extended Size
|
||
// 00 00 00 01 6D 64 61 74 00 00 00 00 00 00 16 64
|
||
// 0001 m d a t 5732
|
||
|
||
if (m_audioDataSize == 1) { // Extended Size
|
||
m_audioDataSize = bigEndian(data + 8, 8);
|
||
m_audioDataSize -= 16;
|
||
extLen = 8;
|
||
} else {
|
||
m_audioDataSize -= 8;
|
||
}
|
||
m_m4aHdr.retvalue = extLen;
|
||
m_m4aHdr.headerSize += extLen;
|
||
m_controlCounter = M4A_AMRDY; // last step before starting the audio
|
||
return 0;
|
||
}
|
||
|
||
if (m_controlCounter == M4A_AMRDY) { // almost ready
|
||
m_audioDataStart = m_m4aHdr.headerSize;
|
||
if (m_m4aHdr.picLen) {
|
||
size_t pos = m_audioFilePosition;
|
||
std::vector<uint32_t> vec;
|
||
vec.push_back(m_m4aHdr.picPos);
|
||
vec.push_back(m_m4aHdr.picLen);
|
||
info(*this, evt_image, vec);
|
||
vec.clear();
|
||
audioFileSeek(pos); // the filepointer could have been changed by the user, set it back
|
||
}
|
||
m_stsz_numEntries = m_m4aHdr.stsz_num_entries;
|
||
m_stsz_position = m_m4aHdr.stsz_table_pos;
|
||
info(*this, evt_info, "Audio-Data-Start: {}", m_audioDataStart);
|
||
info(*this, evt_info, "Audio-Length: {}", m_audioDataSize);
|
||
if (m_audioFileDuration) {
|
||
m_nominal_bitrate = (m_audioDataSize * 8) / m_audioFileDuration;
|
||
info(*this, evt_info, "Duration (s): {}", m_audioFileDuration);
|
||
}
|
||
|
||
m_controlCounter = M4A_OKAY; // that's all
|
||
return 0;
|
||
}
|
||
// this section should never be reached
|
||
AUDIO_LOG_ERROR("error");
|
||
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;
|
||
uint64_t current_timestamp = 0;
|
||
|
||
(void)m_f_unsync; // suppress -Wunused-variable
|
||
(void)current_timestamp; // suppress -Wunused-variable
|
||
|
||
if (specialIndexOf(packet, "ID3", 4) != 0) { // ID3 not found
|
||
// AUDIO_LOG_INFO("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;
|
||
// AUDIO_LOG_INFO("ID3 framesSize: {}", id3Size);
|
||
// AUDIO_LOG_INFO("ID3 version: 2.{}", ID3version);
|
||
|
||
if (m_f_exthdr) {
|
||
AUDIO_LOG_ERROR("ID3 extended header in m3u8 files not supported");
|
||
return 0;
|
||
}
|
||
// AUDIO_LOG_INFO("ID3 normal frames");
|
||
|
||
if (specialIndexOf(&packet[10], "PRIV", 5) != 0) { // tag PRIV not found
|
||
AUDIO_LOG_ERROR("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() {
|
||
m_f_lockInBuffer = true; // wait for the decoding to finish
|
||
uint8_t maxWait = 0;
|
||
uint32_t currTime = getAudioCurrentTime();
|
||
|
||
xSemaphoreTake(mutex_audioTaskIsDecoding, 1 * configTICK_RATE_HZ); // wait for audioTask is ready
|
||
{
|
||
if (m_f_running) {
|
||
m_f_running = false;
|
||
if (m_client->connected()) {
|
||
if (m_streamType == ST_WEBSTREAM) { info(*this, evt_info, "Closing web stream \"{}\"", m_lastHost.c_get()); }
|
||
if (m_streamType == ST_WEBFILE) { info(*this, evt_info, "Closing web file \"{}\"", m_lastHost.c_get()); }
|
||
m_client->stop();
|
||
}
|
||
if (m_audiofile) {
|
||
info(*this, evt_info, "Closing audio file \"{}\"", m_audiofile.name());
|
||
m_audiofile.close();
|
||
}
|
||
}
|
||
destroy_decoder();
|
||
m_f_lockInBuffer = false;
|
||
}
|
||
xSemaphoreGive(mutex_audioTaskIsDecoding);
|
||
return currTime;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::pauseResume() {
|
||
xSemaphoreTake(mutex_audioTask, 0.3 * configTICK_RATE_HZ);
|
||
bool retVal = false;
|
||
if (m_dataMode == AUDIO_LOCALFILE || m_streamType == ST_WEBSTREAM || m_streamType == ST_WEBFILE) {
|
||
m_f_running = !m_f_running;
|
||
retVal = true;
|
||
if (!m_f_running) {
|
||
m_outBuff.clear();
|
||
memset(m_resamplesBuff.get(), 0, m_resamplesBuffSize * sizeof(int16_t)); // Clear SamplesBuffer
|
||
m_validSamples = 0;
|
||
}
|
||
}
|
||
xSemaphoreGive(mutex_audioTask);
|
||
return retVal;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
audiolib::BiquadCoeffs Audio::makeButterworthLPF_Q31(float fs) { // Calculation of the biquad coefficients for the resampler
|
||
|
||
float fc = 0.45f * fs;
|
||
if (fc > 20000.0f) fc = 20000.0f; // absolute upper limit (security)
|
||
if (fc < 3000.0f) fc = 3000.0f; // absolute lower limit (prevents “thin” sound)
|
||
|
||
constexpr float Q = 0.70710678f;
|
||
|
||
float w0 = 2.0f * M_PI * fc / fs;
|
||
float cw = cosf(w0);
|
||
float sw = sinf(w0);
|
||
float alpha = sw / (2.0f * Q);
|
||
|
||
float b0 = (1.0f - cw) * 0.5f;
|
||
float b1 = 1.0f - cw;
|
||
float b2 = (1.0f - cw) * 0.5f;
|
||
float a0 = 1.0f + alpha;
|
||
float a1 = -2.0f * cw;
|
||
float a2 = 1.0f - alpha;
|
||
|
||
// normalize
|
||
b0 /= a0;
|
||
b1 /= a0;
|
||
b2 /= a0;
|
||
a1 /= a0;
|
||
a2 /= a0;
|
||
|
||
// convert to Q31
|
||
constexpr float Q31 = 2147483648.0f; // 2^31
|
||
|
||
audiolib::BiquadCoeffs c;
|
||
c.b0 = (int32_t)lrintf(b0 * Q31);
|
||
c.b1 = (int32_t)lrintf(b1 * Q31);
|
||
c.b2 = (int32_t)lrintf(b2 * Q31);
|
||
c.a1 = (int32_t)lrintf(a1 * Q31);
|
||
c.a2 = (int32_t)lrintf(a2 * Q31);
|
||
|
||
return c;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
uint32_t Audio::resampleI2Soutput(audiolib::resampler_t& rs, int32_t* input, uint32_t inputSamples, int32_t* output) {
|
||
|
||
auto lerp_q32 = [&](int32_t a, int32_t b, uint32_t frac) -> int32_t { return a + (int32_t)(((int64_t)(b - a) * frac) >> 32); };
|
||
auto biquadProcess = [&](audiolib::Biquad& s, const audiolib::BiquadCoeffs& c, int32_t x) -> int32_t {
|
||
// Q31 signal, Q31 coeffs → Q62 acc
|
||
int64_t acc = (int64_t)c.b0 * x + s.z1;
|
||
s.z1 = (int64_t)c.b1 * x - (int64_t)c.a1 * (acc >> 31) + s.z2;
|
||
s.z2 = (int64_t)c.b2 * x - (int64_t)c.a2 * (acc >> 31);
|
||
int64_t y = acc >> 31;
|
||
if (y > INT32_MAX) return INT32_MAX;
|
||
if (y < INT32_MIN) return INT32_MIN;
|
||
return (int32_t)y;
|
||
};
|
||
|
||
uint32_t outFrames = 0;
|
||
uint32_t i = 0;
|
||
|
||
// If we have a "last" from the previous frame, start with that
|
||
if (rs.hasLast && inputSamples > 0) {
|
||
int32_t l0 = rs.lastL;
|
||
int32_t r0 = rs.lastR;
|
||
int32_t l1 = input[0];
|
||
int32_t r1 = input[1];
|
||
|
||
// Continue processing with the old phase value
|
||
while ((rs.phase >> 32) == 0) {
|
||
uint32_t frac = (uint32_t)rs.phase;
|
||
int32_t l = lerp_q32(l0, l1, frac);
|
||
int32_t r = lerp_q32(r0, r1, frac);
|
||
|
||
l = biquadProcess(rs.lpLeft, rs.g_lpCoeffs, l);
|
||
r = biquadProcess(rs.lpRight, rs.g_lpCoeffs, r);
|
||
|
||
output[outFrames * 2] = l;
|
||
output[outFrames * 2 + 1] = r;
|
||
++outFrames;
|
||
|
||
rs.phase += rs.phaseStep;
|
||
}
|
||
rs.phase -= (1ULL << 32);
|
||
// i remains 0, we haven't used input[0] as "l0" yet!
|
||
}
|
||
|
||
// Rest of the frame as usual
|
||
for (; i + 1 < inputSamples; ++i) {
|
||
int32_t l0 = input[i * 2];
|
||
int32_t r0 = input[i * 2 + 1];
|
||
int32_t l1 = input[(i + 1) * 2];
|
||
int32_t r1 = input[(i + 1) * 2 + 1];
|
||
|
||
while ((rs.phase >> 32) == 0) {
|
||
uint32_t frac = (uint32_t)rs.phase;
|
||
int32_t l = lerp_q32(l0, l1, frac);
|
||
int32_t r = lerp_q32(r0, r1, frac);
|
||
|
||
l = biquadProcess(rs.lpLeft, rs.g_lpCoeffs, l);
|
||
r = biquadProcess(rs.lpRight, rs.g_lpCoeffs, r);
|
||
|
||
output[outFrames * 2] = l;
|
||
output[outFrames * 2 + 1] = r;
|
||
++outFrames;
|
||
|
||
rs.phase += rs.phaseStep;
|
||
}
|
||
rs.phase -= (1ULL << 32);
|
||
}
|
||
|
||
// Save last sample for next frame
|
||
if (inputSamples > 0) {
|
||
rs.lastL = input[(inputSamples - 1) * 2];
|
||
rs.lastR = input[(inputSamples - 1) * 2 + 1];
|
||
rs.hasLast = true;
|
||
}
|
||
|
||
return outFrames;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void IRAM_ATTR Audio::playChunk() {
|
||
if (m_validSamples == 0) return; // nothing to do
|
||
|
||
bool continueI2S = true;
|
||
m_plCh.i2s_bytesConsumed = 0;
|
||
m_plCh.err = ESP_OK;
|
||
|
||
constexpr int BYTES_PER_FRAME = 2 * sizeof(int32_t);
|
||
|
||
if (m_plCh.count > 0) goto i2swrite; // Not all samples could be written to I2S during the last run
|
||
audio_process_raw_samples(m_outBuff.get(), m_validSamples);
|
||
//------------------------------------------------------------------------------------------
|
||
{
|
||
const bool applyGain = settings.VOLUME_CONTROL && (m_audio_items.limiter[LEFTCHANNEL] != 1.0f || m_audio_items.limiter[RIGHTCHANNEL] != 1.0f);
|
||
for (int i = 0; i < m_validSamples; i++) {
|
||
if (settings.VU_LEVEL) calculateVUlevel(&m_outBuff[i * 2]);
|
||
if (settings.IIR_FILTER) IIR_filter(&m_outBuff[i * 2]);
|
||
if (applyGain) Gain(&m_outBuff[i * 2]);
|
||
}
|
||
}
|
||
if (settings.SPECTRUM) processSpectrum();
|
||
if (m_f_forceMono) stereo2mono(m_outBuff.get(), m_validSamples);
|
||
//------------------------------------------------------------------------------------------
|
||
if (m_output_sr && m_output_sr != m_i2s_items.sampleRate) {
|
||
m_validSamples = resampleI2Soutput(m_resampler, m_outBuff.get(), m_validSamples, m_resamplesBuff.get()); // have new amount of samples
|
||
audio_process_i2s(m_resamplesBuff.get(), m_validSamples, &continueI2S); // resampled stereo 32bps
|
||
} else {
|
||
audio_process_i2s(m_outBuff.get(), (int32_t)m_validSamples, &continueI2S);
|
||
}
|
||
//------------------------------------------------------------------------------------------------------
|
||
if (!continueI2S) {
|
||
m_validSamples = 0;
|
||
m_plCh.count = 0;
|
||
return;
|
||
}
|
||
//------------------------------------------------------------------------------------------------------
|
||
|
||
i2swrite:
|
||
if (m_output_sr && m_output_sr != m_i2s_items.sampleRate) { // with resampler
|
||
m_plCh.err = i2s_channel_write(m_i2s_tx_handle, m_resamplesBuff.get() + m_plCh.count, m_validSamples * BYTES_PER_FRAME, &m_plCh.i2s_bytesConsumed, 50);
|
||
} else { // without resampler
|
||
m_plCh.err = i2s_channel_write(m_i2s_tx_handle, m_outBuff.get() + m_plCh.count, m_validSamples * BYTES_PER_FRAME, &m_plCh.i2s_bytesConsumed, 20);
|
||
}
|
||
|
||
if (!(m_plCh.err == ESP_OK || m_plCh.err == ESP_ERR_TIMEOUT)) goto exit;
|
||
m_validSamples -= m_plCh.i2s_bytesConsumed / BYTES_PER_FRAME;
|
||
m_plCh.count += m_plCh.i2s_bytesConsumed / 2;
|
||
if (m_validSamples <= 0) {
|
||
m_validSamples = 0;
|
||
m_plCh.count = 0;
|
||
}
|
||
|
||
// ---- statistics, bytes written to I2S (every 10s)
|
||
// static int cnt = 0;
|
||
// static uint32_t t = millis();
|
||
|
||
// if(t + 10000 < millis()){
|
||
// AUDIO_LOG_INFO("{}", cnt);
|
||
// cnt = 0;
|
||
// t = millis();
|
||
// }
|
||
// cnt+= i2s_bytesConsumed;
|
||
//-------------------------------------------
|
||
|
||
return;
|
||
exit:
|
||
if (m_plCh.err == ESP_OK)
|
||
return;
|
||
else if (m_plCh.err == ESP_ERR_INVALID_ARG)
|
||
AUDIO_LOG_ERROR("NULL pointer or this handle is not tx handle");
|
||
else if (m_plCh.err == ESP_ERR_TIMEOUT)
|
||
AUDIO_LOG_ERROR("Writing timeout, no writing event received from ISR within ticks_to_wait");
|
||
else if (m_plCh.err == ESP_ERR_INVALID_STATE)
|
||
AUDIO_LOG_ERROR("I2S is not ready to write");
|
||
else
|
||
AUDIO_LOG_ERROR("i2s err {}", m_plCh.err);
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::loop() {
|
||
get_info();
|
||
if (!m_f_running) return;
|
||
|
||
if (m_f_firstLoop) {
|
||
m_f_firstLoop = false;
|
||
memset(&m_lVar, 0, sizeof(m_lVar));
|
||
}
|
||
|
||
if (m_playlistFormat != FORMAT_M3U8) { // normal process
|
||
switch (m_dataMode) {
|
||
case AUDIO_LOCALFILE: processLocalFile(); break;
|
||
case HTTP_RESPONSE_HEADER:
|
||
if (!parseHttpResponseHeader()) {
|
||
if (m_f_timeout && m_lVar.count < 3) {
|
||
m_f_timeout = false;
|
||
m_lVar.count++;
|
||
connecttohost(m_lastHost.get());
|
||
}
|
||
} else {
|
||
m_lVar.count = 0;
|
||
}
|
||
break;
|
||
case AUDIO_PLAYLISTINIT: readPlayListData(); break;
|
||
case AUDIO_PLAYLISTDATA:
|
||
if (m_playlistFormat == FORMAT_M3U) httpPrint(parsePlaylist_M3U());
|
||
if (m_playlistFormat == FORMAT_PLS) httpPrint(parsePlaylist_PLS());
|
||
if (m_playlistFormat == FORMAT_ASX) httpPrint(parsePlaylist_ASX());
|
||
break;
|
||
case AUDIO_DATA:
|
||
if (m_streamType == ST_WEBSTREAM) processWebStream();
|
||
if (m_streamType == ST_WEBFILE) processWebFile();
|
||
break;
|
||
}
|
||
} else { // m3u8 datastream only
|
||
ps_ptr<char> host;
|
||
switch (m_dataMode) {
|
||
case HTTP_RESPONSE_HEADER:
|
||
if (!parseHttpResponseHeader()) {
|
||
if (m_lVar.count < 3) {
|
||
m_lVar.count++;
|
||
connecttohost(m_lastHost.get());
|
||
} else {
|
||
stopSong();
|
||
}
|
||
} else {
|
||
m_lVar.count = 0;
|
||
m_f_firstCall = true; // read ID3 header in processWebStreamHLS/TS()
|
||
}
|
||
break;
|
||
case AUDIO_PLAYLISTINIT:
|
||
if (readPlayListData())
|
||
break;
|
||
else { // readPlayListData == false means connect to m3u8 URL
|
||
httpPrint(m_m3u8_host.get());
|
||
m_dataMode = HTTP_RESPONSE_HEADER; // we have a new playlist now
|
||
break;
|
||
}
|
||
case AUDIO_PLAYLISTDATA:
|
||
host = parsePlaylist_M3U8();
|
||
if (host.valid()) { // host contains the next playlist URL
|
||
httpPrint(host.get());
|
||
m_dataMode = HTTP_RESPONSE_HEADER;
|
||
} else { // host == NULL means connect to m3u8 URL
|
||
if (m_lVar.no_host_timer > millis()) {
|
||
// AUDIO_LOG_DEBUG("wait");
|
||
break;
|
||
}
|
||
if (m_f_stream) m_lVar.no_host_timer = millis() + 10000;
|
||
httpPrint(m_m3u8_host.get());
|
||
m_dataMode = HTTP_RESPONSE_HEADER; // we have a new playlist now
|
||
}
|
||
break;
|
||
case AUDIO_DATA:
|
||
if (m_f_ts) {
|
||
processWebStreamTS(); // aac, mp3 or aacp with ts packets
|
||
} else {
|
||
processWebStreamHLS(); // aac, mp3 or aacp normal stream
|
||
}
|
||
|
||
if (m_f_continue) { // at this point m_f_continue is true, means processWebStream() needs more data
|
||
m_dataMode = AUDIO_PLAYLISTDATA;
|
||
m_f_continue = false;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::readPlayListData() {
|
||
|
||
int32_t chunkLen = 0;
|
||
uint16_t readedBytes = 0;
|
||
ps_ptr<char> pl;
|
||
uint32_t ctl = 0;
|
||
size_t plSize = 0;
|
||
|
||
auto detectTimeout = [&]() -> bool {
|
||
uint32_t t = millis();
|
||
while (!m_client->available()) {
|
||
vTaskDelay(2);
|
||
if (t + 2000 < millis()) {
|
||
AUDIO_LOG_WARN("Playlist is incomplete, fetch again");
|
||
if (m_f_chunked) getChunkSize(0, true);
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
};
|
||
|
||
if (m_dataMode != AUDIO_PLAYLISTINIT) {
|
||
AUDIO_LOG_ERROR("wrong datamode {}", dataModeStr[m_dataMode]);
|
||
goto exit;
|
||
}
|
||
|
||
if (m_f_chunked) {
|
||
getChunkSize(0, true);
|
||
chunkLen = getChunkSize(&readedBytes);
|
||
if (chunkLen <= 0) {
|
||
AUDIO_LOG_ERROR("chunked datatransfer but chunkLen is invalid");
|
||
goto exit;
|
||
}
|
||
plSize = chunkLen;
|
||
} else {
|
||
plSize = m_audioFileSize;
|
||
}
|
||
|
||
if (!plSize) { // maybe playlist without contentLength or chunkSize
|
||
AUDIO_LOG_ERROR("file size is not given");
|
||
goto exit;
|
||
}
|
||
|
||
pl.alloc(2048, "pl");
|
||
// delete all memory in m_playlistContent
|
||
if (m_playlistFormat == FORMAT_M3U8 && !psramFound()) { AUDIO_LOG_ERROR("m3u8 playlists requires PSRAM enabled!"); }
|
||
vector_clear_and_shrink(m_playlistContent);
|
||
|
||
while (true) { // outer while
|
||
uint32_t ctime = millis();
|
||
uint32_t timeout = 2000; // ms
|
||
|
||
pl.clear(); // playlistLine
|
||
|
||
while (true) { // inner while
|
||
uint16_t pos = 0;
|
||
while (true) { // super inner while :-))
|
||
uint32_t t = millis();
|
||
if (ctl == plSize) break;
|
||
if (detectTimeout()) goto exit;
|
||
pl[pos] = audioFileRead();
|
||
ctl++;
|
||
if (pl[pos] == '\n') {
|
||
pl[pos] = '\0';
|
||
pos++;
|
||
break;
|
||
}
|
||
if (pl[pos] == '\r') {
|
||
pl[pos] = '\0';
|
||
pos++;
|
||
continue;
|
||
}
|
||
pos++;
|
||
if (pos == 2044) {
|
||
pos--;
|
||
continue;
|
||
}
|
||
if (ctl == plSize) {
|
||
pl[pos] = '\0';
|
||
break;
|
||
}
|
||
}
|
||
if (pos) {
|
||
pl[pos] = '\0';
|
||
break;
|
||
}
|
||
if (ctl == plSize) break;
|
||
if (detectTimeout()) goto exit;
|
||
} // inner while
|
||
AUDIO_LOG_DEBUG("PL: {}", pl.c_get());
|
||
if (pl.starts_with_icase("<!DOCTYPE")) {
|
||
AUDIO_LOG_WARN("url is a webpage!");
|
||
goto exit;
|
||
}
|
||
if (pl.starts_with_icase("<html")) {
|
||
AUDIO_LOG_WARN("url is a webpage!");
|
||
goto exit;
|
||
}
|
||
|
||
//--------------------------------------------------------------------------------------------------------------
|
||
if (pl.starts_with_icase("#EXT-X-SERVER-CONTROL")) {
|
||
/* The parameter "?_HLS_skip=YES" is an HLS delivery directive that allows a client to request a playlist delta update from the server in order to optimise data transmission.
|
||
Functionality: The server replaces outdated media segments that lie beyond the "skip boundary" with an #EXT-X-SKIP tag, rather than resending the entire playlist content.
|
||
Prerequisites: The playlist must contain an #EXT-X-SERVER-CONTROL tag with the CAN-SKIP-UNTIL attribute and must not be an #EXT-X-ENDLIST tag (i.e. a live stream).
|
||
Effect: This drastically reduces the size of the playlist file and speeds up parsing, which is particularly critical for low-latency HLS with short segments and large DVR windows
|
||
in order to save bandwidth and avoid playback disruptions.
|
||
*/
|
||
if (m_playlistFormat == FORMAT_M3U8) {
|
||
if (!m_m3u8_host.ends_with("HLS_skip=YES")) {
|
||
AUDIO_LOG_DEBUG("#EXT-X-SERVER-CONTROL found");
|
||
m_m3u8_host.append("?_HLS_skip=YES");
|
||
m_client->stop();
|
||
httpPrint(m_m3u8_host.c_get());
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
//--------------------------------------------------------------------------------------------------------------
|
||
|
||
// AUDIO_LOG_INFO("current playlist line: {}", pl.get());
|
||
if (pl.size() > 0) m_playlistContent.emplace_back(pl);
|
||
|
||
// 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 == plSize) {
|
||
if (m_f_chunked) {
|
||
chunkLen = getChunkSize(&readedBytes); // expected: "\r\n\0\r\n\r\n" if ready
|
||
if (chunkLen > 0) {
|
||
plSize += chunkLen;
|
||
continue; // next round
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
} // outer while
|
||
|
||
m_dataMode = AUDIO_PLAYLISTDATA;
|
||
return true;
|
||
|
||
exit:
|
||
vector_clear_and_shrink(m_playlistContent);
|
||
getChunkSize(0, true);
|
||
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++) {
|
||
// m_playlistContent[i].println();
|
||
if (m_playlistContent[i].contains("#EXTINF:")) { // Info?
|
||
pos = m_playlistContent[i].index_of(","); // Comma in this line?
|
||
if (pos > 0) {
|
||
// Show artist and title if present in metadata
|
||
info(*this, evt_id3data, "{}", m_playlistContent[i].get() + pos + 1);
|
||
}
|
||
continue;
|
||
}
|
||
if (m_playlistContent[i].starts_with("#")) { // Commentline?
|
||
continue;
|
||
}
|
||
|
||
pos = m_playlistContent[i].index_of("http://:@", 0); // ":@"?? remove that!
|
||
if (pos >= 0) {
|
||
AUDIO_LOG_INFO("Entry in playlist found: {}", (m_playlistContent[i].get() + pos + 9));
|
||
host = m_playlistContent[i].get() + pos + 9;
|
||
break;
|
||
}
|
||
// AUDIO_LOG_INFO("Entry in playlist found: {}", pl);
|
||
pos = m_playlistContent[i].index_of("http", 0); // Search for "http"
|
||
if (pos >= 0) { // Does URL contain "http://"?
|
||
// AUDIO_LOG_ERROR("{} pos={}", m_playlistContent[i], pos);
|
||
host = m_playlistContent[i].get() + 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 (m_playlistContent[0].strlen() == 0) goto exit; // empty line
|
||
if (!m_playlistContent[0].starts_with("[playlist]")) { // first entry in valid pls
|
||
m_dataMode = HTTP_RESPONSE_HEADER; // pls is not valid
|
||
AUDIO_LOG_INFO("Playlist is not valid, switch to HTTP_RESPONSE_HEADER");
|
||
goto exit;
|
||
}
|
||
continue;
|
||
}
|
||
if (m_playlistContent[i].starts_with("File1")) {
|
||
if (host) continue; // we have already a url
|
||
pos = m_playlistContent[i].index_of("http", 0); // File1=http://streamplus30.leonex.de:14840/;
|
||
if (pos >= 0) { // yes, URL contains "http"?
|
||
host = m_playlistContent[i].get() + pos; // Now we have an URL for a stream in host.
|
||
}
|
||
continue;
|
||
}
|
||
if (m_playlistContent[i].starts_with("Title1")) { // Title1=Antenne Tirol
|
||
const char* plsStationName = (m_playlistContent[i].get() + 7);
|
||
info(*this, evt_name, "{}", plsStationName);
|
||
continue;
|
||
}
|
||
if (m_playlistContent[i].starts_with("Length1")) { continue; }
|
||
if (m_playlistContent[i].contains("Invalid username")) { // Unable to access account:
|
||
goto exit; // Invalid username or password
|
||
}
|
||
}
|
||
return host;
|
||
|
||
exit:
|
||
m_f_running = false;
|
||
stopSong();
|
||
vector_clear_and_shrink(m_playlistContent);
|
||
m_dataMode = 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 = m_playlistContent[i].index_of("<", 0);
|
||
int p2 = m_playlistContent[i].index_of(">", 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 (m_playlistContent[i].contains("<entry>")) f_entry = true; // found entry tag (returns -1 if not found)
|
||
if (f_entry) {
|
||
if (m_playlistContent[i].contains("ref href")) { // <ref href="http://87.98.217.63:24112/stream" />
|
||
pos = m_playlistContent[i].index_of("http", 0);
|
||
if (pos > 0) {
|
||
host = (m_playlistContent[i].get() + 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 = m_playlistContent[i].index_of("<title>", 0);
|
||
if (pos >= 0) {
|
||
char* plsStationName = (m_playlistContent[i].get() + pos + 7); // remove <Title>
|
||
pos = indexOf(plsStationName, "</", 0);
|
||
if (pos >= 0) {
|
||
*(plsStationName + pos) = 0; // remove </Title>
|
||
}
|
||
info(*this, evt_name, "{}", plsStationName);
|
||
}
|
||
|
||
if (m_playlistContent[i].starts_with("http") && !f_entry) { // url only in asx
|
||
host = m_playlistContent[i].get();
|
||
}
|
||
}
|
||
return host;
|
||
}
|
||
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
||
uint16_t Audio::accomplish_m3u8_url() {
|
||
|
||
for (uint16_t i = 0; i < m_linesWithURL.size(); i++) {
|
||
|
||
ps_ptr<char> tmp;
|
||
|
||
if (!m_linesWithURL[i].starts_with("http")) { // playlist: http://station.com/aaa/bbb/xxx.m3u8
|
||
// chunklist: http://station.com/aaa/bbb/ddd.aac
|
||
// result: http://station.com/aaa/bbb/ddd.aac
|
||
tmp = m_m3u8_host;
|
||
if (m_linesWithURL[i][0] != '/') { // playlist: http://station.com/aaa/bbb/xxx.m3u8 // tmp
|
||
// chunklist: ddd.aac // m_linesWithURL[i]
|
||
// result: http://station.com/aaa/bbb/ddd.aac // m_linesWithURL[i]
|
||
int idx = tmp.last_index_of('/');
|
||
tmp[idx + 1] = '\0';
|
||
tmp.append(m_linesWithURL[i].get());
|
||
m_linesWithURL[i].clone_from(tmp);
|
||
continue;
|
||
} else { // playlist: http://station.com/aaa/bbb/xxx.m3u8
|
||
// chunklist: /aaa/bbb/ddd.aac
|
||
// result: http://station.com/aaa/bbb/ddd.aac
|
||
int idx = tmp.index_of('/', 8);
|
||
tmp[idx] = '\0';
|
||
tmp.append(m_linesWithURL[i].get());
|
||
m_linesWithURL[i].clone_from(tmp);
|
||
continue;
|
||
}
|
||
} else { /* nothing todo*/
|
||
continue;
|
||
}
|
||
}
|
||
// for(uint16_t i = 0; i < m_linesWithURL.size(); i++) m_linesWithURL[i].println();
|
||
return 0;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
||
int16_t Audio::prepare_first_m3u8_url(ps_ptr<char>& playlistBuff) {
|
||
// in m_lineswithURL, searches for the first new URL through the recent URL.
|
||
// URLs that have already been used are removed from the DEQUE
|
||
// If all URLs are new, there is nothing to do and 0 is returned.
|
||
// no new URL is found -1 returned
|
||
int idx = -1;
|
||
for (int i = 0; i < m_linesWithURL.size(); i++) {
|
||
if (playlistBuff.equals(m_linesWithURL[i].get())) idx = i;
|
||
}
|
||
if (idx == -1) {
|
||
AUDIO_LOG_DEBUG("nothing found, all entries are new");
|
||
return 0;
|
||
}
|
||
if (idx == m_linesWithURL.size()) {
|
||
AUDIO_LOG_DEBUG("only last entry found, nothing is new");
|
||
return -1;
|
||
}
|
||
for (int j = 0; j < idx + 1; j++) { m_linesWithURL.pop_front(); }
|
||
AUDIO_LOG_DEBUG("{} entries are known and removed", idx + 1);
|
||
return idx + 1;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
||
ps_ptr<char> Audio::parsePlaylist_M3U8() {
|
||
// 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
|
||
|
||
// #EXTM3U
|
||
// #EXT-X-VERSION:3
|
||
// #EXT-X-MEDIA-SEQUENCE:0
|
||
// #EXT-X-TARGETDURATION:4
|
||
// #EXTINF:3.997,90s90s - Rock
|
||
// #EXT-X-PROGRAM-DATE-TIME:2025-08-17T14:08:21.088877044Z
|
||
// https://hz71.streamabc.net/hls/d2gu4l4peavc72tgotd0/regc-90s90srock1436287-mp3-192-2191420/0.mp3
|
||
// #EXTINF:3.997,90s90s - Rock
|
||
// #EXT-X-PROGRAM-DATE-TIME:2025-08-17T14:08:24.088877044Z
|
||
// https://hz71.streamabc.net/hls/d2gu4l4peavc72tgotd0/regc-90s90srock1436287-mp3-192-2191420/1.mp3
|
||
// #EXTINF:3.997,90s90s - Rock
|
||
// #EXT-X-PROGRAM-DATE-TIME:2025-08-17T14:08:28.088877044Z
|
||
// https://hz71.streamabc.net/hls/d2gu4l4peavc72tgotd0/regc-90s90srock1436287-mp3-192-2191420/2.mp3
|
||
// #EXTINF:3.997,90s90s - Rock
|
||
// #EXT-X-PROGRAM-DATE-TIME:2025-08-17T14:08:32.088877044Z
|
||
// https://hz71.streamabc.net/hls/d2gu4l4peavc72tgotd0/regc-90s90srock1436287-mp3-192-2191420/3.mp3
|
||
|
||
if (!m_lastHost.valid()) {
|
||
AUDIO_LOG_ERROR("m_lastHost is NULL");
|
||
return {};
|
||
} // guard
|
||
|
||
uint32_t lines = m_playlistContent.size();
|
||
bool f_haveRedirection = false;
|
||
|
||
if (lines) {
|
||
bool addNextLine = false;
|
||
deque_clear_and_shrink(m_linesWithURL);
|
||
vector_clear_and_shrink(m_linesWithEXTINF);
|
||
for (uint32_t i = 0; i < lines; i++) {
|
||
// AUDIO_LOG_INFO("pl{} = {}", i, m_playlistContent[i].get());
|
||
if (m_playlistContent[i].starts_with("#EXT-X-STREAM-INF:")) { f_haveRedirection = true; /*AUDIO_LOG_ERROR("we have a redirection");*/ }
|
||
if (addNextLine) {
|
||
if (m_playlistContent[i].starts_with("#EXT-X-PROGRAM-DATE-TIME:")) continue; // skip this line
|
||
addNextLine = false;
|
||
// size_t len = strlen(linesWithSeqNr[idx].get()) + strlen(m_playlistContent[i].get()) + 1;
|
||
m_linesWithURL.emplace_back(m_playlistContent[i]);
|
||
}
|
||
if (m_playlistContent[i].starts_with("#EXTINF:")) {
|
||
m_linesWithEXTINF.emplace_back(m_playlistContent[i]);
|
||
addNextLine = true;
|
||
}
|
||
}
|
||
if (!f_haveRedirection) {
|
||
accomplish_m3u8_url();
|
||
prepare_first_m3u8_url(m_playlistBuff);
|
||
vector_clear_and_shrink(m_playlistContent);
|
||
}
|
||
}
|
||
|
||
for (int i = 0; i < m_linesWithURL.size(); i++) { /*AUDIO_LOG_INFO("{}", m_linesWithURL[i].get())*/
|
||
;
|
||
}
|
||
for (int i = 0; i < m_linesWithEXTINF.size(); i++) { /*AUDIO_LOG_INFO("{}", m_linesWithEXTINF[i].get());*/
|
||
showstreamtitle(m_linesWithEXTINF[i].get());
|
||
}
|
||
|
||
if (f_haveRedirection) {
|
||
m_m3u8_host = m3u8redirection(&m_m3u8Codec);
|
||
vector_clear_and_shrink(m_playlistContent);
|
||
return {};
|
||
}
|
||
|
||
if (m_codec == CODEC_NONE) {
|
||
m_codec = CODEC_AAC;
|
||
if (m_m3u8Codec == CODEC_MP3) m_codec = CODEC_MP3;
|
||
} // if we have no redirection
|
||
//----------------------------------------------------------------------------------------------------------------------------------------------------
|
||
|
||
if (m_linesWithURL.size() > 0) {
|
||
ps_ptr<char> playlistBuff;
|
||
if (m_linesWithURL[0].valid()) {
|
||
playlistBuff = m_linesWithURL[0];
|
||
m_linesWithURL.pop_front();
|
||
m_linesWithURL.shrink_to_fit();
|
||
}
|
||
AUDIO_LOG_DEBUG("now playing {}", playlistBuff.get());
|
||
if (playlistBuff.ends_with("ts")) m_f_ts = true;
|
||
if (playlistBuff.contains(".ts?") > 0) m_f_ts = true;
|
||
m_playlistBuff = playlistBuff;
|
||
return playlistBuff;
|
||
}
|
||
return {};
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
|
||
ps_ptr<char> Audio::m3u8redirection(uint8_t* codec) {
|
||
// example: redirection
|
||
// #EXTM3U
|
||
// #EXT-X-STREAM-INF:BANDWIDTH=117500,AVERAGE-BANDWIDTH=117000,CODECS="mp4a.40.2"
|
||
// 112/playlist.m3u8?hlssid=7562d0e101b84aeea0fa35f8b963a174
|
||
// #EXT-X-STREAM-INF:BANDWIDTH=69500,AVERAGE-BANDWIDTH=69000,CODECS="mp4a.40.5"
|
||
// 64/playlist.m3u8?hlssid=7562d0e101b84aeea0fa35f8b963a174
|
||
// #EXT-X-STREAM-INF:BANDWIDTH=37500,AVERAGE-BANDWIDTH=37000,CODECS="mp4a.40.29"
|
||
// 32/playlist.m3u8?hlssid=7562d0e101b84aeea0fa35f8b963a174
|
||
|
||
if (!m_lastHost.valid()) {
|
||
AUDIO_LOG_ERROR("m_lastHost is empty");
|
||
return {};
|
||
} // guard
|
||
const char codecString[9][11] = {
|
||
"mp4a.40.34", // mp3 stream
|
||
"mp4a.40.01", // AAC Main
|
||
"mp4a.40.2", // MPEG-4 AAC LC
|
||
"mp4a.40.02", // MPEG-4 AAC LC, leading 0 for Aud-OTI compatibility
|
||
"mp4a.40.29", // MPEG-4 HE-AAC v2 (AAC LC + SBR + PS)
|
||
"mp4a.40.42", // xHE-AAC
|
||
"mp4a.40.5", // MPEG-4 HE-AAC v1 (AAC LC + SBR)
|
||
"mp4a.40.05", // MPEG-4 HE-AAC v1 (AAC LC + SBR), leading 0 for Aud-OTI compatibility
|
||
"mp4a.67", // MPEG-2 AAC LC
|
||
};
|
||
|
||
uint16_t choosenLine = 0;
|
||
uint16_t plcSize = m_playlistContent.size();
|
||
int8_t cS = 100;
|
||
|
||
for (uint16_t i = 0; i < plcSize; i++) { // looking for lowest codeString
|
||
if (m_playlistContent[i].contains("CODECS=\"mp4a")) {
|
||
for (uint8_t j = 0; j < 9; j++) {
|
||
if (m_playlistContent[i].contains(codecString[j])) {
|
||
if (j < cS) {
|
||
cS = j;
|
||
choosenLine = i;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (cS == 0)
|
||
*codec = CODEC_MP3;
|
||
else if (cS < 100)
|
||
*codec = CODEC_AAC;
|
||
|
||
if (cS == 100) { // "mp4a.xx.xx" not found
|
||
*codec = CODEC_AAC; // assume AAC
|
||
for (uint16_t i = 0; i < plcSize; i++) { // we have no codeString, looking for "http"
|
||
if (m_playlistContent[i].contains("#EXT-X-STREAM-INF")) { choosenLine = i; }
|
||
}
|
||
}
|
||
|
||
choosenLine++; // next line is the redirection url
|
||
|
||
ps_ptr<char> result;
|
||
ps_ptr<char> line;
|
||
line.clone_from(m_playlistContent[choosenLine]);
|
||
|
||
if (line.starts_with("../")) {
|
||
// ../../2093120-b/RISMI/stream01/streamPlaylist.m3u8
|
||
while (line.starts_with("../")) { line.remove_prefix("../"); }
|
||
result.clone_from(m_currentHost);
|
||
int idx = result.last_index_of('/');
|
||
if (idx > 0 && (size_t)idx != result.strlen() - 1) result.truncate_at((size_t)idx + 1);
|
||
result.append(line.get());
|
||
} else if (!line.starts_with_icase("http")) {
|
||
|
||
// http://arcast.com.ar:1935/radio/radionar.stream/playlist.m3u8 m_lastHost
|
||
// chunklist_w789468022.m3u8 line
|
||
// http://arcast.com.ar:1935/radio/radionar.stream/chunklist_w789468022.m3u8 --> result
|
||
|
||
result.clone_from(m_currentHost);
|
||
int pos = m_currentHost.last_index_of('/');
|
||
if (pos > 0) {
|
||
result.truncate_at((size_t)pos + 1);
|
||
result.append(line.get());
|
||
}
|
||
} else {
|
||
result.clone_from(line);
|
||
}
|
||
|
||
return result; // it's a redirection, a new m3u8 playlist
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::processLocalFile() {
|
||
if (!(m_audiofile && m_f_running && m_dataMode == AUDIO_LOCALFILE)) return; // guard
|
||
|
||
m_prlf.availableBytes = 0;
|
||
m_prlf.bytesAddedToBuffer = 0;
|
||
|
||
if (m_f_firstCall) { // runs only one time per connection, prepare for start
|
||
m_f_firstCall = false;
|
||
m_f_stream = false;
|
||
m_prlf.audioHeaderFound = false;
|
||
m_prlf.newFilePos = 0;
|
||
m_prlf.ctime = millis();
|
||
m_audioFilePosition = 0;
|
||
m_audioDataSize = m_audioFileSize;
|
||
m_audioDataStart = 0;
|
||
m_f_allDataReceived = false;
|
||
m_prlf.timeout = 8000; // ms
|
||
}
|
||
|
||
if (m_resumeFilePos >= 0) { // we have a resume file position
|
||
m_prlf.newFilePos = newInBuffStart(m_resumeFilePos);
|
||
if (m_prlf.newFilePos < 0) AUDIO_LOG_WARN("skip to new position was not successful");
|
||
m_haveNewFilePos = m_prlf.newFilePos;
|
||
m_audioDataReadPtr = m_prlf.newFilePos;
|
||
m_resumeFilePos = -1;
|
||
m_f_allDataReceived = false;
|
||
return;
|
||
}
|
||
|
||
m_prlf.availableBytes = min(InBuff.writeSpace(), (size_t)(m_audioFileSize - m_audioFilePosition));
|
||
m_prlf.bytesAddedToBuffer = audioFileRead(InBuff.getWritePtr(), min(m_prlf.availableBytes, (uint32_t)UINT16_MAX));
|
||
if (m_prlf.bytesAddedToBuffer > 0) { InBuff.bytesWritten(m_prlf.bytesAddedToBuffer); }
|
||
if (m_audioDataSize && m_audioFilePosition >= m_audioDataSize) {
|
||
if (!m_f_allDataReceived) m_f_allDataReceived = true;
|
||
}
|
||
if (!m_audioDataSize && m_audioFilePosition == m_audioFileSize) {
|
||
if (!m_f_allDataReceived) m_f_allDataReceived = true;
|
||
}
|
||
// AUDIO_LOG_DEBUG("m_audioFilePosition {} >= m_audioDataSize {}, m_f_allDataReceived {}", m_audioFilePosition, m_audioDataSize, m_f_allDataReceived);
|
||
|
||
if (!m_decoder && InBuff.bufferFilled() > 127) {
|
||
if (!initializeDecoder()) return;
|
||
m_prlf.maxFrameSize = InBuff.getMaxBlockSize();
|
||
}
|
||
|
||
if (!m_f_stream) {
|
||
if (m_controlCounter != 100) {
|
||
if (m_f_ogg) { m_controlCounter = 100; }
|
||
if ((millis() - m_prlf.ctime) > m_prlf.timeout) {
|
||
AUDIO_LOG_ERROR("audioHeader reading timeout");
|
||
m_f_running = false;
|
||
goto exit;
|
||
}
|
||
if (InBuff.bufferFilled() > m_prlf.maxFrameSize || (InBuff.bufferFilled() == m_audioFileSize) || m_f_allDataReceived) { // at least one complete frame or the file is smaller
|
||
InBuff.bytesWasRead(readAudioHeader(InBuff.readSpace()));
|
||
}
|
||
if (m_controlCounter == 100) {
|
||
if (m_audioDataStart > 0) { m_prlf.audioHeaderFound = true; }
|
||
if (!m_audioDataSize) m_audioDataSize = m_audioFileSize;
|
||
}
|
||
return;
|
||
} else {
|
||
m_f_stream = true;
|
||
info(*this, evt_info, "stream ready");
|
||
}
|
||
}
|
||
|
||
if (m_fileStartTime > 0 && m_nominal_bitrate) {
|
||
if (getBitRate() > 0)
|
||
setAudioPlayTime(m_fileStartTime);
|
||
else
|
||
info(*this, evt_info, "can't set audio play time directly");
|
||
m_fileStartTime = -1;
|
||
}
|
||
|
||
// end of file reached? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_f_eof) { // m_f_eof and m_f_ID3v1TagFound will be set in playAudioData()
|
||
if (m_f_ID3v1TagFound) readID3V1Tag();
|
||
exit:
|
||
ps_ptr<char> afn; // audio file name
|
||
if (m_audiofile) afn.assign(m_audiofile.name()); // store temporary the name
|
||
m_audioCurrentTime = 0;
|
||
m_audioFileDuration = 0;
|
||
m_resumeFilePos = -1;
|
||
m_haveNewFilePos = 0;
|
||
m_codec = CODEC_NONE;
|
||
stopSong();
|
||
|
||
if (afn.valid()) { info(*this, evt_eof, "{}", afn.c_get()); }
|
||
return;
|
||
}
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
||
void Audio::processWebStream() {
|
||
if (m_dataMode != AUDIO_DATA) return; // guard
|
||
uint16_t readedBytes = 0;
|
||
|
||
m_pwst.availableBytes = 0; // available from stream
|
||
m_pwst.f_clientIsConnected = m_client->connected();
|
||
m_pwst.writeSpace = UINT16_MAX;
|
||
|
||
// first call, set some values to default - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_f_firstCall) { // runs only ont time per connection, prepare for start
|
||
m_f_firstCall = false;
|
||
m_f_stream = false;
|
||
m_pwst.chunkSize = 0;
|
||
m_metacount = m_metaint;
|
||
m_f_allDataReceived = false;
|
||
readMetadata(0, &readedBytes, true);
|
||
getChunkSize(0, true);
|
||
m_audioFilePosition = 0;
|
||
}
|
||
|
||
m_pwst.availableBytes = m_client->available(); // available from stream
|
||
// chunked data tramsfer
|
||
if (m_f_chunked && m_pwst.availableBytes) {
|
||
if (m_pwst.chunkSize == 0) {
|
||
int chunkLen = getChunkSize(&m_pwst.readedBytes);
|
||
if (chunkLen == -1) { // need more data
|
||
vTaskDelay(10);
|
||
return;
|
||
}
|
||
if (chunkLen == -100) { // error
|
||
stopSong();
|
||
return;
|
||
}
|
||
if (chunkLen == 0) { m_f_allDataReceived = true; }
|
||
m_pwst.chunkSize = chunkLen;
|
||
m_pwst.readedBytes = 0; // readedBytes is not a part of chunkSize
|
||
}
|
||
m_pwst.writeSpace = min(m_pwst.writeSpace, m_pwst.chunkSize);
|
||
}
|
||
|
||
// we have metadata - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if ((m_metaint) && (m_metacount == 0)) {
|
||
if (!m_pwst.availableBytes) return;
|
||
readedBytes = 0;
|
||
bool res = false;
|
||
if (m_f_chunked) {
|
||
res = readMetadata(min(m_pwst.availableBytes, m_pwst.chunkSize), &readedBytes);
|
||
} else {
|
||
res = readMetadata(m_pwst.availableBytes, &readedBytes);
|
||
}
|
||
m_pwst.readedBytes += readedBytes;
|
||
if (m_f_chunked) m_pwst.chunkSize -= readedBytes; // reduce chunkSize by metadata length
|
||
if (res == false) return;
|
||
m_metacount = m_metaint;
|
||
return;
|
||
}
|
||
if (m_metaint) m_pwst.writeSpace = min(m_pwst.writeSpace, m_metacount);
|
||
|
||
// buffer fill routine - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_pwst.availableBytes) {
|
||
m_pwst.writeSpace = min(m_pwst.writeSpace, (uint32_t)InBuff.writeSpace());
|
||
int32_t bytesAddedToBuffer = audioFileRead(InBuff.getWritePtr(), min(m_pwst.writeSpace, (uint32_t)UINT16_MAX));
|
||
|
||
if (bytesAddedToBuffer > 0) {
|
||
m_pwst.writeSpace -= bytesAddedToBuffer;
|
||
if (m_metaint) m_metacount -= bytesAddedToBuffer;
|
||
if (m_f_chunked) m_pwst.chunkSize -= bytesAddedToBuffer;
|
||
InBuff.bytesWritten(bytesAddedToBuffer);
|
||
}
|
||
}
|
||
|
||
// if the buffer is often almost empty issue a warning - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_f_stream) {
|
||
if (!m_f_allDataReceived) streamDetection(m_pwst.availableBytes);
|
||
if (!m_pwst.f_clientIsConnected) {
|
||
if (m_f_tts && !m_f_allDataReceived) m_f_allDataReceived = true;
|
||
} // connection closed (OpenAi)
|
||
}
|
||
|
||
if (!m_decoder && InBuff.bufferFilled() > 127) {
|
||
if (initializeDecoder())
|
||
m_pwst.maxFrameSize = InBuff.getMaxBlockSize();
|
||
else
|
||
return;
|
||
}
|
||
|
||
// start audio decoding - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (((InBuff.bufferFilled() > m_pwst.maxFrameSize * 2) || (m_f_allDataReceived)) && !m_f_stream) { // waiting for buffer filled
|
||
info(*this, evt_info, "stream ready");
|
||
m_f_stream = true; // ready to play the audio data
|
||
}
|
||
|
||
if (m_f_eof) {
|
||
info(*this, evt_eof, "{}", m_lastHost.c_get());
|
||
stopSong();
|
||
}
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::processWebFile() {
|
||
|
||
if (!m_lastHost.valid()) {
|
||
AUDIO_LOG_ERROR("m_lastHost is empty");
|
||
return;
|
||
} // guard
|
||
uint32_t availableBytes = 0;
|
||
int32_t bytesAddedToBuffer = 0;
|
||
// m_pwf.f_clientIsConnected = m_client->connected(); // we are not connected
|
||
|
||
if (m_f_firstCall) { // runs only ont time per connection, prepare for start
|
||
m_f_firstCall = false;
|
||
m_f_stream = false;
|
||
m_controlCounter = 0;
|
||
m_pwf.audioHeaderFound = false;
|
||
m_pwf.newFilePos = 0;
|
||
m_pwf.ctime = millis();
|
||
m_audioFilePosition = 0;
|
||
m_audioDataSize = m_audioFileSize;
|
||
m_audioDataStart = 0;
|
||
m_f_allDataReceived = false;
|
||
m_pwf.timeout = 8000; // ms
|
||
if (!initializeDecoder()) return;
|
||
m_pwf.maxFrameSize = InBuff.getMaxBlockSize(); // every frame is not bigger
|
||
}
|
||
|
||
if (m_resumeFilePos >= 0) { // we have a resume file position
|
||
m_pwf.newFilePos = newInBuffStart(m_resumeFilePos);
|
||
if (m_pwf.newFilePos < 0) AUDIO_LOG_WARN("skip to new position was not successful");
|
||
m_haveNewFilePos = m_pwf.newFilePos;
|
||
m_audioDataReadPtr = m_pwf.newFilePos;
|
||
m_resumeFilePos = -1;
|
||
m_f_allDataReceived = false;
|
||
return;
|
||
}
|
||
|
||
m_pwf.availableBytes = min(m_client->available(), (int)InBuff.writeSpace());
|
||
m_pwf.bytesAddedToBuffer = audioFileRead(InBuff.getWritePtr(), min(m_pwf.availableBytes, (uint32_t)UINT16_MAX));
|
||
if (m_pwf.bytesAddedToBuffer > 0) { InBuff.bytesWritten(m_pwf.bytesAddedToBuffer); }
|
||
if (m_audioDataSize && m_audioFilePosition >= m_audioDataSize) {
|
||
if (!m_f_allDataReceived) m_f_allDataReceived = true;
|
||
}
|
||
if (!m_audioDataSize && m_audioFilePosition == m_audioFileSize) {
|
||
if (!m_f_allDataReceived) m_f_allDataReceived = true;
|
||
}
|
||
// AUDIO_LOG_ERROR("m_audioFilePosition {} >= m_audioDataSize {}, m_f_allDataReceived {}", m_audioFilePosition, m_audioDataSize, m_f_allDataReceived);
|
||
if (!m_decoder && InBuff.bufferFilled() > 127) {
|
||
if (!initializeDecoder()) return;
|
||
}
|
||
|
||
if (!m_f_stream) {
|
||
if (m_controlCounter != 100) {
|
||
if (m_f_ogg) { m_controlCounter = 100; }
|
||
if ((millis() - m_pwf.ctime) > m_pwf.timeout) {
|
||
AUDIO_LOG_ERROR("audioHeader reading timeout");
|
||
m_f_running = false;
|
||
goto exit;
|
||
}
|
||
if (InBuff.bufferFilled() > m_pwf.maxFrameSize || (InBuff.bufferFilled() == m_audioFileSize) || m_f_allDataReceived) { // at least one complete frame or the file is smaller
|
||
InBuff.bytesWasRead(readAudioHeader(InBuff.readSpace()));
|
||
}
|
||
if (m_controlCounter == 100) {
|
||
if (m_audioDataStart > 0) { m_pwf.audioHeaderFound = true; }
|
||
if (!m_audioDataSize) m_audioDataSize = m_audioFileSize;
|
||
}
|
||
return;
|
||
} else {
|
||
if (m_resumeFilePos == -1) {
|
||
if (InBuff.bufferFilled() > 2 * InBuff.getMaxBlockSize()) {
|
||
m_f_stream = true;
|
||
info(*this, evt_info, "stream ready");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (m_fileStartTime > 0 && m_nominal_bitrate) {
|
||
if (getBitRate() > 0)
|
||
setAudioPlayTime(m_fileStartTime);
|
||
else
|
||
info(*this, evt_info, "can't set audio play time directly");
|
||
m_fileStartTime = -1;
|
||
}
|
||
|
||
// end of file reached? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_f_eof) { // m_f_eof and m_f_ID3v1TagFound will be set in playAudioData()
|
||
if (m_f_ID3v1TagFound) readID3V1Tag();
|
||
exit:
|
||
stopSong();
|
||
info(*this, evt_eof, "{}", m_lastHost.c_get());
|
||
|
||
m_audioCurrentTime = 0;
|
||
m_audioFileDuration = 0;
|
||
m_resumeFilePos = -1;
|
||
m_haveNewFilePos = 0;
|
||
m_codec = CODEC_NONE;
|
||
return;
|
||
}
|
||
}
|
||
// ——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::processWebStreamTS() {
|
||
|
||
// first call, set some values to default ———————————————————————————————————
|
||
if (m_f_firstCall) { // runs only one time per connection, prepare for start
|
||
m_f_firstCall = false;
|
||
m_f_m3u8data = true;
|
||
m_audioFilePosition = 0;
|
||
m_pwsst.f_firstPacket = true;
|
||
m_pwsst.f_chunkFinished = false;
|
||
m_pwsst.f_nextRound = false;
|
||
m_pwsst.byteCounter = 0;
|
||
m_t0 = millis();
|
||
if (!m_pwsst.ts_packet.valid()) m_pwsst.ts_packet.alloc_array(m_pwsst.ts_packetsize, "m_pwsst.ts_packet"); // first init
|
||
if (!m_decoder) { // first init
|
||
getChunkSize(0, true);
|
||
m_pwsst.chunkSize = 0;
|
||
ts_parsePacket(0, 0, 0);
|
||
m_pwsst.ts_packetPtr = 0;
|
||
if (!initializeDecoder()) return;
|
||
}
|
||
} // —————————————————————————————————————————————————————————————————————————
|
||
|
||
m_pwsst.ts_packetStart = 0;
|
||
m_pwsst.ts_packetLength = 0;
|
||
uint32_t availableBytes = m_client->available(); // available bytes in stream
|
||
|
||
if (availableBytes) {
|
||
/* If the m3u8 stream uses 'chunked data transfer' no content length is supplied. Then the chunk size determines the audio data to be processed.
|
||
However, the chunk size in some streams is limited to 32768 bytes, although the chunk can be larger. Then the chunk size is
|
||
calculated again. The data used to calculate (here readedBytes) the chunk size is not part of it.
|
||
*/
|
||
uint16_t readedBytes = 0;
|
||
uint32_t minAvBytes = 0;
|
||
if (m_pwsst.f_chunkFinished) goto chunkFinished;
|
||
if (m_f_chunked && m_pwsst.chunkSize == m_pwsst.byteCounter) {
|
||
int chunkLen = getChunkSize(&readedBytes);
|
||
if (chunkLen == -1) { // need more data
|
||
vTaskDelay(10);
|
||
return;
|
||
}
|
||
if (chunkLen == -100) { // error
|
||
stopSong();
|
||
return;
|
||
}
|
||
m_pwsst.chunkSize += chunkLen;
|
||
AUDIO_LOG_DEBUG("chunkLen {}, rb {}", chunkLen, readedBytes);
|
||
if (chunkLen == 0) {
|
||
m_pwsst.f_chunkFinished = true;
|
||
m_pwsst.chunkSize = 0;
|
||
m_pwsst.byteCounter = 0;
|
||
goto chunkFinished;
|
||
}
|
||
}
|
||
if (m_pwsst.chunkSize)
|
||
minAvBytes = min3(availableBytes, m_pwsst.ts_packetsize - m_pwsst.ts_packetPtr, m_pwsst.chunkSize - m_pwsst.byteCounter);
|
||
else
|
||
minAvBytes = min(availableBytes, (uint32_t)(m_pwsst.ts_packetsize - m_pwsst.ts_packetPtr));
|
||
|
||
int res = audioFileRead(m_pwsst.ts_packet.get() + m_pwsst.ts_packetPtr, minAvBytes);
|
||
if (res > 0) {
|
||
m_pwsst.ts_packetPtr += res;
|
||
m_pwsst.byteCounter += res;
|
||
if (m_pwsst.ts_packetPtr < m_pwsst.ts_packetsize) return; // not enough data yet, the process must be repeated if the packet size (188 bytes) is not reached
|
||
m_pwsst.ts_packetPtr = 0;
|
||
if (m_pwsst.f_firstPacket) { // search for ID3 Header in the first packet
|
||
m_pwsst.f_firstPacket = false;
|
||
uint8_t ID3_HeaderSize = process_m3u8_ID3_Header(m_pwsst.ts_packet.get());
|
||
if (ID3_HeaderSize > m_pwsst.ts_packetsize) {
|
||
AUDIO_LOG_ERROR("ID3 Header is too big");
|
||
stopSong();
|
||
return;
|
||
}
|
||
if (ID3_HeaderSize) {
|
||
memcpy(m_pwsst.ts_packet.get(), &m_pwsst.ts_packet.get()[ID3_HeaderSize], m_pwsst.ts_packetsize - ID3_HeaderSize);
|
||
m_pwsst.ts_packetPtr = m_pwsst.ts_packetsize - ID3_HeaderSize;
|
||
return;
|
||
}
|
||
}
|
||
if (!ts_parsePacket(&m_pwsst.ts_packet.get()[0], &m_pwsst.ts_packetStart, &m_pwsst.ts_packetLength)) {
|
||
stopSong();
|
||
AUDIO_LOG_ERROR("song stopped");
|
||
return;
|
||
};
|
||
|
||
if (m_pwsst.ts_packetLength) {
|
||
size_t ws = InBuff.writeSpace();
|
||
if (ws >= m_pwsst.ts_packetLength) {
|
||
memcpy(InBuff.getWritePtr(), m_pwsst.ts_packet.get() + m_pwsst.ts_packetStart, m_pwsst.ts_packetLength);
|
||
InBuff.bytesWritten(m_pwsst.ts_packetLength);
|
||
m_pwsst.f_nextRound = true;
|
||
} else {
|
||
int t = 0;
|
||
memcpy(InBuff.getWritePtr(), m_pwsst.ts_packet.get() + m_pwsst.ts_packetStart, ws); // write everything that fits into the buffer
|
||
InBuff.bytesWritten(ws);
|
||
while (true) {
|
||
if (InBuff.writeSpace() >= m_pwsst.ts_packetLength - ws) {
|
||
memcpy(InBuff.getWritePtr(), &m_pwsst.ts_packet.get()[ws + m_pwsst.ts_packetStart], m_pwsst.ts_packetLength - ws); // write the rest
|
||
InBuff.bytesWritten(m_pwsst.ts_packetLength - ws);
|
||
break;
|
||
}
|
||
t++;
|
||
vTaskDelay(10); // wait until the buffer is free
|
||
if (t == 10) {
|
||
AUDIO_LOG_ERROR("InBuff is full");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (m_audioFileSize && m_pwsst.byteCounter > m_audioFileSize) {
|
||
AUDIO_LOG_ERROR("byteCounter overflow, byteCounter: {}, contentlength: {}", m_pwsst.byteCounter, m_audioFileSize);
|
||
return;
|
||
}
|
||
if (m_pwsst.chunkSize && m_pwsst.byteCounter > m_pwsst.chunkSize) {
|
||
AUDIO_LOG_ERROR("byteCounter overflow, byteCounter: {}, chunkSize: {}", m_pwsst.byteCounter, m_pwsst.chunkSize);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
if (m_audioFileSize && m_pwsst.byteCounter == m_audioFileSize) {
|
||
if (InBuff.bufferFilled() < settings.BUFFER_TRESHOLD_HLS) {
|
||
m_f_continue = true;
|
||
m_pwsst.byteCounter = 0;
|
||
m_pwsst.ts_packetPtr = 0;
|
||
}
|
||
m_pwsst.f_nextRound = false;
|
||
goto exit;
|
||
}
|
||
|
||
chunkFinished:
|
||
if (m_pwsst.f_chunkFinished) {
|
||
if (InBuff.bufferFilled() < settings.BUFFER_TRESHOLD_HLS) {
|
||
m_pwsst.f_chunkFinished = false;
|
||
m_f_continue = true;
|
||
m_pwsst.byteCounter = 0;
|
||
m_pwsst.chunkSize = 0;
|
||
m_pwsst.ts_packetPtr = 0;
|
||
}
|
||
goto exit;
|
||
}
|
||
|
||
// if the buffer is often almost empty issue a warning - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_f_stream) { streamDetection(availableBytes); }
|
||
|
||
// buffer fill routine - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
{
|
||
if (InBuff.bufferFilled() > settings.BUFFER_TRESHOLD_HLS && !m_f_stream) { // waiting for buffer filled
|
||
m_f_stream = true; // ready to play the audio data
|
||
uint16_t filltime = millis() - m_t0;
|
||
info(*this, evt_info, "stream ready");
|
||
info(*this, evt_info, "buffer filled in {} ms", filltime);
|
||
}
|
||
}
|
||
|
||
exit:
|
||
return;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::processWebStreamHLS() {
|
||
|
||
// first call, set some values to default - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (m_f_firstCall) { // runs only ont time per connection, prepare for start
|
||
m_f_firstCall = false;
|
||
m_f_m3u8data = true;
|
||
m_t0 = millis();
|
||
m_controlCounter = 0;
|
||
m_audioFilePosition = 0;
|
||
m_pwsHLS.ID3BuffSize = 4096;
|
||
m_pwsHLS.availableBytes = 0;
|
||
m_pwsHLS.firstBytes = true;
|
||
m_pwsHLS.f_chunkFinished = false;
|
||
m_pwsHLS.byteCounter = 0;
|
||
m_pwsHLS.chunkSize = 0;
|
||
m_pwsHLS.ID3WritePtr = 0;
|
||
m_pwsHLS.ID3ReadPtr = 0;
|
||
m_pwsHLS.ID3Buff.alloc(m_pwsHLS.ID3BuffSize, "m_pwsHLS.ID3Buff");
|
||
getChunkSize(0, true);
|
||
if (!m_decoder && !initializeDecoder()) return;
|
||
m_pwsHLS.maxFrameSize = InBuff.getMaxBlockSize(); // every mp3/aac frame is not bigger
|
||
}
|
||
|
||
if (m_dataMode != AUDIO_DATA) return; // guard
|
||
|
||
m_pwsHLS.availableBytes = m_client->available();
|
||
if (m_pwsHLS.availableBytes) { // an ID3 header could come here
|
||
uint16_t readedBytes = 0;
|
||
|
||
if (m_f_chunked && !m_pwsHLS.chunkSize) {
|
||
m_pwsHLS.chunkSize = getChunkSize(&readedBytes);
|
||
if (m_pwsHLS.chunkSize == -1) { // need more data
|
||
vTaskDelay(10);
|
||
return;
|
||
}
|
||
if (m_pwsHLS.chunkSize == -100) { // error
|
||
stopSong();
|
||
return;
|
||
}
|
||
m_pwsHLS.byteCounter += readedBytes;
|
||
}
|
||
|
||
if (m_pwsHLS.firstBytes) {
|
||
if (m_pwsHLS.ID3WritePtr < m_pwsHLS.ID3BuffSize) {
|
||
m_pwsHLS.ID3WritePtr += audioFileRead(&m_pwsHLS.ID3Buff[m_pwsHLS.ID3WritePtr], m_pwsHLS.ID3BuffSize - m_pwsHLS.ID3WritePtr);
|
||
return;
|
||
}
|
||
if (m_controlCounter < 100) {
|
||
int res = read_ID3_Header(&m_pwsHLS.ID3Buff[m_pwsHLS.ID3ReadPtr], m_pwsHLS.ID3BuffSize - m_pwsHLS.ID3ReadPtr);
|
||
if (res >= 0) m_pwsHLS.ID3ReadPtr += res;
|
||
if (m_pwsHLS.ID3ReadPtr > m_pwsHLS.ID3BuffSize) {
|
||
AUDIO_LOG_ERROR("buffer overflow");
|
||
stopSong();
|
||
return;
|
||
}
|
||
return;
|
||
}
|
||
if (m_controlCounter != 100) return;
|
||
|
||
size_t ws = InBuff.writeSpace();
|
||
if (ws >= m_pwsHLS.ID3BuffSize - m_pwsHLS.ID3ReadPtr) {
|
||
memcpy(InBuff.getWritePtr(), &m_pwsHLS.ID3Buff[m_pwsHLS.ID3ReadPtr], m_pwsHLS.ID3BuffSize - m_pwsHLS.ID3ReadPtr);
|
||
InBuff.bytesWritten(m_pwsHLS.ID3BuffSize - m_pwsHLS.ID3ReadPtr);
|
||
} else {
|
||
int t = 0;
|
||
memcpy(InBuff.getWritePtr(), &m_pwsHLS.ID3Buff[m_pwsHLS.ID3ReadPtr], ws);
|
||
InBuff.bytesWritten(ws);
|
||
while (true) {
|
||
if (InBuff.writeSpace() >= m_pwsHLS.ID3BuffSize - (m_pwsHLS.ID3ReadPtr + ws)) {
|
||
memcpy(InBuff.getWritePtr(), &m_pwsHLS.ID3Buff[ws + m_pwsHLS.ID3ReadPtr], m_pwsHLS.ID3BuffSize - (m_pwsHLS.ID3ReadPtr + ws)); // write everything that fits into the buffer
|
||
InBuff.bytesWritten(m_pwsHLS.ID3BuffSize - (m_pwsHLS.ID3ReadPtr + ws));
|
||
break;
|
||
}
|
||
t++;
|
||
vTaskDelay(10); // wait until the buffer is free
|
||
if (t == 10) {
|
||
AUDIO_LOG_ERROR("InBuff is full");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
m_pwsHLS.ID3Buff.reset();
|
||
m_pwsHLS.byteCounter += m_pwsHLS.ID3BuffSize;
|
||
m_pwsHLS.firstBytes = false;
|
||
}
|
||
|
||
size_t bytesWasWritten = 0;
|
||
if (InBuff.writeSpace() >= m_pwsHLS.availableBytes) {
|
||
// if(availableBytes > 1024) availableBytes = 1024; // 1K throttle
|
||
bytesWasWritten = audioFileRead(InBuff.getWritePtr(), m_pwsHLS.availableBytes);
|
||
} else {
|
||
bytesWasWritten = audioFileRead(InBuff.getWritePtr(), InBuff.writeSpace());
|
||
}
|
||
InBuff.bytesWritten(bytesWasWritten);
|
||
|
||
m_pwsHLS.byteCounter += bytesWasWritten;
|
||
|
||
if (m_pwsHLS.byteCounter == m_audioFileSize || m_pwsHLS.byteCounter == m_pwsHLS.chunkSize) {
|
||
m_pwsHLS.f_chunkFinished = true;
|
||
m_pwsHLS.byteCounter = 0;
|
||
}
|
||
}
|
||
|
||
if (m_pwsHLS.f_chunkFinished) {
|
||
if (InBuff.bufferFilled() < settings.BUFFER_TRESHOLD_HLS) {
|
||
m_pwsHLS.f_chunkFinished = false;
|
||
m_f_continue = true;
|
||
}
|
||
}
|
||
|
||
// if the buffer is often almost empty issue a warning or try a new connection - - - - - - - - - - - - - - - - - - -
|
||
if (m_f_stream) { streamDetection(m_pwsHLS.availableBytes); }
|
||
|
||
if (InBuff.bufferFilled() > settings.BUFFER_TRESHOLD_HLS && !m_f_stream) { // waiting for buffer filled
|
||
m_f_stream = true; // ready to play the audio data
|
||
// uint16_t filltime = millis() - m_t0;
|
||
info(*this, evt_info, "stream ready");
|
||
// info(*this, evt_info, "buffer filled in {} ms", filltime);
|
||
}
|
||
return;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::playAudioData() {
|
||
|
||
if (m_f_eof || m_f_lockInBuffer || !m_f_stream) {
|
||
vTaskDelay(1);
|
||
return;
|
||
} // guard, stream not ready or eof reached or InBuff is locked or not running
|
||
if (m_validSamples) {
|
||
playChunk();
|
||
return;
|
||
} // guard, play samples first
|
||
//--------------------------------------------------------------------------------
|
||
m_pad.count = 0;
|
||
m_pad.bytesToDecode = InBuff.readSpace();
|
||
m_pad.bytesDecoded = 0;
|
||
|
||
if (m_f_firstPlayCall) {
|
||
m_audioDataReadPtr = 0;
|
||
m_f_firstPlayCall = false;
|
||
m_pad.count = 0;
|
||
m_pad.oldAudioDataSize = 0;
|
||
m_bytesNotConsumed = 0;
|
||
m_pad.lastFrames = false;
|
||
m_f_eof = false;
|
||
}
|
||
//--------------------------------------------------------------------------------
|
||
|
||
bool isFile = false;
|
||
bool isStream = false;
|
||
|
||
if (m_dataMode == AUDIO_LOCALFILE) isFile = true;
|
||
if (m_streamType == ST_WEBFILE && m_playlistFormat != FORMAT_M3U8) isFile = true; // local file or webfile but not m3u8 file
|
||
if (m_streamType == ST_WEBSTREAM || m_playlistFormat == FORMAT_M3U8) isStream = true;
|
||
if (!isFile && !isStream) return;
|
||
|
||
xSemaphoreTake(mutex_audioTaskIsDecoding, 1 * configTICK_RATE_HZ);
|
||
{
|
||
m_pad.bytesDecoded = 0;
|
||
if (isFile) {
|
||
if (!m_audioDataSize) goto exit; // no data to decode if filesize is 0
|
||
if (m_audioDataStart + m_audioDataSize - m_audioDataReadPtr == 128) {
|
||
m_f_ID3v1TagFound = true;
|
||
m_f_eof = true;
|
||
goto exit;
|
||
}
|
||
if (m_audioDataSize <= m_audioDataReadPtr) {
|
||
m_f_eof = true;
|
||
goto exit;
|
||
}
|
||
|
||
if (m_audioDataStart + m_audioDataSize >= m_audioFilePosition) m_f_allDataReceived = true;
|
||
if (m_audioDataSize - m_audioDataReadPtr <= InBuff.getMaxBlockSize()) m_pad.lastFrames = true;
|
||
|
||
if (m_pad.lastFrames) {
|
||
m_pad.bytesToDecode = min(InBuff.readSpace(), (size_t)(m_audioDataStart + m_audioDataSize - m_audioDataReadPtr));
|
||
m_pad.bytesDecoded = sendBytes(InBuff.getReadPtr(), m_pad.bytesToDecode);
|
||
} else {
|
||
m_pad.bytesToDecode = InBuff.readSpace();
|
||
if (m_pad.bytesToDecode >= InBuff.getMaxBlockSize()) { m_pad.bytesDecoded = sendBytes(InBuff.getReadPtr(), m_pad.bytesToDecode); }
|
||
}
|
||
}
|
||
|
||
if (isStream) {
|
||
if (m_f_allDataReceived) { // Google TTS, OpenAI
|
||
m_pad.lastFrames = true;
|
||
if (m_f_tts && !InBuff.bufferFilled()) {
|
||
m_f_eof = true;
|
||
goto exit;
|
||
}
|
||
}
|
||
if (m_pad.lastFrames) {
|
||
if (m_f_chunked) {
|
||
m_pad.bytesToDecode = InBuff.readSpace();
|
||
} else {
|
||
m_pad.bytesToDecode = min(InBuff.readSpace(), (size_t)(m_audioDataStart + m_audioDataSize - m_audioDataReadPtr));
|
||
}
|
||
m_pad.bytesDecoded = sendBytes(InBuff.getReadPtr(), m_pad.bytesToDecode);
|
||
} else {
|
||
m_pad.bytesToDecode = InBuff.readSpace();
|
||
if (m_pad.bytesToDecode >= InBuff.getMaxBlockSize()) { m_pad.bytesDecoded = sendBytes(InBuff.getReadPtr(), m_pad.bytesToDecode); }
|
||
}
|
||
}
|
||
|
||
if (m_pad.bytesDecoded > 0) {
|
||
InBuff.bytesWasRead(m_pad.bytesDecoded);
|
||
m_audioDataReadPtr += m_pad.bytesDecoded;
|
||
}
|
||
}
|
||
exit:
|
||
xSemaphoreGive(mutex_audioTaskIsDecoding);
|
||
|
||
AUDIO_LOG_DEBUG("m_audioDataReadPtr {}, m_audioDataSize {}", m_audioDataReadPtr, m_audioDataSize);
|
||
return;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::parseHttpResponseHeader() { // this is the response to a GET / request
|
||
|
||
if (m_dataMode != HTTP_RESPONSE_HEADER) return false;
|
||
m_icy_items.reset();
|
||
m_metaint = 0; // no metaint yet
|
||
|
||
m_phreh.reset();
|
||
m_phreh.ctime = millis();
|
||
m_phreh.timeout = 5000; // ms
|
||
m_httpRespHdrBuff.clear();
|
||
uint16_t pos = 0;
|
||
|
||
while (true) { // read the header first and store it in m_httpRespHdrBuff
|
||
if (m_client->available()) { m_httpRespHdrBuff[pos++] = audioFileRead(); }
|
||
if (m_httpRespHdrBuff.ends_with("\r\n\r\n")) break;
|
||
if (m_httpRespHdrBuff.ends_with("\n\n")) break;
|
||
if (pos == m_httpRespHdrBuff.size()) {
|
||
AUDIO_LOG_WARN("responseHeaderline overflow");
|
||
m_httpRespHdrBuff[pos - 1] = '\0';
|
||
break;
|
||
}
|
||
if ((millis() - m_phreh.ctime) > m_phreh.timeout) {
|
||
AUDIO_LOG_ERROR("timeout");
|
||
m_phreh.f_time = false;
|
||
// stopSong();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
ps_ptr<char> rhl;
|
||
bool ct_seen = false;
|
||
|
||
// m_httpRespHdrBuff.println();
|
||
|
||
pos = 0;
|
||
while (true) { // read the header line for line
|
||
int idx = m_httpRespHdrBuff.index_of('\n', pos);
|
||
if (idx > pos) {
|
||
rhl = m_httpRespHdrBuff.substr(pos, idx - pos);
|
||
rhl.set_name("rhl");
|
||
rhl.remove_chars("\r");
|
||
pos = idx + 1;
|
||
if (rhl.strlen() == 0) continue;
|
||
} else { // done?
|
||
if (ct_seen)
|
||
goto lastToDo;
|
||
else { goto exit; }
|
||
}
|
||
|
||
// rhl.println();
|
||
|
||
if (rhl.starts_with_icase("icy-")) { // —— ICY BLOCK ——————————————————————————————————————————————————————————
|
||
m_phreh.f_icy_data = true;
|
||
|
||
if (rhl.starts_with_icase("icy-genre:")) {
|
||
m_icy_items.icy_genre.copy_from(rhl.get() + 10); // Ambient, Rock, etc
|
||
m_icy_items.icy_genre.trim();
|
||
latinToUTF8(m_icy_items.icy_genre);
|
||
}
|
||
|
||
if (rhl.starts_with_icase("icy-logo:")) {
|
||
m_icy_items.icy_logo.copy_from(rhl.get() + 9); // https://www.0nradio.com/logos/0n-70s_600x600.jpg
|
||
m_icy_items.icy_logo.trim();
|
||
latinToUTF8(m_icy_items.icy_logo);
|
||
}
|
||
|
||
if (rhl.starts_with_icase("icy-name:")) {
|
||
m_icy_items.icy_name.copy_from(rhl.get() + 9);
|
||
m_icy_items.icy_name.trim();
|
||
latinToUTF8(m_icy_items.icy_name);
|
||
}
|
||
|
||
if (rhl.starts_with_icase("icy-url:")) {
|
||
m_icy_items.icy_url.copy_from(rhl.get() + 8);
|
||
m_icy_items.icy_url.trim();
|
||
}
|
||
|
||
if (rhl.starts_with_icase("icy-description:")) {
|
||
m_icy_items.icy_description.copy_from(rhl.get() + 16);
|
||
m_icy_items.icy_description.trim();
|
||
latinToUTF8(m_icy_items.icy_description);
|
||
}
|
||
|
||
if (rhl.starts_with_icase("icy-metaint:")) {
|
||
m_icy_items.icy_metaint.copy_from(rhl.get() + 12);
|
||
m_icy_items.icy_metaint.trim();
|
||
}
|
||
|
||
if (rhl.starts_with_icase("icy-br:")) {
|
||
m_icy_items.icy_br.copy_from(rhl.get() + 7);
|
||
m_icy_items.icy_br.trim();
|
||
}
|
||
} // —— END ICY BLOCK —————————————————————————————————————————————————————————————————————————————————————————
|
||
|
||
if (rhl.starts_with_icase("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 == 403 && !m_f_alt_user_agent) { // HTTP/1.1 403 Forbidden
|
||
m_f_alt_user_agent = true;
|
||
AUDIO_LOG_WARN("403 Forbidden, test alternative user agent");
|
||
connecttohost(m_lastHost.c_get());
|
||
return true;
|
||
}
|
||
m_f_alt_user_agent = false;
|
||
if (sc > 310) { // e.g. HTTP/1.1 301 Moved Permanently
|
||
info(*this, evt_streamtitle, "{}", rhl.get());
|
||
goto exit;
|
||
}
|
||
} else if (rhl.starts_with_icase("content-type:")) { // content-type: text/html; charset=UTF-8
|
||
int idx = rhl.index_of(';', 13);
|
||
if (idx > 0) rhl[idx] = '\0';
|
||
if (parseContentType(rhl.get() + 13))
|
||
ct_seen = true;
|
||
else {
|
||
AUDIO_LOG_WARN("unknown contentType {}", rhl.get() + 13);
|
||
goto exit;
|
||
}
|
||
}
|
||
|
||
else if (rhl.starts_with_icase("location:")) {
|
||
int pos = rhl.index_of_icase("http", 0);
|
||
if (pos == -1) {
|
||
int doubleSlash = rhl.index_of("//"); // e.g. location: //frontend.streamonkey.net/... host: http://webstream.radiof.de/
|
||
if (doubleSlash >= 9) rhl.insert("http:", doubleSlash); // ==> http://frontend.streamonkey.net/fhn-radiof945/stream/mp3?aggregator=fh-tinyurl
|
||
pos = rhl.index_of_icase("http", 0);
|
||
}
|
||
if (pos >= 0) {
|
||
const char* c_host = (rhl.get() + pos);
|
||
if (!m_currentHost.equals(c_host)) { // prevent a loop
|
||
int pos_slash = indexOf(c_host, "/", 9);
|
||
if (pos_slash > 9) {
|
||
if (!strncmp(c_host, m_currentHost.get(), pos_slash)) {
|
||
info(*this, evt_info, "redirect to new extension at existing host \"{}\"", c_host);
|
||
if (m_playlistFormat == FORMAT_M3U8) {
|
||
// m_lastHost.assign(c_host);
|
||
m_f_m3u8data = true;
|
||
}
|
||
httpPrint(c_host);
|
||
while (m_client->available()) audioFileRead(); // empty client buffer
|
||
return true;
|
||
}
|
||
}
|
||
info(*this, evt_info, "redirect to new host \"{}\"", c_host);
|
||
httpPrint(c_host);
|
||
return true;
|
||
}
|
||
} else {
|
||
AUDIO_LOG_WARN("unknown redirection: {}", rhl.c_get());
|
||
}
|
||
}
|
||
|
||
else if (rhl.starts_with_icase("content-encoding:")) {
|
||
info(*this, evt_info, "{}", rhl.get());
|
||
if (rhl.contains("gzip")) {
|
||
AUDIO_LOG_ERROR("can't extract gzip");
|
||
goto exit;
|
||
}
|
||
}
|
||
|
||
else if (rhl.starts_with_icase("content-disposition:")) { // e.g we have this headerline: content-disposition: attachment; filename=stream.asx
|
||
int idx = rhl.index_of_icase("filename=");
|
||
if (idx >= 0) {
|
||
ps_ptr<char> fn;
|
||
fn.assign(rhl.get() + idx + 9); // Position directly after "filename="
|
||
fn.replace("\"", ""); // remove '\"' around filename if present
|
||
info(*this, evt_info, "Filename is {}", fn.get());
|
||
}
|
||
} else if (rhl.starts_with_icase("connection:")) {
|
||
if (rhl.contains_with_icase("close")) { m_f_connectionClose = true; /* AUDIO_LOG_ERROR("connection will be closed"); */ } // ends after ogg last Page is set
|
||
}
|
||
|
||
else if (rhl.starts_with_icase("content-length:")) {
|
||
const char* c_cl = (rhl.get() + 15);
|
||
int32_t i_cl = atoi(c_cl);
|
||
m_audioFileSize = i_cl;
|
||
// info(*this, evt_info, "content-length: {}", m_audioFileSize);
|
||
}
|
||
|
||
else if (rhl.starts_with_icase("transfer-encoding:")) {
|
||
if (rhl.ends_with_icase("chunked")) { // Station provides chunked transfer
|
||
m_f_chunked = true;
|
||
info(*this, evt_info, "chunked data transfer");
|
||
m_chunkcount = 0; // Expect chunkcount in DATA
|
||
}
|
||
}
|
||
|
||
else if (rhl.starts_with_icase("accept-ranges:")) {
|
||
if (rhl.ends_with_icase("bytes")) m_f_acceptRanges = true;
|
||
// AUDIO_LOG_INFO("{}", rhl.c_get());
|
||
}
|
||
|
||
else if (rhl.starts_with_icase("content-range:")) {
|
||
AUDIO_LOG_INFO("{}", rhl.c_get());
|
||
}
|
||
|
||
else if (rhl.starts_with_icase("www-authenticate:")) {
|
||
AUDIO_LOG_WARN("authentification failed, wrong credentials?");
|
||
goto exit;
|
||
} else {
|
||
;
|
||
}
|
||
} // outer while
|
||
|
||
exit: // termination condition
|
||
m_f_alt_user_agent = false;
|
||
m_dataMode = AUDIO_NONE;
|
||
stopSong();
|
||
return false;
|
||
|
||
lastToDo:
|
||
m_f_alt_user_agent = false;
|
||
m_streamType = ST_WEBSTREAM;
|
||
if (m_audioFileSize > 0) m_streamType = ST_WEBFILE; // content length found
|
||
if (m_phreh.f_icy_data) m_streamType = ST_WEBSTREAM;
|
||
if (m_f_tts) m_streamType = ST_WEBSTREAM; // this is from AI or GoogleTTS(AI response)
|
||
|
||
if (ct_seen && m_playlistFormat == FORMAT_M3U8 && !m_m3u8_host.valid()) { m_m3u8_host = m_lastHost; }
|
||
|
||
if (m_codec != CODEC_NONE) {
|
||
m_dataMode = AUDIO_DATA; // Expecting data now
|
||
} else if (m_playlistFormat != FORMAT_NONE) {
|
||
m_dataMode = AUDIO_PLAYLISTINIT; // playlist expected
|
||
// AUDIO_LOG_INFO("now parse playlist");
|
||
} else {
|
||
AUDIO_LOG_INFO("unknown content found at: {}", m_currentHost.c_get());
|
||
goto exit;
|
||
}
|
||
|
||
if (m_phreh.f_icy_data) {
|
||
if (m_icy_items.icy_description.valid()) info(*this, evt_icydescription, "{}", m_icy_items.icy_description);
|
||
if (m_icy_items.icy_genre.valid()) info(*this, evt_genre, "{}", m_icy_items.icy_genre);
|
||
if (m_icy_items.icy_logo.valid()) info(*this, evt_icylogo, "{}", m_icy_items.icy_logo);
|
||
if (m_icy_items.icy_name.valid()) info(*this, evt_name, "{}", m_icy_items.icy_name);
|
||
if (m_icy_items.icy_url.valid()) info(*this, evt_icyurl, "{}", m_icy_items.icy_url);
|
||
if (m_icy_items.icy_metaint.valid()) m_metaint = m_icy_items.icy_metaint.to_uint32();
|
||
if (m_icy_items.icy_br.valid()) m_nominal_bitrate = m_icy_items.icy_br.to_uint32() * 1000;
|
||
}
|
||
|
||
AUDIO_LOG_DEBUG("playlistFormat {}, dataMode {}, streamType: {}", plsFmtStr[m_playlistFormat], dataModeStr[m_dataMode], streamTypeStr[m_streamType]);
|
||
return true;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::parseHttpRangeHeader() { // this is the response to a Range request
|
||
|
||
ps_ptr<char> rhl;
|
||
rhl.alloc(1024, "rhl"); // responseHeaderline
|
||
bool ct_seen = false;
|
||
|
||
if (m_dataMode != HTTP_RANGE_HEADER) {
|
||
AUDIO_LOG_ERROR("wrong datamode {}", m_dataMode);
|
||
goto exit;
|
||
}
|
||
|
||
m_phrah.ctime = millis();
|
||
m_phrah.timeout = 4500; // ms
|
||
|
||
if (m_client->available() == 0) {
|
||
if (!m_phrah.f_time) {
|
||
m_phrah.stime = millis();
|
||
m_phrah.f_time = true;
|
||
}
|
||
if ((millis() - m_phrah.stime) > m_phrah.timeout) {
|
||
AUDIO_LOG_ERROR("timeout");
|
||
m_phrah.f_time = false;
|
||
return false;
|
||
}
|
||
}
|
||
m_phrah.f_time = false;
|
||
|
||
rhl.clear();
|
||
|
||
while (true) { // outer while
|
||
uint16_t pos = 0;
|
||
if ((millis() - m_phrah.ctime) > m_phrah.timeout) {
|
||
AUDIO_LOG_ERROR("timeout");
|
||
m_f_timeout = true;
|
||
goto exit;
|
||
}
|
||
while (m_client->available()) {
|
||
uint8_t b = audioFileRead();
|
||
if (b == '\n') {
|
||
if (!pos) { // empty line received, is the last line of this responseHeader
|
||
goto lastToDo;
|
||
}
|
||
break;
|
||
}
|
||
if (b == '\r') rhl[pos] = 0;
|
||
if (b < 0x20) continue;
|
||
rhl[pos] = b;
|
||
pos++;
|
||
if (pos == 1023) {
|
||
pos = 1022;
|
||
continue;
|
||
}
|
||
if (pos == 1022) {
|
||
rhl[pos] = '\0';
|
||
AUDIO_LOG_WARN("responseHeaderline overflow");
|
||
}
|
||
} // inner while
|
||
if (!pos) {
|
||
vTaskDelay(5);
|
||
continue;
|
||
}
|
||
// AUDIO_LOG_WARN("rh {}", rhl.c_get());
|
||
if (rhl.starts_with_icase("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
|
||
info(*this, evt_name, "{}", rhl.get());
|
||
goto exit;
|
||
}
|
||
}
|
||
if (rhl.starts_with_icase("Server:")) { info(*this, evt_info, "{}", rhl.c_get()); }
|
||
if (rhl.starts_with_icase("Content-Length:")) {
|
||
// info(*this, evt_info, "{}", rhl.c_get());
|
||
}
|
||
if (rhl.starts_with_icase("Content-Range:")) { info(*this, evt_info, "{}", rhl.c_get()); }
|
||
if (rhl.starts_with_icase("Content-Type:")) {
|
||
// info(*this, evt_info, "{}", rhl.c_get());
|
||
}
|
||
}
|
||
exit:
|
||
return false;
|
||
lastToDo:
|
||
m_dataMode = AUDIO_DATA;
|
||
return true;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::initializeDecoder() {
|
||
if (m_decoder) {
|
||
AUDIO_LOG_WARN("stopSong first");
|
||
return true;
|
||
}
|
||
m_codec = determineCodec(m_codec); // OGG cn be VORBIS, FLAC or OPUS, contentType must not always be correct
|
||
|
||
const char* type = nullptr;
|
||
switch (m_codec) {
|
||
case CODEC_MP3:
|
||
type = "MP3";
|
||
InBuff.setMaxBlocksize(m_frameSizeMP3);
|
||
break;
|
||
case CODEC_AAC:
|
||
type = "AAC";
|
||
InBuff.setMaxBlocksize(m_frameSizeAAC);
|
||
break;
|
||
case CODEC_M4A:
|
||
type = "AAC";
|
||
InBuff.setMaxBlocksize(m_frameSizeAAC);
|
||
break;
|
||
case CODEC_FLAC:
|
||
type = "FLAC";
|
||
InBuff.setMaxBlocksize(m_frameSizeFLAC);
|
||
break;
|
||
case CODEC_OPUS:
|
||
type = "OPUS";
|
||
InBuff.setMaxBlocksize(m_frameSizeOPUS);
|
||
break;
|
||
case CODEC_VORBIS:
|
||
type = "VORBIS";
|
||
InBuff.setMaxBlocksize(m_frameSizeVORBIS);
|
||
break;
|
||
case CODEC_WAV:
|
||
type = "WAV";
|
||
InBuff.setMaxBlocksize(m_frameSizeWav);
|
||
break;
|
||
default:
|
||
AUDIO_LOG_ERROR("unknown decoder");
|
||
stopSong();
|
||
return false;
|
||
break;
|
||
}
|
||
|
||
if (type) {
|
||
m_decoder = createDecoder(type);
|
||
if (!m_decoder || !m_decoder->init()) {
|
||
AUDIO_LOG_ERROR("The {Decoder could not be initialized", type);
|
||
stopSong();
|
||
return false;
|
||
}
|
||
}
|
||
info(*this, evt_info, "{}Decoder has been initialized", m_decoder->whoIsIt());
|
||
return true;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::parseContentType(ps_ptr<char> ct) {
|
||
enum : int { CT_NONE, CT_MP3, CT_AAC, CT_M4A, CT_WAV, CT_FLAC, CT_PLS, CT_M3U, CT_ASX, CT_M3U8, CT_TXT, CT_AACP, CT_OPUS, CT_OGG, CT_VORBIS };
|
||
|
||
// MIME types and their CT_Val values
|
||
static const struct {
|
||
const char* mime_type;
|
||
int ct_val;
|
||
} mime_map[] = {
|
||
{"audio/mpeg", CT_MP3},
|
||
{"audio/mpeg3", CT_MP3},
|
||
{"audio/x-mpeg", CT_MP3},
|
||
{"audio/x-mpeg-3", CT_MP3},
|
||
{"audio/mp3", CT_MP3},
|
||
{"audio/aac", CT_AAC},
|
||
{"audio/x-aac", CT_AAC},
|
||
{"audio/aacp", CT_AAC},
|
||
{"video/mp2t", CT_AAC},
|
||
{"audio/mp2t", CT_AAC},
|
||
{"video/m2ts", CT_AAC},
|
||
{"audio/mp4", CT_M4A},
|
||
{"audio/m4a", CT_M4A},
|
||
{"audio/x-m4a", CT_M4A},
|
||
{"audio/wav", CT_WAV},
|
||
{"audio/x-wav", CT_WAV},
|
||
{"audio/flac", CT_FLAC},
|
||
{"audio/x-flac", CT_FLAC},
|
||
{"audio/scpls", CT_PLS},
|
||
{"audio/x-scpls", CT_PLS},
|
||
{"application/pls+xml", CT_PLS},
|
||
{"audio/mpegurl", CT_M3U},
|
||
{"audio/x-mpegurl", CT_M3U},
|
||
{"audio/ms-asf", CT_ASX},
|
||
{"video/x-ms-asf", CT_ASX},
|
||
{"audio/x-ms-asx", CT_ASX},
|
||
{"application/ogg", CT_OGG},
|
||
{"audio/ogg", CT_OGG},
|
||
{"application/vnd.apple.mpegurl", CT_M3U8},
|
||
{"application/hls+xml", CT_M3U8},
|
||
{"application/x-mpegurl", CT_M3U8},
|
||
{"application/octet-stream", CT_TXT},
|
||
{"text/html", CT_TXT},
|
||
{"text/plain", CT_TXT},
|
||
{"application/json", CT_TXT},
|
||
{NULL, CT_NONE} // Final marker
|
||
};
|
||
|
||
ct.trim();
|
||
|
||
m_codec = CODEC_NONE;
|
||
int ct_val = CT_NONE;
|
||
|
||
// Search in the Lookup table
|
||
for (int i = 0; mime_map[i].mime_type != NULL; i++) {
|
||
ct.toLowerCase();
|
||
if (ct.equals(mime_map[i].mime_type)) {
|
||
ct_val = mime_map[i].ct_val;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Special cases for video/mp2t and audio/mp2t
|
||
if (ct_val == CT_AAC && (ct == "video/mp2t" || ct == "audio/mp2t") && m_m3u8Codec == CODEC_MP3) { ct_val = CT_MP3; }
|
||
// Special case for audio/mpegurl
|
||
if (ct_val == CT_M3U && ct == "audio/mpegurl" && m_expectedPlsFmt == FORMAT_M3U8) { ct_val = CT_M3U8; }
|
||
// Special case for application/json
|
||
if (ct_val == CT_TXT && ct == "application/json" && m_expectedPlsFmt == FORMAT_M3U8) { ct_val = CT_M3U8; }
|
||
|
||
// exam for invalid content types
|
||
if (ct_val == CT_NONE) {
|
||
AUDIO_LOG_WARN("ContentType {} not supported", ct);
|
||
return false;
|
||
}
|
||
|
||
// allocation of m_codec and m_playlist format
|
||
switch (ct_val) {
|
||
case CT_MP3: m_codec = CODEC_MP3; break;
|
||
case CT_AAC: m_codec = CODEC_AAC; break;
|
||
case CT_M4A: m_codec = CODEC_M4A; break;
|
||
case CT_FLAC: m_codec = CODEC_FLAC; break;
|
||
case CT_OPUS:
|
||
m_codec = CODEC_OPUS;
|
||
m_f_ogg = true;
|
||
break;
|
||
case CT_VORBIS:
|
||
m_codec = CODEC_VORBIS;
|
||
m_f_ogg = true;
|
||
break;
|
||
case CT_WAV: m_codec = CODEC_WAV; break;
|
||
case CT_OGG:
|
||
m_codec = CODEC_OGG;
|
||
m_f_ogg = true;
|
||
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:
|
||
if (m_expectedCodec == CODEC_AAC) m_codec = CODEC_AAC;
|
||
if (m_expectedCodec == CODEC_MP3) m_codec = CODEC_MP3;
|
||
if (m_expectedPlsFmt == FORMAT_ASX) m_playlistFormat = FORMAT_ASX;
|
||
if (m_expectedPlsFmt == FORMAT_M3U) m_playlistFormat = FORMAT_M3U;
|
||
if (m_expectedPlsFmt == FORMAT_M3U8) m_playlistFormat = FORMAT_M3U8;
|
||
if (m_expectedPlsFmt == FORMAT_PLS) m_playlistFormat = FORMAT_PLS;
|
||
break;
|
||
default: AUDIO_LOG_ERROR("{}, unsupported audio format", ct); return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::showstreamtitle(char* st) {
|
||
if (!st) return;
|
||
// example for ml:
|
||
// StreamTitle='Oliver Frank - Mega Hitmix';StreamUrl='www.radio-welle-woerthersee.at';
|
||
// or adw_ad='true';durationMilliseconds='10135';adId='34254';insertionType='preroll';
|
||
// html: 'Bielszy odcień bluesa 682 cz.1' --> 'Bielszy odcień bluesa 682 cz.1'
|
||
|
||
ps_ptr<char> ml;
|
||
ps_ptr<char> title;
|
||
ps_ptr<char> artist;
|
||
ps_ptr<char> streamTitle;
|
||
ps_ptr<char> sUrl;
|
||
ml.htmlToUTF8(st); // convert to UTF-8
|
||
|
||
int16_t idx1 = 0, idx2, idx4, idx5, idx6, idx7, titleLen = 0, artistLen = 0, titleStart = 0, artistStart = 0;
|
||
|
||
// if(idx1 < 0) idx1 = indexOf(ml, "Title:", 0); // Title found (e.g. https://stream-hls.bauermedia.pt/comercial.aac/playlist.m3u8)
|
||
// if(idx1 < 0) idx1 = indexOf(ml, "title:", 0); // Title found (e.g. #EXTINF:10,title="The Dan Patrick Show (M-F 9a-12p ET)",artist="zc1401"
|
||
|
||
if (ml.index_of("StreamTitle='<?xml version=", 0) == 0) {
|
||
/* e.g. xmlStreamTitle
|
||
StreamTitle='<?xml version="1.0" encoding="utf-8"?><RadioInfo><Table><DB_ALBUM_ID>37364</DB_ALBUM_ID>
|
||
<DB_ALBUM_IMAGE>00000037364.jpg</DB_ALBUM_IMAGE><DB_ALBUM_NAME>Boyfriend</DB_ALBUM_NAME>
|
||
<DB_ALBUM_TYPE>Single</DB_ALBUM_TYPE><DB_DALET_ARTIST_NAME>DOVE CAMERON</DB_DALET_ARTIST_NAME>
|
||
<DB_DALET_ITEM_CODE>CD4161</DB_DALET_ITEM_CODE><DB_DALET_TITLE_NAME>BOYFRIEND</DB_DALET_TITLE_NAME>
|
||
<DB_FK_SITE_ID>2</DB_FK_SITE_ID><DB_IS_MUSIC>1</DB_IS_MUSIC><DB_LEAD_ARTIST_ID>26303</DB_LEAD_ARTIST_ID>
|
||
<DB_LEAD_ARTIST_NAME>Dove Cameron</DB_LEAD_ARTIST_NAME><DB_RADIO_IMAGE>cidadefm.jpg</DB_RADIO_IMAGE>
|
||
<DB_RADIO_NAME>Cidade</DB_RADIO_NAME><DB_SONG_ID>120126</DB_SONG_ID><DB_SONG_LYRIC>60981</DB_SONG_LYRIC>
|
||
<DB_SONG_NAME>Boyfriend</DB_SONG_NAME></Table><AnimadorInfo><TITLE>Cidade</TITLE>
|
||
<START_TIME_UTC>2022-11-15T22:00:00+00:00</START_TIME_UTC><END_TIME_UTC>2022-11-16T06:59:59+00:00
|
||
</END_TIME_UTC><SHOW_NAME>Cidade</SHOW_NAME><SHOW_HOURS>22h às 07h</SHOW_HOURS><SHOW_PANEL>0</SHOW_PANEL>
|
||
</AnimadorInfo></RadioInfo>';StreamUrl='';
|
||
*/
|
||
idx4 = ml.index_of("<DB_DALET_TITLE_NAME>");
|
||
idx5 = ml.index_of("</DB_DALET_TITLE_NAME>");
|
||
idx6 = ml.index_of("<DB_LEAD_ARTIST_NAME>");
|
||
idx7 = ml.index_of("</DB_LEAD_ARTIST_NAME>");
|
||
if (idx4 == -1 || idx5 == -1) return;
|
||
titleStart = idx4 + 21; // <DB_DALET_TITLE_NAME>
|
||
titleLen = idx5 - titleStart;
|
||
|
||
if (idx6 != -1 && idx7 != -1) {
|
||
artistStart = idx6 + 21; // <DB_LEAD_ARTIST_NAME>
|
||
artistLen = idx7 - artistStart;
|
||
}
|
||
if (titleLen) title.assign(ml.get() + titleStart, titleLen);
|
||
if (artistLen) artist.assign(ml.get() + artistStart, artistLen);
|
||
|
||
if (title.valid() && artist.valid()) {
|
||
streamTitle.assign(title.get());
|
||
streamTitle.append(" - ");
|
||
streamTitle.append(artist.get());
|
||
} else if (title.valid()) {
|
||
streamTitle.assign(title.get());
|
||
} else if (artist.valid()) {
|
||
streamTitle.assign(artist.get());
|
||
}
|
||
}
|
||
|
||
else if (ml.index_of("StreamTitle='") == 0) {
|
||
int start = 13;
|
||
int end = ml.index_of("';", start);
|
||
if (end < 0) end = ml.index_of("'", start); // fallback — when the station is transmitting incorrectly
|
||
if (end > start) {
|
||
int len = end - start;
|
||
streamTitle.assign(ml.get() + start, len);
|
||
}
|
||
}
|
||
|
||
else if (ml.starts_with("Grouping:")) {
|
||
streamTitle.assign(ml.get() + 9);
|
||
}
|
||
|
||
else if (ml.starts_with("#EXTINF")) {
|
||
// 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\" -
|
||
|
||
idx1 = ml.index_of("title=\"");
|
||
idx2 = ml.index_of("artist=\"");
|
||
if (idx1 > 0) {
|
||
int titleStart = idx1 + 7;
|
||
int idx3 = ml.index_of("\"", titleStart);
|
||
if (idx3 > titleStart) {
|
||
int titleLength = idx3 - titleStart;
|
||
title.assign(ml.get() + titleStart, titleLength);
|
||
if (title.starts_with("text=\\")) { // #EXTINF:10,title="text=\"Spot Block End\"",artist=" ",
|
||
titleStart += 7;
|
||
idx3 = ml.index_of("\\", titleStart);
|
||
if (idx3 > titleStart) {
|
||
int titleLength = idx3 - titleStart;
|
||
title.assign(ml.get() + titleStart, titleLength);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (idx2 > 0) {
|
||
int artistStart = idx2 + 8;
|
||
int idx3 = ml.index_of("\"", artistStart);
|
||
if (idx3 > artistStart) {
|
||
int artistLength = idx3 - artistStart;
|
||
artist.assign(ml.get() + artistStart, artistLength);
|
||
if (strcmp(artist.get(), " ") == 0) artist.reset();
|
||
}
|
||
}
|
||
if (title.valid() && artist.valid()) {
|
||
streamTitle.assign(title.get());
|
||
streamTitle.append(" - ");
|
||
streamTitle.append(artist.get());
|
||
} else if (title.valid()) {
|
||
streamTitle.assign(title.get());
|
||
} else if (artist.valid()) {
|
||
streamTitle.assign(artist.get());
|
||
}
|
||
} else {
|
||
;
|
||
}
|
||
|
||
if (ml.index_of("StreamUrl=", 0) > 0) {
|
||
idx1 = ml.index_of("StreamUrl=", 0);
|
||
idx2 = ml.index_of(";", idx1);
|
||
if (idx1 >= 0 && idx2 > idx1) { // StreamURL found
|
||
uint16_t len = idx2 - idx1;
|
||
sUrl.assign(ml.get() + idx1, len);
|
||
if (!sUrl.equals(m_streamURL.c_get())) {
|
||
info(*this, evt_info, "Stream URL: {}", sUrl.c_get()); // e.g. StreamUrl='http://myUrl.com'
|
||
m_streamURL = sUrl;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (ml.index_of("adw_ad=", 0) > 0) {
|
||
idx1 = ml.index_of("adw_ad=", 0);
|
||
if (idx1 >= 0) { // Advertisement found
|
||
idx1 = ml.index_of("durationMilliseconds=", 0);
|
||
idx2 = ml.index_of(";", idx1);
|
||
if (idx1 >= 0 && idx2 > idx1) {
|
||
uint16_t len = idx2 - idx1;
|
||
ps_ptr<char> sAdv;
|
||
sAdv.assign(ml.get() + idx1, len + 1);
|
||
// info(*this, evt_info, "Adveritsement: {}", sAdv.get());
|
||
uint8_t pos = 21; // remove "StreamTitle="
|
||
if (sAdv[pos] == '\'') pos++; // remove leading \'
|
||
if (sAdv[strlen(sAdv.get()) - 1] == '\'') sAdv[strlen(sAdv.get()) - 1] = '\0'; // remove trailing \'
|
||
info(*this, evt_info, "Advertisement: {}", sAdv.get() + pos);
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
if (!streamTitle.valid()) return;
|
||
|
||
if (!m_streamTitle.equals(streamTitle)) {
|
||
m_streamTitle.clone_from(streamTitle);
|
||
} else {
|
||
return; // is equal
|
||
}
|
||
|
||
if (m_streamTitle.starts_with("{\"")) { // maybe a json string
|
||
// "{\"status\":0,\"error\":{\"info\":\"\\u042d\\u0442\\u043e \\u0440\\u0435\\u043a\\u043b\\u0430\\u043c\\u0430 \\u0438\\u043b\\u0438
|
||
// \\u0434\\u0436\\u0438\\u043d\\u0433\\u043b\",\"code\":201},\"result\":\"\\u042d\\u0442\\u043e \\u0440\\u0435\\u043a\\u043b\\u0430\\u043c\\u0430 \\u0438\\u043b\\u0438
|
||
// \\u0434\\u0436\\u0438\\u043d\\u0433\\u043b\"}\n0";
|
||
ps_ptr<char> jsonIn;
|
||
jsonIn.clone_from(m_streamTitle);
|
||
jsonIn.truncate_at('\n'); // can be '{"status":1,"message":"Ok","result":"Ok","errorCode":0}\n0'
|
||
if (!jsonIn.isJson()) return;
|
||
int idx = jsonIn.index_of_icase("\"result\"");
|
||
if (idx < 0) return;
|
||
jsonIn.remove_before(idx + 10); // remove "result":
|
||
jsonIn.truncate_at('"'); // remove after '"' Ok","result":"Ok","errorCode":0} --> OK
|
||
// AUDIO_LOG_INFO("jsonIn: {}", jsonIn.c_get());
|
||
m_streamTitle.unicodeToUTF8(jsonIn.c_get());
|
||
}
|
||
|
||
info(*this, evt_streamtitle, "{}", m_streamTitle.c_get());
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::showCodecParams() {
|
||
|
||
info(*this, evt_info, "Channels: {}", getChannels());
|
||
info(*this, evt_info, "SampleRate (Hz): {}", getSampleRate());
|
||
info(*this, evt_info, "BitsPerSample: {}", getBitsPerSample());
|
||
// if(getBitRate()) { info(*this, evt_info, "BitRate (b/s): {}", getBitRate()); }
|
||
// else { info(*this, evt_info, "BitRate (b/s): N/A"); }
|
||
|
||
if (m_codec == CODEC_AAC) {
|
||
info(*this, evt_info, "{}", m_decoder->arg2()); // AAC Format
|
||
uint8_t answ = m_decoder->val2(); // SBR
|
||
if (answ > 0 && answ < 4) {
|
||
const char sbr[4][50] = {"without SBR", "upsampled SBR", "downsampled SBR", "no SBR used, but file is upsampled by a factor 2"};
|
||
info(*this, evt_info, "Spectral band replication: {}", sbr[answ]);
|
||
}
|
||
}
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
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
|
||
|
||
m_fnsy.nextSync = 0;
|
||
|
||
if (m_codec == CODEC_WAV) {
|
||
m_f_playing = true;
|
||
m_fnsy.nextSync = 0;
|
||
}
|
||
if (m_codec == CODEC_MP3) {
|
||
m_fnsy.nextSync = m_decoder->findSyncWord(data, (int32_t)len);
|
||
if (m_fnsy.nextSync == -1) return len; // syncword not found, search next block
|
||
m_decoder->clear();
|
||
}
|
||
if (m_codec == CODEC_AAC) { m_fnsy.nextSync = m_decoder->findSyncWord(data, (int32_t)len); }
|
||
if (m_codec == CODEC_M4A) {
|
||
if (!m_M4A_chConfig) m_M4A_chConfig = 2; // guard
|
||
if (!m_M4A_sampleRate) m_M4A_sampleRate = 44100;
|
||
if (!m_M4A_objectType) m_M4A_objectType = 2;
|
||
m_decoder->setRawBlockParams(m_M4A_chConfig, m_M4A_sampleRate, 0, m_M4A_objectType, 0);
|
||
m_f_playing = true;
|
||
m_fnsy.nextSync = 0;
|
||
}
|
||
if (m_codec == CODEC_FLAC) {
|
||
m_fnsy.nextSync = m_decoder->findSyncWord(data, (int32_t)len);
|
||
if (m_fnsy.nextSync == -1) return len; // OggS not found, search next block
|
||
}
|
||
if (m_codec == CODEC_OPUS) {
|
||
m_fnsy.nextSync = m_decoder->findSyncWord(data, (int32_t)len);
|
||
if (m_fnsy.nextSync == -1) return len; // OggS not found, search next block
|
||
}
|
||
if (m_codec == CODEC_VORBIS) {
|
||
m_fnsy.nextSync = m_decoder->findSyncWord(data, len);
|
||
if (m_fnsy.nextSync == -1) return len; // OggS not found, search next block
|
||
}
|
||
if (m_fnsy.nextSync == -1) {
|
||
if (m_fnsy.swnf == 0)
|
||
info(*this, evt_info, "syncword not found");
|
||
else {
|
||
m_fnsy.swnf++; // syncword not found counter, can be multimediadata
|
||
}
|
||
}
|
||
if (m_fnsy.nextSync == 0) {
|
||
if (m_fnsy.swnf) {
|
||
info(*this, evt_info, "syncword not found {} times", m_fnsy.swnf);
|
||
m_fnsy.swnf = 0;
|
||
} else {
|
||
info(*this, evt_info, "syncword found at pos 0");
|
||
m_f_decode_ready = true;
|
||
}
|
||
}
|
||
if (m_fnsy.nextSync > 0) { info(*this, evt_info, "syncword found at pos {}", m_fnsy.nextSync); }
|
||
return m_fnsy.nextSync;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::setDecoderItems() {
|
||
setChannels(m_decoder->getChannels());
|
||
setSampleRate(m_decoder->getSampleRate());
|
||
setBitsPerSample(m_decoder->getBitsPerSample());
|
||
if (m_decoder->arg1()) info(*this, evt_info, "{}", m_decoder->arg1());
|
||
if (m_decoder->getAudioDataStart() > 0) { // only flac-ogg, native flac sets audioDataStart in readFlacHeader()
|
||
m_audioDataStart = m_decoder->getAudioDataStart();
|
||
info(*this, evt_info, "AudioDataStart: {}", m_audioDataStart);
|
||
if (m_audioFileSize && !m_audioDataSize) m_audioDataSize = m_audioFileSize - m_audioDataStart;
|
||
}
|
||
if (m_lastGranulePosition && m_audioFileSize && m_i2s_items.sampleRate) {
|
||
m_audioFileDuration = (uint32_t)(m_lastGranulePosition / m_i2s_items.sampleRate);
|
||
m_nominal_bitrate = (m_audioFileSize - m_audioDataStart) * 8 / m_audioFileDuration;
|
||
AUDIO_LOG_DEBUG("m_nominal_bitrate {}, m_lastGranulePosition {}", m_nominal_bitrate, m_lastGranulePosition);
|
||
info(*this, evt_info, "Duration (s): {}", m_audioFileDuration);
|
||
}
|
||
|
||
if (getBitsPerSample() != 8 && getBitsPerSample() != 16 && getBitsPerSample() != 24 && getBitsPerSample() != 32) {
|
||
AUDIO_LOG_ERROR("Bits per sample must be 8, 16, 24 or 32 found {}", getBitsPerSample());
|
||
stopSong();
|
||
}
|
||
|
||
if (getChannels() != 1 && getChannels() != 2) {
|
||
AUDIO_LOG_ERROR("Num of channels must be 1 or 2, found {}", getChannels());
|
||
stopSong();
|
||
}
|
||
showCodecParams();
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
uint32_t Audio::decodeError(int8_t res, uint8_t* data, int32_t bytesDecoded) {
|
||
// for(int i = 0; i < 10; i++){printf("0x%02X ", data[i]);} printf("\n");
|
||
if (res == -100) {
|
||
stopSong();
|
||
return bytesDecoded;
|
||
} // serious error, e.g. decoder could not be initialized
|
||
if (m_codec == CODEC_AAC && res == -21) { // mono <-> stereo change
|
||
// According to the specification, the channel configuration is transferred in the first ADTS header and no longer changes in the entire
|
||
// stream. Some streams send short mono blocks in a stereo stream. e.g. http://mp3.ffh.de/ffhchannels/soundtrack.aac
|
||
// This triggers error -21 because the faad2 decoder cannot switch automatically.
|
||
m_sbyt.channels = 0;
|
||
if ((data[0] == 0xFF) || ((data[1] & 0xF0) == 0xF0)) {
|
||
int channel_config = ((data[2] & 0x01) << 2) | ((data[3] & 0xC0) >> 6);
|
||
if (channel_config != m_sbyt.channels) {
|
||
m_sbyt.channels = channel_config;
|
||
info(*this, evt_info, "AAC channel config changed to {}", m_sbyt.channels);
|
||
}
|
||
}
|
||
m_decoder->reset();
|
||
m_decoder->init();
|
||
return 0;
|
||
}
|
||
if (m_codec == CODEC_MP3) {
|
||
if (res == MP3Decoder::MP3_NEED_RESTART) {
|
||
info(*this, evt_info, "" ANSI_ESC_RED "Network error" ANSI_ESC_RESET "");
|
||
connecttohost(m_lastHost.get());
|
||
return 0;
|
||
}
|
||
}
|
||
m_f_playing = false; // seek for new syncword
|
||
if (bytesDecoded == 0) return 1; // skip one byte and seek for the next sync word
|
||
return bytesDecoded;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
uint32_t Audio::decodeContinue(int8_t res, uint8_t* data, int32_t bytesDecoded, int32_t* bytesLeft) {
|
||
if (m_codec == CODEC_MP3) {
|
||
if (res == MP3Decoder::MP3_NEXT_FRAME) return bytesDecoded;
|
||
}
|
||
return 0;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
int Audio::sendBytes(uint8_t* data, size_t len) {
|
||
if (!m_f_running) return 0; // guard
|
||
if (!m_decoder) return 0; // guard
|
||
|
||
int res = 0;
|
||
int bytesDecoded = 0;
|
||
const char* st = NULL;
|
||
std::vector<uint32_t> vec;
|
||
uint16_t samples_out = 0;
|
||
if (m_validSamples) { goto exit; } // nothing to decode, next round
|
||
|
||
m_sbyt.bytesLeft = 0;
|
||
m_sbyt.nextSync = 0;
|
||
|
||
if (!m_f_playing) {
|
||
m_sbyt.channels = 2; // assume aac stereo
|
||
m_sbyt.isPS = 0;
|
||
m_sbyt.f_setDecodeParamsOnce = true;
|
||
m_sbyt.nextSync = findNextSync(data, len);
|
||
if (m_sbyt.nextSync < 0) return len; // no syncword found
|
||
if (m_sbyt.nextSync == 0) { m_f_playing = true; }
|
||
if (m_sbyt.nextSync > 0) return m_sbyt.nextSync;
|
||
}
|
||
// m_f_playing is true at this pos
|
||
|
||
m_sbyt.bytesLeft = len;
|
||
|
||
if (m_codec == CODEC_NONE && m_playlistFormat == FORMAT_M3U8) return 0; // can happen when the m3u8 playlist is loaded
|
||
if (!m_f_decode_ready) return 0; // find sync first
|
||
|
||
//-----------------------------------------------------------------
|
||
res = m_decoder->decode(data, &m_sbyt.bytesLeft, m_outBuff.get());
|
||
bytesDecoded = len - m_sbyt.bytesLeft;
|
||
//-----------------------------------------------------------------
|
||
|
||
// res - possible values are:
|
||
// 0: okay, no error
|
||
// >= 100: the decoder needs more data
|
||
// < 0: there has been an error
|
||
// -100: serious error, stop song
|
||
|
||
if (res < 0) { return decodeError(res, data, bytesDecoded); } // Error, skip the frame...
|
||
if (res > 99) { return decodeContinue(res, data, bytesDecoded, &m_sbyt.bytesLeft); } // decoder needs more data...
|
||
|
||
if ((bytesDecoded == 0) && (m_codec != CODEC_VORBIS && m_codec != CODEC_FLAC)) { // unlikely framesize, exept VORBIS decodes lastSegmentTable
|
||
info(*this, evt_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 one byte and seek for the next
|
||
return 1;
|
||
}
|
||
// status: bytesDecoded > 0 and res >= 0
|
||
|
||
switch (m_codec) {
|
||
case CODEC_WAV: m_validSamples = m_decoder->getOutputSamples(); break;
|
||
case CODEC_MP3: m_validSamples = m_decoder->getOutputSamples(); break;
|
||
case CODEC_AAC:
|
||
m_validSamples = m_decoder->getOutputSamples() / getChannels();
|
||
if (!m_sbyt.isPS && m_decoder->val1()) { // only change 0 -> 1
|
||
m_sbyt.isPS = 1;
|
||
info(*this, evt_info, "Parametric Stereo");
|
||
} else
|
||
m_sbyt.isPS = m_decoder->val1();
|
||
break;
|
||
case CODEC_M4A: m_validSamples = m_decoder->getOutputSamples() / getChannels(); break;
|
||
case CODEC_FLAC:
|
||
m_validSamples = m_decoder->getOutputSamples();
|
||
st = m_decoder->getStreamTitle();
|
||
if (st) { info(*this, evt_streamtitle, "{}", st); }
|
||
vec = m_decoder->getMetadataBlockPicture();
|
||
if (vec.size() > 0) { // get blockpic data
|
||
// AUDIO_LOG_INFO("---------------------------------------------------------------------------");
|
||
// AUDIO_LOG_INFO("ogg metadata blockpicture found:");
|
||
// for(int i = 0; i < vec.size(); i += 2) { AUDIO_LOG_INFO("segment {:02}, pos {:07}, len {:05}", i / 2, vec[i], vec[i + 1]); }
|
||
// AUDIO_LOG_INFO("---------------------------------------------------------------------------");
|
||
info(*this, evt_image, vec);
|
||
}
|
||
break;
|
||
case CODEC_OPUS:
|
||
m_validSamples = m_decoder->getOutputSamples();
|
||
st = m_decoder->getStreamTitle();
|
||
if (st) { info(*this, evt_streamtitle, st); }
|
||
vec = m_decoder->getMetadataBlockPicture();
|
||
if (vec.size() > 0) { // get blockpic data
|
||
// AUDIO_LOG_INFO("---------------------------------------------------------------------------");
|
||
// AUDIO_LOG_INFO("ogg metadata blockpicture found:");
|
||
// for(int i = 0; i < vec.size(); i += 2) { AUDIO_LOG_INFO("segment {:02}, pos {:07}, len {:05}", i / 2, vec[i], vec[i + 1]); }
|
||
// AUDIO_LOG_INFO("---------------------------------------------------------------------------");
|
||
info(*this, evt_image, vec);
|
||
}
|
||
if (m_decoder->arg1()) {
|
||
if (m_sbyt.opus_mode != m_decoder->arg1()) {
|
||
info(*this, evt_info, m_decoder->arg1());
|
||
m_sbyt.opus_mode = m_decoder->arg1();
|
||
}
|
||
}
|
||
break;
|
||
|
||
case CODEC_VORBIS:
|
||
m_validSamples = m_decoder->getOutputSamples();
|
||
st = m_decoder->getStreamTitle();
|
||
if (st) { info(*this, evt_streamtitle, st); }
|
||
vec = m_decoder->getMetadataBlockPicture();
|
||
if (vec.size() > 0) { // get blockpic data
|
||
// AUDIO_LOG_INFO("---------------------------------------------------------------------------");
|
||
// AUDIO_LOG_INFO("ogg metadata blockpicture found:");
|
||
// for(int i = 0; i < vec.size(); i += 2) { AUDIO_LOG_INFO("segment {:02}, pos {:07}, len {:05}", i / 2, vec[i], vec[i + 1]); }
|
||
// AUDIO_LOG_INFO("---------------------------------------------------------------------------");
|
||
info(*this, evt_image, vec);
|
||
}
|
||
break;
|
||
}
|
||
if (m_sbyt.f_setDecodeParamsOnce && m_validSamples) {
|
||
m_sbyt.f_setDecodeParamsOnce = false;
|
||
setDecoderItems();
|
||
}
|
||
samples_out = m_validSamples;
|
||
|
||
exit:
|
||
m_curSample = 0;
|
||
if (m_validSamples) {
|
||
calculateAudioTime(bytesDecoded, samples_out);
|
||
playChunk();
|
||
}
|
||
return bytesDecoded;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::calculateAudioTime(uint16_t bytesDecoderIn, uint16_t samples_decoder_out) {
|
||
|
||
// if(m_dataMode != AUDIO_LOCALFILE && m_streamType != ST_WEBFILE) return; //guard
|
||
|
||
float audioCurrentTime = 0.0;
|
||
|
||
if (m_cat.firstCall) { // first call
|
||
m_cat.firstCall = false;
|
||
m_cat.reset();
|
||
|
||
if (m_codec == CODEC_FLAC && m_decoder->getAudioFileDuration()) { // BITSTREAMINFO FLAC/OGG
|
||
m_audioFileDuration = m_decoder->getAudioFileDuration();
|
||
m_cat.nominalBitRate = (m_audioDataSize / m_decoder->getAudioFileDuration()) * 8;
|
||
}
|
||
|
||
if (m_nominal_bitrate) {
|
||
info(*this, evt_bitrate, "{}", m_nominal_bitrate);
|
||
m_cat.nominalBitRate = m_nominal_bitrate;
|
||
m_audioFileDuration = round(((float)m_audioDataSize * 8 / m_cat.nominalBitRate));
|
||
if (m_lastGranulePosition)
|
||
m_cat.tota_samples = m_lastGranulePosition;
|
||
else
|
||
m_cat.tota_samples = m_audioFileDuration * m_i2s_items.sampleRate;
|
||
}
|
||
}
|
||
|
||
m_cat.sumBytesIn += bytesDecoderIn;
|
||
m_cat.deltaBytesIn += bytesDecoderIn;
|
||
m_cat.sum_samples += samples_decoder_out;
|
||
|
||
if (m_cat.timeStamp + 50 < millis()) {
|
||
uint32_t t = millis(); // time tracking
|
||
uint32_t delta_t = t - m_cat.timeStamp; // ---"---
|
||
m_cat.timeStamp = t; // ---"---
|
||
|
||
if (m_cat.nominalBitRate) {
|
||
audioCurrentTime = (uint32_t)(m_cat.sum_samples / m_i2s_items.sampleRate);
|
||
} else {
|
||
double instBitRate = (m_cat.deltaBytesIn * 8000.0) / delta_t;
|
||
m_cat.counter++;
|
||
m_cat.avrBitRate += (instBitRate - m_cat.avrBitRate) / m_cat.counter;
|
||
if ((abs(m_cat.avrBitRate - m_cat.oldAvrBitrate < 50)) && !m_cat.avrBitrateStable && m_cat.avrBitRate > 1000) {
|
||
m_cat.brCounter++;
|
||
if (m_cat.brCounter > 6) {
|
||
m_cat.avrBitrateStable = m_cat.avrBitRate;
|
||
info(*this, evt_bitrate, "{}", m_cat.avrBitrateStable); // estimated
|
||
}
|
||
}
|
||
m_avr_bitrate = m_cat.avrBitRate;
|
||
audioCurrentTime = (float)m_cat.sumBytesIn * 8 / m_cat.avrBitRate;
|
||
m_audioFileDuration = round(((float)m_audioDataSize * 8 / m_cat.avrBitRate));
|
||
m_cat.oldAvrBitrate = m_avr_bitrate;
|
||
}
|
||
m_cat.deltaBytesIn = 0;
|
||
m_audioCurrentTime = round(audioCurrentTime);
|
||
}
|
||
|
||
if (m_haveNewFilePos && (m_cat.avrBitRate || m_cat.nominalBitRate)) {
|
||
uint32_t posWhithinAudioBlock = m_haveNewFilePos - m_audioDataStart;
|
||
float newTime = 0;
|
||
if (m_cat.nominalBitRate) {
|
||
newTime = (float)posWhithinAudioBlock / (m_cat.nominalBitRate / 8);
|
||
} else {
|
||
newTime = (float)posWhithinAudioBlock / (m_cat.avrBitRate / 8);
|
||
m_avr_bitrate = m_cat.avrBitRate;
|
||
}
|
||
m_audioCurrentTime = round(newTime);
|
||
m_cat.sumBytesIn = posWhithinAudioBlock;
|
||
m_haveNewFilePos = 0;
|
||
m_cat.syltIdx = 0;
|
||
if (m_syltLines.size()) {
|
||
while (m_cat.syltIdx < m_syltLines.size()) {
|
||
if (m_audioCurrentTime * 1000 < m_syltTimeStamp[m_cat.syltIdx]) break;
|
||
m_cat.syltIdx++;
|
||
}
|
||
if (m_cat.syltIdx) m_cat.syltIdx--;
|
||
}
|
||
}
|
||
|
||
if (m_syltLines.size()) {
|
||
// AUDIO_LOG_INFO("{}", audioCurrentTime * 1000); // ms
|
||
if (m_cat.syltIdx >= m_syltLines.size()) return;
|
||
if (m_audioCurrentTime * 1000 > m_syltTimeStamp[m_cat.syltIdx]) {
|
||
info(*this, evt_lyrics, "{}", m_syltLines[m_cat.syltIdx].c_get());
|
||
m_cat.syltIdx++;
|
||
}
|
||
}
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::i2s_config() {
|
||
esp_err_t result = ESP_OK;
|
||
// -------- I2S configuration -------------------------------------------------------------------------------------------
|
||
memset(&m_i2s_chan_cfg, 0, sizeof(i2s_chan_config_t));
|
||
#if (ESP_IDF_VERSION_MAJOR < 6)
|
||
m_i2s_chan_cfg.id = (i2s_port_t)m_i2s_items.i2s_num; // I2S_NUM_AUTO, I2S_NUM_0, I2S_NUM_1
|
||
#else
|
||
m_i2s_chan_cfg.id = m_i2s_items.i2s_num; // I2S_NUM_AUTO, I2S_NUM_0, I2S_NUM_1
|
||
#endif
|
||
m_i2s_chan_cfg.role = I2S_ROLE_MASTER; // I2S controller master role, bclk and lrc signal will be set to output
|
||
m_i2s_chan_cfg.dma_desc_num = settings.DMA_DESC_NUM; // number of DMA buffer
|
||
m_i2s_chan_cfg.dma_frame_num = settings.DMA_FRAME_NUM; // I2S frame number in one DMA buffer.
|
||
m_i2s_chan_cfg.auto_clear = true; // i2s will always send zero automatically if no data to send
|
||
// m_i2s_chan_cfg.allow_pd = false;
|
||
// m_i2s_chan_cfg.intr_priority = 2;
|
||
result = i2s_new_channel(&m_i2s_chan_cfg, &m_i2s_tx_handle, NULL);
|
||
if (result != ESP_OK) { // ESP_ERR_INVALID_ARG?
|
||
AUDIO_LOG_ERROR("I2S channel: invalid argument");
|
||
return false;
|
||
}
|
||
|
||
memset(&m_i2s_std_cfg, 0, sizeof(i2s_std_config_t));
|
||
m_i2s_std_cfg.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_STEREO); // Set to enable bit shift in Philips mode
|
||
m_i2s_std_cfg.gpio_cfg.bclk = I2S_GPIO_UNUSED; // BCLK, Assignment in setPinout()
|
||
m_i2s_std_cfg.gpio_cfg.din = I2S_GPIO_UNUSED; // not used
|
||
m_i2s_std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED; // DOUT, Assignment in setPinout()
|
||
m_i2s_std_cfg.gpio_cfg.mclk = I2S_GPIO_UNUSED; // MCLK, Assignment in setPinout()
|
||
m_i2s_std_cfg.gpio_cfg.ws = I2S_GPIO_UNUSED; // LRC, Assignment in setPinout()
|
||
m_i2s_std_cfg.gpio_cfg.invert_flags.mclk_inv = false;
|
||
m_i2s_std_cfg.gpio_cfg.invert_flags.bclk_inv = false;
|
||
m_i2s_std_cfg.gpio_cfg.invert_flags.ws_inv = false;
|
||
m_i2s_std_cfg.clk_cfg.sample_rate_hz = m_i2s_items.sampleRate;
|
||
m_i2s_std_cfg.clk_cfg.clk_src = I2S_CLK_SRC_DEFAULT;
|
||
m_i2s_std_cfg.clk_cfg.mclk_multiple = I2S_MCLK_MULTIPLE_256;
|
||
result = i2s_channel_init_std_mode(m_i2s_tx_handle, &m_i2s_std_cfg);
|
||
if (result != ESP_OK) {
|
||
if (result == ESP_ERR_INVALID_ARG) AUDIO_LOG_ERROR("invalid i2s configuration or i2s not standard mode");
|
||
if (result == ESP_ERR_INVALID_STATE) AUDIO_LOG_ERROR("This i2s channel is not initialized or not stopped");
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::setPinout(uint8_t BCLK, uint8_t LRC, uint8_t DOUT, int8_t MCLK) {
|
||
|
||
i2s_std_gpio_config_t gpio_cfg = {};
|
||
bool result = i2s_config();
|
||
|
||
gpio_cfg.bclk = (gpio_num_t)BCLK;
|
||
gpio_cfg.din = (gpio_num_t)I2S_GPIO_UNUSED;
|
||
gpio_cfg.dout = (gpio_num_t)DOUT;
|
||
gpio_cfg.mclk = (gpio_num_t)MCLK;
|
||
gpio_cfg.ws = (gpio_num_t)LRC;
|
||
|
||
#if (ESP_ARDUINO_VERSION_MAJOR < 3)
|
||
AUDIO_LOG_ERROR("Arduino Version must be 3.0.0 or higher!");
|
||
result = false;
|
||
goto exit;
|
||
#endif
|
||
|
||
m_f_psramFound = psramInit();
|
||
if (!m_f_psramFound) {
|
||
AUDIO_LOG_ERROR("PSRAM not found");
|
||
result = false;
|
||
goto exit;
|
||
}
|
||
|
||
m_outBuff.alloc_array(m_outbuffSize, "m_outBuff");
|
||
m_resamplesBuff.alloc_array(m_resamplesBuffSize, "m_resamplesBuff");
|
||
m_vu_items.delay_l.alloc_array(m_i2s_chan_cfg.dma_desc_num * m_i2s_chan_cfg.dma_frame_num, "delay_l");
|
||
m_vu_items.delay_r.alloc_array(m_i2s_chan_cfg.dma_desc_num * m_i2s_chan_cfg.dma_frame_num, "delay_r");
|
||
m_fft_items.buffer.alloc_array(m_fft_items.SIZE, "buffer");
|
||
m_fft_items.window.alloc_array(m_fft_items.SIZE, "window");
|
||
m_fft_items.work.alloc_array(m_fft_items.SIZE * 2, "work");
|
||
m_metadataBuff.alloc(4096 + 1, "m_metadataBuff"); // max 4096 + 1 for null terminator, just to make library code 'safe'
|
||
m_httpRespHdrBuff.alloc(4096, "m_httpRespHdrBuff"); // enough space to store http response header
|
||
|
||
if (!m_outBuff.valid() || !m_vu_items.delay_l.valid() || !m_vu_items.delay_r.valid() || !m_resamplesBuff.valid() || !m_fft_items.buffer.valid() || !m_fft_items.buffer.valid() ||
|
||
!m_fft_items.work.valid()) {
|
||
result = false;
|
||
goto exit;
|
||
}
|
||
|
||
//---------------------------- FFT INIT-----------------------------------------
|
||
for (int i = 0; i < m_fft_items.SIZE; i++) { // Hann window
|
||
m_fft_items.window[i] = 0.5f * (1.0f - cosf(2.0f * M_PI * i / (m_fft_items.SIZE - 1)));
|
||
}
|
||
|
||
if (dsps_fft2r_init_fc32(nullptr, m_fft_items.SIZE) == ESP_OK) {
|
||
m_fft_items.initialized = true;
|
||
} else {
|
||
AUDIO_LOG_ERROR("FFT init failed (size={})", m_fft_items.SIZE);
|
||
result = false;
|
||
goto exit;
|
||
}
|
||
//-----------------------------------------------------------------------------
|
||
|
||
I2Sstop();
|
||
if (i2s_channel_reconfig_std_gpio(m_i2s_tx_handle, &gpio_cfg) != ESP_OK) {
|
||
result = false;
|
||
goto exit;
|
||
}
|
||
I2Sstart();
|
||
|
||
exit:
|
||
|
||
calculateVolumeLimits(); // first init, vol = 21, vol_steps = 21
|
||
startAudioTask();
|
||
|
||
m_f_I2S_init = result;
|
||
return m_f_I2S_init;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
uint32_t Audio::getFileSize() { // returns the size of webfile or local file
|
||
if (!m_audiofile) {
|
||
if (m_audioFileSize > 0) { return m_audioFileSize; }
|
||
return 0;
|
||
}
|
||
return m_audiofile.size();
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
uint32_t Audio::getAudioFileDuration() {
|
||
if (!getBitRate()) return 0;
|
||
if (m_playlistFormat == FORMAT_M3U8) return 0;
|
||
if (!m_audioDataSize) return 0;
|
||
return m_audioFileDuration;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
uint32_t Audio::getAudioCurrentTime() { // return current time in seconds
|
||
return m_audioCurrentTime;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
uint32_t Audio::getAudioFilePosition() {
|
||
if (!m_f_stream) return 0;
|
||
if ((m_dataMode != AUDIO_LOCALFILE) && (m_streamType != ST_WEBFILE)) {
|
||
AUDIO_LOG_WARN("audio is not a file");
|
||
return 0;
|
||
}
|
||
return m_audioFilePosition - inBufferFilled();
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::setAudioFilePosition(uint32_t pos) {
|
||
if (!m_f_stream) return false;
|
||
if ((m_dataMode != AUDIO_LOCALFILE) && (m_streamType != ST_WEBFILE)) {
|
||
AUDIO_LOG_WARN("audio is not a file");
|
||
return false;
|
||
}
|
||
if ((m_streamType == ST_WEBFILE) && (!m_f_acceptRanges)) {
|
||
AUDIO_LOG_WARN("server does not accept ranges");
|
||
return false;
|
||
}
|
||
if (pos > m_audioDataStart + m_audioDataSize) {
|
||
AUDIO_LOG_WARN("given position is too large");
|
||
return false;
|
||
}
|
||
if (pos < m_audioDataStart) {
|
||
AUDIO_LOG_WARN("set audiodatastart at {}", m_audioDataStart);
|
||
m_resumeFilePos = m_audioDataStart;
|
||
}
|
||
m_resumeFilePos = pos;
|
||
|
||
/* m_cat.tota_samples m_cat.sum_samples
|
||
------------------ = ----------------------------------
|
||
m_audioDataSize m_resumeFilePos - m_audioDataStart */
|
||
|
||
m_cat.sum_samples = (float)m_cat.tota_samples * ((float)(m_resumeFilePos - m_audioDataStart) / m_audioDataSize);
|
||
return true;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::setAudioPlayTime(uint16_t sec) {
|
||
// e.g. setAudioPlayTime(300) sets the pointer at pos 5 min
|
||
if ((m_dataMode != AUDIO_LOCALFILE) && (m_streamType != ST_WEBFILE)) return false; // guard
|
||
if (!getBitRate()) return false; // guard
|
||
if (!m_f_running) return false; // guard
|
||
|
||
if (sec > getAudioFileDuration()) {
|
||
AUDIO_LOG_WARN("setAudioPlayTime: {}s >= audioFileDuration: {}s -> stopSong", sec, getAudioFileDuration());
|
||
stopSong();
|
||
return false;
|
||
}
|
||
uint32_t filepos = m_audioDataStart + (getBitRate() * sec / 8);
|
||
m_resumeFilePos = filepos;
|
||
m_cat.sum_samples = (float)m_cat.tota_samples * ((float)(m_resumeFilePos - m_audioDataStart) / m_audioDataSize);
|
||
return true;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::setTimeOffset(int sec) { // fast forward or rewind the current position in seconds
|
||
// info(*this, evt_info, "time offset {} sec", sec);
|
||
if ((m_dataMode != AUDIO_LOCALFILE) && (m_streamType != ST_WEBFILE)) {
|
||
AUDIO_LOG_WARN("{}", "not a file");
|
||
return false;
|
||
} // guard
|
||
if (!getBitRate()) return false; // guard
|
||
if (!m_f_running) return false; // guard
|
||
|
||
int32_t newTime = getAudioCurrentTime() + sec;
|
||
if (newTime < 0) newTime = 0;
|
||
if (newTime > getAudioFileDuration()) {
|
||
stopSong();
|
||
return true;
|
||
}
|
||
|
||
uint32_t oneSec = getBitRate() / 8; // bytes decoded in one sec
|
||
int32_t offset = oneSec * sec; // bytes to be wind/rewind
|
||
int32_t pos = m_audioFilePosition - inBufferFilled();
|
||
pos += offset;
|
||
m_resumeFilePos = pos;
|
||
m_cat.sum_samples = (float)m_cat.tota_samples * ((float)(m_resumeFilePos - m_audioDataStart) / m_audioDataSize);
|
||
return true;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::setVolumeSteps(uint8_t steps) {
|
||
// clamp new steps
|
||
uint8_t new_steps = max(steps, (uint8_t)21);
|
||
uint8_t old_steps = m_audio_items.volume_steps;
|
||
|
||
if (new_steps == old_steps) return;
|
||
|
||
// ratio OLD → NEW
|
||
float corr = (float)new_steps / (float)old_steps;
|
||
|
||
// scale target volume
|
||
float new_volume = (float)m_audio_items.volume * corr;
|
||
|
||
// also scale the current volume (very important!)
|
||
m_audio_items.cur_volume *= corr;
|
||
|
||
// take over + clamp
|
||
m_audio_items.volume_steps = new_steps;
|
||
|
||
m_audio_items.volume = (uint8_t)lroundf(new_volume);
|
||
if (m_audio_items.volume > new_steps) m_audio_items.volume = new_steps;
|
||
|
||
if (m_audio_items.cur_volume > (float)new_steps) m_audio_items.cur_volume = (float)new_steps;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
uint8_t Audio::getVolumeSteps() {
|
||
return m_audio_items.volume_steps;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::setMute(bool mute) {
|
||
m_audio_items.mute = mute;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::getMute() {
|
||
return m_audio_items.mute;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
int32_t Audio::audioFileRead() {
|
||
int res = -1;
|
||
if (m_dataMode == AUDIO_LOCALFILE) {
|
||
res = m_audiofile.read();
|
||
if (res >= 0) m_audioFilePosition++;
|
||
} else {
|
||
res = m_client->read();
|
||
if (res >= 0) m_audioFilePosition++;
|
||
}
|
||
return res;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
int32_t Audio::audioFileRead(uint16_t timeout_ms) {
|
||
int32_t res = -1;
|
||
uint32_t timeout = millis() + timeout_ms;
|
||
|
||
while (true) {
|
||
res = audioFileRead();
|
||
if (res >= 0) break;
|
||
if (timeout < millis()) {
|
||
AUDIO_LOG_ERROR("timeout");
|
||
return -1;
|
||
}
|
||
vTaskDelay(10);
|
||
}
|
||
return res;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
int32_t Audio::audioFileRead(uint8_t* buff, size_t len) {
|
||
if (buff && len == 0) return 0; // nothing to do
|
||
int32_t readed_bytes = 0;
|
||
uint32_t offset = 0;
|
||
int res = -1;
|
||
|
||
// read len
|
||
if (m_dataMode == AUDIO_LOCALFILE) {
|
||
readed_bytes = m_audiofile.read(buff + offset, len);
|
||
if (readed_bytes >= 0) {
|
||
m_audioFilePosition += readed_bytes;
|
||
len -= readed_bytes;
|
||
offset += readed_bytes;
|
||
res = offset;
|
||
}
|
||
} else {
|
||
readed_bytes = m_client->read(buff + offset, len);
|
||
if (readed_bytes > 0) {
|
||
m_audioFilePosition += readed_bytes;
|
||
len -= readed_bytes;
|
||
offset += readed_bytes;
|
||
res = offset;
|
||
}
|
||
}
|
||
return res;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
int32_t Audio::audioFileRead(uint8_t* buff, size_t len, uint16_t timeout_ms) {
|
||
|
||
uint32_t timeout = millis() + timeout_ms;
|
||
int32_t bytes_has_read = 0;
|
||
uint8_t cnt = 0;
|
||
while (bytes_has_read < len) {
|
||
int32_t res = audioFileRead(buff + bytes_has_read, len - bytes_has_read);
|
||
if (res <= 0) { vTaskDelay(10); }
|
||
if (res > 0) { bytes_has_read += res; }
|
||
if (timeout < millis()) {
|
||
AUDIO_LOG_ERROR("timeout, len: {} != bytes_has_read: {}", len, bytes_has_read);
|
||
return -1;
|
||
}
|
||
AUDIO_LOG_DEBUG("buffFillValue {}, res {}, bytes_has_read {}", len, res, bytes_has_read);
|
||
}
|
||
AUDIO_LOG_DEBUG("len: {} != bytes_has_read: {}", len, bytes_has_read);
|
||
return bytes_has_read;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
int32_t Audio::audioFileSeek(uint32_t position, size_t len) {
|
||
int32_t res = -1;
|
||
|
||
if (m_dataMode == AUDIO_LOCALFILE) {
|
||
uint32_t actualPos = m_audiofile.position(); // starts with 1
|
||
if (actualPos != m_audioFilePosition) {
|
||
AUDIO_LOG_DEBUG("actualPos != m_audioFilePosition {} != {}", actualPos, m_audioFilePosition);
|
||
m_audioFilePosition = actualPos;
|
||
}
|
||
if (!m_audiofile) return -1;
|
||
if (position > m_audiofile.size()) {
|
||
AUDIO_LOG_WARN("position larger than size {} > {}", position, m_audiofile.size());
|
||
position = m_audiofile.size();
|
||
}
|
||
bool r = m_audiofile.seek(position);
|
||
m_audioFilePosition = m_audiofile.position();
|
||
if (r == false) {
|
||
AUDIO_LOG_ERROR("something went wrong");
|
||
return -1;
|
||
} else {
|
||
return position;
|
||
}
|
||
} else {
|
||
if (m_f_acceptRanges) {
|
||
bool r;
|
||
if (len == 0) len = UINT32_MAX;
|
||
r = httpRange(position, len);
|
||
if (res == false) {
|
||
AUDIO_LOG_ERROR("http range request was not successful");
|
||
return 0;
|
||
}
|
||
r = parseHttpRangeHeader();
|
||
if (r == false) {
|
||
AUDIO_LOG_ERROR("http range response was not successful");
|
||
return 0;
|
||
}
|
||
m_audioFilePosition = position;
|
||
return position;
|
||
}
|
||
}
|
||
return res;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::fsRange(uint32_t range) {
|
||
|
||
if ((m_dataMode == AUDIO_LOCALFILE) && !m_audiofile) {
|
||
AUDIO_LOG_WARN("{}", "local file not accessibble");
|
||
return false;
|
||
} // guard
|
||
uint32_t startAB = m_audioDataStart; // audioblock begin
|
||
uint32_t endAB = m_audioDataStart + m_audioDataSize; // audioblock end
|
||
if (range < (int32_t)startAB) { range = startAB; }
|
||
if (range >= (int32_t)endAB) { range = endAB; }
|
||
|
||
m_validSamples = 0;
|
||
m_resumeFilePos = range; // used in processLocalFile()
|
||
return true;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::setSampleRate(uint32_t sampRate) {
|
||
|
||
if (!sampRate) return false;
|
||
if (sampRate < 8000) {
|
||
AUDIO_LOG_WARN("Sample rate must not be smaller than 8kHz, found: {}", sampRate);
|
||
return false;
|
||
}
|
||
m_i2s_items.sampleRate = sampRate;
|
||
reconfigI2S();
|
||
IIR_calculateCoefficients();
|
||
return true;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
uint32_t Audio::getSampleRate() {
|
||
return m_i2s_items.sampleRate;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::setBitsPerSample(int bits) {
|
||
if ((bits != 32) && (bits != 24) && (bits != 16) && (bits != 8)) return false;
|
||
m_bitsPerSample = bits;
|
||
return true;
|
||
}
|
||
uint8_t Audio::getBitsPerSample() {
|
||
return m_bitsPerSample;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::setChannels(int ch) {
|
||
m_channels = ch;
|
||
return true;
|
||
}
|
||
uint8_t Audio::getChannels() {
|
||
if (m_channels == 0) { // this should not happen! #209
|
||
m_channels = 2;
|
||
}
|
||
return m_channels;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
uint32_t Audio::getBitRate() {
|
||
if (m_nominal_bitrate) return m_nominal_bitrate;
|
||
return m_avr_bitrate;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
||
uint64_t Audio::getLastGranulePosition(uint8_t codec) {
|
||
if (codec != CODEC_OPUS && codec != CODEC_VORBIS) return 0; // only opus or vorbis
|
||
if (m_audioFileSize == 0) { return 0; } // only files
|
||
|
||
uint64_t granulePos = 0;
|
||
ps_ptr<char> buff;
|
||
buff.alloc(UINT16_MAX, "buff");
|
||
|
||
int rangeStart = m_audioFileSize - UINT16_MAX - 1;
|
||
audioFileSeek(rangeStart, UINT16_MAX);
|
||
audioFileRead((uint8_t*)buff.get(), UINT16_MAX, 3000);
|
||
|
||
int32_t pos = buff.last_special_index_of("OggS", UINT16_MAX);
|
||
if (buff[pos + 5] & 0x04) { // is last page;
|
||
for (int j = 0; j < 8; j++) { granulePos |= ((uint64_t)buff[pos + 6 + j] << (j * 8)); }
|
||
}
|
||
AUDIO_LOG_DEBUG("granulePos {}", granulePos);
|
||
|
||
m_resumeFilePos = 0;
|
||
return granulePos;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
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) {
|
||
info(*this, evt_info, "commFMT = LSBJ (Least Significant Bit Justified)");
|
||
} else {
|
||
info(*this, evt_info, "commFMT = Philips");
|
||
}
|
||
m_i2s_items.commFMT = commFMT;
|
||
reconfigI2S();
|
||
return;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::reconfigI2S() {
|
||
|
||
i2s_channel_disable(m_i2s_tx_handle);
|
||
|
||
if (m_i2s_items.commFMT) {
|
||
m_i2s_std_cfg.slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_STEREO);
|
||
} else {
|
||
m_i2s_std_cfg.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_STEREO);
|
||
}
|
||
i2s_channel_reconfig_std_slot(m_i2s_tx_handle, &m_i2s_std_cfg.slot_cfg);
|
||
|
||
if (m_output_sr) {
|
||
m_resampler.phase = 0; // prepare resampler
|
||
m_resampler.phaseStep = ((uint64_t)m_i2s_items.sampleRate << 32) / m_output_sr;
|
||
m_resampler.g_lpCoeffs = makeButterworthLPF_Q31(m_i2s_items.sampleRate);
|
||
m_resampler.lpLeft = {};
|
||
m_resampler.lpRight = {};
|
||
m_i2s_std_cfg.clk_cfg.sample_rate_hz = m_output_sr;
|
||
AUDIO_LOG_DEBUG("output samplerate is {}", m_i2s_std_cfg.clk_cfg.sample_rate_hz);
|
||
} else {
|
||
m_i2s_std_cfg.clk_cfg.sample_rate_hz = m_i2s_items.sampleRate;
|
||
}
|
||
i2s_channel_reconfig_std_clock(m_i2s_tx_handle, &m_i2s_std_cfg.clk_cfg);
|
||
i2s_channel_enable(m_i2s_tx_handle);
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::calculateVUlevel(int32_t* sample) { // Envelope-Follower
|
||
|
||
uint16_t DELAY_BUFFER_SIZE = m_i2s_chan_cfg.dma_desc_num * m_i2s_chan_cfg.dma_frame_num; // Runtime in I2S-DMA
|
||
// delay line
|
||
m_vu_items.delay_l[m_vu_items.delay_line_index] = sample[LEFTCHANNEL];
|
||
m_vu_items.delay_r[m_vu_items.delay_line_index] = sample[RIGHTCHANNEL];
|
||
m_vu_items.delay_line_index++;
|
||
if (m_vu_items.delay_line_index == DELAY_BUFFER_SIZE) m_vu_items.delay_line_index = 0;
|
||
|
||
int16_t pos = m_vu_items.delay_line_index - 1;
|
||
if (pos == -1) pos = DELAY_BUFFER_SIZE - 1;
|
||
|
||
// FFT buffer
|
||
float mono = 0.5f * (float)((m_vu_items.delay_l[pos] >> 20) + (m_vu_items.delay_r[pos] >> 20));
|
||
|
||
// --- FFT analyzer AGC ---
|
||
constexpr float TARGET = 0.1f; // gewünschte RMS-Amplitude
|
||
constexpr float ATTACK = 0.05f;
|
||
constexpr float RELEASE_FFT = 0.005f;
|
||
|
||
float level = fabsf(mono);
|
||
|
||
if (level > 1e-6f) {
|
||
float desired = TARGET / level;
|
||
if (desired < m_fft_items.gain)
|
||
m_fft_items.gain += ATTACK * (desired - m_fft_items.gain);
|
||
else
|
||
m_fft_items.gain += RELEASE_FFT * (desired - m_fft_items.gain);
|
||
}
|
||
if (fabsf(mono) < 1e-4f) mono = 0.0f;
|
||
mono *= m_fft_items.gain;
|
||
|
||
m_fft_items.buffer[m_fft_items.buffer_index] = mono;
|
||
m_fft_items.buffer_index++;
|
||
if (m_fft_items.buffer_index == m_fft_items.SIZE) m_fft_items.buffer_index = 0;
|
||
|
||
uint8_t l = 0, r = 0;
|
||
l = abs(m_vu_items.delay_l[pos] >> 23);
|
||
r = abs(m_vu_items.delay_r[pos] >> 23);
|
||
|
||
// Attack immediately
|
||
constexpr float RELEASE = 1.0f; // the bigger, the more sluggish
|
||
if (l > m_vu_items.left) {
|
||
m_vu_items.left = l;
|
||
} else if (m_vu_items.left > RELEASE) {
|
||
m_vu_items.left -= RELEASE;
|
||
}
|
||
if (r > m_vu_items.right) {
|
||
m_vu_items.right = r;
|
||
} else if (m_vu_items.right > RELEASE) {
|
||
m_vu_items.right -= RELEASE;
|
||
}
|
||
|
||
// LEFT
|
||
if (m_vu_items.left > m_vu_items.left_peak) {
|
||
m_vu_items.left_peak = m_vu_items.left;
|
||
m_vu_items.left_hold = settings.PEAK_HOLD_SAMPLES;
|
||
} else {
|
||
if (m_vu_items.left_hold > 0) {
|
||
m_vu_items.left_hold--;
|
||
} else if (m_vu_items.left_peak > settings.PEAK_RELEASE) {
|
||
m_vu_items.left_peak -= settings.PEAK_RELEASE;
|
||
}
|
||
}
|
||
|
||
// RIGHT
|
||
if (m_vu_items.right > m_vu_items.right_peak) {
|
||
m_vu_items.right_peak = m_vu_items.right;
|
||
m_vu_items.right_hold = settings.PEAK_HOLD_SAMPLES;
|
||
} else {
|
||
if (m_vu_items.right_hold > 0) {
|
||
m_vu_items.right_hold--;
|
||
} else if (m_vu_items.right_peak > settings.PEAK_RELEASE) {
|
||
m_vu_items.right_peak -= settings.PEAK_RELEASE;
|
||
}
|
||
}
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
uint16_t Audio::getVUlevel() {
|
||
if (!m_f_running) return 0;
|
||
AUDIO_LOG_DEBUG("{}", m_vu_items.left);
|
||
// avg 0 ... 255 MSB LSB
|
||
return ((uint8_t)m_vu_items.right_peak << 8) + (uint8_t)m_vu_items.left_peak; // returns rrrrrrrrllllllll
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::setTone(float gainLowPass, float gainBandPass, float gainHighPass) {
|
||
|
||
// gainLowPass set between -12 ... +12 dB
|
||
// gainBandPass set between -12 ... +12 dB
|
||
// gainHighPass set between -12 ... +12 dB
|
||
|
||
m_audio_items.gain_ls_db = fminf(fmaxf(gainLowPass, -12.0f), 12.0f);
|
||
m_audio_items.gain_peq_db = fminf(fmaxf(gainBandPass, -12.0f), 12.0f);
|
||
m_audio_items.gain_hs_db = fminf(fmaxf(gainHighPass, -12.0f), 12.0f);
|
||
|
||
IIR_calculateCoefficients();
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::forceMono(bool m) { // #100 mono option
|
||
m_f_forceMono = m; // false stereo, true mono
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::setOutputSampleRate(OutputSR_t sr) { //
|
||
if (sr == SR_44100) {
|
||
m_output_sr = SR_44100; // output sr is always 44.1KHz
|
||
} else if (sr == SR_48000) {
|
||
m_output_sr = SR_48000; // output sr is always 48KHz
|
||
} else {
|
||
m_output_sr = SR_ORIGIN; // output sr is source sr
|
||
}
|
||
reconfigI2S();
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::setBalance(float balance) { // left -16.0dB ... 0dB ... -16.0dB right
|
||
m_audio_items.balance = fminf(fmaxf(balance, -16.0f), 16.0f);
|
||
calculateVolumeLimits();
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::setVolume(uint8_t volume, uint8_t curve) {
|
||
|
||
m_audio_items.volume = min(volume, m_audio_items.volume_steps);
|
||
|
||
// curve is obsolete
|
||
calculateVolumeLimits();
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::setVolumeCurve(VolumeCurveFn curve) {
|
||
|
||
// user defined curve, set it in youe code e.g. somewhere in setup()
|
||
// t is volume / steps; (range 0.0f … 1.0f)
|
||
// should return a value between -60.0[dB] and 0.0[dB]
|
||
// example:
|
||
// audio.setVolumeCurve([](float t) {
|
||
// return -60.0f + 60.0f * t * t;
|
||
// });
|
||
|
||
m_volumeCurve = curve;
|
||
calculateVolumeLimits();
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
uint8_t Audio::getVolume() {
|
||
return m_audio_items.volume;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
int32_t Audio::getI2sPort() {
|
||
return m_i2s_items.i2s_num;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::gain_ramp() {
|
||
settings.VOL_FADING_SPEED = std::clamp(settings.VOL_FADING_SPEED, 1.0f, 100.0f); // avoid div0
|
||
// determine goal
|
||
float target = m_audio_items.mute ? 0.0f : (float)m_audio_items.volume;
|
||
|
||
// maximum change per tick
|
||
float step = (float)m_audio_items.volume_steps / settings.VOL_FADING_SPEED;
|
||
|
||
float diff = target - m_audio_items.cur_volume;
|
||
|
||
if (fabsf(diff) <= step) {
|
||
// Goal achieved (no oscillation!)
|
||
m_audio_items.cur_volume = target;
|
||
} else {
|
||
// Ramp
|
||
m_audio_items.cur_volume += (diff > 0.0f ? step : -step);
|
||
}
|
||
|
||
// Security-Clamp
|
||
if (m_audio_items.cur_volume < 0.0f)
|
||
m_audio_items.cur_volume = 0.0f;
|
||
else if (m_audio_items.cur_volume > m_audio_items.volume_steps)
|
||
m_audio_items.cur_volume = m_audio_items.volume_steps;
|
||
|
||
calculateVolumeLimits();
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::calculateVolumeLimits() { // is calculated when the volume or balance changes
|
||
|
||
constexpr float BALANCE_DB = -16.0f;
|
||
constexpr float MIN_DB = -60.0f; // quiet
|
||
constexpr float MAX_DB = 0.0f; // full level
|
||
|
||
auto volumeToLinear = [&](float volume, uint8_t steps) {
|
||
if (volume <= 0.0f) {
|
||
return 0.0f; // real silence
|
||
}
|
||
|
||
float t = volume / (float)steps; // 0.0f … 1.0f
|
||
t = fminf(fmaxf(t, 0.0f), 1.0f);
|
||
|
||
// float dB = MIN_DB + t * (MAX_DB - MIN_DB);
|
||
float dB = m_volumeCurve ? m_volumeCurve(t) : (-112.0f * t * t * t + 172.0f * t * t + MIN_DB);
|
||
dB = fminf(fmaxf(dB, MIN_DB), MAX_DB);
|
||
|
||
return powf(10.0f, dB / 20.0f);
|
||
};
|
||
|
||
float vol = volumeToLinear(m_audio_items.cur_volume, m_audio_items.volume_steps);
|
||
|
||
float l_db = 0.0f;
|
||
float r_db = 0.0f;
|
||
|
||
if (m_audio_items.balance > 0.0f) { // emphasize on the right → quieter on the left
|
||
l_db = BALANCE_DB * ((float)m_audio_items.balance / 16.0f);
|
||
} else if (m_audio_items.balance < 0.0f) { // emphasize on the left → quieter on the right
|
||
r_db = BALANCE_DB * ((float)-m_audio_items.balance / 16.0f);
|
||
}
|
||
|
||
m_audio_items.limiter[LEFTCHANNEL] = vol * powf(10.0f, l_db / 20.0f);
|
||
m_audio_items.limiter[RIGHTCHANNEL] = vol * powf(10.0f, r_db / 20.0f);
|
||
AUDIO_LOG_DEBUG("m_limiter[LEFTCHANNEL] {}, m_limiter[RIGHTCHANNEL] {}", m_audio_items.limiter[LEFTCHANNEL], m_audio_items.limiter[RIGHTCHANNEL]);
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::processSpectrum() {
|
||
|
||
// --- 10 Hz update ---
|
||
uint32_t now = millis();
|
||
if (now - m_fft_items.last_ms < 100) return;
|
||
m_fft_items.last_ms = now;
|
||
|
||
// --- Window + real → complex ---
|
||
for (int i = 0; i < m_fft_items.SIZE; i++) {
|
||
m_fft_items.work[2 * i] = m_fft_items.buffer[i] * m_fft_items.window[i];
|
||
m_fft_items.work[2 * i + 1] = 0.0f;
|
||
}
|
||
|
||
// --- FFT ---
|
||
dsps_fft2r_fc32(m_fft_items.work.get(), m_fft_items.SIZE);
|
||
dsps_bit_rev_fc32(m_fft_items.work.get(), m_fft_items.SIZE);
|
||
dsps_cplx2reC_fc32(m_fft_items.work.get(), m_fft_items.SIZE);
|
||
|
||
const float bin_hz = (float)m_i2s_items.sampleRate / m_fft_items.SIZE;
|
||
const float norm = 2.0f / m_fft_items.SIZE;
|
||
|
||
// --- 5 internal bands ---
|
||
float band[m_fft_items.BANDS] = {0};
|
||
int bins[m_fft_items.BANDS] = {0};
|
||
|
||
for (int i = 1; i < m_fft_items.SIZE / 2; i++) {
|
||
|
||
float re = m_fft_items.work[2 * i];
|
||
float im = m_fft_items.work[2 * i + 1];
|
||
float mag = sqrtf(re * re + im * im) * norm;
|
||
float f = i * bin_hz;
|
||
|
||
int b = -1;
|
||
if (f < 250)
|
||
b = 0;
|
||
else if (f < 400)
|
||
b = -1;
|
||
else if (f < 700)
|
||
b = 1;
|
||
else if (f < 1000)
|
||
b = -1;
|
||
else if (f < 1550)
|
||
b = 2;
|
||
else if (f < 2200)
|
||
b = -1;
|
||
else if (f < 4250)
|
||
b = 3;
|
||
else if (f < 6300)
|
||
b = -1;
|
||
else if (f < 11150)
|
||
b = 4;
|
||
else if (f < 16000)
|
||
b = 5;
|
||
else
|
||
b = -1;
|
||
|
||
if (b >= 0) {
|
||
band[b] += mag * mag;
|
||
bins[b]++;
|
||
}
|
||
}
|
||
|
||
// --- RMS + weighting ---
|
||
for (int i = 0; i < m_fft_items.BANDS; i++) {
|
||
if (bins[i])
|
||
band[i] = sqrtf(band[i] / bins[i]);
|
||
else
|
||
band[i] = 0.0f;
|
||
}
|
||
|
||
// band weighting (psychoacoustic / UI)
|
||
band[0] *= 0.5f;
|
||
band[1] *= 0.8f;
|
||
band[2] *= 3.0f;
|
||
band[3] *= 2.8f;
|
||
band[4] *= 1.5f;
|
||
band[5] *= 1.5f;
|
||
|
||
// log scale
|
||
for (int i = 0; i < m_fft_items.BANDS; i++) { band[i] = log10f(band[i] + 1e-6f); }
|
||
|
||
// --- temporal smoothing (only displayed bands) ---
|
||
auto smooth = [](float old, float in) {
|
||
constexpr float ATTACK = 0.6f;
|
||
constexpr float RELEASE = 0.6f;
|
||
return (in > old) ? old + ATTACK * (in - old) : old + RELEASE * (in - old);
|
||
};
|
||
|
||
for (int i = 0; i < m_fft_items.BANDS; i++) { m_fft_items.spec_smooth[i] = smooth(m_fft_items.spec_smooth[i], band[i]); }
|
||
|
||
// --- map to 0..255 using dB window ---
|
||
constexpr float DB_MIN = -50.0f;
|
||
constexpr float DB_MAX = 0.0f;
|
||
|
||
for (int i = 0; i < m_fft_items.BANDS; i++) {
|
||
|
||
float db = m_fft_items.spec_smooth[i] * 20.0f;
|
||
|
||
if (db < DB_MIN) db = DB_MIN;
|
||
if (db > DB_MAX) db = DB_MAX;
|
||
|
||
float norm = (db - DB_MIN) / (DB_MAX - DB_MIN);
|
||
|
||
m_fft_items.spectrum[i] = (uint8_t)(norm * 255.0f);
|
||
}
|
||
// AUDIO_LOG_INFO("{:4}, {:4}, {:4}, {:4}, {:4}, {:4} ", m_fft_items.spectrum[0], m_fft_items.spectrum[1], m_fft_items.spectrum[2], m_fft_items.spectrum[3], m_fft_items.spectrum[4],
|
||
// m_fft_items.spectrum[3]);
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::Gain(int32_t* sample) {
|
||
/* important: these multiplications must all be signed ints, or the result will be invalid */
|
||
int32_t* s32 = (int32_t*)sample;
|
||
s32[LEFTCHANNEL] *= m_audio_items.limiter[LEFTCHANNEL];
|
||
s32[RIGHTCHANNEL] *= m_audio_items.limiter[RIGHTCHANNEL];
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
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();
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
uint32_t Audio::getInBufferSize() {
|
||
// current audio input buffer size in bytes
|
||
return InBuff.getBufsize();
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::stereo2mono(int32_t* buff, uint16_t validSamples) {
|
||
|
||
for (uint16_t i = 0; i < validSamples * 2; i += 2) {
|
||
int64_t l = buff[i];
|
||
int64_t r = buff[i + 1];
|
||
int32_t m = (int32_t)((l + r) >> 1); // average, without overflow
|
||
buff[i] = m; // Left
|
||
buff[i + 1] = m; // Right
|
||
}
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
// *** 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() { // Infinite Impulse Response (IIR) filters
|
||
|
||
AUDIO_LOG_DEBUG("gain gain_ls_db {}, gain gain_peq_db {}, gain gain_hs_db {}", m_audio_items.gain_ls_db, m_audio_items.gain_peq_db, m_audio_items.gain_hs_db);
|
||
|
||
const float FcLS = settings.FREQ_LS_HZ; // Frequency LowShelf(Hz)
|
||
const float FcPKEQ = settings.FREQ_PEAK_HZ; // Frequency PeakEQ(Hz)
|
||
const float FcHS = settings.FREQ_HS_HZ; // Frequency HighShelf(Hz)
|
||
const float QS = settings.QUALITY_SLOPE; // Quality Slope (Shelf)
|
||
|
||
float normFreqLS = FcLS / m_i2s_items.sampleRate; // filter cut off frequency
|
||
float normFreqPEQ = FcPKEQ / m_i2s_items.sampleRate; // filter center frequency
|
||
float normFreqHS = FcHS / m_i2s_items.sampleRate; // filter cut off frequency
|
||
|
||
float total_boost_db = fmax(fmax(fmax(0, m_audio_items.gain_ls_db), m_audio_items.gain_peq_db), m_audio_items.gain_hs_db); // dynamic headroom
|
||
m_audio_items.pre_gain = powf(10.0, -total_boost_db / 20);
|
||
|
||
auto dsps_biquad_gen_peakingEQ_f32 = [&](float* c, float f, int8_t g, const float Q) -> void {
|
||
float A = powf(10.0f, g / 40.0f);
|
||
float w0 = 2.0f * M_PI * f;
|
||
float cw = cosf(w0);
|
||
float sw = sinf(w0);
|
||
float alpha = sw / (2.0f * Q);
|
||
float b0 = 1.0f + alpha * A;
|
||
float b1 = -2.0f * cw;
|
||
float b2 = 1.0f - alpha * A;
|
||
float a0 = 1.0f + alpha / A;
|
||
float a1 = -2.0f * cw;
|
||
float a2 = 1.0f - alpha / A;
|
||
c[0] = b0 / a0;
|
||
c[1] = b1 / a0;
|
||
c[2] = b2 / a0;
|
||
c[3] = a1 / a0;
|
||
c[4] = a2 / a0;
|
||
};
|
||
|
||
dsps_biquad_gen_lowShelf_f32(m_audio_items.coeffs[LOWSHELF], normFreqLS, m_audio_items.gain_ls_db, QS);
|
||
dsps_biquad_gen_peakingEQ_f32(m_audio_items.coeffs[PEAKINGEQ], normFreqPEQ, m_audio_items.gain_peq_db, QS); // my own calc.
|
||
dsps_biquad_gen_highShelf_f32(m_audio_items.coeffs[HIFGSHELF], normFreqHS, m_audio_items.gain_hs_db, QS);
|
||
|
||
AUDIO_LOG_DEBUG("\n([{}, {}, {}], [1.0, {}, {}]), # LOWSHELF\n([{}, {}, {} ], [1.0, {}, {} ]), # PEAKINGEQ\n([{}, {}, {}], [1.0, {}, {}]), # HIGHSHELF\n", m_audio_items.coeffs[0][0],
|
||
m_audio_items.coeffs[0][1], m_audio_items.coeffs[0][2], m_audio_items.coeffs[0][3], m_audio_items.coeffs[0][4], m_audio_items.coeffs[1][0], m_audio_items.coeffs[1][1],
|
||
m_audio_items.coeffs[1][2], m_audio_items.coeffs[1][3], m_audio_items.coeffs[1][4], m_audio_items.coeffs[2][0], m_audio_items.coeffs[2][1], m_audio_items.coeffs[2][2],
|
||
m_audio_items.coeffs[2][3], m_audio_items.coeffs[2][4]);
|
||
AUDIO_LOG_DEBUG("m_audio_items.pre_gain {}", m_audio_items.pre_gain);
|
||
memset(m_audio_items.state_biquad, 0, sizeof(m_audio_items.state_biquad));
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::IIR_filter(int32_t* sample) {
|
||
|
||
int32_t* s32 = sample;
|
||
float s[2];
|
||
s[LEFTCHANNEL] = (float)(s32[LEFTCHANNEL] * m_audio_items.pre_gain);
|
||
s[RIGHTCHANNEL] = (float)(s32[RIGHTCHANNEL] * m_audio_items.pre_gain);
|
||
dsps_biquad_f32(s, s, 1, m_audio_items.coeffs[0], m_audio_items.state_biquad[0]);
|
||
dsps_biquad_f32(s, s, 1, m_audio_items.coeffs[1], m_audio_items.state_biquad[1]);
|
||
dsps_biquad_f32(s, s, 1, m_audio_items.coeffs[2], m_audio_items.state_biquad[2]);
|
||
s32[LEFTCHANNEL] = (int32_t)std::clamp(s[LEFTCHANNEL], -2147483648.0f, 2147483647.0f);
|
||
s32[RIGHTCHANNEL] = (int32_t)std::clamp(s[RIGHTCHANNEL], -2147483648.0f, 2147483647.0f);
|
||
return;
|
||
}
|
||
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
||
// 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) {
|
||
|
||
bool log = false;
|
||
|
||
const uint8_t TS_PACKET_SIZE = 188;
|
||
const uint8_t PAYLOAD_SIZE = 184;
|
||
const uint8_t PID_ARRAY_LEN = 4;
|
||
|
||
(void)PAYLOAD_SIZE; // suppress [-Wunused-variable]
|
||
|
||
if (packet == NULL) {
|
||
if (log) AUDIO_LOG_WARN("parseTS reset");
|
||
m_tspp.reset();
|
||
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|AFC|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)
|
||
|
||
// for(int i = 1; i < 188; i++) {printf("%02X ", packet[i - 1]); if(i && (i % 16 == 0)) printf("\n");}
|
||
// printf("\n----------\n");
|
||
|
||
if (packet[0] != 0x47) {
|
||
AUDIO_LOG_ERROR("ts SyncByte not found, first bytes are 0x{:02X} 0x{:02X} 0x{:02X} 0x{:02X}", packet[0], packet[1], packet[2], packet[3]);
|
||
stopSong();
|
||
return false;
|
||
}
|
||
int PID = (packet[1] & 0x1F) << 8 | (packet[2] & 0xFF);
|
||
if (log) AUDIO_LOG_DEBUG("PID: 0x{:04X} ({})", PID, PID);
|
||
int PUSI = (packet[1] & 0x40) >> 6;
|
||
if (log) AUDIO_LOG_DEBUG("Payload Unit Start Indicator: {}", PUSI);
|
||
int AFC = (packet[3] & 0x30) >> 4;
|
||
if (log) AUDIO_LOG_DEBUG("Adaption Field Control: {}", AFC);
|
||
|
||
int AFL = -1;
|
||
if ((AFC & 0b10) == 0b10) { // AFC '11' Adaptation Field followed
|
||
AFL = packet[4] & 0xFF; // Adaptation Field Length
|
||
if (log) AUDIO_LOG_DEBUG("Adaptation Field Length: {}", AFL);
|
||
}
|
||
int PLS = PUSI ? 5 : 4; // PayLoadStart, Payload Unit Start Indicator
|
||
if (AFL > 0) PLS += AFL + 1; // skip adaption field
|
||
|
||
if (AFC == 2) { // The TS package contains only an adaptation Field and no user data.
|
||
*packetStart = AFL + 1;
|
||
*packetLength = 0;
|
||
return true;
|
||
}
|
||
|
||
if (PID == 0) {
|
||
// Program Association Table (PAT) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (log) AUDIO_LOG_DEBUG("PAT");
|
||
m_tspp.pidNumber = 0;
|
||
m_tspp.pidOfAAC = 0;
|
||
|
||
int startOfProgramNums = 8;
|
||
int lengthOfPATValue = 4;
|
||
int sectionLength = ((packet[PLS + 1] & 0x0F) << 8) | (packet[PLS + 2] & 0xFF);
|
||
if (log) AUDIO_LOG_DEBUG("Section Length: {}", sectionLength);
|
||
int program_number, program_map_PID;
|
||
int indexOfPids = 0;
|
||
(void)program_number; // [-Wunused-but-set-variable]
|
||
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 (log) AUDIO_LOG_DEBUG("Program Num: 0x{:04X}({}) PMT PID: 0x{:04X}({})", program_number, program_number, program_map_PID, program_map_PID);
|
||
m_tspp.pids[indexOfPids++] = program_map_PID;
|
||
}
|
||
m_tspp.pidNumber = indexOfPids;
|
||
*packetStart = 0;
|
||
*packetLength = 0;
|
||
return true;
|
||
} else if (PID == m_tspp.pidOfAAC) {
|
||
if (log) AUDIO_LOG_DEBUG("AAC");
|
||
uint8_t posOfPacketStart = 4;
|
||
if (AFL >= 0) {
|
||
posOfPacketStart = 5 + AFL;
|
||
if (log) AUDIO_LOG_DEBUG("posOfPacketStart: {}", posOfPacketStart);
|
||
}
|
||
// Packetized Elementary Stream (PES) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (log) AUDIO_LOG_DEBUG("PES_DataLength {}", m_tspp.PES_DataLength);
|
||
if (m_tspp.PES_DataLength > 0) {
|
||
*packetStart = posOfPacketStart + m_tspp.fillData;
|
||
*packetLength = TS_PACKET_SIZE - posOfPacketStart - m_tspp.fillData;
|
||
if (log) AUDIO_LOG_DEBUG("packetlength {}", *packetLength);
|
||
m_tspp.fillData = 0;
|
||
m_tspp.PES_DataLength -= (*packetLength);
|
||
return true;
|
||
} else {
|
||
int firstByte = packet[posOfPacketStart] & 0xFF;
|
||
int secondByte = packet[posOfPacketStart + 1] & 0xFF;
|
||
int thirdByte = packet[posOfPacketStart + 2] & 0xFF;
|
||
if (log) AUDIO_LOG_DEBUG("First 3 bytes: 0x{:02X}, 0x{:02X}, 0x{:02X}", firstByte, secondByte, thirdByte);
|
||
if (firstByte == 0x00 && secondByte == 0x00 && thirdByte == 0x01) { // Packet start code prefix
|
||
// --------------------------------------------------------------------------------------------------------
|
||
// posOfPacketStart + 0...2 0x00, 0x00, 0x01 PES-Startcode
|
||
//---------------------------------------------------------------------------------------------------------
|
||
// posOfPacketStart + 3 0xE0 (Video) od 0xC0 (Audio) StreamID
|
||
//---------------------------------------------------------------------------------------------------------
|
||
// posOfPacketStart + 4...5 0xLL, 0xLL PES Packet length
|
||
//---------------------------------------------------------------------------------------------------------
|
||
// posOfPacketStart + 6...7 PTS/DTS Flags
|
||
//---------------------------------------------------------------------------------------------------------
|
||
// posOfPacketStart + 8 0xXX header length
|
||
//---------------------------------------------------------------------------------------------------------
|
||
|
||
uint8_t StreamID = packet[posOfPacketStart + 3] & 0xFF;
|
||
if (StreamID >= 0xC0 && StreamID <= 0xDF) { ; } // okay ist audio stream
|
||
if (StreamID >= 0xE0 && StreamID <= 0xEF) {
|
||
AUDIO_LOG_ERROR("video stream!");
|
||
return false;
|
||
}
|
||
int PES_PacketLength = ((packet[posOfPacketStart + 4] & 0xFF) << 8) + (packet[posOfPacketStart + 5] & 0xFF);
|
||
if (log) AUDIO_LOG_DEBUG("PES PacketLength: {}", PES_PacketLength);
|
||
bool PTS_flag = false;
|
||
bool DTS_flag = false;
|
||
int flag_byte1 = packet[posOfPacketStart + 6] & 0xFF;
|
||
int flag_byte2 = packet[posOfPacketStart + 7] & 0xFF;
|
||
(void)flag_byte2; // unused yet
|
||
if (flag_byte1 & 0b10000000) PTS_flag = true;
|
||
if (flag_byte1 & 0b00000100) DTS_flag = true;
|
||
if (log && PTS_flag) AUDIO_LOG_DEBUG("PTS_flag is set");
|
||
if (log && DTS_flag) AUDIO_LOG_DEBUG("DTS_flag is set");
|
||
uint8_t PES_HeaderDataLength = packet[posOfPacketStart + 8] & 0xFF;
|
||
if (log) AUDIO_LOG_DEBUG("PES_headerDataLength {}", PES_HeaderDataLength);
|
||
|
||
m_tspp.PES_DataLength = PES_PacketLength;
|
||
int startOfData = PES_HeaderDataLength + 9;
|
||
if (posOfPacketStart + startOfData >= 188) { // only fillers in packet
|
||
if (log) AUDIO_LOG_DEBUG("posOfPacketStart + startOfData {}", posOfPacketStart + startOfData);
|
||
*packetStart = 0;
|
||
*packetLength = 0;
|
||
m_tspp.PES_DataLength -= (PES_HeaderDataLength + 3);
|
||
m_tspp.fillData = (posOfPacketStart + startOfData) - 188;
|
||
if (log) AUDIO_LOG_DEBUG("fillData {}", m_tspp.fillData);
|
||
return true;
|
||
}
|
||
if (log) AUDIO_LOG_DEBUG("First AAC data byte: {:02X}", packet[posOfPacketStart + startOfData]);
|
||
if (log) AUDIO_LOG_DEBUG("Second AAC data byte: {:02X}", packet[posOfPacketStart + startOfData + 1]);
|
||
*packetStart = posOfPacketStart + startOfData;
|
||
*packetLength = TS_PACKET_SIZE - posOfPacketStart - startOfData;
|
||
m_tspp.PES_DataLength -= (*packetLength);
|
||
m_tspp.PES_DataLength -= (PES_HeaderDataLength + 3);
|
||
return true;
|
||
}
|
||
if (firstByte == 0 && secondByte == 0 && thirdByte == 0) {
|
||
// PES packet startcode prefix is 0x000000
|
||
// skip such packets
|
||
return true;
|
||
}
|
||
}
|
||
*packetStart = 0;
|
||
*packetLength = 0;
|
||
AUDIO_LOG_ERROR("PES not found");
|
||
return false;
|
||
} else if (m_tspp.pidNumber) {
|
||
// Program Map Table (PMT) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
for (int i = 0; i < m_tspp.pidNumber; i++) {
|
||
if (PID == m_tspp.pids[i]) {
|
||
if (log) AUDIO_LOG_DEBUG("PMT");
|
||
int staticLengthOfPMT = 12;
|
||
int sectionLength = ((packet[PLS + 1] & 0x0F) << 8) | (packet[PLS + 2] & 0xFF);
|
||
if (log) AUDIO_LOG_DEBUG("Section Length: {}", sectionLength);
|
||
int programInfoLength = ((packet[PLS + 10] & 0x0F) << 8) | (packet[PLS + 11] & 0xFF);
|
||
if (log) AUDIO_LOG_DEBUG("Program Info Length: {}", 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 (log) AUDIO_LOG_DEBUG("Stream Type: 0x{:02X} Elementary PID: 0x{:04X}", streamType, elementaryPID);
|
||
|
||
if (streamType == 0x0F || streamType == 0x11 || streamType == 0x04) {
|
||
if (log) AUDIO_LOG_DEBUG("AAC PID discover");
|
||
m_tspp.pidOfAAC = elementaryPID;
|
||
}
|
||
int esInfoLength = ((packet[PLS + cursor + 3] & 0x0F) << 8) | (packet[PLS + cursor + 4] & 0xFF);
|
||
if (log) AUDIO_LOG_DEBUG("ES Info Length: 0x{:04X}", esInfoLength);
|
||
cursor += 5 + esInfoLength;
|
||
}
|
||
}
|
||
}
|
||
*packetStart = 0;
|
||
*packetLength = 0;
|
||
return true;
|
||
}
|
||
// PES received before PAT and PMT seen
|
||
*packetStart = 0;
|
||
*packetLength = 0;
|
||
if (PID > 0) { return true; }
|
||
return false;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
||
// W E B S T R E A M - H E L P F U N C T I O N S
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
||
bool Audio::readMetadata(uint32_t maxBytes, uint16_t* readedBytes, bool first) {
|
||
*readedBytes = 0;
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (first) {
|
||
m_rmet.pos_ml = 0; // determines the current position in metaline
|
||
m_rmet.metaDataSize = 0;
|
||
m_rmet.res = 0;
|
||
m_metadataBuff.clear();
|
||
return true;
|
||
}
|
||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
if (!maxBytes) return false; // guard
|
||
if (!m_rmet.metaDataSize) {
|
||
int b = audioFileRead(); // First byte of metadata?
|
||
if (b < 0) {
|
||
AUDIO_LOG_WARN("client->read() failed ({})", b);
|
||
return false;
|
||
}
|
||
m_rmet.metaDataSize = b * 16; // New count for metadata including length byte, max 4096
|
||
m_rmet.pos_ml = 0;
|
||
m_metadataBuff[m_rmet.pos_ml] = 0; // Prepare for new line
|
||
*readedBytes = 1;
|
||
maxBytes -= 1;
|
||
}
|
||
if (!m_rmet.metaDataSize) { return *readedBytes; } // metalen is 0
|
||
|
||
int32_t a = audioFileRead((uint8_t*)&m_metadataBuff[m_rmet.pos_ml], min(m_rmet.metaDataSize - m_rmet.pos_ml, maxBytes));
|
||
if (a > 0) {
|
||
m_rmet.res += a;
|
||
*readedBytes += a;
|
||
m_rmet.pos_ml += a;
|
||
}
|
||
if (m_rmet.pos_ml == m_rmet.metaDataSize) {
|
||
m_metadataBuff[m_rmet.pos_ml] = '\0';
|
||
// m_metadataBuff.hex_dump(m_rmet.metaDataSize);
|
||
if (m_metadataBuff.strlen() > 0) { // 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.
|
||
latinToUTF8(m_metadataBuff); // convert to UTF-8 if necessary
|
||
int pos = m_metadataBuff.index_of_icase("song_spot", 0); // remove some irrelevant infos
|
||
if (pos > 3) { // e.g. song_spot="T" MediaBaseId="0" itunesTrackId="0"
|
||
m_metadataBuff[pos] = 0;
|
||
}
|
||
showstreamtitle(m_metadataBuff.get()); // Show artist and title if present in metadata
|
||
}
|
||
m_metacount = m_metaint;
|
||
m_rmet.metaDataSize = 0;
|
||
m_rmet.pos_ml = 0;
|
||
m_metadataBuff[m_rmet.pos_ml] = 0; // Prepare for new line
|
||
} else {
|
||
return false; // not enough data, next round
|
||
}
|
||
m_metadataBuff.clear();
|
||
return true;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
||
int32_t Audio::getChunkSize(uint16_t* readedBytes, bool first) {
|
||
|
||
// <chunk-size in hex>\r\n
|
||
// <chunk-data\r\n
|
||
// <chunk-size in hex>\r\n
|
||
// <chunk-data\r\n
|
||
// ...
|
||
// 0\r\n
|
||
// \r\n
|
||
|
||
constexpr uint16_t CHUNK_LINE_LENGTH = 255;
|
||
|
||
ps_ptr<char> hex_str;
|
||
int16_t idx1, idx2, idx3;
|
||
int32_t chunkSize = -1;
|
||
|
||
if (first) {
|
||
m_gchs.reset();
|
||
return 0;
|
||
}
|
||
if (!m_gchs.chunkLine.valid()) m_gchs.chunkLine.calloc(CHUNK_LINE_LENGTH, "m_gchs1.chunkLine");
|
||
|
||
auto hex_to_int = [&](ps_ptr<char> hexstr) -> int32_t {
|
||
if (!hexstr.valid()) return -1;
|
||
for (int i = 0; i < hexstr.strlen(); i++) {
|
||
if (!isxdigit(hexstr[i])) return -1;
|
||
}
|
||
return hexstr.to_int32(16);
|
||
};
|
||
|
||
*readedBytes = 0;
|
||
|
||
while (true) {
|
||
int16_t b = audioFileRead();
|
||
if (b == -1) { // need mor data
|
||
chunkSize = -1;
|
||
break;
|
||
}
|
||
|
||
m_gchs.chunkLine[m_gchs.position] = b;
|
||
*readedBytes += 1;
|
||
m_gchs.position += 1;
|
||
|
||
if (m_gchs.chunkLine == "\r\n") { // skip CRLF
|
||
m_gchs.chunkLine.clear();
|
||
m_gchs.position = 0;
|
||
AUDIO_LOG_DEBUG("skip CRLF");
|
||
}
|
||
|
||
if (m_gchs.position == CHUNK_LINE_LENGTH) {
|
||
AUDIO_LOG_ERROR("ChunkLine is too long");
|
||
m_gchs.chunkLine.hex_dump(CHUNK_LINE_LENGTH);
|
||
goto error;
|
||
}
|
||
if (m_gchs.chunkLine.ends_with("\r\n") && m_gchs.chunkSize == -1) {
|
||
idx1 = m_gchs.chunkLine.index_of(";");
|
||
if (idx1 > 0) { // extension follows, e.g. "AF4;test=123\r\n"
|
||
hex_str = m_gchs.chunkLine.substr(0, idx1); // "AF4;test=123\r\n" -> "AF4"
|
||
ps_ptr<char> extension = m_gchs.chunkLine.substr(idx1 + 1); // "AF4;test=123\r\n" -> "test=123\r\n"
|
||
extension = extension.substr(0, extension.strlen() - 2); // "test=123\r\n" -> "test=123"
|
||
if (extension != m_gchs.extension) {
|
||
m_gchs.extension = extension;
|
||
AUDIO_LOG_INFO("extension {}", m_gchs.extension);
|
||
}
|
||
} else {
|
||
hex_str = m_gchs.chunkLine.substr(0, m_gchs.chunkLine.strlen() - 2); // "AF4\r\n" -> "AF4"
|
||
}
|
||
m_gchs.chunkSize = hex_to_int(hex_str); // "AF4" -> 2804
|
||
if (m_gchs.chunkSize == -1) {
|
||
AUDIO_LOG_ERROR("Invalid char in hex_str");
|
||
hex_str.hex_dump(20);
|
||
goto error;
|
||
}
|
||
}
|
||
if (m_gchs.chunkSize > 0) {
|
||
chunkSize = m_gchs.chunkSize;
|
||
break;
|
||
} else { // m_gchs1.chunkSize is 0
|
||
if (m_gchs.chunkLine.ends_with("\r\n\r\n")) {
|
||
idx1 = m_gchs.chunkLine.index_of("\r\n");
|
||
idx2 = m_gchs.chunkLine.index_of("\r\n\r");
|
||
if (idx1 == idx2) {
|
||
// e.g. "000\r\n\r\n" or "0\r\n\r\n" or "0;end=true\r\n\r\n"
|
||
} else { // can have trailer "0\r\nContent-MD5: abcdef\r\nServer: xyz\r\n\r\n"
|
||
m_gchs.trailer = m_gchs.chunkLine.substr(idx1 + 1, idx1 - idx2);
|
||
AUDIO_LOG_INFO("trailer {}", m_gchs.trailer);
|
||
}
|
||
chunkSize = 0;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
AUDIO_LOG_DEBUG("chunkSize {}", chunkSize);
|
||
if (chunkSize == -1) {
|
||
if (m_gchs.timeStamp + 3000 < millis()) {
|
||
AUDIO_LOG_WARN("timeout while get next chunkSize");
|
||
m_gchs.timeStamp = millis();
|
||
}
|
||
}
|
||
if (chunkSize >= 0) {
|
||
m_gchs.chunkSize = -1;
|
||
m_gchs.position = 0;
|
||
m_gchs.chunkLine.clear();
|
||
m_gchs.timeStamp = millis();
|
||
}
|
||
return chunkSize;
|
||
|
||
error:
|
||
m_gchs.reset();
|
||
return -100;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
||
bool Audio::readID3V1Tag() {
|
||
if (m_codec != CODEC_MP3) return false;
|
||
ps_ptr<char> chBuff;
|
||
chBuff.alloc(256, "chBuff");
|
||
|
||
const uint8_t* p = InBuff.getReadPtr();
|
||
|
||
// Lambda for simplification
|
||
auto readID3Field = [&](ps_ptr<char>& field, const uint8_t* src, size_t len, const char* label = nullptr) {
|
||
field.alloc(len + 1, "field");
|
||
memcpy(field.get(), src, len);
|
||
field[len] = '\0';
|
||
latinToUTF8(field);
|
||
if (field.strlen() > 0 && label) {
|
||
field.insert(label, 0);
|
||
info(*this, evt_id3data, "{}", field.get());
|
||
}
|
||
};
|
||
|
||
if (InBuff.bufferFilled() == 128 && startsWith((const char*)p, "TAG")) {
|
||
ps_ptr<char> title, artist, album, year, comment;
|
||
|
||
readID3Field(title, p + 3, 30, "Title: ");
|
||
readID3Field(artist, p + 33, 30, "Artist: ");
|
||
readID3Field(album, p + 63, 30, "Album: ");
|
||
readID3Field(year, p + 93, 4, "Year: ");
|
||
readID3Field(comment, p + 97, 30, "Comment: ");
|
||
|
||
uint8_t zeroByte = p[125];
|
||
uint8_t track = p[126];
|
||
uint8_t genre8 = p[127];
|
||
|
||
info(*this, evt_info, zeroByte ? "ID3 version: 1" : "ID3 Version 1.1");
|
||
|
||
if (zeroByte == 0) {
|
||
sprintf(chBuff.get(), "Track Number: %d", track);
|
||
info(*this, evt_id3data, "{}", chBuff.get());
|
||
}
|
||
|
||
if (genre8 < 192) {
|
||
sprintf(chBuff.get(), "Genre: %d", genre8);
|
||
info(*this, evt_id3data, "{}", chBuff.get());
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
if (InBuff.bufferFilled() == 227 && startsWith((const char*)p, "TAG+")) { // "TAG+" "can exist as an extension, does not overwrite" TAG"
|
||
info(*this, evt_info, "ID3 version: 1 - Enhanced TAG");
|
||
|
||
ps_ptr<char> title, artist, album, genre;
|
||
|
||
readID3Field(title, p + 4, 60, "Title: ");
|
||
readID3Field(artist, p + 64, 60, "Artist: ");
|
||
readID3Field(album, p + 124, 60, "Album: ");
|
||
readID3Field(genre, p + 185, 30, "Genre: ");
|
||
|
||
// optional expansion: speed, start-time, end-time
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
// [1] https://en.wikipedia.org/wiki/List_of_ID3v1_Genres
|
||
// [2] https://en.wikipedia.org/wiki/ID3#ID3v1_and_ID3v1.1[5]
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
||
int32_t Audio::newInBuffStart(int32_t resumeFilePos) {
|
||
|
||
if ((m_controlCounter != 100) || (m_resumeFilePos >= (int32_t)m_audioDataStart + m_audioDataSize) || ((m_codec == CODEC_M4A) && !m_stsz_position)) {
|
||
AUDIO_LOG_WARN("timeOffset not possible");
|
||
return 0;
|
||
}
|
||
|
||
// keep resumeFilePos within the audio data
|
||
if (resumeFilePos < (int32_t)m_audioDataStart) resumeFilePos = m_audioDataStart;
|
||
|
||
uint32_t buffFillValue = std::min<uint32_t>(m_audioDataSize - resumeFilePos, UINT16_MAX);
|
||
|
||
AUDIO_LOG_DEBUG("new InBuff start at m_resumeFilePos {}, m_audioDataStart {}", m_resumeFilePos, m_audioDataStart);
|
||
|
||
// --- enter critical area ------------------------------------------
|
||
xSemaphoreTake(mutex_audioTaskIsDecoding, 1 * configTICK_RATE_HZ);
|
||
m_f_lockInBuffer = true;
|
||
|
||
// ------- prepare InBuff ------
|
||
m_f_allDataReceived = false;
|
||
audioFileSeek(resumeFilePos);
|
||
InBuff.reset();
|
||
int32_t bw = audioFileRead(InBuff.getWritePtr(), buffFillValue, 3000);
|
||
if (bw == buffFillValue)
|
||
InBuff.bytesWritten(bw);
|
||
else
|
||
return -1;
|
||
|
||
int32_t offset = 0;
|
||
int32_t newFilePos = resumeFilePos;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// codec specific handling
|
||
// ---------------------------------------------------------------------------
|
||
|
||
auto fail = [&]() {
|
||
AUDIO_LOG_ERROR("can't set newFilePos");
|
||
InBuff.bytesWasRead(0);
|
||
m_f_lockInBuffer = false;
|
||
xSemaphoreGive(mutex_audioTaskIsDecoding);
|
||
stopSong();
|
||
return -1;
|
||
};
|
||
|
||
switch (m_codec) {
|
||
case CODEC_M4A:
|
||
offset = m4a_correctResumeFilePos();
|
||
if (offset < 0) return fail();
|
||
break;
|
||
|
||
case CODEC_WAV:
|
||
// WAV have 4-Byte-Boundaries
|
||
offset = wav_correctResumeFilePos();
|
||
if (offset < 0) return fail();
|
||
break;
|
||
|
||
case CODEC_MP3:
|
||
offset = mp3_correctResumeFilePos();
|
||
if (offset < 0) return fail();
|
||
break;
|
||
|
||
case CODEC_FLAC:
|
||
offset = flac_correctResumeFilePos();
|
||
if (offset < 0) return fail();
|
||
break;
|
||
|
||
case CODEC_VORBIS:
|
||
offset = ogg_correctResumeFilePos(); // next OggS
|
||
if (offset < 0) return fail();
|
||
break;
|
||
|
||
case CODEC_OPUS:
|
||
offset = ogg_correctResumeFilePos(); // next OggS
|
||
if (offset < 0) return fail();
|
||
break;
|
||
|
||
default: return fail();
|
||
}
|
||
|
||
AUDIO_LOG_DEBUG("offset {}, readSpace {}", offset, InBuff.readSpace());
|
||
newFilePos += offset;
|
||
m_decoder->clear();
|
||
|
||
// --- leaf critical area ----------------------------------------------
|
||
InBuff.bytesWasRead(offset);
|
||
m_f_lockInBuffer = false;
|
||
xSemaphoreGive(mutex_audioTaskIsDecoding);
|
||
|
||
return newFilePos;
|
||
}
|
||
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
||
boolean Audio::streamDetection(uint32_t bytesAvail) {
|
||
|
||
if (InBuff.bufferFilled() < InBuff.getMaxBlockSize()) {
|
||
if (m_sdet.cnt_slow == 0) m_sdet.tmr_slow = millis();
|
||
m_sdet.cnt_slow++;
|
||
} else {
|
||
m_sdet.cnt_slow = 0;
|
||
m_sdet.cnt_lost = 0;
|
||
}
|
||
|
||
// if within one second the content of the audio buffer falls below the size of an audio frame 100 times,
|
||
// issue a message
|
||
if (m_sdet.cnt_slow && m_sdet.tmr_slow + 2000 < millis()) {
|
||
m_sdet.tmr_slow = millis();
|
||
info(*this, evt_info, "slow stream");
|
||
m_sdet.cnt_slow = 0;
|
||
m_sdet.cnt_lost++;
|
||
}
|
||
|
||
if (bytesAvail) { m_sdet.cnt_lost = 0; }
|
||
if (InBuff.bufferFilled() > InBuff.getMaxBlockSize() * 2) return true; // enough data available to play
|
||
|
||
// if no audio data is received within 10 seconds, a new connection attempt is started.
|
||
if (m_sdet.cnt_lost == 5) {
|
||
info(*this, evt_info, "Stream lost");
|
||
connecttohost(m_lastHost.get());
|
||
m_sdet.cnt_slow = 0;
|
||
m_sdet.cnt_lost = 0;
|
||
return false;
|
||
}
|
||
return false;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
||
uint32_t Audio::m4a_correctResumeFilePos() {
|
||
// In order to jump within an m4a file, the exact beginning of an aac block must be found. Since m4a cannot be
|
||
// streamed, i.e. there is no syncword, an imprecise jump can lead to a crash.
|
||
|
||
if (!m_stsz_position) return m_audioDataStart; // guard
|
||
|
||
typedef union {
|
||
uint8_t u8[4];
|
||
uint32_t u32;
|
||
} tu;
|
||
tu uu = {};
|
||
|
||
uint32_t i = 0, pos = m_audioDataStart;
|
||
uint32_t filePtr = m_audioFilePosition;
|
||
bool found = false;
|
||
audioFileSeek(m_stsz_position, m_stsz_numEntries * 4);
|
||
|
||
auto read_next = [&]() -> int {
|
||
int r = 0;
|
||
int cnt = 0;
|
||
while (true) {
|
||
r = audioFileRead();
|
||
if (r >= 0) break;
|
||
vTaskDelay(10);
|
||
cnt++;
|
||
if (cnt > 300) {
|
||
AUDIO_LOG_ERROR("timeout");
|
||
break;
|
||
}
|
||
}
|
||
return r;
|
||
};
|
||
|
||
while (i < m_stsz_numEntries) {
|
||
i++;
|
||
uu.u8[3] = read_next();
|
||
uu.u8[2] = read_next();
|
||
uu.u8[1] = read_next();
|
||
uu.u8[0] = read_next();
|
||
|
||
pos += uu.u32;
|
||
if (pos >= m_resumeFilePos) {
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!found) return -1; // not found
|
||
|
||
audioFileSeek(filePtr); // restore file pointer
|
||
return pos - m_resumeFilePos; // return the number of bytes to jump
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
||
uint32_t Audio::ogg_correctResumeFilePos() {
|
||
// The starting point is the next OggS magic word
|
||
if (InBuff.bufferFilled() < 0xFFFF) return 0;
|
||
vTaskDelay(1);
|
||
// // AUDIO_LOG_INFO("in_resumeFilePos {}", resumeFilePos);
|
||
|
||
auto find_sync_word = [&](uint8_t* pos, uint32_t av) -> int {
|
||
int steps = 0;
|
||
while (av--) {
|
||
if (pos[steps] == 'O') {
|
||
if (pos[steps + 1] == 'g') {
|
||
if (pos[steps + 2] == 'g') {
|
||
if (pos[steps + 3] == 'S') { // Check for the second part of magic word
|
||
return steps; // Magic word found, return the number of steps
|
||
}
|
||
}
|
||
}
|
||
}
|
||
steps++;
|
||
}
|
||
return -1; // Return -1 if OggS magic word is not found
|
||
};
|
||
|
||
uint8_t* readPtr = InBuff.getReadPtr();
|
||
size_t av = InBuff.readSpace();
|
||
int32_t steps = 0;
|
||
|
||
if (av < InBuff.getMaxBlockSize()) return -1; // guard
|
||
|
||
steps = find_sync_word(readPtr, av);
|
||
if (steps == -1) return -1;
|
||
return steps; // Return the number of steps to the sync word
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
||
int32_t Audio::flac_correctResumeFilePos() {
|
||
uint8_t* p = InBuff.getReadPtr();
|
||
size_t av = InBuff.readSpace();
|
||
|
||
if (av < 32) return -1; // sicher gehen
|
||
|
||
auto utf8_length = [&](uint8_t first) -> int {
|
||
if ((first & 0x80) == 0x00) return 1; // 0xxxxxxx
|
||
if ((first & 0xE0) == 0xC0) return 2; // 110xxxxx
|
||
if ((first & 0xF0) == 0xE0) return 3; // 1110xxxx
|
||
if ((first & 0xF8) == 0xF0) return 4; // 11110xxx
|
||
if ((first & 0xFC) == 0xF8) return 5; // 111110xx
|
||
if ((first & 0xFE) == 0xFC) return 6; // 1111110x
|
||
if (first == 0xFE) return 7; // 11111110
|
||
return -1; // invalid UTF-8
|
||
};
|
||
|
||
// FLAC CRC-8
|
||
auto crc8_update = [](uint8_t crc, uint8_t data) -> uint8_t {
|
||
crc ^= data;
|
||
for (int i = 0; i < 8; ++i) {
|
||
if (crc & 0x80)
|
||
crc = (crc << 1) ^ 0x07;
|
||
else
|
||
crc <<= 1;
|
||
}
|
||
return crc;
|
||
};
|
||
|
||
for (size_t i = 0; i + 14 < av; ++i) {
|
||
// 14-Bit-Sync: 0xFFF8 oder 0xFFF9 (last 2 Bits variable!)
|
||
uint8_t b0 = p[i + 0]; // 11111111 sync
|
||
uint8_t b1 = p[i + 1]; // 111111rb sync r-reserved b-blocking strategy
|
||
if (b0 != 0xFF) continue;
|
||
if ((b1 & 0xFE) != 0xF8) continue;
|
||
|
||
uint8_t b2 = p[i + 2]; // bbbbssss b-blocksize s samplerate
|
||
uint8_t blocksize_code = (b2 & 0xF0) >> 4;
|
||
uint8_t samplerate_code = (b2 & 0x0F);
|
||
|
||
uint8_t b3 = p[i + 3]; // ccccsssr c-channel assignment, s sample size, r reserved
|
||
uint8_t channel_assign = (b3 & 0xF0) >> 4;
|
||
uint8_t bps_code = (b3 & 0x0E) >> 1; // bits per sample
|
||
uint8_t reserved_bit = b3 & 0x01; // has to be 0
|
||
|
||
if (reserved_bit != 0) continue;
|
||
if (channel_assign >= 11) continue; // 11-15 reserved
|
||
if (bps_code == 0x07) continue; // reserved
|
||
|
||
size_t pos = i + 4; // first byte of UTF8
|
||
int len = utf8_length(p[pos]);
|
||
if (len < 1 || pos + len >= av) continue; // invalid/incomplete header
|
||
|
||
size_t crc_end = pos + len; // this is where the CRC byte lives
|
||
|
||
AUDIO_LOG_DEBUG("samplerate_code {}, bps_code {} channel_assign {}, reserved_bit {}", samplerate_code, bps_code, channel_assign, reserved_bit);
|
||
AUDIO_LOG_DEBUG("b0 {}, b1 {}, b2 {}, b3 {}", b0, b1, b2, b3);
|
||
|
||
uint8_t crc = 0;
|
||
for (size_t k = i; k < crc_end; ++k) crc = crc8_update(crc, p[k]);
|
||
|
||
if (crc != p[crc_end]) continue;
|
||
|
||
// === When we get here: 99.9999% certain it’s a real frame ===
|
||
AUDIO_LOG_DEBUG(">>> REAL FLAC-FRAME found at offset {}", i);
|
||
return (int32_t)i;
|
||
}
|
||
|
||
return -1; // No frame found in the first 64 KB
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
||
int32_t Audio::mp3_correctResumeFilePos() {
|
||
|
||
int32_t steps = 0;
|
||
int32_t sumSteps = 0;
|
||
uint8_t* pos = InBuff.getReadPtr();
|
||
size_t av = InBuff.readSpace();
|
||
|
||
if (av < InBuff.getMaxBlockSize()) return -1; // guard
|
||
|
||
while (true) {
|
||
steps = m_decoder->findSyncWord(pos, av);
|
||
if (steps == 0) break;
|
||
if (steps == -1) return -1;
|
||
pos += steps;
|
||
sumSteps += steps;
|
||
}
|
||
// AUDIO_LOG_INFO("found sync word at {} sync1 = 0x{:02X}, sync2 = 0x{;02X}", readPtr - pos, *readPtr, *(readPtr + 1));
|
||
return sumSteps; // return the position of the first byte of the frame
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
||
int32_t Audio::wav_correctResumeFilePos() {
|
||
int32_t pos = m_resumeFilePos;
|
||
uint8_t offset = 0;
|
||
;
|
||
// WAV must be on 4 byte limit
|
||
while (pos % 4 != 0) {
|
||
offset++;
|
||
pos++;
|
||
}
|
||
if (pos >= m_audioDataStart + m_audioDataSize) return 0;
|
||
return offset;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
||
uint8_t Audio::determineCodec(uint8_t presumed_codec) {
|
||
if (presumed_codec == CODEC_OGG) {
|
||
// if we have contentType == application/ogg; codec cn be OPUS, FLAC or VORBIS
|
||
// let's have a look, what it is
|
||
uint8_t res = CODEC_NONE;
|
||
int idx = -1;
|
||
idx = specialIndexOf(InBuff.getReadPtr(), "OggS", 127);
|
||
AUDIO_LOG_DEBUG("idx {}", idx);
|
||
if (idx == 0) {
|
||
idx = specialIndexOf(InBuff.getReadPtr(), "OpusHead", 127);
|
||
if (idx >= 28) { res = CODEC_OPUS; }
|
||
idx = specialIndexOf(InBuff.getReadPtr(), "fLaC", 127);
|
||
if (idx >= 28) { res = CODEC_FLAC; }
|
||
idx = specialIndexOf(InBuff.getReadPtr(), "vorbis", 127);
|
||
if (idx >= 28) { res = CODEC_VORBIS; }
|
||
|
||
m_lastGranulePosition = getLastGranulePosition(res); // VORBIS or OPUS only
|
||
audioFileSeek(0); // if isFile?
|
||
AUDIO_LOG_DEBUG("lastGranulePosition {}", m_lastGranulePosition);
|
||
}
|
||
return res;
|
||
}
|
||
|
||
if (m_playlistFormat == FORMAT_M3U8) return presumed_codec; // HLS or TS, nothing todo
|
||
if (m_streamType != ST_WEBSTREAM) return presumed_codec; // is webfile, nothing todo
|
||
|
||
if (presumed_codec == CODEC_AAC || presumed_codec == CODEC_MP3) { // only webstream, is AAC or MP3?
|
||
uint8_t* data = InBuff.getReadPtr();
|
||
uint8_t b0 = data[0];
|
||
uint8_t b1 = data[1];
|
||
int8_t mimeType = CODEC_NONE;
|
||
if (b0 == 0xFF && (b1 & 0xF6) == 0xF0) { // AAC ADTS
|
||
mimeType = CODEC_AAC;
|
||
}
|
||
|
||
else if (b0 == 0x56 && (b1 & 0xE0) == 0xE0) { // AAC LATM
|
||
mimeType = CODEC_AAC;
|
||
}
|
||
|
||
else if (b0 == 0xFF && (b1 & 0xE0) == 0xE0) { // MP3
|
||
uint8_t layer = (b1 >> 1) & 0x03;
|
||
if (layer != 0) { // layer must not be "reserved".
|
||
mimeType = CODEC_MP3;
|
||
}
|
||
}
|
||
|
||
if (presumed_codec == mimeType) return presumed_codec;
|
||
if (mimeType == CODEC_NONE) return presumed_codec; // is not AAC or MP3
|
||
info(*this, evt_info, "contentType is {}, but {} found", codecname[presumed_codec], codecname[mimeType]);
|
||
return mimeType;
|
||
}
|
||
|
||
return presumed_codec; // all other (native FLAC or WAV)
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
const char* Audio::getVersion() {
|
||
trim(audioI2SVers);
|
||
return audioI2SVers + 8;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::get_info() {
|
||
if (m_info_queue.e.size() == 0) return false;
|
||
msg_t i = {0};
|
||
while (m_info_queue.e.size()) {
|
||
ps_ptr<char> msg = m_info_queue.msg.back();
|
||
i.msg = msg.c_get();
|
||
i.e = (event_t)m_info_queue.e.back();
|
||
ps_ptr<char> evtstr = m_info_queue.s.back();
|
||
i.s = evtstr.c_get();
|
||
i.arg1 = m_info_queue.arg1.back();
|
||
i.arg2 = m_info_queue.arg2.back();
|
||
i.i2s_num = m_i2s_items.i2s_num;
|
||
i.vec = m_info_queue.vec.back();
|
||
|
||
m_info_queue.msg.pop_back();
|
||
m_info_queue.e.pop_back();
|
||
m_info_queue.s.pop_back();
|
||
m_info_queue.arg1.pop_back();
|
||
m_info_queue.arg2.pop_back();
|
||
m_info_queue.vec.pop_back();
|
||
audio_info_callback(i);
|
||
}
|
||
return true;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::trim(char* str) {
|
||
char* start = str; // keep the original pointer
|
||
char* end;
|
||
while (isspace((unsigned char)*start)) start++; // find the first non-space character
|
||
|
||
if (*start == 0) { // all characters were spaces
|
||
str[0] = '\0'; // return a empty string
|
||
return;
|
||
}
|
||
|
||
end = start + strlen(start) - 1; // find the end of the string
|
||
|
||
while (end > start && isspace((unsigned char)*end)) end--;
|
||
end[1] = '\0'; // Null-terminate the string after the last non-space character
|
||
|
||
// Move the trimmed string to the beginning of the memory area
|
||
memmove(str, start, strlen(start) + 1); // +1 for '\0'
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::startsWith(const char* base, const char* str) {
|
||
// fb
|
||
char c;
|
||
while ((c = *str++) != '\0')
|
||
if (c != *base++) return false;
|
||
return true;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
int Audio::indexOf(const char* base, const char* str, int startIndex) {
|
||
// fbi
|
||
const char* p = base;
|
||
for (; startIndex > 0; startIndex--)
|
||
if (*p++ == '\0') return -1;
|
||
char* pos = strstr(p, str);
|
||
if (pos == nullptr) return -1;
|
||
return pos - base;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
int Audio::indexOf(const char* base, char ch, int startIndex) {
|
||
// fb
|
||
const char* p = base;
|
||
for (; startIndex > 0; startIndex--)
|
||
if (*p++ == '\0') return -1;
|
||
char* pos = strchr(p, ch);
|
||
if (pos == nullptr) return -1;
|
||
return pos - base;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
int Audio::specialIndexOf(uint8_t* base, const char* str, int baselen, bool exact) {
|
||
int result = 0; // seek for str in buffer or in header up to baselen, not nullterninated
|
||
if (strlen(str) > baselen) return -1; // if exact == true seekstr in buffer must have "\0" at the end
|
||
for (int i = 0; i < baselen - strlen(str); i++) {
|
||
result = i;
|
||
for (int j = 0; j < strlen(str) + exact; j++) {
|
||
if (*(base + i + j) != *(str + j)) {
|
||
result = -1;
|
||
break;
|
||
}
|
||
}
|
||
if (result >= 0) break;
|
||
}
|
||
return result;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
int32_t Audio::min3(int32_t a, int32_t b, int32_t c) {
|
||
uint32_t min_val = a;
|
||
if (b < min_val) min_val = b;
|
||
if (c < min_val) min_val = c;
|
||
return min_val;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
// some other functions
|
||
uint64_t Audio::bigEndian(uint8_t* base, uint8_t numBytes, uint8_t shiftLeft) {
|
||
uint64_t result = 0; // Use uint64_t for greater caching
|
||
if (numBytes < 1 || numBytes > 8) return 0;
|
||
for (int i = 0; i < numBytes; i++) {
|
||
result |= (uint64_t)(*(base + i)) << ((numBytes - i - 1) * shiftLeft); // Make sure the calculation is done correctly
|
||
}
|
||
if (result > SIZE_MAX) {
|
||
log_e("range overflow");
|
||
return 0;
|
||
}
|
||
return result;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
bool Audio::b64encode(const char* source, uint16_t sourceLength, char* dest) {
|
||
size_t size = base64_encode_expected_len(sourceLength) + 1;
|
||
char* buffer = (char*)malloc(size);
|
||
if (buffer) {
|
||
base64_encodestate _state;
|
||
base64_init_encodestate(&_state);
|
||
int len = base64_encode_block(&source[0], sourceLength, &buffer[0], &_state);
|
||
base64_encode_blockend((buffer + len), &_state);
|
||
memcpy(dest, buffer, strlen(buffer));
|
||
dest[strlen(buffer)] = '\0';
|
||
free(buffer);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::vector_clear_and_shrink(std::vector<ps_ptr<char>>& vec) {
|
||
for (int i = 0; i < vec.size(); i++) vec[i].reset();
|
||
vec.clear(); // unique_ptr takes care of free()
|
||
vec.shrink_to_fit(); // put back memory
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
void Audio::deque_clear_and_shrink(std::deque<ps_ptr<char>>& deq) {
|
||
for (int i = 0; i < deq.size(); i++) deq[i].reset();
|
||
deq.clear(); // unique_ptr takes care of free()
|
||
deq.shrink_to_fit(); // put back memory
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
uint32_t Audio::simpleHash(const char* str) {
|
||
if (str == NULL) return 0;
|
||
uint32_t hash = 0;
|
||
for (int i = 0; i < strlen(str); i++) {
|
||
if (str[i] < 32) continue; // ignore control sign
|
||
hash += (str[i] - 31) * i * 32;
|
||
}
|
||
return hash;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
ps_ptr<char> Audio::urlencode(const char* str, bool spacesOnly) {
|
||
if (!str) { return {}; } // Enter is zero
|
||
|
||
// Reserve memory for the result (3x the length of the input string, worst-case)
|
||
size_t inputLength = strlen(str);
|
||
size_t bufferSize = inputLength * 3 + 1; // Worst-case-Szenario
|
||
ps_ptr<char> encoded;
|
||
encoded.alloc(bufferSize, "encoded");
|
||
if (!encoded.valid()) { return {}; } // memory allocation failed
|
||
|
||
const char* p_input = str; // Copy of the input pointer
|
||
char* p_encoded = encoded.get(); // pointer of the output buffer
|
||
size_t remainingSpace = bufferSize; // remaining space in the output buffer
|
||
|
||
while (*p_input) {
|
||
if (isalnum((unsigned char)*p_input)) {
|
||
// adopt alphanumeric characters directly
|
||
if (remainingSpace > 1) {
|
||
*p_encoded++ = *p_input;
|
||
remainingSpace--;
|
||
} else {
|
||
return {}; // security check failed
|
||
}
|
||
} else if (spacesOnly && *p_input != 0x20) {
|
||
// Nur Leerzeichen nicht kodieren
|
||
if (remainingSpace > 1) {
|
||
*p_encoded++ = *p_input;
|
||
remainingSpace--;
|
||
} else {
|
||
return {}; // security check failed
|
||
}
|
||
} else {
|
||
// encode unsafe characters as '%XX'
|
||
if (remainingSpace > 3) {
|
||
int written = snprintf(p_encoded, remainingSpace, "%%%02X", (unsigned char)*p_input);
|
||
if (written < 0 || written >= (int)remainingSpace) {
|
||
return {}; // error writing to buffer
|
||
}
|
||
p_encoded += written;
|
||
remainingSpace -= written;
|
||
} else {
|
||
return {}; // security check failed
|
||
}
|
||
}
|
||
p_input++;
|
||
}
|
||
|
||
// Null-terminieren
|
||
if (remainingSpace > 0) {
|
||
*p_encoded = '\0';
|
||
} else {
|
||
return {}; // security check failed
|
||
}
|
||
encoded.shrink_to_fit();
|
||
return encoded;
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
// separate task for decoding and outputting the data. 'playAudioData()' is started periodically and fetches the data from the InBuffer. This ensures
|
||
// that the I2S-DMA is always sufficiently filled, even if the Arduino 'loop' is stuck.
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
|
||
void Audio::setAudioTaskCore(uint8_t coreID) { // Recommendation:If the ARDUINO RUNNING CORE is 1, the audio task should be core 0 or vice versa
|
||
if (coreID > 1) return;
|
||
stopAudioTask();
|
||
xSemaphoreTake(mutex_audioTask, 0.3 * configTICK_RATE_HZ);
|
||
m_audioTaskCoreId = coreID;
|
||
xSemaphoreGive(mutex_audioTask);
|
||
startAudioTask();
|
||
}
|
||
|
||
void Audio::startAudioTask() {
|
||
|
||
if (m_f_audioTaskIsRunning) {
|
||
AUDIO_LOG_DEBUG("audio task is already running.");
|
||
return;
|
||
}
|
||
AUDIO_LOG_INFO("start audio task.");
|
||
m_f_audioTaskIsRunning = true;
|
||
|
||
m_audioTaskHandle = xTaskCreateStaticPinnedToCore(&Audio::audioTaskWrapper, /* Function to implement the task */
|
||
"PeriodicTask", /* Name of the task */
|
||
AUDIO_STACK_SIZE, /* Stack size in words */
|
||
this, /* Task input parameter */
|
||
2, /* Priority of the task */
|
||
xAudioStack, /* Task stack */
|
||
&xAudioTaskBuffer, /* Memory for the task's control block */
|
||
m_audioTaskCoreId /* Core where the task should run */
|
||
);
|
||
}
|
||
|
||
void Audio::stopAudioTask() {
|
||
if (!m_f_audioTaskIsRunning) {
|
||
AUDIO_LOG_DEBUG("audio task is not running.");
|
||
return;
|
||
}
|
||
AUDIO_LOG_INFO("stop audio task.");
|
||
xSemaphoreTake(mutex_audioTask, 0.3 * configTICK_RATE_HZ);
|
||
m_f_audioTaskIsRunning = false;
|
||
if (m_audioTaskHandle != nullptr) {
|
||
vTaskDelete(m_audioTaskHandle);
|
||
m_audioTaskHandle = nullptr;
|
||
}
|
||
xSemaphoreGive(mutex_audioTask);
|
||
}
|
||
|
||
void Audio::audioTaskWrapper(void* param) {
|
||
Audio* audioRunner = static_cast<Audio*>(param);
|
||
audioRunner->audioTask();
|
||
}
|
||
|
||
void Audio::audioTask() {
|
||
while (m_f_audioTaskIsRunning) {
|
||
vTaskDelay(1 / portTICK_PERIOD_MS); // periodically every x ms
|
||
if (m_f_I2S_init) performAudioTask();
|
||
}
|
||
vTaskDelete(nullptr); // Delete this task
|
||
}
|
||
|
||
void Audio::performAudioTask() {
|
||
if (m_decoder) {
|
||
xSemaphoreTake(mutex_audioTask, 0.3 * configTICK_RATE_HZ);
|
||
while (m_validSamples) {
|
||
vTaskDelay(20 / portTICK_PERIOD_MS);
|
||
playChunk();
|
||
} // I2S buffer full
|
||
playAudioData();
|
||
xSemaphoreGive(mutex_audioTask);
|
||
gain_ramp();
|
||
return;
|
||
} else {
|
||
int32_t c[2] = {0};
|
||
calculateVUlevel(c);
|
||
gain_ramp();
|
||
vTaskDelay(20);
|
||
return;
|
||
}
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||
uint32_t Audio::getHighWatermark() {
|
||
UBaseType_t highWaterMark = uxTaskGetStackHighWaterMark(m_audioTaskHandle);
|
||
return highWaterMark; // dwords
|
||
}
|
||
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|