This commit is contained in:
pschatzmann 2024-03-26 18:43:14 +01:00
parent e87406e61b
commit 822c827585
15 changed files with 307 additions and 117 deletions

View File

@ -13,7 +13,7 @@ namespace audio_tools {
#endif
#ifndef MTS_UNDERFLOW_LIMIT
# define MTS_UNDERFLOW_LIMIT 200
# define MTS_UNDERFLOW_LIMIT 188
#endif
#ifndef MTS_WRITE_BUFFER_SIZE
@ -177,13 +177,13 @@ class MTSDecoder : public AudioDecoder {
TRACED();
TSDCode res = TSD_OK;
int count = 0;
while (res == TSD_OK && buffer.available() > limit) {
while (res == TSD_OK && buffer.available() >= limit) {
// Unfortunatly we need to reset the demux after each file
if (is_new_file(buffer.data())){
LOGD("parsing new file");
begin();
}
size_t len;
size_t len = 0;
res = tsd_demux(&ctx, (void *)buffer.data(), buffer.available(), &len);
// remove processed bytes
buffer.clearArray(len);

View File

@ -226,7 +226,7 @@
#define USE_EXT_BUTTON_LOGIC
//#define USE_ALLOCATOR true
#define HAS_IOSTRAM
#define USE_TASK true
#define USE_TASK false
#define PWM_FREQENCY 30000
#define PIN_PWM_START 12
@ -741,6 +741,10 @@ typedef WiFiClient WiFiClientSecure;
# define URL_HANDSHAKE_TIMEOUT 120000
#endif
#ifndef USE_TASK
# define USE_TASK false
#endif
#ifndef USE_SERVER_ACCEPT
# define USE_SERVER_ACCEPT false
#endif

View File

@ -1,6 +1,6 @@
#pragma once
#if defined(ESP32) && defined(USE_URL_ARDUINO)
#include "AudioConfig.h"
#include "AudioConfig.h"
#if USE_TASK && defined(USE_URL_ARDUINO)
#include "AudioHttp/ICYStream.h"
namespace audio_tools {
@ -105,4 +105,4 @@ class ICYStreamBuffered : public AbstractURLStream {
} // namespace audio_tools
#endif // ESP32
#endif // USE_TASK

View File

@ -1,6 +1,6 @@
#pragma once
#if defined(ESP32) && defined(USE_URL_ARDUINO)
#include "AudioConfig.h"
#if USE_TASK && defined(USE_URL_ARDUINO)
#include "AudioHttp/URLStream.h"
#include "AudioTools/AudioStreams.h"
#include "Concurrency/SynchronizedBuffers.h"

View File

@ -8,38 +8,83 @@
#include "Concurrency/Concurrency.h"
#define MAX_HLS_LINE 512
#define START_URLS_LIMIT 4
#define HLS_BUFFER_COUNT 10
namespace audio_tools {
/// @brief Abstract API for URLLoaderHLS
class URLLoaderHLSBase {
public:
virtual bool begin() = 0;
virtual void end() = 0;
virtual void addUrl(const char *url) = 0;
virtual int urlCount() = 0;
virtual int available() { return 0; }
virtual size_t readBytes(uint8_t *data, size_t len) { return 0; }
const char *contentType() { return nullptr; }
int contentLength() { return 0; }
virtual void setBuffer(int size, int count) {}
};
/// URLLoader which saves the HLS segments to the indicated output
class URLLoaderHLSOutput {
public:
URLLoaderHLSOutput(Print &out, int maxUrls = 20) {
max = maxUrls;
p_print = &out;
}
virtual bool begin() { return true; };
virtual void end(){};
virtual void addUrl(const char *url) {
LOGI("saving data for %s", url);
url_stream.begin(url);
url_stream.waitForData(500);
copier.begin(*p_print, url_stream);
int bytes_copied = copier.copyAll();
LOGI("Copied %d of %d", bytes_copied, url_stream.contentLength());
assert(bytes_copied == url_stream.contentLength());
url_stream.end();
}
virtual int urlCount() { return 0; }
protected:
int count = 0;
int max = 20;
Print *p_print;
URLStream url_stream;
StreamCopy copier;
};
/***
* @brief We feed the URLLoader with some url strings. At each readBytes or
* available() call we refill the buffer. The buffer must be big enough to
* bridge the delays caused by the reloading of the segments
* @brief We feed the URLLoaderHLS with some url strings. The data of the
* related segments are provided via the readBytes() method.
* @author Phil Schatzmann
* @copyright GPLv3
*/
class URLLoader {
class URLLoaderHLS : public URLLoaderHLSBase {
public:
URLLoader(URLStream &stream) { p_stream = &stream; };
URLLoader() { p_stream = &default_stream; };
// URLLoaderHLS(URLStream &stream) { p_stream = &stream; };
URLLoaderHLS() = default;
~URLLoader() { end(); }
~URLLoaderHLS() { end(); }
bool begin() {
bool begin() override {
TRACED();
#if USE_TASK
buffer.resize(buffer_size * buffer_count);
task.begin(std::bind(&URLLoader::bufferRefill, this));
task.begin(std::bind(&URLLoaderHLS::bufferRefill, this));
#else
buffer.resize(buffer_size, buffer_count);
buffer.resize(buffer_size * buffer_count);
#endif
active = true;
return true;
}
void end() {
void end() override {
TRACED();
#if USE_TASK
task.end();
@ -51,7 +96,7 @@ class URLLoader {
}
/// Adds the next url to be played in sequence
void addUrl(const char *url) {
void addUrl(const char *url) override {
LOGI("Adding %s", url);
Str url_str(url);
char *str = new char[url_str.length() + 1];
@ -64,10 +109,10 @@ class URLLoader {
/// Provides the number of open urls which can be played. Refills them, when
/// min limit is reached.
int urlCount() { return urls.size(); }
int urlCount() override { return urls.size(); }
/// Available bytes of the audio stream
int available() {
int available() override {
if (!active) return 0;
TRACED();
#if !USE_TASK
@ -77,7 +122,7 @@ class URLLoader {
}
/// Provides data from the audio stream
size_t readBytes(uint8_t *data, size_t len) {
size_t readBytes(uint8_t *data, size_t len) override {
if (!active) return 0;
TRACED();
#if !USE_TASK
@ -97,25 +142,25 @@ class URLLoader {
return p_stream->contentLength();
}
void setBuffer(int size, int count) {
void setBuffer(int size, int count) override {
buffer_size = size;
buffer_count = count;
}
protected:
URLStream default_stream;
Vector<const char *> urls{10};
#if USE_TASK
BufferRTOS<uint8_t> buffer{0};
Task task{"Refill", 1024 * 5, 1, 1};
Mutex mutex;
#else
NBuffer<uint8_t> buffer{0, 0};
RingBuffer<uint8_t> buffer{0};
#endif
bool active = false;
int buffer_size = DEFAULT_BUFFER_SIZE;
int buffer_count = 10;
URLStream *p_stream = nullptr;
int buffer_count = HLS_BUFFER_COUNT;
URLStream default_stream;
URLStream *p_stream = &default_stream;
const char *url_to_play = nullptr;
/// try to keep the buffer filled
@ -134,15 +179,14 @@ class URLLoader {
}
// switch current stream if we have no more data
if ((!*p_stream || p_stream->totalRead() == p_stream->contentLength()) &&
!urls.empty()) {
if (!*p_stream && !urls.empty()) {
LOGD("Refilling");
if (url_to_play != nullptr) {
delete url_to_play;
}
url_to_play = urls[0];
LOGI("playing %s", url_to_play);
if (p_stream->httpRequest().connected()) p_stream->end();
p_stream->setTimeout(5000);
p_stream->begin(url_to_play);
p_stream->waitForData(500);
#if USE_TASK
@ -157,25 +201,36 @@ class URLLoader {
LOGI("Playing %s of %d", p_stream->urlStr(), (int)urls.size());
}
int total = 0;
int failed = 0;
int to_write = min(buffer.availableForWrite(), DEFAULT_BUFFER_SIZE);
if (to_write > 0) {
int total = 0;
int failed = 0;
while (to_write > 0) {
if (p_stream->totalRead() == p_stream->contentLength()) break;
uint8_t tmp[to_write] = {0};
int read = p_stream->readBytes(tmp, to_write);
total += read;
if (read > 0) {
buffer.writeArray(tmp, read);
to_write = min(buffer.availableForWrite(), DEFAULT_BUFFER_SIZE);
} else {
delay(5);
// this should not really happen
failed++;
if (failed >= 3) break;
// try to keep the buffer filled
while (to_write > 0) {
uint8_t tmp[to_write] = {0};
int read = p_stream->readBytes(tmp, to_write);
total += read;
if (read > 0) {
failed = 0;
buffer.writeArray(tmp, read);
LOGI("buffer add %d -> %d:", read, buffer.available());
to_write = min(buffer.availableForWrite(), DEFAULT_BUFFER_SIZE);
} else {
delay(10);
// this should not really happen
failed++;
LOGW("No data idx %d: available: %d", failed, p_stream->available());
if (failed >= 5) {
LOGE("No data idx %d: available: %d", failed, p_stream->available());
if (p_stream->available() == 0) p_stream->end();
break;
}
}
// After we processed all data we close the stream to get a new url
if (p_stream->totalRead() == p_stream->contentLength()) {
p_stream->end();
break;
}
LOGD("Refilled with %d now %d available to write", total,
buffer.availableForWrite());
}
@ -244,7 +299,7 @@ class HLSParser {
return false;
}
if (!url_loader.begin()) {
if (!p_url_loader->begin()) {
TRACEE();
return false;
}
@ -261,10 +316,10 @@ class HLSParser {
TRACED();
int result = 0;
custom_log_level.set();
#if USE_TASK
#if !USE_TASK
reloadSegments();
#endif
if (active) result = url_loader.available();
if (active) result = p_url_loader->available();
custom_log_level.reset();
return result;
}
@ -273,10 +328,10 @@ class HLSParser {
TRACED();
size_t result = 0;
custom_log_level.set();
#if USE_TASK
#if !USE_TASK
reloadSegments();
#endif
if (active) result = url_loader.readBytes(buffer, len);
if (active) result = p_url_loader->readBytes(buffer, len);
custom_log_level.reset();
return result;
}
@ -292,9 +347,9 @@ class HLSParser {
const char *getCodec() { return codec.c_str(); }
/// Provides the content type of the audio data
const char *contentType() { return url_loader.contentType(); }
const char *contentType() { return p_url_loader->contentType(); }
int contentLength() { return url_loader.contentLength(); }
int contentLength() { return p_url_loader->contentLength(); }
/// Closes the processing
void end() {
@ -305,18 +360,20 @@ class HLSParser {
codec.clear();
segments_url_str.clear();
url_stream.end();
url_loader.end();
p_url_loader->end();
url_history.clear();
active = false;
}
/// Defines the number of urls that are preloaded in the URLLoader
/// Defines the number of urls that are preloaded in the URLLoaderHLS
void setUrlCount(int count) { url_count = count; }
/// Defines the class specific custom log level
void setLogLevel(AudioLogger::LogLevel level) { custom_log_level.set(level); }
void setBuffer(int size, int count) { url_loader.setBuffer(size, count); }
void setBuffer(int size, int count) { p_url_loader->setBuffer(size, count); }
void setUrlLoader(URLLoaderHLSBase &loader) { p_url_loader = &loader; }
protected:
CustomLogLevel custom_log_level;
@ -329,7 +386,8 @@ class HLSParser {
StrExt url_str;
const char *index_url_str = nullptr;
URLStream url_stream;
URLLoader url_loader;
URLLoaderHLS default_url_loader;
URLLoaderHLSBase *p_url_loader = &default_url_loader;
URLHistory url_history;
#if USE_TASK
Task segment_load_task{"Refill", 1024 * 5, 1, 1};
@ -353,7 +411,7 @@ class HLSParser {
// parse the index file and the segments
bool parseIndex() {
TRACED();
url_stream.setTimeout(1000);
url_stream.setTimeout(5000);
// url_stream.setConnectionClose(true);
// we only update the content length
@ -364,54 +422,7 @@ class HLSParser {
return rc;
}
// parse the segment url provided by the index
bool parseSegments() {
TRACED();
if (parse_segments_active) {
return false;
}
// make sure that we load at relevant schedule
if (millis() < next_sement_load_time && url_loader.urlCount() > 0) {
return false;
}
parse_segments_active = true;
LOGI("Available urls: %d", url_loader.urlCount());
if (url_stream) url_stream.clear();
LOGI("parsing %s", segments_url_str.c_str());
if (segments_url_str.isEmpty()) {
TRACEE();
parse_segments_active = false;
return false;
}
if (!url_stream.begin(segments_url_str.c_str())) {
TRACEE();
parse_segments_active = false;
return false;
}
segment_count = 0;
if (!parseSegmentLines()) {
TRACEE();
parse_segments_active = false;
return false;
}
next_sement_load_time = millis() + (segment_count * tartget_duration_ms);
// assert(segment_count > 0);
// we request a minimum of collected urls to play before we start
if (url_history.size() > 8) active = true;
parse_segments_active = false;
return true;
}
// parse the index file and the segments
// parse the index file
bool parseIndexLines() {
TRACEI();
char tmp[MAX_HLS_LINE];
@ -441,7 +452,57 @@ class HLSParser {
return result;
}
// parse the index file and the segments
// parse the segment url provided by the index
bool parseSegments() {
TRACED();
if (parse_segments_active) {
return false;
}
// make sure that we load at relevant schedule
if (millis() < next_sement_load_time && p_url_loader->urlCount() > 1) {
delay(1);
return false;
}
parse_segments_active = true;
LOGI("Available urls: %d", p_url_loader->urlCount());
if (url_stream) url_stream.clear();
LOGI("parsing %s", segments_url_str.c_str());
if (segments_url_str.isEmpty()) {
TRACEE();
parse_segments_active = false;
return false;
}
if (!url_stream.begin(segments_url_str.c_str())) {
TRACEE();
parse_segments_active = false;
return false;
}
segment_count = 0;
if (!parseSegmentLines()) {
TRACEE();
parse_segments_active = false;
// do not display as erro
return true;
}
next_sement_load_time = millis() + (segment_count * tartget_duration_ms);
// assert(segment_count > 0);
// we request a minimum of collected urls to play before we start
if (url_history.size() > START_URLS_LIMIT) active = true;
parse_segments_active = false;
return true;
}
// parse the segments
bool parseSegmentLines() {
TRACEI();
char tmp[MAX_HLS_LINE];
@ -509,9 +570,9 @@ class HLSParser {
url_str.add("/");
url_str.add(str.c_str());
}
url_loader.addUrl(url_str.c_str());
p_url_loader->addUrl(url_str.c_str());
} else {
LOGD("Douplicate ignored: %s", str.c_str());
LOGD("Duplicate ignored: %s", str.c_str());
}
}
return true;

View File

@ -259,6 +259,11 @@ public:
/// same as setOutput
void setStream(Print &out) { p_out = &out; }
bool begin(AudioInfo info){
setAudioInfo(info);
return AudioStream::begin();
}
void setAudioInfo(AudioInfo info) override {
AudioStream::setAudioInfo(info);
fade_last.setAudioInfo(info);

View File

@ -3,6 +3,7 @@
#include "AudioTools/AudioLogger.h"
#include "AudioBasic/Collections/Allocator.h"
#if USE_TASK
#ifdef ESP32
# include <freertos/stream_buffer.h>
# include "freertos/FreeRTOS.h"
@ -195,4 +196,6 @@ class BufferRTOS : public BaseBuffer<T> {
template <class T>
using SynchronizedBufferRTOS = BufferRTOS<T>;
} // namespace audio_tools
} // namespace audio_tools
#endif

View File

@ -6,6 +6,7 @@
# include <mutex>
#endif
#if USE_TASK
#ifdef ESP32
# include "freertos/FreeRTOS.h"
# include "freertos/semphr.h"
@ -84,6 +85,7 @@ protected:
SemaphoreHandle_t xSemaphore = NULL;
};
/**
* @brief RAII implementaion using a Mutex: Only a few microcontrollers provide
* lock guards, so I decided to roll my own solution where we can just use a
@ -114,4 +116,6 @@ protected:
Mutex *p_mutex = nullptr;
};
}
}
#endif

View File

@ -2,6 +2,7 @@
#include "AudioBasic/Collections/Allocator.h"
#include "AudioConfig.h"
#if USE_TASK
#ifdef ESP32
# include <freertos/queue.h>
# include "freertos/FreeRTOS.h"
@ -109,4 +110,6 @@ class QueueRTOS {
}
};
} // namespace audio_tools
} // namespace audio_tools
#endif

View File

@ -4,6 +4,7 @@
#include "AudioTools/Buffers.h"
#include "AudioTools/AudioLogger.h"
#if USE_TASK
#ifdef ESP32
# include "freertos/FreeRTOS.h"
# include "Concurrency/QueueRTOS.h"
@ -205,3 +206,4 @@ protected:
} // namespace audio_tools
#endif

View File

@ -1,5 +1,6 @@
#pragma once
#if USE_TASK
#ifdef ESP32
# include "freertos/FreeRTOS.h"
# include "freertos/task.h"
@ -64,4 +65,6 @@ class Task {
}
};
}
}
#endif

View File

@ -26,7 +26,7 @@ if(NOT tsdemux_POPULATED)
endif()
# Build with libhelix
FetchContent_Declare(arduino_helix GIT_REPOSITORY "https://github.com/pschatzmann/arduino-libhelix.git" GIT_TAG main )
FetchContent_Declare(arduino_helix GIT_REPOSITORY "https://github.com/pschatzmann/arduino-libhelix.git" GIT_TAG development )
FetchContent_GetProperties(arduino_helix)
if(NOT arduino_helix_POPULATED)
FetchContent_Populate(arduino_helix)

View File

@ -1,5 +1,5 @@
#include "AudioTools.h"
#include "AudioCodecs/CodecMTS.h"
#include "AudioCodecs/CodecTSDemux.h"
#include "AudioCodecs/CodecADTS.h"
#include "AudioCodecs/CodecAACHelix.h"
//#include "AudioLibs/PortAudioStream.h"
@ -29,7 +29,7 @@ void setup(void) {
//adts_stream.setLogLevel(AudioLogger::Debug);
//mts_stream.setLogLevel(AudioLogger::Debug);
//aac.setAudioInfoNotifications(false);
aac.setAudioInfoNotifications(false);
auto cfg = out.defaultConfig(TX_MODE);
cfg.copyFrom(info);

View File

@ -0,0 +1,55 @@
cmake_minimum_required(VERSION 3.20)
# set the project name
project(hls)
set (CMAKE_CXX_STANDARD 11)
set (DCMAKE_CXX_FLAGS "-Werror")
include(FetchContent)
# Activate Emulator and Portaudio
set(ADD_ARDUINO_EMULATOR ON CACHE BOOL "Add Arduino Emulator Library")
set(ADD_PORTAUDIO OFF CACHE BOOL "Add Portaudio Library")
# Build with arduino-audio-tools
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../.. ${CMAKE_CURRENT_BINARY_DIR}/arduino-audio-tools )
endif()
# Build with tms
FetchContent_Declare(tsdemux GIT_REPOSITORY "https://github.com/pschatzmann/arduino-tsdemux" )
FetchContent_GetProperties(tsdemux)
if(NOT tsdemux_POPULATED)
FetchContent_Populate(tsdemux)
add_subdirectory(${tsdemux_SOURCE_DIR})
endif()
# Build with libhelix
FetchContent_Declare(arduino_helix GIT_REPOSITORY "https://github.com/pschatzmann/arduino-libhelix.git" GIT_TAG main )
FetchContent_GetProperties(arduino_helix)
if(NOT arduino_helix_POPULATED)
FetchContent_Populate(arduino_helix)
add_subdirectory(${arduino_helix_SOURCE_DIR})
endif()
# Download miniaudio.h
file(DOWNLOAD https://raw.githubusercontent.com/mackron/miniaudio/master/miniaudio.h
${CMAKE_CURRENT_SOURCE_DIR}/miniaudio.h)
# build sketch as executable
set_source_files_properties(hls.ino PROPERTIES LANGUAGE CXX)
add_executable (hls hls.cpp ../../main.cpp )
# set preprocessor defines
target_compile_definitions(hls PUBLIC -DARDUINO -DIS_DESKTOP -DEXIT_ON_STOP -DHELIX_PRINT)
# access to miniaudio in sketch directory
target_include_directories(hls PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
# specify libraries
target_link_libraries(hls arduino-audio-tools arduino_emulator tsdemux arduino_helix)

View File

@ -0,0 +1,50 @@
#include "AudioTools.h"
#include "AudioCodecs/CodecMTS.h"
#include "AudioCodecs/CodecADTS.h"
#include "AudioCodecs/CodecAACHelix.h"
//#include "AudioLibs/PortAudioStream.h"
#include "AudioLibs/MiniAudioStream.h"
#include "AudioLibs/HLSStream.h"
AudioInfo info(48000,2,16);
HLSStream hls_stream("NA", "NA");
// HexDumpOutput hex(Serial);
// NullStream null;
//CsvOutput<int16_t> out(Serial, 2); // Or use StdOuput
//PortAudioStream out;
MiniAudioStream out;
AACDecoderHelix aac;
CodecMTS mts{aac};
// ADTSDecoder adts;
// AACDecoderHelix aac;
// EncodedAudioStream aac_stream(&out, &aac);
// EncodedAudioStream adts_stream(&aac_stream, &adts);
EncodedAudioStream mts_stream(&out, &mts);
StreamCopy copier(mts_stream, hls_stream);
// Arduino Setup
void setup(void) {
//Serial.begin(115200);
AudioLogger::instance().begin(Serial, AudioLogger::Info);
//hls_stream.setLogLevel(AudioLogger::Debug); // hls_stream is quite chatty at Info
//adts_stream.setLogLevel(AudioLogger::Debug);
//mts_stream.setLogLevel(AudioLogger::Debug);
//aac.setAudioInfoNotifications(false);
auto cfg = out.defaultConfig(TX_MODE);
cfg.copyFrom(info);
out.begin();
mts_stream.begin();
// aac_stream.begin();
// adts_stream.begin();
hls_stream.begin("http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_vlow/ak/bbc_world_service.m3u8");
Serial.println("playing...");
}
// Arduino loop
void loop() {
copier.copy();
}