17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-28 18:07:29 +01:00
Files
maszyna/audio/radiostream.cpp
IwanXXI b4b1e6c32f Broadcast radio receiver in the cab
Plays internet radio streams (http, mp3) through a positional emitter in the cab, with analog tuning, inter-station noise and two speaker profiles. Stations are listed in fmradio.txt, cab controls are defined with fmradio entries in the .mmd file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 00:14:32 +02:00

911 lines
26 KiB
C++

/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "audio/radiostream.h"
#ifdef DBG_NEW
#undef new
#endif
#define ASIO_DISABLE_VISIBILITY
#include <asio.hpp>
#define DR_MP3_IMPLEMENTATION
#define DR_MP3_NO_STDIO
#include "ref/dr_libs/dr_mp3.h"
namespace audio
{
namespace
{
auto const EU07_RADIOSTREAM_BUFFERLENGTH{8}; // seconds of decoded audio kept for the owner
auto const EU07_RADIOSTREAM_CONNECTTIMEOUT{std::chrono::seconds{10}};
auto const EU07_RADIOSTREAM_STALLTIMEOUT{std::chrono::seconds{10}}; // max time without any incoming data
auto const EU07_RADIOSTREAM_MAXREDIRECTS{5};
std::size_t const EU07_RADIOSTREAM_MAXHEADERSIZE{32 * 1024};
std::size_t const EU07_RADIOSTREAM_MAXPLAYLISTSIZE{64 * 1024};
// the decoder needs to see the beginning of the next frame to stay in sync, so it's only fed when there's more data than a frame can take.
// we start with the size safe for the largest frames, then scale it down to the frames of the stream,
// otherwise audio of low bitrate streams would be delivered in pieces up to a second long
std::size_t const EU07_RADIOSTREAM_DECODETHRESHOLD{4096};
std::size_t const EU07_RADIOSTREAM_MINDECODETHRESHOLD{512};
// amount of data without a single valid frame after which the stream is deemed not to be mp3
std::size_t const EU07_RADIOSTREAM_MAXJUNKSIZE{512 * 1024};
std::string lowercase(std::string Text)
{
std::transform(std::begin(Text), std::end(Text), std::begin(Text), [](unsigned char const c) { return static_cast<char>(std::tolower(c)); });
return Text;
}
std::string trim(std::string const &Text)
{
auto const first{Text.find_first_not_of(" \t\r\n")};
if (first == std::string::npos)
{
return {};
}
auto const last{Text.find_last_not_of(" \t\r\n")};
return Text.substr(first, last - first + 1);
}
bool starts_with_nocase(std::string const &Text, std::string const &Prefix)
{
return Text.size() >= Prefix.size() && lowercase(Text.substr(0, Prefix.size())) == Prefix;
}
struct stream_address
{
std::string host;
std::string port{"80"};
std::string target{"/"};
// host with optional port, as expected by http host header
std::string authority() const
{
auto const name{host.find(':') != std::string::npos ? "[" + host + "]" : host};
return port == "80" ? name : name + ":" + port;
}
};
// splits provided address into components. returns: empty string on success, or description of the problem
std::string parse_address(std::string const &Url, stream_address &Address)
{
auto url{trim(Url)};
if (starts_with_nocase(url, "https://"))
{
return "encrypted (https) streams aren't supported";
}
if (starts_with_nocase(url, "http://"))
{
url.erase(0, 7);
}
else if (url.find("://") != std::string::npos)
{
return "unsupported protocol";
}
Address = stream_address{};
auto const hostend{url.find_first_of("/?")};
auto authority{url.substr(0, hostend)};
if (hostend != std::string::npos)
{
Address.target = url.substr(hostend);
if (Address.target.front() == '?')
{
Address.target.insert(0, "/");
}
}
// credentials aren't supported, but shouldn't prevent the connection
auto const credentialsend{authority.rfind('@')};
if (credentialsend != std::string::npos)
{
authority.erase(0, credentialsend + 1);
}
if (false == authority.empty() && authority.front() == '[')
{
// ipv6 literal
auto const literalend{authority.find(']')};
if (literalend == std::string::npos)
{
return "malformed address";
}
Address.host = authority.substr(1, literalend - 1);
if (literalend + 2 < authority.size() && authority[literalend + 1] == ':')
{
Address.port = authority.substr(literalend + 2);
}
}
else
{
auto const portstart{authority.rfind(':')};
Address.host = authority.substr(0, portstart);
if (portstart != std::string::npos && portstart + 1 < authority.size())
{
Address.port = authority.substr(portstart + 1);
}
}
if (Address.host.empty())
{
return "malformed address";
}
return {};
}
// combines address received in a redirect with the address of the redirecting resource
std::string resolve_redirect(std::string const &Location, stream_address const &Origin)
{
if (Location.find("://") != std::string::npos)
{
return Location;
}
if (Location.rfind("//", 0) == 0)
{
return "http:" + Location;
}
auto const root{"http://" + Origin.authority()};
if (false == Location.empty() && Location.front() == '/')
{
return root + Location;
}
auto directory{Origin.target.substr(0, Origin.target.find('?'))};
directory.erase(directory.rfind('/') + 1);
return root + directory + Location;
}
// extracts address of the first stream listed in provided m3u or pls playlist. returns: the address, or empty string
std::string parse_playlist(std::string const &Playlist)
{
std::istringstream lines{Playlist};
std::string line;
while (std::getline(lines, line))
{
line = trim(line);
if (line.empty() || line.front() == '#' || line.front() == '[')
{
continue;
}
// pls entries have form of FileN=address, m3u ones are plain addresses
if (starts_with_nocase(line, "file"))
{
auto const separator{line.find('=')};
if (separator != std::string::npos)
{
line = trim(line.substr(separator + 1));
}
}
if (starts_with_nocase(line, "http://") || starts_with_nocase(line, "https://"))
{
return line;
}
}
return {};
}
struct http_response
{
int status{0};
std::map<std::string, std::string> headers; // names are stored in lower case
std::string body; // part of the content which arrived together with the header
std::string header(std::string const &Name) const
{
auto const lookup{headers.find(Name)};
return lookup != std::end(headers) ? lookup->second : std::string{};
}
};
// parses provided response header. returns: true on success
bool parse_response(std::string const &Header, http_response &Response)
{
std::istringstream lines{Header};
std::string line;
if (false == static_cast<bool>(std::getline(lines, line)))
{
return false;
}
// status line, either regular 'HTTP/1.x 200 OK' or 'ICY 200 OK' sent by shoutcast servers
std::istringstream statusline{line};
std::string protocol;
statusline >> protocol >> Response.status;
if (statusline.fail() || (false == starts_with_nocase(protocol, "http/") && lowercase(protocol) != "icy"))
{
return false;
}
while (std::getline(lines, line))
{
auto const separator{line.find(':')};
if (separator == std::string::npos)
{
continue;
}
Response.headers[lowercase(trim(line.substr(0, separator)))] = trim(line.substr(separator + 1));
}
return true;
}
// removes http chunked transfer framing from received data, if there's any
class content_decoder
{
public:
explicit content_decoder(bool const Chunked) : m_chunked{Chunked} {}
// appends payload of provided data to the output. returns: false if the data is malformed
bool feed(std::uint8_t const *Data, std::size_t const Size, std::vector<std::uint8_t> &Output)
{
if (false == m_chunked)
{
Output.insert(std::end(Output), Data, Data + Size);
return true;
}
std::size_t index{0};
while (index < Size)
{
switch (m_stage)
{
case stage::size:
{
auto const character{static_cast<char>(Data[index++])};
if (character == '\n')
{
// chunk size is a hex number, optionally followed by extensions we don't care about
char *numberend{nullptr};
m_remaining = std::strtoul(m_line.c_str(), &numberend, 16);
if (numberend == m_line.c_str())
{
return false;
}
m_line.clear();
m_stage = (m_remaining > 0 ? stage::payload : stage::done);
}
else if (character != '\r')
{
m_line += character;
if (m_line.size() > 256)
{
return false;
}
}
break;
}
case stage::payload:
{
auto const count{std::min<std::size_t>(m_remaining, Size - index)};
Output.insert(std::end(Output), Data + index, Data + index + count);
index += count;
m_remaining -= count;
if (m_remaining == 0)
{
m_stage = stage::separator;
}
break;
}
case stage::separator:
{
// payload of each chunk is followed by a line break
if (Data[index++] == '\n')
{
m_stage = stage::size;
}
break;
}
case stage::done:
{
return true;
}
}
}
return true;
}
bool done() const
{
return m_stage == stage::done;
}
private:
enum class stage
{
size,
payload,
separator,
done
};
bool m_chunked;
stage m_stage{stage::size};
std::string m_line;
unsigned long m_remaining{0};
};
} // namespace
// single connection attempt of the radio stream worker
class radio_session
{
public:
enum class result
{
stopped, // the owner lost interest in the stream
failed, // connection didn't work out or was lost, it's worth to try again
unsupported // there's nothing we can do with this stream
};
radio_session(radio_stream &Owner, std::stop_token Stoptoken, std::uint32_t const Generation) : m_owner{Owner}, m_stoptoken{std::move(Stoptoken)}, m_generation{Generation}, m_resolver{m_io}, m_socket{m_io} {}
~radio_session()
{
asio::error_code ignored;
m_socket.close(ignored);
}
// connects with specified stream and feeds the owner with decoded audio until the connection ends
result play(std::string Url);
// returns true if the session delivered any audio
bool delivered() const
{
return m_delivered;
}
private:
// runs pending network operation until specified flag is set by its completion handler. returns: false if the operation was aborted
bool wait(bool const &Done, std::chrono::seconds const Timeout);
bool connect(stream_address const &Address);
bool send(std::string const &Data);
// reads some data from the socket. returns: number of received bytes, or 0 if the connection is done for
std::size_t receive(std::uint8_t *Data, std::size_t const Size);
bool receive_response(http_response &Response);
// retrieves the playlist and replaces provided address with the one found in it. returns: false if it wasn't possible
bool receive_playlist(http_response const &Response, std::string &Url, result &Failure);
result receive_audio(http_response const &Response);
result fail(std::string const &Reason, result const Result = result::failed);
radio_stream &m_owner;
std::stop_token m_stoptoken;
std::uint32_t m_generation;
asio::io_context m_io;
asio::ip::tcp::resolver m_resolver;
asio::ip::tcp::socket m_socket;
std::string m_error; // description of the most recent network problem
bool m_delivered{false};
};
bool radio_session::wait(bool const &Done, std::chrono::seconds const Timeout)
{
auto const deadline{std::chrono::steady_clock::now() + Timeout};
while (false == Done)
{
auto const timedout{std::chrono::steady_clock::now() >= deadline};
if (timedout || m_stoptoken.stop_requested())
{
m_error = (timedout ? "operation timed out" : "");
// abort the operation, then let its completion handler run as it refers to data of our caller
asio::error_code ignored;
m_resolver.cancel();
m_socket.close(ignored);
m_io.restart();
m_io.run();
return false;
}
if (m_io.stopped())
{
m_io.restart();
}
m_io.run_one_for(std::chrono::milliseconds{100});
}
return true;
}
bool radio_session::connect(stream_address const &Address)
{
asio::error_code error;
asio::ip::tcp::resolver::results_type endpoints;
bool done{false};
m_resolver.async_resolve(Address.host, Address.port, [&](asio::error_code const &Error, asio::ip::tcp::resolver::results_type Results) {
error = Error;
endpoints = std::move(Results);
done = true;
});
if (false == wait(done, EU07_RADIOSTREAM_CONNECTTIMEOUT))
{
return false;
}
if (error)
{
m_error = "can't find the server (" + error.message() + ")";
return false;
}
done = false;
asio::async_connect(m_socket, endpoints, [&](asio::error_code const &Error, asio::ip::tcp::endpoint const &) {
error = Error;
done = true;
});
if (false == wait(done, EU07_RADIOSTREAM_CONNECTTIMEOUT))
{
return false;
}
if (error)
{
m_error = "can't connect with the server (" + error.message() + ")";
return false;
}
return true;
}
bool radio_session::send(std::string const &Data)
{
asio::error_code error;
bool done{false};
asio::async_write(m_socket, asio::buffer(Data), [&](asio::error_code const &Error, std::size_t) {
error = Error;
done = true;
});
if (false == wait(done, EU07_RADIOSTREAM_CONNECTTIMEOUT))
{
return false;
}
if (error)
{
m_error = error.message();
return false;
}
return true;
}
std::size_t radio_session::receive(std::uint8_t *Data, std::size_t const Size)
{
asio::error_code error;
std::size_t received{0};
bool done{false};
m_socket.async_read_some(asio::buffer(Data, Size), [&](asio::error_code const &Error, std::size_t const Count) {
error = Error;
received = Count;
done = true;
});
if (false == wait(done, EU07_RADIOSTREAM_STALLTIMEOUT))
{
return 0;
}
if (error && received == 0)
{
m_error = (error == asio::error::eof ? "connection closed by the server" : error.message());
}
return received;
}
bool radio_session::receive_response(http_response &Response)
{
std::string data;
std::array<std::uint8_t, 2048> chunk;
while (true)
{
// some servers end lines with a bare line feed, so we look for both variants
auto headerend{data.find("\r\n\r\n")};
auto headerendsize{std::size_t{4}};
if (headerend == std::string::npos)
{
headerend = data.find("\n\n");
headerendsize = 2;
}
if (headerend != std::string::npos)
{
Response = http_response{};
if (false == parse_response(data.substr(0, headerend), Response))
{
m_error = "unrecognized reply from the server";
return false;
}
Response.body = data.substr(headerend + headerendsize);
return true;
}
if (data.size() > EU07_RADIOSTREAM_MAXHEADERSIZE)
{
m_error = "unrecognized reply from the server";
return false;
}
auto const received{receive(chunk.data(), chunk.size())};
if (received == 0)
{
return false;
}
data.append(reinterpret_cast<char const *>(chunk.data()), received);
}
}
radio_session::result radio_session::fail(std::string const &Reason, result const Result)
{
if (m_stoptoken.stop_requested())
{
return result::stopped;
}
if (false == Reason.empty())
{
m_owner.post("radio stream: " + Reason, true);
}
return Result;
}
radio_session::result radio_session::play(std::string Url)
{
for (auto hop{0}; hop <= EU07_RADIOSTREAM_MAXREDIRECTS; ++hop)
{
stream_address address;
auto const addresserror{parse_address(Url, address)};
if (false == addresserror.empty())
{
return fail(addresserror + " \"" + Url + "\"", result::unsupported);
}
m_owner.status(radio_stream::state::connecting, m_generation);
m_owner.post("radio stream: connecting with \"" + Url + "\"");
{
// each hop needs a fresh connection
asio::error_code ignored;
m_socket.close(ignored);
}
if (false == connect(address))
{
return fail(m_error);
}
// NOTE: we don't ask for the in-band metadata, so the content is pure audio
auto const request{"GET " + address.target + " HTTP/1.1\r\nHost: " + address.authority() + "\r\nUser-Agent: MaSzyna\r\nAccept: */*\r\nConnection: close\r\n\r\n"};
http_response response;
if (false == send(request) || false == receive_response(response))
{
return fail(m_error);
}
if (response.status >= 300 && response.status < 400)
{
auto const location{response.header("location")};
if (location.empty())
{
return fail("server sent a redirect without the target");
}
Url = resolve_redirect(location, address);
continue;
}
if (response.status != 200)
{
return fail("server replied with status " + std::to_string(response.status));
}
auto const contenttype{lowercase(response.header("content-type"))};
auto const path{lowercase(address.target.substr(0, address.target.find('?')))};
auto const isplaylist{contenttype.find("mpegurl") != std::string::npos || contenttype.find("scpls") != std::string::npos || path.ends_with(".m3u") || path.ends_with(".pls")};
if (isplaylist)
{
auto failure{result::failed};
if (false == receive_playlist(response, Url, failure))
{
return failure;
}
continue;
}
for (auto const *format : {"aac", "ogg", "opus", "flac"})
{
if (contenttype.find(format) != std::string::npos)
{
return fail("unsupported audio format \"" + contenttype + "\", only mp3 streams can be played", result::unsupported);
}
}
auto const name{response.header("icy-name")};
auto const bitrate{response.header("icy-br")};
m_owner.post("radio stream: connected" + (name.empty() ? "" : ", station \"" + name + "\"") + (bitrate.empty() ? "" : ", " + bitrate + " kbps"));
return receive_audio(response);
}
return fail("too many redirects");
}
bool radio_session::receive_playlist(http_response const &Response, std::string &Url, result &Failure)
{
content_decoder content{lowercase(Response.header("transfer-encoding")).find("chunked") != std::string::npos};
std::vector<std::uint8_t> playlist;
if (false == content.feed(reinterpret_cast<std::uint8_t const *>(Response.body.data()), Response.body.size(), playlist))
{
Failure = fail("malformed reply from the server");
return false;
}
std::array<std::uint8_t, 2048> chunk;
while (false == content.done() && playlist.size() < EU07_RADIOSTREAM_MAXPLAYLISTSIZE)
{
// the playlist is complete when the server closes the connection
auto const received{receive(chunk.data(), chunk.size())};
if (received == 0)
{
break;
}
if (false == content.feed(chunk.data(), received, playlist))
{
Failure = fail("malformed reply from the server");
return false;
}
}
if (m_stoptoken.stop_requested())
{
Failure = result::stopped;
return false;
}
auto const address{parse_playlist(std::string{std::begin(playlist), std::end(playlist)})};
if (address.empty())
{
Failure = fail("playlist \"" + Url + "\" doesn't contain any stream address", result::unsupported);
return false;
}
Url = address;
return true;
}
radio_session::result radio_session::receive_audio(http_response const &Response)
{
content_decoder content{lowercase(Response.header("transfer-encoding")).find("chunked") != std::string::npos};
std::vector<std::uint8_t> input;
input.reserve(4 * EU07_RADIOSTREAM_DECODETHRESHOLD);
if (false == content.feed(reinterpret_cast<std::uint8_t const *>(Response.body.data()), Response.body.size(), input))
{
return fail("malformed reply from the server");
}
auto decoder{std::make_unique<drmp3dec>()};
::drmp3dec_init(decoder.get());
std::vector<std::int16_t> frame(DRMP3_MAX_SAMPLES_PER_FRAME);
std::array<std::uint8_t, 4096> chunk;
std::size_t junksize{0};
auto threshold{EU07_RADIOSTREAM_DECODETHRESHOLD};
m_owner.status(radio_stream::state::receiving, m_generation);
while (true)
{
// decode what we have...
std::size_t position{0};
while (input.size() - position >= threshold)
{
drmp3dec_frame_info info{};
auto const samplecount{::drmp3dec_decode_frame(decoder.get(), input.data() + position, static_cast<int>(input.size() - position), frame.data(), &info)};
if (samplecount > 0)
{
if (info.channels > 1)
{
// mix the channels down, the sound is going to be emitted from a single point in the cab
for (auto index{0}; index < samplecount; ++index)
{
auto sum{0};
for (auto channel{0}; channel < info.channels; ++channel)
{
sum += frame[index * info.channels + channel];
}
frame[index] = static_cast<std::int16_t>(sum / info.channels);
}
}
m_owner.store(frame.data(), static_cast<std::size_t>(samplecount), static_cast<unsigned int>(info.sample_rate), m_generation);
m_delivered = true;
junksize = 0;
threshold = std::clamp(static_cast<std::size_t>(info.frame_bytes) * 3, EU07_RADIOSTREAM_MINDECODETHRESHOLD, EU07_RADIOSTREAM_DECODETHRESHOLD);
}
else if (info.frame_bytes > 0)
{
// data which isn't part of any frame. the decoder is looking for the stream again, which takes more data
junksize += static_cast<std::size_t>(info.frame_bytes);
threshold = EU07_RADIOSTREAM_DECODETHRESHOLD;
}
else
{
// the decoder wants more data
break;
}
position += static_cast<std::size_t>(info.frame_bytes);
}
input.erase(std::begin(input), std::begin(input) + static_cast<std::ptrdiff_t>(position));
if (junksize > EU07_RADIOSTREAM_MAXJUNKSIZE || input.size() > EU07_RADIOSTREAM_MAXJUNKSIZE)
{
return fail(m_delivered ? "lost track of audio data in the stream" : "no mp3 audio found in the stream, only mp3 streams can be played", m_delivered ? result::failed : result::unsupported);
}
// ...then get some more
if (content.done())
{
return fail("stream ended");
}
auto const received{receive(chunk.data(), chunk.size())};
if (received == 0)
{
return fail(m_error);
}
if (false == content.feed(chunk.data(), received, input))
{
return fail("malformed data from the server");
}
}
}
radio_stream::~radio_stream()
{
close();
// NOTE: destructors of retired workers wait for their threads to finish
}
// starts reception of specified stream, dropping current one if there's any
void radio_stream::open(std::string const &Url)
{
close();
if (Url.empty())
{
return;
}
m_url = Url;
std::uint32_t generation;
{
std::scoped_lock lock{m_mutex};
generation = m_generation;
m_state = state::connecting;
}
m_worker.finished = std::make_shared<std::atomic<bool>>(false);
m_worker.thread = std::jthread{[this](std::stop_token Stoptoken, std::string Address, std::uint32_t const Workergeneration, std::shared_ptr<std::atomic<bool>> Finished) { run(std::move(Stoptoken), std::move(Address), Workergeneration, std::move(Finished)); },
Url, generation, m_worker.finished};
}
// terminates reception of the current stream
void radio_stream::close()
{
{
std::scoped_lock lock{m_mutex};
// from now on the current worker can't deliver anything
++m_generation;
m_samplesstart = 0;
m_samplescount = 0;
m_state = state::idle;
m_rate = 0;
}
m_url.clear();
if (m_worker.thread.joinable())
{
// the worker may need a moment to shut down its connection. we don't wait for it here, as we're likely called from the main thread
m_worker.thread.request_stop();
m_retired.emplace_back(std::move(m_worker));
m_worker = worker{};
}
reap();
}
// disposes of retired workers which are done with their cleanup
void radio_stream::reap()
{
std::erase_if(m_retired, [](worker const &Worker) { return Worker.finished->load(); });
}
// returns number of decoded samples waiting to be collected
std::size_t radio_stream::available() const
{
std::scoped_lock lock{m_mutex};
return m_samplescount;
}
// moves up to specified number of decoded samples to provided buffer. returns: number of samples moved
std::size_t radio_stream::read(std::int16_t *Output, std::size_t const Count)
{
std::scoped_lock lock{m_mutex};
auto const count{std::min(Count, m_samplescount)};
for (std::size_t index{0}; index < count; ++index)
{
Output[index] = m_samples[(m_samplesstart + index) % m_samples.size()];
}
if (count > 0)
{
m_samplesstart = (m_samplesstart + count) % m_samples.size();
m_samplescount -= count;
}
return count;
}
// retrieves next diagnostic message posted by the worker. returns: false if there's no message waiting
bool radio_stream::fetch_message(message &Message)
{
std::scoped_lock lock{m_messagesmutex};
if (m_messages.empty())
{
return false;
}
Message = std::move(m_messages.front());
m_messages.pop_front();
return true;
}
void radio_stream::run(std::stop_token Stoptoken, std::string Url, std::uint32_t const Generation, std::shared_ptr<std::atomic<bool>> Finished)
{
auto failures{0};
while (false == Stoptoken.stop_requested())
{
auto result{radio_session::result::failed};
auto delivered{false};
{
radio_session session{*this, Stoptoken, Generation};
result = session.play(Url);
delivered = session.delivered();
}
if (result == radio_session::result::stopped || Stoptoken.stop_requested())
{
break;
}
if (result == radio_session::result::unsupported)
{
status(state::failed, Generation);
break;
}
// try again after a pause, which gets longer if the server keeps refusing to cooperate
failures = (delivered ? 0 : failures + 1);
auto const pause{std::chrono::seconds{std::min(15, 1 << std::min(failures, 4))}};
status(state::waiting, Generation);
post("radio stream: next connection attempt in " + std::to_string(pause.count()) + " s");
auto const deadline{std::chrono::steady_clock::now() + pause};
while (false == Stoptoken.stop_requested() && std::chrono::steady_clock::now() < deadline)
{
std::this_thread::sleep_for(std::chrono::milliseconds{50});
}
}
Finished->store(true);
}
// stores decoded samples, discarding oldest data if there's no room left. data from outdated workers is ignored
void radio_stream::store(std::int16_t const *Samples, std::size_t const Count, unsigned int const Rate, std::uint32_t const Generation)
{
std::scoped_lock lock{m_mutex};
if (Generation != m_generation || Rate == 0)
{
return;
}
if (Rate != m_rate)
{
// (re)initialize the buffer for the new format
m_samples.assign(static_cast<std::size_t>(Rate) * EU07_RADIOSTREAM_BUFFERLENGTH, 0);
m_samplesstart = 0;
m_samplescount = 0;
m_rate = Rate;
}
auto const capacity{m_samples.size()};
for (std::size_t index{0}; index < Count; ++index)
{
m_samples[(m_samplesstart + m_samplescount) % capacity] = Samples[index];
if (m_samplescount < capacity)
{
++m_samplescount;
}
else
{
// the buffer is full, the oldest sample was just overwritten
m_samplesstart = (m_samplesstart + 1) % capacity;
}
}
}
void radio_stream::status(state const State, std::uint32_t const Generation)
{
std::scoped_lock lock{m_mutex};
if (Generation == m_generation)
{
m_state = State;
}
}
void radio_stream::post(std::string Text, bool const Error)
{
std::scoped_lock lock{m_messagesmutex};
m_messages.emplace_back(std::move(Text), Error);
// if nobody collects the messages there's no point in hoarding them
while (m_messages.size() > 64)
{
m_messages.pop_front();
}
}
} // namespace audio