297 lines
10 KiB
C++
297 lines
10 KiB
C++
#include "./AudioOutput.h"
|
|
#include "./AudioMerger.h"
|
|
#include "./AudioResampler.h"
|
|
#include "./AudioInterleaved.h"
|
|
#include "./AudioGain.h"
|
|
#include "./processing/AudioProcessor.h"
|
|
#include "../logger.h"
|
|
#include <cstring>
|
|
#include <algorithm>
|
|
#include <string>
|
|
|
|
using namespace std;
|
|
using namespace tc;
|
|
using namespace tc::audio;
|
|
|
|
AudioOutput::AudioOutput(size_t channels, size_t rate) : channel_count_{channels}, sample_rate_{rate} {
|
|
assert(this->sample_rate_ % kChunkTimeMs == 0);
|
|
}
|
|
|
|
AudioOutput::~AudioOutput() {
|
|
this->close_device();
|
|
this->cleanup_buffers();
|
|
}
|
|
|
|
std::shared_ptr<AudioOutputSource> AudioOutput::create_source(ssize_t buf) {
|
|
auto result = std::shared_ptr<AudioOutputSource>(new AudioOutputSource(this->channel_count_, this->sample_rate_, buf));
|
|
{
|
|
std::lock_guard source_lock{this->sources_mutex};
|
|
this->sources_.push_back(result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void AudioOutput::register_audio_processor(const std::shared_ptr<AudioProcessor> &processor) {
|
|
std::lock_guard processor_lock{this->processors_mutex};
|
|
this->audio_processors_.push_back(processor);
|
|
}
|
|
|
|
bool AudioOutput::unregister_audio_processor(const std::shared_ptr<AudioProcessor> &processor) {
|
|
std::lock_guard processor_lock{this->processors_mutex};
|
|
auto index = std::find(this->audio_processors_.begin(), this->audio_processors_.end(), processor);
|
|
if(index == this->audio_processors_.end()) {
|
|
return false;
|
|
}
|
|
|
|
this->audio_processors_.erase(index);
|
|
return true;
|
|
}
|
|
|
|
void AudioOutput::cleanup_buffers() {
|
|
free(this->chunk_buffer);
|
|
free(this->source_merge_buffer);
|
|
|
|
this->source_merge_buffer = nullptr;
|
|
this->source_merge_buffer_length = 0;
|
|
|
|
this->chunk_buffer = nullptr;
|
|
this->chunk_buffer_length = 0;
|
|
}
|
|
|
|
void AudioOutput::ensure_chunk_buffer_space(size_t output_samples) {
|
|
const auto own_chunk_size = (AudioOutput::kChunkTimeMs * this->sample_rate_ * this->channel_count_) / 1000;
|
|
const auto min_chunk_byte_size = std::max(own_chunk_size, output_samples * this->current_output_channels) * sizeof(float);
|
|
|
|
if(this->chunk_buffer_length < min_chunk_byte_size) {
|
|
if(this->chunk_buffer) {
|
|
::free(this->chunk_buffer);
|
|
}
|
|
|
|
this->chunk_buffer = malloc(min_chunk_byte_size);
|
|
this->chunk_buffer_length = min_chunk_byte_size;
|
|
}
|
|
}
|
|
|
|
void AudioOutput::fill_buffer(void *output, size_t request_sample_count, size_t out_channels) {
|
|
assert(output);
|
|
|
|
if(out_channels != this->current_output_channels) {
|
|
log_info(category::audio, tr("Output channel count changed from {} to {}"), this->current_output_channels, out_channels);
|
|
this->current_output_channels = out_channels;
|
|
|
|
/*
|
|
* Mark buffer as fully replayed and refill it with new data which fits the new channel count.
|
|
*/
|
|
this->chunk_buffer_sample_length = 0;
|
|
this->chunk_buffer_sample_offset = 0;
|
|
}
|
|
|
|
auto remaining_samples{request_sample_count};
|
|
auto remaining_buffer{output};
|
|
|
|
if(this->chunk_buffer_sample_offset < this->chunk_buffer_sample_length) {
|
|
/*
|
|
* We can (partially) fill the output buffer with our current chunk.
|
|
*/
|
|
|
|
const auto sample_count = std::min(this->chunk_buffer_sample_length - this->chunk_buffer_sample_offset, request_sample_count);
|
|
memcpy(output, (float*) this->chunk_buffer + this->chunk_buffer_sample_offset * this->current_output_channels, sample_count * this->current_output_channels * sizeof(float));
|
|
this->chunk_buffer_sample_offset += sample_count;
|
|
|
|
if(sample_count == request_sample_count) {
|
|
/* We've successfully willed the whole output buffer. */
|
|
return;
|
|
}
|
|
|
|
remaining_samples = request_sample_count - sample_count;
|
|
remaining_buffer = (float*) output + sample_count * this->current_output_channels;
|
|
}
|
|
|
|
this->fill_chunk_buffer();
|
|
this->chunk_buffer_sample_offset = 0;
|
|
|
|
return this->fill_buffer(remaining_buffer, remaining_samples, out_channels);
|
|
}
|
|
|
|
constexpr static auto kTempChunkBufferSize{64 * 1024};
|
|
constexpr static auto kMaxChannelCount{32};
|
|
void AudioOutput::fill_chunk_buffer() {
|
|
|
|
const auto chunk_local_sample_count = this->chunk_local_sample_count();
|
|
assert(chunk_local_sample_count > 0);
|
|
assert(this->current_output_channels <= kMaxChannelCount);
|
|
|
|
std::vector<std::shared_ptr<AudioOutputSource>> sources{};
|
|
sources.reserve(8);
|
|
|
|
std::unique_lock sources_lock{this->sources_mutex};
|
|
{
|
|
sources.reserve(this->sources_.size());
|
|
this->sources_.erase(std::remove_if(this->sources_.begin(), this->sources_.end(), [&](const std::weak_ptr<AudioOutputSource>& weak_source) {
|
|
auto source = weak_source.lock();
|
|
if(!source) {
|
|
return true;
|
|
}
|
|
|
|
sources.push_back(std::move(source));
|
|
return false;
|
|
}), this->sources_.end());
|
|
}
|
|
|
|
{
|
|
size_t actual_sources{0};
|
|
auto sources_it = sources.begin();
|
|
auto sources_end = sources.end();
|
|
|
|
/* Initialize the buffer */
|
|
while(sources_it != sources_end) {
|
|
auto source = *sources_it;
|
|
sources_it++;
|
|
|
|
if(source->pop_samples(this->chunk_buffer, chunk_local_sample_count)) {
|
|
/* Chunk buffer initialized */
|
|
actual_sources++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!actual_sources) {
|
|
/* We don't have any sources. Just provide silence */
|
|
sources_lock.unlock();
|
|
goto zero_chunk_exit;
|
|
}
|
|
|
|
/* Lets merge the rest */
|
|
uint8_t temp_chunk_buffer[kTempChunkBufferSize];
|
|
assert(kTempChunkBufferSize >= chunk_local_sample_count * this->channel_count_ * sizeof(float));
|
|
|
|
while(sources_it != sources_end) {
|
|
auto source = *sources_it;
|
|
sources_it++;
|
|
|
|
if(!source->pop_samples(temp_chunk_buffer, chunk_local_sample_count)) {
|
|
continue;
|
|
}
|
|
|
|
actual_sources++;
|
|
merge::merge_sources(this->chunk_buffer, this->chunk_buffer, temp_chunk_buffer, this->channel_count_, chunk_local_sample_count);
|
|
}
|
|
}
|
|
sources_lock.unlock();
|
|
|
|
if(this->volume_modifier == 0) {
|
|
goto zero_chunk_exit;
|
|
} else {
|
|
audio::apply_gain(this->chunk_buffer, this->channel_count_, chunk_local_sample_count, this->volume_modifier);
|
|
}
|
|
|
|
/* Lets resample our chunk with our sample rate up/down to the device sample rate */
|
|
if(this->resampler_) {
|
|
this->chunk_buffer_sample_length = this->resampler_->estimated_output_size(chunk_local_sample_count);
|
|
this->ensure_chunk_buffer_space(this->chunk_buffer_sample_length);
|
|
|
|
if(!this->resampler_->process(this->chunk_buffer, this->chunk_buffer, chunk_local_sample_count, this->chunk_buffer_sample_length)) {
|
|
log_error(category::audio, tr("Failed to resample audio output."));
|
|
goto zero_chunk_exit;
|
|
}
|
|
|
|
if(!this->chunk_buffer_sample_length) {
|
|
/* We need more input to create some resampled output */
|
|
log_warn(category::audio, tr("Audio output resampling returned zero samples"));
|
|
return;
|
|
}
|
|
} else {
|
|
this->chunk_buffer_sample_length = chunk_local_sample_count;
|
|
}
|
|
|
|
/* Increase/decrease channel count */
|
|
if(this->channel_count_ != this->current_output_channels) {
|
|
if(!merge::merge_channels_interleaved(this->chunk_buffer, this->current_output_channels, this->chunk_buffer, this->channel_count_, this->chunk_buffer_sample_length)) {
|
|
log_error(category::audio, tr("Failed to adjust channel count for audio output."));
|
|
goto zero_chunk_exit;
|
|
}
|
|
}
|
|
|
|
{
|
|
std::unique_lock processor_lock{this->processors_mutex};
|
|
auto processors = this->audio_processors_;
|
|
processor_lock.unlock();
|
|
|
|
if(!processors.empty()) {
|
|
float temp_chunk_buffer[kTempChunkBufferSize / sizeof(float)];
|
|
assert(kTempChunkBufferSize >= this->current_output_channels * this->chunk_buffer_sample_length * sizeof(float));
|
|
|
|
audio::deinterleave(temp_chunk_buffer, (const float*) this->chunk_buffer, this->current_output_channels, this->chunk_buffer_sample_length);
|
|
webrtc::StreamConfig stream_config{(int) this->playback_->sample_rate(), this->current_output_channels};
|
|
|
|
float* channel_ptr[kMaxChannelCount];
|
|
for(size_t channel{0}; channel < this->current_output_channels; channel++) {
|
|
channel_ptr[channel] = temp_chunk_buffer + (channel * this->chunk_buffer_sample_length);
|
|
}
|
|
|
|
for(const std::shared_ptr<AudioProcessor>& processor : processors) {
|
|
processor->analyze_reverse_stream(channel_ptr, stream_config);
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
zero_chunk_exit: {
|
|
this->chunk_buffer_sample_length = (this->playback_->sample_rate() * kChunkTimeMs) / 1000;
|
|
this->ensure_chunk_buffer_space(this->chunk_buffer_sample_length);
|
|
memset(this->chunk_buffer, 0, this->chunk_buffer_sample_length * this->current_output_channels * sizeof(float));
|
|
return;
|
|
};
|
|
}
|
|
|
|
void AudioOutput::set_device(const std::shared_ptr<AudioDevice> &new_device) {
|
|
lock_guard lock(this->device_lock);
|
|
if(this->device == new_device) {
|
|
return;
|
|
}
|
|
|
|
this->close_device();
|
|
this->device = new_device;
|
|
}
|
|
|
|
void AudioOutput::close_device() {
|
|
lock_guard lock(this->device_lock);
|
|
if(this->playback_) {
|
|
this->playback_->remove_source(this);
|
|
this->playback_->stop_if_possible();
|
|
this->playback_.reset();
|
|
}
|
|
|
|
this->resampler_ = nullptr;
|
|
this->device = nullptr;
|
|
}
|
|
|
|
bool AudioOutput::playback(std::string& error) {
|
|
lock_guard lock(this->device_lock);
|
|
if(!this->device) {
|
|
error = "invalid device handle";
|
|
return false;
|
|
}
|
|
if(this->playback_) {
|
|
return true;
|
|
}
|
|
|
|
this->playback_ = this->device->playback();
|
|
if(!this->playback_) {
|
|
error = "failed to allocate memory";
|
|
return false;
|
|
}
|
|
|
|
if(this->playback_->sample_rate() != this->sample_rate()) {
|
|
this->resampler_ = std::make_unique<AudioResampler>(this->sample_rate(), this->playback_->sample_rate(), this->channel_count());
|
|
if(!this->resampler_->valid()) {
|
|
error = "failed to allocate a resampler";
|
|
this->playback_ = nullptr;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
this->ensure_chunk_buffer_space(0);
|
|
this->playback_->register_source(this);
|
|
return this->playback_->start(error);
|
|
} |