See CHANGELOG - release

This commit is contained in:
philippe44
2024-01-16 18:51:33 -08:00
parent 9e3c6dcf30
commit ccc7b86369
13 changed files with 136 additions and 73 deletions

View File

@@ -1,3 +1,9 @@
2024-01-16
- catch-up with cspot latest
- refactor airplay flush/first packet
- new libFLAC that supports multi-stream OggFlac
- fix output threshold
2024-01-10 2024-01-10
- add OggFlac to stream metadata - add OggFlac to stream metadata
- fix OggFlac deadlock in flac callback when not enough data in streambuf - fix OggFlac deadlock in flac callback when not enough data in streambuf

Binary file not shown.

View File

@@ -126,11 +126,6 @@ typedef struct rtp_s {
u32_t rtp, time; u32_t rtp, time;
u8_t status; u8_t status;
} synchro; } synchro;
struct {
u32_t time;
seq_t seqno;
u32_t rtptime;
} record;
int latency; // rtp hold depth in samples int latency; // rtp hold depth in samples
u32_t resent_req, resent_rec; // total resent + recovered frames u32_t resent_req, resent_rec; // total resent + recovered frames
u32_t silent_frames; // total silence frames u32_t silent_frames; // total silence frames
@@ -147,8 +142,8 @@ typedef struct rtp_s {
#endif #endif
struct alac_codec_s *alac_codec; struct alac_codec_s *alac_codec;
int flush_seqno; int first_seqno;
bool playing; enum { RTP_WAIT, RTP_STREAM, RTP_PLAY } state;
int stalled; int stalled;
raop_data_cb_t data_cb; raop_data_cb_t data_cb;
raop_cmd_cb_t cmd_cb; raop_cmd_cb_t cmd_cb;
@@ -231,7 +226,7 @@ rtp_resp_t rtp_init(struct in_addr host, int latency, char *aeskey, char *aesiv,
ctx->rtp_host.sin_family = AF_INET; ctx->rtp_host.sin_family = AF_INET;
ctx->rtp_host.sin_addr.s_addr = INADDR_ANY; ctx->rtp_host.sin_addr.s_addr = INADDR_ANY;
pthread_mutex_init(&ctx->ab_mutex, 0); pthread_mutex_init(&ctx->ab_mutex, 0);
ctx->flush_seqno = -1; ctx->first_seqno = -1;
ctx->latency = latency; ctx->latency = latency;
ctx->ab_read = ctx->ab_write; ctx->ab_read = ctx->ab_write;
@@ -339,24 +334,23 @@ void rtp_end(rtp_t *ctx)
/*---------------------------------------------------------------------------*/ /*---------------------------------------------------------------------------*/
bool rtp_flush(rtp_t *ctx, unsigned short seqno, unsigned int rtptime, bool exit_locked) bool rtp_flush(rtp_t *ctx, unsigned short seqno, unsigned int rtptime, bool exit_locked)
{ {
bool rc = true; pthread_mutex_lock(&ctx->ab_mutex);
u32_t now = gettime_ms();
// always store flush seqno as we only want stricly above it, even when equal to RECORD
ctx->first_seqno = seqno;
bool flushed = false;
if (now < ctx->record.time + 250 || (ctx->record.seqno == seqno && ctx->record.rtptime == rtptime)) { // no need to stop playing if recent or equal to record - but first_seqno is needed
rc = false; if (ctx->state == RTP_PLAY) {
LOG_ERROR("[%p]: FLUSH ignored as same as RECORD (%hu - %u)", ctx, seqno, rtptime); buffer_reset(ctx->audio_buffer);
} else { ctx->state = RTP_WAIT;
pthread_mutex_lock(&ctx->ab_mutex); flushed = true;
buffer_reset(ctx->audio_buffer); LOG_INFO("[%p]: FLUSH packets below %hu - %u", ctx, seqno, rtptime);
ctx->playing = false;
ctx->flush_seqno = seqno;
if (!exit_locked) pthread_mutex_unlock(&ctx->ab_mutex);
} }
LOG_INFO("[%p]: flush %hu %u", ctx, seqno, rtptime); if (!exit_locked || !flushed) pthread_mutex_unlock(&ctx->ab_mutex);
return flushed;
return rc;
} }
/*---------------------------------------------------------------------------*/ /*---------------------------------------------------------------------------*/
@@ -367,11 +361,9 @@ void rtp_flush_release(rtp_t *ctx) {
/*---------------------------------------------------------------------------*/ /*---------------------------------------------------------------------------*/
void rtp_record(rtp_t *ctx, unsigned short seqno, unsigned rtptime) { void rtp_record(rtp_t *ctx, unsigned short seqno, unsigned rtptime) {
ctx->record.seqno = seqno; ctx->first_seqno = (seqno || rtptime) ? seqno : -1;
ctx->record.rtptime = rtptime; ctx->state = RTP_WAIT;
ctx->record.time = gettime_ms(); LOG_INFO("[%p]: record %hu - %u", ctx, seqno, rtptime);
LOG_INFO("[%p]: record %hu %u", ctx, seqno, rtptime);
} }
/*---------------------------------------------------------------------------*/ /*---------------------------------------------------------------------------*/
@@ -442,26 +434,50 @@ static void alac_decode(rtp_t *ctx, s16_t *dest, char *buf, int len, u16_t *outs
/*---------------------------------------------------------------------------*/ /*---------------------------------------------------------------------------*/
static void buffer_put_packet(rtp_t *ctx, seq_t seqno, unsigned rtptime, bool first, char *data, int len) { static void buffer_put_packet(rtp_t *ctx, seq_t seqno, unsigned rtptime, bool first, char *data, int len) {
abuf_t *abuf = NULL; abuf_t *abuf = NULL;
u32_t playtime;
pthread_mutex_lock(&ctx->ab_mutex); pthread_mutex_lock(&ctx->ab_mutex);
if (!ctx->playing) {
if ((ctx->flush_seqno == -1 || seq_order(ctx->flush_seqno, seqno)) &&
(ctx->synchro.status & RTP_SYNC) && (ctx->synchro.status & NTP_SYNC)) {
ctx->ab_write = seqno-1;
ctx->ab_read = seqno;
ctx->flush_seqno = -1;
ctx->playing = true;
ctx->resent_req = ctx->resent_rec = ctx->silent_frames = ctx->discarded = 0;
playtime = ctx->synchro.time + ((rtptime - ctx->synchro.rtp) * 10) / (RAOP_SAMPLE_RATE / 100);
ctx->cmd_cb(RAOP_PLAY, playtime);
} else {
pthread_mutex_unlock(&ctx->ab_mutex);
return;
}
}
/* if we have received a RECORD with a seqno, then this is the first allowed rtp sequence number
* and we are in RTP_WAIT state. If seqno was 0, then we are waiting for a flush that will tell
* us what should be our first allowed packet but we must accept everything, wait and clean when
* we the it arrives. This means that first packet moves us to RTP_STREAM state where we accept
* frames but wait for the FLUSH. If this was a FLUSH while playing, then we are also in RTP_WAIT
* state but we do have an allowed seqno and we should not accept any frame before we have it */
// if we have a pending first seqno and we are below, always ignore it
if (ctx->first_seqno != -1 && seq_order(seqno, ctx->first_seqno)) {
pthread_mutex_unlock(&ctx->ab_mutex);
return;
}
if (ctx->state == RTP_WAIT) {
ctx->ab_write = seqno - 1;
ctx->ab_read = ctx->ab_write + 1;
ctx->resent_req = ctx->resent_rec = ctx->silent_frames = ctx->discarded = 0;
if (ctx->first_seqno != -1) {
LOG_INFO("[%p]: 1st accepted packet:%d, now playing", ctx, seqno);
ctx->state = RTP_PLAY;
ctx->first_seqno = -1;
u32_t playtime = ctx->synchro.time + ((rtptime - ctx->synchro.rtp) * 10) / (RAOP_SAMPLE_RATE / 100);
ctx->cmd_cb(RAOP_PLAY, playtime);
} else {
ctx->state = RTP_STREAM;
LOG_INFO("[%p]: 1st accepted packet:%hu, waiting for FLUSH", ctx, seqno);
}
} else if (ctx->state == RTP_STREAM && ctx->first_seqno != -1 && seq_order(ctx->first_seqno, seqno + 1)) {
// now we're talking, but first discard all packets with a seqno below first_seqno AND not ready
while (seq_order(ctx->ab_read, ctx->first_seqno) ||
!ctx->audio_buffer[BUFIDX(ctx->ab_read)].ready) {
ctx->audio_buffer[BUFIDX(ctx->ab_read)].ready = false;
ctx->ab_read++;
}
LOG_INFO("[%p]: done waiting for FLUSH with packet:%d, now playing starting:%hu", ctx, seqno, ctx->ab_read);
ctx->state = RTP_PLAY;
ctx->first_seqno = -1;
u32_t playtime = ctx->synchro.time + ((rtptime - ctx->synchro.rtp) * 10) / (RAOP_SAMPLE_RATE / 100);
ctx->cmd_cb(RAOP_PLAY, playtime);
}
if (seqno == (u16_t) (ctx->ab_write+1)) { if (seqno == (u16_t) (ctx->ab_write+1)) {
// expected packet // expected packet
abuf = ctx->audio_buffer + BUFIDX(seqno); abuf = ctx->audio_buffer + BUFIDX(seqno);
@@ -475,7 +491,7 @@ static void buffer_put_packet(rtp_t *ctx, seq_t seqno, unsigned rtptime, bool fi
ctx->ab_read = seqno; ctx->ab_read = seqno;
} else { } else {
// request re-send missed frames and evaluate resent date as a whole *after* // request re-send missed frames and evaluate resent date as a whole *after*
rtp_request_resend(ctx, ctx->ab_write + 1, seqno-1); if (ctx->state == RTP_PLAY) rtp_request_resend(ctx, ctx->ab_write + 1, seqno-1);
// resend date is after all requests have been sent // resend date is after all requests have been sent
u32_t now = gettime_ms(); u32_t now = gettime_ms();
@@ -528,7 +544,7 @@ static void buffer_push_packet(rtp_t *ctx) {
u32_t now, playtime, hold = max((ctx->latency * 1000) / (8 * RAOP_SAMPLE_RATE), 100); u32_t now, playtime, hold = max((ctx->latency * 1000) / (8 * RAOP_SAMPLE_RATE), 100);
// not ready to play yet // not ready to play yet
if (!ctx->playing || ctx->synchro.status != (RTP_SYNC | NTP_SYNC)) return; if (ctx->state != RTP_PLAY || ctx->synchro.status != (RTP_SYNC | NTP_SYNC)) return;
// there is always at least one frame in the buffer // there is always at least one frame in the buffer
do { do {

View File

@@ -22482,9 +22482,13 @@ mg_init_library(unsigned features)
file_mutex_init = file_mutex_init =
pthread_mutex_init(&global_log_file_lock, &pthread_mutex_attr); pthread_mutex_init(&global_log_file_lock, &pthread_mutex_attr);
if (file_mutex_init == 0) { if (file_mutex_init == 0) {
#ifdef WINSOCK_START
/* Start WinSock */ /* Start WinSock */
WSADATA data; WSADATA data;
failed = wsa = WSAStartup(MAKEWORD(2, 2), &data); failed = wsa = WSAStartup(MAKEWORD(2, 2), &data);
#else
failed = wsa = 0;
#endif
} }
#else #else
mutexattr_init = pthread_mutexattr_init(&pthread_mutex_attr); mutexattr_init = pthread_mutexattr_init(&pthread_mutex_attr);
@@ -22498,7 +22502,9 @@ mg_init_library(unsigned features)
if (failed) { if (failed) {
#if defined(_WIN32) #if defined(_WIN32)
if (wsa == 0) { if (wsa == 0) {
#ifdef WINSOCK_START
(void)WSACleanup(); (void)WSACleanup();
#endif
} }
if (file_mutex_init == 0) { if (file_mutex_init == 0) {
(void)pthread_mutex_destroy(&global_log_file_lock); (void)pthread_mutex_destroy(&global_log_file_lock);
@@ -22598,7 +22604,9 @@ mg_exit_library(void)
#endif #endif
#if defined(_WIN32) #if defined(_WIN32)
#ifdef WINSOCK_START
(void)WSACleanup(); (void)WSACleanup();
#endif
(void)pthread_mutex_destroy(&global_log_file_lock); (void)pthread_mutex_destroy(&global_log_file_lock);
#else #else
(void)pthread_mutexattr_destroy(&pthread_mutex_attr); (void)pthread_mutexattr_destroy(&pthread_mutex_attr);

View File

@@ -12,6 +12,8 @@
using namespace bell; using namespace bell;
std::mutex BellHTTPServer::initMutex;
class WebSocketHandler : public CivetWebSocketHandler { class WebSocketHandler : public CivetWebSocketHandler {
public: public:
BellHTTPServer::WSDataHandler dataHandler; BellHTTPServer::WSDataHandler dataHandler;
@@ -187,6 +189,7 @@ bool BellHTTPServer::handlePost(CivetServer* server,
} }
BellHTTPServer::BellHTTPServer(int serverPort) { BellHTTPServer::BellHTTPServer(int serverPort) {
std::lock_guard lock(initMutex);
mg_init_library(0); mg_init_library(0);
BELL_LOG(info, "HttpServer", "Server listening on port %d", serverPort); BELL_LOG(info, "HttpServer", "Server listening on port %d", serverPort);
this->serverPort = serverPort; this->serverPort = serverPort;
@@ -197,6 +200,11 @@ BellHTTPServer::BellHTTPServer(int serverPort) {
server = std::make_unique<CivetServer>(civetWebOptions); server = std::make_unique<CivetServer>(civetWebOptions);
} }
BellHTTPServer::~BellHTTPServer() {
std::lock_guard lock(initMutex);
mg_exit_library();
}
std::unique_ptr<BellHTTPServer::HTTPResponse> BellHTTPServer::makeJsonResponse( std::unique_ptr<BellHTTPServer::HTTPResponse> BellHTTPServer::makeJsonResponse(
const std::string& json, int status) { const std::string& json, int status) {
auto response = std::make_unique<BellHTTPServer::HTTPResponse>(); auto response = std::make_unique<BellHTTPServer::HTTPResponse>();

View File

@@ -19,6 +19,7 @@ namespace bell {
class BellHTTPServer : public CivetHandler { class BellHTTPServer : public CivetHandler {
public: public:
BellHTTPServer(int serverPort); BellHTTPServer(int serverPort);
~BellHTTPServer();
enum class WSState { CONNECTED, READY, CLOSED }; enum class WSState { CONNECTED, READY, CLOSED };
@@ -100,6 +101,8 @@ class BellHTTPServer : public CivetHandler {
std::mutex responseMutex; std::mutex responseMutex;
HTTPHandler notFoundHandler; HTTPHandler notFoundHandler;
static std::mutex initMutex;
bool handleGet(CivetServer* server, struct mg_connection* conn); bool handleGet(CivetServer* server, struct mg_connection* conn);
bool handlePost(CivetServer* server, struct mg_connection* conn); bool handlePost(CivetServer* server, struct mg_connection* conn);
}; };

View File

@@ -6,6 +6,7 @@
#include <cstring> #include <cstring>
#include <vector> #include <vector>
#include <mutex> #include <mutex>
#include <atomic>
#if __has_include("avahi-client/client.h") #if __has_include("avahi-client/client.h")
#include <avahi-client/client.h> #include <avahi-client/client.h>
@@ -41,8 +42,9 @@ class implMDNSService : public MDNSService {
#endif #endif
static struct mdnsd* mdnsServer; static struct mdnsd* mdnsServer;
static in_addr_t host; static in_addr_t host;
static std::atomic<size_t> instances;
implMDNSService(struct mdns_service* service) : service(service){}; implMDNSService(struct mdns_service* service) : service(service){ instances++; };
#ifndef BELL_DISABLE_AVAHI #ifndef BELL_DISABLE_AVAHI
implMDNSService(AvahiEntryGroup* avahiGroup) : avahiGroup(avahiGroup){}; implMDNSService(AvahiEntryGroup* avahiGroup) : avahiGroup(avahiGroup){};
#endif #endif
@@ -51,6 +53,7 @@ class implMDNSService : public MDNSService {
struct mdnsd* implMDNSService::mdnsServer = NULL; struct mdnsd* implMDNSService::mdnsServer = NULL;
in_addr_t implMDNSService::host = INADDR_ANY; in_addr_t implMDNSService::host = INADDR_ANY;
std::atomic<size_t> implMDNSService::instances = 0;
static std::mutex registerMutex; static std::mutex registerMutex;
#ifndef BELL_DISABLE_AVAHI #ifndef BELL_DISABLE_AVAHI
AvahiClient* implMDNSService::avahiClient = NULL; AvahiClient* implMDNSService::avahiClient = NULL;
@@ -66,11 +69,21 @@ void implMDNSService::unregisterService() {
#ifndef BELL_DISABLE_AVAHI #ifndef BELL_DISABLE_AVAHI
if (avahiGroup) { if (avahiGroup) {
avahi_entry_group_free(avahiGroup); avahi_entry_group_free(avahiGroup);
if (!--instances && implMDNSService::avahiClient) {
avahi_client_free(implMDNSService::avahiClient);
avahi_simple_poll_free(implMDNSService::avahiPoll);
implMDNSService::avahiClient = nullptr;
implMDNSService::avahiPoll = nullptr;
}
} else } else
#endif #endif
{ {
mdns_service_remove(implMDNSService::mdnsServer, service); mdns_service_remove(implMDNSService::mdnsServer, service);
} if (!--instances && implMDNSService::mdnsServer) {
mdnsd_stop(implMDNSService::mdnsServer);
implMDNSService::mdnsServer = nullptr;
}
}
} }
std::unique_ptr<MDNSService> MDNSService::registerService( std::unique_ptr<MDNSService> MDNSService::registerService(
@@ -180,19 +193,14 @@ std::unique_ptr<MDNSService> MDNSService::registerService(
std::string type(serviceType + "." + serviceProto + ".local"); std::string type(serviceType + "." + serviceProto + ".local");
BELL_LOG(info, "MDNS", "using built-in mDNS for %s", serviceName.c_str()); BELL_LOG(info, "MDNS", "using built-in mDNS for %s", serviceName.c_str());
struct mdns_service* mdnsService = auto service =
mdnsd_register_svc(implMDNSService::mdnsServer, serviceName.c_str(), mdnsd_register_svc(implMDNSService::mdnsServer, serviceName.c_str(),
type.c_str(), servicePort, NULL, txt.data());
if (mdnsService) {
auto service =
mdnsd_register_svc(implMDNSService::mdnsServer, serviceName.c_str(),
type.c_str(), servicePort, NULL, txt.data()); type.c_str(), servicePort, NULL, txt.data());
return std::make_unique<implMDNSService>(service); if (service) return std::make_unique<implMDNSService>(service);
}
} }
BELL_LOG(error, "MDNS", "cannot start any mDNS listener for %s", BELL_LOG(error, "MDNS", "cannot start any mDNS listener for %s",
serviceName.c_str()); serviceName.c_str());
return NULL; return nullptr;
} }

View File

@@ -19,13 +19,12 @@ using namespace bell;
class implMDNSService : public MDNSService { class implMDNSService : public MDNSService {
private: private:
struct mdns_service* service; struct mdns_service* service;
void unregisterService(void) { void unregisterService(void);
mdns_service_remove(implMDNSService::mdnsServer, service);
};
public: public:
static struct mdnsd* mdnsServer; static struct mdnsd* mdnsServer;
implMDNSService(struct mdns_service* service) : service(service){}; static std::atomic<size_t> instances;
implMDNSService(struct mdns_service* service) : service(service) { instances++; };
}; };
/** /**
@@ -33,8 +32,18 @@ class implMDNSService : public MDNSService {
**/ **/
struct mdnsd* implMDNSService::mdnsServer = NULL; struct mdnsd* implMDNSService::mdnsServer = NULL;
std::atomic<size_t> implMDNSService::instances = 0;
static std::mutex registerMutex; static std::mutex registerMutex;
void implMDNSService::unregisterService() {
mdns_service_remove(implMDNSService::mdnsServer, service);
if (!--instances && implMDNSService::mdnsServer) {
mdnsd_stop(implMDNSService::mdnsServer);
implMDNSService::mdnsServer = nullptr;
}
}
std::unique_ptr<MDNSService> MDNSService::registerService( std::unique_ptr<MDNSService> MDNSService::registerService(
const std::string& serviceName, const std::string& serviceType, const std::string& serviceName, const std::string& serviceType,
const std::string& serviceProto, const std::string& serviceHost, const std::string& serviceProto, const std::string& serviceHost,
@@ -94,5 +103,5 @@ std::unique_ptr<MDNSService> MDNSService::registerService(
mdnsd_register_svc(implMDNSService::mdnsServer, serviceName.c_str(), mdnsd_register_svc(implMDNSService::mdnsServer, serviceName.c_str(),
type.c_str(), servicePort, NULL, txt.data()); type.c_str(), servicePort, NULL, txt.data());
return std::make_unique<implMDNSService>(service); return service ? std::make_unique<implMDNSService>(service) : nullptr;
} }

View File

@@ -72,7 +72,11 @@ class Task {
(LPTHREAD_START_ROUTINE)taskEntryFunc, this, 0, NULL); (LPTHREAD_START_ROUTINE)taskEntryFunc, this, 0, NULL);
return thread != NULL; return thread != NULL;
#else #else
return (pthread_create(&thread, NULL, taskEntryFunc, this) == 0); if (!pthread_create(&thread, NULL, taskEntryFunc, this)) {
pthread_detach(thread);
return true;
}
return false;
#endif #endif
} }
@@ -108,13 +112,7 @@ class Task {
#endif #endif
static void* taskEntryFunc(void* This) { static void* taskEntryFunc(void* This) {
Task* self = (Task*)This; ((Task*)This)->runTask();
self->runTask();
#if _WIN32
WaitForSingleObject(self->thread, INFINITE);
#else
pthread_join(self->thread, NULL);
#endif
return NULL; return NULL;
} }
}; };

View File

@@ -24,7 +24,7 @@ class CDNAudioFile;
// Used in got track info event // Used in got track info event
struct TrackInfo { struct TrackInfo {
std::string name, album, artist, imageUrl, trackId; std::string name, album, artist, imageUrl, trackId;
uint32_t duration; uint32_t duration, number, discNumber;
void loadPbTrack(Track* pbTrack, const std::vector<uint8_t>& gid); void loadPbTrack(Track* pbTrack, const std::vector<uint8_t>& gid);
void loadPbEpisode(Episode* pbEpisode, const std::vector<uint8_t>& gid); void loadPbEpisode(Episode* pbEpisode, const std::vector<uint8_t>& gid);

View File

@@ -32,6 +32,8 @@ message Artist {
message Track { message Track {
optional bytes gid = 1; optional bytes gid = 1;
optional string name = 2; optional string name = 2;
optional sint32 number = 5;
optional sint32 disc_number = 6;
optional sint32 duration = 0x7; optional sint32 duration = 0x7;
optional Album album = 0x3; optional Album album = 0x3;
repeated Artist artist = 0x4; repeated Artist artist = 0x4;
@@ -44,6 +46,7 @@ message Episode {
optional bytes gid = 1; optional bytes gid = 1;
optional string name = 2; optional string name = 2;
optional sint32 duration = 7; optional sint32 duration = 7;
optional sint32 number = 65;
repeated AudioFile file = 12; repeated AudioFile file = 12;
repeated Restriction restriction = 0x4B; repeated Restriction restriction = 0x4B;
optional ImageGroup covers = 0x44; optional ImageGroup covers = 0x44;

View File

@@ -97,6 +97,8 @@ void TrackInfo::loadPbTrack(Track* pbTrack, const std::vector<uint8_t>& gid) {
} }
} }
number = pbTrack->has_number ? pbTrack->number : 0;
discNumber = pbTrack->has_disc_number ? pbTrack->disc_number : 0;
duration = pbTrack->duration; duration = pbTrack->duration;
} }
@@ -113,6 +115,8 @@ void TrackInfo::loadPbEpisode(Episode* pbEpisode,
imageUrl = "https://i.scdn.co/image/" + bytesToHexString(imageId); imageUrl = "https://i.scdn.co/image/" + bytesToHexString(imageId);
} }
number = pbEpisode->has_number ? pbEpisode->number : 0;
discNumber = 0;
duration = pbEpisode->duration; duration = pbEpisode->duration;
} }

View File

@@ -60,7 +60,7 @@ frames_t _output_frames(frames_t avail) {
silence = false; silence = false;
// start when threshold met // start when threshold met
if (output.state == OUTPUT_BUFFER && (frames * BYTES_PER_FRAME) > output.threshold * output.next_sample_rate / 10 && frames > output.start_frames) { if (output.state == OUTPUT_BUFFER && frames > output.threshold * output.next_sample_rate / 10 && frames > output.start_frames) {
output.state = OUTPUT_RUNNING; output.state = OUTPUT_RUNNING;
LOG_INFO("start buffer frames: %u", frames); LOG_INFO("start buffer frames: %u", frames);
wake_controller(); wake_controller();