diff --git a/.travis.yml b/.travis.yml index ce9e997..2b6fbe4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,11 +10,12 @@ before_install: install: - sudo apt-get install build-essential libgl1-mesa-dev - sudo apt-get install qt514base qt514multimedia qt514quickcontrols2 + - sudo apt-get install libjack-dev - source /opt/qt514/bin/qt514-env.sh script: - - /opt/qt514/bin/qmake PREFIX=/usr + - /opt/qt514/bin/qmake PREFIX=/usr CONFIG+=jack - make - # Generate AppImage # - sudo apt-get -y install checkinstall diff --git a/OpenSoundMeter.pro b/OpenSoundMeter.pro index a52b502..f9cef4a 100644 --- a/OpenSoundMeter.pro +++ b/OpenSoundMeter.pro @@ -300,6 +300,18 @@ unix:!macx:!ios { src/audio/plugins/alsa.cpp LIBS += -lasound + + CONFIG(jack) { + HEADERS += \ + src/audio/plugins/jack.h + + SOURCES += \ + src/audio/plugins/jack.cpp + + LIBS += -ljack + + DEFINES += USE_JACK + } } GRAPH = $$(GRAPH_BACKEND) diff --git a/src/audio/client.cpp b/src/audio/client.cpp index d319c72..db7c377 100644 --- a/src/audio/client.cpp +++ b/src/audio/client.cpp @@ -35,6 +35,10 @@ #include "plugins/alsa.h" #endif +#ifdef USE_JACK +#include "plugins/jack.h" +#endif + #ifdef USE_ASIO #include "plugins/asioplugin.h" #endif @@ -89,6 +93,10 @@ void Client::initPlugins() m_plugins.push_back(QSharedPointer(new AlsaPlugin())); #endif +#ifdef USE_JACK + m_plugins.push_back(QSharedPointer(new JackPlugin())); +#endif + for (auto &&plugin : m_plugins) { connect(plugin.data(), &Plugin::deviceListChanged, this, [this]() { refreshDeviceList(); diff --git a/src/audio/plugins/jack.cpp b/src/audio/plugins/jack.cpp new file mode 100644 index 0000000..3a87edf --- /dev/null +++ b/src/audio/plugins/jack.cpp @@ -0,0 +1,462 @@ +/** + * OSM + * Copyright (C) 2022 Adrian Schollmeyer + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "jack.h" +#include +#include + +namespace { +constexpr unsigned int DEFAULT_SAMPLE_RATE{48000}; +constexpr unsigned int DEFAULT_CHANNEL_COUNT_INPUT{2}; +constexpr unsigned int DEFAULT_CHANNEL_COUNT_OUTPUT{1}; + +constexpr const char* PLUGIN_NAME{"JACK"}; +constexpr const char* JACK_CLIENT_NAME{"OpenSoundMeter"}; +constexpr const char* DEFAULT_MEASUREMENT_CHANNEL{"measurement"}; +constexpr const char* DEFAULT_REFERENCE_CHANNEL{"reference"}; +constexpr const char* DEFAULT_OUTPUT_CHANNEL{"out"}; +} + +namespace audio { +JackClient::JackClient(const Plugin::Direction &direction, QIODevice& endpoint) + : m_client(nullptr, jackClientDeleter()), m_direction(direction), m_endpoint(endpoint) +{ + initJackClient(); + initPorts(); + initSampleRate(); + initBufferSize(); + initProcessing(); +} + +JackClient::~JackClient() +{ + // Explicitly stop processing first, to prevent us still running the JACK + // processor while the rest of the object is being cleaned up + if (m_client) + close(); +} + +jack_nframes_t JackClient::currentSampleRate() const +{ + return m_sampleRate; +} + +std::size_t JackClient::currentChannelCount() const +{ + return m_ports.size(); +} + +Plugin::Direction JackClient::direction() const +{ + return m_direction; +} + +void JackClient::close() +{ + deactivate(); + m_client.reset(nullptr); +} + +void JackClient::initJackClient() +{ + jack_status_t jackStatus; + jack_client_t* jackClient = jack_client_open( + JACK_CLIENT_NAME, + JackOptions::JackNoStartServer, + &jackStatus + ); + + if (!jackClient) + throw JackPluginException{"Failed to open JACK client"}; + + m_client.reset(jackClient); +} + +void JackClient::initSampleRate() +{ + assert(m_client); + m_sampleRate = jack_get_sample_rate(m_client.get()); + qDebug() << "[JACK] Sample rate set: " << m_sampleRate; + + int res = jack_set_sample_rate_callback( + m_client.get(), + jackSampleRateCallback, + this + ); + if (res != 0) + throw JackPluginException{"Failed to register sample rate callback"}; +} + +void JackClient::initBufferSize() +{ + assert(m_client); + assert(!m_ports.empty()); + m_bufferSize = jack_get_buffer_size(m_client.get()); + m_sampleBuffer.resize(m_bufferSize * m_ports.size()); + qDebug() << "[JACK] Buffer size set: " << m_bufferSize; + + int res = jack_set_buffer_size_callback( + m_client.get(), + jackBufferSizeCallback, + this + ); + if (res != 0) + throw JackPluginException{"Failed to register buffer size callback"}; +} + +void JackClient::initPorts() +{ + assert(m_client); + switch (m_direction) { + case Plugin::Direction::Input: + registerInputs(); + break; + + case Plugin::Direction::Output: + registerOutputs(); + break; + + default: + throw JackPluginException{"Unsupported signal direction"}; + } +} + +void JackClient::initProcessing() +{ + assert(m_client); + int res = jack_set_process_callback( + m_client.get(), + jackProcessCallback, + this + ); + if (res != 0) + throw JackPluginException{"Failed to register process callback"}; +} + +void JackClient::activate() +{ + assert(m_client); + if (m_active) + return; + + qDebug() << "[JACK] Activating"; + int res = jack_activate(m_client.get()); + if (res != 0) + throw JackPluginException{"Failed to activate client"}; + + m_active = true; +} + +void JackClient::deactivate() +{ + if (!m_active) + return; + + m_active = false; + + assert(m_client); + int res = jack_deactivate(m_client.get()); + if (res != 0) + throw JackPluginException{"Failed to deactivate client"}; +} + +void JackClient::registerInputs() +{ + qDebug() << "[JACK] Registering Inputs"; + jack_port_t *measurementPort = jack_port_register( + m_client.get(), + DEFAULT_MEASUREMENT_CHANNEL, + JACK_DEFAULT_AUDIO_TYPE, + JackPortFlags::JackPortIsInput, + 0 + ); + if (!measurementPort) + throw JackPluginException{"Failed to register measurement port"}; + m_ports.emplace_back(measurementPort, jackPortDeleter(m_client.get())); + m_portBuffers.push_back(nullptr); + + jack_port_t *referencePort = jack_port_register( + m_client.get(), + DEFAULT_REFERENCE_CHANNEL, + JACK_DEFAULT_AUDIO_TYPE, + JackPortFlags::JackPortIsInput, + 0 + ); + if (!referencePort) + throw JackPluginException{"Failed to register reference port"}; + m_ports.emplace_back(referencePort, jackPortDeleter(m_client.get())); + m_portBuffers.push_back(nullptr); +} + +void JackClient::registerOutputs() +{ + qDebug() << "[JACK] Registering Outputs"; + jack_port_t *outputPort = jack_port_register( + m_client.get(), + DEFAULT_OUTPUT_CHANNEL, + JACK_DEFAULT_AUDIO_TYPE, + JackPortFlags::JackPortIsOutput, + 0 + ); + if (!outputPort) + throw JackPluginException{"Failed to register output port"}; + m_ports.emplace_back(outputPort, jackPortDeleter(m_client.get())); + m_portBuffers.push_back(nullptr); +} + +std::function JackClient::jackClientDeleter() +{ + return [this](jack_client_t *client) -> void { + m_ports.resize(0); + jack_client_close(client); + }; +} + +std::function JackClient::jackPortDeleter(jack_client_t *client) +{ + // We can't use the object's client member here as it might already have + // changed if this deleter is called in the client object's deleter, + // resulting in an assertion failure or segfault. + assert(client); + return [client](jack_port_t *port) -> void { + jack_port_unregister(client, port); + }; +} + +int JackClient::jackSampleRateCallback(jack_nframes_t newSampleRate, void* obj) +{ + assert(obj); + return reinterpret_cast(obj)->jackSampleRateCallbackInt(newSampleRate); +} + +int JackClient::jackSampleRateCallbackInt(jack_nframes_t newSampleRate) +{ + m_sampleRate = newSampleRate; + qInfo() << "[JACK] Sample rate updated: " << m_sampleRate; + emit sampleRateChanged(m_sampleRate); + return 0; +} + +int JackClient::jackBufferSizeCallback(jack_nframes_t newBufferSize, void* obj) +{ + assert(obj); + return reinterpret_cast(obj)->jackBufferSizeCallbackInt(newBufferSize); +} + +int JackClient::jackBufferSizeCallbackInt(jack_nframes_t newBufferSize) +{ + m_bufferSize = newBufferSize; + qInfo() << "[JACK] Buffer size updated: " << m_bufferSize; + if (newBufferSize > 0) + m_sampleBuffer.resize(newBufferSize * m_ports.size()); + + return 0; +} + +int JackClient::jackProcessCallback(jack_nframes_t nframes, void* obj) +{ + assert(obj); + return reinterpret_cast(obj)->jackProcessCallbackInt(nframes); +} + +int JackClient::jackProcessCallbackInt(jack_nframes_t nframes) +{ + // Yes, this can actually happen + if (m_bufferSize == 0) + return 1; + + if (!m_active) + return 0; + + assert(m_ports.size() == m_portBuffers.size()); + assert(nframes == m_bufferSize); + assert(m_endpoint.isOpen()); + + if (m_direction == Plugin::Direction::Input) { + processInputPorts(nframes); + } else { + processOutputPorts(nframes); + } + return 0; +} + +void JackClient::processInputPorts(jack_nframes_t nframes) +{ + if (!m_endpoint.isWritable()) + return; + + for (std::size_t i = 0; i < m_ports.size(); ++i) { + m_portBuffers[i] = reinterpret_cast( + jack_port_get_buffer(m_ports[i].get(), nframes)); + } + + std::size_t sampleBufferPos{0}; + for (jack_nframes_t i = 0; i < nframes; ++i) { + for (const float* portBuf : m_portBuffers) { + assert(sampleBufferPos < m_sampleBuffer.size()); + m_sampleBuffer[sampleBufferPos] = portBuf[i]; + sampleBufferPos++; + } + } + assert(sampleBufferPos == nframes * m_portBuffers.size()); + m_endpoint.write( + reinterpret_cast(m_sampleBuffer.data()), + sampleBufferPos * sizeof(float) + ); +} + +void JackClient::processOutputPorts(jack_nframes_t nframes) +{ + if (!m_endpoint.isReadable()) + return; + + for (std::size_t i = 0; i < m_ports.size(); ++i) { + m_portBuffers[i] = reinterpret_cast( + jack_port_get_buffer(m_ports[i].get(), nframes)); + } + + const std::size_t samplesToWrite = std::max( + static_cast(nframes), + static_cast(m_endpoint.bytesAvailable() / sizeof(float)) + ); + assert(samplesToWrite % m_ports.size() == 0); + + std::size_t portIdx{0}; + std::size_t portBufferPos{0}; + for (std::size_t i = 0; i < samplesToWrite; ++i) { + assert(portIdx < m_portBuffers.size()); + assert(portBufferPos < m_bufferSize); + const std::size_t bytesRead = m_endpoint.read( + reinterpret_cast(m_portBuffers[portIdx] + portBufferPos), + sizeof(float) + ); + assert(bytesRead == sizeof(float)); + + portIdx = (portIdx + 1) % m_portBuffers.size(); + if (portIdx == 0) + portBufferPos++; + } +} + +JackPlugin::JackPlugin() +{ + DeviceInfo m_defaultDeviceInfo{PLUGIN_NAME, PLUGIN_NAME}; + m_defaultDeviceInfo.setName(PLUGIN_NAME); + m_defaultDeviceInfo.setInputChannels({ + DEFAULT_MEASUREMENT_CHANNEL, + DEFAULT_REFERENCE_CHANNEL + }); + m_defaultDeviceInfo.setOutputChannels({DEFAULT_OUTPUT_CHANNEL}); + m_defaultDeviceInfo.setDefaultSampleRate(DEFAULT_SAMPLE_RATE); + m_list.push_back(m_defaultDeviceInfo); +} + +QString JackPlugin::name() const +{ + return PLUGIN_NAME; +} + +DeviceInfo::List JackPlugin::getDeviceInfoList() const +{ + return m_list; +} + +DeviceInfo::Id JackPlugin::defaultDeviceId( + [[maybe_unused]] const Plugin::Direction &mode) const +{ + return DeviceInfo::Id(); +} + +Format JackPlugin::deviceFormat( + [[maybe_unused]] const DeviceInfo::Id &id, + const Plugin::Direction &mode) const +{ + const auto client_it = std::find_if( + m_clients.begin(), + m_clients.end(), + [mode](const auto& client) { + + return client.second->direction() == mode; + }); + + if (client_it == m_clients.end()) { + if (mode == Plugin::Direction::Input) + return {DEFAULT_SAMPLE_RATE, DEFAULT_CHANNEL_COUNT_INPUT}; + else + return {DEFAULT_SAMPLE_RATE, DEFAULT_CHANNEL_COUNT_OUTPUT}; + } else { + return { + static_cast(client_it->second->currentSampleRate()), + static_cast(client_it->second->currentChannelCount()) + }; + } +} + +Stream *JackPlugin::open( + [[maybe_unused]] const DeviceInfo::Id &id, + const Plugin::Direction &mode, + [[maybe_unused]] const Format &format, + QIODevice *endpoint) +{ + // The sample rate is set by JACK, so we don't mess with it. + // The channel count is fixed to the minimum amount of channels necessary + // for OSM to work as we can freely patch the cannels in JACK. + // + // The ID can be ignored as we create individual JACK clients + // for each requested Stream. + try { + std::unique_ptr jack = std::make_unique(mode, *endpoint); + + Format f{ + jack->currentSampleRate(), + static_cast(jack->currentChannelCount()) + }; + Stream *stream = new Stream(f); + + m_clients.emplace(stream, std::move(jack)); + JackClient& jackClient = *m_clients.at(stream).get(); + + connect(stream, &Stream::closeMe, this, [this, stream, endpoint]() { + // Order is important here to prevent JACK still writing data to + // the endpoint after it has been closed. + m_clients.erase(stream); + + if (endpoint->isOpen()) + endpoint->close(); + + stream->deleteLater(); + }); + + connect( + &jackClient, + &JackClient::sampleRateChanged, + this, + [stream](jack_nframes_t newRate) { + assert(stream); + stream->setSampleRate(static_cast(newRate)); + }); + + endpoint->open(mode == Input ? QIODevice::WriteOnly : QIODevice::ReadOnly); + jackClient.activate(); + + return stream; + } catch (JackPluginException& e) { + return nullptr; + } +} + +} diff --git a/src/audio/plugins/jack.h b/src/audio/plugins/jack.h new file mode 100644 index 0000000..9685f65 --- /dev/null +++ b/src/audio/plugins/jack.h @@ -0,0 +1,127 @@ +/** + * OSM + * Copyright (C) 2022 Adrian Schollmeyer + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef AUDIO_JACKPLUGIN_H +#define AUDIO_JACKPLUGIN_H + +#include "../plugin.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace audio { +class JackClient : public QObject +{ + Q_OBJECT + +public: + constexpr static std::size_t BUFFER_SIZE_MULTIPLIER{4}; + + JackClient(const Plugin::Direction &direction, QIODevice& endpoint); + JackClient(JackClient&&) = default; + JackClient(const JackClient &) = delete; + ~JackClient(); + + void activate(); + void deactivate(); + + jack_nframes_t currentSampleRate() const; + std::size_t currentChannelCount() const; + + Plugin::Direction direction() const; + +signals: + void sampleRateChanged(jack_nframes_t newSampleRate); + +public slots: + void close(); + +private: + void initJackClient(); + void initPorts(); + void initSampleRate(); + void initBufferSize(); + void initProcessing(); + + void registerInputs(); + void registerOutputs(); + + std::function jackClientDeleter(); + static std::function jackPortDeleter(jack_client_t *client); + + static int jackSampleRateCallback(jack_nframes_t newSampleRate, void* obj); + int jackSampleRateCallbackInt(jack_nframes_t newSampleRate); + + static int jackBufferSizeCallback(jack_nframes_t newBufferSize, void* obj); + int jackBufferSizeCallbackInt(jack_nframes_t newBufferSize); + + static int jackProcessCallback(jack_nframes_t nframes, void* obj); + int jackProcessCallbackInt(jack_nframes_t nframes); + + void processInputPorts(jack_nframes_t nframes); + void processOutputPorts(jack_nframes_t nframes); + + std::unique_ptr> m_client; + std::vector>> m_ports; + std::vector m_portBuffers; + jack_nframes_t m_sampleRate; + jack_nframes_t m_bufferSize; + const Plugin::Direction m_direction; + QIODevice& m_endpoint; + std::vector m_sampleBuffer; + bool m_active{false}; +}; + +class JackPlugin : public Plugin +{ + Q_OBJECT + +public: + JackPlugin(); + JackPlugin(const JackPlugin &) = delete; + virtual ~JackPlugin() = default; + + QString name() const override; + DeviceInfo::List getDeviceInfoList() const override; + DeviceInfo::Id defaultDeviceId(const Direction &mode) const override; + + Format deviceFormat(const DeviceInfo::Id &id, const Direction &mode) const override; + Stream *open(const DeviceInfo::Id &id, const Direction &mode, const Format &format, QIODevice *endpoint) override; + +private: + DeviceInfo::List m_list{}; + DeviceInfo m_defaultDeviceInfoInput; + DeviceInfo m_defaultDeviceInfoOutput; + std::map> m_clients{}; +}; + +class JackPluginException : public std::runtime_error +{ +public: + template + JackPluginException(const T& arg) + : std::runtime_error(arg) + {} +}; +} + +#endif // AUDIO_JACKPLUGIN_H