musikcube/src/plugins/pulseout/PulseOut.cpp
2022-12-04 11:32:02 -08:00

399 lines
12 KiB
C++
Executable File

//////////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2007-2016 musikcube team
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// * Neither the name of the author nor the names of other contributors may
// be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
//////////////////////////////////////////////////////////////////////////////
#include "PulseOut.h"
#include <musikcore/sdk/constants.h>
#include <musikcore/sdk/IPreferences.h>
#include <pulse/volume.h>
#include <pulse/pulseaudio.h>
#include <pulse/thread-mainloop.h>
#include <math.h>
#include <vector>
using namespace musik::core::sdk;
typedef std::unique_lock<std::recursive_mutex> Lock;
typedef musik::core::sdk::IOutput IOutput;
static musik::core::sdk::IPreferences* prefs = nullptr;
#define PREF_FORCE_LINEAR_VOLUME "force_linear_volume"
#define PREF_DEVICE_ID "device_id"
class PulseDevice : public musik::core::sdk::IDevice {
public:
PulseDevice(const std::string& id, const std::string& name) {
this->id = id;
this->name = name;
}
virtual void Release() {
delete this;
}
virtual const char* Name() const {
return name.c_str();
}
virtual const char* Id() const {
return id.c_str();
}
private:
std::string name, id;
};
class PulseDeviceList : public musik::core::sdk::IDeviceList {
public:
virtual void Release() {
delete this;
}
virtual size_t Count() const {
return devices.size();
}
virtual const IDevice* At(size_t index) const {
return &devices.at(index);
}
void Add(const std::string& id, const std::string& name) {
devices.push_back(PulseDevice(id, name));
}
private:
std::vector<PulseDevice> devices;
};
struct DeviceListContext {
pa_threaded_mainloop* mainLoop;
PulseDeviceList* devices;
};
static void deviceEnumerator(pa_context* context, const pa_sink_info* info, int eol, void* userdata) {
DeviceListContext* deviceListContext = (DeviceListContext*) userdata;
if (info) {
deviceListContext->devices->Add(info->name, info->description);
}
if (eol) {
pa_threaded_mainloop_signal(deviceListContext->mainLoop, 0);
}
}
static std::string getDeviceId() {
return getPreferenceString<std::string>(prefs, PREF_DEVICE_ID, "");
}
extern "C" void SetPreferences(musik::core::sdk::IPreferences* prefs) {
::prefs = prefs;
prefs->GetBool(PREF_FORCE_LINEAR_VOLUME, false);
prefs->GetString(PREF_DEVICE_ID, nullptr, 0, "");
prefs->Save();
}
PulseOut::PulseOut() {
std::cerr << "PulseOut::PulseOut() called" << std::endl;
this->audioConnection = nullptr;
this->state = StateStopped;
this->volume = 1.0f;
this->volumeUpdated = false;
this->channels = 0;
this->rate = 0;
this->linearVolume = false;
}
PulseOut::~PulseOut() {
std::cerr << "PulseOut: destructor\n";
this->CloseDevice();
}
void PulseOut::CloseDevice() {
Lock lock(this->stateMutex);
if (this->audioConnection) {
std::cerr << "PulseOut: closing device\n";
int error = 0;
pa_blocking_flush(this->audioConnection, &error);
pa_blocking_free(this->audioConnection);
this->audioConnection = nullptr;
this->rate = 0;
this->channels = 0;
}
}
void PulseOut::Drain() {
Lock lock(this->stateMutex);
if (this->state != StateStopped && this->audioConnection) {
std::cerr << "draining...\n";
pa_blocking_drain(this->audioConnection, 0);
std::cerr << "drained...\n";
}
}
musik::core::sdk::IDevice* PulseOut::GetDefaultDevice() {
return findDeviceById<PulseDevice, IOutput>(this, getDeviceId());
}
bool PulseOut::SetDefaultDevice(const char* deviceId) {
return setDefaultDevice<IPreferences, PulseDevice, IOutput>(prefs, this, PREF_DEVICE_ID, deviceId);
}
musik::core::sdk::IDeviceList* PulseOut::GetDeviceList() {
PulseDeviceList* result = new PulseDeviceList();
auto mainLoop = pa_threaded_mainloop_new();
if (mainLoop) {
auto context = pa_context_new(pa_threaded_mainloop_get_api(mainLoop), "musikcube");
if (context) {
if (pa_context_connect(context, nullptr, (pa_context_flags_t) 0, nullptr) >= 0) {
if (pa_threaded_mainloop_start(mainLoop) >= 0) {
bool contextOk = false;
for (;;) {
pa_context_state_t state;
state = pa_context_get_state(context);
if (state == PA_CONTEXT_READY) {
contextOk = true;
break;
}
else if (state == PA_CONTEXT_FAILED || state == PA_CONTEXT_TERMINATED) {
break;
}
pa_threaded_mainloop_wait(mainLoop);
}
pa_threaded_mainloop_lock(mainLoop);
if (contextOk) {
DeviceListContext dlc;
dlc.mainLoop = mainLoop;
dlc.devices = result;
auto op = pa_context_get_sink_info_list(context, deviceEnumerator, (void*) &dlc);
if (op) {
while (pa_operation_get_state(op) == PA_OPERATION_RUNNING) {
pa_threaded_mainloop_wait(mainLoop);
}
pa_operation_unref(op);
}
}
pa_threaded_mainloop_unlock(mainLoop);
}
pa_context_disconnect(context);
pa_context_unref(context);
}
}
pa_threaded_mainloop_stop(mainLoop);
pa_threaded_mainloop_free(mainLoop);
}
return result;
}
std::string PulseOut::GetPreferredDeviceId() {
std::string deviceId = getDeviceId();
auto device = findDeviceById<PulseDevice>(this, deviceId);
if (device) {
device->Release();
return deviceId;
}
return "";
}
void PulseOut::OpenDevice(musik::core::sdk::IBuffer* buffer) {
if (!this->audioConnection ||
this->rate != buffer->SampleRate() ||
this->channels != buffer->Channels())
{
int errorCode = 0;
this->CloseDevice();
pa_sample_spec spec;
spec.format = PA_SAMPLE_FLOAT32LE;
spec.channels = buffer->Channels();
spec.rate = buffer->SampleRate();
std::string deviceId = this->GetPreferredDeviceId();
std::cerr << "PulseOut: opening device " << deviceId << "\n";
/* output to preferred device id, as specified in prefs */
this->audioConnection = pa_blocking_new(
nullptr,
"musikcube",
PA_STREAM_PLAYBACK,
deviceId.size() ? deviceId.c_str() : nullptr,
"music",
&spec,
nullptr,
nullptr,
&errorCode);
if (!this->audioConnection) {
std::cerr << "PulseOut: failed to open device. errorCode=" << errorCode << "\n";
}
if (!this->audioConnection && deviceId.size()) {
/* fall back to default if preferred is not found */
this->audioConnection = pa_blocking_new(
nullptr,
"musikcube",
PA_STREAM_PLAYBACK,
nullptr,
"music",
&spec,
nullptr,
nullptr,
&errorCode);
if (!this->audioConnection) {
std::cerr << "PulseOut: failed to open default device. errorCode=" << errorCode << "\n";
}
}
if (this->audioConnection) {
this->rate = buffer->SampleRate();
this->channels = buffer->Channels();
this->state = StatePlaying;
this->linearVolume = ::prefs->GetBool(PREF_FORCE_LINEAR_VOLUME, false);
this->SetVolume(this->volume);
}
}
}
void PulseOut::Release() {
std::cerr << "PulseOut: destroy\n";
delete this;
}
void PulseOut::Stop() {
Lock lock(this->stateMutex);
if (this->audioConnection) {
pa_blocking_flush(this->audioConnection, 0);
this->state = StateStopped;
}
}
void PulseOut::Pause() {
Lock lock(this->stateMutex);
if (this->audioConnection) {
pa_blocking_flush(this->audioConnection, 0);
this->state = StatePaused;
}
}
void PulseOut::Resume() {
Lock lock(this->stateMutex);
if (this->audioConnection) {
this->state = StatePlaying;
}
}
void PulseOut::SetVolume(double volume) {
Lock lock(this->stateMutex);
if (volume > 1.0) { volume = 1.0; }
if (volume < 0) { volume = 0; }
this->volume = volume;
this->volumeUpdated = false;
if (this->audioConnection) {
int normalized;
if (this->linearVolume) {
normalized = (int) round((double) PA_VOLUME_NORM * volume);
}
else {
normalized = (int) pa_sw_volume_from_linear(this->volume);
}
this->volumeUpdated = pa_blocking_set_volume(this->audioConnection, normalized, 0) == 0;
//std::cerr << "PulseOut: volumeUpdated = " << this->volumeUpdated << ", value = " << normalized << "\n";
}
}
double PulseOut::GetVolume() {
return this->volume;
}
OutputState PulseOut::Play(IBuffer *buffer, IBufferProvider* provider) {
int error = 0;
{
Lock lock(this->stateMutex);
if (this->state == StatePaused) {
return OutputState::InvalidState;
}
this->OpenDevice(buffer);
if (!this->audioConnection || this->state != StatePlaying) {
return OutputState::InvalidState;
}
if (!this->volumeUpdated) {
this->SetVolume(this->volume);
}
pa_blocking_write(
this->audioConnection,
buffer->BufferPointer(),
buffer->Samples() * sizeof(float),
&error);
if (error > 0) {
this->CloseDevice();
return (OutputState) 1000; /* try again in a second */
}
}
provider->OnBufferProcessed(buffer);
return OutputState::BufferWritten;
}
double PulseOut::Latency() {
Lock lock(this->stateMutex);
if (!this->audioConnection) {
return 0.0;
}
int error = 0;
unsigned long long latency = pa_blocking_get_latency(this->audioConnection, &error);
return (double) latency / 1000000.0; /* microseconds to seconds */
}