Fork of Tangara with customizations
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
tangara-fw/src/tangara/audio/processor.cpp

271 lines
8.6 KiB

/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#include "audio/processor.hpp"
#include <stdint.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <limits>
#include "audio/audio_events.hpp"
#include "audio/audio_sink.hpp"
#include "drivers/i2s_dac.hpp"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "events/event_queue.hpp"
#include "freertos/portmacro.h"
#include "freertos/projdefs.h"
#include "audio/resample.hpp"
#include "sample.hpp"
#include "tasks.hpp"
[[maybe_unused]] static constexpr char kTag[] = "mixer";
static constexpr std::size_t kSampleBufferLength =
drivers::kI2SBufferLengthFrames * sizeof(sample::Sample) * 2;
static constexpr std::size_t kSourceBufferLength = kSampleBufferLength * 2;
namespace audio {
SampleProcessor::SampleProcessor(StreamBufferHandle_t sink)
: commands_(xQueueCreate(1, sizeof(Args))),
resampler_(nullptr),
source_(xStreamBufferCreateWithCaps(kSourceBufferLength,
sizeof(sample::Sample) * 2,
MALLOC_CAP_DMA)),
sink_(sink),
leftover_bytes_(0),
samples_written_(0) {
input_buffer_ = {
reinterpret_cast<sample::Sample*>(heap_caps_calloc(
kSampleBufferLength, sizeof(sample::Sample), MALLOC_CAP_DMA)),
kSampleBufferLength};
input_buffer_as_bytes_ = {reinterpret_cast<std::byte*>(input_buffer_.data()),
input_buffer_.size_bytes()};
resampled_buffer_ = {
reinterpret_cast<sample::Sample*>(heap_caps_calloc(
kSampleBufferLength, sizeof(sample::Sample), MALLOC_CAP_DMA)),
kSampleBufferLength};
tasks::StartPersistent<tasks::Type::kAudioConverter>([&]() { Main(); });
}
SampleProcessor::~SampleProcessor() {
vQueueDelete(commands_);
vStreamBufferDelete(source_);
}
auto SampleProcessor::SetOutput(std::shared_ptr<IAudioOutput> output) -> void {
assert(xStreamBufferIsEmpty(sink_));
// FIXME: We should add synchronisation here, but we should be careful
// about not impacting performance given that the output will change only
// very rarely (if ever).
output_ = output;
samples_written_ = output_->samplesUsed();
}
auto SampleProcessor::beginStream(std::shared_ptr<TrackInfo> track) -> void {
Args args{
.track = new std::shared_ptr<TrackInfo>(track),
.samples_available = 0,
.is_end_of_stream = false,
.clear_buffers = false,
};
xQueueSend(commands_, &args, portMAX_DELAY);
}
auto SampleProcessor::continueStream(std::span<sample::Sample> input) -> void {
Args args{
.track = nullptr,
.samples_available = input.size(),
.is_end_of_stream = false,
.clear_buffers = false,
};
xQueueSend(commands_, &args, portMAX_DELAY);
xStreamBufferSend(source_, input.data(), input.size_bytes(), portMAX_DELAY);
}
auto SampleProcessor::endStream(bool cancelled) -> void {
Args args{
.track = nullptr,
.samples_available = 0,
.is_end_of_stream = true,
.clear_buffers = cancelled,
};
xQueueSend(commands_, &args, portMAX_DELAY);
}
auto SampleProcessor::Main() -> void {
for (;;) {
Args args;
while (!xQueueReceive(commands_, &args, portMAX_DELAY)) {
}
if (args.track) {
handleBeginStream(*args.track);
delete args.track;
}
if (args.samples_available) {
handleContinueStream(args.samples_available);
}
if (args.is_end_of_stream) {
handleEndStream(args.clear_buffers);
}
}
}
auto SampleProcessor::handleBeginStream(std::shared_ptr<TrackInfo> track)
-> void {
if (track->format != source_format_) {
source_format_ = track->format;
// The new stream has a different format to the previous stream (or there
// was no previous stream).
// First, clean up our filters.
resampler_.reset();
leftover_bytes_ = 0;
// If the output is idle, then we can reconfigure it to the closest format
// to our new source.
// If the output *wasn't* idle, then we can't reconfigure without an
// audible gap in playback. So instead, we simply keep the same target
// format and begin resampling.
if (xStreamBufferIsEmpty(sink_)) {
target_format_ = output_->PrepareFormat(track->format);
output_->Configure(target_format_);
}
}
if (xStreamBufferIsEmpty(sink_)) {
samples_written_ = output_->samplesUsed();
}
events::Audio().Dispatch(internal::StreamStarted{
.track = track,
.sink_format = target_format_,
.cue_at_sample = samples_written_,
});
}
auto SampleProcessor::handleContinueStream(size_t samples_available) -> void {
// Loop until we finish reading all the bytes indicated. There might be
// leftovers from each iteration, and from this process as a whole,
// depending on the resampling stage.
size_t bytes_read = 0;
size_t bytes_to_read = samples_available * sizeof(sample::Sample);
while (bytes_read < bytes_to_read) {
// First top up the input buffer, taking care not to overwrite anything
// remaining from a previous iteration.
size_t bytes_read_this_it = xStreamBufferReceive(
source_, input_buffer_as_bytes_.subspan(leftover_bytes_).data(),
std::min(input_buffer_as_bytes_.size() - leftover_bytes_,
bytes_to_read - bytes_read),
portMAX_DELAY);
bytes_read += bytes_read_this_it;
// Calculate the number of whole samples that are now in the input buffer.
size_t bytes_in_buffer = bytes_read_this_it + leftover_bytes_;
size_t samples_in_buffer = bytes_in_buffer / sizeof(sample::Sample);
size_t samples_used = handleSamples(input_buffer_.first(samples_in_buffer));
// Maybe the resampler didn't consume everything. Maybe the last few
// bytes we read were half a frame. Either way, we need to calculate the
// size of the remainder in bytes, then move it to the front of our
// buffer.
size_t bytes_used = samples_used * sizeof(sample::Sample);
assert(bytes_used <= bytes_in_buffer);
leftover_bytes_ = bytes_in_buffer - bytes_used;
if (leftover_bytes_ > 0) {
std::memmove(input_buffer_as_bytes_.data(),
input_buffer_as_bytes_.data() + bytes_used, leftover_bytes_);
}
}
}
auto SampleProcessor::handleSamples(std::span<sample::Sample> input) -> size_t {
if (source_format_ == target_format_) {
// The happiest possible case: the input format matches the output
// format already.
sendToSink(input);
return input.size();
}
size_t samples_used = 0;
while (samples_used < input.size()) {
std::span<sample::Sample> output_source;
if (source_format_.sample_rate != target_format_.sample_rate) {
if (resampler_ == nullptr) {
ESP_LOGI(kTag, "creating new resampler for %lu -> %lu",
source_format_.sample_rate, target_format_.sample_rate);
resampler_.reset(new Resampler(source_format_.sample_rate,
target_format_.sample_rate,
source_format_.num_channels));
}
size_t read, written;
std::tie(read, written) = resampler_->Process(input.subspan(samples_used),
resampled_buffer_, false);
samples_used += read;
if (read == 0 && written == 0) {
// Zero samples used or written. We need more input.
break;
}
output_source = resampled_buffer_.first(written);
} else {
output_source = input;
samples_used = input.size();
}
sendToSink(output_source);
}
return samples_used;
}
auto SampleProcessor::handleEndStream(bool clear_bufs) -> void {
if (resampler_ && !clear_bufs) {
size_t read, written;
std::tie(read, written) = resampler_->Process({}, resampled_buffer_, true);
if (written > 0) {
sendToSink(resampled_buffer_.first(written));
}
}
if (clear_bufs) {
assert(xStreamBufferReset(sink_));
samples_written_ = output_->samplesUsed();
}
// FIXME: This discards any leftover samples, but there probably shouldn't be
// any leftover samples. Can this be an assert instead?
leftover_bytes_ = 0;
events::Audio().Dispatch(internal::StreamEnded{
.cue_at_sample = samples_written_,
});
}
auto SampleProcessor::sendToSink(std::span<sample::Sample> samples) -> void {
auto data = std::as_bytes(samples);
xStreamBufferSend(sink_, data.data(), data.size(), portMAX_DELAY);
uint32_t samples_before_overflow =
std::numeric_limits<uint32_t>::max() - samples_written_;
if (samples_before_overflow < samples.size()) {
samples_written_ = samples.size() - samples_before_overflow;
} else {
samples_written_ += samples.size();
}
}
} // namespace audio