sbencoder.h
Rama main · réplica de lectura del repositorio original.
// 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 <atomic>
#include <functional>
#include <mutex>
#include <FLAC++/encoder.h>
#include <FLAC++/metadata.h>
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<void()>& firstPcm = {},
const std::function<bool()>& 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<bool()>& 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<uint64_t> frameOrigin{0};
void pacingClock(std::function<StreamClock()> 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<bool()> &interrupted = {});
std::function<bool()> m_writeInterrupted;
int pendingEncodedBytes() const;
int acceptEncodedBytes(const char* data, int len);
int drainEncodedBytes(char* data, int maxlen);
std::atomic<uint64_t> m_encodedFrames{0};
std::atomic<Phase> m_phase;
std::atomic<bool> m_historyReader{false};
std::atomic<bool> m_cancelled{false};
std::atomic<bool> m_responseEnded{false};
std::atomic<bool> m_producerRetired{false};
std::atomic<bool> m_producedAudio{false};
std::mutex m_writeMutex;
std::atomic<uint64_t> m_firstReadAtMs; // set on the first successful read()
StreamPacer m_pacer;
std::function<StreamClock()> m_clock;
std::atomic<uint64_t> 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