X-Git-Url: https://main.carlh.net/gitweb/?a=blobdiff_plain;f=libs%2Fsurfaces%2Fpush2%2Fpush2.cc;h=30b521686cfdc80bb21cfe1f4a41c35ee580b1f7;hb=df2b0e33b913f842ed9964cce4c870c45cd02b38;hp=a293c58c6456e1708c1c47d2dd97115ac98ba120;hpb=c4602603b4a229ab7d017f0733a9b771179b4319;p=ardour.git diff --git a/libs/surfaces/push2/push2.cc b/libs/surfaces/push2/push2.cc index a293c58c64..30b521686c 100644 --- a/libs/surfaces/push2/push2.cc +++ b/libs/surfaces/push2/push2.cc @@ -16,6 +16,9 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ +#include +#include + #include "pbd/compose.h" #include "pbd/convert.h" #include "pbd/debug.h" @@ -28,21 +31,37 @@ #include "timecode/time.h" #include "timecode/bbt_time.h" +#include "ardour/amp.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 "ardour/types_convert.h" -#include "push2.h" +#include "gtkmm2ext/gui_thread.h" +#include "gtkmm2ext/rgb_macros.h" + +#include "canvas/colors.h" + +#include "canvas.h" #include "gui.h" +#include "layout.h" #include "menu.h" +#include "mix.h" +#include "push2.h" +#include "scale.h" +#include "splash.h" +#include "track_mix.h" + +#include "pbd/i18n.h" -#include "i18n.h" +#ifdef PLATFORM_WINDOWS +#define random() rand() +#endif using namespace ARDOUR; using namespace std; @@ -52,76 +71,18 @@ using namespace ArdourSurface; #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)) + , in_use (false) , _modifier_state (None) , splash_start (0) , _current_layout (0) - , drawn_layout (0) + , _previous_layout (0) , connection_state (ConnectionState (0)) , gui (0) , _mode (MusicalMode::IonianMajor) @@ -130,26 +91,37 @@ Push2::Push2 (ARDOUR::Session& s) , _in_key (true) , octave_shift (0) , percussion (false) + , _pressure_mode (AfterTouch) + , selection_color (LED::Green) + , contrast_color (LED::Green) + , in_range_select (false) { - context = Cairo::Context::create (frame_buffer); + /* we're going to need this */ + + libusb_init (NULL); - build_pad_table (); build_maps (); + build_color_map (); + fill_color_table (); /* master cannot be removed, so no need to connect to going-away signal */ master = session->master_out (); - if (open ()) { - throw failed_constructor (); - } - ControlProtocol::StripableSelectionChanged.connect (selection_connection, MISSING_INVALIDATOR, boost::bind (&Push2::stripable_selection_change, this, _1), this); - /* catch current selection, if any */ - { - StripableNotificationListPtr sp (new StripableNotificationList (ControlProtocol::last_selected())); - stripable_selection_change (sp); - } + /* allocate graphics layouts, even though we're not using them yet */ + + _canvas = new Push2Canvas (*this, 960, 160); + mix_layout = new MixLayout (*this, *session, "globalmix"); + scale_layout = new ScaleLayout (*this, *session, "scale"); + track_mix_layout = new TrackMixLayout (*this, *session, "trackmix"); + splash_layout = new SplashLayout (*this, *session, "splash"); + + run_event_loop (); + + /* Ports exist for the life of this instance */ + + ports_acquire (); /* 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); @@ -157,70 +129,113 @@ Push2::Push2 (ARDOUR::Session& s) /* 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 */ + /* Push 2 ports might already be there */ port_registration_handler (); } Push2::~Push2 () { - stop (); + DEBUG_TRACE (DEBUG::Push2, "push2 control surface object being destroyed\n"); + + /* do this before stopping the event loop, so that we don't get any notifications */ + selection_connection.disconnect (); + port_reg_connection.disconnect (); + port_connection.disconnect (); + + stop_using_device (); + device_release (); + ports_release (); + + if (_current_layout) { + _canvas->root()->remove (_current_layout); + _current_layout = 0; + } + + delete mix_layout; + mix_layout = 0; + delete scale_layout; + scale_layout = 0; + delete splash_layout; + splash_layout = 0; + delete track_mix_layout; + track_mix_layout = 0; + + stop_event_loop (); } + void -Push2::port_registration_handler () +Push2::run_event_loop () { - if (_async_in->connected() && _async_out->connected()) { - /* don't waste cycles here */ - return; - } + DEBUG_TRACE (DEBUG::Push2, "start event loop\n"); + BaseUI::run (); +} - 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; +void +Push2::stop_event_loop () +{ + DEBUG_TRACE (DEBUG::Push2, "stop event loop\n"); + BaseUI::quit (); +} - AudioEngine::instance()->get_ports (string_compose (".*%1", input_port_name), DataType::MIDI, PortFlags (IsPhysical|IsOutput), in); - AudioEngine::instance()->get_ports (string_compose (".*%1", output_port_name), DataType::MIDI, PortFlags (IsPhysical|IsInput), out); +int +Push2::begin_using_device () +{ + DEBUG_TRACE (DEBUG::Push2, "begin using device\n"); - 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()); - } + /* 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()); + + connect_session_signals (); + + init_buttons (true); + init_touch_strip (); + set_pad_scale (_scale_root, _root_octave, _mode, _in_key); + splash (); + + /* catch current selection, if any so that we can wire up the pads if appropriate */ + { + StripableNotificationListPtr sp (new StripableNotificationList (ControlProtocol::last_selected())); + stripable_selection_change (sp); } + + request_pressure_mode (); + + in_use = true; + + return 0; } int -Push2::open () +Push2::stop_using_device () { - int err; + DEBUG_TRACE (DEBUG::Push2, "stop using device\n"); - if (handle) { - /* already open */ + if (!in_use) { + DEBUG_TRACE (DEBUG::Push2, "nothing to do, device not in use\n"); 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; - } + init_buttons (false); + strip_buttons_off (); - device_frame_buffer = new uint16_t[rows*pixels_per_row]; + vblank_connection.disconnect (); + session_connections.drop_connections (); - memset (device_frame_buffer, 0, sizeof (uint16_t) * rows * pixels_per_row); + in_use = false; + return 0; +} - frame_header[0] = 0xef; - frame_header[1] = 0xcd; - frame_header[2] = 0xab; - frame_header[3] = 0x89; - memset (&frame_header[4], 0, 12); +int +Push2::ports_acquire () +{ + DEBUG_TRACE (DEBUG::Push2, "acquiring ports\n"); /* setup ports */ @@ -228,12 +243,13 @@ Push2::open () _async_out = AudioEngine::instance()->register_output_port (DataType::MIDI, X_("Push 2 out"), true); if (_async_in == 0 || _async_out == 0) { + DEBUG_TRACE (DEBUG::Push2, "cannot register ports\n"); 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. + * really insist on that (and use JACK) */ _input_port = boost::dynamic_pointer_cast(_async_in).get(); @@ -262,29 +278,21 @@ Push2::open () connect_to_parser (); - mix_layout = new MixLayout (*this, *session, context); - scale_layout = new ScaleLayout (*this, *session, context); - _current_layout = mix_layout; + /* Connect input port to event loop */ - return 0; -} + AsyncMIDIPort* asp; -list > -Push2::bundles () -{ - list > b; + 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()); - if (_output_bundle) { - b.push_back (_output_bundle); - } - - return b; + return 0; } -int -Push2::close () +void +Push2::ports_release () { - init_buttons (false); + DEBUG_TRACE (DEBUG::Push2, "releasing ports\n"); /* wait for button data to be flushed */ AsyncMIDIPort* asp; @@ -298,30 +306,75 @@ Push2::close () _async_out.reset ((ARDOUR::Port*) 0); _input_port = 0; _output_port = 0; +} - vblank_connection.disconnect (); - periodic_connection.disconnect (); - session_connections.drop_connections (); +int +Push2::device_acquire () +{ + int err; - _current_layout = 0; - drawn_layout = 0; - delete mix_layout; - mix_layout = 0; - delete scale_layout; - scale_layout = 0; + DEBUG_TRACE (DEBUG::Push2, "acquiring device\n"); + if (handle) { + DEBUG_TRACE (DEBUG::Push2, "open() called with handle already set\n"); + /* already open */ + return 0; + } + + if ((handle = libusb_open_device_with_vid_pid (NULL, ABLETON, PUSH2)) == 0) { + DEBUG_TRACE (DEBUG::Push2, "failed to open USB handle\n"); + return -1; + } + + if ((err = libusb_claim_interface (handle, 0x00))) { + DEBUG_TRACE (DEBUG::Push2, "failed to claim USB device\n"); + libusb_close (handle); + handle = 0; + return -1; + } + + return 0; +} + +void +Push2::device_release () +{ + DEBUG_TRACE (DEBUG::Push2, "releasing device\n"); if (handle) { libusb_release_interface (handle, 0x00); libusb_close (handle); handle = 0; } +} - delete [] device_frame_buffer; - device_frame_buffer = 0; +list > +Push2::bundles () +{ + list > b; - return 0; + if (_output_bundle) { + b.push_back (_output_bundle); + } + + return b; } +void +Push2::strip_buttons_off () +{ + 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()); + } +} + + void Push2::init_buttons (bool startup) { @@ -330,7 +383,7 @@ Push2::init_buttons (bool startup) */ 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, + Metronome, Shift, Select, Play, RecordEnable, Automate, Repeat, Note, Session, Quantize, Duplicate, Browse, PageRight, PageLeft, OctaveUp, OctaveDown, Layout, Scale }; @@ -346,21 +399,6 @@ Push2::init_buttons (bool startup) 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) */ @@ -392,16 +430,6 @@ Push2::init_buttons (bool startup) 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; } @@ -419,132 +447,43 @@ Push2::request_factory (uint32_t 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 (); + stop_using_device (); } } -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 () +void +Push2::splash () { - /* 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; + set_current_layout (splash_layout); + splash_start = get_microseconds (); } bool -Push2::redraw () +Push2::vblank () { if (splash_start) { - /* display splash for 3 seconds */ + /* display splash for 2 seconds */ - if (get_microseconds() - splash_start > 3000000) { + if (get_microseconds() - splash_start > 2000000) { splash_start = 0; - } else { - return false; + DEBUG_TRACE (DEBUG::Push2, "splash interval ended, switch to mix layout\n"); + set_current_layout (mix_layout); } } - Glib::Threads::Mutex::Lock lm (layout_lock, Glib::Threads::TRY_LOCK); - - if (!lm.locked()) { - /* can't get layout, no re-render needed */ - return false; - } - - bool render_needed = false; - - if (drawn_layout != _current_layout) { - render_needed = true; + if (_current_layout) { + _current_layout->update_meters (); + _current_layout->update_clocks (); } - bool dirty = _current_layout->redraw (context); - drawn_layout = _current_layout; - - return dirty || render_needed; -} - -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; - } + _canvas->vblank(); return true; } @@ -560,50 +499,20 @@ Push2::set_active (bool yn) if (yn) { - /* start event loop */ - - BaseUI::run (); - - if (open ()) { - DEBUG_TRACE (DEBUG::Push2, "device open failed\n"); - close (); + if (device_acquire ()) { 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); - splash (); - + if ((connection_state & (InputConnected|OutputConnected)) == (InputConnected|OutputConnected)) { + begin_using_device (); + } else { + /* begin_using_device () will get called once we're connected */ + } } else { - - stop (); - + /* Control Protocol Manager never calls us with false, but + * insteads destroys us. + */ } ControlProtocol::set_active (yn); @@ -644,27 +553,23 @@ Push2::midi_input_handler (IOCondition ioc, MIDI::Port* port) if (ioc & IO_IN) { - // DEBUG_TRACE (DEBUG::Push2, string_compose ("something happend on %1\n", port->name())); + 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); + DEBUG_TRACE (DEBUG::Push2, string_compose ("data available on %1\n", port->name())); + if (in_use) { + framepos_t now = AudioEngine::instance()->sample_time(); + port->parse (now); + } } return true; } -bool -Push2::periodic () -{ - return true; -} - void Push2::connect_to_parser () { @@ -688,6 +593,31 @@ 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)); + + if (sz < 8) { + return; + } + + MidiByteArray msg (sz, raw_bytes); + MidiByteArray push2_sysex_header (6, 0xF0, 0x00, 0x21, 0x1D, 0x01, 0x01); + + if (!push2_sysex_header.compare_n (msg, 6)) { + return; + } + + switch (msg[6]) { + case 0x1f: /* pressure mode */ + if (msg[7] == 0x0) { + _pressure_mode = AfterTouch; + PressureModeChange (AfterTouch); + cerr << "Pressure mode is after\n"; + } else { + _pressure_mode = PolyPressure; + PressureModeChange (PolyPressure); + cerr << "Pressure mode is poly\n"; + } + break; + } } void @@ -786,7 +716,7 @@ Push2::handle_midi_controller_message (MIDI::Parser&, MIDI::EventTwoBytes* ev) 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)); + // 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); @@ -844,22 +774,26 @@ Push2::handle_midi_note_on_message (MIDI::Parser& parser, MIDI::EventTwoBytes* e return; } - /* Pad */ + /* Pad illuminations */ - NNPadMap::iterator pi = nn_pad_map.find (ev->note_number); + NNPadMap::const_iterator pm = nn_pad_map.find (ev->note_number); - if (pi == nn_pad_map.end()) { + if (pm == nn_pad_map.end()) { return; } - Pad* pad = pi->second; + const Pad * const pad_pressed = pm->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); + pair pads_with_note = fn_pad_map.equal_range (pad_pressed->filtered); + + if (pads_with_note.first == fn_pad_map.end()) { + return; + } + + for (FNPadMap::iterator pi = pads_with_note.first; pi != pads_with_note.second; ++pi) { + Pad* pad = pi->second; + + pad->set_color (contrast_color); pad->set_state (LED::OneShot24th); write (pad->state_msg()); } @@ -868,7 +802,7 @@ Push2::handle_midi_note_on_message (MIDI::Parser& parser, MIDI::EventTwoBytes* e 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)); + // 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 @@ -878,22 +812,34 @@ Push2::handle_midi_note_off_message (MIDI::Parser&, MIDI::EventTwoBytes* ev) return; } - NNPadMap::iterator pi = nn_pad_map.find (ev->note_number); + /* Pad illuminations */ + + NNPadMap::const_iterator pm = nn_pad_map.find (ev->note_number); - if (pi == nn_pad_map.end()) { + if (pm == nn_pad_map.end()) { return; } - Pad* pad = pi->second; + const Pad * const pad_pressed = pm->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()); + pair pads_with_note = fn_pad_map.equal_range (pad_pressed->filtered); + + if (pads_with_note.first == fn_pad_map.end()) { + return; + } + + for (FNPadMap::iterator pi = pads_with_note.first; pi != pads_with_note.second; ++pi) { + 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()); + } } } @@ -1049,10 +995,10 @@ Push2::get_state() 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)); + node.set_property (X_("root"), _scale_root); + node.set_property (X_("root-octave"), _root_octave); + node.set_property (X_("in-key"), _in_key); + node.set_property (X_("mode"), _mode); return node; } @@ -1084,23 +1030,10 @@ Push2::set_state (const XMLNode & node, int 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); - } + node.get_property (X_("root"), _scale_root); + node.get_property (X_("root-octave"), _root_octave); + node.get_property (X_("in-key"), _in_key); + node.get_property (X_("mode"), _mode); return retval; } @@ -1108,17 +1041,31 @@ Push2::set_state (const XMLNode & node, int version) void Push2::other_vpot (int n, int delta) { + boost::shared_ptr click_gain; switch (n) { case 0: + /* tempo control */ break; case 1: + /* metronome gain control */ + click_gain = session->click_gain(); + if (click_gain) { + boost::shared_ptr ac = click_gain->gain_control(); + if (ac) { + ac->set_value (ac->interface_to_internal ( + min (ac->upper(), max (ac->lower(), ac->internal_to_interface (ac->get_value()) + (delta/256.0)))), + PBD::Controllable::UseGroup); + } + } 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); + ac->set_value (ac->interface_to_internal ( + min (ac->upper(), max (ac->lower(), ac->internal_to_interface (ac->get_value()) + (delta/256.0)))), + PBD::Controllable::UseGroup); } } break; @@ -1172,65 +1119,6 @@ Push2::end_shift () } } -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 { @@ -1269,7 +1157,7 @@ Push2::pad_filter (MidiBuffer& in, MidiBuffer& out) const matched = true; } - } else if ((*ev).is_pitch_bender() || (*ev).is_aftertouch() || (*ev).is_channel_pressure()) { + } else if ((*ev).is_pitch_bender() || (*ev).is_poly_pressure() || (*ev).is_channel_pressure()) { out.push_back (*ev); } } @@ -1277,10 +1165,51 @@ Push2::pad_filter (MidiBuffer& in, MidiBuffer& out) const return matched; } +void +Push2::port_registration_handler () +{ + if (!_async_in && !_async_out) { + /* ports not registered yet */ + return; + } + + if (_async_in->connected() && _async_out->connected()) { + /* don't waste cycles here */ + return; + } + +#ifdef __APPLE__ + /* the origin of the numeric magic identifiers is known only to Ableton + and may change in time. This is part of how CoreMIDI works. + */ + string input_port_name = X_("system:midi_capture_1319078870"); + string output_port_name = X_("system:midi_playback_3409210341"); +#else + string input_port_name = X_("Ableton Push 2 MIDI 1 in"); + string output_port_name = X_("Ableton Push 2 MIDI 1 out"); +#endif + vector in; + vector out; + + AudioEngine::instance()->get_ports (string_compose (".*%1", input_port_name), DataType::MIDI, PortFlags (IsPhysical|IsOutput), in); + AudioEngine::instance()->get_ports (string_compose (".*%1", output_port_name), DataType::MIDI, PortFlags (IsPhysical|IsInput), 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()); + } + } +} + 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"); + DEBUG_TRACE (DEBUG::FaderPort, "FaderPort::connection_handler start\n"); if (!_input_port || !_output_port) { return false; } @@ -1301,11 +1230,14 @@ Push2::connection_handler (boost::weak_ptr, std::string name1, boo connection_state &= ~OutputConnected; } } else { - DEBUG_TRACE (DEBUG::FaderPort, string_compose ("Connections between %1 and %2 changed, but I ignored it\n", name1, name2)); + DEBUG_TRACE (DEBUG::Push2, string_compose ("Connections between %1 and %2 changed, but I ignored it\n", name1, name2)); /* not our ports */ return false; } + DEBUG_TRACE (DEBUG::Push2, string_compose ("our ports changed connection state: %1 -> %2 connected ? %3\n", + name1, name2, yn)); + if ((connection_state & (InputConnected|OutputConnected)) == (InputConnected|OutputConnected)) { /* XXX this is a horrible hack. Without a short sleep here, @@ -1314,11 +1246,19 @@ Push2::connection_handler (boost::weak_ptr, std::string name1, boo */ g_usleep (100000); - DEBUG_TRACE (DEBUG::FaderPort, "device now connected for both input and output\n"); - // connected (); + DEBUG_TRACE (DEBUG::Push2, "device now connected for both input and output\n"); + + /* may not have the device open if it was just plugged + in. Really need USB device detection rather than MIDI port + detection for this to work well. + */ + + device_acquire (); + begin_using_device (); } else { DEBUG_TRACE (DEBUG::FaderPort, "Device disconnected (input or output or both) or not yet fully connected\n"); + stop_using_device (); } ConnectionChange (); /* emit signal for our GUI */ @@ -1340,16 +1280,6 @@ 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 { @@ -1362,6 +1292,27 @@ Push2::pad_note (int row, int col) const return 0; } +void +Push2::update_selection_color () +{ + boost::shared_ptr current_midi_track = current_pad_target.lock(); + + if (!current_midi_track) { + return; + } + + selection_color = get_color_index (current_midi_track->presentation_info().color()); + contrast_color = get_color_index (ArdourCanvas::HSV (current_midi_track->presentation_info().color()).opposite().color()); + + reset_pad_colors (); +} + +void +Push2::reset_pad_colors () +{ + set_pad_scale (_scale_root, _root_octave, _mode, _in_key); +} + void Push2::set_pad_scale (int root, int octave, MusicalMode::Type mode, bool inkey) { @@ -1406,10 +1357,13 @@ Push2::set_pad_scale (int root, int octave, MusicalMode::Type mode, bool inkey) } } + fn_pad_map.clear (); + 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 @@ -1429,9 +1383,11 @@ Push2::set_pad_scale (int root, int octave, MusicalMode::Type mode, bool inkey) notenum = *notei; pad->filtered = notenum; + fn_pad_map.insert (make_pair (notenum, pad)); + if ((notenum % 12) == original_root) { - pad->set_color (LED::Green); - pad->perma_color = LED::Green; + pad->set_color (selection_color); + pad->perma_color = selection_color; } else { pad->set_color (LED::White); pad->perma_color = LED::White; @@ -1447,6 +1403,7 @@ Push2::set_pad_scale (int root, int octave, MusicalMode::Type mode, bool inkey) pad->filtered = -1; } + pad->set_state (LED::OneShot24th); write (pad->state_msg()); } } @@ -1465,11 +1422,13 @@ Push2::set_pad_scale (int root, int octave, MusicalMode::Type mode, bool inkey) pad->filtered = root_start + (note - 36); + fn_pad_map.insert (make_pair (pad->filtered, pad)); + if (mode_map.find (note) != mode_map.end()) { if ((note % 12) == original_root) { - pad->set_color (LED::Green); - pad->perma_color = LED::Green; + pad->set_color (selection_color); + pad->perma_color = selection_color; } else { pad->set_color (LED::White); pad->perma_color = LED::White; @@ -1486,18 +1445,35 @@ Push2::set_pad_scale (int root, int octave, MusicalMode::Type mode, bool inkey) } + pad->set_state (LED::OneShot24th); write (pad->state_msg()); } } - PadChange (); /* EMIT SIGNAL */ - /* store state */ - _scale_root = original_root; - _root_octave = octave; - _in_key = inkey; - _mode = mode; + bool changed = false; + + if (_scale_root != original_root) { + _scale_root = original_root; + changed = true; + } + if (_root_octave != octave) { + _root_octave = octave; + changed = true; + } + if (_in_key != inkey) { + _in_key = inkey; + changed = true; + } + if (_mode != mode) { + _mode = mode; + changed = true; + } + + if (changed) { + ScaleChange (); /* EMIT SIGNAL */ + } } void @@ -1512,6 +1488,8 @@ Push2::set_percussive_mode (bool yn) int drum_note = 36; + fn_pad_map.clear (); + for (int row = 0; row < 8; ++row) { for (int col = 0; col < 4; ++col) { @@ -1537,8 +1515,6 @@ Push2::set_percussive_mode (bool yn) } percussion = true; - - PadChange (); /* EMIT SIGNAL */ } Push2Layout* @@ -1566,12 +1542,6 @@ Push2::stripable_selection_change (StripableNotificationListPtr selected) } } - 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; @@ -1585,7 +1555,14 @@ Push2::stripable_selection_change (StripableNotificationListPtr selected) /* disconnect from pad port, if appropriate */ if (current_midi_track && pad_port) { - cerr << "Disconnect pads from " << current_midi_track->name() << endl; + + /* XXX this could possibly leave dangling MIDI notes. + * + * A general libardour fix is required. It isn't obvious + * how note resolution can be done unless disconnecting + * becomes "slow" (i.e. deferred for as long as it takes + * to resolve notes). + */ current_midi_track->input()->disconnect (current_midi_track->input()->nth(0), pad_port->name(), this); } @@ -1594,12 +1571,17 @@ Push2::stripable_selection_change (StripableNotificationListPtr selected) */ 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; + selection_color = get_color_index (new_pad_target->presentation_info().color()); + contrast_color = get_color_index (ArdourCanvas::HSV (new_pad_target->presentation_info().color()).opposite().color()); } else { current_pad_target.reset (); + selection_color = LED::Green; + contrast_color = LED::Green; } + + reset_pad_colors (); } Push2::Button* @@ -1607,3 +1589,179 @@ Push2::button_by_id (ButtonID bid) { return id_button_map[bid]; } + +uint8_t +Push2::get_color_index (ArdourCanvas::Color rgba) +{ + ColorMap::iterator i = color_map.find (rgba); + + if (i != color_map.end()) { + return i->second; + } + + double dr, dg, db, da; + int r, g, b; + ArdourCanvas::color_to_rgba (rgba, dr, dg, db, da); + int w = 126; /* not sure where/when we should get this value */ + + + r = (int) floor (255.0 * dr); + g = (int) floor (255.0 * dg); + b = (int) floor (255.0 * db); + + /* get a free index */ + + uint8_t index; + + if (color_map_free_list.empty()) { + /* random replacement of any entry above zero and below 122 (where the + * Ableton standard colors live) + */ + index = 1 + (random() % 121); + } else { + index = color_map_free_list.top(); + color_map_free_list.pop(); + } + + MidiByteArray palette_msg (17, + 0xf0, + 0x00 , 0x21, 0x1d, 0x01, 0x01, 0x03, /* reset palette header */ + 0x00, /* index = 7 */ + 0x00, 0x00, /* r = 8 & 9 */ + 0x00, 0x00, /* g = 10 & 11 */ + 0x00, 0x00, /* b = 12 & 13 */ + 0x00, 0x00, /* w (a?) = 14 & 15*/ + 0xf7); + palette_msg[7] = index; + palette_msg[8] = r & 0x7f; + palette_msg[9] = (r & 0x80) >> 7; + palette_msg[10] = g & 0x7f; + palette_msg[11] = (g & 0x80) >> 7; + palette_msg[12] = b & 0x7f; + palette_msg[13] = (b & 0x80) >> 7; + palette_msg[14] = w & 0x7f; + palette_msg[15] = w & 0x80; + + write (palette_msg); + + MidiByteArray update_pallette_msg (8, 0xf0, 0x00, 0x21, 0x1d, 0x01, 0x01, 0x05, 0xF7); + write (update_pallette_msg); + + color_map[rgba] = index; + + return index; +} + +void +Push2::build_color_map () +{ + /* These are "standard" colors that Ableton docs suggest will always be + there. Put them in our color map so that when we look up these + colors, we will use the Ableton indices for them. + */ + + color_map.insert (make_pair (RGB_TO_UINT (0,0,0), 0)); + color_map.insert (make_pair (RGB_TO_UINT (204,204,204), 122)); + color_map.insert (make_pair (RGB_TO_UINT (64,64,64), 123)); + color_map.insert (make_pair (RGB_TO_UINT (20,20,20), 124)); + color_map.insert (make_pair (RGB_TO_UINT (0,0,255), 125)); + color_map.insert (make_pair (RGB_TO_UINT (0,255,0), 126)); + color_map.insert (make_pair (RGB_TO_UINT (255,0,0), 127)); + + for (uint8_t n = 1; n < 122; ++n) { + color_map_free_list.push (n); + } +} + +void +Push2::fill_color_table () +{ + colors.insert (make_pair (DarkBackground, ArdourCanvas::rgba_to_color (0, 0, 0, 1))); + colors.insert (make_pair (LightBackground, ArdourCanvas::rgba_to_color (0.98, 0.98, 0.98, 1))); + + colors.insert (make_pair (ParameterName, ArdourCanvas::rgba_to_color (0.98, 0.98, 0.98, 1))); + + colors.insert (make_pair (KnobArcBackground, ArdourCanvas::rgba_to_color (0.3, 0.3, 0.3, 1.0))); + colors.insert (make_pair (KnobArcStart, ArdourCanvas::rgba_to_color (1.0, 0.0, 0.0, 1.0))); + colors.insert (make_pair (KnobArcEnd, ArdourCanvas::rgba_to_color (0.0, 1.0, 0.0, 1.0))); + + colors.insert (make_pair (KnobLineShadow, ArdourCanvas::rgba_to_color (0, 0, 0, 0.3))); + colors.insert (make_pair (KnobLine, ArdourCanvas::rgba_to_color (1, 1, 1, 1))); + + colors.insert (make_pair (KnobForeground, ArdourCanvas::rgba_to_color (0.2, 0.2, 0.2, 1))); + colors.insert (make_pair (KnobBackground, ArdourCanvas::rgba_to_color (0.2, 0.2, 0.2, 1))); + colors.insert (make_pair (KnobShadow, ArdourCanvas::rgba_to_color (0, 0, 0, 0.1))); + colors.insert (make_pair (KnobBorder, ArdourCanvas::rgba_to_color (0, 0, 0, 1))); + +} + +ArdourCanvas::Color +Push2::get_color (ColorName name) +{ + Colors::iterator c = colors.find (name); + if (c != colors.end()) { + return c->second; + } + + return random(); +} + +void +Push2::set_current_layout (Push2Layout* layout) +{ + if (layout && layout == _current_layout) { + _current_layout->show (); + } else { + + if (_current_layout) { + _current_layout->hide (); + _canvas->root()->remove (_current_layout); + _previous_layout = _current_layout; + } + + _current_layout = layout; + + if (_current_layout) { + _canvas->root()->add (_current_layout); + _current_layout->show (); + } + + + _canvas->request_redraw (); + } +} + +void +Push2::use_previous_layout () +{ + if (_previous_layout) { + set_current_layout (_previous_layout); + } +} + +void +Push2::request_pressure_mode () +{ + MidiByteArray msg (8, 0xF0, 0x00, 0x21, 0x1D, 0x01, 0x01, 0x1F, 0xF7); + write (msg); +} + +void +Push2::set_pressure_mode (PressureMode pm) +{ + MidiByteArray msg (9, 0xF0, 0x00, 0x21, 0x1D, 0x01, 0x01, 0x1E, 0x0, 0xF7); + + switch (pm) { + case AfterTouch: + /* nothing to do, message is correct */ + break; + case PolyPressure: + msg[7] = 0x1; + break; + default: + return; + } + + write (msg); + cerr << "Sent PM message " << msg << endl; +}