Files
sunflower-girl/libraries/ESP32-audioI2S/src/Audio.cpp

7799 lines
398 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*****************************************************************************************************************************************************
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 &copy;
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&#324; 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
}
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————