From 01b097d6ee05aaafd20174d013b438e51fb584c8 Mon Sep 17 00:00:00 2001 From: Davide Beatrici Date: Tue, 8 Oct 2019 03:09:22 +0200 Subject: [PATCH] Revamp JackAudio implementation Some users were encountering issues such as the client taking ~8 seconds to start when "jackd" could not be run, due to the library attempting many times to connect to the JACK server (https://bugs.debian.org/941455). While working on a fix I corrected the many warnings emitted by Clang-Tidy and I realized that there were many things that could be improved. This commit almost entirely rewrites the implementation, but here are some of the changes: - Mutexes are used everywhere, race conditions should not be possible anymore. - The JACK client is not opened until it's required (i.e. "JackAudioInput" and/or "JackAudioOutput" start running). The initialization code has been moved to a dedicated function, the constructor doesn't execute it anymore. This is what fixes the issue mentioned above. - The JACK client is deactivated and closed automatically when both "JackAudioInput" and "JackAudioOutput" are not running. - Code specific to audio input or audio output has been moved from "JackAudioSystem" to the corresponding section ("JackAudioInput" or "JackAudioOutput"). - Some variables in "JackAudioSystem" have been replaced with functions which retrieve the corresponding value from the JACK server. - Removed all instances of "delete", raw pointers have been replaced with "std::unique_ptr<>()". - Replaced "NULL" with "nullptr". --- src/mumble/JackAudio.cpp | 957 +++++++++++++++++++++++---------------- src/mumble/JackAudio.h | 99 ++-- 2 files changed, 629 insertions(+), 427 deletions(-) diff --git a/src/mumble/JackAudio.cpp b/src/mumble/JackAudio.cpp index 2880a9770fa..f22243997b6 100644 --- a/src/mumble/JackAudio.cpp +++ b/src/mumble/JackAudio.cpp @@ -5,53 +5,53 @@ #include "JackAudio.h" +// We define a global macro called 'g'. This can lead to issues when included code uses 'g' as a type or parameter name (like protobuf 3.7 does). As such, for now, we have to make this our last include. #include "Global.h" - -static JackAudioSystem *jasys = NULL; +static std::unique_ptr jas; // jackStatusToStringList converts a jack_status_t (a flag type // that can contain multiple Jack statuses) to a QStringList. -QStringList jackStatusToStringList(jack_status_t status) { +static QStringList jackStatusToStringList(const jack_status_t &status) { QStringList statusList; - if ((status & JackFailure) != 0) { + if (status & JackFailure) { statusList << QLatin1String("JackFailure - overall operation failed"); } - if ((status & JackInvalidOption) != 0) { + if (status & JackInvalidOption) { statusList << QLatin1String("JackInvalidOption - the operation contained an invalid or unsupported option"); } - if ((status & JackNameNotUnique) != 0) { + if (status & JackNameNotUnique) { statusList << QLatin1String("JackNameNotUnique - the desired client name is not unique"); } - if ((status & JackServerStarted) != 0) { + if (status & JackServerStarted) { statusList << QLatin1String("JackServerStarted - the server was started as a result of this operation"); } - if ((status & JackServerFailed) != 0) { + if (status & JackServerFailed) { statusList << QLatin1String("JackServerFailed - unable to connect to the JACK server"); } - if ((status & JackServerError) != 0) { + if (status & JackServerError) { statusList << QLatin1String("JackServerError - communication error with the JACK server"); } - if ((status & JackNoSuchClient) != 0) { + if (status & JackNoSuchClient) { statusList << QLatin1String("JackNoSuchClient - requested client does not exist"); } - if ((status & JackLoadFailure) != 0) { + if (status & JackLoadFailure) { statusList << QLatin1String("JackLoadFailure - unable to load initial client"); } - if ((status & JackInitFailure) != 0) { + if (status & JackInitFailure) { statusList << QLatin1String("JackInitFailure - unable to initialize client"); } - if ((status & JackShmFailure) != 0) { + if (status & JackShmFailure) { statusList << QLatin1String("JackShmFailure - unable to access shared memory"); } - if ((status & JackVersionError) != 0) { + if (status & JackVersionError) { statusList << QLatin1String("JackVersionError - client's protocol version does not match"); } - if ((status & JackBackendError) != 0) { + if (status & JackBackendError) { statusList << QLatin1String("JackBackendError - a backend error occurred"); } - if ((status & JackClientZombie) != 0) { + if (status & JackClientZombie) { statusList << QLatin1String("JackClientZombie - client zombified"); } @@ -77,559 +77,736 @@ class JackAudioOutputRegistrar : public AudioOutputRegistrar { class JackAudioInit : public DeferInit { public: - JackAudioInputRegistrar *airJackAudio; - JackAudioOutputRegistrar *aorJackAudio; - void initialize() { - jasys = new JackAudioSystem(); - jasys->qmWait.lock(); - jasys->qwcWait.wait(&jasys->qmWait, 1000); - jasys->qmWait.unlock(); - if (jasys->bJackIsGood) { - airJackAudio = new JackAudioInputRegistrar(); - aorJackAudio = new JackAudioOutputRegistrar(); - } else { - airJackAudio = NULL; - aorJackAudio = NULL; - delete jasys; - jasys = NULL; - } - } - - void destroy() { - delete airJackAudio; - delete aorJackAudio; - if (jasys) { - delete jasys; - jasys = NULL; - } - } + std::unique_ptr airJackAudio; + std::unique_ptr aorJackAudio; + void initialize(); + void destroy(); }; -static JackAudioInit jackinit; // To instantiate the classes (JackAudioSystem, JackAudioInputRegistrar and JackAudioOutputRegistrar). +JackAudioInputRegistrar::JackAudioInputRegistrar() : AudioInputRegistrar(QLatin1String("JACK"), 10) {} -JackAudioSystem::JackAudioSystem() - : bActive(false) - , client(NULL) - , in_port(NULL) - , output_buffer(NULL) - , iBufferSize(0) - , bJackIsGood(false) - , bInputIsGood(false) - , bOutputIsGood(false) - , iSampleRate(0) -{ - if (g.s.qsJackAudioOutput.isEmpty()) { - iOutPorts = 1; - } else { - iOutPorts = g.s.qsJackAudioOutput.toInt(); +AudioInput *JackAudioInputRegistrar::create() { + return new JackAudioInput(); +} + +const QList JackAudioInputRegistrar::getDeviceChoices() { + QList qlReturn; + + auto qlInputDevs = jas->qhInput.keys(); + std::sort(qlInputDevs.begin(), qlInputDevs.end()); + + for (const auto &dev : qlInputDevs) { + qlReturn << audioDevice(jas->qhInput.value(dev), dev); + } + + return qlReturn; +} + +void JackAudioInputRegistrar::setDeviceChoice(const QVariant &, Settings &) {} + +bool JackAudioInputRegistrar::canEcho(const QString &) const { + return false; +} + +JackAudioOutputRegistrar::JackAudioOutputRegistrar() : AudioOutputRegistrar(QLatin1String("JACK"), 10) {} + +AudioOutput *JackAudioOutputRegistrar::create() { + return new JackAudioOutput(); +} + +const QList JackAudioOutputRegistrar::getDeviceChoices() { + QList qlReturn; + + QStringList qlOutputDevs = jas->qhOutput.keys(); + std::sort(qlOutputDevs.begin(), qlOutputDevs.end()); + + if (qlOutputDevs.contains(g.s.qsJackAudioOutput)) { + qlOutputDevs.removeAll(g.s.qsJackAudioOutput); + qlOutputDevs.prepend(g.s.qsJackAudioOutput); } - memset(reinterpret_cast(&out_ports), 0, sizeof(out_ports)); + foreach(const QString &dev, qlOutputDevs) { + qlReturn << audioDevice(jas->qhOutput.value(dev), dev); + } + + return qlReturn; +} + +void JackAudioOutputRegistrar::setDeviceChoice(const QVariant &choice, Settings &s) { + s.qsJackAudioOutput = choice.toString(); +} + +void JackAudioInit::initialize() { + jas.reset(new JackAudioSystem()); + airJackAudio.reset(new JackAudioInputRegistrar()); + aorJackAudio.reset(new JackAudioOutputRegistrar()); +} + +void JackAudioInit::destroy() { + airJackAudio.reset(); + aorJackAudio.reset(); + jas.reset(); +} + +// Instantiate JackAudioSystem, JackAudioInputRegistrar and JackAudioOutputRegistrar +static JackAudioInit jai; + +JackAudioSystem::JackAudioSystem() + : users(0) + , client(nullptr) +{ qhInput.insert(QString(), tr("Hardware Ports")); qhOutput.insert(QString::number(1), tr("Mono")); qhOutput.insert(QString::number(2), tr("Stereo")); +} + +JackAudioSystem::~JackAudioSystem() { + deinitialize(); +} - jack_status_t status = static_cast(0); - int err = 0; +bool JackAudioSystem::initialize() { + QMutexLocker lock(&qmWait); - jack_options_t jack_option = g.s.bJackStartServer ? JackNullOption : JackNoStartServer; - client = jack_client_open(g.s.qsJackClientName.toStdString().c_str(), jack_option, &status); + if (client) { + lock.unlock(); + deinitialize(); + lock.relock(); + } + jack_status_t status; + client = jack_client_open(g.s.qsJackClientName.toStdString().c_str(), g.s.bJackStartServer ? JackNullOption : JackNoStartServer, &status); if (!client) { - QStringList errors = jackStatusToStringList(status); + const auto errors = jackStatusToStringList(status); qWarning("JackAudioSystem: unable to open client due to %i errors:", errors.count()); - for (int i = 0; i < errors.count(); ++i) { + for (auto i = 0; i < errors.count(); ++i) { qWarning("JackAudioSystem: %s", qPrintable(errors.at(i))); } - return; + return false; } - qWarning("JackAudioSystem: client \"%s\" opened successfully", jack_get_client_name(client)); - iBufferSize = jack_get_buffer_size(client); - iSampleRate = jack_get_sample_rate(client); + qDebug("JackAudioSystem: client \"%s\" opened successfully", jack_get_client_name(client)); - err = jack_set_process_callback(client, process_callback, this); - if (err != 0) { - qWarning("JackAudioSystem: unable to set process callback - jack_set_process_callback() returned %i", err); - return; + 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; } - err = jack_set_sample_rate_callback(client, srate_callback, this); - if (err != 0) { - qWarning("JackAudioSystem: unable to set sample rate callback - jack_set_sample_rate_callback() returned %i", err); + 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; } - err = jack_set_buffer_size_callback(client, buffer_size_callback, this); + const auto clientName = QString::fromLatin1(jack_get_client_name(client)); + + const auto err = jack_client_close(client); if (err != 0) { - qWarning("JackAudioSystem: unable to set buffer size callback - jack_set_buffer_size_callback() returned %i", err); + qWarning("JackAudioSystem: unable to disconnect from the server - jack_client_close() returned %i", err); return; } - jack_on_shutdown(client, shutdown_callback, this); + client = nullptr; - // If we made it this far, then everything is okay - bJackIsGood = true; + qDebug("JackAudioSystem: client \"%s\" closed successfully", clientName.toStdString().c_str()); } -JackAudioSystem::~JackAudioSystem() { +bool JackAudioSystem::activate() { QMutexLocker lock(&qmWait); - if (client) { - int err = 0; - err = jack_deactivate(client); - if (err != 0) { - qWarning("JackAudioSystem: unable to remove client from the process graph - jack_deactivate() returned %i", err); - } - - bActive = false; + if (!client) { + lock.unlock(); - err = jack_client_close(client); - if (err != 0) { - qWarning("JackAudioSystem: unable to disconnect from the server - jack_client_close() returned %i", err); + if (!initialize()) { + return false; } - delete [] output_buffer; - output_buffer = NULL; + lock.relock(); + } + + if (users++ > 0) { + // The client is already active, because there is at least a user + return true; + } - client = NULL; + const auto ret = jack_activate(client); + if (ret != 0) { + qWarning("JackAudioSystem: unable to activate client - jack_activate() returned %i", ret); + return false; } - bJackIsGood = false; + qDebug("JackAudioSystem: client activated"); + + return true; } -void JackAudioSystem::auto_connect_ports() { - if (!(client && g.s.bJackAutoConnect)) { +void JackAudioSystem::deactivate() { + QMutexLocker lock(&qmWait); + + if (!client) { return; } - disconnect_ports(); + if (--users > 0) { + // There is still at least a user, we only decrement the counter + return; + } - const char **ports = NULL; - const int wanted_out_flags = JackPortIsPhysical | JackPortIsOutput; - const int wanted_in_flags = JackPortIsPhysical | JackPortIsInput; - int err; - unsigned int connected_out_ports = 0; - unsigned int connected_in_ports = 0; + 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; + } - ports = jack_get_ports(client, 0, "audio", JackPortIsPhysical); - if (ports != NULL) { - int i = 0; - while (ports[i] != NULL) { - jack_port_t * const port = jack_port_by_name(client, ports[i]); - if (port == NULL) { - qWarning("JackAudioSystem: jack_port_by_name() returned an invalid port - skipping it"); - continue; - } + qDebug("JackAudioSystem: client deactivated"); - const int port_flags = jack_port_flags(port); - - if (bInputIsGood && (port_flags & wanted_out_flags) == wanted_out_flags && connected_in_ports < 1) { - err = jack_connect(client, ports[i], jack_port_name(in_port)); - if (err != 0) { - qWarning("JackAudioSystem: unable to connect port '%s' to '%s' - jack_connect() returned %i", ports[i], jack_port_name(in_port), err); - } else { - connected_in_ports++; - } - } else if (bOutputIsGood && (port_flags & wanted_in_flags) == wanted_in_flags && connected_out_ports < iOutPorts) { - err = jack_connect(client, jack_port_name(out_ports[connected_out_ports]), ports[i]); - if (err != 0) { - qWarning("JackAudioSystem: unable to connect port '%s' to '%s' - jack_connect() returned %i", jack_port_name(out_ports[connected_out_ports]), ports[i], err); - } else { - connected_out_ports++; - } - } + lock.unlock(); - ++i; - } - } + deinitialize(); } -void JackAudioSystem::disconnect_ports() { +bool JackAudioSystem::isOk() { + QMutexLocker lock(&qmWait); + return client != nullptr; +} + +uint8_t JackAudioSystem::outPorts() { + return static_cast(qBound(1, g.s.qsJackAudioOutput.toUInt(), JACK_MAX_OUTPUT_PORTS)); +} + +jack_nframes_t JackAudioSystem::sampleRate() { + QMutexLocker lock(&qmWait); + if (!client) { - return; + return 0; } - // Disconnect the input port - if (in_port != NULL) { - int err = jack_port_disconnect(client, in_port); - if (err != 0) { - qWarning("JackAudioSystem: unable to disconnect in port - jack_port_disconnect() returned %i", err); - } - } + return jack_get_sample_rate(client); +} - // Disconnect the output ports - for (unsigned int i = 0; i < iOutPorts; ++i) { - if (out_ports[i] != NULL) { - int err = jack_port_disconnect(client, out_ports[i]); - if (err != 0) { - qWarning("JackAudioSystem: unable to disconnect out port - jack_port_disconnect() returned %i", err); - } - } +jack_nframes_t JackAudioSystem::bufferSize() { + QMutexLocker lock(&qmWait); + + if (!client) { + return 0; } + + return jack_get_buffer_size(client); } -void JackAudioSystem::activate() { +JackPorts JackAudioSystem::getPhysicalPorts(const uint8_t &flags) { QMutexLocker lock(&qmWait); - if (client) { - if (bActive) { - auto_connect_ports(); - return; - } - int err = jack_activate(client); - if (err != 0) { - qWarning("JackAudioSystem: unable to activate client - jack_activate() returned %i", err); - bJackIsGood = false; - return; - } - bActive = true; + if (!client) { + return JackPorts(); + } - auto_connect_ports(); + const auto ports = jack_get_ports(client, nullptr, "audio", JackPortIsPhysical); + if (!ports) { + return JackPorts(); } -} -int JackAudioSystem::process_callback(jack_nframes_t nframes, void *arg) { - JackAudioSystem * const jas = static_cast(arg); + JackPorts ret; - if (jas && jas->bJackIsGood) { - AudioInputPtr ai = g.ai; - AudioOutputPtr ao = g.ao; - JackAudioInput * const jai = dynamic_cast(ai.get()); - JackAudioOutput * const jao = dynamic_cast(ao.get()); + for (auto i = 0; ports[i]; ++i) { + if (!ports[i]) { + // End of the array + break; + } - if (jai && jai->isRunning() && jai->iMicChannels > 0 && !jai->isFinished()) { - QMutexLocker(&jai->qmMutex); - void *input = jack_port_get_buffer(jas->in_port, nframes); - if (input != NULL) { - jai->addMic(input, nframes); - } + 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 (jao && jao->isRunning() && jao->iChannels > 0 && !jao->isFinished()) { - QMutexLocker(&jao->qmMutex); + if (jack_port_flags(port) & flags) { + ret.append(port); + } + } - jack_default_audio_sample_t *port_buffers[JACK_MAX_OUTPUT_PORTS]; - for (unsigned int i = 0; i < jao->iChannels; ++i) { + jack_free(ports); - port_buffers[i] = (jack_default_audio_sample_t*)jack_port_get_buffer(jas->out_ports[i], nframes); - if (port_buffers[i] == NULL) { - return 1; - } - } + return ret; +} - jack_default_audio_sample_t * const buffer = jas->output_buffer; - memset(buffer, 0, sizeof(jack_default_audio_sample_t) * nframes * jao->iChannels); +jack_port_t *JackAudioSystem::registerPort(const char *name, const uint8_t &flags) { + QMutexLocker lock(&qmWait); - jao->mix(buffer, nframes); + if (!client || !name) { + return nullptr; + } - if (jao->iChannels == 1) { + return jack_port_register(client, name, JACK_DEFAULT_AUDIO_TYPE, flags, 0); +} - memcpy(port_buffers[0], buffer, sizeof(jack_default_audio_sample_t) * nframes); - } else { +bool JackAudioSystem::unregisterPort(jack_port_t *port) { + QMutexLocker lock(&qmWait); - // de-interleave channels - for (unsigned int i = 0; i < nframes * jao->iChannels; ++i) { - port_buffers[i % jao->iChannels][i / jao->iChannels] = buffer[i]; - } - } - } + if (!client || !port) { + return false; } - return 0; -} + 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; + } -int JackAudioSystem::srate_callback(jack_nframes_t frames, void *arg) { - JackAudioSystem * const jas = static_cast(arg); - jas->iSampleRate = frames; - return 0; + return true; } -void JackAudioSystem::allocOutputBuffer(jack_nframes_t frames) { - iBufferSize = frames; - AudioOutputPtr ao = g.ao; - JackAudioOutput * const jao = dynamic_cast(ao.get()); +bool JackAudioSystem::connectPort(jack_port_t *sourcePort, jack_port_t *destinationPort) { + QMutexLocker lock(&qmWait); - if (jao) { - jao->qmMutex.lock(); + if (!client || !sourcePort || !destinationPort) { + return false; } - if (output_buffer) { - delete [] output_buffer; - output_buffer = NULL; + + 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; } - output_buffer = new jack_default_audio_sample_t[frames * iOutPorts]; - if (output_buffer == NULL) { - bJackIsGood = false; + + return true; +} + +bool JackAudioSystem::disconnectPort(jack_port_t *port) { + QMutexLocker lock(&qmWait); + + if (!client || !port) { + return false; } - if (jao) { - jao->qmMutex.unlock(); + 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; } -void JackAudioSystem::initializeInput() { - QMutexLocker lock(&qmWait); +int JackAudioSystem::processCallback(jack_nframes_t frames, void *) { + QMutexLocker lock(&jas->qmWait); - if (!jasys->bJackIsGood) { - return; + auto const jai = dynamic_cast(g.ai.get()); + auto const jao = dynamic_cast(g.ao.get()); + + const bool input = (jai && jai->isReady()); + const bool output = (jao && jao->isReady()); + + if (input && !jai->process(frames)) { + return 1; } - AudioInputPtr ai = g.ai; - JackAudioInput * const jai = dynamic_cast(ai.get()); + if (output && !jao->process(frames)) { + return 1; + } + + return 0; +} + +int JackAudioSystem::sampleRateCallback(jack_nframes_t, void *) { + auto const jai = dynamic_cast(g.ai.get()); + auto const jao = dynamic_cast(g.ao.get()); if (jai) { - jai->qmMutex.lock(); + jai->activate(); } - in_port = jack_port_register(client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); - if (in_port == NULL) { - qWarning("JackAudioSystem: unable to register 'input' port"); - return; + if (jao) { + jao->activate(); } - bInputIsGood = true; + return 0; +} - if (jai) { - jai->qmMutex.unlock(); +int JackAudioSystem::bufferSizeCallback(jack_nframes_t frames, void *) { + auto const jao = dynamic_cast(g.ao.get()); + if (jao) { + jao->allocBuffer(frames); } + + return 0; } -void JackAudioSystem::destroyInput() { - AudioInputPtr ai = g.ai; - JackAudioInput * const jai = dynamic_cast(ai.get()); +void JackAudioSystem::shutdownCallback(void *) { + qWarning("JackAudioSystem: server shutdown"); - if (jai) { - jai->qmMutex.lock(); - } + QMutexLocker lock(&jas->qmWait); + jas->client = nullptr; + jas->users = 0; +} - if (in_port != NULL) { - int err = jack_port_unregister(client, in_port); - if (err != 0) { - qWarning("JackAudioSystem: unable to unregister in port - jack_port_unregister() returned %i", err); - return; - } - } +JackAudioInput::JackAudioInput() + : port(nullptr) +{ + bReady = activate(); +} - bInputIsGood = false; +JackAudioInput::~JackAudioInput() { + // Request interruption + qmWait.lock(); + bReady = false; + qwcSleep.wakeAll(); + qmWait.unlock(); - if (jai) { - jai->qmMutex.unlock(); + // Wait for thread to exit + wait(); + + // Cleanup + deactivate(); +} + +bool JackAudioInput::isReady() { + QMutexLocker lock(&qmWait); + return bReady; +} + +bool JackAudioInput::activate() { + QMutexLocker lock(&qmWait); + + if (!jas->activate()) { + return false; } + + eMicFormat = SampleFloat; + iMicChannels = 1; + iMicFreq = jas->sampleRate(); + + initializeMixer(); + + lock.unlock(); + + return registerPorts(); +} + +void JackAudioInput::deactivate() { + unregisterPorts(); + jas->deactivate(); } -void JackAudioSystem::initializeOutput() { +bool JackAudioInput::registerPorts() { + unregisterPorts(); + QMutexLocker lock(&qmWait); - if (!jasys->bJackIsGood) { - return; + port = jas->registerPort("input", JackPortIsInput); + if (!port) { + qWarning("JackAudioInput: unable to register port"); + return false; } - AudioOutputPtr ao = g.ao; - JackAudioOutput * const jao = dynamic_cast(ao.get()); + return true; +} - allocOutputBuffer(iBufferSize); +bool JackAudioInput::unregisterPorts() { + QMutexLocker lock(&qmWait); - if (jao) { - jao->qmMutex.lock(); + if (!port) { + return false; } - for (unsigned int i = 0; i < iOutPorts; ++i) { - char name[10]; - snprintf(name, 10, "output_%d", i + 1); + 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; + } - out_ports[i] = jack_port_register(client, name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); - if (out_ports[i] == NULL) { - qWarning("JackAudioSystem: unable to register 'output' port"); + const JackPorts outputPorts = jas->getPhysicalPorts(JackPortIsOutput); + for (auto outputPort : outputPorts) { + if (jas->connectPort(outputPort, port)) { break; } } +} - bOutputIsGood = true; +bool JackAudioInput::disconnectPorts() { + QMutexLocker lock(&qmWait); - if (jao) { - jao->qmMutex.unlock(); + if (!port) { + return true; + } + + if (!jas->disconnectPort(port)) { + qWarning("JackAudioInput: unable to disconnect port"); + return false; } + + return true; } -void JackAudioSystem::destroyOutput() { - AudioOutputPtr ao = g.ao; - JackAudioOutput * const jao = dynamic_cast(ao.get()); +bool JackAudioInput::process(const jack_nframes_t &frames) { + QMutexLocker lock(&qmWait); - if (jao) { - jao->qmMutex.lock(); + auto input = jack_port_get_buffer(port, frames); + if (!input) { + return false; } - delete [] output_buffer; - output_buffer = NULL; + addMic(input, frames); - for (unsigned int i = 0; i < iOutPorts; ++i) { - if (out_ports[i] != NULL) { - int err = jack_port_unregister(client, out_ports[i]); - if (err != 0) { - qWarning("JackAudioSystem: unable to unregister out port - jack_port_unregister() returned %i", err); - } - out_ports[i] = NULL; - } - } + return true; +} - bOutputIsGood = false; +void JackAudioInput::run() { + if (!bReady) { + return; + } - if (jao) { - jao->qmMutex.unlock(); + // Initialization + if (g.s.bJackAutoConnect) { + connectPorts(); } + + // Pause thread until interruption is requested by the destructor + qmWait.lock(); + qwcSleep.wait(&qmWait); + qmWait.unlock(); + + // Cleanup + disconnectPorts(); } -int JackAudioSystem::buffer_size_callback(jack_nframes_t frames, void *arg) { - JackAudioSystem * const jas = static_cast(arg); - jas->allocOutputBuffer(frames); - return 0; +JackAudioOutput::JackAudioOutput() { + bReady = activate(); } -void JackAudioSystem::shutdown_callback(void *arg) { - JackAudioSystem * const jas = static_cast(arg); - jas->bJackIsGood = false; +JackAudioOutput::~JackAudioOutput() { + // Request interruption + qmWait.lock(); + bReady = false; + qwcSleep.wakeAll(); + qmWait.unlock(); + + // Wait for thread to exit + wait(); + + // Cleanup + deactivate(); } -JackAudioInputRegistrar::JackAudioInputRegistrar() : AudioInputRegistrar(QLatin1String("JACK"), 10) { +bool JackAudioOutput::isReady() { + QMutexLocker lock(&qmWait); + return bReady; } -AudioInput *JackAudioInputRegistrar::create() { - return new JackAudioInput(); +void JackAudioOutput::allocBuffer(const jack_nframes_t &frames) { + QMutexLocker lock(&qmWait); + buffer.reset(new jack_default_audio_sample_t[frames * iChannels]); } -const QList JackAudioInputRegistrar::getDeviceChoices() { - QList qlReturn; +bool JackAudioOutput::activate() { + QMutexLocker lock(&qmWait); - QStringList qlInputDevs = jasys->qhInput.keys(); - std::sort(qlInputDevs.begin(), qlInputDevs.end()); + if (!jas->activate()) { + return false; + } + + 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(); - foreach(const QString &dev, qlInputDevs) { - qlReturn << audioDevice(jasys->qhInput.value(dev), dev); + allocBuffer(jas->bufferSize()); + + if (!registerPorts()) { + return false; } - return qlReturn; + return true; } -void JackAudioInputRegistrar::setDeviceChoice(const QVariant &choice, Settings &s) { - Q_UNUSED(choice); - Q_UNUSED(s); +void JackAudioOutput::deactivate() { + unregisterPorts(); + jas->deactivate(); + buffer.reset(); } -bool JackAudioInputRegistrar::canEcho(const QString &osys) const { - Q_UNUSED(osys); - return false; -} +bool JackAudioOutput::registerPorts() { + unregisterPorts(); -JackAudioOutputRegistrar::JackAudioOutputRegistrar() : AudioOutputRegistrar(QLatin1String("JACK"), 10) { -} + QMutexLocker lock(&qmWait); -AudioOutput *JackAudioOutputRegistrar::create() { - return new JackAudioOutput(); + 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 == nullptr) { + qWarning("JackAudioOutput: unable to register port #%u", i); + return false; + } + + ports.append(port); + } + + return true; } -const QList JackAudioOutputRegistrar::getDeviceChoices() { - QList qlReturn; +bool JackAudioOutput::unregisterPorts() { + QMutexLocker lock(&qmWait); - QStringList qlOutputDevs = jasys->qhOutput.keys(); - std::sort(qlOutputDevs.begin(), qlOutputDevs.end()); + bool ret = true; - if (qlOutputDevs.contains(g.s.qsJackAudioOutput)) { - qlOutputDevs.removeAll(g.s.qsJackAudioOutput); - qlOutputDevs.prepend(g.s.qsJackAudioOutput); - } + for (auto i = 0; i < ports.size(); ++i) { + if (!ports[i]) { + continue; + } - foreach(const QString &dev, qlOutputDevs) { - qlReturn << audioDevice(jasys->qhOutput.value(dev), dev); + if (!jas->unregisterPort(ports[i])) { + qWarning("JackAudioOutput: unable to unregister port #%u", i); + ret = false; + } } - return qlReturn; -} + ports.clear(); -void JackAudioOutputRegistrar::setDeviceChoice(const QVariant &choice, Settings &s) { - s.qsJackAudioOutput = choice.toString(); - jasys->iOutPorts = qBound(1, choice.toInt(), JACK_MAX_OUTPUT_PORTS); + return ret; } -JackAudioInput::JackAudioInput() { - bRunning = true; - iMicChannels = 0; -} +void JackAudioOutput::connectPorts() { + disconnectPorts(); -JackAudioInput::~JackAudioInput() { - bRunning = false; - iMicChannels = 0; - qmMutex.lock(); - qwcWait.wakeAll(); - qmMutex.unlock(); - wait(); -} + QMutexLocker lock(&qmWait); -void JackAudioInput::run() { - if (!jasys) { - exit(1); - } + const auto inputPorts = jas->getPhysicalPorts(JackPortIsInput); + uint8_t i = 0; + + for (auto inputPort : inputPorts) { + if (i == ports.size()) { + break; + } - jasys->initializeInput(); + if (ports[i]) { + if (!jas->connectPort(ports[i], inputPort)) { + continue; + } + } - if (!jasys->bInputIsGood) { - exit(1); + ++i; } +} - iMicFreq = jasys->iSampleRate; - iMicChannels = 1; - eMicFormat = SampleFloat; - initializeMixer(); - jasys->activate(); +bool JackAudioOutput::disconnectPorts() { + QMutexLocker lock(&qmWait); - qmMutex.lock(); - while (bRunning) - qwcWait.wait(&qmMutex); - qmMutex.unlock(); + bool ret = true; - jasys->destroyInput(); -} + 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; + } + } -JackAudioOutput::JackAudioOutput() { - bRunning = true; - iChannels = 0; + return ret; } -JackAudioOutput::~JackAudioOutput() { - bRunning = false; - iChannels = 0; - qmMutex.lock(); - qwcWait.wakeAll(); - qmMutex.unlock(); - wait(); -} +bool JackAudioOutput::process(const jack_nframes_t &frames) { + QMutexLocker lock(&qmWait); -void JackAudioOutput::run() { - if (!jasys) { - exit(1); + const auto audioToReproduce = mix(buffer.get(), frames); + + QVector inputBuffers; + + for (decltype(iChannels) currentChannel = 0; currentChannel < iChannels; ++currentChannel) { + auto inputBuffer = reinterpret_cast(jack_port_get_buffer(ports[currentChannel], frames)); + if (!inputBuffer) { + return false; + } + + if (!audioToReproduce) { + // Clear buffer + memset(inputBuffer, 0, sizeof(jack_default_audio_sample_t) * frames); + } + + inputBuffers.append(inputBuffer); } - jasys->initializeOutput(); + if (!audioToReproduce) { + return true; + } + + const auto samples = frames * iChannels; - if (!jasys->bOutputIsGood) { - exit(1); + if (samples > frames) { + // De-interleave channels + for (auto currentSample = decltype(samples){0}; currentSample < samples; ++currentSample) { + inputBuffers[currentSample % iChannels][currentSample / iChannels] = buffer[currentSample]; + } + } else { + // Single channel + memcpy(inputBuffers[0], buffer.get(), sizeof(jack_default_audio_sample_t) * samples); } - unsigned int chanmasks[32]; + return true; +} - chanmasks[0] = SPEAKER_FRONT_LEFT; - chanmasks[1] = SPEAKER_FRONT_RIGHT; +void JackAudioOutput::run() { + if (!bReady) { + return; + } - eSampleFormat = SampleFloat; - iChannels = jasys->iOutPorts; - iMixerFreq = jasys->iSampleRate; - initializeMixer(chanmasks); - jasys->activate(); + // Initialization + if (g.s.bJackAutoConnect) { + connectPorts(); + } - qmMutex.lock(); - while (bRunning) - qwcWait.wait(&qmMutex); - qmMutex.unlock(); + // Pause thread until interruption is requested by the destructor + qmWait.lock(); + qwcSleep.wait(&qmWait); + qmWait.unlock(); - jasys->destroyOutput(); + // Cleanup + disconnectPorts(); } diff --git a/src/mumble/JackAudio.h b/src/mumble/JackAudio.h index 704677b7412..13005f1c3d4 100644 --- a/src/mumble/JackAudio.h +++ b/src/mumble/JackAudio.h @@ -9,85 +9,110 @@ #include "AudioInput.h" #include "AudioOutput.h" +#include #include #include #define JACK_MAX_OUTPUT_PORTS 2 -class JackAudioOutput; -class JackAudioInput; +typedef QVector JackPorts; + +class JackAudioInit; class JackAudioSystem : public QObject { + friend JackAudioInit; + private: Q_OBJECT Q_DISABLE_COPY(JackAudioSystem) + protected: - bool bActive; + uint8_t users; + QMutex qmWait; + QWaitCondition qwcWait; jack_client_t *client; - jack_port_t *in_port; - jack_port_t *out_ports[JACK_MAX_OUTPUT_PORTS]; - jack_default_audio_sample_t *output_buffer; - jack_nframes_t iBufferSize; - - void allocOutputBuffer(jack_nframes_t frames); - void auto_connect_ports(); - void disconnect_ports(); + static int processCallback(jack_nframes_t frames, void *); + static int sampleRateCallback(jack_nframes_t, void *); + static int bufferSizeCallback(jack_nframes_t frames, void *); + static void shutdownCallback(void *); - static int process_callback(jack_nframes_t nframes, void *arg); - static int srate_callback(jack_nframes_t frames, void *arg); - static int buffer_size_callback(jack_nframes_t frames, void *arg); - static void shutdown_callback(void *arg); public: QHash qhInput; QHash qhOutput; - bool bJackIsGood; - bool bInputIsGood; - bool bOutputIsGood; - int iSampleRate; - unsigned int iOutPorts; - QMutex qmWait; - QWaitCondition qwcWait; - - void activate(); - void initializeInput(); - void destroyInput(); - - void initializeOutput(); - void destroyOutput(); + bool isOk(); + uint8_t outPorts(); + jack_nframes_t sampleRate(); + jack_nframes_t bufferSize(); + JackPorts getPhysicalPorts(const uint8_t &flags); + jack_port_t *registerPort(const char *name, const uint8_t &flags); + bool unregisterPort(jack_port_t *port); + bool connectPort(jack_port_t *sourcePort, jack_port_t *destinationPort); + bool disconnectPort(jack_port_t *port); + + bool initialize(); + void deinitialize(); + bool activate(); + void deactivate(); JackAudioSystem(); ~JackAudioSystem(); }; class JackAudioInput : public AudioInput { - friend class JackAudioSystem; private: Q_OBJECT Q_DISABLE_COPY(JackAudioInput) + protected: - QMutex qmMutex; - QWaitCondition qwcWait; + bool bReady; + QMutex qmWait; + QWaitCondition qwcSleep; + jack_port_t *port; + public: + bool isReady(); + bool process(const jack_nframes_t &frames); + bool activate(); + void deactivate(); + bool registerPorts(); + bool unregisterPorts(); + void connectPorts(); + bool disconnectPorts(); + + void run() Q_DECL_OVERRIDE; JackAudioInput(); ~JackAudioInput() Q_DECL_OVERRIDE; - void run() Q_DECL_OVERRIDE; }; class JackAudioOutput : public AudioOutput { - friend class JackAudioSystem; private: Q_OBJECT Q_DISABLE_COPY(JackAudioOutput) + protected: - QMutex qmMutex; - QWaitCondition qwcWait; + bool bReady; + QMutex qmWait; + QWaitCondition qwcSleep; + JackPorts ports; + std::unique_ptr buffer; + public: + bool isReady(); + bool process(const jack_nframes_t &frames); + void allocBuffer(const jack_nframes_t &frames); + bool activate(); + void deactivate(); + bool registerPorts(); + bool unregisterPorts(); + void connectPorts(); + bool disconnectPorts(); + + void run() Q_DECL_OVERRIDE; JackAudioOutput(); ~JackAudioOutput() Q_DECL_OVERRIDE; - void run() Q_DECL_OVERRIDE; }; #endif