/* Copyright (C) 2016 Paul Davis 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., 675 Mass Ave, Cambridge, MA 02139, USA. */ #include #include #include #include "pbd/compose.h" #include "pbd/convert.h" #include "pbd/debug.h" #include "pbd/failed_constructor.h" #include "pbd/file_utils.h" #include "pbd/search_path.h" #include "pbd/enumwriter.h" #include "midi++/parser.h" #include "timecode/time.h" #include "timecode/bbt_time.h" #include "ardour/async_midi_port.h" #include "ardour/audioengine.h" #include "ardour/debug.h" #include "ardour/filesystem_paths.h" #include "ardour/midiport_manager.h" #include "ardour/midi_track.h" #include "ardour/midi_port.h" #include "ardour/session.h" #include "ardour/tempo.h" #include "push2.h" #include "gui.h" using namespace ARDOUR; using namespace std; using namespace PBD; using namespace Glib; using namespace ArdourSurface; #include "i18n.h" #include "pbd/abstract_ui.cc" // instantiate template const int Push2::cols = 960; const int Push2::rows = 160; const int Push2::pixels_per_row = 1024; #define ABLETON 0x2982 #define PUSH2 0x1967 __attribute__((constructor)) static void register_enums () { EnumWriter& enum_writer (EnumWriter::instance()); vector i; vector s; MusicalMode::Type mode; #define REGISTER(e) enum_writer.register_distinct (typeid(e).name(), i, s); i.clear(); s.clear() #define REGISTER_CLASS_ENUM(t,e) i.push_back (t::e); s.push_back (#e) REGISTER_CLASS_ENUM (MusicalMode,Dorian); REGISTER_CLASS_ENUM (MusicalMode, IonianMajor); REGISTER_CLASS_ENUM (MusicalMode, Minor); REGISTER_CLASS_ENUM (MusicalMode, HarmonicMinor); REGISTER_CLASS_ENUM (MusicalMode, MelodicMinorAscending); REGISTER_CLASS_ENUM (MusicalMode, MelodicMinorDescending); REGISTER_CLASS_ENUM (MusicalMode, Phrygian); REGISTER_CLASS_ENUM (MusicalMode, Lydian); REGISTER_CLASS_ENUM (MusicalMode, Mixolydian); REGISTER_CLASS_ENUM (MusicalMode, Aeolian); REGISTER_CLASS_ENUM (MusicalMode, Locrian); REGISTER_CLASS_ENUM (MusicalMode, PentatonicMajor); REGISTER_CLASS_ENUM (MusicalMode, PentatonicMinor); REGISTER_CLASS_ENUM (MusicalMode, Chromatic); REGISTER_CLASS_ENUM (MusicalMode, BluesScale); REGISTER_CLASS_ENUM (MusicalMode, NeapolitanMinor); REGISTER_CLASS_ENUM (MusicalMode, NeapolitanMajor); REGISTER_CLASS_ENUM (MusicalMode, Oriental); REGISTER_CLASS_ENUM (MusicalMode, DoubleHarmonic); REGISTER_CLASS_ENUM (MusicalMode, Enigmatic); REGISTER_CLASS_ENUM (MusicalMode, Hirajoshi); REGISTER_CLASS_ENUM (MusicalMode, HungarianMinor); REGISTER_CLASS_ENUM (MusicalMode, HungarianMajor); REGISTER_CLASS_ENUM (MusicalMode, Kumoi); REGISTER_CLASS_ENUM (MusicalMode, Iwato); REGISTER_CLASS_ENUM (MusicalMode, Hindu); REGISTER_CLASS_ENUM (MusicalMode, Spanish8Tone); REGISTER_CLASS_ENUM (MusicalMode, Pelog); REGISTER_CLASS_ENUM (MusicalMode, HungarianGypsy); REGISTER_CLASS_ENUM (MusicalMode, Overtone); REGISTER_CLASS_ENUM (MusicalMode, LeadingWholeTone); REGISTER_CLASS_ENUM (MusicalMode, Arabian); REGISTER_CLASS_ENUM (MusicalMode, Balinese); REGISTER_CLASS_ENUM (MusicalMode, Gypsy); REGISTER_CLASS_ENUM (MusicalMode, Mohammedan); REGISTER_CLASS_ENUM (MusicalMode, Javanese); REGISTER_CLASS_ENUM (MusicalMode, Persian); REGISTER_CLASS_ENUM (MusicalMode, Algerian); REGISTER (mode); } Push2::Push2 (ARDOUR::Session& s) : ControlProtocol (s, string (X_("Ableton Push 2"))) , AbstractUI (name()) , handle (0) , device_buffer (0) , frame_buffer (Cairo::ImageSurface::create (Cairo::FORMAT_ARGB32, cols, rows)) , modifier_state (None) , splash_start (0) , bank_start (0) , connection_state (ConnectionState (0)) , gui (0) , _mode (MusicalMode::IonianMajor) , _scale_root (0) , _root_octave (3) , _in_key (true) , octave_shift (0) { context = Cairo::Context::create (frame_buffer); tc_clock_layout = Pango::Layout::create (context); bbt_clock_layout = Pango::Layout::create (context); Pango::FontDescription fd ("Sans Bold 24"); tc_clock_layout->set_font_description (fd); bbt_clock_layout->set_font_description (fd); Pango::FontDescription fd2 ("Sans 10"); for (int n = 0; n < 8; ++n) { upper_layout[n] = Pango::Layout::create (context); upper_layout[n]->set_font_description (fd2); upper_layout[n]->set_text ("solo"); lower_layout[n] = Pango::Layout::create (context); lower_layout[n]->set_font_description (fd2); lower_layout[n]->set_text ("mute"); } Pango::FontDescription fd3 ("Sans Bold 10"); for (int n = 0; n < 8; ++n) { mid_layout[n] = Pango::Layout::create (context); mid_layout[n]->set_font_description (fd3); } build_pad_table (); build_maps (); if (open ()) { throw failed_constructor (); } StripableSelectionChanged.connect (selection_connection, MISSING_INVALIDATOR, boost::bind (&Push2::stripable_selection_change, this, _1), this); /* catch arrival and departure of Push2 itself */ ARDOUR::AudioEngine::instance()->PortRegisteredOrUnregistered.connect (port_reg_connection, MISSING_INVALIDATOR, boost::bind (&Push2::port_registration_handler, this), this); /* Catch port connections and disconnections */ ARDOUR::AudioEngine::instance()->PortConnectedOrDisconnected.connect (port_connection, MISSING_INVALIDATOR, boost::bind (&Push2::connection_handler, this, _1, _2, _3, _4, _5), this); /* ports might already be there */ port_registration_handler (); } Push2::~Push2 () { stop (); } void Push2::port_registration_handler () { if (_async_in->connected() && _async_out->connected()) { /* don't waste cycles here */ return; } string input_port_name = X_("Ableton Push 2 MIDI 1 in"); string output_port_name = X_("Ableton Push 2 MIDI 1 out"); vector in; vector out; AudioEngine::instance()->get_ports (string_compose (".*%1", input_port_name), DataType::MIDI, PortFlags (IsPhysical|IsOutput|ControlOnly), in); AudioEngine::instance()->get_ports (string_compose (".*%1", output_port_name), DataType::MIDI, PortFlags (IsPhysical|IsInput|ControlOnly), out); if (!in.empty() && !out.empty()) { cerr << "Push2: both ports found\n"; cerr << "\tconnecting to " << in.front() << " + " << out.front() << endl; if (!_async_in->connected()) { AudioEngine::instance()->connect (_async_in->name(), in.front()); } if (!_async_out->connected()) { AudioEngine::instance()->connect (_async_out->name(), out.front()); } } } int Push2::open () { int err; if (handle) { /* already open */ return 0; } if ((handle = libusb_open_device_with_vid_pid (NULL, ABLETON, PUSH2)) == 0) { return -1; } if ((err = libusb_claim_interface (handle, 0x00))) { return -1; } device_frame_buffer = new uint16_t[rows*pixels_per_row]; memset (device_frame_buffer, 0, sizeof (uint16_t) * rows * pixels_per_row); frame_header[0] = 0xef; frame_header[1] = 0xcd; frame_header[2] = 0xab; frame_header[3] = 0x89; memset (&frame_header[4], 0, 12); /* setup ports */ _async_in = AudioEngine::instance()->register_input_port (DataType::MIDI, X_("Push 2 in"), true); _async_out = AudioEngine::instance()->register_output_port (DataType::MIDI, X_("Push 2 out"), true); if (_async_in == 0 || _async_out == 0) { return -1; } /* We do not add our ports to the input/output bundles because we don't * want users wiring them by hand. They could use JACK tools if they * really insist on that. */ _input_port = boost::dynamic_pointer_cast(_async_in).get(); _output_port = boost::dynamic_pointer_cast(_async_out).get(); /* Create a shadow port where, depending on the state of the surface, * we will make pad note on/off events appear. The surface code will * automatically this port to the first selected MIDI track. */ boost::dynamic_pointer_cast(_async_in)->add_shadow_port (string_compose (_("%1 Pads"), X_("Push 2")), boost::bind (&Push2::pad_filter, this, _1, _2)); boost::shared_ptr shadow_port = boost::dynamic_pointer_cast(_async_in)->shadow_port(); if (shadow_port) { _output_bundle.reset (new ARDOUR::Bundle (_("Push 2 Pads"), false)); _output_bundle->add_channel ( shadow_port->name(), ARDOUR::DataType::MIDI, session->engine().make_port_name_non_relative (shadow_port->name()) ); } session->BundleAddedOrRemoved (); connect_to_parser (); return 0; } list > Push2::bundles () { list > b; if (_output_bundle) { b.push_back (_output_bundle); } return b; } int Push2::close () { init_buttons (false); /* wait for button data to be flushed */ AsyncMIDIPort* asp; asp = dynamic_cast (_output_port); asp->drain (10000, 500000); AudioEngine::instance()->unregister_port (_async_in); AudioEngine::instance()->unregister_port (_async_out); _async_in.reset ((ARDOUR::Port*) 0); _async_out.reset ((ARDOUR::Port*) 0); _input_port = 0; _output_port = 0; vblank_connection.disconnect (); periodic_connection.disconnect (); session_connections.drop_connections (); stripable_connections.drop_connections (); if (handle) { libusb_release_interface (handle, 0x00); libusb_close (handle); handle = 0; } for (int n = 0; n < 8; ++n) { stripable[n].reset (); } delete [] device_frame_buffer; device_frame_buffer = 0; return 0; } void Push2::init_buttons (bool startup) { /* This is a list of buttons that we want lit because they do something in ardour related (loosely, sometimes) to their illuminated label. */ ButtonID buttons[] = { Mute, Solo, Master, Up, Right, Left, Down, Note, Session, Mix, AddTrack, Delete, Undo, Metronome, Shift, Select, Play, RecordEnable, Automate, Repeat, Note, Session, DoubleLoop, Quantize, Duplicate, Browse, PageRight, PageLeft, OctaveUp, OctaveDown }; for (size_t n = 0; n < sizeof (buttons) / sizeof (buttons[0]); ++n) { Button* b = id_button_map[buttons[n]]; if (startup) { b->set_color (LED::White); } else { b->set_color (LED::Black); } b->set_state (LED::OneShot24th); write (b->state_msg()); } /* Strip buttons should all be off (black) by default. They will change * color to reflect various conditions */ ButtonID strip_buttons[] = { Upper1, Upper2, Upper3, Upper4, Upper5, Upper6, Upper7, Upper8, Lower1, Lower2, Lower3, Lower4, Lower5, Lower6, Lower7, Lower8, }; for (size_t n = 0; n < sizeof (strip_buttons) / sizeof (strip_buttons[0]); ++n) { Button* b = id_button_map[strip_buttons[n]]; b->set_color (LED::Black); b->set_state (LED::OneShot24th); write (b->state_msg()); } if (startup) { /* all other buttons are off (black) */ ButtonID off_buttons[] = { TapTempo, Setup, User, Stop, Convert, New, FixedLength, Fwd32ndT, Fwd32nd, Fwd16thT, Fwd16th, Fwd8thT, Fwd8th, Fwd4trT, Fwd4tr, Accent, Scale, Layout, Note, Session, }; for (size_t n = 0; n < sizeof (off_buttons) / sizeof (off_buttons[0]); ++n) { Button* b = id_button_map[off_buttons[n]]; b->set_color (LED::Black); b->set_state (LED::OneShot24th); write (b->state_msg()); } } if (!startup) { for (NNPadMap::iterator pi = nn_pad_map.begin(); pi != nn_pad_map.end(); ++pi) { Pad* pad = pi->second; pad->set_color (LED::Black); pad->set_state (LED::OneShot24th); write (pad->state_msg()); } } } bool Push2::probe () { libusb_device_handle *h; libusb_init (NULL); if ((h = libusb_open_device_with_vid_pid (NULL, ABLETON, PUSH2)) == 0) { DEBUG_TRACE (DEBUG::Push2, "no Push2 device found\n"); return false; } libusb_close (h); DEBUG_TRACE (DEBUG::Push2, "Push2 device located\n"); return true; } void* Push2::request_factory (uint32_t num_requests) { /* AbstractUI::request_buffer_factory() is a template method only instantiated in this source module. To provide something visible for use in the interface/descriptor, we have this static method that is template-free. */ return request_buffer_factory (num_requests); } void Push2::do_request (Push2Request * req) { DEBUG_TRACE (DEBUG::Push2, string_compose ("doing request type %1\n", req->type)); if (req->type == CallSlot) { call_slot (MISSING_INVALIDATOR, req->the_slot); } else if (req->type == Quit) { stop (); } } int Push2::stop () { BaseUI::quit (); close (); return 0; } /** render host-side frame buffer (a Cairo ImageSurface) to the current * device-side frame buffer. The device frame buffer will be pushed to the * device on the next call to vblank() */ int Push2::blit_to_device_frame_buffer () { /* ensure that all drawing has been done before we fetch pixel data */ frame_buffer->flush (); const int stride = 3840; /* bytes per row for Cairo::FORMAT_ARGB32 */ const uint8_t* data = frame_buffer->get_data (); /* fill frame buffer (320kB) */ uint16_t* fb = (uint16_t*) device_frame_buffer; for (int row = 0; row < rows; ++row) { const uint8_t* dp = data + row * stride; for (int col = 0; col < cols; ++col) { /* fetch r, g, b (range 0..255). Ignore alpha */ const int r = (*((const uint32_t*)dp) >> 16) & 0xff; const int g = (*((const uint32_t*)dp) >> 8) & 0xff; const int b = *((const uint32_t*)dp) & 0xff; /* convert to 5 bits, 6 bits, 5 bits, respectively */ /* generate 16 bit BGB565 value */ *fb++ = (r >> 3) | ((g & 0xfc) << 3) | ((b & 0xf8) << 8); /* the push2 docs state that we should xor the pixel * data. Doing so doesn't work correctly, and not doing * so seems to work fine (colors roughly match intended * values). */ dp += 4; } /* skip 128 bytes to next line. This is filler, used to avoid line borders occuring in the middle of 512 byte USB buffers */ fb += 64; /* 128 bytes = 64 int16_t */ } return 0; } bool Push2::redraw () { string tc_clock_text; string bbt_clock_text; if (splash_start) { if (get_microseconds() - splash_start > 4000000) { splash_start = 0; } else { return false; } } if (session) { framepos_t audible = session->audible_frame(); Timecode::Time TC; bool negative = false; if (audible < 0) { audible = -audible; negative = true; } session->timecode_time (audible, TC); TC.negative = TC.negative || negative; tc_clock_text = Timecode::timecode_format_time(TC); Timecode::BBT_Time bbt = session->tempo_map().bbt_at_frame (audible); char buf[16]; #define BBT_BAR_CHAR "|" if (negative) { snprintf (buf, sizeof (buf), "-%03" PRIu32 BBT_BAR_CHAR "%02" PRIu32 BBT_BAR_CHAR "%04" PRIu32, bbt.bars, bbt.beats, bbt.ticks); } else { snprintf (buf, sizeof (buf), " %03" PRIu32 BBT_BAR_CHAR "%02" PRIu32 BBT_BAR_CHAR "%04" PRIu32, bbt.bars, bbt.beats, bbt.ticks); } bbt_clock_text = buf; } bool dirty = false; if (tc_clock_text != tc_clock_layout->get_text()) { dirty = true; tc_clock_layout->set_text (tc_clock_text); } if (bbt_clock_text != tc_clock_layout->get_text()) { dirty = true; bbt_clock_layout->set_text (bbt_clock_text); } string mid_text; for (int n = 0; n < 8; ++n) { if (stripable[n]) { mid_text = short_version (stripable[n]->name(), 10); if (mid_text != mid_layout[n]->get_text()) { mid_layout[n]->set_text (mid_text); dirty = true; } } } if (!dirty) { return false; } context->set_source_rgb (0.764, 0.882, 0.882); context->rectangle (0, 0, 960, 160); context->fill (); context->set_source_rgb (0.23, 0.0, 0.349); context->move_to (650, 25); tc_clock_layout->update_from_cairo_context (context); tc_clock_layout->show_in_cairo_context (context); context->move_to (650, 60); bbt_clock_layout->update_from_cairo_context (context); bbt_clock_layout->show_in_cairo_context (context); for (int n = 0; n < 8; ++n) { context->move_to (10 + (n*120), 2); upper_layout[n]->update_from_cairo_context (context); upper_layout[n]->show_in_cairo_context (context); } for (int n = 0; n < 8; ++n) { context->move_to (10 + (n*120), 140); lower_layout[n]->update_from_cairo_context (context); lower_layout[n]->show_in_cairo_context (context); } for (int n = 0; n < 8; ++n) { if (stripable[n] && stripable[n]->presentation_info().selected()) { context->rectangle (10 + (n*120) - 5, 115, 120, 22); context->set_source_rgb (1.0, 0.737, 0.172); context->fill(); } context->set_source_rgb (0.0, 0.0, 0.0); context->move_to (10 + (n*120), 120); mid_layout[n]->update_from_cairo_context (context); mid_layout[n]->show_in_cairo_context (context); } /* render clock */ /* render foo */ /* render bar */ return true; } bool Push2::vblank () { int transferred = 0; const int timeout_msecs = 1000; int err; if ((err = libusb_bulk_transfer (handle, 0x01, frame_header, sizeof (frame_header), &transferred, timeout_msecs))) { return false; } if (redraw()) { /* things changed */ blit_to_device_frame_buffer (); } if ((err = libusb_bulk_transfer (handle, 0x01, (uint8_t*) device_frame_buffer , 2 * rows * pixels_per_row, &transferred, timeout_msecs))) { return false; } return true; } int Push2::set_active (bool yn) { DEBUG_TRACE (DEBUG::Push2, string_compose("Push2Protocol::set_active init with yn: '%1'\n", yn)); if (yn == active()) { return 0; } if (yn) { /* start event loop */ BaseUI::run (); if (open ()) { DEBUG_TRACE (DEBUG::Push2, "device open failed\n"); close (); return -1; } /* Connect input port to event loop */ AsyncMIDIPort* asp; asp = dynamic_cast (_input_port); asp->xthread().set_receive_handler (sigc::bind (sigc::mem_fun (this, &Push2::midi_input_handler), _input_port)); asp->xthread().attach (main_loop()->get_context()); connect_session_signals (); /* set up periodic task used to push a frame buffer to the * device (25fps). The device can handle 60fps, but we don't * need that frame rate. */ Glib::RefPtr vblank_timeout = Glib::TimeoutSource::create (40); // milliseconds vblank_connection = vblank_timeout->connect (sigc::mem_fun (*this, &Push2::vblank)); vblank_timeout->attach (main_loop()->get_context()); Glib::RefPtr periodic_timeout = Glib::TimeoutSource::create (1000); // milliseconds periodic_connection = periodic_timeout->connect (sigc::mem_fun (*this, &Push2::periodic)); periodic_timeout->attach (main_loop()->get_context()); init_buttons (true); init_touch_strip (); set_pad_scale (_scale_root, _root_octave, _mode, _in_key); switch_bank (0); splash (); /* catch current selection, if any */ { StripableNotificationListPtr sp (new StripableNotificationList (ControlProtocol::last_selected())); stripable_selection_change (sp); } } else { stop (); } ControlProtocol::set_active (yn); DEBUG_TRACE (DEBUG::Push2, string_compose("Push2Protocol::set_active done with yn: '%1'\n", yn)); return 0; } void Push2::init_touch_strip () { MidiByteArray msg (9, 0xf0, 0x00, 0x21, 0x1d, 0x01, 0x01, 0x17, 0x00, 0xf7); /* flags are the final byte (ignore end-of-sysex */ /* show bar, not point autoreturn to center bar starts at center */ msg[7] = (1<<4) | (1<<5) | (1<<6); write (msg); } void Push2::write (const MidiByteArray& data) { /* immediate delivery */ _output_port->write (&data[0], data.size(), 0); } bool Push2::midi_input_handler (IOCondition ioc, MIDI::Port* port) { if (ioc & ~IO_IN) { DEBUG_TRACE (DEBUG::Push2, "MIDI port closed\n"); return false; } if (ioc & IO_IN) { // DEBUG_TRACE (DEBUG::Push2, string_compose ("something happend on %1\n", port->name())); AsyncMIDIPort* asp = dynamic_cast(port); if (asp) { asp->clear (); } //DEBUG_TRACE (DEBUG::Push2, string_compose ("data available on %1\n", port->name())); framepos_t now = AudioEngine::instance()->sample_time(); port->parse (now); } return true; } bool Push2::periodic () { return true; } void Push2::connect_to_parser () { DEBUG_TRACE (DEBUG::Push2, string_compose ("Connecting to signals on port %2\n", _input_port->name())); MIDI::Parser* p = _input_port->parser(); /* Incoming sysex */ p->sysex.connect_same_thread (*this, boost::bind (&Push2::handle_midi_sysex, this, _1, _2, _3)); /* V-Pot messages are Controller */ p->controller.connect_same_thread (*this, boost::bind (&Push2::handle_midi_controller_message, this, _1, _2)); /* Button messages are NoteOn */ p->note_on.connect_same_thread (*this, boost::bind (&Push2::handle_midi_note_on_message, this, _1, _2)); /* Button messages are NoteOn but libmidi++ sends note-on w/velocity = 0 as note-off so catch them too */ p->note_off.connect_same_thread (*this, boost::bind (&Push2::handle_midi_note_on_message, this, _1, _2)); /* Fader messages are Pitchbend */ p->channel_pitchbend[0].connect_same_thread (*this, boost::bind (&Push2::handle_midi_pitchbend_message, this, _1, _2)); } void Push2::handle_midi_sysex (MIDI::Parser&, MIDI::byte* raw_bytes, size_t sz) { DEBUG_TRACE (DEBUG::Push2, string_compose ("Sysex, %1 bytes\n", sz)); } void Push2::handle_midi_controller_message (MIDI::Parser&, MIDI::EventTwoBytes* ev) { DEBUG_TRACE (DEBUG::Push2, string_compose ("CC %1 (value %2)\n", (int) ev->controller_number, (int) ev->value)); CCButtonMap::iterator b = cc_button_map.find (ev->controller_number); if (ev->value) { /* any press cancels any pending long press timeouts */ for (set::iterator x = buttons_down.begin(); x != buttons_down.end(); ++x) { Button* bb = id_button_map[*x]; bb->timeout_connection.disconnect (); } } if (b != cc_button_map.end()) { Button* button = b->second; if (ev->value) { buttons_down.insert (button->id); start_press_timeout (*button, button->id); } else { buttons_down.erase (button->id); button->timeout_connection.disconnect (); } set::iterator c = consumed.find (button->id); if (c == consumed.end()) { if (ev->value == 0) { (this->*button->release_method)(); } else { (this->*button->press_method)(); } } else { DEBUG_TRACE (DEBUG::Push2, "button was consumed, ignored\n"); consumed.erase (c); } } else { /* encoder/vpot */ int delta = ev->value; if (delta > 63) { delta = -(128 - delta); } switch (ev->controller_number) { case 71: strip_vpot (0, delta); break; case 72: strip_vpot (1, delta); break; case 73: strip_vpot (2, delta); break; case 74: strip_vpot (3, delta); break; case 75: strip_vpot (4, delta); break; case 76: strip_vpot (5, delta); break; case 77: strip_vpot (6, delta); break; case 78: strip_vpot (7, delta); break; /* left side pair */ case 14: strip_vpot (8, delta); break; case 15: other_vpot (1, delta); break; /* right side */ case 79: other_vpot (2, delta); break; } } } void Push2::handle_midi_note_on_message (MIDI::Parser& parser, MIDI::EventTwoBytes* ev) { DEBUG_TRACE (DEBUG::Push2, string_compose ("Note On %1 (velocity %2)\n", (int) ev->note_number, (int) ev->velocity)); if (ev->velocity == 0) { handle_midi_note_off_message (parser, ev); return; } switch (ev->note_number) { case 0: strip_vpot_touch (0, ev->velocity > 64); break; case 1: strip_vpot_touch (1, ev->velocity > 64); break; case 2: strip_vpot_touch (2, ev->velocity > 64); break; case 3: strip_vpot_touch (3, ev->velocity > 64); break; case 4: strip_vpot_touch (4, ev->velocity > 64); break; case 5: strip_vpot_touch (5, ev->velocity > 64); break; case 6: strip_vpot_touch (6, ev->velocity > 64); break; case 7: strip_vpot_touch (7, ev->velocity > 64); break; /* left side */ case 10: other_vpot_touch (0, ev->velocity > 64); break; case 9: other_vpot_touch (1, ev->velocity > 64); break; /* right side */ case 8: other_vpot_touch (3, ev->velocity > 64); break; /* touch strip */ case 12: if (ev->velocity < 64) { transport_stop (); } break; } if (ev->note_number < 11) { return; } /* Pad */ NNPadMap::iterator pi = nn_pad_map.find (ev->note_number); if (pi == nn_pad_map.end()) { return; } Pad* pad = pi->second; if (pad->do_when_pressed == Pad::FlashOn) { pad->set_color (LED::White); pad->set_state (LED::OneShot24th); write (pad->state_msg()); } else if (pad->do_when_pressed == Pad::FlashOff) { pad->set_color (LED::Black); pad->set_state (LED::OneShot24th); write (pad->state_msg()); } } void Push2::handle_midi_note_off_message (MIDI::Parser&, MIDI::EventTwoBytes* ev) { DEBUG_TRACE (DEBUG::Push2, string_compose ("Note Off %1 (velocity %2)\n", (int) ev->note_number, (int) ev->velocity)); if (ev->note_number < 11) { /* theoretically related to encoder touch start/end, but * actually they send note on with two different velocity * values (127 & 64). */ return; } NNPadMap::iterator pi = nn_pad_map.find (ev->note_number); if (pi == nn_pad_map.end()) { return; } Pad* pad = pi->second; if (pad->do_when_pressed == Pad::FlashOn) { pad->set_color (LED::Black); pad->set_state (LED::OneShot24th); write (pad->state_msg()); } else if (pad->do_when_pressed == Pad::FlashOff) { pad->set_color (pad->perma_color); pad->set_state (LED::OneShot24th); write (pad->state_msg()); } } void Push2::handle_midi_pitchbend_message (MIDI::Parser&, MIDI::pitchbend_t pb) { if (!session) { return; } float speed; /* range of +1 .. -1 */ speed = ((int32_t) pb - 8192) / 8192.0; /* convert to range of +3 .. -3 */ session->request_transport_speed (speed * 3.0); } void Push2::thread_init () { struct sched_param rtparam; pthread_set_name (event_loop_name().c_str()); PBD::notify_event_loops_about_thread_creation (pthread_self(), event_loop_name(), 2048); ARDOUR::SessionEvent::create_per_thread_pool (event_loop_name(), 128); memset (&rtparam, 0, sizeof (rtparam)); rtparam.sched_priority = 9; /* XXX should be relative to audio (JACK) thread */ if (pthread_setschedparam (pthread_self(), SCHED_FIFO, &rtparam) != 0) { // do we care? not particularly. } } void Push2::connect_session_signals() { // receive routes added //session->RouteAdded.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&MackieControlProtocol::notify_routes_added, this, _1), this); // receive VCAs added //session->vca_manager().VCAAdded.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&Push2::notify_vca_added, this, _1), this); // receive record state toggled session->RecordStateChanged.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&Push2::notify_record_state_changed, this), this); // receive transport state changed session->TransportStateChange.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&Push2::notify_transport_state_changed, this), this); session->TransportLooped.connect (session_connections, MISSING_INVALIDATOR, boost::bind (&Push2::notify_loop_state_changed, this), this); // receive punch-in and punch-out Config->ParameterChanged.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&Push2::notify_parameter_changed, this, _1), this); session->config.ParameterChanged.connect (session_connections, MISSING_INVALIDATOR, boost::bind (&Push2::notify_parameter_changed, this, _1), this); // receive rude solo changed session->SoloActive.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&Push2::notify_solo_active_changed, this, _1), this); } void Push2::notify_record_state_changed () { IDButtonMap::iterator b = id_button_map.find (RecordEnable); if (b == id_button_map.end()) { return; } switch (session->record_status ()) { case Session::Disabled: b->second->set_color (LED::White); b->second->set_state (LED::NoTransition); break; case Session::Enabled: b->second->set_color (LED::Red); b->second->set_state (LED::Blinking4th); break; case Session::Recording: b->second->set_color (LED::Red); b->second->set_state (LED::OneShot24th); break; } write (b->second->state_msg()); } void Push2::notify_transport_state_changed () { Button* b = id_button_map[Play]; if (session->transport_rolling()) { b->set_state (LED::OneShot24th); b->set_color (LED::Green); } else { /* disable any blink on FixedLength from pending edit range op */ Button* fl = id_button_map[FixedLength]; fl->set_color (LED::Black); fl->set_state (LED::NoTransition); write (fl->state_msg()); b->set_color (LED::White); b->set_state (LED::NoTransition); } write (b->state_msg()); } void Push2::notify_loop_state_changed () { } void Push2::notify_parameter_changed (std::string param) { IDButtonMap::iterator b; if (param == "clicking") { if ((b = id_button_map.find (Metronome)) == id_button_map.end()) { return; } if (Config->get_clicking()) { b->second->set_state (LED::Blinking4th); b->second->set_color (LED::White); } else { b->second->set_color (LED::White); b->second->set_state (LED::NoTransition); } write (b->second->state_msg ()); } } void Push2::notify_solo_active_changed (bool yn) { IDButtonMap::iterator b = id_button_map.find (Solo); if (b == id_button_map.end()) { return; } if (yn) { b->second->set_state (LED::Blinking4th); b->second->set_color (LED::Red); } else { b->second->set_state (LED::NoTransition); b->second->set_color (LED::White); } write (b->second->state_msg()); } XMLNode& Push2::get_state() { XMLNode& node (ControlProtocol::get_state()); XMLNode* child; child = new XMLNode (X_("Input")); child->add_child_nocopy (_async_in->get_state()); node.add_child_nocopy (*child); child = new XMLNode (X_("Output")); child->add_child_nocopy (_async_out->get_state()); node.add_child_nocopy (*child); node.add_property (X_("root"), to_string (_scale_root, std::dec)); node.add_property (X_("root_octave"), to_string (_root_octave, std::dec)); node.add_property (X_("in_key"), _in_key ? X_("yes") : X_("no")); node.add_property (X_("mode"), enum_2_string (_mode)); return node; } int Push2::set_state (const XMLNode & node, int version) { DEBUG_TRACE (DEBUG::Push2, string_compose ("Push2::set_state: active %1\n", active())); int retval = 0; if (ControlProtocol::set_state (node, version)) { return -1; } XMLNode* child; if ((child = node.child (X_("Input"))) != 0) { XMLNode* portnode = child->child (Port::state_node_name.c_str()); if (portnode) { _async_in->set_state (*portnode, version); } } if ((child = node.child (X_("Output"))) != 0) { XMLNode* portnode = child->child (Port::state_node_name.c_str()); if (portnode) { _async_out->set_state (*portnode, version); } } XMLProperty const* prop; if ((prop = node.property (X_("root"))) != 0) { _scale_root = atoi (prop->value()); } if ((prop = node.property (X_("root_octave"))) != 0) { _root_octave = atoi (prop->value()); } if ((prop = node.property (X_("in_key"))) != 0) { _in_key = string_is_affirmative (prop->value()); } if ((prop = node.property (X_("mode"))) != 0) { _mode = (MusicalMode::Type) string_2_enum (prop->value(), _mode); } return retval; } void Push2::switch_bank (uint32_t base) { if (!session) { return; } stripable_connections.drop_connections (); /* try to get the first stripable for the requested bank */ stripable[0] = session->get_remote_nth_stripable (base, PresentationInfo::Flag (PresentationInfo::Route|PresentationInfo::VCA)); if (!stripable[0]) { return; } /* at least one stripable in this bank */ bank_start = base; stripable[1] = session->get_remote_nth_stripable (base+1, PresentationInfo::Flag (PresentationInfo::Route|PresentationInfo::VCA)); stripable[2] = session->get_remote_nth_stripable (base+2, PresentationInfo::Flag (PresentationInfo::Route|PresentationInfo::VCA)); stripable[3] = session->get_remote_nth_stripable (base+3, PresentationInfo::Flag (PresentationInfo::Route|PresentationInfo::VCA)); stripable[4] = session->get_remote_nth_stripable (base+4, PresentationInfo::Flag (PresentationInfo::Route|PresentationInfo::VCA)); stripable[5] = session->get_remote_nth_stripable (base+5, PresentationInfo::Flag (PresentationInfo::Route|PresentationInfo::VCA)); stripable[6] = session->get_remote_nth_stripable (base+6, PresentationInfo::Flag (PresentationInfo::Route|PresentationInfo::VCA)); stripable[7] = session->get_remote_nth_stripable (base+7, PresentationInfo::Flag (PresentationInfo::Route|PresentationInfo::VCA)); for (int n = 0; n < 8; ++n) { if (!stripable[n]) { continue; } /* stripable goes away? refill the bank, starting at the same point */ stripable[n]->DropReferences.connect (stripable_connections, MISSING_INVALIDATOR, boost::bind (&Push2::switch_bank, this, bank_start), this); boost::shared_ptr sc = stripable[n]->solo_control(); if (sc) { sc->Changed.connect (stripable_connections, MISSING_INVALIDATOR, boost::bind (&Push2::solo_change, this, n), this); } boost::shared_ptr mc = stripable[n]->mute_control(); if (mc) { mc->Changed.connect (stripable_connections, MISSING_INVALIDATOR, boost::bind (&Push2::mute_change, this, n), this); } stripable[n]->presentation_info().PropertyChanged.connect (stripable_connections, MISSING_INVALIDATOR, boost::bind (&Push2::stripable_property_change, this, _1, n), this); solo_change (n); mute_change (n); } /* master cannot be removed, so no need to connect to going-away signal */ master = session->master_out (); } void Push2::stripable_property_change (PropertyChange const& what_changed, int which) { if (what_changed.contains (Properties::selected)) { if (!stripable[which]) { return; } /* cancel string, which will cause a redraw on the next update * cycle. The redraw will reflect selected status */ mid_layout[which]->set_text (string()); } } void Push2::stripable_selection_change (StripableNotificationListPtr selected) { boost::shared_ptr pad_port = boost::dynamic_pointer_cast(_async_in)->shadow_port(); boost::shared_ptr current_midi_track = current_pad_target.lock(); boost::shared_ptr new_pad_target; /* See if there's a MIDI track selected */ for (StripableNotificationList::iterator si = selected->begin(); si != selected->end(); ++si) { new_pad_target = boost::dynamic_pointer_cast ((*si).lock()); if (new_pad_target) { break; } } if (new_pad_target) { cerr << "new midi pad target " << new_pad_target->name() << endl; } else { cerr << "no midi pad target\n"; } if (current_midi_track == new_pad_target) { /* nothing to do */ return; } if (!new_pad_target) { /* leave existing connection alone */ return; } /* disconnect from pad port, if appropriate */ if (current_midi_track && pad_port) { cerr << "Disconnect pads from " << current_midi_track->name() << endl; current_midi_track->input()->disconnect (current_midi_track->input()->nth(0), pad_port->name(), this); } /* now connect the pad port to this (newly) selected midi * track, if indeed there is one. */ if (new_pad_target && pad_port) { cerr << "Reconnect pads to " << new_pad_target->name() << endl; new_pad_target->input()->connect (new_pad_target->input()->nth (0), pad_port->name(), this); current_pad_target = new_pad_target; } else { current_pad_target.reset (); } } void Push2::solo_change (int n) { ButtonID bid; switch (n) { case 0: bid = Upper1; break; case 1: bid = Upper2; break; case 2: bid = Upper3; break; case 3: bid = Upper4; break; case 4: bid = Upper5; break; case 5: bid = Upper6; break; case 6: bid = Upper7; break; case 7: bid = Upper8; break; default: return; } boost::shared_ptr ac = stripable[n]->solo_control (); if (!ac) { return; } Button* b = id_button_map[bid]; if (ac->soloed()) { b->set_color (LED::Green); } else { b->set_color (LED::Black); } if (ac->soloed_by_others_upstream() || ac->soloed_by_others_downstream()) { b->set_state (LED::Blinking4th); } else { b->set_state (LED::OneShot24th); } write (b->state_msg()); } void Push2::mute_change (int n) { ButtonID bid; if (!stripable[n]) { return; } cerr << "Mute changed on " << n << ' ' << stripable[n]->name() << endl; switch (n) { case 0: bid = Lower1; break; case 1: bid = Lower2; break; case 2: bid = Lower3; break; case 3: bid = Lower4; break; case 4: bid = Lower5; break; case 5: bid = Lower6; break; case 6: bid = Lower7; break; case 7: bid = Lower8; break; default: return; } boost::shared_ptr mc = stripable[n]->mute_control (); if (!mc) { return; } Button* b = id_button_map[bid]; if (Config->get_show_solo_mutes() && !Config->get_solo_control_is_listen_control ()) { if (mc->muted_by_self ()) { /* full mute */ b->set_color (LED::Blue); b->set_state (LED::OneShot24th); cerr << "FULL MUTE1\n"; } else if (mc->muted_by_others_soloing () || mc->muted_by_masters ()) { /* this will reflect both solo mutes AND master mutes */ b->set_color (LED::Blue); b->set_state (LED::Blinking4th); cerr << "OTHER MUTE1\n"; } else { /* no mute at all */ b->set_color (LED::Black); b->set_state (LED::OneShot24th); cerr << "NO MUTE1\n"; } } else { if (mc->muted_by_self()) { /* full mute */ b->set_color (LED::Blue); b->set_state (LED::OneShot24th); cerr << "FULL MUTE2\n"; } else if (mc->muted_by_masters ()) { /* this shows only master mutes, not mute-by-others-soloing */ b->set_color (LED::Blue); b->set_state (LED::Blinking4th); cerr << "OTHER MUTE1\n"; } else { /* no mute at all */ b->set_color (LED::Black); b->set_state (LED::OneShot24th); cerr << "NO MUTE2\n"; } } write (b->state_msg()); } void Push2::strip_vpot (int n, int delta) { if (stripable[n]) { boost::shared_ptr ac = stripable[n]->gain_control(); if (ac) { ac->set_value (ac->get_value() + ((2.0/64.0) * delta), PBD::Controllable::UseGroup); } } } void Push2::strip_vpot_touch (int n, bool touching) { if (stripable[n]) { boost::shared_ptr ac = stripable[n]->gain_control(); if (ac) { if (touching) { ac->start_touch (session->audible_frame()); } else { ac->stop_touch (true, session->audible_frame()); } } } } void Push2::other_vpot (int n, int delta) { switch (n) { case 0: break; case 1: break; case 2: /* master gain control */ if (master) { boost::shared_ptr ac = master->gain_control(); if (ac) { ac->set_value (ac->get_value() + ((2.0/64.0) * delta), PBD::Controllable::UseGroup); } } break; } } void Push2::other_vpot_touch (int n, bool touching) { switch (n) { case 0: break; case 1: break; case 2: if (master) { boost::shared_ptr ac = master->gain_control(); if (ac) { if (touching) { ac->start_touch (session->audible_frame()); } else { ac->stop_touch (true, session->audible_frame()); } } } } } void Push2::start_shift () { cerr << "start shift\n"; modifier_state = ModifierState (modifier_state | ModShift); Button* b = id_button_map[Shift]; b->set_color (LED::White); b->set_state (LED::Blinking16th); write (b->state_msg()); } void Push2::end_shift () { if (modifier_state & ModShift) { cerr << "end shift\n"; modifier_state = ModifierState (modifier_state & ~(ModShift)); Button* b = id_button_map[Shift]; b->timeout_connection.disconnect (); b->set_color (LED::White); b->set_state (LED::OneShot24th); write (b->state_msg()); } } void Push2::start_select () { cerr << "start select\n"; modifier_state = ModifierState (modifier_state | ModSelect); Button* b = id_button_map[Select]; b->set_color (LED::White); b->set_state (LED::Blinking16th); write (b->state_msg()); } void Push2::end_select () { if (modifier_state & ModSelect) { cerr << "end select\n"; modifier_state = ModifierState (modifier_state & ~(ModSelect)); Button* b = id_button_map[Select]; b->timeout_connection.disconnect (); b->set_color (LED::White); b->set_state (LED::OneShot24th); write (b->state_msg()); } } void Push2::splash () { std::string splash_file; Searchpath rc (ARDOUR::ardour_data_search_path()); rc.add_subdirectory_to_paths ("resources"); if (!find_file (rc, PROGRAM_NAME "-splash.png", splash_file)) { cerr << "Cannot find splash screen image file\n"; throw failed_constructor(); } Cairo::RefPtr img = Cairo::ImageSurface::create_from_png (splash_file); double x_ratio = (double) img->get_width() / (cols - 20); double y_ratio = (double) img->get_height() / (rows - 20); double scale = min (x_ratio, y_ratio); /* background */ context->set_source_rgb (0.764, 0.882, 0.882); context->paint (); /* image */ context->save (); context->translate (5, 5); context->scale (scale, scale); context->set_source (img, 0, 0); context->paint (); context->restore (); /* text */ Glib::RefPtr some_text = Pango::Layout::create (context); Pango::FontDescription fd ("Sans 38"); some_text->set_font_description (fd); some_text->set_text (string_compose ("%1 %2", PROGRAM_NAME, VERSIONSTRING)); context->move_to (200, 10); context->set_source_rgb (0, 0, 0); some_text->update_from_cairo_context (context); some_text->show_in_cairo_context (context); Pango::FontDescription fd2 ("Sans Italic 18"); some_text->set_font_description (fd2); some_text->set_text (_("Ableton Push 2 Support")); context->move_to (200, 80); context->set_source_rgb (0, 0, 0); some_text->update_from_cairo_context (context); some_text->show_in_cairo_context (context); splash_start = get_microseconds (); blit_to_device_frame_buffer (); } bool Push2::pad_filter (MidiBuffer& in, MidiBuffer& out) const { /* This filter is called asynchronously from a realtime process context. It must use atomics to check state, and must not block. */ bool matched = false; for (MidiBuffer::iterator ev = in.begin(); ev != in.end(); ++ev) { if ((*ev).is_note_on() || (*ev).is_note_off()) { /* encoder touch start/touch end use note * 0-10. touchstrip uses note 12 */ if ((*ev).note() > 10 && (*ev).note() != 12) { const int n = (*ev).note (); NNPadMap::const_iterator nni = nn_pad_map.find (n); if (nni != nn_pad_map.end()) { Pad const * pad = nni->second; /* shift for output to the shadow port */ if (pad->filtered >= 0) { (*ev).set_note (pad->filtered); out.push_back (*ev); /* shift back so that the pads light correctly */ (*ev).set_note (n); } else { /* no mapping, don't send event */ } } else { out.push_back (*ev); } matched = true; } } } return matched; } bool Push2::connection_handler (boost::weak_ptr, std::string name1, boost::weak_ptr, std::string name2, bool yn) { DEBUG_TRACE (DEBUG::FaderPort, "FaderPort::connection_handler start\n"); if (!_input_port || !_output_port) { return false; } string ni = ARDOUR::AudioEngine::instance()->make_port_name_non_relative (boost::shared_ptr(_async_in)->name()); string no = ARDOUR::AudioEngine::instance()->make_port_name_non_relative (boost::shared_ptr(_async_out)->name()); if (ni == name1 || ni == name2) { if (yn) { connection_state |= InputConnected; } else { connection_state &= ~InputConnected; } } else if (no == name1 || no == name2) { if (yn) { connection_state |= OutputConnected; } else { connection_state &= ~OutputConnected; } } else { DEBUG_TRACE (DEBUG::FaderPort, string_compose ("Connections between %1 and %2 changed, but I ignored it\n", name1, name2)); /* not our ports */ return false; } if ((connection_state & (InputConnected|OutputConnected)) == (InputConnected|OutputConnected)) { /* XXX this is a horrible hack. Without a short sleep here, something prevents the device wakeup messages from being sent and/or the responses from being received. */ g_usleep (100000); DEBUG_TRACE (DEBUG::FaderPort, "device now connected for both input and output\n"); // connected (); } else { DEBUG_TRACE (DEBUG::FaderPort, "Device disconnected (input or output or both) or not yet fully connected\n"); } ConnectionChange (); /* emit signal for our GUI */ DEBUG_TRACE (DEBUG::FaderPort, "FaderPort::connection_handler end\n"); return true; /* connection status changed */ } boost::shared_ptr Push2::output_port() { return _async_out; } boost::shared_ptr Push2::input_port() { return _async_in; } void Push2::build_pad_table () { for (int n = 36; n < 100; ++n) { pad_map[n] = n + (octave_shift*12); } PadChange (); /* emit signal */ } int Push2::pad_note (int row, int col) const { NNPadMap::const_iterator nni = nn_pad_map.find (36+(row*8)+col); if (nni != nn_pad_map.end()) { return nni->second->filtered; } return 0; } void Push2::set_pad_scale (int root, int octave, MusicalMode::Type mode, bool inkey) { MusicalMode m (mode); vector::iterator interval; int note; const int original_root = root; interval = m.steps.begin(); root += (octave*12); note = root; const int root_start = root; set mode_map; /* contains only notes in mode, O(logN) lookup */ vector mode_vector; /* sorted in note order */ mode_map.insert (note); mode_vector.push_back (note); /* build a map of all notes in the mode, from the root to 127 */ while (note < 128) { if (interval == m.steps.end()) { /* last distance was the end of the scale, so wrap, adding the next note at one octave above the last root. */ interval = m.steps.begin(); root += 12; mode_map.insert (root); mode_vector.push_back (root); } else { note = (int) floor (root + (2.0 * (*interval))); interval++; mode_map.insert (note); mode_vector.push_back (note); } } if (inkey) { vector::iterator notei; int row_offset = 0; for (int row = 0; row < 8; ++row) { /* Ableton's grid layout wraps the available notes in the scale * by offsetting 3 notes per row (from the bottom) */ notei = mode_vector.begin(); notei += row_offset; row_offset += 3; for (int col = 0; col < 8; ++col) { int index = 36 + (row*8) + col; Pad* pad = nn_pad_map[index]; int notenum; if (notei != mode_vector.end()) { notenum = *notei; pad->filtered = notenum; if ((notenum % 12) == original_root) { pad->set_color (LED::Green); pad->perma_color = LED::Green; } else { pad->set_color (LED::White); pad->perma_color = LED::White; } pad->do_when_pressed = Pad::FlashOff; notei++; } else { pad->set_color (LED::Black); pad->do_when_pressed = Pad::Nothing; pad->filtered = -1; } write (pad->state_msg()); } } } else { /* chromatic: all notes available, but highlight those in the scale */ for (note = 36; note < 100; ++note) { Pad* pad = nn_pad_map[note]; /* Chromatic: all pads play, half-tone steps. Light * those in the scale, and highlight root notes */ pad->filtered = root_start + (note - 36); if (mode_map.find (note) != mode_map.end()) { if ((note % 12) == original_root) { pad->set_color (LED::Green); pad->perma_color = LED::Green; } else { pad->set_color (LED::White); pad->perma_color = LED::White; } pad->do_when_pressed = Pad::FlashOff; } else { /* note is not in mode, turn it off */ pad->do_when_pressed = Pad::FlashOn; pad->set_color (LED::Black); } write (pad->state_msg()); } } PadChange (); /* EMIT SIGNAL */ /* store state */ _scale_root = original_root; _root_octave = octave; _in_key = inkey; _mode = mode; }