Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions src/decoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ bool SendspinDecoder::process_header(const uint8_t* data, size_t data_size, Chun
AudioStreamInfo(static_cast<uint8_t>(info.bits_per_sample()),
static_cast<uint8_t>(info.num_channels()), info.sample_rate());
*stream_info = this->current_stream_info_;
this->maximum_decoded_size_ = static_cast<size_t>(info.max_block_size()) *
static_cast<size_t>(info.num_channels()) *
static_cast<size_t>(info.bytes_per_sample());
this->decode_buffer_size_ = static_cast<size_t>(info.max_block_size()) *
static_cast<size_t>(info.num_channels()) *
static_cast<size_t>(info.bytes_per_sample());
break;
}
case CHUNK_TYPE_OPUS_DUMMY_HEADER: {
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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<OpusDecoder>(), 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;
Expand Down
25 changes: 16 additions & 9 deletions src/decoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -48,7 +50,7 @@ namespace sendspin {
*
* decoder.process_header(header_data, header_size, CHUNK_TYPE_FLAC_HEADER, &stream_info);
*
* std::vector<uint8_t> output(decoder.get_maximum_decoded_size());
* std::vector<uint8_t> 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);
Expand Down Expand Up @@ -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);

Expand All @@ -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_;
}

// ========================================
Expand All @@ -115,7 +122,7 @@ class SendspinDecoder {
std::unique_ptr<micro_flac::FLACDecoder> 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;
Expand Down
27 changes: 21 additions & 6 deletions src/sync_task.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -470,8 +470,9 @@ 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();
// 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(
needed, this->player_impl_->config.decode_buffer_location);
Expand Down Expand Up @@ -508,10 +509,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;
Expand Down
Loading