Files
JustOneCacophony/src/stream/stream.cpp
T
TheM14 42d6da6302
ci / ubuntu-latest (push) Failing after 13s
ci / macos-latest (push) Has been cancelled
ci / windows-latest (push) Has been cancelled
Initial pure C++ implementation
2026-09-25 04:17:30 +08:00

349 lines
13 KiB
C++

#include "stream/stream.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include "adm/adm_metadata.h"
#include "foundation/status.h"
#include "hrtf/jochrtf.h"
#include "joc_bitstream/joc_parser.h"
#include "joc_core/objects16.h"
namespace joc::stream {
namespace {
constexpr std::size_t kFrameSamples = JOC_FRAME_SAMPLES;
constexpr std::size_t kBedChannels = 6;
constexpr int kCoreChannels[5] = {0, 1, 2, 4, 5};
constexpr int kLfeChannel = 3;
} // namespace
Stream::~Stream() {
if (rebuilder_ != nullptr) {
ejoc_renderer_destroy(rebuilder_);
rebuilder_ = nullptr;
}
if (speaker_.handle != nullptr) {
ejoc_speaker_renderer_destroy(speaker_.handle);
speaker_.handle = nullptr;
}
}
void Stream::reset_state() {
reader_ = eac3::FrameReader();
metadata_.clear();
bed_pending_.clear();
objects16_.clear();
output_.clear();
read_offset_ = 0;
info_ = Info();
info_.output_channels = output_channels_;
if (rebuilder_ != nullptr) {
ejoc_renderer_reset(rebuilder_);
}
if (speaker_.handle != nullptr) {
ejoc_speaker_renderer_reset(speaker_.handle);
speaker_.state.reset();
speaker_.output.clear();
speaker_.last_had_payload = false;
}
if (binaural_ready_) {
binaural_.reset();
}
}
Status Stream::create(const Config& config) {
if (rebuilder_ != nullptr) {
return Status::fail(JOC_ERR_STATE, stage::kRender, "stream already created");
}
config_ = config;
if (config_.input > JOC_STREAM_IN_PCM_OBJECTS16 ||
config_.output > JOC_STREAM_OUT_BINAURAL) {
return Status::fail(JOC_ERR_INVALID_CONFIG, stage::kRender, "unknown stream kind");
}
if (config_.input == JOC_STREAM_IN_PCM_OBJECTS16 &&
config_.output == JOC_STREAM_OUT_PCM_OBJECTS16) {
return Status::fail(JOC_ERR_INVALID_CONFIG, stage::kRender,
"objects16 input with objects16 output would do nothing");
}
gain_ = static_cast<float>(std::pow(10.0, config_.gain_db / 20.0));
if (config_.input == JOC_STREAM_IN_EAC3) {
rebuilder_ = ejoc_renderer_create();
if (rebuilder_ == nullptr) {
return Status::fail(JOC_ERR_OUT_OF_MEMORY, stage::kDsp, "cannot create the JOC kernel");
}
if (config_.native_threads != 0u) {
ejoc_renderer_set_threads(rebuilder_, config_.native_threads);
}
}
if (config_.output == JOC_STREAM_OUT_SPEAKER) {
speaker_enabled_ = true;
if (!speaker::layout_by_name(config_.layout.c_str(), &speaker_.layout)) {
return Status::fail(JOC_ERR_LAYOUT_UNSUPPORTED, stage::kRender,
"unknown speaker layout: " + config_.layout);
}
speaker_.metadata_offset = config_.metadata_offset;
speaker_.handle = ejoc_speaker_renderer_create(speaker_.layout.speaker_bitfield);
if (speaker_.handle == nullptr) {
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kRender,
"cannot create the speaker renderer");
}
output_channels_ = speaker_.layout.channel_count;
} else if (config_.output == JOC_STREAM_OUT_BINAURAL) {
binaural_enabled_ = true;
hrtf::Field field;
hrtf::Kernels kernels;
Status status = hrtf::load_jochrtf(config_.hrtf_path, &field);
if (status.ok()) {
status = config_.kernels_path.empty()
? (kernels = hrtf::builtin_kernels(), Status::success())
: hrtf::load_kernels(config_.kernels_path, &kernels);
}
binaural::Profile profile = binaural::Profile::Mid;
if (status.ok() && config_.binaural_mode == JOC_BINAURAL_NEAR) {
profile = binaural::Profile::Near;
} else if (status.ok() && config_.binaural_mode == JOC_BINAURAL_FAR) {
profile = binaural::Profile::Far;
}
if (status.ok()) {
status = binaural_.open(field, kernels, profile);
}
if (!status.ok()) {
return status;
}
binaural_ready_ = true;
output_channels_ = 2;
} else {
output_channels_ = JOC_OUTPUT_CHANNELS;
}
reset_state();
return Status::success();
}
Status Stream::push_eac3(const std::uint8_t* data, std::size_t size, std::size_t* consumed) {
if (rebuilder_ == nullptr) {
return Status::fail(JOC_ERR_STATE, stage::kEmdf, "stream is not an E-AC-3 input");
}
if (consumed != nullptr) {
*consumed = size;
}
if (data != nullptr && size != 0u) {
reader_.push(data, size);
info_.bytes_in += size;
}
for (;;) {
eac3::Frame frame;
const eac3::FrameReader::Next state = reader_.next(&frame);
if (state == eac3::FrameReader::Next::End) {
break;
}
if (state == eac3::FrameReader::Next::Fail) {
return Status::fail(reader_.error(), stage::kEac3, reader_.error_message());
}
FrameMetadata entry;
emdf::Container container;
const Status parsed =
joc::parse_eac3_frame(frame.data, frame.size, &entry.params, &container, nullptr);
if (!parsed.ok()) {
return parsed;
}
if (const emdf::Payload* payload = container.find(emdf::kIdOamd)) {
std::vector<std::uint8_t> bytes;
const Status extracted =
emdf::extract_payload_bytes(frame.data, frame.size, *payload, &bytes);
if (!extracted.ok()) {
return extracted;
}
const Status oamd = oamd::parse_id11(bytes.data(), bytes.size(), &entry.update);
if (!oamd.ok()) {
return oamd;
}
entry.has_update = true;
entry.outer_offset = static_cast<std::int64_t>(payload->sample_offset);
}
metadata_.push_back(entry);
}
return process_ready_frames();
}
Status Stream::push_bed(const float* interleaved6, std::size_t samples, std::size_t* consumed) {
if (rebuilder_ == nullptr) {
return Status::fail(JOC_ERR_STATE, stage::kDsp, "stream is not an E-AC-3 input");
}
if (consumed != nullptr) {
*consumed = samples;
}
if (interleaved6 != nullptr && samples != 0u) {
bed_pending_.insert(bed_pending_.end(), interleaved6,
interleaved6 + samples * kBedChannels);
}
return process_ready_frames();
}
Status Stream::push_objects16(const float* planar16, std::size_t samples, std::size_t* consumed) {
if (consumed != nullptr) {
*consumed = samples;
}
if (planar16 == nullptr || samples == 0u) {
return Status::success();
}
// Rendered immediately: the host has already done the JOC rebuild.
for (std::size_t offset = 0; offset < samples; offset += kFrameSamples) {
const std::size_t count = std::min(kFrameSamples, samples - offset);
std::vector<float> frame(static_cast<std::size_t>(JOC_OUTPUT_CHANNELS) * kFrameSamples,
0.0f);
for (std::size_t channel = 0; channel < JOC_OUTPUT_CHANNELS; ++channel) {
std::memcpy(frame.data() + channel * kFrameSamples,
planar16 + channel * samples + offset, count * sizeof(float));
}
++info_.frames_in;
info_.samples_in += count;
const Status rendered = render_objects16(frame);
if (!rendered.ok()) {
return rendered;
}
if (count != kFrameSamples) {
break; // a partial frame is dropped; the host should push whole frames
}
}
return Status::success();
}
Status Stream::process_ready_frames() {
while (bed_pending_.size() / kBedChannels >= kFrameSamples && !metadata_.empty()) {
const FrameMetadata entry = metadata_.front();
metadata_.pop_front();
std::vector<float> bed5(static_cast<std::size_t>(JOC_CORE_CHANNELS) * kFrameSamples, 0.0f);
std::vector<float> lfe(kFrameSamples, 0.0f);
for (std::size_t sample = 0; sample < kFrameSamples; ++sample) {
for (std::size_t channel = 0; channel < JOC_CORE_CHANNELS; ++channel) {
bed5[channel * kFrameSamples + sample] =
bed_pending_[sample * kBedChannels + kCoreChannels[channel]];
}
lfe[sample] = bed_pending_[sample * kBedChannels + kLfeChannel];
}
bed_pending_.erase(bed_pending_.begin(),
bed_pending_.begin() + static_cast<std::ptrdiff_t>(kFrameSamples *
kBedChannels));
std::string error;
const Status rebuilt = joc::rebuild_objects16(rebuilder_, entry.params, bed5.data(),
lfe.data(), gain_, &objects16_, &error);
if (!rebuilt.ok()) {
return Status::fail(rebuilt.code(), stage::kDsp, error);
}
pending_metadata_ = entry;
const Status rendered = render_objects16(objects16_);
if (!rendered.ok()) {
return rendered;
}
++info_.frames_in;
info_.samples_in += kFrameSamples;
}
return Status::success();
}
Status Stream::render_objects16(const std::vector<float>& objects16) {
if (config_.output == JOC_STREAM_OUT_PCM_OBJECTS16) {
output_.insert(output_.end(), objects16.begin(), objects16.end());
info_.frames_out++;
info_.samples_out += kFrameSamples;
return Status::success();
}
if (speaker_enabled_) {
std::string error;
const Status stepped =
speaker::step(&speaker_, objects16, pending_metadata_.has_update
? &pending_metadata_.update
: nullptr,
&error);
if (!stepped.ok()) {
return Status::fail(stepped.code(), stage::kRender, error);
}
for (const double value : speaker_.output) {
output_.push_back(static_cast<float>(value));
}
info_.frames_out++;
info_.samples_out += kFrameSamples;
return Status::success();
}
const Status submitted =
binaural_.submit_frame(objects16.data(),
pending_metadata_.has_update ? &pending_metadata_.update : nullptr,
static_cast<std::int64_t>(info_.frames_out),
pending_metadata_.outer_offset,
static_cast<std::int64_t>(config_.object_delay_samples));
if (!submitted.ok()) {
return submitted;
}
std::vector<double> produced;
binaural_.take_output(&produced);
for (const double value : produced) {
output_.push_back(static_cast<float>(value));
}
info_.frames_out++;
info_.samples_out += produced.size() / 2u;
return Status::success();
}
Status Stream::pull(float* destination, std::size_t capacity_samples, std::size_t* produced) {
if (produced != nullptr) {
*produced = 0;
}
if (destination == nullptr || produced == nullptr) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOutput, "null pull buffer");
}
const std::size_t available = buffered_samples();
const std::size_t count = std::min(capacity_samples, available);
if (count != 0u) {
std::memcpy(destination, output_.data() + read_offset_,
count * output_channels_ * sizeof(float));
read_offset_ += count * output_channels_;
if (read_offset_ == output_.size()) {
output_.clear();
read_offset_ = 0;
} else if (read_offset_ > (1u << 20)) {
output_.erase(output_.begin(),
output_.begin() + static_cast<std::ptrdiff_t>(read_offset_));
read_offset_ = 0;
}
}
*produced = static_cast<std::uint32_t>(count);
return Status::success();
}
Status Stream::flush() {
if (binaural_ready_) {
std::vector<double> tail;
const Status drained =
binaural_.finish(binaural_.finish_capacity(config_.tail_seconds), &tail);
if (!drained.ok()) {
return drained;
}
for (const double value : tail) {
output_.push_back(static_cast<float>(value));
}
info_.samples_out += tail.size() / 2u;
}
info_.ended = 1;
return Status::success();
}
Status Stream::reset() {
if (rebuilder_ == nullptr && !speaker_enabled_ && !binaural_ready_ &&
config_.input != JOC_STREAM_IN_PCM_OBJECTS16) {
return Status::fail(JOC_ERR_STATE, stage::kRender, "stream is not created");
}
reset_state();
return Status::success();
}
} // namespace joc::stream