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.
265 lines
8.3 KiB
265 lines
8.3 KiB
2 years ago
|
/*
|
||
|
* Copyright 2023 jacqueline <me@jacqueline.id.au>
|
||
|
*
|
||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||
|
*/
|
||
|
|
||
12 months ago
|
#include "audio/processor.hpp"
|
||
2 years ago
|
|
||
2 years ago
|
#include <algorithm>
|
||
2 years ago
|
#include <cmath>
|
||
2 years ago
|
#include <cstdint>
|
||
2 years ago
|
|
||
1 year ago
|
#include "audio/audio_events.hpp"
|
||
|
#include "audio/audio_sink.hpp"
|
||
1 year ago
|
#include "drivers/i2s_dac.hpp"
|
||
2 years ago
|
#include "esp_heap_caps.h"
|
||
|
#include "esp_log.h"
|
||
1 year ago
|
#include "events/event_queue.hpp"
|
||
2 years ago
|
#include "freertos/portmacro.h"
|
||
|
#include "freertos/projdefs.h"
|
||
2 years ago
|
|
||
1 year ago
|
#include "audio/resample.hpp"
|
||
2 years ago
|
#include "sample.hpp"
|
||
2 years ago
|
#include "tasks.hpp"
|
||
|
|
||
2 years ago
|
[[maybe_unused]] static constexpr char kTag[] = "mixer";
|
||
2 years ago
|
|
||
2 years ago
|
static constexpr std::size_t kSampleBufferLength =
|
||
1 year ago
|
drivers::kI2SBufferLengthFrames * sizeof(sample::Sample) * 2;
|
||
2 years ago
|
static constexpr std::size_t kSourceBufferLength = kSampleBufferLength * 2;
|
||
2 years ago
|
|
||
|
namespace audio {
|
||
|
|
||
12 months ago
|
SampleProcessor::SampleProcessor()
|
||
2 years ago
|
: commands_(xQueueCreate(1, sizeof(Args))),
|
||
|
resampler_(nullptr),
|
||
2 years ago
|
source_(xStreamBufferCreateWithCaps(kSourceBufferLength,
|
||
2 years ago
|
sizeof(sample::Sample) * 2,
|
||
1 year ago
|
MALLOC_CAP_DMA)),
|
||
|
leftover_bytes_(0),
|
||
|
samples_sunk_(0) {
|
||
2 years ago
|
input_buffer_ = {
|
||
|
reinterpret_cast<sample::Sample*>(heap_caps_calloc(
|
||
2 years ago
|
kSampleBufferLength, sizeof(sample::Sample), MALLOC_CAP_DMA)),
|
||
2 years ago
|
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(
|
||
2 years ago
|
kSampleBufferLength, sizeof(sample::Sample), MALLOC_CAP_DMA)),
|
||
2 years ago
|
kSampleBufferLength};
|
||
2 years ago
|
|
||
2 years ago
|
tasks::StartPersistent<tasks::Type::kAudioConverter>([&]() { Main(); });
|
||
2 years ago
|
}
|
||
|
|
||
12 months ago
|
SampleProcessor::~SampleProcessor() {
|
||
2 years ago
|
vQueueDelete(commands_);
|
||
|
vStreamBufferDelete(source_);
|
||
|
}
|
||
|
|
||
12 months ago
|
auto SampleProcessor::SetOutput(std::shared_ptr<IAudioOutput> output) -> void {
|
||
2 years ago
|
// 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).
|
||
|
sink_ = output;
|
||
|
}
|
||
|
|
||
12 months ago
|
auto SampleProcessor::beginStream(std::shared_ptr<TrackInfo> track) -> void {
|
||
2 years ago
|
Args args{
|
||
1 year ago
|
.track = new std::shared_ptr<TrackInfo>(track),
|
||
|
.samples_available = 0,
|
||
|
.is_end_of_stream = false,
|
||
|
};
|
||
|
xQueueSend(commands_, &args, portMAX_DELAY);
|
||
|
}
|
||
|
|
||
12 months ago
|
auto SampleProcessor::continueStream(std::span<sample::Sample> input) -> void {
|
||
1 year ago
|
Args args{
|
||
|
.track = nullptr,
|
||
2 years ago
|
.samples_available = input.size(),
|
||
1 year ago
|
.is_end_of_stream = false,
|
||
2 years ago
|
};
|
||
|
xQueueSend(commands_, &args, portMAX_DELAY);
|
||
1 year ago
|
xStreamBufferSend(source_, input.data(), input.size_bytes(), portMAX_DELAY);
|
||
|
}
|
||
2 years ago
|
|
||
12 months ago
|
auto SampleProcessor::endStream() -> void {
|
||
1 year ago
|
Args args{
|
||
|
.track = nullptr,
|
||
|
.samples_available = 0,
|
||
|
.is_end_of_stream = true,
|
||
|
};
|
||
|
xQueueSend(commands_, &args, portMAX_DELAY);
|
||
2 years ago
|
}
|
||
|
|
||
12 months ago
|
auto SampleProcessor::Main() -> void {
|
||
2 years ago
|
for (;;) {
|
||
|
Args args;
|
||
|
while (!xQueueReceive(commands_, &args, portMAX_DELAY)) {
|
||
|
}
|
||
1 year ago
|
|
||
1 year ago
|
if (args.track) {
|
||
|
handleBeginStream(*args.track);
|
||
|
delete args.track;
|
||
|
}
|
||
|
if (args.samples_available) {
|
||
|
handleContinueStream(args.samples_available);
|
||
|
}
|
||
|
if (args.is_end_of_stream) {
|
||
|
handleEndStream();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
12 months ago
|
auto SampleProcessor::handleBeginStream(std::shared_ptr<TrackInfo> track)
|
||
1 year ago
|
-> void {
|
||
|
if (track->format != source_format_) {
|
||
|
resampler_.reset();
|
||
|
source_format_ = track->format;
|
||
|
leftover_bytes_ = 0;
|
||
|
|
||
|
auto new_target = sink_->PrepareFormat(track->format);
|
||
|
if (new_target != target_format_) {
|
||
|
// The new format is different to the old one. Wait for the sink to
|
||
|
// drain before continuing.
|
||
|
while (!xStreamBufferIsEmpty(sink_->stream())) {
|
||
|
ESP_LOGI(kTag, "waiting for sink stream to drain...");
|
||
|
// TODO(jacqueline): Get the sink drain ISR to notify us of this
|
||
|
// via semaphore instead of busy-ish waiting.
|
||
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||
1 year ago
|
}
|
||
|
|
||
1 year ago
|
sink_->Configure(new_target);
|
||
2 years ago
|
}
|
||
1 year ago
|
target_format_ = new_target;
|
||
|
}
|
||
2 years ago
|
|
||
1 year ago
|
samples_sunk_ = 0;
|
||
|
events::Audio().Dispatch(internal::StreamStarted{
|
||
|
.track = track,
|
||
|
.src_format = source_format_,
|
||
|
.dst_format = target_format_,
|
||
|
});
|
||
|
}
|
||
|
|
||
12 months ago
|
auto SampleProcessor::handleContinueStream(size_t samples_available) -> void {
|
||
1 year ago
|
// 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_);
|
||
2 years ago
|
}
|
||
2 years ago
|
}
|
||
2 years ago
|
}
|
||
2 years ago
|
|
||
12 months ago
|
auto SampleProcessor::handleSamples(std::span<sample::Sample> input) -> size_t {
|
||
2 years ago
|
if (source_format_ == target_format_) {
|
||
2 years ago
|
// The happiest possible case: the input format matches the output
|
||
2 years ago
|
// format already.
|
||
1 year ago
|
sendToSink(input);
|
||
1 year ago
|
return input.size();
|
||
2 years ago
|
}
|
||
|
|
||
2 years ago
|
size_t samples_used = 0;
|
||
2 years ago
|
while (samples_used < input.size()) {
|
||
1 year ago
|
std::span<sample::Sample> output_source;
|
||
2 years ago
|
if (source_format_.sample_rate != target_format_.sample_rate) {
|
||
|
if (resampler_ == nullptr) {
|
||
2 years ago
|
ESP_LOGI(kTag, "creating new resampler for %lu -> %lu",
|
||
|
source_format_.sample_rate, target_format_.sample_rate);
|
||
2 years ago
|
resampler_.reset(new Resampler(source_format_.sample_rate,
|
||
|
target_format_.sample_rate,
|
||
|
source_format_.num_channels));
|
||
|
}
|
||
|
|
||
|
size_t read, written;
|
||
2 years ago
|
std::tie(read, written) = resampler_->Process(input.subspan(samples_used),
|
||
1 year ago
|
resampled_buffer_, false);
|
||
2 years ago
|
samples_used += read;
|
||
2 years ago
|
|
||
2 years ago
|
if (read == 0 && written == 0) {
|
||
2 years ago
|
// Zero samples used or written. We need more input.
|
||
|
break;
|
||
|
}
|
||
2 years ago
|
output_source = resampled_buffer_.first(written);
|
||
2 years ago
|
} else {
|
||
2 years ago
|
output_source = input;
|
||
|
samples_used = input.size();
|
||
2 years ago
|
}
|
||
|
|
||
1 year ago
|
sendToSink(output_source);
|
||
2 years ago
|
}
|
||
1 year ago
|
|
||
2 years ago
|
return samples_used;
|
||
2 years ago
|
}
|
||
|
|
||
12 months ago
|
auto SampleProcessor::handleEndStream() -> void {
|
||
1 year ago
|
if (resampler_) {
|
||
|
size_t read, written;
|
||
|
std::tie(read, written) = resampler_->Process({}, resampled_buffer_, true);
|
||
|
|
||
|
if (written > 0) {
|
||
|
sendToSink(resampled_buffer_.first(written));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Send a final update to finish off this stream's samples.
|
||
|
if (samples_sunk_ > 0) {
|
||
|
events::Audio().Dispatch(internal::StreamUpdate{
|
||
|
.samples_sunk = samples_sunk_,
|
||
|
});
|
||
|
samples_sunk_ = 0;
|
||
|
}
|
||
1 year ago
|
leftover_bytes_ = 0;
|
||
1 year ago
|
|
||
|
events::Audio().Dispatch(internal::StreamEnded{});
|
||
|
}
|
||
|
|
||
12 months ago
|
auto SampleProcessor::sendToSink(std::span<sample::Sample> samples) -> void {
|
||
1 year ago
|
// Update the number of samples sunk so far *before* actually sinking them,
|
||
|
// since writing to the stream buffer will block when the buffer gets full.
|
||
|
samples_sunk_ += samples.size();
|
||
|
if (samples_sunk_ >=
|
||
|
target_format_.sample_rate * target_format_.num_channels) {
|
||
1 year ago
|
events::Audio().Dispatch(internal::StreamUpdate{
|
||
1 year ago
|
.samples_sunk = samples_sunk_,
|
||
|
});
|
||
|
samples_sunk_ = 0;
|
||
|
}
|
||
|
|
||
|
xStreamBufferSend(sink_->stream(),
|
||
|
reinterpret_cast<std::byte*>(samples.data()),
|
||
|
samples.size_bytes(), portMAX_DELAY);
|
||
|
}
|
||
|
|
||
2 years ago
|
} // namespace audio
|