Ctrl K
ComunidadGuías…

core_sonos.cpp

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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 <algorithm>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <stdexcept>

#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<bool> 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::milliseconds>(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<timing_probe::TimingTap>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<uint64_t>(position, started > streamBase ? streamBase + 1 : position));
}

bool CoreSonosSink::drained(uint64_t) const {
    if (yeney_start_lead_ms()) audibleFrames();
    std::lock_guard<std::mutex> 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<std::mutex> 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<Cancellation*>(ptr);
            return int(!c.sink->running.load() || c.sink->generation.load() != c.generation);
        }, &cancel);
        {
            std::lock_guard<std::mutex> 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.
}