mirror of
https://github.com/AxioDL/boo.git
synced 2025-05-14 19:31:20 +00:00
Alphabetizes includes and resolves quite a few instances of indirect inclusions, making the requirements of several interfaces explicit. This also trims out includes that aren't actually necessary (likely due to changes in the API over time).
361 lines
12 KiB
C++
361 lines
12 KiB
C++
#include "lib/audiodev/AudioVoiceEngine.hpp"
|
|
|
|
#include "boo/boo.hpp"
|
|
#include "lib/audiodev/LinuxMidi.hpp"
|
|
|
|
#include <logvisor/logvisor.hpp>
|
|
#include <pulse/pulseaudio.h>
|
|
#include <unistd.h>
|
|
|
|
namespace boo {
|
|
static logvisor::Module Log("boo::PulseAudio");
|
|
logvisor::Module ALSALog("boo::ALSA");
|
|
|
|
static const uint64_t StereoChans = (1 << PA_CHANNEL_POSITION_FRONT_LEFT) | (1 << PA_CHANNEL_POSITION_FRONT_RIGHT);
|
|
|
|
static const uint64_t QuadChans = (1 << PA_CHANNEL_POSITION_FRONT_LEFT) | (1 << PA_CHANNEL_POSITION_FRONT_RIGHT) |
|
|
(1 << PA_CHANNEL_POSITION_REAR_LEFT) | (1 << PA_CHANNEL_POSITION_REAR_RIGHT);
|
|
|
|
static const uint64_t S51Chans = (1 << PA_CHANNEL_POSITION_FRONT_LEFT) | (1 << PA_CHANNEL_POSITION_FRONT_RIGHT) |
|
|
(1 << PA_CHANNEL_POSITION_REAR_LEFT) | (1 << PA_CHANNEL_POSITION_REAR_RIGHT) |
|
|
(1 << PA_CHANNEL_POSITION_FRONT_CENTER) | (1 << PA_CHANNEL_POSITION_LFE);
|
|
|
|
static const uint64_t S71Chans = (1 << PA_CHANNEL_POSITION_FRONT_LEFT) | (1 << PA_CHANNEL_POSITION_FRONT_RIGHT) |
|
|
(1 << PA_CHANNEL_POSITION_REAR_LEFT) | (1 << PA_CHANNEL_POSITION_REAR_RIGHT) |
|
|
(1 << PA_CHANNEL_POSITION_FRONT_CENTER) | (1 << PA_CHANNEL_POSITION_LFE) |
|
|
(1 << PA_CHANNEL_POSITION_SIDE_LEFT) | (1 << PA_CHANNEL_POSITION_SIDE_RIGHT);
|
|
|
|
struct PulseAudioVoiceEngine : LinuxMidi {
|
|
pa_mainloop* m_mainloop = nullptr;
|
|
pa_context* m_ctx = nullptr;
|
|
pa_stream* m_stream = nullptr;
|
|
std::string m_sinkName;
|
|
bool m_handleMove = false;
|
|
pa_sample_spec m_sampleSpec = {};
|
|
pa_channel_map m_chanMap = {};
|
|
|
|
int _paWaitReady() {
|
|
int retval = 0;
|
|
while (pa_context_get_state(m_ctx) < PA_CONTEXT_READY)
|
|
pa_mainloop_iterate(m_mainloop, 1, &retval);
|
|
return retval;
|
|
}
|
|
|
|
int _paStreamWaitReady() {
|
|
int retval = 0;
|
|
while (pa_stream_get_state(m_stream) < PA_STREAM_READY)
|
|
pa_mainloop_iterate(m_mainloop, 1, &retval);
|
|
return retval;
|
|
}
|
|
|
|
int _paIterate(pa_operation* op) const {
|
|
int retval = 0;
|
|
while (pa_operation_get_state(op) == PA_OPERATION_RUNNING)
|
|
pa_mainloop_iterate(m_mainloop, 1, &retval);
|
|
return retval;
|
|
}
|
|
|
|
bool _setupSink() {
|
|
if (m_stream) {
|
|
pa_stream_disconnect(m_stream);
|
|
pa_stream_unref(m_stream);
|
|
m_stream = nullptr;
|
|
}
|
|
|
|
pa_operation* op;
|
|
m_sampleSpec.format = PA_SAMPLE_INVALID;
|
|
op = pa_context_get_sink_info_by_name(m_ctx, m_sinkName.c_str(), pa_sink_info_cb_t(_getSinkInfoReply), this);
|
|
_paIterate(op);
|
|
pa_operation_unref(op);
|
|
|
|
if (m_sampleSpec.format == PA_SAMPLE_INVALID) {
|
|
Log.report(logvisor::Error, fmt("Unable to setup audio stream"));
|
|
goto err;
|
|
}
|
|
|
|
m_5msFrames = m_sampleSpec.rate * 5 / 1000;
|
|
|
|
m_mixInfo.m_sampleRate = m_sampleSpec.rate;
|
|
m_mixInfo.m_sampleFormat = SOXR_FLOAT32;
|
|
m_mixInfo.m_bitsPerSample = 32;
|
|
m_mixInfo.m_periodFrames = m_5msFrames;
|
|
if (!(m_stream = pa_stream_new(m_ctx, "master", &m_sampleSpec, &m_chanMap))) {
|
|
Log.report(logvisor::Error, fmt("Unable to pa_stream_new(): {}"), pa_strerror(pa_context_errno(m_ctx)));
|
|
goto err;
|
|
}
|
|
|
|
pa_buffer_attr bufAttr;
|
|
bufAttr.minreq = uint32_t(m_5msFrames * m_sampleSpec.channels * sizeof(float));
|
|
bufAttr.maxlength = bufAttr.minreq * 24;
|
|
bufAttr.tlength = bufAttr.maxlength;
|
|
bufAttr.prebuf = UINT32_MAX;
|
|
bufAttr.fragsize = UINT32_MAX;
|
|
|
|
if (pa_stream_connect_playback(m_stream, m_sinkName.c_str(), &bufAttr,
|
|
pa_stream_flags_t(PA_STREAM_START_UNMUTED | PA_STREAM_EARLY_REQUESTS), nullptr,
|
|
nullptr)) {
|
|
Log.report(logvisor::Error, fmt("Unable to pa_stream_connect_playback()"));
|
|
goto err;
|
|
}
|
|
|
|
pa_stream_set_moved_callback(m_stream, pa_stream_notify_cb_t(_streamMoved), this);
|
|
|
|
_paStreamWaitReady();
|
|
|
|
_resetSampleRate();
|
|
return true;
|
|
err:
|
|
if (m_stream) {
|
|
pa_stream_disconnect(m_stream);
|
|
pa_stream_unref(m_stream);
|
|
m_stream = nullptr;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
PulseAudioVoiceEngine() {
|
|
if (!(m_mainloop = pa_mainloop_new())) {
|
|
Log.report(logvisor::Error, fmt("Unable to pa_mainloop_new()"));
|
|
return;
|
|
}
|
|
|
|
pa_mainloop_api* mlApi = pa_mainloop_get_api(m_mainloop);
|
|
pa_proplist* propList = pa_proplist_new();
|
|
pa_proplist_sets(propList, PA_PROP_APPLICATION_ICON_NAME, APP->getUniqueName().data());
|
|
pa_proplist_sets(propList, PA_PROP_APPLICATION_PROCESS_ID, fmt::format(fmt("{}"), int(getpid())).c_str());
|
|
if (!(m_ctx = pa_context_new_with_proplist(mlApi, APP->getFriendlyName().data(), propList))) {
|
|
Log.report(logvisor::Error, fmt("Unable to pa_context_new_with_proplist()"));
|
|
pa_mainloop_free(m_mainloop);
|
|
m_mainloop = nullptr;
|
|
return;
|
|
}
|
|
|
|
pa_operation* op;
|
|
|
|
if (pa_context_connect(m_ctx, nullptr, PA_CONTEXT_NOFLAGS, nullptr)) {
|
|
Log.report(logvisor::Error, fmt("Unable to pa_context_connect()"));
|
|
goto err;
|
|
}
|
|
|
|
_paWaitReady();
|
|
|
|
op = pa_context_get_server_info(m_ctx, pa_server_info_cb_t(_getServerInfoReply), this);
|
|
_paIterate(op);
|
|
pa_operation_unref(op);
|
|
|
|
if (!_setupSink())
|
|
goto err;
|
|
|
|
return;
|
|
err:
|
|
pa_context_disconnect(m_ctx);
|
|
pa_context_unref(m_ctx);
|
|
m_ctx = nullptr;
|
|
pa_mainloop_free(m_mainloop);
|
|
m_mainloop = nullptr;
|
|
}
|
|
|
|
~PulseAudioVoiceEngine() override {
|
|
if (m_stream) {
|
|
pa_stream_disconnect(m_stream);
|
|
pa_stream_unref(m_stream);
|
|
}
|
|
if (m_ctx) {
|
|
pa_context_disconnect(m_ctx);
|
|
pa_context_unref(m_ctx);
|
|
}
|
|
if (m_mainloop) {
|
|
pa_mainloop_free(m_mainloop);
|
|
}
|
|
}
|
|
|
|
static void _streamMoved(pa_stream* p, PulseAudioVoiceEngine* userdata) {
|
|
userdata->m_sinkName = pa_stream_get_device_name(p);
|
|
userdata->m_handleMove = true;
|
|
}
|
|
|
|
static void _getServerInfoReply(pa_context* c, const pa_server_info* i, PulseAudioVoiceEngine* userdata) {
|
|
userdata->m_sinkName = i->default_sink_name;
|
|
}
|
|
|
|
void _parseAudioChannelSet(const pa_channel_map* chm) {
|
|
m_chanMap = *chm;
|
|
|
|
ChannelMap& chmapOut = m_mixInfo.m_channelMap;
|
|
m_mixInfo.m_channels = AudioChannelSet::Unknown;
|
|
|
|
uint64_t chBits = 0;
|
|
chmapOut.m_channelCount = chm->channels;
|
|
for (unsigned c = 0; c < chm->channels; ++c) {
|
|
chBits |= 1 << chm->map[c];
|
|
switch (chm->map[c]) {
|
|
case PA_CHANNEL_POSITION_FRONT_LEFT:
|
|
chmapOut.m_channels[c] = AudioChannel::FrontLeft;
|
|
break;
|
|
case PA_CHANNEL_POSITION_FRONT_RIGHT:
|
|
chmapOut.m_channels[c] = AudioChannel::FrontRight;
|
|
break;
|
|
case PA_CHANNEL_POSITION_REAR_LEFT:
|
|
chmapOut.m_channels[c] = AudioChannel::RearLeft;
|
|
break;
|
|
case PA_CHANNEL_POSITION_REAR_RIGHT:
|
|
chmapOut.m_channels[c] = AudioChannel::RearRight;
|
|
break;
|
|
case PA_CHANNEL_POSITION_FRONT_CENTER:
|
|
chmapOut.m_channels[c] = AudioChannel::FrontCenter;
|
|
break;
|
|
case PA_CHANNEL_POSITION_LFE:
|
|
chmapOut.m_channels[c] = AudioChannel::LFE;
|
|
break;
|
|
case PA_CHANNEL_POSITION_SIDE_LEFT:
|
|
chmapOut.m_channels[c] = AudioChannel::SideLeft;
|
|
break;
|
|
case PA_CHANNEL_POSITION_SIDE_RIGHT:
|
|
chmapOut.m_channels[c] = AudioChannel::SideRight;
|
|
break;
|
|
default:
|
|
chmapOut.m_channels[c] = AudioChannel::Unknown;
|
|
break;
|
|
}
|
|
}
|
|
|
|
static const std::array<AudioChannelSet, 4> testSets = {
|
|
{AudioChannelSet::Surround71, AudioChannelSet::Surround51, AudioChannelSet::Quad, AudioChannelSet::Stereo}};
|
|
for (AudioChannelSet set : testSets) {
|
|
switch (set) {
|
|
case AudioChannelSet::Stereo: {
|
|
if ((chBits & StereoChans) == StereoChans) {
|
|
m_mixInfo.m_channels = AudioChannelSet::Stereo;
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
case AudioChannelSet::Quad: {
|
|
if ((chBits & QuadChans) == QuadChans) {
|
|
m_mixInfo.m_channels = AudioChannelSet::Quad;
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
case AudioChannelSet::Surround51: {
|
|
if ((chBits & S51Chans) == S51Chans) {
|
|
m_mixInfo.m_channels = AudioChannelSet::Surround51;
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
case AudioChannelSet::Surround71: {
|
|
if ((chBits & S71Chans) == S71Chans) {
|
|
m_mixInfo.m_channels = AudioChannelSet::Surround71;
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void _getSinkInfoReply(pa_context* c, const pa_sink_info* i, int eol, PulseAudioVoiceEngine* userdata) {
|
|
if (!i)
|
|
return;
|
|
userdata->m_sampleSpec.format = PA_SAMPLE_FLOAT32;
|
|
userdata->m_sampleSpec.rate = i->sample_spec.rate;
|
|
userdata->m_sampleSpec.channels = i->sample_spec.channels;
|
|
userdata->_parseAudioChannelSet(&i->channel_map);
|
|
}
|
|
|
|
mutable std::vector<std::pair<std::string, std::string>> m_sinks;
|
|
static void _getSinkInfoListReply(pa_context* c, const pa_sink_info* i, int eol, PulseAudioVoiceEngine* userdata) {
|
|
if (i)
|
|
userdata->m_sinks.push_back(std::make_pair(i->name, i->description));
|
|
}
|
|
std::vector<std::pair<std::string, std::string>> enumerateAudioOutputs() const override {
|
|
pa_operation* op = pa_context_get_sink_info_list(m_ctx, pa_sink_info_cb_t(_getSinkInfoListReply), (void*)this);
|
|
_paIterate(op);
|
|
pa_operation_unref(op);
|
|
std::vector<std::pair<std::string, std::string>> ret;
|
|
ret.swap(m_sinks);
|
|
return ret;
|
|
}
|
|
|
|
std::string getCurrentAudioOutput() const override { return m_sinkName; }
|
|
|
|
bool m_sinkOk = false;
|
|
static void _checkAudioSinkReply(pa_context* c, const pa_sink_info* i, int eol, PulseAudioVoiceEngine* userdata) {
|
|
if (i)
|
|
userdata->m_sinkOk = true;
|
|
}
|
|
bool setCurrentAudioOutput(const char* name) override {
|
|
m_sinkOk = false;
|
|
pa_operation* op;
|
|
op = pa_context_get_sink_info_by_name(m_ctx, name, pa_sink_info_cb_t(_checkAudioSinkReply), this);
|
|
_paIterate(op);
|
|
pa_operation_unref(op);
|
|
if (m_sinkOk) {
|
|
m_sinkName = name;
|
|
return _setupSink();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void _doIterate() {
|
|
int retval;
|
|
pa_mainloop_iterate(m_mainloop, 1, &retval);
|
|
if (m_handleMove) {
|
|
m_handleMove = false;
|
|
_setupSink();
|
|
}
|
|
}
|
|
|
|
void pumpAndMixVoices() override {
|
|
if (!m_stream) {
|
|
/* Dummy pump mode - use failsafe defaults for 1/60sec of samples */
|
|
m_mixInfo.m_sampleRate = 32000.0;
|
|
m_mixInfo.m_sampleFormat = SOXR_FLOAT32_I;
|
|
m_mixInfo.m_bitsPerSample = 32;
|
|
m_5msFrames = 32000 / 60;
|
|
m_mixInfo.m_periodFrames = m_5msFrames;
|
|
m_mixInfo.m_channels = AudioChannelSet::Stereo;
|
|
m_mixInfo.m_channelMap.m_channelCount = 2;
|
|
m_mixInfo.m_channelMap.m_channels[0] = AudioChannel::FrontLeft;
|
|
m_mixInfo.m_channelMap.m_channels[1] = AudioChannel::FrontRight;
|
|
_pumpAndMixVoices(m_5msFrames, (float*)nullptr);
|
|
return;
|
|
}
|
|
|
|
size_t writableSz = pa_stream_writable_size(m_stream);
|
|
size_t frameSz = m_mixInfo.m_channelMap.m_channelCount * sizeof(float);
|
|
size_t writableFrames = writableSz / frameSz;
|
|
size_t writablePeriods = writableFrames / m_mixInfo.m_periodFrames;
|
|
|
|
if (!writablePeriods) {
|
|
_doIterate();
|
|
return;
|
|
}
|
|
|
|
void* data = nullptr;
|
|
size_t periodSz = m_mixInfo.m_periodFrames * frameSz;
|
|
size_t nbytes = writablePeriods * periodSz;
|
|
if (pa_stream_begin_write(m_stream, &data, &nbytes)) {
|
|
pa_stream_state_t st = pa_stream_get_state(m_stream);
|
|
Log.report(logvisor::Error, fmt("Unable to pa_stream_begin_write(): {} {}"), pa_strerror(pa_context_errno(m_ctx)), st);
|
|
_doIterate();
|
|
return;
|
|
}
|
|
|
|
writablePeriods = nbytes / periodSz;
|
|
_pumpAndMixVoices(m_mixInfo.m_periodFrames * writablePeriods, reinterpret_cast<float*>(data));
|
|
|
|
if (pa_stream_write(m_stream, data, nbytes, nullptr, 0, PA_SEEK_RELATIVE))
|
|
Log.report(logvisor::Error, fmt("Unable to pa_stream_write()"));
|
|
|
|
_doIterate();
|
|
}
|
|
};
|
|
|
|
std::unique_ptr<IAudioVoiceEngine> NewAudioVoiceEngine() { return std::make_unique<PulseAudioVoiceEngine>(); }
|
|
|
|
} // namespace boo
|