481 lines
19 KiB
C++
481 lines
19 KiB
C++
#include "./AudioOutput.h"
|
|
#include "./AudioMerger.h"
|
|
#include "./AudioResampler.h"
|
|
#include "../logger.h"
|
|
#include <cstring>
|
|
#include <algorithm>
|
|
#include <string>
|
|
|
|
using namespace std;
|
|
using namespace tc;
|
|
using namespace tc::audio;
|
|
|
|
void AudioOutputSource::clear() {
|
|
this->buffer.clear();
|
|
this->buffering = true;
|
|
this->fade_in_start = this->buffer.write_ptr();
|
|
}
|
|
|
|
void AudioOutputSource::do_fade_out(size_t pop_count) {
|
|
if(this->will_buffer_in != -1) return;
|
|
|
|
_test_for_fade:
|
|
const auto samples_left = this->current_latency();
|
|
if(samples_left < this->fadeout_sample_length + pop_count) {
|
|
if(auto fn = this->on_underflow; fn && fn(0))
|
|
goto _test_for_fade;
|
|
|
|
auto total_samples = std::min(samples_left, this->fadeout_sample_length);
|
|
if(total_samples == 0) return; //TODO Test against min_buffered_samples
|
|
|
|
auto wptr = (float*) this->buffer.calculate_backward_write_ptr(total_samples * this->channel_count * sizeof(float));
|
|
for(size_t index{0}; index <= total_samples; index++) {
|
|
const auto offset = (float) ((float) index / (float) total_samples);
|
|
const auto volume = log10f(offset) / -2.71828182845904f;
|
|
for(int channel{0}; channel < this->channel_count; channel++)
|
|
*wptr++ *= volume;
|
|
}
|
|
|
|
log_trace(category::audio, tr("Will buffer due to fade out ({} | {})"), total_samples, *(float*) this->buffer.write_ptr());
|
|
this->will_buffer_in = total_samples;
|
|
}
|
|
}
|
|
|
|
void AudioOutputSource::do_fade_in() {
|
|
if(!this->fade_in_start)
|
|
return;
|
|
|
|
const auto samples_available = this->current_latency();
|
|
auto wptr = (float*) this->fade_in_start;
|
|
auto total_samples = std::min(samples_available, this->fadeout_sample_length);
|
|
if(total_samples == 0) {
|
|
log_trace(category::audio, tr("Ignoring fade in 0: {} {}"), samples_available, this->fadeout_sample_length);
|
|
return;
|
|
}
|
|
|
|
for(size_t index{0}; index < total_samples; index++) {
|
|
const auto offset = (float) ((float) index / (float) total_samples);
|
|
const auto volume = log10f(1 - offset) / -2.71828182845904f;
|
|
for(int channel{0}; channel < this->channel_count; channel++)
|
|
*wptr++ *= volume;
|
|
}
|
|
|
|
log_trace(category::audio, tr("Fade in to new buffer ({})"), total_samples);
|
|
|
|
this->fade_in_start = nullptr;
|
|
}
|
|
|
|
ssize_t AudioOutputSource::pop_samples(void *buffer, size_t samples) {
|
|
size_t written{0}, written_bytes{0};
|
|
|
|
load_buffer:
|
|
auto available_bytes = this->buffer.fill_count();
|
|
if(available_bytes < sizeof(float) * this->channel_count) return written;
|
|
auto available_samples = available_bytes / sizeof(float) / this->channel_count;
|
|
|
|
if(this->buffering && available_samples < this->min_buffered_samples) return -2;
|
|
this->do_fade_in();
|
|
this->do_fade_out(samples); /* will also call for underflow */
|
|
//log_trace(category::audio, tr("Min: {}, Max: {}, Current: {}, Buffering: {} Required: {}, left: {}, will buffer in {}"), this->min_buffered_samples, this->max_buffered_samples, available_samples, this->buffering, samples, (int) available_samples - samples, this->will_buffer_in);
|
|
|
|
if(this->will_buffer_in > 0) {
|
|
if(samples > this->will_buffer_in) {
|
|
samples = this->will_buffer_in;
|
|
this->buffering = true;
|
|
this->fade_in_start = this->buffer.calculate_advanced_write_ptr(samples * sizeof(float) * this->channel_count);
|
|
this->will_buffer_in = -1;
|
|
log_trace(category::audio, tr("Start buffering due to fade out. Fade in ptr {}"), (void*) this->fade_in_start);
|
|
} else {
|
|
this->will_buffer_in -= samples;
|
|
}
|
|
} else {
|
|
this->buffering = false;
|
|
}
|
|
|
|
if(available_samples >= samples - written) {
|
|
const auto byte_length = (samples - written) * sizeof(float) * this->channel_count;
|
|
if(buffer)memcpy((char*) buffer + written_bytes, this->buffer.read_ptr(), byte_length);
|
|
this->buffer.advance_read_ptr(byte_length);
|
|
|
|
if(this->on_read)
|
|
this->on_read();
|
|
|
|
return samples;
|
|
} else {
|
|
const auto byte_length = available_samples * sizeof(float) * this->channel_count;
|
|
if(buffer) memcpy((char*) buffer + written_bytes, this->buffer.read_ptr(), byte_length);
|
|
this->buffer.advance_read_ptr(byte_length);
|
|
written += available_samples;
|
|
written_bytes += byte_length;
|
|
}
|
|
|
|
|
|
if(auto fn = this->on_underflow; fn)
|
|
if(fn(samples - written))
|
|
goto load_buffer;
|
|
|
|
if(buffer)
|
|
memset((char*) buffer + written_bytes, 0, (samples - written) * sizeof(float) * this->channel_count);
|
|
|
|
this->buffering = true;
|
|
this->fade_in_start = this->buffer.write_ptr();
|
|
log_trace(category::audio, tr("Start buffering due to underflow."), (void*) this->fade_in_start);
|
|
this->will_buffer_in = -1;
|
|
|
|
if(this->on_read)
|
|
this->on_read();
|
|
return written; /* return the written samples */
|
|
}
|
|
|
|
ssize_t AudioOutputSource::enqueue_silence(size_t samples) {
|
|
size_t enqueued{0};
|
|
|
|
auto free_bytes = this->buffer.free_count();
|
|
auto free_samples = free_bytes / sizeof(float) / this->channel_count;
|
|
if(this->max_buffered_samples && free_samples > this->max_buffered_samples) free_samples = this->max_buffered_samples;
|
|
|
|
if(free_samples >= samples) {
|
|
const auto byte_length = samples * sizeof(float) * this->channel_count;
|
|
memset(this->buffer.write_ptr(), 0, byte_length);
|
|
this->buffer.advance_write_ptr(byte_length);
|
|
return samples;
|
|
} else {
|
|
const auto byte_length = free_samples * sizeof(float) * this->channel_count;
|
|
memset(this->buffer.write_ptr(), 0, byte_length);
|
|
this->buffer.advance_write_ptr(byte_length);
|
|
enqueued += free_samples;
|
|
}
|
|
|
|
if(auto fn = this->on_overflow; fn)
|
|
fn(samples - enqueued);
|
|
|
|
switch (this->overflow_strategy) {
|
|
case overflow_strategy::discard_input:
|
|
return -2;
|
|
case overflow_strategy::discard_buffer_all:
|
|
this->buffer.clear();
|
|
break;
|
|
case overflow_strategy::discard_buffer_half:
|
|
this->buffer.advance_read_ptr(this->buffer.fill_count() / 2);
|
|
break;
|
|
case overflow_strategy::ignore:
|
|
break;
|
|
}
|
|
|
|
this->fade_in_start = this->buffer.write_ptr(); /* so we fade in from silence */
|
|
return enqueued;
|
|
}
|
|
|
|
ssize_t AudioOutputSource::enqueue_samples(const void *buffer, size_t samples) {
|
|
size_t enqueued{0};
|
|
|
|
auto free_bytes = this->buffer.free_count();
|
|
auto free_samples = free_bytes / sizeof(float) / this->channel_count;
|
|
if(this->max_buffered_samples && free_samples > this->max_buffered_samples) free_samples = this->max_buffered_samples;
|
|
|
|
if(free_samples >= samples) {
|
|
const auto byte_length = samples * sizeof(float) * this->channel_count;
|
|
memcpy(this->buffer.write_ptr(), buffer, byte_length);
|
|
this->buffer.advance_write_ptr(byte_length);
|
|
return samples;
|
|
} else {
|
|
const auto byte_length = free_samples * sizeof(float) * this->channel_count;
|
|
memcpy(this->buffer.write_ptr(), buffer, byte_length);
|
|
this->buffer.advance_write_ptr(byte_length);
|
|
enqueued += free_samples;
|
|
}
|
|
|
|
if(auto fn = this->on_overflow; fn)
|
|
fn(samples - enqueued);
|
|
|
|
switch (this->overflow_strategy) {
|
|
case overflow_strategy::discard_input:
|
|
return -2;
|
|
case overflow_strategy::discard_buffer_all:
|
|
this->buffer.clear();
|
|
break;
|
|
case overflow_strategy::discard_buffer_half:
|
|
this->buffer.advance_read_ptr(this->buffer.fill_count() / 2);
|
|
break;
|
|
case overflow_strategy::ignore:
|
|
break;
|
|
}
|
|
|
|
return enqueued;
|
|
}
|
|
|
|
ssize_t AudioOutputSource::enqueue_samples_no_interleave(const void *buffer, size_t samples) {
|
|
auto free_bytes = this->buffer.free_count();
|
|
auto free_samples = free_bytes / sizeof(float) / this->channel_count;
|
|
if(this->max_buffered_samples && free_samples > this->max_buffered_samples) free_samples = this->max_buffered_samples;
|
|
|
|
auto samples_to_write{samples};
|
|
if(samples_to_write > free_samples) samples_to_write = free_samples;
|
|
const auto enqueued{samples_to_write};
|
|
{
|
|
auto src_buffer = (const float*) buffer;
|
|
auto target_buffer = (float*) this->buffer.write_ptr();
|
|
|
|
while (samples_to_write-- > 0) {
|
|
*target_buffer = *src_buffer;
|
|
*(target_buffer + 1) = *(src_buffer + samples);
|
|
|
|
target_buffer += 2;
|
|
src_buffer++;
|
|
}
|
|
}
|
|
this->buffer.advance_write_ptr(enqueued * this->channel_count * sizeof(float));
|
|
if(enqueued == samples) return enqueued;
|
|
|
|
if(auto fn = this->on_overflow; fn)
|
|
fn(samples - enqueued);
|
|
|
|
switch (this->overflow_strategy) {
|
|
case overflow_strategy::discard_input:
|
|
return -2;
|
|
case overflow_strategy::discard_buffer_all:
|
|
this->buffer.clear();
|
|
break;
|
|
case overflow_strategy::discard_buffer_half:
|
|
this->buffer.advance_read_ptr(this->buffer.fill_count() / 2);
|
|
break;
|
|
case overflow_strategy::ignore:
|
|
break;
|
|
}
|
|
|
|
return enqueued;
|
|
}
|
|
|
|
AudioOutput::AudioOutput(size_t channels, size_t rate) : _channel_count(channels), _sample_rate(rate) { }
|
|
|
|
AudioOutput::~AudioOutput() {
|
|
this->close_device();
|
|
this->cleanup_buffers();
|
|
}
|
|
|
|
std::shared_ptr<AudioOutputSource> AudioOutput::create_source(ssize_t buf) {
|
|
auto result = shared_ptr<AudioOutputSource>(new AudioOutputSource(this, this->_channel_count, this->_sample_rate, buf));
|
|
{
|
|
lock_guard lock(this->sources_lock);
|
|
this->_sources.push_back(result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void AudioOutput::delete_source(const std::shared_ptr<tc::audio::AudioOutputSource> &source) {
|
|
{
|
|
lock_guard lock(this->sources_lock);
|
|
auto it = find(this->_sources.begin(), this->_sources.end(), source);
|
|
if(it != this->_sources.end())
|
|
this->_sources.erase(it);
|
|
}
|
|
|
|
source->handle = nullptr;
|
|
}
|
|
|
|
void AudioOutput::cleanup_buffers() {
|
|
free(this->source_buffer);
|
|
free(this->source_merge_buffer);
|
|
free(this->resample_overhead_buffer);
|
|
|
|
this->source_merge_buffer = nullptr;
|
|
this->source_buffer = nullptr;
|
|
this->resample_overhead_buffer = nullptr;
|
|
|
|
this->source_merge_buffer_length = 0;
|
|
this->source_buffer_length = 0;
|
|
this->resample_overhead_buffer_length = 0;
|
|
this->resample_overhead_samples = 0;
|
|
}
|
|
|
|
void AudioOutput::fill_buffer(void *output, size_t out_frame_count, size_t out_channels) {
|
|
if(out_channels != this->_channel_count) {
|
|
log_critical(category::audio, tr("Channel count miss match (output)! Expected: {} Received: {}. Fixme!"), this->_channel_count, out_channels);
|
|
return;
|
|
}
|
|
auto local_frame_count = this->_resampler ? this->_resampler->input_size(out_frame_count) : out_frame_count;
|
|
void* const original_output{output};
|
|
|
|
if(this->resample_overhead_samples > 0) {
|
|
const auto samples_to_write = this->resample_overhead_samples > out_frame_count ? out_frame_count : this->resample_overhead_samples;
|
|
const auto byte_length = samples_to_write * sizeof(float) * out_channels;
|
|
|
|
if(output) memcpy(output, this->resample_overhead_buffer, byte_length);
|
|
if(samples_to_write == out_frame_count) {
|
|
this->resample_overhead_samples -= samples_to_write;
|
|
memcpy(this->resample_overhead_buffer, (char*) this->resample_overhead_buffer + byte_length, this->resample_overhead_samples * this->_channel_count * sizeof(float));
|
|
return;
|
|
} else {
|
|
this->resample_overhead_samples = 0;
|
|
output = (char*) output + byte_length;
|
|
out_frame_count -= samples_to_write;
|
|
local_frame_count -= this->_resampler ? this->_resampler->input_size(samples_to_write) : samples_to_write;
|
|
}
|
|
}
|
|
|
|
if(!original_output) {
|
|
for(auto& source : this->_sources)
|
|
source->pop_samples(nullptr, local_frame_count);
|
|
return;
|
|
} else if(this->_volume <= 0) {
|
|
for(auto& source : this->_sources)
|
|
source->pop_samples(nullptr, local_frame_count);
|
|
memset(output, 0, local_frame_count * out_channels * sizeof(float));
|
|
return;
|
|
}
|
|
|
|
const size_t local_buffer_length = local_frame_count * 4 * this->_channel_count;
|
|
const size_t out_buffer_length = out_frame_count * 4 * this->_channel_count;
|
|
size_t sources = 0;
|
|
size_t actual_sources = 0;
|
|
|
|
{
|
|
lock_guard lock(this->sources_lock);
|
|
sources = this->_sources.size();
|
|
actual_sources = sources;
|
|
|
|
if(sources > 0) {
|
|
/* allocate the required space */
|
|
const auto required_source_buffer_length = (out_buffer_length > local_buffer_length ? out_buffer_length : local_buffer_length) * sources; /* ensure enough space for later resample */
|
|
const auto required_source_merge_buffer_length = sizeof(void*) * sources;
|
|
|
|
{
|
|
|
|
if(this->source_buffer_length < required_source_buffer_length || !this->source_buffer) {
|
|
if(this->source_buffer)
|
|
free(this->source_buffer);
|
|
this->source_buffer = malloc(required_source_buffer_length);
|
|
this->source_buffer_length = required_source_buffer_length;
|
|
}
|
|
if(this->source_merge_buffer_length < required_source_merge_buffer_length || !this->source_merge_buffer) {
|
|
if (this->source_merge_buffer)
|
|
free(this->source_merge_buffer);
|
|
this->source_merge_buffer = (void **) malloc(required_source_merge_buffer_length);
|
|
this->source_merge_buffer_length = required_source_merge_buffer_length;
|
|
}
|
|
}
|
|
|
|
for(size_t index = 0; index < sources; index++) {
|
|
auto& source = this->_sources[index];
|
|
|
|
this->source_merge_buffer[index] = (char*) this->source_buffer + (local_buffer_length * index);
|
|
auto written_frames = this->_sources[index]->pop_samples(this->source_merge_buffer[index], local_frame_count);
|
|
if(written_frames != local_frame_count) {
|
|
if(written_frames <= 0) {
|
|
this->source_merge_buffer[index] = nullptr;
|
|
actual_sources--;
|
|
} else {
|
|
/* fill up the rest with silence (0) */
|
|
auto written = written_frames * this->_channel_count * 4;
|
|
memset((char*) this->source_merge_buffer[index] + written, 0, (local_frame_count - written_frames) * this->_channel_count * 4);
|
|
}
|
|
}
|
|
}
|
|
} else
|
|
goto clear_buffer_exit;
|
|
}
|
|
|
|
if(actual_sources > 0) {
|
|
if(local_frame_count == out_frame_count) {
|
|
if(!merge::merge_n_sources(output, this->source_merge_buffer, sources, this->_channel_count, local_frame_count))
|
|
log_warn(category::audio, tr("failed to merge buffers!"));
|
|
} else {
|
|
if(!merge::merge_n_sources(this->source_buffer, this->source_merge_buffer, sources, this->_channel_count, local_frame_count))
|
|
log_warn(category::audio, tr("failed to merge buffers!"));
|
|
|
|
/* this->source_buffer could hold the amount of resampled data (checked above) */
|
|
auto resampled_samples = this->_resampler->process(this->source_buffer, this->source_buffer, local_frame_count);
|
|
if(resampled_samples <= 0) {
|
|
log_warn(category::audio, tr("Failed to resample audio data for client ({})"));
|
|
goto clear_buffer_exit;
|
|
}
|
|
if(resampled_samples != out_frame_count) {
|
|
if((size_t) resampled_samples > out_frame_count) {
|
|
const auto diff_length = resampled_samples - out_frame_count;
|
|
log_warn(category::audio, tr("enqueuing {} samples"), diff_length);
|
|
const auto overhead_buffer_offset = this->resample_overhead_samples * sizeof(float) * this->_channel_count;
|
|
const auto diff_byte_length = diff_length * sizeof(float) * this->_channel_count;
|
|
|
|
if(this->resample_overhead_buffer_length < diff_byte_length + overhead_buffer_offset) {
|
|
this->resample_overhead_buffer_length = diff_byte_length + overhead_buffer_offset;
|
|
auto new_buffer = malloc(this->resample_overhead_buffer_length);
|
|
if(this->resample_overhead_buffer)
|
|
memcpy(new_buffer, this->resample_overhead_buffer, overhead_buffer_offset);
|
|
free(this->resample_overhead_buffer);
|
|
this->resample_overhead_buffer = new_buffer;
|
|
}
|
|
memcpy(
|
|
(char*) this->resample_overhead_buffer + overhead_buffer_offset,
|
|
(char*) this->source_buffer + out_frame_count * sizeof(float) * this->_channel_count,
|
|
diff_byte_length
|
|
);
|
|
this->resample_overhead_samples += diff_length;
|
|
} else {
|
|
log_warn(category::audio, tr("Resampled samples does not match requested sampeles: {} <> {}. Sampled from {} to {}"), resampled_samples, out_frame_count, this->_resampler->input_rate(), this->_resampler->output_rate());
|
|
}
|
|
}
|
|
memcpy(output, this->source_buffer, out_frame_count * sizeof(float) * this->_channel_count);
|
|
}
|
|
|
|
/* lets apply the volume */
|
|
auto volume = this->_volume;
|
|
if(volume != 1) {
|
|
auto float_length = this->_channel_count * out_frame_count;
|
|
auto data = (float*) output;
|
|
while(float_length-- > 0)
|
|
*data++ *= volume;
|
|
}
|
|
|
|
} else {
|
|
clear_buffer_exit:
|
|
memset(output, 0, this->_channel_count * sizeof(float) * out_frame_count);
|
|
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->_playback->register_source(this);
|
|
return this->_playback->start(error);
|
|
} |