// sbencoder.cpp -- 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. #include "sbencoder.h" #include "start_lead.h" #include #include #include #include namespace { // One PCM conversion batch's worth of interleaved stereo samples. constexpr int kSamplesPerChunk = 1024; constexpr int kEncodedRingCapacity = 256; constexpr int kChannelCount = 2; // Exact legacy pacing window when the configured start lead is zero. constexpr uint32_t kMaxEncodeLeadMs = 250; // Unpacks one little-endian PCM sample at `cursor` (advancing it) into the // FLAC__int32 form libFLAC's interleaved encoder expects. FLAC__int32 nextSampleAsInt32(const char*& cursor, int bitDepth) { switch (bitDepth) { case 8: { FLAC__int32 v = (unsigned char)(*cursor) - 128; cursor += 1; return v; } case 16: { FLAC__int32 v = upnp::littleEndianSample(cursor, 16); cursor += 2; return v; } case 24: { FLAC__int32 v = upnp::littleEndianSample(cursor, 24); cursor += 3; return v; } case 32: { FLAC__int32 v = upnp::littleEndianSample(cursor, 32); cursor += 4; return v; } default: return 0; } } } // namespace extern "C" { uint64_t get_sb_time_ms(void); unsigned get_squeezebox_stream_id(void); int yeney_is_paused(void); } // extern "C" using namespace bridge; SBEncoder::SBEncoder(unsigned streamId) : m_phase(Phase::Init) , m_firstReadAtMs(0) , m_pcmBytesAccepted(0) , m_bytesPerFrame(0) , m_sampleBits(0) , m_streamId(streamId) , m_interleaveBuf(nullptr) , m_encodedRing(nullptr) , m_pendingPacket(nullptr) , m_pendingPacketConsumed(0) , m_flac(nullptr) { m_encodedRing = new upnp::EncodedBuffer(kEncodedRingCapacity); m_flac = new WriteBridge(this); } SBEncoder::~SBEncoder() { m_cancelled = true; m_flac->finish(); delete m_flac; delete[] m_interleaveBuf; if (m_pendingPacket) m_encodedRing->freePacket(m_pendingPacket); delete m_encodedRing; } bool SBEncoder::open() { return open(16); } bool SBEncoder::open(uint8_t sampleBits, unsigned sampleRate) { if (m_phase != Phase::Init) { printf("SBEncoder::open(stream=%u) -- already opened\n", m_streamId); return false; } m_flac->set_verify(true); m_flac->set_compression_level(5); m_flac->set_channels(kChannelCount); m_flac->set_bits_per_sample(sampleBits); m_flac->set_sample_rate(sampleRate); m_sampleRate = sampleRate; m_bytesPerFrame = (sampleBits / 8) * kChannelCount; m_sampleBits = sampleBits; m_encodedRing->clear(); if (m_pendingPacket) { m_encodedRing->freePacket(m_pendingPacket); m_pendingPacket = nullptr; } delete[] m_interleaveBuf; m_interleaveBuf = new FLAC__int32[kSamplesPerChunk * kChannelCount]; FLAC__StreamEncoderInitStatus status = m_flac->init(); if (status != FLAC__STREAM_ENCODER_INIT_STATUS_OK) { printf("SBEncoder::open(stream=%u) -- FLAC encoder error %s\n", m_streamId, FLAC__StreamEncoderInitStatusString[status]); m_phase = Phase::Closed; return false; } m_phase = Phase::Encoding; return true; } void SBEncoder::close() { std::lock_guard lock(m_writeMutex); if (m_phase == Phase::Closed) return; m_flac->finish(); m_phase = Phase::Closed; } int SBEncoder::pendingEncodedBytes() const { if (m_pendingPacket) return m_pendingPacket->size - m_pendingPacketConsumed; return m_encodedRing->bytesAvailable(); } int SBEncoder::drainEncodedBytes(char* data, int maxlen) { if (!m_pendingPacket) { m_pendingPacket = m_encodedRing->read(); m_pendingPacketConsumed = 0; } if (!m_pendingPacket) return 0; int available = m_pendingPacket->size - m_pendingPacketConsumed; int n = (maxlen < available) ? maxlen : available; memcpy(data, m_pendingPacket->data + m_pendingPacketConsumed, n); m_pendingPacketConsumed += n; if (n) m_encodedRing->readerProgress(); if (m_pendingPacketConsumed >= m_pendingPacket->size) { m_encodedRing->freePacket(m_pendingPacket); m_pendingPacket = nullptr; } return n; } int SBEncoder::acceptEncodedBytes(const char* data, int len) { history.append(data, len); return m_historyReader.load() ? len : m_encodedRing->write(data, len, [this] { return cancelled() || responseEnded() || producerRetired() || (m_writeInterrupted && m_writeInterrupted()); }); } int SBEncoder::encodePcm(const char* data, int len, const std::function &interrupted) { m_writeInterrupted = interrupted; struct Reset { std::function &callback; ~Reset() { callback = {}; } } reset{m_writeInterrupted}; int samplesLeft = len / m_bytesPerFrame; while (samplesLeft > 0) { int chunk = (samplesLeft > kSamplesPerChunk) ? kSamplesPerChunk : samplesLeft; const char* cursor = data; for (int i = 0; i < chunk * kChannelCount; ++i) m_interleaveBuf[i] = nextSampleAsInt32(cursor, m_sampleBits); data = cursor; if (!m_flac->process_interleaved(m_interleaveBuf, chunk)) break; samplesLeft -= chunk; } return len - samplesLeft*m_bytesPerFrame; } FLAC__StreamEncoderWriteStatus SBEncoder::WriteBridge::write_callback( const FLAC__byte buffer[], size_t bytes, unsigned samples, unsigned current_frame) { (void)current_frame; m_owner->m_encodedFrames.fetch_add(samples); int written = m_owner->acceptEncodedBytes((const char*)buffer, (int)bytes); if (samples && written == (int)bytes) m_owner->m_producedAudio.store(true); return (written == (int)bytes) ? FLAC__STREAM_ENCODER_WRITE_STATUS_OK : FLAC__STREAM_ENCODER_WRITE_STATUS_FATAL_ERROR; } int SBEncoder::read(char* data, int maxlen, unsigned timeout, bool holdWhilePaused, const std::function& peerClosed) { const bool limited = timeout != 0; auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout); for (;;) { if (cancelled() || responseEnded() || (peerClosed && peerClosed())) return 0; if (m_phase == Phase::Closed) { printf("SBEncoder::read: encoder is closed\n"); return 0; } if (m_streamId != get_squeezebox_stream_id()) { printf("SBEncoder::read: stream mismatch (%u != %u)\n", m_streamId, get_squeezebox_stream_id()); usleep(1000); return 0; } if (pendingEncodedBytes()) { if (!m_firstReadAtMs) m_firstReadAtMs = get_sb_time_ms(); return drainEncodedBytes(data, maxlen); } if (m_phase == Phase::Closing) { printf("All data consumed\n"); close(); return 0; } auto now = std::chrono::steady_clock::now(); if (holdWhilePaused && yeney_is_paused()) deadline = now + std::chrono::milliseconds(timeout); if (limited && now >= deadline) { printf("SBEncoder::read: timeout\n"); return 0; } usleep(1000); } } int SBEncoder::write(const char* data, int len, unsigned timeout, const std::function& firstPcm, const std::function& interrupted) { const bool limited = timeout != 0; const bool sonos = streamPacingSonos(); const int requested = len; int accepted = 0; for (;;) { if (interrupted && interrupted()) return 0; if (cancelled() || responseEnded() || producerRetired()) return 0; if (m_phase != Phase::Encoding) { printf("SBEncoder::write: encoder not active\n"); return 0; } if (m_streamId != get_squeezebox_stream_id()) { printf("SBEncoder::write: stream mismatch (%u != %u)\n", m_streamId, get_squeezebox_stream_id()); usleep(1000); return 0; } if (len == 0) { printf("Reached end of stream\n"); m_phase = Phase::Closing; if (m_historyReader.load()) close(); return 0; } const uint64_t encodedMs = m_pcmBytesAccepted / m_bytesPerFrame * 1000ULL / m_sampleRate; const uint64_t firstRead = m_firstReadAtMs.load(); const uint64_t playedMs = firstRead ? get_sb_time_ms() - firstRead : 0; if (sonos && len > 0) { auto clock = m_clock ? m_clock() : StreamClock{}; double played = m_pacer.update(get_sb_time_ms(), firstRead, clock, yeney_is_paused()); double allowance = StreamPacer::ceiling(played, yeney_start_lead_ms()) - double(m_pcmBytesAccepted.load()/m_bytesPerFrame)*1000/m_sampleRate; auto frames = uint64_t(std::max(0., allowance)*m_sampleRate/1000); if (!yeney_is_paused() && frames) { int count = int(std::min({frames, uint64_t(len/m_bytesPerFrame), uint64_t(kSamplesPerChunk)}))*m_bytesPerFrame; if (!count) return accepted; std::lock_guard lock(m_writeMutex); if (cancelled() || responseEnded() || producerRetired() || m_phase != Phase::Encoding) return 0; if (!m_pcmBytesAccepted && firstPcm) firstPcm(); m_pcmBytesAccepted += count; if (encodePcm(data,count,interrupted) != count) return 0; data += count; len -= count; accepted += count; if (!len) return requested; continue; } } // The same per-encoder clock covers the initial burst and steady // delivery. Checking before accepting a whole batch bounds overshoot // to one producer batch. Gapless writes keep this clock and allowance. if (!sonos && !yeney_is_paused() && encodedMs < playedMs + (yeney_start_lead_ms() ? yeney_start_lead_ms() : kMaxEncodeLeadMs)) { std::lock_guard lock(m_writeMutex); if (cancelled() || responseEnded() || producerRetired() || m_phase != Phase::Encoding) return 0; if (!m_pcmBytesAccepted && firstPcm) firstPcm(); m_pcmBytesAccepted += len; return encodePcm(data, len, interrupted); } if (limited && !yeney_is_paused()) { if (!timeout--) { printf("SBEncoder::write: timeout\n"); return 0; } } usleep(1000); } }