-
Notifications
You must be signed in to change notification settings - Fork 12
Add an Opus encode path to the source role #116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Draft
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
1ec53c8
Add an Opus encode path to the source role
chrisuthe 7da5565
Merge branch 'ci-fix/04' into ci-fix/05
chrisuthe 4f32695
Merge branch 'rev-fix/04-wire-tests' into rev-fix/05-opus
chrisuthe cf56448
Merge branch 'source-role/04-wire-tests' into source-role/05-opus
chrisuthe 27faaf0
Merge branch 'source-role/04-wire-tests' into source-role/05-opus
chrisuthe 7e9cd01
Merge branch 'source-role/04-wire-tests' into source-role/05-opus
chrisuthe e853ac7
Streamline Opus encoding and accept 5 ms frames
chrisuthe 607aee8
Merge branch 'source-role/04-wire-tests' into source-role/05-opus
chrisuthe c8a939a
Guard the source role's Opus allocation-mode assumptions
chrisuthe 3e90224
Merge branch 'source-role/04-wire-tests' into source-role/05-opus
chrisuthe 4464996
Merge branch 'source-role/04-wire-tests' into source-role/05-opus
chrisuthe 3a50c39
Merge branch 'source-role/04-wire-tests' into source-role/05-opus
chrisuthe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| // 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 opus_state_location.h | ||
| /// @brief Shared memory-placement rule for the libopus codec state this library allocates | ||
|
|
||
| #pragma once | ||
|
|
||
| #include "sendspin/types.h" | ||
|
|
||
| namespace sendspin { | ||
|
|
||
| // Mirrors micro-opus's CONFIG_OPUS_STATE_MEMORY_PREFERENCE for the codec-state buffers this | ||
| // library allocates itself via the *_init() variants (decoder.cpp, source_encoder_opus.cpp), so | ||
| // one placement rule governs Opus state regardless of who allocated it. The strict *_ONLY modes | ||
| // are honored as a soft preference. | ||
| #if defined(CONFIG_OPUS_STATE_PREFER_INTERNAL) || defined(CONFIG_OPUS_STATE_INTERNAL_ONLY) | ||
| constexpr MemoryLocation OPUS_STATE_LOCATION = MemoryLocation::PREFER_INTERNAL; | ||
| #else | ||
| constexpr MemoryLocation OPUS_STATE_LOCATION = MemoryLocation::PREFER_EXTERNAL; | ||
| #endif | ||
|
|
||
| } // namespace sendspin |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| // 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. | ||
|
|
||
| #include "source_encoder_opus.h" | ||
|
|
||
| #include "opus_state_location.h" | ||
| #include "platform/logging.h" | ||
| #include "source_task.h" | ||
| #include <opus.h> | ||
|
|
||
| #include <algorithm> | ||
| #include <cstring> | ||
| #include <iterator> | ||
|
|
||
| namespace sendspin { | ||
|
|
||
| static const char* const TAG = "sendspin.source_encoder"; | ||
|
|
||
| bool OpusSourceEncoder::init(const SourceRoleConfig& config) { | ||
| this->sample_rate_ = config.sample_rate; | ||
| this->bytes_per_frame_ = source_bytes_per_frame(config.channels, config.bit_depth); | ||
|
|
||
| const int state_size = opus_encoder_get_size(config.channels); | ||
| if (state_size <= 0 || | ||
| !this->encoder_state_.allocate(static_cast<size_t>(state_size), OPUS_STATE_LOCATION)) { | ||
| SS_LOGE(TAG, "Couldn't allocate %d bytes for the Opus encoder state", state_size); | ||
| return false; | ||
| } | ||
|
|
||
| // AUDIO fixed for line-in/music capture; a tuning knob waits for a demonstrated need | ||
| int err = opus_encoder_init(this->encoder_state_.as<OpusEncoder>(), | ||
| static_cast<opus_int32>(config.sample_rate), config.channels, | ||
| OPUS_APPLICATION_AUDIO); | ||
| if (err == OPUS_OK) { | ||
| err = opus_encoder_ctl(this->encoder_state_.as<OpusEncoder>(), | ||
| OPUS_SET_BITRATE(static_cast<opus_int32>(config.opus_bitrate))); | ||
| } | ||
| if (err == OPUS_OK) { | ||
| err = | ||
| opus_encoder_ctl(this->encoder_state_.as<OpusEncoder>(), | ||
| OPUS_SET_COMPLEXITY(static_cast<opus_int32>(config.opus_complexity))); | ||
| } | ||
| // OPUS_GET_LOOKAHEAD returns SAMPLES at the encoder's rate, not ms; stable for fixed | ||
| // settings so queried once | ||
| opus_int32 lookahead_samples = 0; | ||
| if (err == OPUS_OK) { | ||
| err = opus_encoder_ctl(this->encoder_state_.as<OpusEncoder>(), | ||
| OPUS_GET_LOOKAHEAD(&lookahead_samples)); | ||
| } | ||
| if (err != OPUS_OK) { | ||
| SS_LOGE(TAG, "Couldn't initialize the Opus encoder, error %d", err); | ||
| this->encoder_state_.reset(); | ||
| return false; | ||
| } | ||
| this->lookahead_us_ = | ||
| source_frames_to_us(static_cast<uint64_t>(lookahead_samples), config.sample_rate); | ||
|
|
||
| // Both scratches follow the audio buffers' placement choice (same bytes, same access) | ||
| const uint64_t chunk_bytes = | ||
| source_ms_to_frames(config.chunk_duration_ms, config.sample_rate) * this->bytes_per_frame_; | ||
| if (!this->pcm_scratch_.allocate(static_cast<size_t>(chunk_bytes), config.buffer_location)) { | ||
| SS_LOGE(TAG, "Couldn't allocate the Opus chunk scratch buffer"); | ||
| this->encoder_state_.reset(); | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| bool OpusSourceEncoder::can_encode(size_t in_len) const { | ||
| if (in_len == 0 || (in_len % this->bytes_per_frame_) != 0U) { | ||
| return false; | ||
| } | ||
| // One opus_encode() call takes exactly one legal frame (RFC 6716 durations), tabled in | ||
| // tenth-ms so 2.5 stays integral; counts are exact for every accepted rate | ||
| static constexpr uint32_t OPUS_FRAME_TENTH_MS[] = {25, 50, 100, 200, 400, 600}; | ||
| static constexpr uint32_t TENTH_MS_PER_SECOND = 10000U; | ||
| const size_t frames = in_len / this->bytes_per_frame_; | ||
| return std::any_of( | ||
| std::begin(OPUS_FRAME_TENTH_MS), std::end(OPUS_FRAME_TENTH_MS), [&](uint32_t tenth_ms) { | ||
| return frames == | ||
| static_cast<size_t>(this->sample_rate_) * tenth_ms / TENTH_MS_PER_SECOND; | ||
| }); | ||
| } | ||
|
|
||
| size_t OpusSourceEncoder::encode(const uint8_t* in, size_t in_len, uint8_t* out, | ||
| size_t out_capacity) { | ||
| if (!this->can_encode(in_len) || in_len > this->pcm_scratch_.size()) { | ||
| // Defensive: the task consults can_encode() before handing over a remainder | ||
| SS_LOGD(TAG, "Opus cannot encode a %u-byte chunk; dropping it", | ||
| static_cast<unsigned>(in_len)); | ||
| return 0; | ||
| } | ||
|
|
||
| // `in` sits behind the 9-byte wire header and is not int16-aligned, so the PCM is copied to | ||
| // the aligned scratch; that copy is also what honors the seam's in == out contract, so the | ||
| // packet can be encoded straight into `out` with no second buffer or copy. | ||
| memcpy(this->pcm_scratch_.data(), in, in_len); | ||
| // libopus treats max_data_bytes as a hard packet cap and degrades quality to fit it, so a | ||
| // small payload area yields a valid smaller packet rather than an error. The task offers | ||
| // MAX_PACKET_BYTES, above every accepted config's worst case, so in-tree streams never | ||
| // degrade. | ||
| const auto capacity = | ||
| static_cast<opus_int32>(std::min(out_capacity, static_cast<size_t>(MAX_PACKET_BYTES))); | ||
| const opus_int32 written = | ||
| opus_encode(this->encoder_state_.as<OpusEncoder>(), this->pcm_scratch_.as<opus_int16>(), | ||
| static_cast<int>(in_len / this->bytes_per_frame_), out, capacity); | ||
| if (written <= 0) { | ||
| SS_LOGE(TAG, "Opus encode failed, error %d", static_cast<int>(written)); | ||
| return 0; | ||
| } | ||
| return static_cast<size_t>(written); | ||
| } | ||
|
|
||
| void OpusSourceEncoder::reset() { | ||
| // Keeps the allocations (unlike the decode side): this encoder's format is the role's | ||
| // contract for every stream it opens, so the cached lookahead stays valid too | ||
| opus_encoder_ctl(this->encoder_state_.as<OpusEncoder>(), OPUS_RESET_STATE); | ||
| } | ||
|
|
||
| } // namespace sendspin |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.