287 lines
8.4 KiB
C++
287 lines
8.4 KiB
C++
#include "AudioOutput.h"
|
|
#include "AudioMerger.h"
|
|
#include "../logger.h"
|
|
#include <cstring>
|
|
#include <algorithm>
|
|
#include <string>
|
|
|
|
using namespace std;
|
|
using namespace tc;
|
|
using namespace tc::audio;
|
|
|
|
void AudioOutputSource::clear() {
|
|
lock_guard lock(this->buffer_lock);
|
|
this->sample_buffers.clear();
|
|
this->buffered_samples = 0;
|
|
}
|
|
|
|
ssize_t AudioOutputSource::pop_samples(void *buffer, size_t samples) {
|
|
auto sample_count = samples;
|
|
|
|
_retest:
|
|
{
|
|
lock_guard lock(this->buffer_lock);
|
|
if(this->buffering) {
|
|
if(this->buffered_samples > this->min_buffer) {
|
|
this->buffering = false;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
while(sample_count > 0 && !this->sample_buffers.empty()) {
|
|
auto buf = this->sample_buffers[0];
|
|
auto sc = min((size_t) (buf->sample_size - buf->sample_index), (size_t) sample_count);
|
|
if(sc > 0 && buffer) { /* just to ensure */
|
|
memcpy(buffer, (char *) buf->sample_data + this->channel_count * buf->sample_index * 4, sc * this->channel_count * 4);
|
|
} else {
|
|
#ifndef WIN32
|
|
/* for my debugger */
|
|
__asm__("nop");
|
|
#endif
|
|
}
|
|
|
|
sample_count -= sc;
|
|
buf->sample_index += (uint16_t) sc;
|
|
if(buf->sample_index == buf->sample_size)
|
|
this->sample_buffers.pop_front();
|
|
|
|
if(buffer)
|
|
buffer = (char*) buffer + sc * this->channel_count * 4;
|
|
}
|
|
this->buffered_samples -= samples - sample_count;
|
|
}
|
|
|
|
if(sample_count > 0) {
|
|
if(this->on_underflow) {
|
|
if(this->on_underflow()) {
|
|
goto _retest;
|
|
}
|
|
}
|
|
|
|
this->buffering = true;
|
|
}
|
|
|
|
if(this->on_read)
|
|
this->on_read();
|
|
|
|
return samples - sample_count; /* return the written samples */
|
|
}
|
|
|
|
ssize_t AudioOutputSource::enqueue_samples(const void *buffer, size_t samples) {
|
|
auto buf = SampleBuffer::allocate((uint8_t) this->channel_count, (uint16_t) samples);
|
|
if(!buf)
|
|
return -1;
|
|
|
|
buf->sample_index = 0;
|
|
memcpy(buf->sample_data, buffer, this->channel_count * samples * 4);
|
|
|
|
return this->enqueue_samples(buf);
|
|
}
|
|
|
|
ssize_t AudioOutputSource::enqueue_samples(const std::shared_ptr<tc::audio::SampleBuffer> &buf) {
|
|
if(!buf) return 0;
|
|
|
|
{
|
|
unique_lock lock(this->buffer_lock);
|
|
if(this->max_latency > 0 && this->buffered_samples + buf->sample_size > this->max_latency) {
|
|
/* overflow! */
|
|
auto overflow_length = this->buffered_samples + buf->sample_size - this->max_latency;
|
|
if(this->on_overflow) {
|
|
lock.unlock();
|
|
this->on_overflow(overflow_length);
|
|
lock.lock();
|
|
}
|
|
|
|
switch (this->overflow_strategy) {
|
|
case overflow_strategy::discard_input:
|
|
return -2;
|
|
case overflow_strategy::discard_buffer_all:
|
|
this->sample_buffers.clear();
|
|
break;
|
|
case overflow_strategy::discard_buffer_half:
|
|
this->sample_buffers.erase(this->sample_buffers.begin(), this->sample_buffers.begin() + (int) ceil(this->sample_buffers.size() / 2));
|
|
break;
|
|
case overflow_strategy::ignore:
|
|
break;
|
|
}
|
|
}
|
|
|
|
this->sample_buffers.push_back(buf);
|
|
this->buffered_samples += buf->sample_size;
|
|
}
|
|
|
|
return buf->sample_size;
|
|
}
|
|
|
|
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() {
|
|
auto result = shared_ptr<AudioOutputSource>(new AudioOutputSource(this, this->_channel_count, this->_sample_rate));
|
|
{
|
|
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() {
|
|
lock_guard buffer_lock(this->buffer_lock);
|
|
if(this->source_buffer)
|
|
free(this->source_buffer);
|
|
if(this->source_merge_buffer)
|
|
free(this->source_merge_buffer);
|
|
|
|
this->source_merge_buffer = nullptr;
|
|
this->source_buffer = nullptr;
|
|
this->source_merge_buffer_length = 0;
|
|
this->source_buffer_length = 0;
|
|
}
|
|
|
|
int AudioOutput::_audio_callback(const void *a, void *b, unsigned long c, const PaStreamCallbackTimeInfo* d, PaStreamCallbackFlags e, void *_ptr_audio_output) {
|
|
return reinterpret_cast<AudioOutput*>(_ptr_audio_output)->audio_callback(a, b, c, d, e);
|
|
}
|
|
|
|
int AudioOutput::audio_callback(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags) {
|
|
if(!output) /* hmmm.. suspicious */
|
|
return 0;
|
|
|
|
lock_guard buffer_lock(this->buffer_lock);
|
|
size_t buffer_length = frameCount * 4 * this->_channel_count;
|
|
size_t sources = 0;
|
|
size_t actual_sources = 0;
|
|
|
|
auto volume = this->_volume;
|
|
{
|
|
lock_guard lock(this->sources_lock);
|
|
sources = this->_sources.size();
|
|
actual_sources = sources;
|
|
|
|
if(sources > 0) {
|
|
if(volume > 0) { /* allocate the required space */
|
|
auto source_buffer_length = buffer_length * sources;
|
|
auto source_merge_buffer_length = sizeof(void*) * sources;
|
|
|
|
if(this->source_buffer_length < source_buffer_length || !this->source_buffer) {
|
|
if(this->source_buffer)
|
|
free(this->source_buffer);
|
|
this->source_buffer = malloc(source_buffer_length);
|
|
this->source_buffer_length = source_buffer_length;
|
|
}
|
|
if(this->source_merge_buffer_length < source_merge_buffer_length || !this->source_merge_buffer) {
|
|
if(this->source_merge_buffer)
|
|
free(this->source_merge_buffer);
|
|
this->source_merge_buffer = (void**) malloc(source_merge_buffer_length);
|
|
this->source_merge_buffer_length = source_merge_buffer_length;
|
|
}
|
|
}
|
|
|
|
for(size_t index = 0; index < sources; index++) {
|
|
auto& source = this->_sources[index];
|
|
|
|
if(volume > 0) {
|
|
this->source_merge_buffer[index] = (char*) this->source_buffer + (buffer_length * index);
|
|
auto written_frames = this->_sources[index]->pop_samples(this->source_merge_buffer[index], frameCount);
|
|
if(written_frames != frameCount) {
|
|
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, (frameCount - written_frames) * this->_channel_count * 4);
|
|
}
|
|
}
|
|
} else {
|
|
this->_sources[index]->pop_samples(nullptr, frameCount);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(actual_sources > 0 && volume > 0) {
|
|
if(!merge::merge_n_sources(output, this->source_merge_buffer, sources, this->_channel_count, frameCount)) {
|
|
log_warn(category::audio, tr("failed to merge buffers!"));
|
|
}
|
|
auto float_length = this->_channel_count * frameCount;
|
|
auto data = (float*) output;
|
|
while(float_length-- > 0)
|
|
*data++ *= volume;
|
|
|
|
} else {
|
|
memset(output, 0, this->_channel_count * 4 * frameCount);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool AudioOutput::open_device(std::string& error, PaDeviceIndex index) {
|
|
lock_guard lock(this->output_stream_lock);
|
|
|
|
if(index == this->_current_device_index)
|
|
return true;
|
|
|
|
this->close_device();
|
|
this->_current_device_index = index;
|
|
this->_current_device = Pa_GetDeviceInfo(index);
|
|
|
|
if(!this->_current_device) {
|
|
this->_current_device_index = paNoDevice;
|
|
error = "failed to get device info";
|
|
return false;
|
|
}
|
|
|
|
PaStreamParameters output_parameters{};
|
|
memset(&output_parameters, 0, sizeof(output_parameters));
|
|
output_parameters.channelCount = (int) this->_channel_count;
|
|
output_parameters.device = this->_current_device_index;
|
|
output_parameters.sampleFormat = paFloat32;
|
|
output_parameters.suggestedLatency = this->_current_device->defaultLowOutputLatency;
|
|
|
|
auto err = Pa_OpenStream(&output_stream, nullptr, &output_parameters, (double) this->_sample_rate, paFramesPerBufferUnspecified, paClipOff, &AudioOutput::_audio_callback, this);
|
|
if(err != paNoError) {
|
|
error = to_string(err) + "/" + Pa_GetErrorText(err);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool AudioOutput::playback() {
|
|
lock_guard lock(this->output_stream_lock);
|
|
if(!this->output_stream)
|
|
return false;
|
|
|
|
auto err = Pa_StartStream(this->output_stream);
|
|
if(err != paNoError && err != paStreamIsNotStopped) {
|
|
log_error(category::audio, tr("Pa_StartStream returned {}"), err);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void AudioOutput::close_device() {
|
|
lock_guard lock(this->output_stream_lock);
|
|
if(this->output_stream) {
|
|
/* TODO: Test for errors */
|
|
Pa_StopStream(this->output_stream);
|
|
Pa_CloseStream(this->output_stream);
|
|
|
|
this->output_stream = nullptr;
|
|
}
|
|
} |