parent
fbebc52511
commit
3511852f39
@ -0,0 +1,88 @@ |
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
* |
||||
* SPDX-License-Identifier: GPL-3.0-only |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <sys/_stdint.h> |
||||
#include <cstdint> |
||||
#include <memory> |
||||
|
||||
#include "samplerate.h" |
||||
|
||||
#include "audio_decoder.hpp" |
||||
#include "audio_sink.hpp" |
||||
#include "audio_source.hpp" |
||||
#include "codec.hpp" |
||||
#include "pipeline.hpp" |
||||
#include "stream_info.hpp" |
||||
|
||||
namespace audio { |
||||
|
||||
/*
|
||||
* Handles the final downmix + resample + quantisation stage of audio, |
||||
* generation sending the result directly to an IAudioSink. |
||||
*/ |
||||
class SinkMixer { |
||||
public: |
||||
SinkMixer(StreamBufferHandle_t dest); |
||||
~SinkMixer(); |
||||
|
||||
auto MixAndSend(InputStream&, const StreamInfo::Pcm&) -> std::size_t; |
||||
|
||||
private: |
||||
auto Main() -> void; |
||||
|
||||
auto SetTargetFormat(const StreamInfo::Pcm& format) -> void; |
||||
auto HandleBytes() -> void; |
||||
|
||||
template <typename T> |
||||
auto ConvertFixedToFloating(InputStream&, OutputStream&) -> void; |
||||
auto Resample(float, int, InputStream&, OutputStream&) -> void; |
||||
template <typename T> |
||||
auto Quantise(InputStream&) -> std::size_t; |
||||
|
||||
enum class Command { |
||||
kReadBytes, |
||||
kSetSourceFormat, |
||||
kSetTargetFormat, |
||||
}; |
||||
|
||||
struct Args { |
||||
Command cmd; |
||||
StreamInfo::Pcm format; |
||||
}; |
||||
|
||||
QueueHandle_t commands_; |
||||
SemaphoreHandle_t is_idle_; |
||||
|
||||
SRC_STATE* resampler_; |
||||
|
||||
std::unique_ptr<RawStream> input_stream_; |
||||
std::unique_ptr<RawStream> floating_point_stream_; |
||||
std::unique_ptr<RawStream> resampled_stream_; |
||||
|
||||
cpp::span<std::byte> quantisation_buffer_; |
||||
cpp::span<short> quantisation_buffer_as_shorts_; |
||||
cpp::span<int> quantisation_buffer_as_ints_; |
||||
|
||||
StreamInfo::Pcm target_format_; |
||||
StreamBufferHandle_t source_; |
||||
StreamBufferHandle_t sink_; |
||||
}; |
||||
|
||||
template <> |
||||
auto SinkMixer::ConvertFixedToFloating<short>(InputStream&, OutputStream&) |
||||
-> void; |
||||
template <> |
||||
auto SinkMixer::ConvertFixedToFloating<int>(InputStream&, OutputStream&) |
||||
-> void; |
||||
|
||||
template <> |
||||
auto SinkMixer::Quantise<short>(InputStream&) -> std::size_t; |
||||
template <> |
||||
auto SinkMixer::Quantise<int>(InputStream&) -> std::size_t; |
||||
|
||||
} // namespace audio
|
@ -0,0 +1,301 @@ |
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
* |
||||
* SPDX-License-Identifier: GPL-3.0-only |
||||
*/ |
||||
|
||||
#include "sink_mixer.hpp" |
||||
|
||||
#include <stdint.h> |
||||
#include <cmath> |
||||
|
||||
#include "esp_heap_caps.h" |
||||
#include "esp_log.h" |
||||
#include "freertos/portmacro.h" |
||||
#include "freertos/projdefs.h" |
||||
#include "samplerate.h" |
||||
|
||||
#include "stream_info.hpp" |
||||
#include "tasks.hpp" |
||||
|
||||
static constexpr char kTag[] = "mixer"; |
||||
|
||||
static constexpr std::size_t kSourceBufferLength = 4 * 1024; |
||||
static constexpr std::size_t kInputBufferLength = 4 * 1024; |
||||
static constexpr std::size_t kReformatBufferLength = 4 * 1024; |
||||
static constexpr std::size_t kResampleBufferLength = kReformatBufferLength; |
||||
static constexpr std::size_t kQuantisedBufferLength = 2 * 1024; |
||||
|
||||
namespace audio { |
||||
|
||||
SinkMixer::SinkMixer(StreamBufferHandle_t dest) |
||||
: commands_(xQueueCreate(1, sizeof(Args))), |
||||
is_idle_(xSemaphoreCreateBinary()), |
||||
resampler_(nullptr), |
||||
source_(xStreamBufferCreate(kSourceBufferLength, 1)), |
||||
sink_(dest) { |
||||
input_stream_.reset(new RawStream(kInputBufferLength)); |
||||
floating_point_stream_.reset(new RawStream(kReformatBufferLength)); |
||||
resampled_stream_.reset(new RawStream(kResampleBufferLength)); |
||||
|
||||
quantisation_buffer_ = { |
||||
reinterpret_cast<std::byte*>(heap_caps_malloc( |
||||
kQuantisedBufferLength, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)), |
||||
kQuantisedBufferLength}; |
||||
quantisation_buffer_as_ints_ = { |
||||
reinterpret_cast<int*>(quantisation_buffer_.data()), |
||||
quantisation_buffer_.size_bytes() / 4}; |
||||
quantisation_buffer_as_shorts_ = { |
||||
reinterpret_cast<short*>(quantisation_buffer_.data()), |
||||
quantisation_buffer_.size_bytes() / 2}; |
||||
|
||||
tasks::StartPersistent<tasks::Type::kMixer>([&]() { Main(); }); |
||||
} |
||||
|
||||
SinkMixer::~SinkMixer() { |
||||
vQueueDelete(commands_); |
||||
vSemaphoreDelete(is_idle_); |
||||
vStreamBufferDelete(source_); |
||||
heap_caps_free(quantisation_buffer_.data()); |
||||
if (resampler_ != nullptr) { |
||||
src_delete(resampler_); |
||||
} |
||||
} |
||||
|
||||
auto SinkMixer::MixAndSend(InputStream& input, const StreamInfo::Pcm& target) |
||||
-> std::size_t { |
||||
if (input.info().format_as<StreamInfo::Pcm>() != |
||||
input_stream_->info().format_as<StreamInfo::Pcm>()) { |
||||
xSemaphoreTake(is_idle_, portMAX_DELAY); |
||||
Args args{ |
||||
.cmd = Command::kSetSourceFormat, |
||||
.format = input.info().format_as<StreamInfo::Pcm>().value(), |
||||
}; |
||||
xQueueSend(commands_, &args, portMAX_DELAY); |
||||
xSemaphoreGive(is_idle_); |
||||
} |
||||
if (target_format_ != target) { |
||||
xSemaphoreTake(is_idle_, portMAX_DELAY); |
||||
Args args{ |
||||
.cmd = Command::kSetTargetFormat, |
||||
.format = target, |
||||
}; |
||||
xQueueSend(commands_, &args, portMAX_DELAY); |
||||
xSemaphoreGive(is_idle_); |
||||
} |
||||
|
||||
Args args{ |
||||
.cmd = Command::kReadBytes, |
||||
.format = {}, |
||||
}; |
||||
xQueueSend(commands_, &args, portMAX_DELAY); |
||||
|
||||
auto buf = input.data(); |
||||
std::size_t bytes_sent = |
||||
xStreamBufferSend(source_, buf.data(), buf.size_bytes(), portMAX_DELAY); |
||||
input.consume(bytes_sent); |
||||
return bytes_sent; |
||||
} |
||||
|
||||
auto SinkMixer::Main() -> void { |
||||
OutputStream input_receiver{input_stream_.get()}; |
||||
xSemaphoreGive(is_idle_); |
||||
|
||||
for (;;) { |
||||
Args args; |
||||
while (!xQueueReceive(commands_, &args, portMAX_DELAY)) { |
||||
} |
||||
switch (args.cmd) { |
||||
case Command::kSetSourceFormat: |
||||
ESP_LOGI(kTag, "setting source format"); |
||||
input_receiver.prepare(args.format, {}); |
||||
break; |
||||
case Command::kSetTargetFormat: |
||||
ESP_LOGI(kTag, "setting target format"); |
||||
target_format_ = args.format; |
||||
break; |
||||
case Command::kReadBytes: |
||||
xSemaphoreTake(is_idle_, 0); |
||||
while (!xStreamBufferIsEmpty(source_)) { |
||||
auto buf = input_receiver.data(); |
||||
std::size_t bytes_received = xStreamBufferReceive( |
||||
source_, buf.data(), buf.size_bytes(), portMAX_DELAY); |
||||
input_receiver.add(bytes_received); |
||||
HandleBytes(); |
||||
} |
||||
xSemaphoreGive(is_idle_); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
auto SinkMixer::HandleBytes() -> void { |
||||
InputStream input{input_stream_.get()}; |
||||
auto pcm = input.info().format_as<StreamInfo::Pcm>(); |
||||
if (!pcm) { |
||||
ESP_LOGE(kTag, "mixer got unsupported data"); |
||||
return; |
||||
} |
||||
|
||||
if (*pcm == target_format_) { |
||||
// The happiest possible case: the input format matches the output
|
||||
// format already. Streams like this should probably have bypassed the
|
||||
// mixer.
|
||||
// TODO(jacqueline): Make this an error; it's slow to use the mixer in this
|
||||
// case, compared to just writing directly to the sink.
|
||||
auto buf = input.data(); |
||||
std::size_t bytes_sent = |
||||
xStreamBufferSend(sink_, buf.data(), buf.size_bytes(), portMAX_DELAY); |
||||
input.consume(bytes_sent); |
||||
return; |
||||
} |
||||
|
||||
// Work out the resampling ratio using floating point arithmetic, since
|
||||
// relying on the FPU for this will be much faster, and the difference in
|
||||
// accuracy is unlikely to be noticeable.
|
||||
float src_ratio = static_cast<float>(target_format_.sample_rate) / |
||||
static_cast<float>(pcm->sample_rate); |
||||
|
||||
// Loop until we don't have any complete frames left in the input stream,
|
||||
// where a 'frame' is one complete sample per channel.
|
||||
while (!input_stream_->empty()) { |
||||
// The first step of both resampling and requantising is to convert the
|
||||
// fixed point pcm input data into 32 bit floating point samples.
|
||||
OutputStream floating_writer{floating_point_stream_.get()}; |
||||
if (pcm->bits_per_sample == 16) { |
||||
ConvertFixedToFloating<short>(input, floating_writer); |
||||
} else { |
||||
// FIXME: We should consider treating 24 bit and 32 bit samples
|
||||
// differently.
|
||||
ConvertFixedToFloating<int>(input, floating_writer); |
||||
} |
||||
|
||||
InputStream floating_reader{floating_point_stream_.get()}; |
||||
|
||||
while (!floating_point_stream_->empty()) { |
||||
RawStream* quantisation_source; |
||||
if (pcm->sample_rate != target_format_.sample_rate) { |
||||
// The input data needs to be resampled before being sent to the sink.
|
||||
OutputStream resample_writer{resampled_stream_.get()}; |
||||
Resample(src_ratio, pcm->channels, floating_reader, resample_writer); |
||||
quantisation_source = resampled_stream_.get(); |
||||
} else { |
||||
// The input data already has an acceptable sample rate. All we need to
|
||||
// do is quantise it.
|
||||
quantisation_source = floating_point_stream_.get(); |
||||
} |
||||
|
||||
InputStream quantise_reader{quantisation_source}; |
||||
while (!quantisation_source->empty()) { |
||||
std::size_t samples_available; |
||||
if (target_format_.bits_per_sample == 16) { |
||||
samples_available = Quantise<short>(quantise_reader); |
||||
} else { |
||||
samples_available = Quantise<int>(quantise_reader); |
||||
} |
||||
|
||||
assert(samples_available * target_format_.real_bytes_per_sample() <= |
||||
quantisation_buffer_.size_bytes()); |
||||
|
||||
std::size_t bytes_sent = xStreamBufferSend( |
||||
sink_, quantisation_buffer_.data(), |
||||
samples_available * target_format_.real_bytes_per_sample(), |
||||
portMAX_DELAY); |
||||
assert(bytes_sent == |
||||
samples_available * target_format_.real_bytes_per_sample()); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
template <> |
||||
auto SinkMixer::ConvertFixedToFloating<short>(InputStream& in_str, |
||||
OutputStream& out_str) -> void { |
||||
auto in = in_str.data_as<short>(); |
||||
auto out = out_str.data_as<float>(); |
||||
std::size_t samples_converted = std::min(in.size(), out.size()); |
||||
|
||||
src_short_to_float_array(in.data(), out.data(), samples_converted); |
||||
|
||||
in_str.consume(samples_converted * sizeof(short)); |
||||
out_str.add(samples_converted * sizeof(float)); |
||||
} |
||||
|
||||
template <> |
||||
auto SinkMixer::ConvertFixedToFloating<int>(InputStream& in_str, |
||||
OutputStream& out_str) -> void { |
||||
auto in = in_str.data_as<int>(); |
||||
auto out = out_str.data_as<float>(); |
||||
std::size_t samples_converted = std::min(in.size(), out.size()); |
||||
|
||||
src_int_to_float_array(in.data(), out.data(), samples_converted); |
||||
|
||||
in_str.consume(samples_converted * sizeof(int)); |
||||
out_str.add(samples_converted * sizeof(float)); |
||||
} |
||||
|
||||
auto SinkMixer::Resample(float src_ratio, |
||||
int channels, |
||||
InputStream& in, |
||||
OutputStream& out) -> void { |
||||
if (resampler_ == nullptr || src_get_channels(resampler_) != channels) { |
||||
if (resampler_ != nullptr) { |
||||
src_delete(resampler_); |
||||
} |
||||
|
||||
ESP_LOGI(kTag, "creating new resampler with %u channels", channels); |
||||
|
||||
int err = 0; |
||||
resampler_ = src_new(SRC_LINEAR, channels, &err); |
||||
assert(resampler_ != NULL); |
||||
assert(err == 0); |
||||
} |
||||
|
||||
auto in_buf = in.data_as<float>(); |
||||
auto out_buf = out.data_as<float>(); |
||||
|
||||
src_set_ratio(resampler_, src_ratio); |
||||
SRC_DATA args{ |
||||
.data_in = in_buf.data(), |
||||
.data_out = out_buf.data(), |
||||
.input_frames = static_cast<long>(in_buf.size()), |
||||
.output_frames = static_cast<long>(out_buf.size()), |
||||
.input_frames_used = 0, |
||||
.output_frames_gen = 0, |
||||
.end_of_input = 0, |
||||
.src_ratio = src_ratio, |
||||
}; |
||||
int err = src_process(resampler_, &args); |
||||
if (err != 0) { |
||||
ESP_LOGE(kTag, "resampler error: %s", src_strerror(err)); |
||||
} |
||||
|
||||
in.consume(args.input_frames_used * sizeof(float)); |
||||
out.add(args.output_frames_gen * sizeof(float)); |
||||
} |
||||
|
||||
template <> |
||||
auto SinkMixer::Quantise<short>(InputStream& in) -> std::size_t { |
||||
auto src = in.data_as<float>(); |
||||
cpp::span<short> dest = quantisation_buffer_as_shorts_; |
||||
dest = dest.first(std::min(src.size(), dest.size())); |
||||
|
||||
src_float_to_short_array(src.data(), dest.data(), dest.size()); |
||||
|
||||
in.consume(dest.size() * sizeof(float)); |
||||
return dest.size(); |
||||
} |
||||
|
||||
template <> |
||||
auto SinkMixer::Quantise<int>(InputStream& in) -> std::size_t { |
||||
auto src = in.data_as<float>(); |
||||
cpp::span<int> dest = quantisation_buffer_as_ints_; |
||||
dest = dest.first(std::min<int>(src.size(), dest.size())); |
||||
|
||||
src_float_to_int_array(src.data(), dest.data(), dest.size()); |
||||
|
||||
in.consume(dest.size() * sizeof(float)); |
||||
return dest.size(); |
||||
} |
||||
|
||||
} // namespace audio
|
Loading…
Reference in new issue