diff options
Diffstat (limited to 'src/server/JackDriver.cpp')
-rw-r--r-- | src/server/JackDriver.cpp | 584 |
1 files changed, 584 insertions, 0 deletions
diff --git a/src/server/JackDriver.cpp b/src/server/JackDriver.cpp new file mode 100644 index 00000000..a331811e --- /dev/null +++ b/src/server/JackDriver.cpp @@ -0,0 +1,584 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen_config.h" + +#include <cstdlib> +#include <string> + +#include <jack/midiport.h> +#ifdef INGEN_JACK_SESSION +#include <jack/session.h> +#include <boost/format.hpp> +#include "ingen/Serialiser.hpp" +#endif +#ifdef HAVE_JACK_METADATA +#include <jack/metadata.h> +#include "jackey.h" +#endif + +#include "ingen/Configuration.hpp" +#include "ingen/LV2Features.hpp" +#include "ingen/Log.hpp" +#include "ingen/URI.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/World.hpp" +#include "lv2/atom/util.h" + +#include "Buffer.hpp" +#include "DuplexPort.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "JackDriver.hpp" +#include "PortImpl.hpp" +#include "ThreadManager.hpp" +#include "util.hpp" + +typedef jack_default_audio_sample_t jack_sample_t; + +namespace ingen { +namespace server { + +JackDriver::JackDriver(Engine& engine) + : _engine(engine) + , _sem(0) + , _flag(false) + , _client(nullptr) + , _block_length(0) + , _seq_size(0) + , _sample_rate(0) + , _is_activated(false) + , _old_bpm(120.0f) + , _old_frame(0) + , _old_rolling(false) +{ + _midi_event_type = _engine.world()->uris().midi_MidiEvent; + lv2_atom_forge_init( + &_forge, &engine.world()->uri_map().urid_map_feature()->urid_map); +} + +JackDriver::~JackDriver() +{ + deactivate(); + _ports.clear_and_dispose([](EnginePort* p) { delete p; }); +} + +bool +JackDriver::attach(const std::string& server_name, + const std::string& client_name, + void* jack_client) +{ + assert(!_client); + if (!jack_client) { +#ifdef INGEN_JACK_SESSION + const std::string uuid = _engine.world()->jack_uuid(); + if (!uuid.empty()) { + _client = jack_client_open(client_name.c_str(), + JackSessionID, nullptr, + uuid.c_str()); + _engine.log().info(fmt("Connected to Jack as `%1%' (UUID `%2%')\n") + % client_name.c_str() % uuid); + } +#endif + + // Try supplied server name + if (!_client && !server_name.empty()) { + if ((_client = jack_client_open(client_name.c_str(), + JackServerName, nullptr, + server_name.c_str()))) { + _engine.log().info(fmt("Connected to Jack server `%1%'\n") + % server_name); + } + } + + // Either server name not specified, or supplied server name does not exist + // Connect to default server + if (!_client) { + if ((_client = jack_client_open(client_name.c_str(), JackNullOption, nullptr))) { + _engine.log().info("Connected to default Jack server\n"); + } + } + + // Still failed + if (!_client) { + _engine.log().error("Unable to connect to Jack\n"); + return false; + } + } else { + _client = (jack_client_t*)jack_client; + } + + _sample_rate = jack_get_sample_rate(_client); + _block_length = jack_get_buffer_size(_client); + _seq_size = jack_port_type_get_buffer_size(_client, JACK_DEFAULT_MIDI_TYPE); + + _fallback_buffer = AudioBufPtr( + static_cast<float*>( + Buffer::aligned_alloc(sizeof(float) * _block_length))); + + jack_on_shutdown(_client, shutdown_cb, this); + + jack_set_thread_init_callback(_client, thread_init_cb, this); + jack_set_buffer_size_callback(_client, block_length_cb, this); +#ifdef INGEN_JACK_SESSION + jack_set_session_callback(_client, session_cb, this); +#endif + + for (auto& p : _ports) { + register_port(p); + } + + return true; +} + +bool +JackDriver::activate() +{ + World* world = _engine.world(); + + if (_is_activated) { + _engine.log().warn("Jack driver already activated\n"); + return false; + } + + if (!_client) { + attach(world->conf().option("jack-server").ptr<char>(), + world->conf().option("jack-name").ptr<char>(), nullptr); + } + + if (!_client) { + return false; + } + + jack_set_process_callback(_client, process_cb, this); + + _is_activated = true; + + if (jack_activate(_client)) { + _engine.log().error("Could not activate Jack client, aborting\n"); + return false; + } else { + _engine.log().info(fmt("Activated Jack client `%1%'\n") % + world->conf().option("jack-name").ptr<char>()); + } + return true; +} + +void +JackDriver::deactivate() +{ + if (_is_activated) { + _flag = true; + _is_activated = false; + _sem.timed_wait(std::chrono::seconds(1)); + + for (auto& p : _ports) { + unregister_port(p); + } + + if (_client) { + jack_deactivate(_client); + jack_client_close(_client); + _client = nullptr; + } + + _engine.log().info("Deactivated Jack client\n"); + } +} + +EnginePort* +JackDriver::get_port(const Raul::Path& path) +{ + for (auto& p : _ports) { + if (p.graph_port()->path() == path) { + return &p; + } + } + + return nullptr; +} + +void +JackDriver::add_port(RunContext& context, EnginePort* port) +{ + _ports.push_back(*port); + + DuplexPort* graph_port = port->graph_port(); + if (graph_port->is_a(PortType::AUDIO) || graph_port->is_a(PortType::CV)) { + const SampleCount nframes = context.nframes(); + jack_port_t* jport = (jack_port_t*)port->handle(); + void* jbuf = jack_port_get_buffer(jport, nframes); + + /* Jack fails to return a buffer if this is too soon after registering + the port, so use a silent fallback buffer if necessary. */ + graph_port->set_driver_buffer( + jbuf ? jbuf : _fallback_buffer.get(), + nframes * sizeof(float)); + } +} + +void +JackDriver::remove_port(RunContext& context, EnginePort* port) +{ + _ports.erase(_ports.iterator_to(*port)); +} + +void +JackDriver::register_port(EnginePort& port) +{ + jack_port_t* jack_port = jack_port_register( + _client, + port.graph_port()->path().substr(1).c_str(), + ((port.graph_port()->is_a(PortType::AUDIO) || + port.graph_port()->is_a(PortType::CV)) + ? JACK_DEFAULT_AUDIO_TYPE : JACK_DEFAULT_MIDI_TYPE), + (port.graph_port()->is_input() + ? JackPortIsInput : JackPortIsOutput), + 0); + + if (!jack_port) { + throw JackDriver::PortRegistrationFailedException(); + } + + port.set_handle(jack_port); + + for (const auto& p : port.graph_port()->properties()) { + port_property_internal(jack_port, p.first, p.second); + } +} + +void +JackDriver::unregister_port(EnginePort& port) +{ + if (jack_port_unregister(_client, (jack_port_t*)port.handle())) { + _engine.log().error("Failed to unregister Jack port\n"); + } + + port.set_handle(nullptr); +} + +void +JackDriver::rename_port(const Raul::Path& old_path, + const Raul::Path& new_path) +{ + EnginePort* eport = get_port(old_path); + if (eport) { +#ifdef HAVE_JACK_PORT_RENAME + jack_port_rename( + _client, (jack_port_t*)eport->handle(), new_path.substr(1).c_str()); +#else + jack_port_set_name((jack_port_t*)eport->handle(), + new_path.substr(1).c_str()); +#endif + } +} + +void +JackDriver::port_property(const Raul::Path& path, + const URI& uri, + const Atom& value) +{ +#ifdef HAVE_JACK_METADATA + EnginePort* eport = get_port(path); + if (eport) { + const jack_port_t* const jport = (const jack_port_t*)eport->handle(); + port_property_internal(jport, uri, value); + } +#endif +} + +void +JackDriver::port_property_internal(const jack_port_t* jport, + const URI& uri, + const Atom& value) +{ +#ifdef HAVE_JACK_METADATA + if (uri == _engine.world()->uris().lv2_name) { + jack_set_property(_client, jack_port_uuid(jport), + JACK_METADATA_PRETTY_NAME, value.ptr<char>(), "text/plain"); + } else if (uri == _engine.world()->uris().lv2_index) { + jack_set_property(_client, jack_port_uuid(jport), + JACKEY_ORDER, std::to_string(value.get<int32_t>()).c_str(), + "http://www.w3.org/2001/XMLSchema#integer"); + } else if (uri == _engine.world()->uris().rdf_type) { + if (value == _engine.world()->uris().lv2_CVPort) { + jack_set_property(_client, jack_port_uuid(jport), + JACKEY_SIGNAL_TYPE, "CV", "text/plain"); + } + } +#endif +} + +EnginePort* +JackDriver::create_port(DuplexPort* graph_port) +{ + EnginePort* eport = nullptr; + if (graph_port->is_a(PortType::AUDIO) || graph_port->is_a(PortType::CV)) { + // Audio buffer port, use Jack buffer directly + eport = new EnginePort(graph_port); + graph_port->set_is_driver_port(*_engine.buffer_factory()); + } else if (graph_port->is_a(PortType::ATOM) && + graph_port->buffer_type() == _engine.world()->uris().atom_Sequence) { + // Sequence port, make Jack port but use internal LV2 format buffer + eport = new EnginePort(graph_port); + } + + if (eport) { + register_port(*eport); + } + + return eport; +} + +void +JackDriver::pre_process_port(RunContext& context, EnginePort* port) +{ + const URIs& uris = context.engine().world()->uris(); + const SampleCount nframes = context.nframes(); + jack_port_t* jack_port = (jack_port_t*)port->handle(); + DuplexPort* graph_port = port->graph_port(); + Buffer* graph_buf = graph_port->buffer(0).get(); + void* jack_buf = jack_port_get_buffer(jack_port, nframes); + + if (graph_port->is_a(PortType::AUDIO) || graph_port->is_a(PortType::CV)) { + graph_port->set_driver_buffer(jack_buf, nframes * sizeof(float)); + if (graph_port->is_input()) { + graph_port->monitor(context); + } else { + graph_port->buffer(0)->clear(); // TODO: Avoid when possible + } + } else if (graph_port->buffer_type() == uris.atom_Sequence) { + graph_buf->prepare_write(context); + if (graph_port->is_input()) { + // Copy events from Jack port buffer into graph port buffer + const jack_nframes_t event_count = jack_midi_get_event_count(jack_buf); + for (jack_nframes_t i = 0; i < event_count; ++i) { + jack_midi_event_t ev; + jack_midi_event_get(&ev, jack_buf, i); + if (!graph_buf->append_event( + ev.time, ev.size, _midi_event_type, ev.buffer)) { + _engine.log().rt_error("Failed to write to MIDI buffer, events lost!\n"); + } + } + } + graph_port->monitor(context); + } +} + +void +JackDriver::post_process_port(RunContext& context, EnginePort* port) +{ + const URIs& uris = context.engine().world()->uris(); + const SampleCount nframes = context.nframes(); + jack_port_t* jack_port = (jack_port_t*)port->handle(); + DuplexPort* graph_port = port->graph_port(); + void* jack_buf = port->buffer(); + + if (port->graph_port()->is_output()) { + if (!jack_buf) { + // First cycle for a new output, so pre_process wasn't called + jack_buf = jack_port_get_buffer(jack_port, nframes); + port->set_buffer(jack_buf); + } + + if (graph_port->buffer_type() == uris.atom_Sequence) { + // Copy LV2 MIDI events to Jack MIDI buffer + Buffer* const graph_buf = graph_port->buffer(0).get(); + LV2_Atom_Sequence* seq = graph_buf->get<LV2_Atom_Sequence>(); + + jack_midi_clear_buffer(jack_buf); + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + const uint8_t* buf = (const uint8_t*)LV2_ATOM_BODY(&ev->body); + if (ev->body.type == _midi_event_type) { + jack_midi_event_write( + jack_buf, ev->time.frames, buf, ev->body.size); + } + } + } + } + + // Reset graph port buffer pointer to no longer point to the Jack buffer + if (graph_port->is_driver_port()) { + graph_port->set_driver_buffer(nullptr, 0); + } +} + +void +JackDriver::append_time_events(RunContext& context, + Buffer& buffer) +{ + const URIs& uris = context.engine().world()->uris(); + const jack_position_t* pos = &_position; + const bool rolling = (_transport_state == JackTransportRolling); + + // Do nothing if there is no unexpected time change (other than rolling) + if (rolling == _old_rolling && + pos->frame == _old_frame && + pos->beats_per_minute == _old_bpm) { + return; + } + + // Update old time information to detect change next cycle + _old_frame = pos->frame; + _old_rolling = rolling; + _old_bpm = pos->beats_per_minute; + + // Build an LV2 position object to append to the buffer + LV2_Atom pos_buf[16]; + LV2_Atom_Forge_Frame frame; + lv2_atom_forge_set_buffer(&_forge, (uint8_t*)pos_buf, sizeof(pos_buf)); + lv2_atom_forge_object(&_forge, &frame, 0, uris.time_Position); + lv2_atom_forge_key(&_forge, uris.time_frame); + lv2_atom_forge_long(&_forge, pos->frame); + lv2_atom_forge_key(&_forge, uris.time_speed); + lv2_atom_forge_float(&_forge, rolling ? 1.0 : 0.0); + if (pos->valid & JackPositionBBT) { + lv2_atom_forge_key(&_forge, uris.time_barBeat); + lv2_atom_forge_float( + &_forge, pos->beat - 1 + (pos->tick / pos->ticks_per_beat)); + lv2_atom_forge_key(&_forge, uris.time_bar); + lv2_atom_forge_long(&_forge, pos->bar - 1); + lv2_atom_forge_key(&_forge, uris.time_beatUnit); + lv2_atom_forge_int(&_forge, pos->beat_type); + lv2_atom_forge_key(&_forge, uris.time_beatsPerBar); + lv2_atom_forge_float(&_forge, pos->beats_per_bar); + lv2_atom_forge_key(&_forge, uris.time_beatsPerMinute); + lv2_atom_forge_float(&_forge, pos->beats_per_minute); + } + + // Append position to buffer at offset 0 (start of this cycle) + LV2_Atom* lpos = (LV2_Atom*)pos_buf; + buffer.append_event( + 0, lpos->size, lpos->type, (const uint8_t*)LV2_ATOM_BODY_CONST(lpos)); +} + +/**** Jack Callbacks ****/ + +/** Jack process callback, drives entire audio thread. + * + * \callgraph + */ +REALTIME int +JackDriver::_process_cb(jack_nframes_t nframes) +{ + if (nframes == 0 || ! _is_activated) { + if (_flag) { + _sem.post(); + } + return 0; + } + + /* Note that Jack may not call this function for a cycle, if overloaded, + so a rolling counter here would not always be correct. */ + const jack_nframes_t start_of_current_cycle = jack_last_frame_time(_client); + + _transport_state = jack_transport_query(_client, &_position); + + _engine.locate(start_of_current_cycle, nframes); + + // Read input + for (auto& p : _ports) { + pre_process_port(_engine.run_context(), &p); + } + + // Process + _engine.run(nframes); + + // Write output + for (auto& p : _ports) { + post_process_port(_engine.run_context(), &p); + } + + // Update expected transport frame for next cycle to detect changes + if (_transport_state == JackTransportRolling) { + _old_frame += nframes; + } + + return 0; +} + +void +JackDriver::_thread_init_cb() +{ + ThreadManager::set_flag(THREAD_PROCESS); + ThreadManager::set_flag(THREAD_IS_REAL_TIME); +} + +void +JackDriver::_shutdown_cb() +{ + _engine.log().info("Jack shutdown, exiting\n"); + _is_activated = false; + _client = nullptr; +} + +int +JackDriver::_block_length_cb(jack_nframes_t nframes) +{ + if (_engine.root_graph()) { + _block_length = nframes; + _seq_size = jack_port_type_get_buffer_size(_client, JACK_DEFAULT_MIDI_TYPE); + _engine.root_graph()->set_buffer_size( + _engine.run_context(), *_engine.buffer_factory(), PortType::AUDIO, + _engine.buffer_factory()->audio_buffer_size(nframes)); + _engine.root_graph()->set_buffer_size( + _engine.run_context(), *_engine.buffer_factory(), PortType::ATOM, + _seq_size); + } + return 0; +} + +#ifdef INGEN_JACK_SESSION +void +JackDriver::_session_cb(jack_session_event_t* event) +{ + _engine.log().info(fmt("Jack session save to %1%\n") % event->session_dir); + + const std::string cmd = ( + boost::format("ingen -eg -n %1% -u %2% -l ${SESSION_DIR}") + % jack_get_client_name(_client) + % event->client_uuid).str(); + + SPtr<Serialiser> serialiser = _engine.world()->serialiser(); + if (serialiser) { + std::lock_guard<std::mutex> lock(_engine.world()->rdf_mutex()); + + SPtr<Node> root(_engine.root_graph(), NullDeleter<Node>); + serialiser->write_bundle(root, + URI(std::string("file://") + event->session_dir)); + } + + event->command_line = (char*)malloc(cmd.size() + 1); + memcpy(event->command_line, cmd.c_str(), cmd.size() + 1); + jack_session_reply(_client, event); + + switch (event->type) { + case JackSessionSave: + break; + case JackSessionSaveAndQuit: + _engine.log().warn("Jack session quit\n"); + _engine.quit(); + break; + case JackSessionSaveTemplate: + break; + } + + jack_session_event_free(event); +} +#endif + +} // namespace server +} // namespace ingen |