From 3dce42c98fc6fc2c1f680192da95ed61b55dc124 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 11 May 2026 10:38:44 -0400 Subject: [PATCH 1/2] Don't allocate the worst case scenario size for opus packets. Only increases the buffer size to the max if actually necessary. --- src/decoder.cpp | 26 +++++++++++++++++++------- src/decoder.h | 25 ++++++++++++++++--------- src/sync_task.cpp | 24 +++++++++++++++++++----- 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/src/decoder.cpp b/src/decoder.cpp index 3b2eb6b..f9c7801 100644 --- a/src/decoder.cpp +++ b/src/decoder.cpp @@ -81,9 +81,9 @@ bool SendspinDecoder::process_header(const uint8_t* data, size_t data_size, Chun AudioStreamInfo(static_cast(info.bits_per_sample()), static_cast(info.num_channels()), info.sample_rate()); *stream_info = this->current_stream_info_; - this->maximum_decoded_size_ = static_cast(info.max_block_size()) * - static_cast(info.num_channels()) * - static_cast(info.bytes_per_sample()); + this->decode_buffer_size_ = static_cast(info.max_block_size()) * + static_cast(info.num_channels()) * + static_cast(info.bytes_per_sample()); break; } case CHUNK_TYPE_OPUS_DUMMY_HEADER: { @@ -107,9 +107,11 @@ bool SendspinDecoder::process_header(const uint8_t* data, size_t data_size, Chun return false; } - static constexpr uint32_t OPUS_MAX_FRAME_MS = 120U; - this->maximum_decoded_size_ = - stream_info->ms_to_bytes(OPUS_MAX_FRAME_MS); // Opus max frame size is 120ms + // Opus packets are almost always a single 20ms frame, so size the decode buffer for + // that. decode_audio_chunk() raises this estimate (up to the 120ms spec maximum) the + // first time it sees a larger packet, signalling the caller to grow its buffer. + static constexpr uint32_t OPUS_TYPICAL_FRAME_MS = 20U; + this->decode_buffer_size_ = stream_info->ms_to_bytes(OPUS_TYPICAL_FRAME_MS); this->current_stream_info_ = *stream_info; this->current_codec_ = SendspinCodecFormat::OPUS; break; @@ -121,7 +123,7 @@ bool SendspinDecoder::process_header(const uint8_t* data, size_t data_size, Chun this->current_stream_info_ = *stream_info; this->current_codec_ = SendspinCodecFormat::PCM; static constexpr uint32_t PCM_MAX_CHUNK_MS = 120U; - this->maximum_decoded_size_ = + this->decode_buffer_size_ = stream_info->ms_to_bytes(PCM_MAX_CHUNK_MS); // PCM max chunk size break; } @@ -172,6 +174,16 @@ bool SendspinDecoder::decode_audio_chunk(const uint8_t* data, size_t data_size, int output_frames = opus_decode( this->opus_decoder_buf_.as(), data, data_size, (int16_t*)output_buffer, this->current_stream_info_.bytes_to_frames(output_buffer_size), 0); + if (output_frames == OPUS_BUFFER_TOO_SMALL) { + // The output buffer was sized for a typical 20ms frame but this packet decodes to + // more. Raise the estimate to the Opus spec maximum (120ms) so the caller can grow + // the buffer via get_decode_buffer_size() and call again. + static constexpr uint32_t OPUS_MAX_FRAME_MS = 120U; + this->decode_buffer_size_ = this->current_stream_info_.ms_to_bytes(OPUS_MAX_FRAME_MS); + SS_LOGD(TAG, "Opus packet exceeds decode buffer; raising estimate to %zu bytes", + this->decode_buffer_size_); + return false; + } if (output_frames < 0) { SS_LOGE(TAG, "Error decoding opus chunk: %d", output_frames); return false; diff --git a/src/decoder.h b/src/decoder.h index 6269e13..67b7b61 100644 --- a/src/decoder.h +++ b/src/decoder.h @@ -38,8 +38,10 @@ namespace sendspin { * * Usage: * 1. Call process_header() with the first chunk to initialize the codec and stream info - * 2. Allocate an output buffer of at least get_maximum_decoded_size() bytes - * 3. Call decode_audio_chunk() for each encoded chunk to fill the output buffer + * 2. Allocate an output buffer of at least get_decode_buffer_size() bytes + * 3. Call decode_audio_chunk() for each encoded chunk to fill the output buffer. For Opus this + * estimate can grow mid-stream: if decode_audio_chunk() returns false and + * get_decode_buffer_size() has increased, enlarge the buffer to the new size and call again. * 4. Call reset_decoders() when the stream ends or a new stream starts * * @code @@ -48,7 +50,7 @@ namespace sendspin { * * decoder.process_header(header_data, header_size, CHUNK_TYPE_FLAC_HEADER, &stream_info); * - * std::vector output(decoder.get_maximum_decoded_size()); + * std::vector output(decoder.get_decode_buffer_size()); * size_t decoded_size = 0; * decoder.decode_audio_chunk(encoded_data, encoded_size, * output.data(), output.size(), &decoded_size); @@ -80,7 +82,9 @@ class SendspinDecoder { /// @param output_buffer Pointer to the buffer where decoded audio will be written. /// @param output_buffer_size Size of the output buffer in bytes. /// @param[out] decoded_size Pointer to store the number of decoded bytes written. - /// @return True if successful, false otherwise. + /// @return True if successful, false otherwise. For Opus, a false return may simply mean the + /// chunk decodes to more than output_buffer_size bytes; in that case get_decode_buffer_size() + /// has increased, so resize output_buffer to it and call again. bool decode_audio_chunk(const uint8_t* data, size_t data_size, uint8_t* output_buffer, size_t output_buffer_size, size_t* decoded_size); @@ -90,10 +94,13 @@ class SendspinDecoder { return this->current_codec_; } - /// @brief Returns the maximum number of bytes a single decoded frame can produce. - /// @return Maximum decoded output size in bytes. - size_t get_maximum_decoded_size() const { - return this->maximum_decoded_size_; + /// @brief Returns the size to allocate for the decoded-output buffer. + /// @details For FLAC and PCM this is a fixed upper bound. For Opus it starts at the common 20ms + /// frame size and grows (up to the 120ms spec maximum) when decode_audio_chunk() meets a larger + /// packet; that call returns false until the caller resizes its buffer to the new value. + /// @return Required decoded-output buffer size in bytes. + size_t get_decode_buffer_size() const { + return this->decode_buffer_size_; } // ======================================== @@ -115,7 +122,7 @@ class SendspinDecoder { std::unique_ptr flac_decoder_; // size_t fields - size_t maximum_decoded_size_{0}; + size_t decode_buffer_size_{0}; // 32-bit fields SendspinCodecFormat current_codec_ = SendspinCodecFormat::UNSUPPORTED; diff --git a/src/sync_task.cpp b/src/sync_task.cpp index 928a03a..7beb6f8 100644 --- a/src/sync_task.cpp +++ b/src/sync_task.cpp @@ -471,7 +471,7 @@ DecodeResult SyncTask::decode_chunk(SyncContext& sync_context) { } // Create or resize the decode buffer now that we know the maximum decoded size - size_t needed = sync_context.decoder->get_maximum_decoded_size(); + size_t needed = sync_context.decoder->get_decode_buffer_size(); if (sync_context.decode_buffer == nullptr) { sync_context.decode_buffer = TransferBuffer::create( needed, this->player_impl_->config.decode_buffer_location); @@ -508,10 +508,24 @@ DecodeResult SyncTask::decode_chunk(SyncContext& sync_context) { } size_t decoded_size = 0; - if (!sync_context.decoder->decode_audio_chunk( - sync_context.encoded_entry->data(), sync_context.encoded_entry->data_size, - sync_context.decode_buffer->get_buffer_end(), sync_context.decode_buffer->free(), - &decoded_size)) { + bool decoded = sync_context.decoder->decode_audio_chunk( + sync_context.encoded_entry->data(), sync_context.encoded_entry->data_size, + sync_context.decode_buffer->get_buffer_end(), sync_context.decode_buffer->free(), + &decoded_size); + if (!decoded) { + // The decoder raises its decoded-size estimate when it meets an unusually large chunk + // (e.g. a multi-frame Opus packet bigger than the typical 20ms buffer). Grow the + // buffer to the new estimate and retry once. + size_t needed = sync_context.decoder->get_decode_buffer_size(); + if (needed > sync_context.decode_buffer->capacity() && + sync_context.decode_buffer->reallocate(needed)) { + decoded = sync_context.decoder->decode_audio_chunk( + sync_context.encoded_entry->data(), sync_context.encoded_entry->data_size, + sync_context.decode_buffer->get_buffer_end(), + sync_context.decode_buffer->free(), &decoded_size); + } + } + if (!decoded) { SS_LOGE(TAG, "Failed to decode audio chunk"); this->encoded_ring_buffer_->return_chunk(sync_context.encoded_entry); sync_context.encoded_entry = nullptr; From 6b1d54d5393a721ba1de2534e4972a33db19a5d9 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 11 May 2026 12:47:35 -0400 Subject: [PATCH 2/2] Fix stale comment Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/sync_task.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sync_task.cpp b/src/sync_task.cpp index 7beb6f8..f878704 100644 --- a/src/sync_task.cpp +++ b/src/sync_task.cpp @@ -470,7 +470,8 @@ DecodeResult SyncTask::decode_chunk(SyncContext& sync_context) { } } - // Create or resize the decode buffer now that we know the maximum decoded size + // Create or resize the decode buffer using the decoder's current required size + // estimate; some codecs (for example, Opus) may require this to grow later. size_t needed = sync_context.decoder->get_decode_buffer_size(); if (sync_context.decode_buffer == nullptr) { sync_context.decode_buffer = TransferBuffer::create(