// Copyright 2007-2023 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

#include "PulseAudio.h"

#include "MainWindow.h"
#include "Timer.h"
#include "User.h"
#include "Global.h"

#include <cstdint>
#include <vector>

#ifdef Q_CC_GNU
#	define RESOLVE(var)                                                          \
		{                                                                         \
			var = reinterpret_cast< __typeof__(var) >(m_lib.resolve("pa_" #var)); \
			if (!var)                                                             \
				return;                                                           \
		}
#else
#	define RESOLVE(var)                                                                           \
		{                                                                                          \
			*reinterpret_cast< void ** >(&var) = static_cast< void * >(m_lib.resolve("pa_" #var)); \
			if (!var)                                                                              \
				return;                                                                            \
		}
#endif

static const char *mumble_sink_input = "Mumble Speakers";
static const char *mumble_echo       = "Mumble Speakers (Echo)";

static PulseAudioSystem *pasys = nullptr;

#define NBLOCKS 8

class PulseAudioInputRegistrar : public AudioInputRegistrar {
public:
	PulseAudioInputRegistrar();
	virtual AudioInput *create();
	virtual const QVariant getDeviceChoice();
	virtual const QList< audioDevice > getDeviceChoices();
	virtual void setDeviceChoice(const QVariant &, Settings &);
	virtual bool canEcho(EchoCancelOptionID echoCancelID, const QString &outputSystem) const;
	virtual bool isMicrophoneAccessDeniedByOS() { return false; };
};


class PulseAudioOutputRegistrar : public AudioOutputRegistrar {
public:
	PulseAudioOutputRegistrar();
	virtual AudioOutput *create();
	virtual const QVariant getDeviceChoice();
	virtual const QList< audioDevice > getDeviceChoices();
	virtual void setDeviceChoice(const QVariant &, Settings &);
	bool canMuteOthers() const;
};

class PulseAudioInit : public DeferInit {
public:
	PulseAudioInputRegistrar *airPulseAudio;
	PulseAudioOutputRegistrar *aorPulseAudio;
	void initialize() {
		pasys = new PulseAudioSystem();
		if (pasys->bPulseIsGood) {
			airPulseAudio = new PulseAudioInputRegistrar();
			aorPulseAudio = new PulseAudioOutputRegistrar();
		} else {
			airPulseAudio = nullptr;
			aorPulseAudio = nullptr;
			delete pasys;
			pasys = nullptr;
		}
	};
	void destroy() {
		delete airPulseAudio;
		delete aorPulseAudio;
		delete pasys;
		pasys = nullptr;
	};
};

static PulseAudioInit pulseinit;

PulseAudioSystem::PulseAudioSystem() {
	pasInput = pasOutput = pasSpeaker = nullptr;
	bSourceDone = bSinkDone = bServerDone = false;
	iDelayCache                           = 0;
	bAttenuating                          = false;
	iRemainingOperations                  = 0;
	bPulseIsGood                          = false;
	iSinkId                               = -1;
	bRunning                              = false;

	if (!m_pulseAudio.m_ok) {
		return;
	}

	pam                  = m_pulseAudio.threaded_mainloop_new();
	pa_mainloop_api *api = m_pulseAudio.threaded_mainloop_get_api(pam);

	pa_proplist *proplist = m_pulseAudio.proplist_new();
	m_pulseAudio.proplist_sets(proplist, PA_PROP_APPLICATION_NAME, "Mumble");
	m_pulseAudio.proplist_sets(proplist, PA_PROP_APPLICATION_ID, "net.sourceforge.mumble.mumble");
	m_pulseAudio.proplist_sets(proplist, PA_PROP_APPLICATION_ICON_NAME, "mumble");
	m_pulseAudio.proplist_sets(proplist, PA_PROP_MEDIA_ROLE, "game");

	pacContext = m_pulseAudio.context_new_with_proplist(api, nullptr, proplist);
	m_pulseAudio.proplist_free(proplist);

	m_pulseAudio.context_set_subscribe_callback(pacContext, subscribe_callback, this);

	m_pulseAudio.context_set_state_callback(pacContext, context_state_callback, this);
	m_pulseAudio.context_connect(pacContext, nullptr, PA_CONTEXT_NOAUTOSPAWN, nullptr);

	pade = api->defer_new(api, defer_event_callback, this);
	api->defer_enable(pade, false);

	m_pulseAudio.threaded_mainloop_start(pam);

	bRunning = true;

	std::unique_lock< std::mutex > guard(m_initLock);
	if (!m_initialized) {
		// The mutex is atomically released as soon as this thread starts waiting and will be
		// re-acquired when waking up.
		// Spurious wake-ups are avoided by checking m_initialized in the given predicate
		m_initWaiter.wait(guard, [this]() { return m_initialized; });
	}
}

PulseAudioSystem::~PulseAudioSystem() {
	if (!m_pulseAudio.m_ok) {
		return;
	}

	bRunning = false;

	if (bAttenuating) {
		bAttenuating = false;
		setVolumes();
	}
	m_pulseAudio.threaded_mainloop_stop(pam);
	m_pulseAudio.context_disconnect(pacContext);
	m_pulseAudio.context_unref(pacContext);
	m_pulseAudio.threaded_mainloop_free(pam);
}

QString PulseAudioSystem::outputDevice() const {
	QString odev = Global::get().s.qsPulseAudioOutput;
	if (odev.isEmpty()) {
		odev = qsDefaultOutput;
	}
	if (!qhOutput.contains(odev)) {
		odev = qsDefaultOutput;
	}
	return odev;
}

QString PulseAudioSystem::inputDevice() const {
	QString idev = Global::get().s.qsPulseAudioInput;
	if (idev.isEmpty()) {
		idev = qsDefaultInput;
	}
	if (!qhInput.contains(idev)) {
		idev = qsDefaultInput;
	}
	return idev;
}

void PulseAudioSystem::wakeup() {
	pa_mainloop_api *api = m_pulseAudio.threaded_mainloop_get_api(pam);
	api->defer_enable(pade, true);
}

void PulseAudioSystem::wakeup_lock() {
	m_pulseAudio.threaded_mainloop_lock(pam);
	pa_mainloop_api *api = m_pulseAudio.threaded_mainloop_get_api(pam);
	api->defer_enable(pade, true);
	m_pulseAudio.threaded_mainloop_unlock(pam);
}

void PulseAudioSystem::defer_event_callback(pa_mainloop_api *a, pa_defer_event *e, void *userdata) {
	PulseAudioSystem *pas = reinterpret_cast< PulseAudioSystem * >(userdata);
	pas->eventCallback(a, e);
}

void PulseAudioSystem::eventCallback(pa_mainloop_api *api, pa_defer_event *) {
	api->defer_enable(pade, false);

	if (!bSourceDone || !bSinkDone || !bServerDone)
		return;

	AudioInputPtr ai      = Global::get().ai;
	AudioOutputPtr ao     = Global::get().ao;
	AudioInput *raw_ai    = ai.get();
	AudioOutput *raw_ao   = ao.get();
	PulseAudioInput *pai  = dynamic_cast< PulseAudioInput * >(raw_ai);
	PulseAudioOutput *pao = dynamic_cast< PulseAudioOutput * >(raw_ao);

	if (raw_ao) {
		QString odev        = outputDevice();
		pa_stream_state ost = pasOutput ? m_pulseAudio.stream_get_state(pasOutput) : PA_STREAM_TERMINATED;
		bool do_stop        = false;
		bool do_start       = false;

		if (!pao && (ost == PA_STREAM_READY)) {
			do_stop = true;
		} else if (pao) {
			switch (ost) {
				case PA_STREAM_TERMINATED: {
					if (pasOutput)
						m_pulseAudio.stream_unref(pasOutput);

					pa_sample_spec pss = qhSpecMap.value(odev);
					pa_channel_map pcm = qhChanMap.value(odev);
					if ((pss.format != PA_SAMPLE_FLOAT32NE) && (pss.format != PA_SAMPLE_S16NE))
						pss.format = PA_SAMPLE_FLOAT32NE;
					pss.rate = SAMPLE_RATE;
					if (pss.channels == 0)
						pss.channels = 1;

					pasOutput = m_pulseAudio.stream_new(pacContext, mumble_sink_input, &pss,
														(pss.channels == 1) ? nullptr : &pcm);
					m_pulseAudio.stream_set_state_callback(pasOutput, write_stream_callback, this);
					m_pulseAudio.stream_set_write_callback(pasOutput, write_callback, this);
				}
					// Fallthrough
				case PA_STREAM_UNCONNECTED:
					do_start = true;
					break;
				case PA_STREAM_READY: {
					if (Global::get().s.iOutputDelay != iDelayCache) {
						do_stop = true;
					} else if (odev != qsOutputCache) {
						do_stop = true;
					}
					break;
				}
				default:
					break;
			}
		}
		if (do_stop) {
			qWarning("PulseAudio: Stopping output");
			m_pulseAudio.stream_disconnect(pasOutput);
			iSinkId = -1;
		} else if (do_start) {
			qWarning("PulseAudio: Starting output: %s", qPrintable(odev));
			pa_buffer_attr buff;
			const pa_sample_spec *pss    = m_pulseAudio.stream_get_sample_spec(pasOutput);
			const size_t sampleSize      = (pss->format == PA_SAMPLE_FLOAT32NE) ? sizeof(float) : sizeof(short);
			const unsigned int iBlockLen = pao->iFrameSize * pss->channels * static_cast< unsigned int >(sampleSize);
			buff.tlength                 = iBlockLen * static_cast< unsigned int >(Global::get().s.iOutputDelay + 1);
			buff.minreq                  = iBlockLen;
			buff.maxlength               = static_cast< decltype(buff.maxlength) >(-1);
			buff.prebuf                  = static_cast< decltype(buff.prebuf) >(-1);
			buff.fragsize                = iBlockLen;

			iDelayCache   = Global::get().s.iOutputDelay;
			qsOutputCache = odev;

			m_pulseAudio.stream_connect_playback(pasOutput, qPrintable(odev), &buff, PA_STREAM_ADJUST_LATENCY, nullptr,
												 nullptr);
			m_pulseAudio.context_get_sink_info_by_name(pacContext, qPrintable(odev), sink_info_callback, this);
		}
	}

	if (raw_ai) {
		QString idev        = inputDevice();
		pa_stream_state ist = pasInput ? m_pulseAudio.stream_get_state(pasInput) : PA_STREAM_TERMINATED;
		bool do_stop        = false;
		bool do_start       = false;

		if (!pai && (ist == PA_STREAM_READY)) {
			do_stop = true;
		} else if (pai) {
			switch (ist) {
				case PA_STREAM_TERMINATED: {
					if (pasInput)
						m_pulseAudio.stream_unref(pasInput);

					pa_sample_spec pss = qhSpecMap.value(idev);
					if ((pss.format != PA_SAMPLE_FLOAT32NE) && (pss.format != PA_SAMPLE_S16NE))
						pss.format = PA_SAMPLE_FLOAT32NE;
					pss.rate     = SAMPLE_RATE;
					pss.channels = 1;

					pasInput = m_pulseAudio.stream_new(pacContext, "Microphone", &pss, nullptr);
					m_pulseAudio.stream_set_state_callback(pasInput, read_stream_callback, this);
					m_pulseAudio.stream_set_read_callback(pasInput, read_callback, this);
				}
					// Fallthrough
				case PA_STREAM_UNCONNECTED:
					do_start = true;
					break;
				case PA_STREAM_READY: {
					if (idev != qsInputCache) {
						do_stop = true;
					}
					break;
				}
				default:
					break;
			}
		}
		if (do_stop) {
			qWarning("PulseAudio: Stopping input");
			m_pulseAudio.stream_disconnect(pasInput);
		} else if (do_start) {
			qWarning("PulseAudio: Starting input %s", qPrintable(idev));
			pa_buffer_attr buff;
			const pa_sample_spec *pss = m_pulseAudio.stream_get_sample_spec(pasInput);
			const size_t sampleSize   = (pss->format == PA_SAMPLE_FLOAT32NE) ? sizeof(float) : sizeof(short);
			const unsigned int iBlockLen =
				static_cast< unsigned int >(static_cast< unsigned int >(pai->iFrameSize) * pss->channels * sampleSize);
			buff.tlength   = iBlockLen;
			buff.minreq    = iBlockLen;
			buff.maxlength = static_cast< decltype(buff.maxlength) >(-1);
			buff.prebuf    = static_cast< decltype(buff.prebuf) >(-1);
			buff.fragsize  = iBlockLen;

			qsInputCache = idev;

			m_pulseAudio.stream_connect_record(pasInput, qPrintable(idev), &buff, PA_STREAM_ADJUST_LATENCY);
		}
	}

	if (raw_ai) {
		QString odev        = outputDevice();
		QString edev        = qhEchoMap.value(odev);
		pa_stream_state est = pasSpeaker ? m_pulseAudio.stream_get_state(pasSpeaker) : PA_STREAM_TERMINATED;
		bool do_stop        = false;
		bool do_start       = false;

		if ((!pai || Global::get().s.echoOption == EchoCancelOptionID::DISABLED) && (est == PA_STREAM_READY)) {
			do_stop = true;
		} else if (pai && Global::get().s.echoOption != EchoCancelOptionID::DISABLED) {
			switch (est) {
				case PA_STREAM_TERMINATED: {
					if (pasSpeaker)
						m_pulseAudio.stream_unref(pasSpeaker);

					pa_sample_spec pss = qhSpecMap.value(edev);
					pa_channel_map pcm = qhChanMap.value(edev);
					if ((pss.format != PA_SAMPLE_FLOAT32NE) && (pss.format != PA_SAMPLE_S16NE))
						pss.format = PA_SAMPLE_FLOAT32NE;
					pss.rate = SAMPLE_RATE;
					if ((pss.channels == 0) || (Global::get().s.echoOption != EchoCancelOptionID::SPEEX_MULTICHANNEL))
						pss.channels = 1;

					pasSpeaker =
						m_pulseAudio.stream_new(pacContext, mumble_echo, &pss, (pss.channels == 1) ? nullptr : &pcm);
					m_pulseAudio.stream_set_state_callback(pasSpeaker, read_stream_callback, this);
					m_pulseAudio.stream_set_read_callback(pasSpeaker, read_callback, this);
				}
					// Fallthrough
				case PA_STREAM_UNCONNECTED:
					do_start = true;
					break;
				case PA_STREAM_READY: {
					if ((Global::get().s.echoOption == EchoCancelOptionID::SPEEX_MULTICHANNEL) != bEchoMultiCache) {
						do_stop = true;
					} else if (edev != qsEchoCache) {
						do_stop = true;
					}
					break;
				}
				default:
					break;
			}
		}
		if (do_stop) {
			qWarning("PulseAudio: Stopping echo");
			m_pulseAudio.stream_disconnect(pasSpeaker);
		} else if (do_start) {
			qWarning("PulseAudio: Starting echo: %s", qPrintable(edev));
			pa_buffer_attr buff;
			const pa_sample_spec *pss = m_pulseAudio.stream_get_sample_spec(pasSpeaker);
			const size_t sampleSize   = (pss->format == PA_SAMPLE_FLOAT32NE) ? sizeof(float) : sizeof(short);
			const unsigned int iBlockLen =
				static_cast< unsigned int >(static_cast< unsigned int >(pai->iFrameSize) * pss->channels * sampleSize);
			buff.tlength   = iBlockLen;
			buff.minreq    = iBlockLen;
			buff.maxlength = static_cast< decltype(buff.maxlength) >(-1);
			buff.prebuf    = static_cast< decltype(buff.prebuf) >(-1);
			buff.fragsize  = iBlockLen;

			bEchoMultiCache = (Global::get().s.echoOption == EchoCancelOptionID::SPEEX_MULTICHANNEL);
			qsEchoCache     = edev;

			m_pulseAudio.stream_connect_record(pasSpeaker, qPrintable(edev), &buff, PA_STREAM_ADJUST_LATENCY);
		}
	}
}

void PulseAudioSystem::context_state_callback(pa_context *c, void *userdata) {
	PulseAudioSystem *pas = reinterpret_cast< PulseAudioSystem * >(userdata);
	pas->contextCallback(c);
}

void PulseAudioSystem::subscribe_callback(pa_context *, pa_subscription_event_type evt, unsigned int, void *userdata) {
	switch (evt & PA_SUBSCRIPTION_EVENT_TYPE_MASK) {
		case PA_SUBSCRIPTION_EVENT_NEW:
		case PA_SUBSCRIPTION_EVENT_REMOVE:
			break;
		default:
			return;
	}
	switch (evt & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {
		case PA_SUBSCRIPTION_EVENT_SINK:
		case PA_SUBSCRIPTION_EVENT_SOURCE:
			break;
		default:
			return;
	}
	PulseAudioSystem *pas = reinterpret_cast< PulseAudioSystem * >(userdata);
	qWarning("PulseAudio: Sinks or inputs changed (inserted or removed sound card)");
	pas->query();
}

void PulseAudioSystem::sink_callback(pa_context *, const pa_sink_info *i, int eol, void *userdata) {
	PulseAudioSystem *pas = reinterpret_cast< PulseAudioSystem * >(userdata);
	if (!i || eol) {
		pas->bSinkDone = true;
		pas->wakeup();
		return;
	}

	const QString name = QString::fromUtf8(i->name);

	pas->qhSpecMap.insert(name, i->sample_spec);
	pas->qhChanMap.insert(name, i->channel_map);
	pas->qhOutput.insert(name, QString::fromUtf8(i->description));
	pas->qhEchoMap.insert(name, QString::fromUtf8(i->monitor_source_name));
}

void PulseAudioSystem::source_callback(pa_context *, const pa_source_info *i, int eol, void *userdata) {
	PulseAudioSystem *pas = reinterpret_cast< PulseAudioSystem * >(userdata);
	if (!i || eol) {
		pas->bSourceDone = true;
		pas->wakeup();
		return;
	}

	const QString name = QString::fromUtf8(i->name);

	pas->qhSpecMap.insert(name, i->sample_spec);
	pas->qhChanMap.insert(name, i->channel_map);

	pas->qhInput.insert(QString::fromUtf8(i->name), QString::fromUtf8(i->description));
}

void PulseAudioSystem::server_callback(pa_context *, const pa_server_info *i, void *userdata) {
	PulseAudioSystem *pas = reinterpret_cast< PulseAudioSystem * >(userdata);

	pas->qsDefaultInput  = QString::fromUtf8(i->default_source_name);
	pas->qsDefaultOutput = QString::fromUtf8(i->default_sink_name);

	pas->bServerDone = true;
	pas->wakeup();
}

void PulseAudioSystem::sink_info_callback(pa_context *, const pa_sink_info *i, int eol, void *userdata) {
	PulseAudioSystem *pas = reinterpret_cast< PulseAudioSystem * >(userdata);
	if (!i || eol) {
		return;
	}

	pas->iSinkId = static_cast< int >(i->index);
}

void PulseAudioSystem::write_stream_callback(pa_stream *s, void *userdata) {
	PulseAudioSystem *pas = reinterpret_cast< PulseAudioSystem * >(userdata);
	const auto &pa        = pas->m_pulseAudio;

	switch (pa.stream_get_state(s)) {
		case PA_STREAM_FAILED:
			qWarning("PulseAudio: Stream error: %s", pa.strerror(pa.context_errno(pa.stream_get_context(s))));
			break;
		default:
			break;
	}
	const pa_buffer_attr *bufferAttr;
	if ((bufferAttr = pa.stream_get_buffer_attr(s))) {
		Global::get().ao->setBufferSize(bufferAttr->maxlength);
	}
	pas->wakeup();
}

void PulseAudioSystem::read_stream_callback(pa_stream *s, void *userdata) {
	PulseAudioSystem *pas = reinterpret_cast< PulseAudioSystem * >(userdata);
	const auto &pa        = pas->m_pulseAudio;

	switch (pa.stream_get_state(s)) {
		case PA_STREAM_FAILED:
			qWarning("PulseAudio: Stream error: %s", pa.strerror(pa.context_errno(pa.stream_get_context(s))));
			break;
		default:
			break;
	}
	pas->wakeup();
}

void PulseAudioSystem::read_callback(pa_stream *s, size_t bytes, void *userdata) {
	PulseAudioSystem *pas = reinterpret_cast< PulseAudioSystem * >(userdata);
	const auto &pa        = pas->m_pulseAudio;

	size_t length    = bytes;
	const void *data = nullptr;
	pa.stream_peek(s, &data, &length);
	if (!data && length > 0) {
		qWarning("PulseAudio: pa_stream_peek reports no data at current read index.");
	} else if (!data && length == 0) {
		qWarning("PulseAudio: pa_stream_peek reports empty memblockq.");
	} else if (!data || length == 0) {
		qWarning("PulseAudio: invalid pa_stream_peek state encountered.");
		return;
	}

	AudioInputPtr ai     = Global::get().ai;
	PulseAudioInput *pai = dynamic_cast< PulseAudioInput * >(ai.get());
	if (!pai) {
		if (length > 0) {
			pa.stream_drop(s);
		}
		pas->wakeup();
		return;
	}

	const pa_sample_spec *pss = pa.stream_get_sample_spec(s);

	if (s == pas->pasInput) {
		if (!pa.sample_spec_equal(pss, &pai->pssMic)) {
			pai->pssMic       = *pss;
			pai->iMicFreq     = pss->rate;
			pai->iMicChannels = pss->channels;
			if (pss->format == PA_SAMPLE_FLOAT32NE)
				pai->eMicFormat = PulseAudioInput::SampleFloat;
			else
				pai->eMicFormat = PulseAudioInput::SampleShort;
			pai->initializeMixer();
		}
		if (data) {
			pai->addMic(data, static_cast< unsigned int >(length) / pai->iMicSampleSize);
		}
	} else if (s == pas->pasSpeaker) {
		if (!pa.sample_spec_equal(pss, &pai->pssEcho)) {
			pai->pssEcho       = *pss;
			pai->iEchoFreq     = pss->rate;
			pai->iEchoChannels = pss->channels;
			if (pss->format == PA_SAMPLE_FLOAT32NE)
				pai->eEchoFormat = PulseAudioInput::SampleFloat;
			else
				pai->eEchoFormat = PulseAudioInput::SampleShort;
			pai->initializeMixer();
		}
		if (data) {
			pai->addEcho(data, static_cast< unsigned int >(length) / pai->iEchoSampleSize);
		}
	}

	if (length > 0) {
		pa.stream_drop(s);
	}
}

void PulseAudioSystem::write_callback(pa_stream *s, size_t bytes, void *userdata) {
	PulseAudioSystem *pas = reinterpret_cast< PulseAudioSystem * >(userdata);
	Q_ASSERT(s == pas->pasOutput);

	const PulseAudio &pa = pas->m_pulseAudio;

	AudioOutputPtr ao     = Global::get().ao;
	PulseAudioOutput *pao = dynamic_cast< PulseAudioOutput * >(ao.get());

	if (!pao) {
		// The AudioSystem pointer is invalid, probably because of an AudioSystem restart.
		// However, PA expects this callback to return exactly the provided number of
		// bytes. Failing to do so will cause PA to never call this again,
		// effectively removing the audio output completely until Mumble is restarted.
		// See: https://github.com/mumble-voip/mumble/issues/4883
		// See: https://gitlab.freedesktop.org/pulseaudio/pulseaudio/-/issues/1132
		static std::vector< std::uint8_t > m_silenceBuffer;
		m_silenceBuffer.resize(bytes);

		pa.stream_write(s, m_silenceBuffer.data(), bytes, nullptr, 0, PA_SEEK_RELATIVE);
		return;
	}

	const pa_sample_spec *pss = pa.stream_get_sample_spec(s);
	const pa_channel_map *pcm = pa.stream_get_channel_map(pas->pasOutput);
	if (!pa.sample_spec_equal(pss, &pao->pss) || !pa.channel_map_equal(pcm, &pao->pcm)) {
		pao->pss = *pss;
		pao->pcm = *pcm;
		if (pss->format == PA_SAMPLE_FLOAT32NE)
			pao->eSampleFormat = PulseAudioOutput::SampleFloat;
		else
			pao->eSampleFormat = PulseAudioOutput::SampleShort;
		pao->iMixerFreq = pss->rate;
		pao->iChannels  = pss->channels;
		static std::vector< unsigned int > chanmasks;
		chanmasks.resize(pss->channels);
		for (int i = 0; i < pss->channels; ++i) {
			unsigned int cm = 0;
			switch (pcm->map[i]) {
				case PA_CHANNEL_POSITION_LEFT:
					cm = SPEAKER_FRONT_LEFT;
					break;
				case PA_CHANNEL_POSITION_RIGHT:
					cm = SPEAKER_FRONT_RIGHT;
					break;
				case PA_CHANNEL_POSITION_CENTER:
					cm = SPEAKER_FRONT_CENTER;
					break;
				case PA_CHANNEL_POSITION_REAR_LEFT:
					cm = SPEAKER_BACK_LEFT;
					break;
				case PA_CHANNEL_POSITION_REAR_RIGHT:
					cm = SPEAKER_BACK_RIGHT;
					break;
				case PA_CHANNEL_POSITION_REAR_CENTER:
					cm = SPEAKER_BACK_CENTER;
					break;
				case PA_CHANNEL_POSITION_LFE:
					cm = SPEAKER_LOW_FREQUENCY;
					break;
				case PA_CHANNEL_POSITION_SIDE_LEFT:
					cm = SPEAKER_SIDE_LEFT;
					break;
				case PA_CHANNEL_POSITION_SIDE_RIGHT:
					cm = SPEAKER_SIDE_RIGHT;
					break;
				case PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER:
					cm = SPEAKER_FRONT_LEFT_OF_CENTER;
					break;
				case PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER:
					cm = SPEAKER_FRONT_RIGHT_OF_CENTER;
					break;
				default:
					cm = 0;
					break;
			}
			chanmasks[static_cast< std::size_t >(i)] = cm;
		}
		pao->initializeMixer(chanmasks.data());
	}

	const unsigned int iSampleSize = pao->iSampleSize;
	const unsigned int samples     = static_cast< unsigned int >(bytes) / iSampleSize;
	bool oldAttenuation            = pas->bAttenuating;

	static std::vector< unsigned char > buffer;
	buffer.resize(bytes);
	// do we have some mixed output?
	if (pao->mix(buffer.data(), samples)) {
		// attenuate if instructed to or it's in settings
		pas->bAttenuating = (Global::get().bAttenuateOthers || Global::get().s.bAttenuateOthers);

	} else {
		std::fill(buffer.begin(), buffer.end(), 0);

		// attenuate if intructed to (self-activated)
		pas->bAttenuating = Global::get().bAttenuateOthers;
	}

	// if the attenuation state has changed
	if (oldAttenuation != pas->bAttenuating) {
		pas->setVolumes();
	}

	pa.stream_write(s, buffer.data(), iSampleSize * samples, nullptr, 0, PA_SEEK_RELATIVE);
}

void PulseAudioSystem::volume_sink_input_list_callback(pa_context *c, const pa_sink_input_info *i, int eol,
													   void *userdata) {
	PulseAudioSystem *pas = reinterpret_cast< PulseAudioSystem * >(userdata);
	const auto &pa        = pas->m_pulseAudio;

	if (eol == 0) {
		// If we're using the default of "enable attenuation on all outputs" and output from an application is
		// loopbacked, both the loopback and the application will be attenuated leading to double attenuation.
		if (!Global::get().s.bOnlyAttenuateSameOutput && pas->iSinkId > -1 && !strcmp(i->driver, "module-loopback.c")) {
			return;
		}
		// If we're not attenuating different sinks and the input is not on this sink, don't attenuate. Or,
		// if the input is a loopback module and connected to Mumble's sink, also ignore it (loopbacks are used to
		// connect sinks). An attenuated loopback means an indirect application attenuation.
		if (Global::get().s.bOnlyAttenuateSameOutput && pas->iSinkId > -1) {
			if (int(i->sink) != pas->iSinkId
				|| (int(i->sink) == pas->iSinkId && !strcmp(i->driver, "module-loopback.c")
					&& !Global::get().s.bAttenuateLoopbacks)) {
				return;
			}
		}
		// ensure we're not attenuating ourselves!
		if (strcmp(i->name, mumble_sink_input) != 0) {
			// create a new entry
			PulseAttenuation patt;
			patt.index             = i->index;
			patt.name              = QString::fromUtf8(i->name);
			patt.stream_restore_id = QString::fromUtf8(pa.proplist_gets(i->proplist, "module-stream-restore.id"));
			patt.normal_volume     = i->volume;

			// calculate the attenuated volume
			pa_volume_t adj = static_cast< pa_volume_t >(PA_VOLUME_NORM * Global::get().s.fOtherVolume);
			pa.sw_cvolume_multiply_scalar(&patt.attenuated_volume, &i->volume, adj);

			// set it on the sink input
			pa.operation_unref(
				pa.context_set_sink_input_volume(c, i->index, &patt.attenuated_volume, nullptr, nullptr));

			// store it
			pas->qhVolumes[i->index] = patt;
		}

	} else if (eol < 0) {
		qWarning("PulseAudio: Sink input introspection error.");
	}
}

void PulseAudioSystem::restore_sink_input_list_callback(pa_context *c, const pa_sink_input_info *i, int eol,
														void *userdata) {
	PulseAudioSystem *pas = reinterpret_cast< PulseAudioSystem * >(userdata);
	const auto &pa        = pas->m_pulseAudio;

	if (eol == 0) {
		// if we were tracking this specific sink previously
		if (pas->qhVolumes.contains(i->index)) {
			// and if it has the attenuated volume we applied to it
			if (pa.cvolume_equal(&i->volume, &pas->qhVolumes[i->index].attenuated_volume) != 0) {
				// mark it as matched
				pas->qlMatchedSinks.append(i->index);

				// reset the volume to normal
				pas->iRemainingOperations++;
				pa.operation_unref(pa.context_set_sink_input_volume(
					c, i->index, &pas->qhVolumes[i->index].normal_volume, restore_volume_success_callback, pas));
			}

			// otherwise, save for matching at the end of iteration
		} else {
			QString restore_id = QString::fromUtf8(pa.proplist_gets(i->proplist, "module-stream-restore.id"));
			PulseAttenuation patt;
			patt.index                        = i->index;
			patt.normal_volume                = i->volume;
			pas->qhUnmatchedSinks[restore_id] = patt;
		}

	} else if (eol < 0) {
		qWarning("PulseAudio: Sink input introspection error.");

	} else {
		// build a list of missing streams by iterating our active list
		QHash< uint32_t, PulseAttenuation >::const_iterator it;
		for (it = pas->qhVolumes.constBegin(); it != pas->qhVolumes.constEnd(); ++it) {
			// skip if previously matched
			if (pas->qlMatchedSinks.contains(it.key())) {
				continue;
			}

			// check if the restore id matches. the only case where this would
			// happen is if the application was reopened during attenuation.
			if (pas->qhUnmatchedSinks.contains(it.value().stream_restore_id)) {
				PulseAttenuation active_sink = pas->qhUnmatchedSinks[it.value().stream_restore_id];
				// if the volume wasn't changed from our attenuation
				if (pa.cvolume_equal(&active_sink.normal_volume, &it.value().attenuated_volume) != 0) {
					// reset the volume to normal
					pas->iRemainingOperations++;
					pa.operation_unref(pa.context_set_sink_input_volume(c, active_sink.index, &it.value().normal_volume,
																		restore_volume_success_callback, pas));
				}
				continue;
			}

			// at this point, we don't know what happened to the sink. add
			// it to a list to check the stream restore database for.
			pas->qhMissingSinks[it.value().stream_restore_id] = it.value();
		}

		// clean up
		pas->qlMatchedSinks.clear();
		pas->qhUnmatchedSinks.clear();
		pas->qhVolumes.clear();

		// if we had missing sinks, check the stream restore database
		// to see if we can find and update them.
		if (pas->qhMissingSinks.count() > 0) {
			pas->iRemainingOperations++;
			pa.operation_unref(pa.ext_stream_restore_read(c, stream_restore_read_callback, pas));
		}

		// trigger the volume completion callback;
		// necessary so that shutdown actions are called
		restore_volume_success_callback(c, 1, pas);
	}
}

void PulseAudioSystem::stream_restore_read_callback(pa_context *c, const pa_ext_stream_restore_info *i, int eol,
													void *userdata) {
	PulseAudioSystem *pas = reinterpret_cast< PulseAudioSystem * >(userdata);
	const auto &pa        = pas->m_pulseAudio;

	if (eol == 0) {
		QString name = QString::fromUtf8(i->name);

		// were we looking for this restoration?
		if (pas->qhMissingSinks.contains(name)) {
			// make sure it still has the volume we gave it
			if (pa.cvolume_equal(&pas->qhMissingSinks[name].attenuated_volume, &i->volume) != 0) {
				// update the stream restore record
				pa_ext_stream_restore_info restore = *i;
				restore.volume                     = pas->qhMissingSinks[name].normal_volume;
				pas->iRemainingOperations++;
				pa.operation_unref(pa.ext_stream_restore_write(c, PA_UPDATE_REPLACE, &restore, 1, 1,
															   restore_volume_success_callback, pas));
			}

			pas->qhMissingSinks.remove(name);
		}

	} else if (eol < 0) {
		qWarning("PulseAudio: Couldn't read stream restore database.");
		pas->qhMissingSinks.clear();

	} else {
		// verify missing list is empty
		if (pas->qhMissingSinks.count() > 0) {
			qWarning("PulseAudio: Failed to match %d stream(s).", pas->qhMissingSinks.count());
			pas->qhMissingSinks.clear();
		}

		// trigger the volume completion callback;
		// necessary so that shutdown actions are called
		restore_volume_success_callback(c, 1, pas);
	}
}

void PulseAudioSystem::restore_volume_success_callback(pa_context *, int, void *userdata) {
	PulseAudioSystem *pas = reinterpret_cast< PulseAudioSystem * >(userdata);

	pas->iRemainingOperations--;
}

void PulseAudioSystem::query() {
	bSourceDone = bSinkDone = bServerDone = false;
	qhInput.clear();
	qhOutput.clear();
	qhEchoMap.clear();
	qhSpecMap.clear();
	qhChanMap.clear();
	qhInput.insert(QString(), tr("Default Input"));
	qhOutput.insert(QString(), tr("Default Output"));
	m_pulseAudio.operation_unref(m_pulseAudio.context_get_server_info(pacContext, server_callback, this));
	m_pulseAudio.operation_unref(m_pulseAudio.context_get_sink_info_list(pacContext, sink_callback, this));
	m_pulseAudio.operation_unref(m_pulseAudio.context_get_source_info_list(pacContext, source_callback, this));
	wakeup();
}

void PulseAudioSystem::setVolumes() {
	// set attenuation state and volumes
	if (bAttenuating) {
		// ensure the volume map is empty, otherwise it may be dangerous to change
		if (qhVolumes.empty()) {
			// set the new per-application volumes and store the old ones
			m_pulseAudio.operation_unref(
				m_pulseAudio.context_get_sink_input_info_list(pacContext, volume_sink_input_list_callback, this));
		}
		// clear attenuation state and restore normal volumes
	} else {
		iRemainingOperations++;
		m_pulseAudio.operation_unref(
			m_pulseAudio.context_get_sink_input_info_list(pacContext, restore_sink_input_list_callback, this));
	}
}

void PulseAudioSystem::contextCallback(pa_context *c) {
	Q_ASSERT(c == pacContext);
	switch (m_pulseAudio.context_get_state(c)) {
		case PA_CONTEXT_READY: {
			bPulseIsGood = true;
			const auto mask =
				static_cast< pa_subscription_mask_t >(PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE);
			m_pulseAudio.operation_unref(m_pulseAudio.context_subscribe(pacContext, mask, nullptr, this));
			query();
			break;
		}
		case PA_CONTEXT_TERMINATED:
			qWarning("PulseAudio: Forcibly disconnected from PulseAudio");
			break;
		case PA_CONTEXT_FAILED:
			qWarning("PulseAudio: Connection failure: %s", m_pulseAudio.strerror(m_pulseAudio.context_errno(c)));
			break;
		default:
			// These are other status callbacks we don't care about. However we explicitly want to wait until
			// one of the status flags listed above are emitted before claiming we are initialized (this callback
			// will be called multiple times).
			return;
	}

	{
		std::unique_lock< std::mutex > guard(m_initLock);
		m_initialized = true;
	}
	m_initWaiter.notify_all();
}

PulseAudioInputRegistrar::PulseAudioInputRegistrar() : AudioInputRegistrar(QLatin1String("PulseAudio"), 10) {
	echoOptions.push_back(EchoCancelOptionID::SPEEX_MIXED);
	echoOptions.push_back(EchoCancelOptionID::SPEEX_MULTICHANNEL);
}

AudioInput *PulseAudioInputRegistrar::create() {
	return new PulseAudioInput();
}

const QVariant PulseAudioInputRegistrar::getDeviceChoice() {
	return Global::get().s.qsPulseAudioInput;
}

const QList< audioDevice > PulseAudioInputRegistrar::getDeviceChoices() {
	QList< audioDevice > choices;

	QStringList keys = pasys->qhInput.keys();
	std::sort(keys.begin(), keys.end());

	for (const auto &key : keys) {
		choices << audioDevice(pasys->qhInput.value(key), key);
	}

	return choices;
}

void PulseAudioInputRegistrar::setDeviceChoice(const QVariant &choice, Settings &s) {
	s.qsPulseAudioInput = choice.toString();
}

bool PulseAudioInputRegistrar::canEcho(EchoCancelOptionID echoOption, const QString &osys) const {
	return (echoOption == EchoCancelOptionID::SPEEX_MIXED || echoOption == EchoCancelOptionID::SPEEX_MULTICHANNEL)
		   && (osys == name);
}

PulseAudioOutputRegistrar::PulseAudioOutputRegistrar() : AudioOutputRegistrar(QLatin1String("PulseAudio"), 10) {
}

AudioOutput *PulseAudioOutputRegistrar::create() {
	return new PulseAudioOutput();
}

const QVariant PulseAudioOutputRegistrar::getDeviceChoice() {
	return Global::get().s.qsPulseAudioOutput;
}

const QList< audioDevice > PulseAudioOutputRegistrar::getDeviceChoices() {
	QList< audioDevice > choices;

	QStringList keys = pasys->qhOutput.keys();
	std::sort(keys.begin(), keys.end());

	for (const auto &key : keys) {
		choices << audioDevice(pasys->qhOutput.value(key), key);
	}

	return choices;
}

void PulseAudioOutputRegistrar::setDeviceChoice(const QVariant &choice, Settings &s) {
	s.qsPulseAudioOutput = choice.toString();
}

bool PulseAudioOutputRegistrar::canMuteOthers() const {
	return true;
}

PulseAudioInput::PulseAudioInput() {
	memset(&pssMic, 0, sizeof(pssMic));
	memset(&pssEcho, 0, sizeof(pssEcho));
	bRunning = true;
	if (pasys)
		pasys->wakeup_lock();
}

PulseAudioInput::~PulseAudioInput() {
	bRunning = false;
	qmMutex.lock();
	qwcWait.wakeAll();
	qmMutex.unlock();
	wait();
	if (pasys)
		pasys->wakeup_lock();
}

void PulseAudioInput::run() {
	qmMutex.lock();
	while (bRunning)
		qwcWait.wait(&qmMutex);
	qmMutex.unlock();
}

PulseAudioOutput::PulseAudioOutput() {
	memset(&pss, 0, sizeof(pss));
	memset(&pcm, 0, sizeof(pcm));
	bRunning = true;
	if (pasys)
		pasys->wakeup_lock();
}

PulseAudioOutput::~PulseAudioOutput() {
	bRunning = false;
	qmMutex.lock();
	qwcWait.wakeAll();
	qmMutex.unlock();
	wait();
	if (pasys)
		pasys->wakeup_lock();
}

void PulseAudioOutput::run() {
	qmMutex.lock();
	while (bRunning)
		qwcWait.wait(&qmMutex);
	qmMutex.unlock();
}

PulseAudio::PulseAudio() : m_ok(false) {
	const QStringList alternatives{ QLatin1String("libpulse.so"), QLatin1String("libpulse.so.0") };

	for (const QString &alternative : alternatives) {
		m_lib.setFileName(alternative);
		if (m_lib.load()) {
			break;
		}
	}

	if (!m_lib.isLoaded()) {
		return;
	}

	RESOLVE(get_library_version);
	RESOLVE(strerror);
	RESOLVE(operation_unref);
	RESOLVE(cvolume_equal);
	RESOLVE(sw_cvolume_multiply_scalar);
	RESOLVE(sample_spec_equal);
	RESOLVE(channel_map_equal);
	RESOLVE(proplist_new);
	RESOLVE(proplist_free);
	RESOLVE(proplist_gets);
	RESOLVE(proplist_sets);
	RESOLVE(threaded_mainloop_new);
	RESOLVE(threaded_mainloop_free);
	RESOLVE(threaded_mainloop_start);
	RESOLVE(threaded_mainloop_stop);
	RESOLVE(threaded_mainloop_lock);
	RESOLVE(threaded_mainloop_unlock);
	RESOLVE(threaded_mainloop_get_api);
	RESOLVE(context_errno);
	RESOLVE(context_new_with_proplist);
	RESOLVE(context_unref);
	RESOLVE(context_connect);
	RESOLVE(context_disconnect);
	RESOLVE(context_subscribe);
	RESOLVE(context_get_state);
	RESOLVE(context_get_server_info);
	RESOLVE(context_get_sink_info_by_name);
	RESOLVE(context_get_sink_info_list);
	RESOLVE(context_get_sink_input_info_list);
	RESOLVE(context_get_source_info_list);
	RESOLVE(context_set_sink_input_volume);
	RESOLVE(context_set_subscribe_callback);
	RESOLVE(context_set_state_callback);
	RESOLVE(stream_new);
	RESOLVE(stream_unref);
	RESOLVE(stream_connect_playback);
	RESOLVE(stream_connect_record);
	RESOLVE(stream_disconnect);
	RESOLVE(stream_peek);
	RESOLVE(stream_write);
	RESOLVE(stream_drop);
	RESOLVE(stream_cork);
	RESOLVE(stream_get_state);
	RESOLVE(stream_get_context);
	RESOLVE(stream_get_sample_spec);
	RESOLVE(stream_get_channel_map);
	RESOLVE(stream_get_buffer_attr);
	RESOLVE(stream_set_state_callback);
	RESOLVE(stream_set_read_callback);
	RESOLVE(stream_set_write_callback);
	RESOLVE(ext_stream_restore_read);
	RESOLVE(ext_stream_restore_write);

	qDebug("PulseAudio %s from %s", get_library_version(), qPrintable(m_lib.fileName()));

	m_ok = true;
}

#undef RESOLVE
#undef NBLOCKS
