extern "C" void yeney_stream_track_changed() __attribute__((weak)); extern "C" void yeney_stream_new_track() __attribute__((weak)); // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 #include "core_sonos.h" #include "timing_probe_runtime.h" #include "timing_tap.h" #include "audio_mode.h" #include "pcm_pack.h" #include "start_lead.h" #include "sonos-position.h" #include #include #include #include #include #include "lms_icon.h" extern "C" { void yeney_transport(char); unsigned get_squeezebox_stream_id(); void new_squeezebox_stream_id(); int encode_squeezebox_audio_cancellable(const char*, int, uint64_t, int (*)(void*), void*); } static std::atomic coreRunning{false}, coreStop{false}; extern "C" int sonos_output_running() { return coreRunning.load(); } extern "C" uint64_t get_sb_time_ms() { return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); } static void stopCore(int sig) { coreStop.store(true); std::signal(sig, SIG_DFL); } CoreSonosSink::CoreSonosSink(const uint8_t* mac, uint64_t (*clock)()) : clockMs(clock ? clock : get_sb_time_ms) { if (mac && timing_probe::publishEnabled()) { try { tap = std::make_unique(mac, maxSampleRate()); } catch (const std::exception& e) { printf("yeney: timing tap unavailable reason=%s\n", e.what()); } } coreRunning.store(true); feeder = std::thread(&CoreSonosSink::feed, this); } CoreSonosSink::~CoreSonosSink() { { // Publish the wait predicate under its mutex so the feeder cannot // miss the shutdown notification between testing it and sleeping. std::lock_guard lock(mutex); running.store(false); ++generation; } coreRunning.store(false); changed.notify_all(); feeder.join(); } uint32_t CoreSonosSink::maxSampleRate() const { return sonos_audio_legacy() ? 44100 : 48000; } void CoreSonosSink::command(const yeney::Command& c) { bool continuation = c.letter == 's' && !sonos_audio_legacy() && c.playing; if (c.letter == 's') starts.push_back(continuation); if (!continuation && (c.letter != 'p' || c.value == 0)) yeney_transport(c.letter); } void CoreSonosSink::trackBoundary(uint64_t frame, const yeney::Format& f, bool) { std::lock_guard lock(mutex); next = frame; rate = sonos_audio_legacy() ? 44100 : f.rate; pending = true; continuous = !starts.empty() && starts.front(); if (!starts.empty()) starts.pop_front(); if (tap) { if (!continuous || (streamRate && streamRate != rate)) timing_probe::diagnostics().reset("tap-boundary", false, true); yeney::Format exported = f; exported.rate = rate; tap->boundary(frame, exported, continuous); } } size_t CoreSonosSink::write(const yeney::Frame* frames, size_t count) { return write(frames, count, false); } size_t CoreSonosSink::write(const yeney::Frame* frames, size_t count, bool processed) { std::lock_guard lock(mutex); if (paused || !running.load()) return 0; count = std::min(count, capacity - staged); if (!count) return 0; const unsigned bytes = sonos_audio_legacy() ? 2 : 3; Batch b{{}, next, generation.load(), count, pending, continuous, rate}; b.pcm.reserve(count * bytes * 2); b.pcm.resize(count * bytes * 2); for (size_t i = 0; i < count; ++i) { packSonosSample(b.pcm.data() + i * bytes * 2, frames[i].left, bytes, processed, dither); packSonosSample(b.pcm.data() + (i * 2 + 1) * bytes, frames[i].right, bytes, processed, dither); } if (tap) { b.exported = tap->write(frames, count, b.shmGeneration, b.shmFirst); if (b.exported) timing_probe::diagnostics().exported(b.shmGeneration, b.shmFirst + count); else timing_probe::diagnostics().mapFrames(0, rate, b.shmGeneration, 0, 0, 0, false); } queue.push_back(std::move(b)); staged += count; next += count; pending = false; changed.notify_one(); return count; } void CoreSonosSink::pause() { if (timing_probe::enabled()) timing_probe::diagnostics().reset("pause",false,true); std::lock_guard lock(mutex); if (!paused && awaitingStream) { heldPosition = std::min(streamBase, heldPosition + (clockMs() - heldAt) * heldRate / 1000); audible = std::max(audible, heldPosition); } paused = true; if (tap) tap->pause(); } void CoreSonosSink::resume() { std::lock_guard lock(mutex); if (paused && awaitingStream) heldAt = clockMs(); paused = false; if (tap) tap->resume(); changed.notify_one(); } void CoreSonosSink::stop() { pause(); flush(); } void CoreSonosSink::flush() { if (timing_probe::enabled()) timing_probe::diagnostics().reset("flush",false,true); const auto position = audibleFrames(); std::lock_guard lock(mutex); ++generation; if (tap) tap->flush(); queue.clear(); starts.clear(); staged = 0; pending = false; handed = next = streamBase = started = position; streamRate = 0; rateChangedStream = awaitingStream = false; changed.notify_one(); } uint64_t CoreSonosSink::audibleFrames() const { std::lock_guard lock(mutex); if (!paused && awaitingStream) { if (!sonos_position_audio_started()) { // The new HTTP coordinate does not belong to the old audible track. // Continue its clock at its own rate, bounded by its final PCM frame. audible = std::max(audible, std::min(streamBase, heldPosition + (clockMs() - heldAt) * heldRate / 1000)); return audible; } awaitingStream = false; } if (!paused && streamRate) audible = std::max(audible, std::min(handed, streamBase + get_sonos_audible_frames(streamRate))); return audible; } uint64_t CoreSonosSink::startedFrames() const { const auto position = audibleFrames(); std::lock_guard lock(mutex); // A rate change starts a new speaker stream, even for a queued successor. if (rateChangedStream) return sonos_position_audio_started() ? position : std::min(position, streamBase); if (!yeney_start_lead_ms()) return started; return std::min(started, std::max(position, started > streamBase ? streamBase + 1 : position)); } bool CoreSonosSink::drained(uint64_t) const { if (yeney_start_lead_ms()) audibleFrames(); std::lock_guard lock(mutex); return queue.empty() && !inflight && (!yeney_start_lead_ms() || audible >= handed); } bool CoreSonosSink::outputEmpty(uint64_t submitted) const { // The feeder may be empty while encoded HTTP/Sonos audio still plays. return audibleFrames() >= submitted && drained(submitted); } void CoreSonosSink::feed() { while (running.load()) { Batch b; uint64_t first; { std::unique_lock lock(mutex); changed.wait(lock, [&] { return !running.load() || (!paused && !queue.empty()); }); if (!running.load()) break; b = std::move(queue.front()); queue.pop_front(); staged -= b.frames; inflight = true; if (b.boundary && (!b.continuous || streamRate != b.rate || sonos_audio_legacy())) { const bool rateChange = streamRate && streamRate != b.rate; if (rateChange) { // Snapshot before reset changes the shared connection coordinate. audible = std::max(audible, std::min(handed, streamBase + get_sonos_audible_frames(streamRate))); heldPosition = audible; heldAt = clockMs(); heldRate = streamRate; awaitingStream = true; } rateChangedStream = rateChange; set_squeezebox_audio_rate(get_squeezebox_stream_id() + 1, b.rate); new_squeezebox_stream_id(); if (rateChange) sonos_position_allow_zero_start(); streamRate = b.rate; streamBase = b.first; printf("stream %u: FLAC %u-bit %u Hz\n", get_squeezebox_stream_id(), sonos_audio_legacy() ? 16u : 24u, b.rate); } first = b.first - streamBase; started = std::max(started, b.first + b.frames); } if (tap && generation.load() == b.generation) timing_probe::diagnostics().mapFrames(get_squeezebox_stream_id(), b.rate, b.shmGeneration, first, b.shmFirst, b.frames, b.exported); struct Cancellation { CoreSonosSink* sink; uint64_t generation; } cancel{this, b.generation}; int ok = encode_squeezebox_audio_cancellable(b.pcm.data(), b.pcm.size(), first, [](void* ptr) { auto& c = *static_cast(ptr); return int(!c.sink->running.load() || c.sink->generation.load() != c.generation); }, &cancel); { std::lock_guard lock(mutex); if (ok && generation.load() == b.generation) handed = std::max(handed, b.first + b.frames); inflight = false; } } } void runCoreClient(const char* server, const uint8_t* mac, const char* name) { for (int sig : {SIGINT, SIGTERM, SIGQUIT, SIGHUP}) std::signal(sig, stopCore); try { CoreSonosSink sink(mac); bridge::LmsIcon icon(mac, [](const auto &line) { printf("%s\n", line.c_str()); }); yeney::Config config; config.server = server ? server : ""; auto colon = config.server.find(':'); if (colon != std::string::npos) { auto port = std::stoul(config.server.substr(colon + 1)); if (!port || port > 65535 || port == 9000) throw std::runtime_error("invalid Slimproto port"); config.port = port; config.server.resize(colon); } config.name = name; config.connectionReady = [&icon](const auto &host) { icon.connected(host); }; std::copy(mac, mac + 6, config.mac.begin()); config.log = [](const std::string& line) { printf("core: %s\n", line.c_str()); if ((line.rfind("STMs ", 0) == 0 || line.rfind("STMt ", 0) == 0) && yeney_stream_track_changed) yeney_stream_track_changed(); }; config.observeCommand = [&](const yeney::Command& c) { if (c.letter == 's' && yeney_stream_new_track) yeney_stream_new_track(); sink.command(c); }; config.startOnSubmit = true; yeney::Player player(config, sink); player.run(coreStop); } catch (const std::exception& error) { printf("core: fatal: %s\n", error.what()); std::exit(1); } std::exit(0); // Run the registered HTTP/event cleanup. }