From d63aaf14c5520f3ae1c6c9a85bbb9afc39df0ef9 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 12 Oct 2026 11:52:34 -0400 Subject: [PATCH 1/3] Keep Opus PCM little-endian on big-endian hosts libopus reads and writes host-order opus_int16, but the player's on_audio_write() output and the source's write_audio() input are little-endian. Swap the samples after opus_decode() and before opus_encode() on big-endian hosts; the swap compiles away elsewhere. Bump the host micro-flac pin to v0.3.0, whose decoder output is little-endian on big-endian hosts too. The IDF component minimum stays until ESPHome moves to v0.3.0. Fixes #132 --- cmake/host.cmake | 2 +- include/sendspin/player_role.h | 2 +- src/decoder.cpp | 2 ++ src/pcm_byte_order.h | 42 ++++++++++++++++++++++++++++++++++ src/source_encoder_opus.cpp | 2 ++ 5 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 src/pcm_byte_order.h diff --git a/cmake/host.cmake b/cmake/host.cmake index 46da9c59..cedb81b9 100644 --- a/cmake/host.cmake +++ b/cmake/host.cmake @@ -68,7 +68,7 @@ function(sendspin_configure_host TARGET_LIB SOURCE_DIR) FetchContent_Declare( micro_flac GIT_REPOSITORY https://github.com/esphome-libs/micro-flac.git - GIT_TAG v0.1.1 + GIT_TAG v0.3.0 GIT_SUBMODULES "lib/micro-ogg-demuxer" ) FetchContent_MakeAvailable(micro_flac) diff --git a/include/sendspin/player_role.h b/include/sendspin/player_role.h index 438429ba..3119b7ab 100644 --- a/include/sendspin/player_role.h +++ b/include/sendspin/player_role.h @@ -81,7 +81,7 @@ class PlayerRoleListener { /// @brief Writes decoded PCM audio to the platform's audio output /// /// Fires on the sync task's background thread. May block up to timeout_ms. - /// @param data Pointer to the decoded PCM audio data + /// @param data Decoded PCM audio data, interleaved and little-endian /// @param length Number of bytes to write; always a whole number of PCM frames /// @param timeout_ms Maximum time to wait for the write to complete /// @return Number of bytes actually written. Partial writes are allowed but must be a diff --git a/src/decoder.cpp b/src/decoder.cpp index 14f14955..3dff139a 100644 --- a/src/decoder.cpp +++ b/src/decoder.cpp @@ -15,6 +15,7 @@ #include "decoder.h" #include "opus_state_location.h" +#include "pcm_byte_order.h" #include "platform/logging.h" #include @@ -197,6 +198,7 @@ bool SendspinDecoder::decode_audio_chunk(const uint8_t* data, size_t data_size, } *consumed = data_size; *decoded_size = this->current_stream_info_.frames_to_bytes(output_frames); + pcm16_le_to_from_host(output_buffer, *decoded_size); #endif } else { return false; diff --git a/src/pcm_byte_order.h b/src/pcm_byte_order.h new file mode 100644 index 00000000..1fa3af63 --- /dev/null +++ b/src/pcm_byte_order.h @@ -0,0 +1,42 @@ +// Copyright 2026 Sendspin Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// @file pcm_byte_order.h +/// @brief Converts 16-bit PCM between the library's little-endian buffers and host order + +#pragma once + +#include +#include +#include +#include + +namespace sendspin { + +/// @brief Converts 16-bit samples between little-endian and host byte order, in place +/// +/// The library's PCM buffers are little-endian; libopus reads and writes host-order opus_int16. +/// The conversion is its own inverse, so one call serves both directions. A no-op on +/// little-endian hosts. +/// @param data Sample buffer; needs no alignment. +/// @param size Buffer size in bytes; an odd trailing byte is left alone. +inline void pcm16_le_to_from_host(uint8_t* data, size_t size) { + if constexpr (std::endian::native == std::endian::big) { + for (size_t i = 0; i + 1 < size; i += 2) { + std::swap(data[i], data[i + 1]); + } + } +} + +} // namespace sendspin diff --git a/src/source_encoder_opus.cpp b/src/source_encoder_opus.cpp index 937f8750..8c9fedeb 100644 --- a/src/source_encoder_opus.cpp +++ b/src/source_encoder_opus.cpp @@ -15,6 +15,7 @@ #include "source_encoder_opus.h" #include "opus_state_location.h" +#include "pcm_byte_order.h" #include "platform/logging.h" #include "source_task.h" #include @@ -87,6 +88,7 @@ size_t OpusSourceEncoder::encode(const uint8_t* in, size_t in_len, uint8_t* out, if (in != this->pcm_scratch_.data()) { std::memcpy(this->pcm_scratch_.data(), in, in_len); } + pcm16_le_to_from_host(this->pcm_scratch_.data(), in_len); // max_data_bytes is a hard packet cap: libopus lowers quality to fit rather than failing. // The task offers at least MAX_PACKET_BYTES, so its chunks never degrade. const auto capacity = From bfe6b87a6db5948756aab02e05d7a92075f717bf Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 12 Oct 2026 12:04:32 -0400 Subject: [PATCH 2/3] Trim pcm16_le_to_from_host's doc to its contract --- src/pcm_byte_order.h | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pcm_byte_order.h b/src/pcm_byte_order.h index 1fa3af63..2d02718a 100644 --- a/src/pcm_byte_order.h +++ b/src/pcm_byte_order.h @@ -25,12 +25,8 @@ namespace sendspin { /// @brief Converts 16-bit samples between little-endian and host byte order, in place -/// -/// The library's PCM buffers are little-endian; libopus reads and writes host-order opus_int16. -/// The conversion is its own inverse, so one call serves both directions. A no-op on -/// little-endian hosts. -/// @param data Sample buffer; needs no alignment. -/// @param size Buffer size in bytes; an odd trailing byte is left alone. +/// @param data Sample buffer; needs no alignment +/// @param size Buffer size in bytes inline void pcm16_le_to_from_host(uint8_t* data, size_t size) { if constexpr (std::endian::native == std::endian::big) { for (size_t i = 0; i + 1 < size; i += 2) { From cf7f54b2db85857546f0bdd476cde2103eb0de7c Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 12 Oct 2026 12:06:33 -0400 Subject: [PATCH 3/3] Document on_audio_write()'s PCM byte order in the integration guide --- docs/integration-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integration-guide.md b/docs/integration-guide.md index 2af1880f..b25367a6 100644 --- a/docs/integration-guide.md +++ b/docs/integration-guide.md @@ -230,7 +230,7 @@ the other is `SendspinNetworkProvider::is_network_ready()`. ```cpp struct MyPlayerListener : PlayerRoleListener { - // REQUIRED: Write decoded PCM audio to your audio output. + // REQUIRED: Write decoded PCM audio (interleaved, little-endian) to your audio output. // Called from a background thread. May block up to timeout_ms. // Must return the number of bytes actually written. size_t on_audio_write(uint8_t* data, size_t length, uint32_t timeout_ms) override {