// sbencoder.h -- streams decoded PCM into a FLAC-encoded byte stream for Sonos // // Copyright (c) 2026 Jaap van Vliet // // Original implementation for the YeneY project. // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 // Licensed under the PolyForm Noncommercial License 1.0.0. See LICENSE. // // THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. #ifndef FLACENCODER_H #define FLACENCODER_H #include "upnp/encoded_buffer.h" #include "encoded_history.h" #include "stream_pacing.h" #include #include #include #include #include namespace bridge { // Bridges the core feeder's decoded PCM (pushed via write()) to the HTTP // streamer's FLAC output (pulled via read()), with bounded pacing against // monotonic response time and a clean hold while paused. class SBEncoder { friend class WriteBridge; #ifdef SBENCODER_TEST friend struct EncoderTestAccess; #endif public: explicit SBEncoder(unsigned streamId); ~SBEncoder(); bool open(); bool open(uint8_t sampleBits, unsigned sampleRate = 44100); // Blocks (up to timeout ms, 0 = forever) until len bytes of PCM have // been accepted into the encoder, or the stream ends/is cancelled. int write(const char* data, int len, unsigned timeout, const std::function& firstPcm = {}, const std::function& interrupted = {}); // Blocks (up to timeout ms, 0 = forever) until some encoded FLAC bytes // are available, or the stream ends/is cancelled. While holdWhilePaused // is set, a paused LMS source never lets this call time out — the // deadline is pushed forward on every poll until playback resumes. // peerClosed, if given, is polled each iteration to detect a client // that has already disconnected. int read(char* data, int maxlen, unsigned timeout, bool holdWhilePaused = true, const std::function& peerClosed = {}); void close(); // A GET whose producer has been superseded by a newer generation but // whose reader must still be allowed to drain: stop accepting writes, // never touch the reader side. void retireProducer() { m_producerRetired.store(true); } bool producerRetired() const { return m_producerRetired.load(); } // The HTTP response itself has ended (client gone) — distinct from // retireProducer(): this also unblocks read(). void endResponse() { m_responseEnded.store(true); } bool responseEnded() const { return m_responseEnded.load(); } // Hard stop for both sides at once (e.g. an outdated generation). void cancel() { m_cancelled.store(true); } bool cancelled() const { return m_cancelled.load(); } // True once at least one non-empty FLAC frame has actually been written. bool hasAudio() const { return m_producedAudio.load(); } std::atomic frameOrigin{0}; void pacingClock(std::function reader) { m_clock = std::move(reader); } bool hasAcceptedPcm() const { return m_pcmBytesAccepted.load() != 0; } unsigned sampleRate() const { return m_sampleRate; } uint64_t encodedAudioMs() const { return m_encodedFrames.load() * 1000 / m_sampleRate; } EncodedHistory history; void useHistoryReader() { m_historyReader.store(true); m_encodedRing->clear(); if (m_phase == Phase::Closing) close(); } bool historyFinished() const { return m_phase == Phase::Closed; } unsigned streamId() const { return m_streamId; } private: enum class Phase { Init, Encoding, Closing, Closed, }; int encodePcm(const char* data, int len, const std::function &interrupted = {}); std::function m_writeInterrupted; int pendingEncodedBytes() const; int acceptEncodedBytes(const char* data, int len); int drainEncodedBytes(char* data, int maxlen); std::atomic m_encodedFrames{0}; std::atomic m_phase; std::atomic m_historyReader{false}; std::atomic m_cancelled{false}; std::atomic m_responseEnded{false}; std::atomic m_producerRetired{false}; std::atomic m_producedAudio{false}; std::mutex m_writeMutex; std::atomic m_firstReadAtMs; // set on the first successful read() StreamPacer m_pacer; std::function m_clock; std::atomic m_pcmBytesAccepted; // running total handed to the encoder int m_bytesPerFrame; int m_sampleBits; unsigned m_sampleRate = 44100; unsigned m_streamId; FLAC__int32* m_interleaveBuf; upnp::EncodedBuffer* m_encodedRing; upnp::EncodedBuffer::Packet* m_pendingPacket; int m_pendingPacketConsumed; class WriteBridge : public FLAC::Encoder::Stream { public: explicit WriteBridge(SBEncoder* owner) : m_owner(owner) { } FLAC__StreamEncoderWriteStatus write_callback(const FLAC__byte buffer[], size_t bytes, unsigned samples, unsigned current_frame) override; private: SBEncoder* m_owner; }; WriteBridge* m_flac; }; } // namespace bridge #endif // FLACENCODER_H