350 lines
10 KiB
C++
350 lines
10 KiB
C++
//=========================================================================
|
|
// Name: PulseAudioEngine.cpp
|
|
// Purpose: Defines the interface to the PulseAudio audio engine.
|
|
//
|
|
// Authors: Mooneer Salem
|
|
// License:
|
|
//
|
|
// All rights reserved.
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License version 2.1,
|
|
// as published by the Free Software Foundation. This program is
|
|
// distributed in the hope that it will be useful, but WITHOUT ANY
|
|
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
|
// License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
//
|
|
//=========================================================================
|
|
|
|
#include "PulseAudioDevice.h"
|
|
#include "PulseAudioEngine.h"
|
|
|
|
#include <map>
|
|
|
|
PulseAudioEngine::PulseAudioEngine()
|
|
: initialized_(false)
|
|
{
|
|
// empty
|
|
}
|
|
|
|
PulseAudioEngine::~PulseAudioEngine()
|
|
{
|
|
if (initialized_)
|
|
{
|
|
stop();
|
|
}
|
|
}
|
|
|
|
void PulseAudioEngine::start()
|
|
{
|
|
// Allocate PA main loop and context.
|
|
mainloop_ = pa_threaded_mainloop_new();
|
|
|
|
if (mainloop_ == nullptr)
|
|
{
|
|
if (onAudioErrorFunction)
|
|
{
|
|
onAudioErrorFunction(*this, "Could not allocate PulseAudio main loop.", onAudioErrorState);
|
|
}
|
|
return;
|
|
}
|
|
|
|
mainloopApi_ = pa_threaded_mainloop_get_api(mainloop_);
|
|
context_ = pa_context_new(mainloopApi_, "FreeDV");
|
|
|
|
if (context_ == nullptr)
|
|
{
|
|
if (onAudioErrorFunction)
|
|
{
|
|
onAudioErrorFunction(*this, "Could not allocate PulseAudio context.", onAudioErrorState);
|
|
}
|
|
|
|
pa_threaded_mainloop_free(mainloop_);
|
|
mainloop_ = nullptr;
|
|
return;
|
|
}
|
|
|
|
pa_context_set_state_callback(context_, [](pa_context* context, void* mainloop) {
|
|
pa_threaded_mainloop *threadedML = static_cast<pa_threaded_mainloop *>(mainloop);
|
|
pa_threaded_mainloop_signal(threadedML, 0);
|
|
}, mainloop_);
|
|
|
|
// Start main loop.
|
|
pa_threaded_mainloop_lock(mainloop_);
|
|
if (pa_threaded_mainloop_start(mainloop_) != 0)
|
|
{
|
|
pa_threaded_mainloop_unlock(mainloop_);
|
|
|
|
if (onAudioErrorFunction)
|
|
{
|
|
onAudioErrorFunction(*this, "Could not start PulseAudio main loop.", onAudioErrorState);
|
|
}
|
|
|
|
pa_context_unref(context_);
|
|
pa_threaded_mainloop_free(mainloop_);
|
|
mainloop_ = nullptr;
|
|
context_ = nullptr;
|
|
return;
|
|
}
|
|
|
|
// Connect context to default PA server.
|
|
if (pa_context_connect(context_, NULL, PA_CONTEXT_NOFLAGS, NULL) != 0)
|
|
{
|
|
pa_threaded_mainloop_unlock(mainloop_);
|
|
|
|
if (onAudioErrorFunction)
|
|
{
|
|
onAudioErrorFunction(*this, "Could not connect PulseAudio context.", onAudioErrorState);
|
|
}
|
|
|
|
pa_threaded_mainloop_stop(mainloop_);
|
|
pa_context_unref(context_);
|
|
pa_threaded_mainloop_free(mainloop_);
|
|
return;
|
|
}
|
|
|
|
// Wait for the context to be ready
|
|
for(;;)
|
|
{
|
|
pa_context_state_t context_state = pa_context_get_state(context_);
|
|
assert(PA_CONTEXT_IS_GOOD(context_state));
|
|
if (context_state == PA_CONTEXT_READY) break;
|
|
pa_threaded_mainloop_wait(mainloop_);
|
|
}
|
|
|
|
pa_threaded_mainloop_unlock(mainloop_);
|
|
initialized_ = true;
|
|
}
|
|
|
|
void PulseAudioEngine::stop()
|
|
{
|
|
if (initialized_)
|
|
{
|
|
pa_threaded_mainloop_lock(mainloop_);
|
|
pa_context_disconnect(context_);
|
|
pa_threaded_mainloop_unlock(mainloop_);
|
|
|
|
pa_threaded_mainloop_stop(mainloop_);
|
|
pa_context_unref(context_);
|
|
pa_threaded_mainloop_free(mainloop_);
|
|
|
|
mainloop_ = nullptr;
|
|
mainloopApi_ = nullptr;
|
|
context_ = nullptr;
|
|
initialized_ = false;
|
|
}
|
|
}
|
|
|
|
struct PulseAudioDeviceListTemp
|
|
{
|
|
std::vector<AudioDeviceSpecification> result;
|
|
std::map<int, std::string> cardResult;
|
|
PulseAudioEngine* thisPtr;
|
|
};
|
|
|
|
std::vector<AudioDeviceSpecification> PulseAudioEngine::getAudioDeviceList(AudioDirection direction)
|
|
{
|
|
PulseAudioDeviceListTemp tempObj;
|
|
tempObj.thisPtr = this;
|
|
|
|
pa_operation* op = nullptr;
|
|
|
|
pa_threaded_mainloop_lock(mainloop_);
|
|
if (direction == AUDIO_ENGINE_OUT)
|
|
{
|
|
op = pa_context_get_sink_info_list(context_, [](pa_context *c, const pa_sink_info *i, int eol, void *userdata) {
|
|
PulseAudioDeviceListTemp* tempObj = static_cast<PulseAudioDeviceListTemp*>(userdata);
|
|
|
|
if (eol)
|
|
{
|
|
pa_threaded_mainloop_signal(tempObj->thisPtr->mainloop_, 0);
|
|
return;
|
|
}
|
|
|
|
AudioDeviceSpecification device;
|
|
device.deviceId = i->index;
|
|
device.name = wxString::FromUTF8(i->name);
|
|
device.apiName = "PulseAudio";
|
|
device.maxChannels = i->sample_spec.channels;
|
|
device.minChannels = 1; // TBD: can minimum be >1 on PulseAudio or pipewire?
|
|
device.defaultSampleRate = i->sample_spec.rate;
|
|
|
|
tempObj->result.push_back(device);
|
|
|
|
}, &tempObj);
|
|
}
|
|
else
|
|
{
|
|
op = pa_context_get_source_info_list(context_, [](pa_context *c, const pa_source_info *i, int eol, void *userdata) {
|
|
PulseAudioDeviceListTemp* tempObj = static_cast<PulseAudioDeviceListTemp*>(userdata);
|
|
|
|
if (eol)
|
|
{
|
|
pa_threaded_mainloop_signal(tempObj->thisPtr->mainloop_, 0);
|
|
return;
|
|
}
|
|
|
|
AudioDeviceSpecification device;
|
|
device.deviceId = i->index;
|
|
device.name = wxString::FromUTF8(i->name);
|
|
device.apiName = "PulseAudio";
|
|
device.maxChannels = i->sample_spec.channels;
|
|
device.minChannels = 1; // TBD: can minimum be >1 on PulseAudio or pipewire?
|
|
device.defaultSampleRate = i->sample_spec.rate;
|
|
|
|
tempObj->result.push_back(device);
|
|
}, &tempObj);
|
|
}
|
|
|
|
// Wait for the operation to complete
|
|
for(;;)
|
|
{
|
|
if (pa_operation_get_state(op) != PA_OPERATION_RUNNING) break;
|
|
pa_threaded_mainloop_wait(mainloop_);
|
|
}
|
|
|
|
pa_operation_unref(op);
|
|
|
|
// Get list of cards
|
|
op = pa_context_get_card_info_list(context_, [](pa_context *c, const pa_card_info *i, int eol, void *userdata)
|
|
{
|
|
PulseAudioDeviceListTemp* tempObj = static_cast<PulseAudioDeviceListTemp*>(userdata);
|
|
|
|
if (eol)
|
|
{
|
|
pa_threaded_mainloop_signal(tempObj->thisPtr->mainloop_, 0);
|
|
return;
|
|
}
|
|
|
|
tempObj->cardResult[i->index] = i->name;
|
|
}, &tempObj);
|
|
|
|
// Wait for the operation to complete
|
|
for(;;)
|
|
{
|
|
if (pa_operation_get_state(op) != PA_OPERATION_RUNNING) break;
|
|
pa_threaded_mainloop_wait(mainloop_);
|
|
}
|
|
|
|
pa_operation_unref(op);
|
|
|
|
pa_threaded_mainloop_unlock(mainloop_);
|
|
|
|
// Iterate over result and populate cardName
|
|
for (auto& obj : tempObj.result)
|
|
{
|
|
if (tempObj.cardResult.find(obj.deviceId) != tempObj.cardResult.end())
|
|
{
|
|
obj.cardName = wxString::FromUTF8(tempObj.cardResult[obj.deviceId].c_str());
|
|
}
|
|
}
|
|
|
|
return tempObj.result;
|
|
}
|
|
|
|
std::vector<int> PulseAudioEngine::getSupportedSampleRates(wxString deviceName, AudioDirection direction)
|
|
{
|
|
std::vector<int> result;
|
|
|
|
int index = 0;
|
|
while (IAudioEngine::StandardSampleRates[index] != -1)
|
|
{
|
|
if (IAudioEngine::StandardSampleRates[index] <= 192000)
|
|
{
|
|
result.push_back(IAudioEngine::StandardSampleRates[index]);
|
|
}
|
|
index++;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
struct PaDefaultAudioDeviceTemp
|
|
{
|
|
std::string defaultSink;
|
|
std::string defaultSource;
|
|
pa_threaded_mainloop *mainloop;
|
|
};
|
|
|
|
AudioDeviceSpecification PulseAudioEngine::getDefaultAudioDevice(AudioDirection direction)
|
|
{
|
|
PaDefaultAudioDeviceTemp tempData;
|
|
tempData.mainloop = mainloop_;
|
|
|
|
pa_threaded_mainloop_lock(mainloop_);
|
|
auto op = pa_context_get_server_info(context_, [](pa_context *c, const pa_server_info *i, void *userdata) {
|
|
PaDefaultAudioDeviceTemp* tempData = static_cast<PaDefaultAudioDeviceTemp*>(userdata);
|
|
|
|
tempData->defaultSink = i->default_sink_name;
|
|
tempData->defaultSource = i->default_source_name;
|
|
pa_threaded_mainloop_signal(tempData->mainloop, 0);
|
|
}, &tempData);
|
|
|
|
// Wait for the operation to complete
|
|
for(;;)
|
|
{
|
|
if (pa_operation_get_state(op) != PA_OPERATION_RUNNING) break;
|
|
pa_threaded_mainloop_wait(mainloop_);
|
|
}
|
|
|
|
pa_operation_unref(op);
|
|
pa_threaded_mainloop_unlock(mainloop_);
|
|
|
|
auto devices = getAudioDeviceList(direction);
|
|
std::string defaultDeviceName = direction == AUDIO_ENGINE_IN ? tempData.defaultSource : tempData.defaultSink;
|
|
for (auto& device : devices)
|
|
{
|
|
if (device.name == defaultDeviceName)
|
|
{
|
|
return device;
|
|
}
|
|
}
|
|
|
|
return AudioDeviceSpecification::GetInvalidDevice();
|
|
}
|
|
|
|
std::shared_ptr<IAudioDevice> PulseAudioEngine::getAudioDevice(wxString deviceName, AudioDirection direction, int sampleRate, int numChannels)
|
|
{
|
|
auto deviceList = getAudioDeviceList(direction);
|
|
|
|
auto supportedSampleRates = getSupportedSampleRates(deviceName, direction);
|
|
bool found = false;
|
|
for (auto& rate : supportedSampleRates)
|
|
{
|
|
if (rate == sampleRate)
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (auto& dev : deviceList)
|
|
{
|
|
if (dev.name == deviceName)
|
|
{
|
|
if (!found)
|
|
{
|
|
// Use device's default sample rate if we somehow got an unsupported one.
|
|
sampleRate = dev.defaultSampleRate;
|
|
}
|
|
|
|
// Cap number of channels to allowed range.
|
|
numChannels = std::max(numChannels, dev.minChannels);
|
|
numChannels = std::min(numChannels, dev.maxChannels);
|
|
|
|
// Create device object.
|
|
auto devObj =
|
|
new PulseAudioDevice(
|
|
mainloop_, context_, deviceName, direction, sampleRate,
|
|
dev.maxChannels >= numChannels ? numChannels : dev.maxChannels);
|
|
return std::shared_ptr<IAudioDevice>(devObj);
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|