/* Copyright (C) 2019 Paul Davis Author: Johannes Mueller 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 2 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include "pbd/compose.h" #include "pbd/error.h" #include "ardour/debug.h" #include "ardour/session.h" #include "ardour/tempo.h" #include "pbd/i18n.h" #include "contourdesign.h" using namespace ARDOUR; using namespace PBD; using namespace Glib; using namespace std; using namespace ArdourSurface; #include "pbd/abstract_ui.cc" // instantiate template static const uint16_t ContourDesign = 0x0b33; static const uint16_t ShuttlePRO_id = 0x0030; static const uint16_t ShuttleXpress_id = 0x0020; static void LIBUSB_CALL event_callback (struct libusb_transfer* transfer); ContourDesignControlProtocol::ContourDesignControlProtocol (Session& session) : ControlProtocol (session, X_("ContourDesign")) , AbstractUI ("contourdesign") , _io_source (0) , _dev_handle (0) , _usb_transfer (0) , _supposed_to_quit (false) , _device_type (None) , _shuttle_was_zero (true) , _was_rolling_before_shuttle (false) , _test_mode (false) , _keep_rolling (true) , _shuttle_speeds () , _jog_distance () , _gui (0) { libusb_init (0); // libusb_set_debug(0, LIBUSB_LOG_LEVEL_WARNING); _shuttle_speeds.push_back (0.50); _shuttle_speeds.push_back (0.75); _shuttle_speeds.push_back (1.0); _shuttle_speeds.push_back (1.5); _shuttle_speeds.push_back (2.0); _shuttle_speeds.push_back (5.0); _shuttle_speeds.push_back (10.0); setup_default_button_actions (); BaseUI::run(); } ContourDesignControlProtocol::~ContourDesignControlProtocol () { stop (); libusb_exit (0); BaseUI::quit(); tear_down_gui (); } bool ContourDesignControlProtocol::probe () { return true; } int ContourDesignControlProtocol::set_active (bool yn) { DEBUG_TRACE (DEBUG::ContourDesignControl, string_compose ("set_active() init with yn: '%1'\n", yn)); if (yn == active()) { return 0; } if (yn) { start (); } else { stop (); } ControlProtocol::set_active (yn); return _error; } XMLNode& ContourDesignControlProtocol::get_state () { XMLNode& node (ControlProtocol::get_state()); node.set_property (X_("keep-rolling"), _keep_rolling); ostringstream os; vector::const_iterator it = _shuttle_speeds.begin (); os << *(it++); for (; it != _shuttle_speeds.end (); ++it) { os << ' ' << *it; } string s = os.str (); node.set_property (X_("shuttle-speeds"), s); node.set_property (X_("jog-distance"), _jog_distance.value); switch (_jog_distance.unit) { case SECONDS: s = X_("seconds"); break; case BARS: s = X_("bars"); break; case BEATS: default: s = X_("beats"); } node.set_property (X_("jog-unit"), s); for (unsigned int i=0; i<_button_actions.size(); ++i) { XMLNode* child = new XMLNode (string_compose (X_("button-%1"), i+1)); node.add_child_nocopy (_button_actions[i]->get_state (*child)); } return node; } int ContourDesignControlProtocol::set_state (const XMLNode& node, int version) { if (ControlProtocol::set_state (node, version)) { return -1; } node.get_property (X_("keep-rolling"), _keep_rolling); string s; node.get_property (X_("shuttle-speeds"), s); istringstream is (s); for (vector::iterator it = _shuttle_speeds.begin (); it != _shuttle_speeds.end (); ++it) { is >> *it; } node.get_property (X_("jog-distance"), _jog_distance.value); node.get_property (X_("jog-unit"), s); if (s == "seconds") { _jog_distance.unit = SECONDS; } else if (s == "bars") { _jog_distance.unit = BARS; } else { _jog_distance.unit = BEATS; } XMLNode* child; for (unsigned int i=0; i<_button_actions.size(); ++i) { if ((child = node.child (string_compose(X_("button-%1"), i+1).c_str())) == 0) { continue; } string type; child->get_property (X_("type"), type); if (type == X_("action")) { string path (""); child->get_property (X_("path"), path); boost::shared_ptr b (new ButtonAction (path, *this)); _button_actions[i] = b; } else { double value; child->get_property(X_("value"), value); string s; child->get_property(X_("unit"), s); JumpUnit unit; if (s == X_("seconds")) { unit = SECONDS; } else if (s == X_("bars")) { unit = BARS; } else { unit = BEATS; } boost::shared_ptr b (new ButtonJump (JumpDistance (value, unit), *this)); } } return 0; } void ContourDesignControlProtocol::do_request (ContourDesignControlUIRequest* req) { if (req->type == CallSlot) { DEBUG_TRACE (DEBUG::ContourDesignControl, "do_request type CallSlot\n"); call_slot (MISSING_INVALIDATOR, req->the_slot); } else if (req->type == Quit) { DEBUG_TRACE (DEBUG::ContourDesignControl, "do_request type Quit\n"); stop (); } } void ContourDesignControlProtocol::thread_init () { DEBUG_TRACE (DEBUG::ContourDesignControl, "thread_init()\n"); pthread_set_name (X_("contourdesign")); PBD::notify_event_loops_about_thread_creation (pthread_self (), X_("contourdesign"), 2048); ARDOUR::SessionEvent::create_per_thread_pool (X_("contourdesign"), 128); set_thread_priority (); } bool ContourDesignControlProtocol::wait_for_event () { DEBUG_TRACE (DEBUG::ContourDesignControl, "wait_for_event\n"); if (!_supposed_to_quit) { libusb_handle_events (0); } return true; } int get_usb_device (uint16_t vendor_id, uint16_t product_id, libusb_device** device) { struct libusb_device **devs; struct libusb_device *dev; size_t i = 0; int r; *device = 0; if (libusb_get_device_list (0, &devs) < 0) { return LIBUSB_ERROR_NO_DEVICE; } while ((dev = devs[i++])) { struct libusb_device_descriptor desc; r = libusb_get_device_descriptor (dev, &desc); if (r < 0) { goto out; } if (desc.idVendor == vendor_id && desc.idProduct == product_id) { *device = dev; break; } } out: libusb_free_device_list(devs, 1); if (!dev && !r) { return LIBUSB_ERROR_NO_DEVICE; } return r; } int ContourDesignControlProtocol::acquire_device () { DEBUG_TRACE (DEBUG::ContourDesignControl, "acquire_device()\n"); int err; if (_dev_handle) { DEBUG_TRACE (DEBUG::ContourDesignControl, "already have a device handle\n"); return LIBUSB_SUCCESS; } libusb_device* dev; if ((err = get_usb_device (ContourDesign, ShuttleXpress_id, &dev)) == 0) { _device_type = ShuttleXpress; } else if ((err = get_usb_device (ContourDesign, ShuttlePRO_id, &dev)) == 0) { _device_type = ShuttlePRO; } else { _device_type = None; return err; } err = libusb_open (dev, &_dev_handle); if (err < 0) { return err; } libusb_set_auto_detach_kernel_driver (_dev_handle, true); if ((err = libusb_claim_interface (_dev_handle, 0x00))) { DEBUG_TRACE (DEBUG::ContourDesignControl, "failed to claim USB device\n"); goto usb_close; } _usb_transfer = libusb_alloc_transfer (0); if (!_usb_transfer) { DEBUG_TRACE (DEBUG::ContourDesignControl, "failed to alloc usb transfer\n"); err = LIBUSB_ERROR_NO_MEM; goto usb_close; } libusb_fill_interrupt_transfer (_usb_transfer, _dev_handle, 1 | LIBUSB_ENDPOINT_IN, _buf, sizeof(_buf), event_callback, this, 0); DEBUG_TRACE (DEBUG::ContourDesignControl, "callback installed\n"); if ((err = libusb_submit_transfer (_usb_transfer))) { DEBUG_TRACE (DEBUG::ContourDesignControl, string_compose ("failed to submit tansfer: %1\n", err)); goto free_transfer; } return LIBUSB_SUCCESS; free_transfer: libusb_free_transfer (_usb_transfer); usb_close: libusb_close (_dev_handle); _dev_handle = 0; return err; } void ContourDesignControlProtocol::release_device () { if (!_dev_handle) { return; } libusb_close (_dev_handle); libusb_free_transfer (_usb_transfer); libusb_release_interface (_dev_handle, 0); _usb_transfer = 0; _dev_handle = 0; } void ContourDesignControlProtocol::start () { DEBUG_TRACE (DEBUG::ContourDesignControl, "start()\n"); _supposed_to_quit = false; _error = acquire_device(); if (_error) { return; } if (!_dev_handle) { // can this actually happen? _error = -1; return; } _state.shuttle = 0; _state.jog = 0; _state.buttons = 0; Glib::RefPtr source = Glib::IdleSource::create (); source->connect (sigc::mem_fun (*this, &ContourDesignControlProtocol::wait_for_event)); source->attach (_main_loop->get_context ()); _io_source = source->gobj (); g_source_ref (_io_source); } void ContourDesignControlProtocol::stop () { DEBUG_TRACE (DEBUG::ContourDesignControl, "stop()\n"); _supposed_to_quit = true; if (_io_source) { g_source_destroy (_io_source); g_source_unref (_io_source); _io_source = 0; } if (_dev_handle) { release_device (); } } void ContourDesignControlProtocol::handle_event () { if (_usb_transfer->status == LIBUSB_TRANSFER_TIMED_OUT) { goto resubmit; } if (_usb_transfer->status != LIBUSB_TRANSFER_COMPLETED) { DEBUG_TRACE (DEBUG::ContourDesignControl, string_compose("libusb_transfer not completed: %1\n", _usb_transfer->status)); _error = LIBUSB_ERROR_NO_DEVICE; return; } State new_state; new_state.shuttle = _buf[0]; new_state.jog = _buf[1]; new_state.buttons = (_buf[4] << 8) + _buf[3]; // cout << "event " << (int)new_state.shuttle << " " << (int)new_state.jog << " " << (int)new_state.buttons << endl;; for (uint8_t btn=0; btn<16; btn++) { if ( (new_state.buttons & (1< _state.jog) { jog_event_forward(); } if (new_state.shuttle != _state.shuttle) { shuttle_event(new_state.shuttle); } _state = new_state; resubmit: if (libusb_submit_transfer (_usb_transfer)) { DEBUG_TRACE (DEBUG::ContourDesignControl, "failed to resubmit usb transfer after callback\n"); stop (); } } boost::shared_ptr ContourDesignControlProtocol::make_button_action (string action_string) { return boost::shared_ptr (new ButtonAction (action_string, *this)); } /* The buttons have the following layout * * 00 01 02 03 * 04 05 06 07 08 * * 13 Jog 14 * * 09 10 * 11 12 */ void ContourDesignControlProtocol::setup_default_button_actions () { _button_actions.push_back (make_button_action ("MIDI/panic")); _button_actions.push_back (make_button_action ("Editor/remove-last-capture")); _button_actions.push_back (make_button_action ("Editor/undo")); _button_actions.push_back (make_button_action ("Editor/redo")); _button_actions.push_back (make_button_action ("Common/jump-backward-to-mark")); _button_actions.push_back (make_button_action ("Transport/Record")); _button_actions.push_back (make_button_action ("Transport/Stop")); _button_actions.push_back (make_button_action ("Transport/Roll")); _button_actions.push_back (make_button_action ("Common/jump-forward-to-mark")); _button_actions.push_back (boost::shared_ptr (new ButtonJump (JumpDistance (-4.0, BARS), *this))); _button_actions.push_back (boost::shared_ptr (new ButtonJump (JumpDistance (+4.0, BARS), *this))); _button_actions.push_back (make_button_action ("")); _button_actions.push_back (make_button_action ("Common/add-location-from-playhead")); _button_actions.push_back (make_button_action ("Transport/GotoStart")); _button_actions.push_back (make_button_action ("Transport/GotoEnd")); } void ContourDesignControlProtocol::handle_button_press (unsigned short btn) { if (_test_mode) { ButtonPress (btn); /* emit signal */ return; } if (btn >= _button_actions.size ()) { DEBUG_TRACE (DEBUG::ContourDesignControl, string_compose ("ContourDesign button number out of bounds %1, max is %2\n", btn, _button_actions.size ())); return; } _button_actions[btn]->execute (); } void ContourDesignControlProtocol::handle_button_release (unsigned short btn) { if (_test_mode) { ButtonRelease (btn); /* emit signal */ } } void ContourDesignControlProtocol::prev_marker_keep_rolling () { samplepos_t pos = session->locations()->first_mark_before (session->transport_sample()); if (pos >= 0) { session->request_locate (pos, _keep_rolling && session->transport_rolling()); } else { session->goto_start (); } } void ContourDesignControlProtocol::next_marker_keep_rolling () { samplepos_t pos = session->locations()->first_mark_after (session->transport_sample()); if (pos >= 0) { session->request_locate (pos, _keep_rolling && session->transport_rolling()); } else { session->goto_end(); } } void ContourDesignControlProtocol::jog_event_backward () { DEBUG_TRACE (DEBUG::ContourDesignControl, "jog event backward\n"); jump_backward (_jog_distance); } void ContourDesignControlProtocol::jog_event_forward () { DEBUG_TRACE (DEBUG::ContourDesignControl, "jog event forward\n"); jump_forward (_jog_distance); } void ContourDesignControlProtocol::jump_forward (JumpDistance dist) { bool kr = _keep_rolling && session->transport_rolling (); switch (dist.unit) { case SECONDS: jump_by_seconds (dist.value, kr); break; case BEATS: jump_by_beats (dist.value, kr); break; case BARS: jump_by_bars (dist.value, kr); break; default: break; } } void ContourDesignControlProtocol::jump_backward (JumpDistance dist) { JumpDistance bw = dist; bw.value = -bw.value; jump_forward(bw); } void ContourDesignControlProtocol::set_shuttle_speed (int index, double speed) { /* called from GUI thread */ // XXX this may race with ContourDesignControlProtocol::shuttle_event() // TODO: add bounds check _shuttle_speeds[index] = speed; } void ContourDesignControlProtocol::shuttle_event(int position) { DEBUG_TRACE (DEBUG::ContourDesignControl, string_compose ("shuttle event %1\n", position)); if (position != 0) { if (_shuttle_was_zero) { _was_rolling_before_shuttle = session->transport_rolling (); } double speed = position > 0 ? _shuttle_speeds[position-1] : -_shuttle_speeds[-position-1]; set_transport_speed (speed); _shuttle_was_zero = false; } else { if (_keep_rolling && _was_rolling_before_shuttle) { set_transport_speed (1.0); } else { transport_stop (); } _shuttle_was_zero = true; } } void ButtonJump::execute () { _spc.jump_forward (_dist); } XMLNode& ButtonJump::get_state (XMLNode& node) const { string ts (X_("jump")); node.set_property (X_("type"), ts); node.set_property (X_("distance"), _dist.value); string s; switch (_dist.unit) { case SECONDS: s = X_("seconds"); break; case BARS: s = X_("bars"); break; case BEATS: default: s = X_("beats"); } node.set_property (X_("unit"), s); return node; } void ButtonAction::execute () { _spc.access_action (_action_string); } XMLNode& ButtonAction::get_state (XMLNode& node) const { string ts (X_("action")); node.set_property (X_("type"), ts); node.set_property (X_("path"), _action_string); return node; } static void LIBUSB_CALL event_callback (libusb_transfer* transfer) { ContourDesignControlProtocol* spc = static_cast (transfer->user_data); spc->handle_event(); }