|
| 1 | +// Copyright 2026 Sendspin Contributors |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +#include "source_encoder_opus.h" |
| 16 | + |
| 17 | +#include "opus_state_location.h" |
| 18 | +#include "platform/logging.h" |
| 19 | +#include "source_task.h" |
| 20 | +#include <opus.h> |
| 21 | + |
| 22 | +#include <algorithm> |
| 23 | +#include <cstring> |
| 24 | +#include <iterator> |
| 25 | + |
| 26 | +namespace sendspin { |
| 27 | + |
| 28 | +static const char* const TAG = "sendspin.source_encoder"; |
| 29 | + |
| 30 | +bool OpusSourceEncoder::init(const SourceRoleConfig& config) { |
| 31 | + this->sample_rate_ = config.sample_rate; |
| 32 | + this->bytes_per_frame_ = source_bytes_per_frame(config.channels, config.bit_depth); |
| 33 | + |
| 34 | + const int state_size = opus_encoder_get_size(config.channels); |
| 35 | + if (state_size <= 0 || |
| 36 | + !this->encoder_state_.allocate(static_cast<size_t>(state_size), OPUS_STATE_LOCATION)) { |
| 37 | + SS_LOGE(TAG, "Couldn't allocate %d bytes for the Opus encoder state", state_size); |
| 38 | + return false; |
| 39 | + } |
| 40 | + |
| 41 | + // AUDIO fixed for line-in/music capture; a tuning knob waits for a demonstrated need |
| 42 | + int err = opus_encoder_init(this->encoder_state_.as<OpusEncoder>(), |
| 43 | + static_cast<opus_int32>(config.sample_rate), config.channels, |
| 44 | + OPUS_APPLICATION_AUDIO); |
| 45 | + if (err == OPUS_OK) { |
| 46 | + err = opus_encoder_ctl(this->encoder_state_.as<OpusEncoder>(), |
| 47 | + OPUS_SET_BITRATE(static_cast<opus_int32>(config.opus_bitrate))); |
| 48 | + } |
| 49 | + if (err == OPUS_OK) { |
| 50 | + err = |
| 51 | + opus_encoder_ctl(this->encoder_state_.as<OpusEncoder>(), |
| 52 | + OPUS_SET_COMPLEXITY(static_cast<opus_int32>(config.opus_complexity))); |
| 53 | + } |
| 54 | + // OPUS_GET_LOOKAHEAD returns SAMPLES at the encoder's rate, not ms; stable for fixed |
| 55 | + // settings so queried once |
| 56 | + opus_int32 lookahead_samples = 0; |
| 57 | + if (err == OPUS_OK) { |
| 58 | + err = opus_encoder_ctl(this->encoder_state_.as<OpusEncoder>(), |
| 59 | + OPUS_GET_LOOKAHEAD(&lookahead_samples)); |
| 60 | + } |
| 61 | + if (err != OPUS_OK) { |
| 62 | + SS_LOGE(TAG, "Couldn't initialize the Opus encoder, error %d", err); |
| 63 | + this->encoder_state_.reset(); |
| 64 | + return false; |
| 65 | + } |
| 66 | + this->lookahead_us_ = |
| 67 | + source_frames_to_us(static_cast<uint64_t>(lookahead_samples), config.sample_rate); |
| 68 | + |
| 69 | + // Both scratches follow the audio buffers' placement choice (same bytes, same access) |
| 70 | + const uint64_t chunk_bytes = |
| 71 | + source_ms_to_frames(config.chunk_duration_ms, config.sample_rate) * this->bytes_per_frame_; |
| 72 | + if (!this->pcm_scratch_.allocate(static_cast<size_t>(chunk_bytes), config.buffer_location) || |
| 73 | + !this->packet_scratch_.allocate(MAX_PACKET_BYTES, config.buffer_location)) { |
| 74 | + SS_LOGE(TAG, "Couldn't allocate the Opus chunk scratch buffers"); |
| 75 | + this->encoder_state_.reset(); |
| 76 | + return false; |
| 77 | + } |
| 78 | + return true; |
| 79 | +} |
| 80 | + |
| 81 | +bool OpusSourceEncoder::can_encode(size_t in_len) const { |
| 82 | + if (in_len == 0 || (in_len % this->bytes_per_frame_) != 0U) { |
| 83 | + return false; |
| 84 | + } |
| 85 | + // One opus_encode() call takes exactly one legal frame (RFC 6716 durations), tabled in |
| 86 | + // tenth-ms so 2.5 stays integral; counts are exact for every accepted rate |
| 87 | + static constexpr uint32_t OPUS_FRAME_TENTH_MS[] = {25, 50, 100, 200, 400, 600}; |
| 88 | + static constexpr uint32_t TENTH_MS_PER_SECOND = 10000U; |
| 89 | + const size_t frames = in_len / this->bytes_per_frame_; |
| 90 | + return std::any_of( |
| 91 | + std::begin(OPUS_FRAME_TENTH_MS), std::end(OPUS_FRAME_TENTH_MS), [&](uint32_t tenth_ms) { |
| 92 | + return frames == |
| 93 | + static_cast<size_t>(this->sample_rate_) * tenth_ms / TENTH_MS_PER_SECOND; |
| 94 | + }); |
| 95 | +} |
| 96 | + |
| 97 | +size_t OpusSourceEncoder::encode(const uint8_t* in, size_t in_len, uint8_t* out, |
| 98 | + size_t out_capacity) { |
| 99 | + if (!this->can_encode(in_len) || in_len > this->pcm_scratch_.size()) { |
| 100 | + // Defensive: the task consults can_encode() before handing over a remainder |
| 101 | + SS_LOGD(TAG, "Opus cannot encode a %u-byte chunk; dropping it", |
| 102 | + static_cast<unsigned>(in_len)); |
| 103 | + return 0; |
| 104 | + } |
| 105 | + |
| 106 | + // `in` sits behind the 9-byte wire header and is not int16-aligned, so copy to the aligned |
| 107 | + // scratch; encoding into the packet scratch (never `out`) is what honors in == out |
| 108 | + memcpy(this->pcm_scratch_.data(), in, in_len); |
| 109 | + const opus_int32 written = |
| 110 | + opus_encode(this->encoder_state_.as<OpusEncoder>(), this->pcm_scratch_.as<opus_int16>(), |
| 111 | + static_cast<int>(in_len / this->bytes_per_frame_), this->packet_scratch_.data(), |
| 112 | + static_cast<opus_int32>(MAX_PACKET_BYTES)); |
| 113 | + if (written <= 0) { |
| 114 | + SS_LOGE(TAG, "Opus encode failed, error %d", static_cast<int>(written)); |
| 115 | + return 0; |
| 116 | + } |
| 117 | + if (static_cast<size_t>(written) > out_capacity) { |
| 118 | + SS_LOGE(TAG, "Opus packet of %d bytes exceeds the %u-byte payload capacity; dropping chunk", |
| 119 | + static_cast<int>(written), static_cast<unsigned>(out_capacity)); |
| 120 | + return 0; |
| 121 | + } |
| 122 | + memcpy(out, this->packet_scratch_.data(), static_cast<size_t>(written)); |
| 123 | + return static_cast<size_t>(written); |
| 124 | +} |
| 125 | + |
| 126 | +void OpusSourceEncoder::reset() { |
| 127 | + // Keeps the allocations (unlike the decode side): this encoder's format is the role's |
| 128 | + // contract for every stream it opens, so the cached lookahead stays valid too |
| 129 | + opus_encoder_ctl(this->encoder_state_.as<OpusEncoder>(), OPUS_RESET_STATE); |
| 130 | +} |
| 131 | + |
| 132 | +} // namespace sendspin |
0 commit comments