0.9.11-rc-2
Switch from JACK to ALSA audio.
This commit is contained in:
@@ -0,0 +1,512 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2022 Robin E. R. Davies
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
/* Status of Jack support.
|
||||
|
||||
PiPedal was originally written for Jack, but subsequently ported to Alsa because running
|
||||
Jack as a systemd daemon proved to be unsupportable,.
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
#include "pch.h"
|
||||
#include "JackDriver.hpp"
|
||||
|
||||
#include <jack/jack.h>
|
||||
#include <jack/types.h>
|
||||
#include <jack/session.h>
|
||||
#include <jack/midiport.h>
|
||||
#include "Lv2Log.hpp"
|
||||
|
||||
#if JACK_HOST
|
||||
|
||||
namespace pipedal {
|
||||
|
||||
|
||||
std::string JackStatusToString(jack_status_t status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
default:
|
||||
return "Unknown Jack Audio error.";
|
||||
case JackStatus::JackFailure:
|
||||
return "Jack Audio operation failed.";
|
||||
case JackStatus::JackInvalidOption:
|
||||
return "Invalid Jack Audio operation.";
|
||||
case JackStatus::JackNameNotUnique:
|
||||
return "Jack Audio name in use.";
|
||||
case JackStatus::JackServerStarted:
|
||||
return "Jack Audio Server started.";
|
||||
case JackStatus::JackServerFailed:
|
||||
return "Jack Audio Server failed.";
|
||||
case JackStatus::JackServerError:
|
||||
return "Can't connect to the Jack Audio Server.";
|
||||
case JackStatus::JackNoSuchClient:
|
||||
return "Jack Audio client not found.";
|
||||
case JackLoadFailure:
|
||||
return "Jack Audio client failed to load.";
|
||||
case JackStatus::JackInitFailure:
|
||||
return "Failed to initialize the Jack Audio client.";
|
||||
case JackStatus::JackShmFailure:
|
||||
return "Jack Audio failed to access shared memory.";
|
||||
case JackStatus::JackVersionError:
|
||||
return "Jack Audio Server version error.";
|
||||
case JackStatus::JackClientZombie:
|
||||
return "JackClientZombie error.";
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
This code relies on the fact that jack_midi_event_t and pipedal::MidiEvent are binary-compatible.
|
||||
(the default build does not require Jack Audio headers to be present).
|
||||
|
||||
|
||||
Test to make sure this is still true.
|
||||
*/
|
||||
|
||||
|
||||
static_assert(sizeof(jack_midi_event_t) == sizeof(pipedal::MidiEvent));
|
||||
|
||||
#define CHECK_FIELD(fieldName) \
|
||||
static_assert(offsetof(jack_midi_event_t,fieldName) == offsetof(pipedal::MidiEvent,fieldName)); \
|
||||
static_assert(sizeof(jack_midi_event_t::fieldName) == sizeof(pipedal::MidiEvent::fieldName));
|
||||
|
||||
CHECK_FIELD(size)
|
||||
CHECK_FIELD(buffer)
|
||||
CHECK_FIELD(time)
|
||||
|
||||
|
||||
// private import from JackConfiguration.cpp
|
||||
std::string GetJackErrorMessage(jack_status_t status);
|
||||
|
||||
|
||||
// forward decl
|
||||
static int process_fn(jack_nframes_t nframes, void *arg);
|
||||
|
||||
static void jack_error_fn(const char *msg)
|
||||
{
|
||||
Lv2Log::error("Jack - %s", msg);
|
||||
}
|
||||
static void jack_info_fn(const char *msg)
|
||||
{
|
||||
Lv2Log::info("Jack - %s", msg);
|
||||
}
|
||||
|
||||
|
||||
class JackDriverImpl : public AudioDriver {
|
||||
private:
|
||||
jack_client_t *client = nullptr;
|
||||
std::vector<jack_port_t *> inputPorts;
|
||||
std::vector<jack_port_t *> outputPorts;
|
||||
std::vector<jack_port_t *> midiInputPorts;
|
||||
uint32_t sampleRate = 0;
|
||||
|
||||
AudioDriverHost *driverHost = nullptr;
|
||||
|
||||
public:
|
||||
JackDriverImpl(AudioDriverHost*driverHost)
|
||||
: driverHost(driverHost)
|
||||
{
|
||||
jack_set_error_function(jack_error_fn);
|
||||
jack_set_info_function(jack_info_fn);
|
||||
|
||||
}
|
||||
virtual ~JackDriverImpl()
|
||||
{
|
||||
jack_set_error_function(nullptr);
|
||||
jack_set_info_function(nullptr);
|
||||
|
||||
}
|
||||
private:
|
||||
|
||||
void OnShutdown()
|
||||
{
|
||||
Lv2Log::info("Jack Audio Server has shut down.");
|
||||
}
|
||||
|
||||
static void
|
||||
jack_shutdown_fn(void *arg)
|
||||
{
|
||||
((JackDriverImpl *)arg)->OnShutdown();
|
||||
}
|
||||
|
||||
|
||||
static int jackInstanceId;
|
||||
|
||||
static int xrun_callback_fn(void *arg)
|
||||
{
|
||||
((AudioDriverHost *)arg)->OnUnderrun();
|
||||
return 0;
|
||||
}
|
||||
|
||||
virtual uint32_t GetSampleRate() {
|
||||
return this->sampleRate;
|
||||
}
|
||||
|
||||
bool open = false;
|
||||
virtual void Open(const JackServerSettings&jackServerSettings,const JackChannelSelection &channelSelection) {
|
||||
if (open) throw PiPedalStateException("Already open");
|
||||
open = true;
|
||||
|
||||
this->channelSelection = channelSelection;
|
||||
jack_status_t status;
|
||||
// need a unique instance name every timme.
|
||||
std::stringstream s;
|
||||
s << "PiPedal";
|
||||
if (jackInstanceId != 0) {
|
||||
s << jackInstanceId;
|
||||
}
|
||||
++jackInstanceId;
|
||||
|
||||
std::string instanceName = s.str();
|
||||
|
||||
client = jack_client_open(instanceName.c_str(), JackNullOption, &status);
|
||||
|
||||
if (client == nullptr || status & JackFailure)
|
||||
{
|
||||
if (client)
|
||||
{
|
||||
jack_client_close(client);
|
||||
client = nullptr;
|
||||
}
|
||||
std::string error = GetJackErrorMessage(status);
|
||||
Lv2Log::error(error);
|
||||
throw PiPedalStateException(error.c_str());
|
||||
}
|
||||
|
||||
if (status & JackServerStarted)
|
||||
{
|
||||
Lv2Log::info("Jack server started.");
|
||||
}
|
||||
|
||||
jack_set_process_callback(client, process_fn, this->driverHost);
|
||||
|
||||
jack_on_shutdown(client, jack_shutdown_fn, this);
|
||||
|
||||
jack_set_xrun_callback(client, xrun_callback_fn, this->driverHost);
|
||||
|
||||
this->sampleRate = jack_get_sample_rate(client);
|
||||
|
||||
|
||||
auto &selectedInputPorts = channelSelection.GetInputAudioPorts();
|
||||
this->inputPorts.clear();
|
||||
this->outputPorts.clear();
|
||||
this->midiInputPorts.clear();
|
||||
|
||||
this->inputPorts.resize(selectedInputPorts.size());
|
||||
for (int i = 0; i < selectedInputPorts.size(); ++i)
|
||||
{
|
||||
std::stringstream name;
|
||||
name << "input" << (i + 1);
|
||||
this->inputPorts[i] =
|
||||
jack_port_register(
|
||||
client, name.str().c_str(),
|
||||
JACK_DEFAULT_AUDIO_TYPE,
|
||||
JackPortIsInput, 0);
|
||||
if (this->inputPorts[i] == nullptr)
|
||||
{
|
||||
Lv2Log::error("Can't allocate Jack Audio input ports.");
|
||||
throw PiPedalStateException("Failed to allocate Jack Audio port.");
|
||||
}
|
||||
}
|
||||
auto &selectedOutputPorts = channelSelection.GetOutputAudioPorts();
|
||||
this->outputPorts.resize(selectedOutputPorts.size());
|
||||
for (int i = 0; i < selectedOutputPorts.size(); ++i)
|
||||
{
|
||||
std::stringstream name;
|
||||
name << "output" << (i + 1);
|
||||
this->outputPorts[i] =
|
||||
jack_port_register(client, name.str().c_str(),
|
||||
JACK_DEFAULT_AUDIO_TYPE,
|
||||
JackPortIsOutput, 0);
|
||||
if (this->outputPorts[i] == nullptr)
|
||||
{
|
||||
Lv2Log::error("Can't allocate Jack Audio output port.");
|
||||
throw PiPedalStateException("Failed to allocate Jack Audio port.");
|
||||
}
|
||||
}
|
||||
auto &selectedMidiPorts = channelSelection.GetInputMidiDevices();
|
||||
|
||||
this->midiInputPorts.resize(selectedMidiPorts.size());
|
||||
for (int i = 0; i < selectedMidiPorts.size(); ++i)
|
||||
{
|
||||
std::stringstream name;
|
||||
name << "midiIn" << (i + 1);
|
||||
this->midiInputPorts[i] =
|
||||
jack_port_register(client, name.str().c_str(),
|
||||
JACK_DEFAULT_MIDI_TYPE,
|
||||
JackPortIsInput, 0);
|
||||
if (this->midiInputPorts[i] == nullptr)
|
||||
{
|
||||
std::stringstream s;
|
||||
s << "can't register Jack port " << name.str().c_str();
|
||||
Lv2Log::error(s.str());
|
||||
throw PiPedalStateException(s.str().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool jackActive = false;
|
||||
|
||||
JackChannelSelection channelSelection;
|
||||
virtual void Activate()
|
||||
{
|
||||
int activateResult = jack_activate(client);
|
||||
if (activateResult == 0)
|
||||
{
|
||||
jackActive = true;
|
||||
|
||||
auto &selectedInputPorts = channelSelection.GetInputAudioPorts();
|
||||
|
||||
for (int i = 0; i < selectedInputPorts.size(); ++i)
|
||||
{
|
||||
auto result = jack_connect(client, selectedInputPorts[i].c_str(), jack_port_name(this->inputPorts[i]));
|
||||
if (result)
|
||||
{
|
||||
Lv2Log::error("Can't connect input port %s", selectedInputPorts[i].c_str());
|
||||
throw PiPedalStateException("Jack Audio port connection failed.");
|
||||
}
|
||||
}
|
||||
|
||||
auto &selectedOutputPorts = channelSelection.GetOutputAudioPorts();
|
||||
|
||||
for (int i = 0; i < selectedOutputPorts.size(); ++i)
|
||||
{
|
||||
auto result = jack_connect(client, jack_port_name(this->outputPorts[i]), selectedOutputPorts[i].c_str());
|
||||
if (result)
|
||||
{
|
||||
Lv2Log::error("Can't connect output port %s", selectedOutputPorts[i].c_str());
|
||||
throw PiPedalStateException("Jack Audio port connection failed.");
|
||||
}
|
||||
}
|
||||
|
||||
auto &selectedMidiPorts = channelSelection.GetInputMidiDevices();
|
||||
|
||||
for (int i = 0; i < selectedMidiPorts.size(); ++i)
|
||||
{
|
||||
auto result = jack_connect(client, selectedMidiPorts[i].name_.c_str(), jack_port_name(this->midiInputPorts[i]));
|
||||
if (result)
|
||||
{
|
||||
Lv2Log::error("Can't connect midi input port %s", selectedMidiPorts[i].name_.c_str());
|
||||
throw PiPedalStateException("Jack Audio midi port connection failed.");
|
||||
}
|
||||
}
|
||||
|
||||
Lv2Log::info("Jack configuration complete.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Lv2Log::error("Failed to activate Jack Audio client. (%d)", (int)activateResult);
|
||||
throw PiPedalStateException("Failed to activate Jack Audio client.");
|
||||
}
|
||||
}
|
||||
|
||||
void FillMidiBuffers()
|
||||
{
|
||||
// do nothing. Jack does this for us.
|
||||
}
|
||||
virtual void Deactivate()
|
||||
{
|
||||
if (!jackActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
jackActive = false;
|
||||
// disconnect ports.
|
||||
for (size_t i = 0; i < this->inputPorts.size(); ++i)
|
||||
{
|
||||
auto port = inputPorts[i];
|
||||
if (port != nullptr)
|
||||
{
|
||||
jack_disconnect(client, channelSelection.GetInputAudioPorts()[i].c_str(), jack_port_name(port));
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < outputPorts.size(); ++i)
|
||||
{
|
||||
auto port = outputPorts[i];
|
||||
if (port != nullptr)
|
||||
{
|
||||
jack_disconnect(client, jack_port_name(port),channelSelection.GetOutputAudioPorts()[i].c_str());
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < midiInputPorts.size(); ++i)
|
||||
{
|
||||
auto port = midiInputPorts[i];
|
||||
if (port != nullptr)
|
||||
{
|
||||
jack_disconnect(client, channelSelection.GetInputMidiDevices()[i].name_.c_str(), jack_port_name(port));
|
||||
}
|
||||
}
|
||||
|
||||
if (jackActive)
|
||||
{
|
||||
jack_deactivate(client);
|
||||
jackActive = false;
|
||||
}
|
||||
|
||||
jack_set_process_callback(client, nullptr, nullptr);
|
||||
jack_on_shutdown(client, nullptr, nullptr);
|
||||
#if JACK_SESSION_CALLBACK // (deprecated, and not actually useful)
|
||||
jack_set_session_callback(client, nullptr, nullptr);
|
||||
#endif
|
||||
jack_set_xrun_callback(client, nullptr, nullptr);
|
||||
|
||||
}
|
||||
|
||||
virtual size_t MidiInputBufferCount() const { return this->midiInputPorts.size(); }
|
||||
virtual void*GetMidiInputBuffer(size_t channel, size_t nFrames) {
|
||||
return (void*)jack_port_get_buffer(this->midiInputPorts[channel], nFrames);
|
||||
}
|
||||
|
||||
virtual size_t GetMidiInputEventCount(void *portBuffer)
|
||||
{
|
||||
return jack_midi_get_event_count(portBuffer);
|
||||
}
|
||||
virtual bool GetMidiInputEvent(MidiEvent*event, void*portBuf, size_t nFrame)
|
||||
{
|
||||
return jack_midi_event_get((jack_midi_event_t*)event,portBuf,(uint32_t)nFrame);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
virtual size_t InputBufferCount() const { return inputPorts.size(); }
|
||||
virtual float*GetInputBuffer(size_t channel, size_t nFrames) {
|
||||
return (float*)jack_port_get_buffer(this->inputPorts[channel], nFrames);
|
||||
}
|
||||
|
||||
virtual size_t OutputBufferCount() const { return outputPorts.size(); }
|
||||
virtual float*GetOutputBuffer(size_t channel, size_t nFrames)
|
||||
{
|
||||
return (float*)jack_port_get_buffer(this->outputPorts[channel], nFrames);
|
||||
}
|
||||
|
||||
virtual void Close()
|
||||
{
|
||||
if (!open) return;
|
||||
open = false;
|
||||
|
||||
// unregister ports.
|
||||
for (size_t i = 0; i < this->inputPorts.size(); ++i)
|
||||
{
|
||||
auto port = inputPorts[i];
|
||||
if (port)
|
||||
{
|
||||
jack_port_unregister(client, port);
|
||||
}
|
||||
}
|
||||
inputPorts.clear();
|
||||
|
||||
for (size_t i = 0; i < this->outputPorts.size(); ++i)
|
||||
{
|
||||
auto port = outputPorts[i];
|
||||
if (port)
|
||||
{
|
||||
jack_port_unregister(client, port);
|
||||
}
|
||||
}
|
||||
outputPorts.clear();
|
||||
for (size_t i = 0; i < this->midiInputPorts.size(); ++i)
|
||||
{
|
||||
auto port = midiInputPorts[i];
|
||||
if (port)
|
||||
{
|
||||
jack_port_unregister(client, port);
|
||||
}
|
||||
}
|
||||
midiInputPorts.resize(0);
|
||||
if (client)
|
||||
{
|
||||
jack_client_close(client);
|
||||
client = nullptr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
virtual float CpuOverhead() { return 0; }
|
||||
virtual float CpuUse()
|
||||
{
|
||||
|
||||
if (client)
|
||||
{
|
||||
return jack_cpu_load(this->client);
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int process_fn(jack_nframes_t nframes, void *arg)
|
||||
{
|
||||
((AudioDriverHost *)arg)->OnProcess(nframes);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void OnSessionCallback(jack_session_event_t *event)
|
||||
{
|
||||
#if JACK_SESSION_CALLBACK // deprecated and not actually useful.
|
||||
char retval[100];
|
||||
Lv2Log::info("path %s, uuid %s, type: %s\n", event->session_dir, event->client_uuid, event->type == JackSessionSave ? "save" : "quit");
|
||||
|
||||
snprintf(retval, 100, "jack_simple_session_client %s", event->client_uuid);
|
||||
event->command_line = strdup(retval);
|
||||
|
||||
jack_session_reply(client, event);
|
||||
|
||||
if (event->type == JackSessionSaveAndQuit)
|
||||
{
|
||||
// simple_quit = 1;
|
||||
}
|
||||
|
||||
jack_session_event_free(event);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
session_callback_fn(jack_session_event_t *event, void *arg)
|
||||
{
|
||||
((JackDriverImpl *)arg)->OnSessionCallback(event);
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
|
||||
AudioDriver* CreateJackDriver(AudioDriverHost*driverHost)
|
||||
{
|
||||
return new JackDriverImpl(driverHost);
|
||||
}
|
||||
|
||||
|
||||
int JackDriverImpl::jackInstanceId = 0;
|
||||
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user