/**
|
|
* OpenAL cross platform audio library
|
|
* Copyright (C) 1999-2007 by authors.
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Library General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This library 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
|
|
* Library General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Library General Public
|
|
* License along with this library; if not, write to the
|
|
* Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
* Or go to http://www.gnu.org/copyleft/lgpl.html
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include "backends/jack.h"
|
|
|
|
#include <cstdlib>
|
|
#include <cstdio>
|
|
#include <memory.h>
|
|
|
|
#include <thread>
|
|
#include <functional>
|
|
|
|
#include "alMain.h"
|
|
#include "alu.h"
|
|
#include "alconfig.h"
|
|
#include "ringbuffer.h"
|
|
#include "threads.h"
|
|
#include "compat.h"
|
|
|
|
#include <jack/jack.h>
|
|
#include <jack/ringbuffer.h>
|
|
|
|
|
|
namespace {
|
|
|
|
constexpr ALCchar jackDevice[] = "JACK Default";
|
|
|
|
|
|
#ifdef HAVE_DYNLOAD
|
|
#define JACK_FUNCS(MAGIC) \
|
|
MAGIC(jack_client_open); \
|
|
MAGIC(jack_client_close); \
|
|
MAGIC(jack_client_name_size); \
|
|
MAGIC(jack_get_client_name); \
|
|
MAGIC(jack_connect); \
|
|
MAGIC(jack_activate); \
|
|
MAGIC(jack_deactivate); \
|
|
MAGIC(jack_port_register); \
|
|
MAGIC(jack_port_unregister); \
|
|
MAGIC(jack_port_get_buffer); \
|
|
MAGIC(jack_port_name); \
|
|
MAGIC(jack_get_ports); \
|
|
MAGIC(jack_free); \
|
|
MAGIC(jack_get_sample_rate); \
|
|
MAGIC(jack_set_error_function); \
|
|
MAGIC(jack_set_process_callback); \
|
|
MAGIC(jack_set_buffer_size_callback); \
|
|
MAGIC(jack_set_buffer_size); \
|
|
MAGIC(jack_get_buffer_size);
|
|
|
|
void *jack_handle;
|
|
#define MAKE_FUNC(f) decltype(f) * p##f
|
|
JACK_FUNCS(MAKE_FUNC);
|
|
decltype(jack_error_callback) * pjack_error_callback;
|
|
#undef MAKE_FUNC
|
|
|
|
#ifndef IN_IDE_PARSER
|
|
#define jack_client_open pjack_client_open
|
|
#define jack_client_close pjack_client_close
|
|
#define jack_client_name_size pjack_client_name_size
|
|
#define jack_get_client_name pjack_get_client_name
|
|
#define jack_connect pjack_connect
|
|
#define jack_activate pjack_activate
|
|
#define jack_deactivate pjack_deactivate
|
|
#define jack_port_register pjack_port_register
|
|
#define jack_port_unregister pjack_port_unregister
|
|
#define jack_port_get_buffer pjack_port_get_buffer
|
|
#define jack_port_name pjack_port_name
|
|
#define jack_get_ports pjack_get_ports
|
|
#define jack_free pjack_free
|
|
#define jack_get_sample_rate pjack_get_sample_rate
|
|
#define jack_set_error_function pjack_set_error_function
|
|
#define jack_set_process_callback pjack_set_process_callback
|
|
#define jack_set_buffer_size_callback pjack_set_buffer_size_callback
|
|
#define jack_set_buffer_size pjack_set_buffer_size
|
|
#define jack_get_buffer_size pjack_get_buffer_size
|
|
#define jack_error_callback (*pjack_error_callback)
|
|
#endif
|
|
#endif
|
|
|
|
|
|
jack_options_t ClientOptions = JackNullOption;
|
|
|
|
ALCboolean jack_load()
|
|
{
|
|
ALCboolean error = ALC_FALSE;
|
|
|
|
#ifdef HAVE_DYNLOAD
|
|
if(!jack_handle)
|
|
{
|
|
std::string missing_funcs;
|
|
|
|
#ifdef _WIN32
|
|
#define JACKLIB "libjack.dll"
|
|
#else
|
|
#define JACKLIB "libjack.so.0"
|
|
#endif
|
|
jack_handle = LoadLib(JACKLIB);
|
|
if(!jack_handle)
|
|
{
|
|
WARN("Failed to load %s\n", JACKLIB);
|
|
return ALC_FALSE;
|
|
}
|
|
|
|
error = ALC_FALSE;
|
|
#define LOAD_FUNC(f) do { \
|
|
p##f = reinterpret_cast<decltype(p##f)>(GetSymbol(jack_handle, #f)); \
|
|
if(p##f == nullptr) { \
|
|
error = ALC_TRUE; \
|
|
missing_funcs += "\n" #f; \
|
|
} \
|
|
} while(0)
|
|
JACK_FUNCS(LOAD_FUNC);
|
|
#undef LOAD_FUNC
|
|
/* Optional symbols. These don't exist in all versions of JACK. */
|
|
#define LOAD_SYM(f) p##f = reinterpret_cast<decltype(p##f)>(GetSymbol(jack_handle, #f))
|
|
LOAD_SYM(jack_error_callback);
|
|
#undef LOAD_SYM
|
|
|
|
if(error)
|
|
{
|
|
WARN("Missing expected functions:%s\n", missing_funcs.c_str());
|
|
CloseLib(jack_handle);
|
|
jack_handle = nullptr;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return !error;
|
|
}
|
|
|
|
|
|
struct JackPlayback final : public BackendBase {
|
|
JackPlayback(ALCdevice *device) noexcept : BackendBase{device} { }
|
|
~JackPlayback() override;
|
|
|
|
static int bufferSizeNotifyC(jack_nframes_t numframes, void *arg);
|
|
int bufferSizeNotify(jack_nframes_t numframes);
|
|
|
|
static int processC(jack_nframes_t numframes, void *arg);
|
|
int process(jack_nframes_t numframes);
|
|
|
|
int mixerProc();
|
|
|
|
ALCenum open(const ALCchar *name) override;
|
|
ALCboolean reset() override;
|
|
ALCboolean start() override;
|
|
void stop() override;
|
|
ClockLatency getClockLatency() override;
|
|
|
|
jack_client_t *mClient{nullptr};
|
|
jack_port_t *mPort[MAX_OUTPUT_CHANNELS]{};
|
|
|
|
RingBufferPtr mRing;
|
|
al::semaphore mSem;
|
|
|
|
std::atomic<bool> mKillNow{true};
|
|
std::thread mThread;
|
|
|
|
static constexpr inline const char *CurrentPrefix() noexcept { return "JackPlayback::"; }
|
|
DEF_NEWDEL(JackPlayback)
|
|
};
|
|
|
|
JackPlayback::~JackPlayback()
|
|
{
|
|
if(!mClient)
|
|
return;
|
|
|
|
std::for_each(std::begin(mPort), std::end(mPort),
|
|
[this](jack_port_t *port) -> void
|
|
{ if(port) jack_port_unregister(mClient, port); }
|
|
);
|
|
std::fill(std::begin(mPort), std::end(mPort), nullptr);
|
|
jack_client_close(mClient);
|
|
mClient = nullptr;
|
|
}
|
|
|
|
|
|
int JackPlayback::bufferSizeNotifyC(jack_nframes_t numframes, void *arg)
|
|
{ return static_cast<JackPlayback*>(arg)->bufferSizeNotify(numframes); }
|
|
|
|
int JackPlayback::bufferSizeNotify(jack_nframes_t numframes)
|
|
{
|
|
std::lock_guard<std::mutex> _{mDevice->StateLock};
|
|
mDevice->UpdateSize = numframes;
|
|
mDevice->BufferSize = numframes*2;
|
|
|
|
ALuint bufsize{mDevice->UpdateSize};
|
|
if(ConfigValueUInt(mDevice->DeviceName.c_str(), "jack", "buffer-size", &bufsize))
|
|
bufsize = maxu(NextPowerOf2(bufsize), mDevice->UpdateSize);
|
|
mDevice->BufferSize = bufsize + mDevice->UpdateSize;
|
|
|
|
TRACE("%u / %u buffer\n", mDevice->UpdateSize, mDevice->BufferSize);
|
|
|
|
mRing = nullptr;
|
|
mRing = CreateRingBuffer(bufsize, mDevice->frameSizeFromFmt(), true);
|
|
if(!mRing)
|
|
{
|
|
ERR("Failed to reallocate ringbuffer\n");
|
|
aluHandleDisconnect(mDevice, "Failed to reallocate %u-sample buffer", bufsize);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
int JackPlayback::processC(jack_nframes_t numframes, void *arg)
|
|
{ return static_cast<JackPlayback*>(arg)->process(numframes); }
|
|
|
|
int JackPlayback::process(jack_nframes_t numframes)
|
|
{
|
|
jack_default_audio_sample_t *out[MAX_OUTPUT_CHANNELS];
|
|
ALsizei numchans{0};
|
|
for(auto port : mPort)
|
|
{
|
|
if(!port) break;
|
|
out[numchans++] = static_cast<float*>(jack_port_get_buffer(port, numframes));
|
|
}
|
|
|
|
auto data = mRing->getReadVector();
|
|
jack_nframes_t todo{minu(numframes, data.first.len)};
|
|
std::transform(out, out+numchans, out,
|
|
[&data,numchans,todo](ALfloat *outbuf) -> ALfloat*
|
|
{
|
|
const ALfloat *RESTRICT in = reinterpret_cast<ALfloat*>(data.first.buf);
|
|
std::generate_n(outbuf, todo,
|
|
[&in,numchans]() noexcept -> ALfloat
|
|
{
|
|
ALfloat ret{*in};
|
|
in += numchans;
|
|
return ret;
|
|
}
|
|
);
|
|
data.first.buf += sizeof(ALfloat);
|
|
return outbuf + todo;
|
|
}
|
|
);
|
|
jack_nframes_t total{todo};
|
|
|
|
todo = minu(numframes-total, data.second.len);
|
|
if(todo > 0)
|
|
{
|
|
std::transform(out, out+numchans, out,
|
|
[&data,numchans,todo](ALfloat *outbuf) -> ALfloat*
|
|
{
|
|
const ALfloat *RESTRICT in = reinterpret_cast<ALfloat*>(data.second.buf);
|
|
std::generate_n(outbuf, todo,
|
|
[&in,numchans]() noexcept -> ALfloat
|
|
{
|
|
ALfloat ret{*in};
|
|
in += numchans;
|
|
return ret;
|
|
}
|
|
);
|
|
data.second.buf += sizeof(ALfloat);
|
|
return outbuf + todo;
|
|
}
|
|
);
|
|
total += todo;
|
|
}
|
|
|
|
mRing->readAdvance(total);
|
|
mSem.post();
|
|
|
|
if(numframes > total)
|
|
{
|
|
todo = numframes-total;
|
|
std::transform(out, out+numchans, out,
|
|
[todo](ALfloat *outbuf) -> ALfloat*
|
|
{
|
|
std::fill_n(outbuf, todo, 0.0f);
|
|
return outbuf + todo;
|
|
}
|
|
);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int JackPlayback::mixerProc()
|
|
{
|
|
SetRTPriority();
|
|
althrd_setname(MIXER_THREAD_NAME);
|
|
|
|
lock();
|
|
while(!mKillNow.load(std::memory_order_acquire) &&
|
|
mDevice->Connected.load(std::memory_order_acquire))
|
|
{
|
|
if(mRing->writeSpace() < mDevice->UpdateSize)
|
|
{
|
|
unlock();
|
|
mSem.wait();
|
|
lock();
|
|
continue;
|
|
}
|
|
|
|
auto data = mRing->getWriteVector();
|
|
auto todo = static_cast<ALuint>(data.first.len + data.second.len);
|
|
todo -= todo%mDevice->UpdateSize;
|
|
|
|
ALuint len1{minu(data.first.len, todo)};
|
|
ALuint len2{minu(data.second.len, todo-len1)};
|
|
|
|
aluMixData(mDevice, data.first.buf, len1);
|
|
if(len2 > 0)
|
|
aluMixData(mDevice, data.second.buf, len2);
|
|
mRing->writeAdvance(todo);
|
|
}
|
|
unlock();
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
ALCenum JackPlayback::open(const ALCchar *name)
|
|
{
|
|
if(!name)
|
|
name = jackDevice;
|
|
else if(strcmp(name, jackDevice) != 0)
|
|
return ALC_INVALID_VALUE;
|
|
|
|
const char *client_name{"alsoft"};
|
|
jack_status_t status;
|
|
mClient = jack_client_open(client_name, ClientOptions, &status, nullptr);
|
|
if(mClient == nullptr)
|
|
{
|
|
ERR("jack_client_open() failed, status = 0x%02x\n", status);
|
|
return ALC_INVALID_VALUE;
|
|
}
|
|
if((status&JackServerStarted))
|
|
TRACE("JACK server started\n");
|
|
if((status&JackNameNotUnique))
|
|
{
|
|
client_name = jack_get_client_name(mClient);
|
|
TRACE("Client name not unique, got `%s' instead\n", client_name);
|
|
}
|
|
|
|
jack_set_process_callback(mClient, &JackPlayback::processC, this);
|
|
jack_set_buffer_size_callback(mClient, &JackPlayback::bufferSizeNotifyC, this);
|
|
|
|
mDevice->DeviceName = name;
|
|
return ALC_NO_ERROR;
|
|
}
|
|
|
|
ALCboolean JackPlayback::reset()
|
|
{
|
|
std::for_each(std::begin(mPort), std::end(mPort),
|
|
[this](jack_port_t *port) -> void
|
|
{ if(port) jack_port_unregister(mClient, port); }
|
|
);
|
|
std::fill(std::begin(mPort), std::end(mPort), nullptr);
|
|
|
|
/* Ignore the requested buffer metrics and just keep one JACK-sized buffer
|
|
* ready for when requested.
|
|
*/
|
|
mDevice->Frequency = jack_get_sample_rate(mClient);
|
|
mDevice->UpdateSize = jack_get_buffer_size(mClient);
|
|
mDevice->BufferSize = mDevice->UpdateSize * 2;
|
|
|
|
ALuint bufsize{mDevice->UpdateSize};
|
|
if(ConfigValueUInt(mDevice->DeviceName.c_str(), "jack", "buffer-size", &bufsize))
|
|
bufsize = maxu(NextPowerOf2(bufsize), mDevice->UpdateSize);
|
|
mDevice->BufferSize = bufsize + mDevice->UpdateSize;
|
|
|
|
/* Force 32-bit float output. */
|
|
mDevice->FmtType = DevFmtFloat;
|
|
|
|
ALsizei numchans{mDevice->channelsFromFmt()};
|
|
auto ports_end = std::begin(mPort) + numchans;
|
|
auto bad_port = std::find_if_not(std::begin(mPort), ports_end,
|
|
[this](jack_port_t *&port) -> bool
|
|
{
|
|
std::string name{"channel_" + std::to_string(&port - mPort + 1)};
|
|
port = jack_port_register(mClient, name.c_str(), JACK_DEFAULT_AUDIO_TYPE,
|
|
JackPortIsOutput, 0);
|
|
return port != nullptr;
|
|
}
|
|
);
|
|
if(bad_port != ports_end)
|
|
{
|
|
ERR("Not enough JACK ports available for %s output\n", DevFmtChannelsString(mDevice->FmtChans));
|
|
if(bad_port == std::begin(mPort)) return ALC_FALSE;
|
|
|
|
if(bad_port == std::begin(mPort)+1)
|
|
mDevice->FmtChans = DevFmtMono;
|
|
else
|
|
{
|
|
ports_end = mPort+2;
|
|
while(bad_port != ports_end)
|
|
{
|
|
jack_port_unregister(mClient, *(--bad_port));
|
|
*bad_port = nullptr;
|
|
}
|
|
mDevice->FmtChans = DevFmtStereo;
|
|
}
|
|
numchans = std::distance(std::begin(mPort), bad_port);
|
|
}
|
|
|
|
mRing = nullptr;
|
|
mRing = CreateRingBuffer(bufsize, mDevice->frameSizeFromFmt(), true);
|
|
if(!mRing)
|
|
{
|
|
ERR("Failed to allocate ringbuffer\n");
|
|
return ALC_FALSE;
|
|
}
|
|
|
|
SetDefaultChannelOrder(mDevice);
|
|
|
|
return ALC_TRUE;
|
|
}
|
|
|
|
ALCboolean JackPlayback::start()
|
|
{
|
|
if(jack_activate(mClient))
|
|
{
|
|
ERR("Failed to activate client\n");
|
|
return ALC_FALSE;
|
|
}
|
|
|
|
const char **ports{jack_get_ports(mClient, nullptr, nullptr,
|
|
JackPortIsPhysical|JackPortIsInput)};
|
|
if(ports == nullptr)
|
|
{
|
|
ERR("No physical playback ports found\n");
|
|
jack_deactivate(mClient);
|
|
return ALC_FALSE;
|
|
}
|
|
std::mismatch(std::begin(mPort), std::end(mPort), ports,
|
|
[this](const jack_port_t *port, const char *pname) -> bool
|
|
{
|
|
if(!port) return false;
|
|
if(!pname)
|
|
{
|
|
ERR("No physical playback port for \"%s\"\n", jack_port_name(port));
|
|
return false;
|
|
}
|
|
if(jack_connect(mClient, jack_port_name(port), pname))
|
|
ERR("Failed to connect output port \"%s\" to \"%s\"\n", jack_port_name(port),
|
|
pname);
|
|
return true;
|
|
}
|
|
);
|
|
jack_free(ports);
|
|
|
|
try {
|
|
mKillNow.store(false, std::memory_order_release);
|
|
mThread = std::thread{std::mem_fn(&JackPlayback::mixerProc), this};
|
|
return ALC_TRUE;
|
|
}
|
|
catch(std::exception& e) {
|
|
ERR("Could not create playback thread: %s\n", e.what());
|
|
}
|
|
catch(...) {
|
|
}
|
|
jack_deactivate(mClient);
|
|
return ALC_FALSE;
|
|
}
|
|
|
|
void JackPlayback::stop()
|
|
{
|
|
if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
|
|
return;
|
|
|
|
mSem.post();
|
|
mThread.join();
|
|
|
|
jack_deactivate(mClient);
|
|
}
|
|
|
|
|
|
ClockLatency JackPlayback::getClockLatency()
|
|
{
|
|
ClockLatency ret;
|
|
|
|
lock();
|
|
ret.ClockTime = GetDeviceClockTime(mDevice);
|
|
ret.Latency = std::chrono::seconds{mRing->readSpace()};
|
|
ret.Latency /= mDevice->Frequency;
|
|
unlock();
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
void jack_msg_handler(const char *message)
|
|
{
|
|
WARN("%s\n", message);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool JackBackendFactory::init()
|
|
{
|
|
if(!jack_load())
|
|
return false;
|
|
|
|
if(!GetConfigValueBool(nullptr, "jack", "spawn-server", 0))
|
|
ClientOptions = static_cast<jack_options_t>(ClientOptions | JackNoStartServer);
|
|
|
|
void (*old_error_cb)(const char*){&jack_error_callback ? jack_error_callback : nullptr};
|
|
jack_set_error_function(jack_msg_handler);
|
|
jack_status_t status;
|
|
jack_client_t *client{jack_client_open("alsoft", ClientOptions, &status, nullptr)};
|
|
jack_set_error_function(old_error_cb);
|
|
if(!client)
|
|
{
|
|
WARN("jack_client_open() failed, 0x%02x\n", status);
|
|
if((status&JackServerFailed) && !(ClientOptions&JackNoStartServer))
|
|
ERR("Unable to connect to JACK server\n");
|
|
return false;
|
|
}
|
|
|
|
jack_client_close(client);
|
|
return true;
|
|
}
|
|
|
|
bool JackBackendFactory::querySupport(BackendType type)
|
|
{ return (type == BackendType::Playback); }
|
|
|
|
void JackBackendFactory::probe(DevProbe type, std::string *outnames)
|
|
{
|
|
switch(type)
|
|
{
|
|
case DevProbe::Playback:
|
|
/* Includes null char. */
|
|
outnames->append(jackDevice, sizeof(jackDevice));
|
|
break;
|
|
|
|
case DevProbe::Capture:
|
|
break;
|
|
}
|
|
}
|
|
|
|
BackendPtr JackBackendFactory::createBackend(ALCdevice *device, BackendType type)
|
|
{
|
|
if(type == BackendType::Playback)
|
|
return BackendPtr{new JackPlayback{device}};
|
|
return nullptr;
|
|
}
|
|
|
|
BackendFactory &JackBackendFactory::getFactory()
|
|
{
|
|
static JackBackendFactory factory{};
|
|
return factory;
|
|
}
|