1139 lines
26 KiB
C++
1139 lines
26 KiB
C++
// Copyright 2018-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 "JackAudio.h"
|
|
|
|
#include "Utils.h"
|
|
#include "Global.h"
|
|
|
|
#ifdef Q_CC_GNU
|
|
# define RESOLVE(var) \
|
|
{ \
|
|
var = reinterpret_cast< __typeof__(var) >(qlJack.resolve(#var)); \
|
|
if (!var) \
|
|
return; \
|
|
}
|
|
#else
|
|
# define RESOLVE(var) \
|
|
{ \
|
|
*reinterpret_cast< void ** >(&var) = static_cast< void * >(qlJack.resolve(#var)); \
|
|
if (!var) \
|
|
return; \
|
|
}
|
|
#endif
|
|
|
|
static std::unique_ptr< JackAudioSystem > jas;
|
|
|
|
// jackStatusToStringList converts a jack_status_t (a flag type
|
|
// that can contain multiple Jack statuses) to a QStringList.
|
|
static QStringList jackStatusToStringList(const jack_status_t &status) {
|
|
QStringList statusList;
|
|
|
|
if (status & JackFailure) {
|
|
statusList << QLatin1String("JackFailure - overall operation failed");
|
|
}
|
|
if (status & JackInvalidOption) {
|
|
statusList << QLatin1String("JackInvalidOption - the operation contained an invalid or unsupported option");
|
|
}
|
|
if (status & JackNameNotUnique) {
|
|
statusList << QLatin1String("JackNameNotUnique - the desired client name is not unique");
|
|
}
|
|
if (status & JackServerStarted) {
|
|
statusList << QLatin1String("JackServerStarted - the server was started as a result of this operation");
|
|
}
|
|
if (status & JackServerFailed) {
|
|
statusList << QLatin1String("JackServerFailed - unable to connect to the JACK server");
|
|
}
|
|
if (status & JackServerError) {
|
|
statusList << QLatin1String("JackServerError - communication error with the JACK server");
|
|
}
|
|
if (status & JackNoSuchClient) {
|
|
statusList << QLatin1String("JackNoSuchClient - requested client does not exist");
|
|
}
|
|
if (status & JackLoadFailure) {
|
|
statusList << QLatin1String("JackLoadFailure - unable to load initial client");
|
|
}
|
|
if (status & JackInitFailure) {
|
|
statusList << QLatin1String("JackInitFailure - unable to initialize client");
|
|
}
|
|
if (status & JackShmFailure) {
|
|
statusList << QLatin1String("JackShmFailure - unable to access shared memory");
|
|
}
|
|
if (status & JackVersionError) {
|
|
statusList << QLatin1String("JackVersionError - client's protocol version does not match");
|
|
}
|
|
if (status & JackBackendError) {
|
|
statusList << QLatin1String("JackBackendError - a backend error occurred");
|
|
}
|
|
if (status & JackClientZombie) {
|
|
statusList << QLatin1String("JackClientZombie - client zombified");
|
|
}
|
|
|
|
return statusList;
|
|
}
|
|
|
|
class JackAudioInputRegistrar : public AudioInputRegistrar {
|
|
private:
|
|
AudioInput *create() Q_DECL_OVERRIDE;
|
|
const QVariant getDeviceChoice() Q_DECL_OVERRIDE;
|
|
const QList< audioDevice > getDeviceChoices() Q_DECL_OVERRIDE;
|
|
void setDeviceChoice(const QVariant &, Settings &) Q_DECL_OVERRIDE;
|
|
bool canEcho(EchoCancelOptionID echoCancelID, const QString &outputSystem) const Q_DECL_OVERRIDE;
|
|
bool isMicrophoneAccessDeniedByOS() Q_DECL_OVERRIDE { return false; };
|
|
|
|
public:
|
|
JackAudioInputRegistrar();
|
|
};
|
|
|
|
class JackAudioOutputRegistrar : public AudioOutputRegistrar {
|
|
private:
|
|
AudioOutput *create() Q_DECL_OVERRIDE;
|
|
const QVariant getDeviceChoice() Q_DECL_OVERRIDE;
|
|
const QList< audioDevice > getDeviceChoices() Q_DECL_OVERRIDE;
|
|
void setDeviceChoice(const QVariant &, Settings &) Q_DECL_OVERRIDE;
|
|
bool usesOutputDelay() const Q_DECL_OVERRIDE;
|
|
|
|
public:
|
|
JackAudioOutputRegistrar();
|
|
};
|
|
|
|
class JackAudioInit : public DeferInit {
|
|
private:
|
|
std::unique_ptr< JackAudioInputRegistrar > airJackAudio;
|
|
std::unique_ptr< JackAudioOutputRegistrar > aorJackAudio;
|
|
void initialize() Q_DECL_OVERRIDE;
|
|
void destroy() Q_DECL_OVERRIDE;
|
|
};
|
|
|
|
JackAudioInputRegistrar::JackAudioInputRegistrar() : AudioInputRegistrar(QLatin1String("JACK"), 10) {
|
|
}
|
|
|
|
AudioInput *JackAudioInputRegistrar::create() {
|
|
return new JackAudioInput();
|
|
}
|
|
|
|
const QVariant JackAudioInputRegistrar::getDeviceChoice() {
|
|
return {};
|
|
}
|
|
|
|
const QList< audioDevice > JackAudioInputRegistrar::getDeviceChoices() {
|
|
QList< audioDevice > choices;
|
|
|
|
QStringList keys = jas->qhInput.keys();
|
|
std::sort(keys.begin(), keys.end());
|
|
|
|
for (const auto &key : keys) {
|
|
choices << audioDevice(jas->qhInput.value(key), key);
|
|
}
|
|
|
|
return choices;
|
|
}
|
|
|
|
void JackAudioInputRegistrar::setDeviceChoice(const QVariant &, Settings &) {
|
|
}
|
|
|
|
bool JackAudioInputRegistrar::canEcho(EchoCancelOptionID, const QString &) const {
|
|
return false;
|
|
}
|
|
|
|
JackAudioOutputRegistrar::JackAudioOutputRegistrar() : AudioOutputRegistrar(QLatin1String("JACK"), 10) {
|
|
}
|
|
|
|
AudioOutput *JackAudioOutputRegistrar::create() {
|
|
return new JackAudioOutput();
|
|
}
|
|
|
|
const QVariant JackAudioOutputRegistrar::getDeviceChoice() {
|
|
return Global::get().s.qsJackAudioOutput;
|
|
}
|
|
|
|
const QList< audioDevice > JackAudioOutputRegistrar::getDeviceChoices() {
|
|
QList< audioDevice > choices;
|
|
|
|
QStringList keys = jas->qhOutput.keys();
|
|
std::sort(keys.begin(), keys.end());
|
|
|
|
for (const auto &key : keys) {
|
|
choices << audioDevice(jas->qhOutput.value(key), key);
|
|
}
|
|
|
|
return choices;
|
|
}
|
|
|
|
void JackAudioOutputRegistrar::setDeviceChoice(const QVariant &choice, Settings &s) {
|
|
s.qsJackAudioOutput = choice.toString();
|
|
}
|
|
|
|
bool JackAudioOutputRegistrar::usesOutputDelay() const {
|
|
return false;
|
|
}
|
|
|
|
void JackAudioInit::initialize() {
|
|
jas.reset(new JackAudioSystem());
|
|
|
|
if (jas->bAvailable) {
|
|
airJackAudio.reset(new JackAudioInputRegistrar());
|
|
aorJackAudio.reset(new JackAudioOutputRegistrar());
|
|
} else {
|
|
jas.reset();
|
|
}
|
|
}
|
|
|
|
void JackAudioInit::destroy() {
|
|
airJackAudio.reset();
|
|
aorJackAudio.reset();
|
|
jas.reset();
|
|
}
|
|
|
|
// Instantiate JackAudioSystem, JackAudioInputRegistrar and JackAudioOutputRegistrar
|
|
static JackAudioInit jai;
|
|
|
|
JackAudioSystem::JackAudioSystem() : bAvailable(false), users(0), client(nullptr) {
|
|
QStringList alternatives;
|
|
#ifdef Q_OS_WIN
|
|
alternatives << QLatin1String("libjack64.dll");
|
|
alternatives << QLatin1String("libjack32.dll");
|
|
#elif defined(Q_OS_MAC)
|
|
alternatives << QLatin1String("libjack.dylib");
|
|
alternatives << QLatin1String("libjack.0.dylib");
|
|
#else
|
|
alternatives << QLatin1String("libjack.so");
|
|
alternatives << QLatin1String("libjack.so.0");
|
|
#endif
|
|
for (const QString &lib : alternatives) {
|
|
qlJack.setFileName(lib);
|
|
if (qlJack.load()) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!qlJack.isLoaded()) {
|
|
return;
|
|
}
|
|
|
|
RESOLVE(jack_get_version_string)
|
|
RESOLVE(jack_free)
|
|
RESOLVE(jack_get_client_name)
|
|
RESOLVE(jack_client_open)
|
|
RESOLVE(jack_client_close)
|
|
RESOLVE(jack_activate)
|
|
RESOLVE(jack_deactivate)
|
|
RESOLVE(jack_get_sample_rate)
|
|
RESOLVE(jack_get_buffer_size)
|
|
RESOLVE(jack_get_client_name)
|
|
RESOLVE(jack_get_ports)
|
|
RESOLVE(jack_connect)
|
|
RESOLVE(jack_port_disconnect)
|
|
RESOLVE(jack_port_register)
|
|
RESOLVE(jack_port_unregister)
|
|
RESOLVE(jack_port_name)
|
|
RESOLVE(jack_port_by_name)
|
|
RESOLVE(jack_port_flags)
|
|
RESOLVE(jack_port_get_buffer)
|
|
RESOLVE(jack_ringbuffer_create)
|
|
RESOLVE(jack_ringbuffer_free)
|
|
RESOLVE(jack_ringbuffer_mlock)
|
|
RESOLVE(jack_ringbuffer_read)
|
|
RESOLVE(jack_ringbuffer_read_space)
|
|
RESOLVE(jack_ringbuffer_write)
|
|
RESOLVE(jack_ringbuffer_write_space)
|
|
RESOLVE(jack_ringbuffer_write_advance)
|
|
RESOLVE(jack_ringbuffer_get_write_vector)
|
|
RESOLVE(jack_set_process_callback)
|
|
RESOLVE(jack_set_sample_rate_callback)
|
|
RESOLVE(jack_set_buffer_size_callback)
|
|
RESOLVE(jack_on_shutdown)
|
|
|
|
qhInput.insert(QString(), tr("Hardware Ports"));
|
|
qhOutput.insert(QString::number(1), tr("Mono"));
|
|
qhOutput.insert(QString::number(2), tr("Stereo"));
|
|
|
|
bAvailable = true;
|
|
|
|
qDebug("JACK %s from %s", jack_get_version_string(), qPrintable(qlJack.fileName()));
|
|
}
|
|
|
|
JackAudioSystem::~JackAudioSystem() {
|
|
deinitialize();
|
|
}
|
|
|
|
bool JackAudioSystem::initialize() {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
if (client) {
|
|
lock.unlock();
|
|
deinitialize();
|
|
lock.relock();
|
|
}
|
|
|
|
jack_status_t status;
|
|
client = jack_client_open(Global::get().s.qsJackClientName.toStdString().c_str(),
|
|
Global::get().s.bJackStartServer ? JackNullOption : JackNoStartServer, &status);
|
|
if (!client) {
|
|
const auto errors = jackStatusToStringList(status);
|
|
qWarning("JackAudioSystem: unable to open client due to %i errors:", errors.count());
|
|
for (auto i = 0; i < errors.count(); ++i) {
|
|
qWarning("JackAudioSystem: %s", qPrintable(errors.at(i)));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
qDebug("JackAudioSystem: client \"%s\" opened successfully", jack_get_client_name(client));
|
|
|
|
auto ret = jack_set_process_callback(client, processCallback, nullptr);
|
|
if (ret != 0) {
|
|
qWarning("JackAudioSystem: unable to set process callback - jack_set_process_callback() returned %i", ret);
|
|
jack_client_close(client);
|
|
client = nullptr;
|
|
return false;
|
|
}
|
|
|
|
ret = jack_set_sample_rate_callback(client, sampleRateCallback, nullptr);
|
|
if (ret != 0) {
|
|
qWarning("JackAudioSystem: unable to set sample rate callback - jack_set_sample_rate_callback() returned %i",
|
|
ret);
|
|
jack_client_close(client);
|
|
client = nullptr;
|
|
return false;
|
|
}
|
|
|
|
ret = jack_set_buffer_size_callback(client, bufferSizeCallback, nullptr);
|
|
if (ret != 0) {
|
|
qWarning("JackAudioSystem: unable to set buffer size callback - jack_set_buffer_size_callback() returned %i",
|
|
ret);
|
|
jack_client_close(client);
|
|
client = nullptr;
|
|
return false;
|
|
}
|
|
|
|
jack_on_shutdown(client, shutdownCallback, nullptr);
|
|
|
|
return true;
|
|
}
|
|
|
|
void JackAudioSystem::deinitialize() {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
if (!client) {
|
|
return;
|
|
}
|
|
|
|
const auto clientName = QString::fromLatin1(jack_get_client_name(client));
|
|
|
|
const auto err = jack_client_close(client);
|
|
if (err != 0) {
|
|
qWarning("JackAudioSystem: unable to disconnect from the server - jack_client_close() returned %i", err);
|
|
return;
|
|
}
|
|
|
|
client = nullptr;
|
|
|
|
qDebug("JackAudioSystem: client \"%s\" closed successfully", clientName.toStdString().c_str());
|
|
}
|
|
|
|
bool JackAudioSystem::activate() {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
if (!client) {
|
|
lock.unlock();
|
|
|
|
if (!initialize()) {
|
|
return false;
|
|
}
|
|
|
|
lock.relock();
|
|
}
|
|
|
|
if (users++ > 0) {
|
|
// The client is already active, because there is at least a user
|
|
return true;
|
|
}
|
|
|
|
const auto ret = jack_activate(client);
|
|
if (ret != 0) {
|
|
qWarning("JackAudioSystem: unable to activate client - jack_activate() returned %i", ret);
|
|
return false;
|
|
}
|
|
|
|
qDebug("JackAudioSystem: client activated");
|
|
|
|
return true;
|
|
}
|
|
|
|
void JackAudioSystem::deactivate() {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
if (!client) {
|
|
return;
|
|
}
|
|
|
|
if (--users > 0) {
|
|
// There is still at least a user, we only decrement the counter
|
|
return;
|
|
}
|
|
|
|
const auto err = jack_deactivate(client);
|
|
if (err != 0) {
|
|
qWarning("JackAudioSystem: unable to remove client from the process graph - jack_deactivate() returned %i",
|
|
err);
|
|
return;
|
|
}
|
|
|
|
qDebug("JackAudioSystem: client deactivated");
|
|
|
|
lock.unlock();
|
|
|
|
deinitialize();
|
|
}
|
|
|
|
bool JackAudioSystem::isOk() {
|
|
QMutexLocker lock(&qmWait);
|
|
return (client);
|
|
}
|
|
|
|
uint8_t JackAudioSystem::outPorts() {
|
|
return static_cast< uint8_t >(
|
|
qBound< unsigned >(1, Global::get().s.qsJackAudioOutput.toUInt(), JACK_MAX_OUTPUT_PORTS));
|
|
}
|
|
|
|
jack_nframes_t JackAudioSystem::sampleRate() {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
if (!client) {
|
|
return 0;
|
|
}
|
|
|
|
return jack_get_sample_rate(client);
|
|
}
|
|
|
|
jack_nframes_t JackAudioSystem::bufferSize() {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
if (!client) {
|
|
return 0;
|
|
}
|
|
|
|
return jack_get_buffer_size(client);
|
|
}
|
|
|
|
JackPorts JackAudioSystem::getPhysicalPorts(const uint8_t flags) {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
if (!client) {
|
|
return JackPorts();
|
|
}
|
|
|
|
const auto ports = jack_get_ports(client, nullptr, nullptr, JackPortIsPhysical | JackPortIsTerminal);
|
|
if (!ports) {
|
|
return JackPorts();
|
|
}
|
|
|
|
JackPorts ret;
|
|
|
|
for (auto i = 0; ports[i]; ++i) {
|
|
if (!ports[i]) {
|
|
// End of the array
|
|
break;
|
|
}
|
|
|
|
auto port = jack_port_by_name(client, ports[i]);
|
|
if (!port) {
|
|
qWarning("JackAudioSystem: jack_port_by_name() returned an invalid port - skipping it");
|
|
continue;
|
|
}
|
|
|
|
if (jack_port_flags(port) & flags) {
|
|
ret.append(port);
|
|
}
|
|
}
|
|
|
|
jack_free(ports);
|
|
|
|
return ret;
|
|
}
|
|
|
|
void *JackAudioSystem::getPortBuffer(jack_port_t *port, const jack_nframes_t frames) {
|
|
if (!port) {
|
|
return nullptr;
|
|
}
|
|
|
|
return jack_port_get_buffer(port, frames);
|
|
}
|
|
|
|
jack_port_t *JackAudioSystem::registerPort(const char *name, const uint8_t flags) {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
if (!client || !name) {
|
|
return nullptr;
|
|
}
|
|
|
|
return jack_port_register(client, name, JACK_DEFAULT_AUDIO_TYPE, flags, 0);
|
|
}
|
|
|
|
bool JackAudioSystem::unregisterPort(jack_port_t *port) {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
if (!client || !port) {
|
|
return false;
|
|
}
|
|
|
|
const auto ret = jack_port_unregister(client, port);
|
|
if (ret != 0) {
|
|
qWarning("JackAudioSystem: unable to unregister port - jack_port_unregister() returned %i", ret);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool JackAudioSystem::connectPort(jack_port_t *sourcePort, jack_port_t *destinationPort) {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
if (!client || !sourcePort || !destinationPort) {
|
|
return false;
|
|
}
|
|
|
|
const auto sourcePortName = jack_port_name(sourcePort);
|
|
const auto destinationPortName = jack_port_name(destinationPort);
|
|
|
|
const auto ret = jack_connect(client, sourcePortName, destinationPortName);
|
|
if (ret != 0) {
|
|
qWarning("JackAudioSystem: unable to connect port '%s' to '%s' - jack_connect() returned %i", sourcePortName,
|
|
destinationPortName, ret);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool JackAudioSystem::disconnectPort(jack_port_t *port) {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
if (!client || !port) {
|
|
return false;
|
|
}
|
|
|
|
const auto ret = jack_port_disconnect(client, port);
|
|
if (ret != 0) {
|
|
qWarning("JackAudioSystem: unable to disconnect port - jack_port_disconnect() returned %i", ret);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Ringbuffer functions do not have locks.
|
|
// They are single-consumer single-producer, lockless.
|
|
jack_ringbuffer_t *JackAudioSystem::ringbufferCreate(const size_t size) {
|
|
if (size == 0) {
|
|
return nullptr;
|
|
}
|
|
|
|
return jack_ringbuffer_create(size);
|
|
}
|
|
|
|
void JackAudioSystem::ringbufferFree(jack_ringbuffer_t *buffer) {
|
|
if (!buffer) {
|
|
return;
|
|
}
|
|
|
|
jack_ringbuffer_free(buffer);
|
|
}
|
|
|
|
int JackAudioSystem::ringbufferMlock(jack_ringbuffer_t *buffer) {
|
|
if (!buffer) {
|
|
return -2;
|
|
}
|
|
|
|
return jack_ringbuffer_mlock(buffer);
|
|
}
|
|
|
|
size_t JackAudioSystem::ringbufferRead(jack_ringbuffer_t *buffer, const size_t size, void *destination) {
|
|
if (!buffer || size == 0 || !destination) {
|
|
return 0;
|
|
}
|
|
|
|
return jack_ringbuffer_read(buffer, static_cast< char * >(destination), size);
|
|
}
|
|
|
|
size_t JackAudioSystem::ringbufferReadSpace(const jack_ringbuffer_t *buffer) {
|
|
if (!buffer) {
|
|
return 0;
|
|
}
|
|
|
|
return jack_ringbuffer_read_space(buffer);
|
|
}
|
|
|
|
size_t JackAudioSystem::ringbufferWrite(jack_ringbuffer_t *buffer, const size_t size, const void *source) {
|
|
if (!buffer || size == 0 || !source) {
|
|
return 0;
|
|
}
|
|
|
|
return jack_ringbuffer_write(buffer, static_cast< const char * >(source), size);
|
|
}
|
|
|
|
void JackAudioSystem::ringbufferGetWriteVector(const jack_ringbuffer_t *buffer, jack_ringbuffer_data_t *vector) {
|
|
if (!buffer || !vector) {
|
|
return;
|
|
}
|
|
|
|
jack_ringbuffer_get_write_vector(buffer, vector);
|
|
}
|
|
|
|
size_t JackAudioSystem::ringbufferWriteSpace(const jack_ringbuffer_t *buffer) {
|
|
if (!buffer) {
|
|
return 0;
|
|
}
|
|
|
|
return jack_ringbuffer_write_space(buffer);
|
|
}
|
|
|
|
void JackAudioSystem::ringbufferWriteAdvance(jack_ringbuffer_t *buffer, const size_t size) {
|
|
if (!buffer || size == 0) {
|
|
return;
|
|
}
|
|
|
|
jack_ringbuffer_write_advance(buffer, size);
|
|
}
|
|
|
|
int JackAudioSystem::processCallback(jack_nframes_t frames, void *) {
|
|
auto const input = dynamic_cast< JackAudioInput * >(Global::get().ai.get());
|
|
auto const output = dynamic_cast< JackAudioOutput * >(Global::get().ao.get());
|
|
|
|
const bool canInput = (input && input->isReady());
|
|
const bool canOutput = (output && output->isReady());
|
|
|
|
if (canInput && !input->process(frames)) {
|
|
return 1;
|
|
}
|
|
|
|
if (canOutput && !output->process(frames)) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int JackAudioSystem::sampleRateCallback(jack_nframes_t, void *) {
|
|
auto const input = dynamic_cast< JackAudioInput * >(Global::get().ai.get());
|
|
auto const output = dynamic_cast< JackAudioOutput * >(Global::get().ao.get());
|
|
|
|
if (input) {
|
|
input->activate();
|
|
}
|
|
|
|
if (output) {
|
|
output->activate();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int JackAudioSystem::bufferSizeCallback(jack_nframes_t frames, void *) {
|
|
auto const input = dynamic_cast< JackAudioInput * >(Global::get().ai.get());
|
|
auto const output = dynamic_cast< JackAudioOutput * >(Global::get().ao.get());
|
|
|
|
if (input && !input->allocBuffer(frames)) {
|
|
return 1;
|
|
}
|
|
|
|
if (output && !output->allocBuffer(frames)) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void JackAudioSystem::shutdownCallback(void *) {
|
|
qWarning("JackAudioSystem: server shutdown");
|
|
jas->client = nullptr;
|
|
jas->users = 0;
|
|
}
|
|
|
|
JackAudioInput::JackAudioInput() : port(nullptr), buffer(nullptr) {
|
|
bReady = activate();
|
|
}
|
|
|
|
JackAudioInput::~JackAudioInput() {
|
|
qmWait.lock();
|
|
bReady = false;
|
|
// Request interruption
|
|
qsSleep.release(1);
|
|
qmWait.unlock();
|
|
|
|
// Wait for thread to exit
|
|
wait();
|
|
|
|
// Cleanup
|
|
deactivate();
|
|
}
|
|
|
|
bool JackAudioInput::isReady() {
|
|
return bReady;
|
|
}
|
|
|
|
bool JackAudioInput::allocBuffer(const jack_nframes_t frames) {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
bufferSize = frames * sizeof(jack_default_audio_sample_t);
|
|
|
|
if (buffer) {
|
|
jas->ringbufferFree(buffer);
|
|
}
|
|
|
|
buffer = jas->ringbufferCreate(bufferSize * JACK_BUFFER_PERIODS);
|
|
|
|
if (!buffer) {
|
|
return false;
|
|
}
|
|
|
|
jas->ringbufferMlock(buffer);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool JackAudioInput::activate() {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
if (!jas->isOk()) {
|
|
jas->initialize();
|
|
}
|
|
|
|
eMicFormat = SampleFloat;
|
|
iMicChannels = 1;
|
|
iMicFreq = jas->sampleRate();
|
|
|
|
initializeMixer();
|
|
|
|
lock.unlock();
|
|
|
|
if (!registerPorts()) {
|
|
return false;
|
|
}
|
|
|
|
if (!allocBuffer(jas->bufferSize())) {
|
|
return false;
|
|
}
|
|
|
|
if (!jas->activate()) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void JackAudioInput::deactivate() {
|
|
unregisterPorts();
|
|
jas->deactivate();
|
|
|
|
if (buffer) {
|
|
jas->ringbufferFree(buffer);
|
|
}
|
|
}
|
|
|
|
bool JackAudioInput::registerPorts() {
|
|
unregisterPorts();
|
|
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
port = jas->registerPort("input", JackPortIsInput);
|
|
if (!port) {
|
|
qWarning("JackAudioInput: unable to register port");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool JackAudioInput::unregisterPorts() {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
if (!port) {
|
|
return false;
|
|
}
|
|
|
|
if (!jas->unregisterPort(port)) {
|
|
qWarning("JackAudioInput: unable to unregister port");
|
|
return false;
|
|
}
|
|
|
|
port = nullptr;
|
|
|
|
return true;
|
|
}
|
|
|
|
void JackAudioInput::connectPorts() {
|
|
disconnectPorts();
|
|
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
if (!port) {
|
|
return;
|
|
}
|
|
|
|
const JackPorts outputPorts = jas->getPhysicalPorts(JackPortIsOutput);
|
|
for (auto outputPort : outputPorts) {
|
|
if (jas->connectPort(outputPort, port)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool JackAudioInput::disconnectPorts() {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
if (!port) {
|
|
return true;
|
|
}
|
|
|
|
if (!jas->disconnectPort(port)) {
|
|
qWarning("JackAudioInput: unable to disconnect port");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool JackAudioInput::process(const jack_nframes_t frames) {
|
|
if (!bReady) {
|
|
return true;
|
|
}
|
|
|
|
const auto portBuffer = jas->getPortBuffer(port, frames);
|
|
|
|
qsSleep.release(1);
|
|
|
|
if (!portBuffer || !buffer) {
|
|
return false;
|
|
}
|
|
|
|
// Ringbuffer will not exceed capacity, just drop the frames.
|
|
// Since the consumer drains it fully every time, this should never be a problem.
|
|
jas->ringbufferWrite(buffer, frames * sizeof(jack_default_audio_sample_t), portBuffer);
|
|
|
|
return true;
|
|
}
|
|
|
|
void JackAudioInput::run() {
|
|
if (!bReady) {
|
|
return;
|
|
}
|
|
|
|
// Initialization
|
|
if (Global::get().s.bJackAutoConnect) {
|
|
connectPorts();
|
|
}
|
|
|
|
// We keep this as the frame size could change, but we are not going to resize this buffer.
|
|
qmWait.lock();
|
|
std::unique_ptr< uint8_t[] > sampleBuffer(new uint8_t[bufferSize]);
|
|
qmWait.unlock();
|
|
|
|
do {
|
|
qmWait.lock();
|
|
|
|
while (const auto bytes = qMin(jas->ringbufferReadSpace(buffer), bufferSize)) {
|
|
jas->ringbufferRead(buffer, bytes, sampleBuffer.get());
|
|
addMic(sampleBuffer.get(), static_cast< unsigned int >(bytes / sizeof(jack_default_audio_sample_t)));
|
|
}
|
|
|
|
qmWait.unlock();
|
|
qsSleep.acquire(1);
|
|
} while (bReady);
|
|
}
|
|
|
|
JackAudioOutput::JackAudioOutput() : buffer(nullptr) {
|
|
bReady = activate();
|
|
}
|
|
|
|
JackAudioOutput::~JackAudioOutput() {
|
|
// Request interruption
|
|
qmWait.lock();
|
|
bReady = false;
|
|
qsSleep.release(1);
|
|
qmWait.unlock();
|
|
|
|
// Wait for thread to exit
|
|
wait();
|
|
// Cleanup
|
|
deactivate();
|
|
}
|
|
|
|
bool JackAudioOutput::isReady() {
|
|
return bReady;
|
|
}
|
|
|
|
bool JackAudioOutput::allocBuffer(const jack_nframes_t frames) {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
iFrameSize = frames;
|
|
if (buffer) {
|
|
jas->ringbufferFree(buffer);
|
|
}
|
|
|
|
buffer = jas->ringbufferCreate(iFrameSize * iSampleSize * JACK_BUFFER_PERIODS);
|
|
if (!buffer) {
|
|
return false;
|
|
}
|
|
|
|
jas->ringbufferMlock(buffer);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool JackAudioOutput::activate() {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
if (!jas->isOk()) {
|
|
jas->initialize();
|
|
}
|
|
|
|
eSampleFormat = SampleFloat;
|
|
iChannels = jas->outPorts();
|
|
iMixerFreq = jas->sampleRate();
|
|
uint32_t channelsMask[32];
|
|
channelsMask[0] = SPEAKER_FRONT_LEFT;
|
|
channelsMask[1] = SPEAKER_FRONT_RIGHT;
|
|
initializeMixer(channelsMask);
|
|
|
|
lock.unlock();
|
|
|
|
if (!allocBuffer(jas->bufferSize())) {
|
|
return false;
|
|
}
|
|
|
|
if (!registerPorts()) {
|
|
return false;
|
|
}
|
|
|
|
if (!jas->activate()) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void JackAudioOutput::deactivate() {
|
|
unregisterPorts();
|
|
jas->deactivate();
|
|
jas->ringbufferFree(buffer);
|
|
}
|
|
|
|
bool JackAudioOutput::registerPorts() {
|
|
unregisterPorts();
|
|
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
for (decltype(iChannels) i = 0; i < iChannels; ++i) {
|
|
char name[10];
|
|
snprintf(name, sizeof(name), "output_%d", i + 1);
|
|
|
|
const auto port = jas->registerPort(name, JackPortIsOutput);
|
|
if (!port) {
|
|
qWarning("JackAudioOutput: unable to register port #%u", i);
|
|
return false;
|
|
}
|
|
|
|
ports.append(port);
|
|
outputBuffers.append(nullptr);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool JackAudioOutput::unregisterPorts() {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
bool ret = true;
|
|
|
|
for (auto i = 0; i < ports.size(); ++i) {
|
|
if (!ports[i]) {
|
|
continue;
|
|
}
|
|
|
|
if (!jas->unregisterPort(ports[i])) {
|
|
qWarning("JackAudioOutput: unable to unregister port #%u", i);
|
|
ret = false;
|
|
}
|
|
}
|
|
|
|
outputBuffers.clear();
|
|
ports.clear();
|
|
|
|
return ret;
|
|
}
|
|
|
|
void JackAudioOutput::connectPorts() {
|
|
disconnectPorts();
|
|
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
const auto inputPorts = jas->getPhysicalPorts(JackPortIsInput);
|
|
uint8_t i = 0;
|
|
|
|
for (auto inputPort : inputPorts) {
|
|
if (i == ports.size()) {
|
|
break;
|
|
}
|
|
|
|
if (ports[i]) {
|
|
if (!jas->connectPort(ports[i], inputPort)) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
++i;
|
|
}
|
|
}
|
|
|
|
bool JackAudioOutput::disconnectPorts() {
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
bool ret = true;
|
|
|
|
for (auto i = 0; i < ports.size(); ++i) {
|
|
if (ports[i] && !jas->disconnectPort(ports[i])) {
|
|
qWarning("JackAudioOutput: unable to disconnect port #%u", i);
|
|
ret = false;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool JackAudioOutput::process(const jack_nframes_t frames) {
|
|
if (!bReady) {
|
|
return true;
|
|
}
|
|
|
|
// FIXME: This is good on Linux, and maybe Windows.
|
|
// However, on certain platforms QSemaphore actually uses QWaitCondition, which uses a Mutex internally for locking.
|
|
// This is in spite of the fact that on most POSIX systems, QMutex is actually implemented using semaphores.
|
|
qsSleep.release(1);
|
|
|
|
for (decltype(iChannels) currentChannel = 0; currentChannel < iChannels; ++currentChannel) {
|
|
auto outputBuffer = jas->getPortBuffer(ports[static_cast< int >(currentChannel)], frames);
|
|
if (!outputBuffer) {
|
|
return false;
|
|
}
|
|
|
|
outputBuffers.replace(static_cast< int >(currentChannel),
|
|
reinterpret_cast< jack_default_audio_sample_t * >(outputBuffer));
|
|
}
|
|
|
|
const auto avail = jas->ringbufferReadSpace(buffer);
|
|
if (avail == 0) {
|
|
for (decltype(iChannels) currentChannel = 0; currentChannel < iChannels; ++currentChannel) {
|
|
memset(outputBuffers[static_cast< int >(currentChannel)], 0, frames * sizeof(jack_default_audio_sample_t));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
const size_t needed = frames * iSampleSize;
|
|
|
|
if (iChannels == 1) {
|
|
jas->ringbufferRead(buffer, avail, reinterpret_cast< char * >(outputBuffers[0]));
|
|
if (avail < needed) {
|
|
memset(reinterpret_cast< char * >(&(outputBuffers[static_cast< int >(avail)])), 0, needed - avail);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
auto samples = qMin(jas->ringbufferReadSpace(buffer), needed) / sizeof(jack_default_audio_sample_t);
|
|
for (auto currentSample = decltype(samples){ 0 }; currentSample < samples; ++currentSample) {
|
|
jas->ringbufferRead(
|
|
buffer, sizeof(jack_default_audio_sample_t),
|
|
reinterpret_cast< char * >(
|
|
&outputBuffers[static_cast< int >(currentSample % iChannels)][currentSample / iChannels]));
|
|
}
|
|
|
|
if ((samples / iChannels) < frames) {
|
|
for (decltype(iChannels) currentChannel = 0; currentChannel < iChannels; ++currentChannel) {
|
|
memset(&outputBuffers[static_cast< int >(currentChannel)][avail / samples], 0,
|
|
(needed - avail) / iChannels);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void JackAudioOutput::run() {
|
|
if (!bReady) {
|
|
return;
|
|
}
|
|
|
|
// Initialization
|
|
if (Global::get().s.bJackAutoConnect) {
|
|
connectPorts();
|
|
}
|
|
|
|
std::unique_ptr< uint8_t[] > spareSample(new uint8_t[iSampleSize]);
|
|
|
|
jack_ringbuffer_data_t _writeVector[2];
|
|
jack_ringbuffer_data_t *writeVector = _writeVector;
|
|
|
|
// Keep feeding more data into the buffer
|
|
do {
|
|
qmWait.lock();
|
|
|
|
if (jas->ringbufferWriteSpace(buffer) < (iFrameSize * iSampleSize)) {
|
|
qmWait.unlock();
|
|
qsSleep.acquire(1);
|
|
continue;
|
|
}
|
|
|
|
jas->ringbufferGetWriteVector(buffer, writeVector);
|
|
|
|
auto bOk = true;
|
|
size_t writtenFrames = 0;
|
|
unsigned int wanted =
|
|
qMin(static_cast< unsigned int >(writeVector->len) / iSampleSize, static_cast< unsigned int >(iFrameSize));
|
|
if (wanted > 0) {
|
|
bOk = mix(writeVector->buf, wanted);
|
|
writtenFrames += bOk ? wanted : 0;
|
|
}
|
|
|
|
const auto gap = writeVector->len - (wanted * iSampleSize);
|
|
|
|
if (!bOk || wanted == iFrameSize) {
|
|
goto next;
|
|
}
|
|
|
|
// Corner case where one sample wraps around the buffer
|
|
if (gap != 0) {
|
|
writeVector->buf += wanted * iSampleSize;
|
|
bOk = mix(spareSample.get(), 1);
|
|
if (bOk) {
|
|
memcpy(writeVector->buf, spareSample.get(), gap);
|
|
++writeVector;
|
|
memcpy(writeVector->buf, spareSample.get() + gap, iSampleSize - gap);
|
|
++wanted;
|
|
++writtenFrames;
|
|
} else {
|
|
goto next;
|
|
}
|
|
writeVector->buf += iSampleSize - gap;
|
|
} else {
|
|
++writeVector;
|
|
}
|
|
|
|
if (wanted == iFrameSize) {
|
|
goto next;
|
|
}
|
|
|
|
bOk = mix(writeVector->buf, iFrameSize - wanted);
|
|
writtenFrames += bOk ? (iFrameSize - wanted) : 0;
|
|
next:
|
|
jas->ringbufferWriteAdvance(buffer, writtenFrames * iSampleSize);
|
|
qmWait.unlock();
|
|
qsSleep.acquire(1);
|
|
} while (bReady);
|
|
}
|
|
|
|
#undef RESOLVE
|