diff --git a/doc/Doxyfile b/doc/Doxyfile index 306ebd5513..79eebbbb17 100644 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -2471,7 +2471,7 @@ CLASS_GRAPH = YES # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. -COLLABORATION_GRAPH = YES +COLLABORATION_GRAPH = NO # If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for # groups, showing the direct groups dependencies. diff --git a/gtk2_ardour/ardev_common.sh.in b/gtk2_ardour/ardev_common.sh.in index e6b78e5279..c6d20a4b85 100644 --- a/gtk2_ardour/ardev_common.sh.in +++ b/gtk2_ardour/ardev_common.sh.in @@ -19,7 +19,7 @@ export GTK2_RC_FILES=/nonexistent # can find all the components. # -export ARDOUR_SURFACES_PATH=$libs/surfaces/osc:$libs/surfaces/faderport8:$libs/surfaces/faderport:$libs/surfaces/generic_midi:$libs/surfaces/tranzport:$libs/surfaces/powermate:$libs/surfaces/mackie:$libs/surfaces/us2400:$libs/surfaces/wiimote:$libs/surfaces/push2:$libs/surfaces/maschine2:$libs/surfaces/cc121:$libs/surfaces/launch_control_xl:$libs/surfaces/contourdesign:$libs/surfaces/websockets:$libs/surfaces/console1:$libs/surfaces/launchpad_pro:$libs/surfaces/launchpad_x +export ARDOUR_SURFACES_PATH=$libs/surfaces/osc:$libs/surfaces/faderport8:$libs/surfaces/faderport:$libs/surfaces/generic_midi:$libs/surfaces/tranzport:$libs/surfaces/powermate:$libs/surfaces/mackie:$libs/surfaces/us2400:$libs/surfaces/wiimote:$libs/surfaces/push2:$libs/surfaces/maschine2:$libs/surfaces/cc121:$libs/surfaces/launch_control_xl:$libs/surfaces/contourdesign:$libs/surfaces/websockets:$libs/surfaces/console1:$libs/surfaces/launchpad_pro:$libs/surfaces/launchpad_x:$libs/surfaces/launchkey_4 export ARDOUR_PANNER_PATH=$libs/panners export ARDOUR_DATA_PATH=$TOP/share:$TOP/build:$TOP/gtk2_ardour:$TOP/build/gtk2_ardour export ARDOUR_MIDIMAPS_PATH=$TOP/share/midi_maps diff --git a/gtk2_ardour/ardour_ui.h b/gtk2_ardour/ardour_ui.h index 1ca6dc59a7..7ae493bd8c 100644 --- a/gtk2_ardour/ardour_ui.h +++ b/gtk2_ardour/ardour_ui.h @@ -48,8 +48,6 @@ #include #include -#include - #include "pbd/xml++.h" #include #include diff --git a/gtk2_ardour/audio_region_editor.cc b/gtk2_ardour/audio_region_editor.cc index 0f84d3001e..d5eead32d4 100644 --- a/gtk2_ardour/audio_region_editor.cc +++ b/gtk2_ardour/audio_region_editor.cc @@ -60,6 +60,7 @@ AudioRegionEditor::AudioRegionEditor (Session* s, AudioRegionView* arv) , _audio_region (arv->audio_region ()) , gain_adjustment(accurate_coefficient_to_dB(fabsf (_audio_region->scale_amplitude())), -40.0, +40.0, 0.1, 1.0, 0) , _polarity_toggle (_("Invert")) + , _pre_fade_fx_toggle (_("Pre-Fade Fx")) , _show_on_touch (_("Show on Touch")) , _peak_channel (false) { @@ -94,10 +95,10 @@ AudioRegionEditor::AudioRegionEditor (Session* s, AudioRegionView* arv) _polarity_label.set_alignment (1, 0.5); _table.attach (_polarity_label, 0, 1, _table_row, _table_row + 1, Gtk::FILL, Gtk::FILL); _table.attach (_polarity_toggle, 1, 2, _table_row, _table_row + 1, Gtk::FILL, Gtk::FILL); + _table.attach (_pre_fade_fx_toggle, 2, 3, _table_row, _table_row + 1, Gtk::FILL, Gtk::FILL); ++_table_row; -#ifndef MIXBUS -#ifndef LIVETRAX +#ifndef LIVETRAX // no region Fx for Trax _region_line_label.set_name ("AudioRegionEditorLabel"); _region_line_label.set_text (_("Region Line:")); _region_line_label.set_alignment (1, 0.5); @@ -106,13 +107,18 @@ AudioRegionEditor::AudioRegionEditor (Session* s, AudioRegionView* arv) _table.attach (_show_on_touch, 2, 3, _table_row, _table_row + 1, Gtk::FILL, Gtk::FILL); ++_table_row; #endif -#endif + + UI::instance()->set_tip (_polarity_toggle, _("Invert the signal polarity (180deg phase shift)")); + UI::instance()->set_tip (_pre_fade_fx_toggle, _("Apply region effects before the region fades.\nThis is useful if the effect(s) have tail, that would otherwise be faded out by the region fade (e.g. reverb, delay)")); + UI::instance()->set_tip (_show_on_touch, _("When touching a control in a region effect plugin UI, the corresponding region-automation line is shown the editor, and edit mode is set to 'draw'.")); gain_changed (); + pre_fade_fx_changed (); refill_region_line (); gain_adjustment.signal_value_changed().connect (sigc::mem_fun (*this, &AudioRegionEditor::gain_adjustment_changed)); _polarity_toggle.signal_toggled().connect (sigc::mem_fun (*this, &AudioRegionEditor::gain_adjustment_changed)); + _pre_fade_fx_toggle.signal_toggled().connect (sigc::mem_fun (*this, &AudioRegionEditor::pre_fade_fx_toggle_changed)); _show_on_touch.signal_toggled().connect (sigc::mem_fun (*this, &AudioRegionEditor::show_on_touch_changed)); arv->region_line_changed.connect ((sigc::mem_fun (*this, &AudioRegionEditor::refill_region_line))); @@ -127,6 +133,7 @@ AudioRegionEditor::AudioRegionEditor (Session* s, AudioRegionView* arv) pthread_create_and_store (name, &_peak_amplitude_thread_handle, _peak_amplitude_thread, this); signal_peak_thread (); + } AudioRegionEditor::~AudioRegionEditor () @@ -145,6 +152,10 @@ AudioRegionEditor::region_changed (const PBD::PropertyChange& what_changed) gain_changed (); } + if (what_changed.contains (ARDOUR::Properties::fade_before_fx)) { + pre_fade_fx_changed (); + } + if (what_changed.contains (ARDOUR::Properties::start) || what_changed.contains (ARDOUR::Properties::length)) { /* ask the peak thread to run again */ signal_peak_thread (); @@ -182,6 +193,18 @@ AudioRegionEditor::gain_adjustment_changed () } } +void +AudioRegionEditor::pre_fade_fx_changed () +{ + _pre_fade_fx_toggle.set_active (_audio_region->fade_before_fx ()); +} + +void +AudioRegionEditor::pre_fade_fx_toggle_changed () +{ + _audio_region->set_fade_before_fx (_pre_fade_fx_toggle.get_active ()); +} + void AudioRegionEditor::signal_peak_thread () { @@ -281,6 +304,10 @@ AudioRegionEditor::refill_region_line () } std::shared_ptr plugin = fx->plugin (); + if (!plugin) { + return; + } + Gtk::Menu* acm = manage (new Gtk::Menu); MenuList& acm_items (acm->items ()); diff --git a/gtk2_ardour/audio_region_editor.h b/gtk2_ardour/audio_region_editor.h index a941840682..b2b9e7589e 100644 --- a/gtk2_ardour/audio_region_editor.h +++ b/gtk2_ardour/audio_region_editor.h @@ -74,6 +74,9 @@ private: void show_on_touch_changed (); void show_touched_automation (std::weak_ptr); + void pre_fade_fx_changed (); + void pre_fade_fx_toggle_changed (); + AudioRegionView* _arv; std::shared_ptr _audio_region; @@ -84,6 +87,8 @@ private: Gtk::Label _polarity_label; Gtk::CheckButton _polarity_toggle; + Gtk::CheckButton _pre_fade_fx_toggle; + Gtk::Label _peak_amplitude_label; Gtk::Entry _peak_amplitude; diff --git a/gtk2_ardour/audio_region_view.cc b/gtk2_ardour/audio_region_view.cc index 517585ba33..63c54166c8 100644 --- a/gtk2_ardour/audio_region_view.cc +++ b/gtk2_ardour/audio_region_view.cc @@ -1568,7 +1568,7 @@ AudioRegionView::add_gain_point_event (ArdourCanvas::Item *item, GdkEvent *ev, b trackview.editor().begin_reversible_command (_("add gain control point")); - _fx_line->enable_autoation (); + _fx_line->enable_automation (); trackview.session()->add_command (new MementoCommand(*_fx_line->the_list(), &before, &after)); diff --git a/gtk2_ardour/automation_line.cc b/gtk2_ardour/automation_line.cc index ae27a3fc14..d835954f1a 100644 --- a/gtk2_ardour/automation_line.cc +++ b/gtk2_ardour/automation_line.cc @@ -38,8 +38,6 @@ #include #include -#include "boost/shared_ptr.hpp" - #include "pbd/floating.h" #include "pbd/memento_command.h" #include "pbd/stl_delete.h" diff --git a/gtk2_ardour/automation_time_axis.cc b/gtk2_ardour/automation_time_axis.cc index 2de5fa9d87..c00b29e98c 100644 --- a/gtk2_ardour/automation_time_axis.cc +++ b/gtk2_ardour/automation_time_axis.cc @@ -1042,6 +1042,13 @@ AutomationTimeAxisView::color_handler () if (_line) { _line->set_colors(); } + + if (_base_rect) { + const std::string fill_color_name = (dynamic_cast(get_parent()) + ? "midi automation track fill" + : "audio automation track fill"); + _base_rect->set_fill_color (UIConfiguration::instance().color_mod (fill_color_name, "automation track fill")); + } } int diff --git a/gtk2_ardour/beatbox_gui.cc b/gtk2_ardour/beatbox_gui.cc index fec33a1aa0..35d59691c2 100644 --- a/gtk2_ardour/beatbox_gui.cc +++ b/gtk2_ardour/beatbox_gui.cc @@ -20,8 +20,6 @@ #include #include -#include - #include "pbd/compose.h" #include "pbd/i18n.h" diff --git a/gtk2_ardour/bundle_env_mingw.cc b/gtk2_ardour/bundle_env_mingw.cc index f540fe9e1b..d4b3fbe8b9 100644 --- a/gtk2_ardour/bundle_env_mingw.cc +++ b/gtk2_ardour/bundle_env_mingw.cc @@ -110,24 +110,30 @@ fixup_bundle_environment (int, char* [], string & localedir) } } +static std::string ardour_mono_file; +static std::string ardour_sans_file; + static __cdecl void unload_custom_fonts() { - std::string font_file; - if (find_file (ardour_data_search_path(), "ArdourMono.ttf", font_file)) { - RemoveFontResource(font_file.c_str()); + if (!ardour_mono_file.empty ()) { + RemoveFontResource(ardour_mono_file.c_str()); } - if (find_file (ardour_data_search_path(), "ArdourSans.ttf", font_file)) { - RemoveFontResource(font_file.c_str()); + if (!ardour_sans_file.empty ()) { + RemoveFontResource(ardour_sans_file.c_str()); } } +static LONG WINAPI +unload_font_at_exception (PEXCEPTION_POINTERS pExceptionInfo) +{ + unload_custom_fonts (); + return EXCEPTION_CONTINUE_SEARCH; +} + void load_custom_fonts() { - std::string ardour_mono_file; - std::string ardour_sans_file; - if (!find_file (ardour_data_search_path(), "ArdourMono.ttf", ardour_mono_file)) { cerr << _("Cannot find ArdourMono TrueType font") << endl; } @@ -144,10 +150,12 @@ load_custom_fonts() FcConfig *config = FcInitLoadConfigAndFonts(); if (!ardour_mono_file.empty () && FcFalse == FcConfigAppFontAddFile(config, reinterpret_cast(ardour_mono_file.c_str()))) { + ardour_mono_file.clear (); cerr << _("Cannot load ArdourMono TrueType font.") << endl; } if (!ardour_sans_file.empty () && FcFalse == FcConfigAppFontAddFile(config, reinterpret_cast(ardour_sans_file.c_str()))) { + ardour_sans_file.clear (); cerr << _("Cannot load ArdourSans TrueType font.") << endl; } @@ -157,11 +165,14 @@ load_custom_fonts() } else { // pango with win32 backend if (0 == AddFontResource(ardour_mono_file.c_str())) { + ardour_mono_file.clear (); cerr << _("Cannot register ArdourMono TrueType font with windows gdi.") << endl; } if (0 == AddFontResource(ardour_sans_file.c_str())) { + ardour_sans_file.clear (); cerr << _("Cannot register ArdourSans TrueType font with windows gdi.") << endl; } atexit (&unload_custom_fonts); + SetUnhandledExceptionFilter (unload_font_at_exception); } } diff --git a/gtk2_ardour/editor.cc b/gtk2_ardour/editor.cc index 2275355578..75a2c98231 100644 --- a/gtk2_ardour/editor.cc +++ b/gtk2_ardour/editor.cc @@ -377,7 +377,7 @@ Editor::Editor () , ignore_gui_changes (false) , _drags (new DragManager (this)) , lock_dialog (0) - /* , last_event_time { 0, 0 } */ /* this initialization style requires C++11 */ + , last_event_time { 0, 0 } , _dragging_playhead (false) , ignore_map_change (false) , _follow_playhead (true) @@ -486,9 +486,6 @@ Editor::Editor () _have_idled = false; - last_event_time.tv_sec = 0; - last_event_time.tv_usec = 0; - selection_op_history.clear(); before.clear(); diff --git a/gtk2_ardour/editor_mouse.cc b/gtk2_ardour/editor_mouse.cc index 194d49a0b9..fe58e03cf7 100644 --- a/gtk2_ardour/editor_mouse.cc +++ b/gtk2_ardour/editor_mouse.cc @@ -1847,8 +1847,12 @@ Editor::button_release_handler (ArdourCanvas::Item* item, GdkEvent* event, ItemT switch (item_type) { case RegionItem: { - /* since we have FreehandLineDrag we can only get here after a drag, when no movement has happend */ - assert (were_dragging); + /* since we have FreehandLineDrag we can only get here after a drag, when no movement has happend. + * Except when a drag was aborted by pressing Esc. + */ + if (!were_dragging) { + return true; + } AudioRegionView* arv = dynamic_cast (clicked_regionview); AutomationRegionView* atv = dynamic_cast (clicked_regionview); diff --git a/gtk2_ardour/editor_tempodisplay.cc b/gtk2_ardour/editor_tempodisplay.cc index f9a562d256..eaf7727098 100644 --- a/gtk2_ardour/editor_tempodisplay.cc +++ b/gtk2_ardour/editor_tempodisplay.cc @@ -202,6 +202,9 @@ Editor::reset_tempo_marks () TempoPoint const * prev_ts = 0; for (auto & t : tempo_marks) { + if (entered_marker == t) { + entered_marker = 0; + } delete t; } diff --git a/gtk2_ardour/export_report.cc b/gtk2_ardour/export_report.cc index 309e078d1e..15315765ac 100644 --- a/gtk2_ardour/export_report.cc +++ b/gtk2_ardour/export_report.cc @@ -856,7 +856,7 @@ ExportReport::run () { do { int i = ArdourDialog::run (); - if (i == Gtk::RESPONSE_DELETE_EVENT || i == RESPONSE_CLOSE) { + if (i == Gtk::RESPONSE_DELETE_EVENT || i == RESPONSE_CLOSE || i == RESPONSE_CANCEL) { break; } } while (1); diff --git a/gtk2_ardour/generic_pluginui.cc b/gtk2_ardour/generic_pluginui.cc index 2122f756f5..2c8d153299 100644 --- a/gtk2_ardour/generic_pluginui.cc +++ b/gtk2_ardour/generic_pluginui.cc @@ -840,7 +840,7 @@ GenericPluginUI::automation_state_changed (ControlUI* cui) GenericPluginUI::ControlUI* GenericPluginUI::build_control_ui (const Evoral::Parameter& param, const ParameterDescriptor& desc, - std::shared_ptr mcontrol, + std::shared_ptr mcontrol, float value, bool is_input, bool use_knob) @@ -1273,9 +1273,9 @@ void GenericPluginUI::output_update () { for (vector::iterator i = output_controls.begin(); i != output_controls.end(); ++i) { - float val = plugin->get_parameter ((*i)->parameter().id()); char buf[32]; std::shared_ptr c = _pib->control_output ((*i)->parameter().id()); + float val = c->get_parameter (); const std::string& str = ARDOUR::value_as_string(c->desc(), Variant(val)); size_t len = str.copy(buf, 31); buf[len] = '\0'; diff --git a/gtk2_ardour/icons/lkmk4.png b/gtk2_ardour/icons/lkmk4.png new file mode 100644 index 0000000000..8d546a9b71 Binary files /dev/null and b/gtk2_ardour/icons/lkmk4.png differ diff --git a/gtk2_ardour/icons/lkmk4mini.png b/gtk2_ardour/icons/lkmk4mini.png new file mode 100644 index 0000000000..a843d5243a Binary files /dev/null and b/gtk2_ardour/icons/lkmk4mini.png differ diff --git a/gtk2_ardour/io_plugin_window.cc b/gtk2_ardour/io_plugin_window.cc index a435789216..0a1abc445b 100644 --- a/gtk2_ardour/io_plugin_window.cc +++ b/gtk2_ardour/io_plugin_window.cc @@ -16,8 +16,6 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include - #include #include #include diff --git a/gtk2_ardour/linux_vst_gui_support.cc b/gtk2_ardour/linux_vst_gui_support.cc index 80bc465b0f..f2d287c2ea 100644 --- a/gtk2_ardour/linux_vst_gui_support.cc +++ b/gtk2_ardour/linux_vst_gui_support.cc @@ -58,7 +58,7 @@ static VSTState * vstfx_first = NULL; const char magic[] = "VSTFX Plugin State v002"; -static volatile int gui_quit = 0; +static volatile int gui_state = -1; /*This will be our connection to X*/ @@ -330,7 +330,7 @@ gui_event_loop (void* ptr) clock1 = g_get_monotonic_time(); /*The 'Forever' loop - runs the plugin UIs etc - based on the FST gui event loop*/ - while (!gui_quit) + while (gui_state == 0) { /* handle window creation requests, destroy requests, and run idle callbacks */ @@ -464,7 +464,7 @@ again: clock1 = g_get_monotonic_time(); } - if (!gui_quit && may_sleep && elapsed_time_ms + 1 < LXVST_sched_timer_interval) { + if (0 == gui_state && may_sleep && elapsed_time_ms + 1 < LXVST_sched_timer_interval) { Glib::usleep (1000 * (LXVST_sched_timer_interval - elapsed_time_ms - 1)); } } @@ -506,7 +506,7 @@ normally started in globals.cc*/ int vstfx_init (void* ptr) { - assert (gui_quit == 0); + assert (gui_state == -1); pthread_mutex_init (&plugin_mutex, NULL); int thread_create_result; @@ -542,6 +542,7 @@ int vstfx_init (void* ptr) } /*We have a connection to X - so start the gui event loop*/ + gui_state = 0; /*Create the thread - use default attrs for now, don't think we need anything special*/ @@ -555,7 +556,7 @@ int vstfx_init (void* ptr) XCloseDisplay (LXVST_XDisplay); LXVST_XDisplay = 0; - gui_quit = 1; + gui_state = 1; return -1; } @@ -567,10 +568,10 @@ int vstfx_init (void* ptr) void vstfx_exit () { - if (gui_quit) { + if (gui_state) { return; } - gui_quit = 1; + gui_state = 1; /*We need to pthread_join the gui_thread here so we know when it has stopped*/ @@ -782,7 +783,7 @@ vstfx_launch_editor (VSTState* vstfx) void vstfx_destroy_editor (VSTState* vstfx) { - assert (!gui_quit); + assert (0 == gui_state); pthread_mutex_lock (&vstfx->lock); if (vstfx->linux_window) { vstfx->destroy = TRUE; diff --git a/gtk2_ardour/lv2_plugin_ui.cc b/gtk2_ardour/lv2_plugin_ui.cc index 70466d478d..45e1a464cf 100644 --- a/gtk2_ardour/lv2_plugin_ui.cc +++ b/gtk2_ardour/lv2_plugin_ui.cc @@ -70,9 +70,8 @@ LV2PluginUI::write_from_ui(void* controller, std::shared_ptr ac = me->_controllables[port_index]; - me->_updates.insert (port_index); - if (ac) { + me->_updates.insert (port_index); ac->set_value(*(const float*)buffer, Controllable::NoGroup); } } else if (format == URIMap::instance().urids.atom_eventTransfer) { @@ -208,7 +207,7 @@ void LV2PluginUI::control_changed (uint32_t port_index) { /* Must run in GUI thread because we modify _updates with no lock */ - if (_lv2->get_parameter (port_index) != _values_last_sent_to_ui[port_index]) { + if (_controllables[port_index]->get_value () != _values_last_sent_to_ui[port_index]) { /* current plugin parameter does not match last value received from GUI, so queue an update to push it to the GUI during our regular timeout. @@ -239,10 +238,8 @@ LV2PluginUI::queue_port_update() { const uint32_t num_ports = _lv2->num_ports(); for (uint32_t i = 0; i < num_ports; ++i) { - bool ok; - uint32_t port = _lv2->nth_parameter(i, ok); - if (ok) { - _updates.insert (port); + if (_lv2->parameter_is_control (i) && _lv2->parameter_is_input(i)) { + _updates.insert (i); } } } @@ -274,10 +271,8 @@ LV2PluginUI::output_update() /* output ports (values set by DSP) need propagating to GUI */ - uint32_t nports = _output_ports.size(); - for (uint32_t i = 0; i < nports; ++i) { - uint32_t index = _output_ports[i]; - float val = _lv2->get_parameter (index); + for (auto const& index: _output_ports) { + float val = _pib->control_output (index)->get_parameter (); if (val != _values_last_sent_to_ui[index]) { /* Send to GUI */ @@ -288,14 +283,14 @@ LV2PluginUI::output_update() } /* Input ports marked for update because the control value changed - since the last redisplay. - */ + * since the last redisplay. + */ - for (Updates::iterator i = _updates.begin(); i != _updates.end(); ++i) { - float val = _lv2->get_parameter (*i); + for (auto const& i : _updates) { + float val = _controllables[i]->get_value (); /* push current value to the GUI */ - suil_instance_port_event ((SuilInstance*)_inst, (*i), 4, 0, &val); - _values_last_sent_to_ui[(*i)] = val; + suil_instance_port_event ((SuilInstance*)_inst, i, 4, 0, &val); + _values_last_sent_to_ui[i] = val; } _updates.clear (); @@ -436,15 +431,6 @@ LV2PluginUI::lv2ui_instantiate(const std::string& title) #define GET_WIDGET(inst) suil_instance_get_widget((SuilInstance*)inst); - const uint32_t num_ports = _lv2->num_ports(); - for (uint32_t i = 0; i < num_ports; ++i) { - if (_lv2->parameter_is_output(i) - && _lv2->parameter_is_control(i) - && is_update_wanted(i)) { - _output_ports.push_back(i); - } - } - _external_ui_ptr = NULL; if (!is_external_ui) { GtkWidget* c_widget = (GtkWidget*)GET_WIDGET(_inst); @@ -465,29 +451,30 @@ LV2PluginUI::lv2ui_instantiate(const std::string& title) _external_ui_ptr = (struct lv2_external_ui*)GET_WIDGET(_inst); } + const uint32_t num_ports = _lv2->num_ports(); + _values_last_sent_to_ui = new float[num_ports]; _controllables.resize(num_ports); for (uint32_t i = 0; i < num_ports; ++i) { - bool ok; - uint32_t port = _lv2->nth_parameter(i, ok); - if (ok) { - /* Cache initial value of the parameter, regardless of - whether it is input or output - */ + if (!_lv2->parameter_is_control (i)) { + continue; + } - _values_last_sent_to_ui[port] = _lv2->get_parameter(port); - _controllables[port] = std::dynamic_pointer_cast ( - _pib->control(Evoral::Parameter(PluginAutomation, 0, port))); + /* Cache initial value of the parameter, regardless of whether it is input or output */ - if (_lv2->parameter_is_control(port) && _lv2->parameter_is_input(port)) { - if (_controllables[port]) { - _controllables[port]->Changed.connect (control_connections, invalidator (*this), boost::bind (&LV2PluginUI::control_changed, this, port), gui_context()); - } - } + _values_last_sent_to_ui[i] = _lv2->get_parameter(i); + _controllables[i] = std::dynamic_pointer_cast (_pib->control(Evoral::Parameter(PluginAutomation, 0, i))); + if (_lv2->parameter_is_input(i)) { + assert (_controllables[i]); + _controllables[i]->Changed.connect (control_connections, invalidator (*this), boost::bind (&LV2PluginUI::control_changed, this, i), gui_context()); /* queue for first update ("push") to GUI */ - _updates.insert (port); + _updates.insert (i); + } + + if (_lv2->parameter_is_output(i) && is_update_wanted(i)) { + _output_ports.push_back (i); } } diff --git a/gtk2_ardour/lv2_plugin_ui.h b/gtk2_ardour/lv2_plugin_ui.h index 217148d20d..322b8394d6 100644 --- a/gtk2_ardour/lv2_plugin_ui.h +++ b/gtk2_ardour/lv2_plugin_ui.h @@ -80,7 +80,7 @@ private: std::shared_ptr _pib; std::shared_ptr _lv2; - std::vector _output_ports; + std::vector _output_ports; sigc::connection _screen_update_connection; sigc::connection _message_update_connection; Gtk::Widget* _gui_widget; diff --git a/gtk2_ardour/midi_channel_selector.h b/gtk2_ardour/midi_channel_selector.h index bc035c8f0d..7ed6a7f718 100644 --- a/gtk2_ardour/midi_channel_selector.h +++ b/gtk2_ardour/midi_channel_selector.h @@ -24,7 +24,6 @@ #define __ardour_ui_midi_channel_selector_h__ #include -#include "boost/shared_ptr.hpp" #include "sigc++/trackable.h" #include "gtkmm/table.h" diff --git a/gtk2_ardour/mixer_strip.cc b/gtk2_ardour/mixer_strip.cc index f7859e4516..5c5229bcf9 100644 --- a/gtk2_ardour/mixer_strip.cc +++ b/gtk2_ardour/mixer_strip.cc @@ -1151,8 +1151,6 @@ MixerStrip::build_route_ops_menu () /* do not allow rename if the track is record-enabled */ items.back().set_sensitive (!is_track() || !track()->rec_enable_control()->get_value()); } - - items.push_back (SeparatorElem()); } if ((!_route->is_singleton () || !active) @@ -1161,11 +1159,18 @@ MixerStrip::build_route_ops_menu () #endif ) { + if (active) { + items.push_back (SeparatorElem()); + } items.push_back (CheckMenuElem (_("Active"))); Gtk::CheckMenuItem* i = dynamic_cast (&items.back()); i->set_active (active); i->set_sensitive (!_session->transport_rolling()); i->signal_activate().connect (sigc::bind (sigc::mem_fun (*this, &RouteUI::set_route_active), !_route->active(), false)); + } + + /* Plugin / Processor related */ + if (active) { items.push_back (SeparatorElem()); } @@ -1174,18 +1179,6 @@ MixerStrip::build_route_ops_menu () Gtk::CheckMenuItem* i = dynamic_cast (&items.back()); i->set_active (_route->strict_io()); i->signal_activate().connect (sigc::hide_return (sigc::bind (sigc::mem_fun (*_route, &Route::set_strict_io), !_route->strict_io()))); - items.push_back (SeparatorElem()); - } - - if (active && is_track()) { - Gtk::Menu* dio_menu = new Menu; - MenuList& dio_items = dio_menu->items(); - dio_items.push_back (MenuElem (_("Record Pre-Fader"), sigc::bind (sigc::mem_fun (*this, &RouteUI::set_disk_io_point), DiskIOPreFader))); - dio_items.push_back (MenuElem (_("Record Post-Fader"), sigc::bind (sigc::mem_fun (*this, &RouteUI::set_disk_io_point), DiskIOPostFader))); - dio_items.push_back (MenuElem (_("Custom Record+Playback Positions"), sigc::bind (sigc::mem_fun (*this, &RouteUI::set_disk_io_point), DiskIOCustom))); - - items.push_back (MenuElem (_("Disk I/O..."), *dio_menu)); - items.push_back (SeparatorElem()); } uint32_t plugin_insert_cnt = 0; @@ -1194,7 +1187,28 @@ MixerStrip::build_route_ops_menu () items.push_back (MenuElem (_("Pin Connections..."), sigc::mem_fun (*this, &RouteUI::manage_pins))); } + if (active) { + items.push_back (CheckMenuElem (_("Protect Against Denormals"), sigc::mem_fun (*this, &RouteUI::toggle_denormal_protection))); + denormal_menu_item = dynamic_cast (&items.back()); + denormal_menu_item->set_active (_route->denormal_protection()); + } + + /* Disk I/O */ + + if (active && is_track()) { + items.push_back (SeparatorElem()); + Gtk::Menu* dio_menu = new Menu; + MenuList& dio_items = dio_menu->items(); + dio_items.push_back (MenuElem (_("Record Pre-Fader"), sigc::bind (sigc::mem_fun (*this, &RouteUI::set_disk_io_point), DiskIOPreFader))); + dio_items.push_back (MenuElem (_("Record Post-Fader"), sigc::bind (sigc::mem_fun (*this, &RouteUI::set_disk_io_point), DiskIOPostFader))); + dio_items.push_back (MenuElem (_("Custom Record+Playback Positions"), sigc::bind (sigc::mem_fun (*this, &RouteUI::set_disk_io_point), DiskIOCustom))); + items.push_back (MenuElem (_("Disk I/O..."), *dio_menu)); + } + + /* MIDI */ + if (active && (std::dynamic_pointer_cast(_route) || _route->the_instrument ())) { + items.push_back (SeparatorElem()); items.push_back (MenuElem (_("Patch Selector..."), sigc::mem_fun(*this, &RouteUI::select_midi_patch))); } @@ -1203,13 +1217,8 @@ MixerStrip::build_route_ops_menu () // TODO ..->n_audio() > 1 && separate_output_groups) hard to check here every time. items.push_back (MenuElem (_("Fan out to Busses"), sigc::bind (sigc::mem_fun (*this, &RouteUI::fan_out), true, true))); items.push_back (MenuElem (_("Fan out to Tracks"), sigc::bind (sigc::mem_fun (*this, &RouteUI::fan_out), false, true))); - items.push_back (SeparatorElem()); } - items.push_back (CheckMenuElem (_("Protect Against Denormals"), sigc::mem_fun (*this, &RouteUI::toggle_denormal_protection))); - denormal_menu_item = dynamic_cast (&items.back()); - denormal_menu_item->set_active (_route->denormal_protection()); - /* note that this relies on selection being shared across editor and * mixer (or global to the backend, in the future), which is the only * sane thing for users anyway. diff --git a/gtk2_ardour/plugin_ui.cc b/gtk2_ardour/plugin_ui.cc index ca619e2f31..ed1b8305e6 100644 --- a/gtk2_ardour/plugin_ui.cc +++ b/gtk2_ardour/plugin_ui.cc @@ -51,6 +51,7 @@ #include "ardour/lv2_plugin.h" #include "ardour/plugin.h" #include "ardour/plugin_insert.h" +#include "ardour/region_fx_plugin.h" #include "ardour/session.h" #include "lv2_plugin_ui.h" @@ -85,7 +86,7 @@ extern VST3PluginUI* create_mac_vst3_gui (std::shared_ptr pib) , cpuload_expander (_("CPU Profile")) , latency_gui (0) , latency_dialog (0) + , tailtime_gui (0) + , tailtime_dialog (0) , eqgui (0) , stats_gui (0) , preset_gui (0) @@ -555,6 +558,7 @@ PlugUIBase::PlugUIBase (std::shared_ptr pib) set_tooltip (_pin_management_button, _("Show Plugin Pin Management Dialog")); set_tooltip (_bypass_button, _("Disable signal processing by the plugin")); set_tooltip (_latency_button, _("Edit Plugin Delay/Latency Compensation")); + set_tooltip (_tailtime_button, _("Edit Plugin tail time")); _no_load_preset = 0; update_preset_list (); @@ -565,6 +569,11 @@ PlugUIBase::PlugUIBase (std::shared_ptr pib) _latency_button.signal_clicked.connect (sigc::mem_fun (*this, &PlugUIBase::latency_button_clicked)); set_latency_label (); + _tailtime_button.set_icon (ArdourIcon::TailTimeClock); + _tailtime_button.add_elements (ArdourButton::Text); + _tailtime_button.signal_clicked.connect (sigc::mem_fun (*this, &PlugUIBase::tailtime_button_clicked)); + set_tailtime_label (); + _add_button.set_name ("generic button"); _add_button.set_icon (ArdourIcon::PsetAdd); _add_button.signal_clicked.connect (sigc::mem_fun (*this, &PlugUIBase::add_plugin_setting)); @@ -636,6 +645,11 @@ PlugUIBase::PlugUIBase (std::shared_ptr pib) _pi->LatencyChanged.connect (*this, invalidator (*this), boost::bind (&PlugUIBase::set_latency_label, this), gui_context ()); automation_state_changed (); } + + shared_ptr tt = std::dynamic_pointer_cast (_pib); + if (tt) { + tt->TailTimeChanged.connect (*this, invalidator (*this), boost::bind (&PlugUIBase::set_tailtime_label, this), gui_context ()); + } } PlugUIBase::~PlugUIBase () @@ -645,6 +659,8 @@ PlugUIBase::~PlugUIBase () delete preset_gui; delete latency_gui; delete latency_dialog; + delete tailtime_gui; + delete tailtime_dialog; delete preset_dialog; delete _focus_out_image; @@ -696,6 +712,9 @@ PlugUIBase::add_common_widgets (Gtk::HBox* b, bool with_focus) b->pack_end (_pin_management_button, false, false); b->pack_start (_latency_button, false, false, 4); } + else if (std::dynamic_pointer_cast (_pib)) { + b->pack_start (_tailtime_button, false, false, 4); + } } void @@ -709,13 +728,26 @@ PlugUIBase::set_latency_label () _latency_button.set_text (samples_as_time_string (l, sr, true)); } +void + +PlugUIBase::set_tailtime_label () +{ + auto rfx = std::dynamic_pointer_cast (_pib); /* may be NULL */ + if (!rfx) { + return; + } + samplecnt_t const l = rfx->effective_tailtime (); + float const sr = rfx->session ().sample_rate (); + + _tailtime_button.set_text (samples_as_time_string (l, sr, true)); +} void PlugUIBase::latency_button_clicked () { assert (_pi); if (!latency_gui) { - latency_gui = new LatencyGUI (*(_pi.get ()), _pi->session ().sample_rate (), _pi->session ().get_block_size ()); + latency_gui = new TimeCtlGUI (*(_pi.get ()), _pi->session ().sample_rate (), _pi->session ().get_block_size ()); latency_dialog = new ArdourWindow (_("Edit Latency")); /* use both keep-above and transient for to try cover as many different WM's as possible. @@ -732,6 +764,29 @@ PlugUIBase::latency_button_clicked () latency_dialog->show_all (); } +void +PlugUIBase::tailtime_button_clicked () +{ + auto rfx = std::dynamic_pointer_cast (_pib); /* may be NULL */ + assert (rfx); + if (!tailtime_gui) { + tailtime_gui = new TimeCtlGUI (*dynamic_cast(rfx.get()), rfx->session ().sample_rate (), rfx->session ().get_block_size ()); + tailtime_dialog = new ArdourWindow (_("Edit Tail Time")); + /* use both keep-above and transient for to try cover as many + different WM's as possible. + */ + tailtime_dialog->set_keep_above (true); + Window* win = dynamic_cast (_bypass_button.get_toplevel ()); + if (win) { + tailtime_dialog->set_transient_for (*win); + } + tailtime_dialog->add (*tailtime_gui); + } + + tailtime_gui->refresh (); + tailtime_dialog->show_all (); +} + void PlugUIBase::processor_active_changed (std::weak_ptr weak_p) { diff --git a/gtk2_ardour/plugin_ui.h b/gtk2_ardour/plugin_ui.h index 2963a6f7a9..d4a38259da 100644 --- a/gtk2_ardour/plugin_ui.h +++ b/gtk2_ardour/plugin_ui.h @@ -81,7 +81,7 @@ namespace ArdourWidgets { class FastMeter; } -class LatencyGUI; +class TimeCtlGUI; class ArdourWindow; class PluginEqGui; class PluginLoadStatsGui; @@ -110,6 +110,7 @@ public: void update_preset (); void latency_button_clicked (); + void tailtime_button_clicked (); virtual bool on_window_show(const std::string& /*title*/) { return true; } virtual void on_window_hide() {} @@ -157,6 +158,8 @@ protected: Gtk::Expander cpuload_expander; /** a button which, when clicked, opens the latency GUI */ ArdourWidgets::ArdourButton _latency_button; + /** a button which, when clicked, opens the tailtime GUI */ + ArdourWidgets::ArdourButton _tailtime_button; /** a button which sets all controls' automation setting to Manual */ ArdourWidgets::ArdourButton automation_manual_all_button; /** a button which sets all controls' automation setting to Play */ @@ -169,9 +172,13 @@ protected: ArdourWidgets::ArdourButton automation_latch_all_button; void set_latency_label (); - LatencyGUI* latency_gui; + TimeCtlGUI* latency_gui; ArdourWindow* latency_dialog; + void set_tailtime_label (); + TimeCtlGUI* tailtime_gui; + ArdourWindow* tailtime_dialog; + PluginEqGui* eqgui; PluginLoadStatsGui* stats_gui; PluginPresetsUI* preset_gui; diff --git a/gtk2_ardour/po/ru.po b/gtk2_ardour/po/ru.po index 4c54c7b05f..d6fe23cf3c 100644 --- a/gtk2_ardour/po/ru.po +++ b/gtk2_ardour/po/ru.po @@ -5,14 +5,14 @@ # Igor Blinov , 2004. # Александр Кольцов , 2014-2016 # Петр Семилетов ? 2016 -# Alexandre Prokoudine , 2006-2023. +# Alexandre Prokoudine , 2006-2024. # msgid "" msgstr "" "Project-Id-Version: Ardour 8\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-16 16:43+0400\n" -"PO-Revision-Date: 2024-07-17 22:11+0200\n" +"POT-Creation-Date: 2024-08-19 01:39+0200\n" +"PO-Revision-Date: 2024-08-19 01:44+0200\n" "Last-Translator: Alexandre Prokoudine \n" "Language-Team: Russian \n" "Language: ru\n" @@ -20,8 +20,8 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " -"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" -"X-Generator: Poedit 3.4.2\n" +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" +"X-Generator: Gtranslator 46.1\n" "%100>=20) ? 1: 2);\n" #: about.cc:133 @@ -681,7 +681,7 @@ msgid "Add:" msgstr "Добавить:" #: add_route_dialog.cc:75 bundle_manager.cc:205 loudness_dialog.cc:524 -#: region_editor.cc:70 route_group_dialog.cc:73 +#: region_editor.cc:70 route_group_dialog.cc:74 msgid "Name:" msgstr "Имя:" @@ -881,7 +881,7 @@ msgstr "Шаблон/Тип" msgid "Modified With" msgstr "Изменено в" -#: add_route_dialog.cc:300 rc_option_editor.cc:4590 recorder_ui.cc:1278 +#: add_route_dialog.cc:300 rc_option_editor.cc:4601 recorder_ui.cc:1278 msgid "" "With strict-i/o enabled, Effect Processors will not modify the number of " "channels on a track. The number of output channels will always match the " @@ -973,7 +973,7 @@ msgstr "12 каналов" #: add_route_dialog.cc:957 gain_meter.cc:864 loudness_dialog.cc:337 #: loudness_dialog.cc:527 loudness_dialog.cc:540 loudness_dialog.cc:585 -#: loudness_dialog.cc:640 mixer_strip.cc:1994 processor_box.cc:4219 +#: loudness_dialog.cc:640 mixer_strip.cc:1994 processor_box.cc:4221 msgid "Custom" msgstr "На заказ" @@ -1108,8 +1108,8 @@ msgid "Audition" msgstr "Прослушивание" #: ardour_ui.cc:315 editor_actions.cc:178 mixer_strip.cc:1699 -#: monitor_section.cc:328 rc_option_editor.cc:4363 route_time_axis.cc:266 -#: route_time_axis.cc:2410 trigger_strip.cc:345 vca_master_strip.cc:238 +#: monitor_section.cc:328 rc_option_editor.cc:4374 route_time_axis.cc:266 +#: route_time_axis.cc:2416 trigger_strip.cc:345 vca_master_strip.cc:238 #: vca_time_axis.cc:283 msgid "Solo" msgstr "Соло" @@ -1170,7 +1170,7 @@ msgstr "Загрузка процессора плагинами" msgid "Performance Meters" msgstr "Замер производительности" -#: ardour_ui.cc:341 rc_option_editor.cc:3832 transport_masters_dialog.cc:654 +#: ardour_ui.cc:341 rc_option_editor.cc:3843 transport_masters_dialog.cc:655 msgid "Transport Masters" msgstr "Ведущие транспорта" @@ -1357,7 +1357,7 @@ msgstr "Не удалять" msgid "none" msgstr "Нет" -#: ardour_ui.cc:1279 editor_ops.cc:8077 editor_ops.cc:8088 rhythm_ferret.cc:131 +#: ardour_ui.cc:1279 editor_ops.cc:8107 editor_ops.cc:8118 rhythm_ferret.cc:131 #: rhythm_ferret.cc:146 msgid "ms" msgstr "мс" @@ -1958,28 +1958,28 @@ msgstr "" msgid "Appearance" msgstr "Внешний вид" -#: ardour_ui2.cc:982 rc_option_editor.cc:4444 rc_option_editor.cc:4445 -#: rc_option_editor.cc:4484 rc_option_editor.cc:4486 rc_option_editor.cc:4488 -#: rc_option_editor.cc:4496 rc_option_editor.cc:4504 rc_option_editor.cc:4512 -#: rc_option_editor.cc:4521 rc_option_editor.cc:4522 rc_option_editor.cc:4530 -#: rc_option_editor.cc:4532 rc_option_editor.cc:4542 rc_option_editor.cc:4550 -#: rc_option_editor.cc:4566 rc_option_editor.cc:4579 rc_option_editor.cc:4588 +#: ardour_ui2.cc:982 rc_option_editor.cc:4455 rc_option_editor.cc:4456 +#: rc_option_editor.cc:4495 rc_option_editor.cc:4497 rc_option_editor.cc:4499 +#: rc_option_editor.cc:4507 rc_option_editor.cc:4515 rc_option_editor.cc:4523 +#: rc_option_editor.cc:4532 rc_option_editor.cc:4533 rc_option_editor.cc:4541 +#: rc_option_editor.cc:4543 rc_option_editor.cc:4553 rc_option_editor.cc:4561 +#: rc_option_editor.cc:4577 rc_option_editor.cc:4590 rc_option_editor.cc:4599 msgid "Signal Flow" msgstr "Поток сигнала" -#: ardour_ui2.cc:991 ardour_ui_ed.cc:185 rc_option_editor.cc:3955 -#: rc_option_editor.cc:3970 rc_option_editor.cc:3971 rc_option_editor.cc:3975 -#: rc_option_editor.cc:3978 rc_option_editor.cc:3988 rc_option_editor.cc:3998 -#: rc_option_editor.cc:4008 rc_option_editor.cc:4019 rc_option_editor.cc:4029 -#: rc_option_editor.cc:4039 rc_option_editor.cc:4276 rc_option_editor.cc:4277 -#: rc_option_editor.cc:4284 rc_option_editor.cc:4292 rc_option_editor.cc:4300 -#: rc_option_editor.cc:4304 rc_option_editor.cc:4306 rc_option_editor.cc:4310 -#: rc_option_editor.cc:4319 rc_option_editor.cc:4328 +#: ardour_ui2.cc:991 ardour_ui_ed.cc:185 rc_option_editor.cc:3966 +#: rc_option_editor.cc:3981 rc_option_editor.cc:3982 rc_option_editor.cc:3986 +#: rc_option_editor.cc:3989 rc_option_editor.cc:3999 rc_option_editor.cc:4009 +#: rc_option_editor.cc:4019 rc_option_editor.cc:4030 rc_option_editor.cc:4040 +#: rc_option_editor.cc:4050 rc_option_editor.cc:4287 rc_option_editor.cc:4288 +#: rc_option_editor.cc:4295 rc_option_editor.cc:4303 rc_option_editor.cc:4311 +#: rc_option_editor.cc:4315 rc_option_editor.cc:4317 rc_option_editor.cc:4321 +#: rc_option_editor.cc:4330 rc_option_editor.cc:4339 msgid "Plugins" msgstr "Плагины" -#: ardour_ui2.cc:1004 rc_option_editor.cc:4596 rc_option_editor.cc:4597 -#: rc_option_editor.cc:4599 rc_option_editor.cc:4610 rc_option_editor.cc:4611 +#: ardour_ui2.cc:1004 rc_option_editor.cc:4607 rc_option_editor.cc:4608 +#: rc_option_editor.cc:4610 rc_option_editor.cc:4621 rc_option_editor.cc:4622 #: session_option_editor.cc:430 msgid "Metronome" msgstr "Метроном" @@ -2117,23 +2117,23 @@ msgid "" "Shift+right-click to unassign" msgstr "" -#: ardour_ui_dialogs.cc:316 +#: ardour_ui_dialogs.cc:317 msgid "Don't close" msgstr "Не закрывать" -#: ardour_ui_dialogs.cc:318 template_dialog.cc:325 +#: ardour_ui_dialogs.cc:319 template_dialog.cc:325 msgid "Discard" msgstr "Отказаться" -#: ardour_ui_dialogs.cc:320 +#: ardour_ui_dialogs.cc:321 msgid "Just close" msgstr "Просто закрыть" -#: ardour_ui_dialogs.cc:322 +#: ardour_ui_dialogs.cc:323 msgid "Save and close" msgstr "Сохранить и закрыть" -#: ardour_ui_dialogs.cc:1126 ardour_ui_ed.cc:444 ardour_ui_ed.cc:455 +#: ardour_ui_dialogs.cc:1127 ardour_ui_ed.cc:444 ardour_ui_ed.cc:455 #: audio_clock.cc:2199 editor.cc:203 editor.cc:331 editor_actions.cc:690 #: editor_actions.cc:720 export_timespan_selector.cc:102 #: session_option_editor.cc:48 session_option_editor.cc:68 @@ -2144,7 +2144,7 @@ msgstr "Сохранить и закрыть" msgid "Timecode" msgstr "Тайм-код" -#: ardour_ui_dialogs.cc:1139 session_option_editor.cc:178 +#: ardour_ui_dialogs.cc:1140 session_option_editor.cc:178 #: session_option_editor.cc:186 session_option_editor.cc:210 msgid "Media" msgstr "Данные" @@ -2162,13 +2162,13 @@ msgid "Session" msgstr "Сессия" #: ardour_ui_ed.cc:170 editor_actions.cc:180 editor_regions.cc:98 -#: port_group.cc:494 port_group.cc:550 session_option_editor.cc:127 +#: port_group.cc:494 port_group.cc:554 session_option_editor.cc:127 #: session_option_editor.cc:128 session_option_editor.cc:135 #: session_option_editor.cc:142 msgid "Sync" msgstr "Синхронизация" -#: ardour_ui_ed.cc:171 rc_option_editor.cc:4599 +#: ardour_ui_ed.cc:171 rc_option_editor.cc:4610 msgid "Options" msgstr "Параметры" @@ -2191,7 +2191,7 @@ msgid "Editor" msgstr "Редактор" #: ardour_ui_ed.cc:175 ardour_ui_ed.cc:713 plugin_manager_ui.cc:171 -#: rc_option_editor.cc:2365 rc_option_editor.cc:5201 +#: rc_option_editor.cc:2365 rc_option_editor.cc:5212 msgid "Preferences" msgstr "Параметры" @@ -2228,11 +2228,11 @@ msgstr "Тип файла" msgid "Sample Format" msgstr "Формат сэмпла" -#: ardour_ui_ed.cc:184 rc_option_editor.cc:4615 rc_option_editor.cc:4616 +#: ardour_ui_ed.cc:184 rc_option_editor.cc:4626 rc_option_editor.cc:4627 msgid "Control Surfaces" msgstr "Устройства управления" -#: ardour_ui_ed.cc:186 rc_option_editor.cc:4624 +#: ardour_ui_ed.cc:186 rc_option_editor.cc:4635 msgid "Metering" msgstr "Индикаторы" @@ -2419,8 +2419,8 @@ msgstr "Сохранить" #: rc_option_editor.cc:3718 rc_option_editor.cc:3739 rc_option_editor.cc:3747 #: rc_option_editor.cc:3754 rc_option_editor.cc:3756 rc_option_editor.cc:3769 #: rc_option_editor.cc:3782 rc_option_editor.cc:3785 rc_option_editor.cc:3795 -#: rc_option_editor.cc:3803 rc_option_editor.cc:3811 rc_option_editor.cc:3955 -#: rc_option_editor.cc:3963 +#: rc_option_editor.cc:3803 rc_option_editor.cc:3811 rc_option_editor.cc:3966 +#: rc_option_editor.cc:3974 msgid "Transport" msgstr "Транспорт" @@ -2838,7 +2838,7 @@ msgstr "Выбрать все видимые полосы" msgid "Select All Tracks" msgstr "Выбрать все дорожки" -#: ardour_ui_ed.cc:702 export_timespan_selector.cc:68 processor_box.cc:4199 +#: ardour_ui_ed.cc:702 export_timespan_selector.cc:68 processor_box.cc:4201 msgid "Deselect All" msgstr "Снять все выделения" @@ -3458,11 +3458,11 @@ msgstr "" msgid "Do not show this window again" msgstr "Больше не показывать это окно" -#: ardour_ui_startup.cc:864 +#: ardour_ui_startup.cc:870 msgid "NSM: The JACK backend is mandatory and can not be loaded." msgstr "" -#: ardour_ui_startup.cc:882 +#: ardour_ui_startup.cc:888 msgid "NSM: %1 cannot connect to the JACK server. Please start jackd first." msgstr "" @@ -3615,18 +3615,22 @@ msgid "Invert" msgstr "Инверсия" #: audio_region_editor.cc:63 +msgid "Pre-Fade Fx" +msgstr "Применить до фейдов" + +#: audio_region_editor.cc:64 msgid "Show on Touch" msgstr "Показывать при касании" -#: audio_region_editor.cc:70 rhythm_ferret.cc:140 rhythm_ferret.cc:157 +#: audio_region_editor.cc:71 rhythm_ferret.cc:140 rhythm_ferret.cc:157 msgid "dB" msgstr "Дб" -#: audio_region_editor.cc:73 +#: audio_region_editor.cc:74 msgid "Region gain:" msgstr "Усиление области:" -#: audio_region_editor.cc:83 export_analysis_graphs.cc:195 +#: audio_region_editor.cc:84 export_analysis_graphs.cc:195 #: export_analysis_graphs.cc:330 export_format_dialog.cc:60 #: export_format_dialog.cc:88 fft_graph.cc:495 normalize_dialog.cc:55 #: normalize_dialog.cc:87 region_peak_cursor.cc:121 region_peak_cursor.cc:123 @@ -3634,25 +3638,45 @@ msgstr "Усиление области:" msgid "dBFS" msgstr "dBFS" -#: audio_region_editor.cc:86 +#: audio_region_editor.cc:87 msgid "Peak amplitude:" msgstr "Пиковая амплитуда:" -#: audio_region_editor.cc:93 +#: audio_region_editor.cc:94 msgid "Polarity:" msgstr "Полярность:" -#: audio_region_editor.cc:100 +#: audio_region_editor.cc:102 msgid "Region Line:" msgstr "Линия области:" -#: audio_region_editor.cc:117 +#: audio_region_editor.cc:109 +msgid "Invert the signal polarity (180deg phase shift)" +msgstr "Инвертировать полярность сигнала (смещение фазы на 180°)" + +#: audio_region_editor.cc:110 +msgid "" +"Apply region effects before the region fades.\n" +"This is useful if the effect(s) have tail, that would otherwise be faded out " +"by the region fade (e.g. reverb, delay)" +msgstr "" +"Применить эффекты области до её фейдов. Это полезно в тех случаях, когда у " +"эффектов (например, реверберации и дилеев) есть «хвост», который в противном " +"случае угасал бы." + +#: audio_region_editor.cc:111 +msgid "" +"When touching a control in a region effect plugin UI, the corresponding " +"region-automation line is shown the editor, and edit mode is set to 'draw'." +msgstr "" + +#: audio_region_editor.cc:125 msgid "Calculating..." msgstr "Выполняется вычисление…" -#: audio_region_editor.cc:259 audio_region_editor.cc:267 +#: audio_region_editor.cc:284 audio_region_editor.cc:292 msgid "Gain Envelope" -msgstr "" +msgstr "Огибающая усиления" #: audio_region_view.cc:1563 msgid "add gain control point" @@ -3662,15 +3686,15 @@ msgstr "добавление точки управления усилением" msgid "AUDIO Region Operations:" msgstr "Операции со звуковой областью:" -#: audio_region_operations_box.cc:59 editor_actions.cc:1888 trigger_ui.cc:747 +#: audio_region_operations_box.cc:59 editor_actions.cc:1892 trigger_ui.cc:747 msgid "Reverse" msgstr "Реверс" -#: audio_region_operations_box.cc:63 editor_actions.cc:1903 +#: audio_region_operations_box.cc:63 editor_actions.cc:1907 msgid "Pitch Shift..." msgstr "Сменить высоту тона…" -#: audio_region_operations_box.cc:67 editor_actions.cc:1885 +#: audio_region_operations_box.cc:67 editor_actions.cc:1889 msgid "Normalize..." msgstr "Нормировать сигнал…" @@ -3800,7 +3824,7 @@ msgstr[0] "Сделать равным %1 удару" msgstr[1] "Сделать равным %1 ударам" msgstr[2] "Сделать равным %1 ударам" -#: automation_line.cc:318 editor_drag.cc:4878 +#: automation_line.cc:318 editor_drag.cc:4880 msgid "automation event move" msgstr "смещение события автоматизации" @@ -3877,9 +3901,9 @@ msgstr "Нотные «леденцы»" msgid "Line" msgstr "Линия" -#: automation_time_axis.cc:680 mixer_ui.cc:4164 rc_option_editor.cc:4151 -#: rc_option_editor.cc:4156 rc_option_editor.cc:4202 rc_option_editor.cc:4207 -#: rc_option_editor.cc:4266 rc_option_editor.cc:4271 trigger_ui.cc:408 +#: automation_time_axis.cc:680 mixer_ui.cc:4164 rc_option_editor.cc:4162 +#: rc_option_editor.cc:4167 rc_option_editor.cc:4213 rc_option_editor.cc:4218 +#: rc_option_editor.cc:4277 rc_option_editor.cc:4282 trigger_ui.cc:408 msgid "Clear" msgstr "Очистить" @@ -3958,15 +3982,15 @@ msgid "Source" msgstr "Источник" #: bundle_manager.cc:276 editor.cc:2078 editor_actions.cc:130 -#: editor_actions.cc:143 lua_script_manager.cc:44 rc_option_editor.cc:4163 -#: rc_option_editor.cc:4182 rc_option_editor.cc:4212 +#: editor_actions.cc:143 lua_script_manager.cc:44 rc_option_editor.cc:4174 +#: rc_option_editor.cc:4193 rc_option_editor.cc:4223 msgid "Edit" msgstr "Правка" #: bundle_manager.cc:277 editor.cc:6832 editor.cc:6862 editor_actions.cc:440 #: editor_actions.cc:441 io_plugin_window.cc:360 luawindow.cc:101 -#: plugin_ui.cc:530 processor_box.cc:4183 processor_box.cc:4185 -#: region_editor.cc:679 +#: plugin_ui.cc:530 processor_box.cc:4185 processor_box.cc:4187 +#: region_editor.cc:680 msgid "Delete" msgstr "Удалить" @@ -3999,7 +4023,7 @@ msgstr "Тема цветового оформления" msgid "Object" msgstr "Объект" -#: color_theme_manager.cc:123 route_group_dialog.cc:56 route_group_dialog.cc:84 +#: color_theme_manager.cc:123 route_group_dialog.cc:57 route_group_dialog.cc:85 msgid "Color" msgstr "Цвет" @@ -4035,7 +4059,7 @@ msgstr "Применить к выделенным точкам" msgid "on" msgstr "Вкл" -#: control_point_dialog.cc:52 rc_option_editor.cc:4634 rc_option_editor.cc:4648 +#: control_point_dialog.cc:52 rc_option_editor.cc:4645 rc_option_editor.cc:4659 msgid "off" msgstr "Выкл" @@ -4440,7 +4464,7 @@ msgstr "Видеолинейка" msgid "mode" msgstr "режим" -#: editor.cc:675 editor.cc:4144 group_tabs.cc:657 route_group_dialog.cc:54 +#: editor.cc:675 editor.cc:4144 group_tabs.cc:657 route_group_dialog.cc:55 #: time_info_box.cc:66 msgid "Selection" msgstr "Выделение" @@ -4477,7 +4501,7 @@ msgstr "Области и маркеры" msgid "Window|Editor" msgstr "Редактор" -#: editor.cc:1356 editor.cc:5317 editor_actions.cc:176 editor_actions.cc:1933 +#: editor.cc:1356 editor.cc:5317 editor_actions.cc:176 editor_actions.cc:1937 msgid "Loop" msgstr "Петля" @@ -4502,8 +4526,8 @@ msgid "Slow" msgstr "Медленно" #: editor.cc:1504 rc_option_editor.cc:3328 session_archive_dialog.cc:50 -#: session_archive_dialog.cc:206 session_archive_dialog.cc:219 sfdb_ui.cc:1993 -#: sfdb_ui.cc:2115 +#: session_archive_dialog.cc:206 session_archive_dialog.cc:219 sfdb_ui.cc:1998 +#: sfdb_ui.cc:2120 msgid "Fast" msgstr "Быстро" @@ -4543,7 +4567,7 @@ msgstr "Удалить все маркеры в части аранжировк msgid "Move Playhead to Marker" msgstr "Переместить указатель воспроизведения к маркеру" -#: editor.cc:1672 editor.cc:1680 editor_ops.cc:4331 +#: editor.cc:1672 editor.cc:1680 editor_ops.cc:4312 msgid "Freeze" msgstr "Заморозить" @@ -4643,11 +4667,11 @@ msgstr "Объединить" msgid "Consolidate (with processing)" msgstr "Объединить (с обработкой)" -#: editor.cc:2008 editor_export_audio.cc:349 editor_ops.cc:4408 +#: editor.cc:2008 editor_export_audio.cc:349 editor_ops.cc:4389 msgid "Bounce" msgstr "Свести" -#: editor.cc:2009 editor_actions.cc:1977 +#: editor.cc:2009 editor_actions.cc:1981 msgid "Bounce (with processing)" msgstr "Свести (с обработкой)" @@ -4732,15 +4756,15 @@ msgstr "Создать выделение между указателем и т msgid "Select" msgstr "Выделить" -#: editor.cc:2069 editor.cc:2143 editor_actions.cc:439 processor_box.cc:4179 +#: editor.cc:2069 editor.cc:2143 editor_actions.cc:439 processor_box.cc:4181 msgid "Cut" msgstr "Вырезать" -#: editor.cc:2070 editor.cc:2144 editor_actions.cc:445 processor_box.cc:4181 +#: editor.cc:2070 editor.cc:2144 editor_actions.cc:445 processor_box.cc:4183 msgid "Copy" msgstr "Копировать" -#: editor.cc:2071 editor.cc:2145 editor_actions.cc:446 processor_box.cc:4193 +#: editor.cc:2071 editor.cc:2145 editor_actions.cc:446 processor_box.cc:4195 msgid "Paste" msgstr "Вставить" @@ -4969,7 +4993,7 @@ msgid "Redo (%1)" msgstr "Вернуть (%1)" #: editor.cc:3918 editor.cc:3942 editor_actions.cc:149 editor_actions.cc:397 -#: editor_actions.cc:1921 +#: editor_actions.cc:1925 msgid "Duplicate" msgstr "Продублировать" @@ -5095,9 +5119,9 @@ msgstr "Сохранить список" msgid "Keep Remaining" msgstr "Сохранить оставшееся" -#: editor.cc:4620 editor_audio_import.cc:729 editor_ops.cc:7718 +#: editor.cc:4620 editor_audio_import.cc:729 editor_ops.cc:7748 #: engine_dialog.cc:3128 sfdb_freesound_mootcher.cc:88 keyeditor.cc:81 -#: library_download_dialog.cc:311 processor_box.cc:3907 processor_box.cc:3932 +#: library_download_dialog.cc:311 processor_box.cc:3909 processor_box.cc:3934 #: pt_import_selector.cc:45 template_dialog.cc:518 #: transport_masters_dialog.cc:717 utils.cc:125 msgid "Cancel" @@ -5157,27 +5181,27 @@ msgstr "Дождитесь загрузки визуальных данных в #: editor.cc:6831 editor.cc:6866 editor_markers.cc:1249 editor_markers.cc:1265 #: editor_markers.cc:1282 io_plugin_window.cc:356 panner_ui.cc:416 -#: processor_box.cc:4226 region_editor.cc:636 trigger_clip_picker.cc:332 +#: processor_box.cc:4228 region_editor.cc:637 trigger_clip_picker.cc:332 msgid "Edit..." msgstr "Изменить" -#: editor.cc:6869 editor_actions.cc:1906 +#: editor.cc:6869 editor_actions.cc:1910 msgid "Transpose..." msgstr "Транспозиция…" -#: editor.cc:6873 editor_actions.cc:1992 +#: editor.cc:6873 editor_actions.cc:1996 msgid "Legatize" msgstr "Добавить легато" -#: editor.cc:6879 editor_actions.cc:1991 midi_region_operations_box.cc:59 +#: editor.cc:6879 editor_actions.cc:1995 midi_region_operations_box.cc:59 msgid "Quantize..." msgstr "Квантование..." -#: editor.cc:6882 editor_actions.cc:1995 +#: editor.cc:6882 editor_actions.cc:1999 msgid "Remove Overlap" msgstr "Убрать перекрытие" -#: editor.cc:6888 editor_actions.cc:1994 midi_region_operations_box.cc:67 +#: editor.cc:6888 editor_actions.cc:1998 midi_region_operations_box.cc:67 msgid "Transform..." msgstr "Преобразовать..." @@ -5185,7 +5209,7 @@ msgstr "Преобразовать..." msgid "Autoconnect" msgstr "Автосоединение" -#: editor_actions.cc:128 rc_option_editor.cc:4959 route_time_axis.cc:280 +#: editor_actions.cc:128 rc_option_editor.cc:4970 route_time_axis.cc:280 #: route_time_axis.cc:818 vca_time_axis.cc:77 vca_time_axis.cc:461 msgid "Automation" msgstr "Автоматизация" @@ -5235,7 +5259,7 @@ msgstr "Маркеры" msgid "Trim" msgstr "Обрезать" -#: editor_actions.cc:145 editor_actions.cc:166 route_group_dialog.cc:48 +#: editor_actions.cc:145 editor_actions.cc:166 route_group_dialog.cc:49 msgid "Gain" msgstr "Усиление" @@ -5243,7 +5267,7 @@ msgstr "Усиление" msgid "Ranges" msgstr "Выделения" -#: editor_actions.cc:147 editor_actions.cc:1918 session_option_editor.cc:146 +#: editor_actions.cc:147 editor_actions.cc:1922 session_option_editor.cc:146 #: session_option_editor.cc:148 session_option_editor.cc:155 #: session_option_editor.cc:162 session_option_editor.cc:169 msgid "Fades" @@ -5277,11 +5301,11 @@ msgstr "Параметры MIDI" msgid "Misc Options" msgstr "Прочие параметры" -#: editor_actions.cc:159 rc_option_editor.cc:4332 rc_option_editor.cc:4350 -#: rc_option_editor.cc:4358 rc_option_editor.cc:4363 rc_option_editor.cc:4372 -#: rc_option_editor.cc:4374 rc_option_editor.cc:4382 rc_option_editor.cc:4390 -#: rc_option_editor.cc:4398 rc_option_editor.cc:4416 rc_option_editor.cc:4428 -#: rc_option_editor.cc:4440 route_group_dialog.cc:57 +#: editor_actions.cc:159 rc_option_editor.cc:4343 rc_option_editor.cc:4361 +#: rc_option_editor.cc:4369 rc_option_editor.cc:4374 rc_option_editor.cc:4383 +#: rc_option_editor.cc:4385 rc_option_editor.cc:4393 rc_option_editor.cc:4401 +#: rc_option_editor.cc:4409 rc_option_editor.cc:4427 rc_option_editor.cc:4439 +#: rc_option_editor.cc:4451 route_group_dialog.cc:58 #: session_option_editor.cc:266 session_option_editor.cc:267 #: session_option_editor.cc:274 session_option_editor.cc:281 #: session_option_editor.cc:287 @@ -5536,13 +5560,13 @@ msgstr "" msgid "Show Playlist Selector" msgstr "Показать окно выбора плейлистов" -#: editor_actions.cc:287 editor_actions.cc:288 editor_actions.cc:2003 -#: editor_actions.cc:2004 +#: editor_actions.cc:287 editor_actions.cc:288 editor_actions.cc:2007 +#: editor_actions.cc:2008 msgid "Nudge Later" msgstr "Толкнуть вперёд" -#: editor_actions.cc:289 editor_actions.cc:290 editor_actions.cc:2005 -#: editor_actions.cc:2006 +#: editor_actions.cc:289 editor_actions.cc:290 editor_actions.cc:2009 +#: editor_actions.cc:2010 msgid "Nudge Earlier" msgstr "Толкнуть назад" @@ -5694,11 +5718,11 @@ msgstr "" msgid "Insert Time Section at Edit Point" msgstr "Вставить часть по точке редактирования" -#: editor_actions.cc:382 editor_actions.cc:1974 +#: editor_actions.cc:382 editor_actions.cc:1978 msgid "Play Selected Regions" msgstr "Воспроизвести выбранные области" -#: editor_actions.cc:383 editor_actions.cc:1975 +#: editor_actions.cc:383 editor_actions.cc:1979 msgid "Tag Selected Regions" msgstr "Пометить выбранные области" @@ -5730,7 +5754,7 @@ msgstr "Активный маркер к указателю мыши" msgid "Set Auto Punch In/Out from Playhead" msgstr "" -#: editor_actions.cc:400 editor_actions.cc:1924 +#: editor_actions.cc:400 editor_actions.cc:1928 msgid "Multi-Duplicate..." msgstr "Продублировать многократно..." @@ -5810,7 +5834,7 @@ msgstr "Следовать за указателем" msgid "Remove Last Capture" msgstr "Удалить последнюю запись" -#: editor_actions.cc:495 editor_ops.cc:5779 +#: editor_actions.cc:495 editor_ops.cc:5761 msgid "Tag Last Capture" msgstr "Пометить последнюю запись" @@ -5830,7 +5854,7 @@ msgstr "Вставить промежуток времени" msgid "Remove Time" msgstr "Удалить промежуток времени" -#: editor_actions.cc:507 editor_ops.cc:9650 +#: editor_actions.cc:507 editor_ops.cc:9680 msgid "Remove Gaps" msgstr "Удалить пробелы" @@ -6388,14 +6412,22 @@ msgid "Move to Original Position" msgstr "К исходной позиции" #: editor_actions.cc:1876 +msgid "Unlock" +msgstr "" + +#: editor_actions.cc:1879 +msgid "Lock (toggle)" +msgstr "" + +#: editor_actions.cc:1880 msgid "Lock to Video" msgstr "Прикрепить к видео" -#: editor_actions.cc:1879 +#: editor_actions.cc:1883 msgid "Remove Sync" msgstr "Удалить синхронизатор" -#: editor_actions.cc:1882 mixer_strip.cc:1686 mixer_strip.cc:1717 +#: editor_actions.cc:1886 mixer_strip.cc:1686 mixer_strip.cc:1717 #: monitor_section.cc:260 monitor_section.cc:320 monitor_section.cc:938 #: route_time_axis.cc:267 route_time_axis.cc:599 surround_strip.cc:442 #: track_record_axis.cc:172 track_record_axis.cc:174 trigger_strip.cc:340 @@ -6403,243 +6435,243 @@ msgstr "Удалить синхронизатор" msgid "Mute" msgstr "Молча" -#: editor_actions.cc:1891 +#: editor_actions.cc:1895 msgid "Make Mono Regions" msgstr "Создать моно-области" -#: editor_actions.cc:1894 +#: editor_actions.cc:1898 msgid "Boost Gain" msgstr "Повысить громкость области" -#: editor_actions.cc:1897 +#: editor_actions.cc:1901 msgid "Cut Gain" msgstr "Понизить громкость области" -#: editor_actions.cc:1900 +#: editor_actions.cc:1904 msgid "Reset Gain" msgstr "Сбросить усиление" -#: editor_actions.cc:1909 +#: editor_actions.cc:1913 msgid "Opaque" msgstr "Непрозрачно" -#: editor_actions.cc:1912 editor_regions.cc:100 +#: editor_actions.cc:1916 editor_regions.cc:100 msgid "Fade In" msgstr "Нарастание" -#: editor_actions.cc:1915 +#: editor_actions.cc:1919 msgid "Fade Out" msgstr "Затухание" -#: editor_actions.cc:1927 +#: editor_actions.cc:1931 msgid "Fill Track" msgstr "Заполнить дорожку" -#: editor_actions.cc:1930 editor_markers.cc:1296 +#: editor_actions.cc:1934 editor_markers.cc:1296 msgid "Set Loop Range" msgstr "Установить область петли" -#: editor_actions.cc:1936 +#: editor_actions.cc:1940 msgid "Set Punch" msgstr "Установить врезку" -#: editor_actions.cc:1939 +#: editor_actions.cc:1943 msgid "Add Single Range Marker" msgstr "Добавить маркер текущей области" -#: editor_actions.cc:1942 +#: editor_actions.cc:1946 msgid "Add Range Marker Per Region" msgstr "Добавить по маркеру на каждую область" -#: editor_actions.cc:1945 +#: editor_actions.cc:1949 msgid "Snap Position to Grid" msgstr "Привязывать позицию к сетке" -#: editor_actions.cc:1948 +#: editor_actions.cc:1952 msgid "Close Gaps" msgstr "Закрыть интервалы" -#: editor_actions.cc:1951 +#: editor_actions.cc:1955 msgid "Rhythm Ferret..." msgstr "Ритмический хорёк..." -#: editor_actions.cc:1954 +#: editor_actions.cc:1958 msgid "Export..." msgstr "Экспортировать..." -#: editor_actions.cc:1957 +#: editor_actions.cc:1961 msgid "Separate Under" msgstr "Разделить под" -#: editor_actions.cc:1959 editor_actions.cc:1960 +#: editor_actions.cc:1963 editor_actions.cc:1964 msgid "Set Fade In Length" msgstr "Установить длительность нарастания" -#: editor_actions.cc:1961 editor_actions.cc:1962 +#: editor_actions.cc:1965 editor_actions.cc:1966 msgid "Set Fade Out Length" msgstr "Установить длительность затухания" -#: editor_actions.cc:1964 +#: editor_actions.cc:1968 msgid "Set Tempo from Region = Bar" msgstr "Установить темп, считая что область = такт" -#: editor_actions.cc:1966 +#: editor_actions.cc:1970 msgid "Split at Percussion Onsets" msgstr "Разделить по атакам перкуссии" -#: editor_actions.cc:1969 +#: editor_actions.cc:1973 msgid "List Editor..." msgstr "Редактор списка событий" -#: editor_actions.cc:1972 +#: editor_actions.cc:1976 msgid "Properties..." msgstr "Свойства..." -#: editor_actions.cc:1978 +#: editor_actions.cc:1982 msgid "Bounce (without processing)" msgstr "Свести (без обработки)" -#: editor_actions.cc:1979 +#: editor_actions.cc:1983 msgid "Combine" msgstr "Объединить" -#: editor_actions.cc:1980 +#: editor_actions.cc:1984 msgid "Uncombine" msgstr "Снять объединение" -#: editor_actions.cc:1982 +#: editor_actions.cc:1986 msgid "Loudness Analysis..." msgstr "Анализ громкости..." -#: editor_actions.cc:1983 +#: editor_actions.cc:1987 msgid "Spectral Analysis..." msgstr "Спектральный анализ..." -#: editor_actions.cc:1985 +#: editor_actions.cc:1989 msgid "Reset Envelope" msgstr "Сбросить огибающую" -#: editor_actions.cc:1987 +#: editor_actions.cc:1991 msgid "Envelope Active" msgstr "Огибающая активна" -#: editor_actions.cc:1989 +#: editor_actions.cc:1993 msgid "Invert Polarity" msgstr "Инвертировать полярность" -#: editor_actions.cc:1993 +#: editor_actions.cc:1997 msgid "Deinterlace Into Layers" msgstr "" -#: editor_actions.cc:1996 editor_actions.cc:1997 +#: editor_actions.cc:2000 editor_actions.cc:2001 msgid "Insert Patch Change..." msgstr "Вставить смену программы..." -#: editor_actions.cc:1998 +#: editor_actions.cc:2002 msgid "Unlink all selected regions" msgstr "Разъединить все выделенные области" -#: editor_actions.cc:1999 editor_ops.cc:6260 +#: editor_actions.cc:2003 editor_ops.cc:6242 msgid "Unlink from unselected" msgstr "" -#: editor_actions.cc:2000 +#: editor_actions.cc:2004 msgid "Strip Silence..." msgstr "Вырезать тишину..." -#: editor_actions.cc:2001 +#: editor_actions.cc:2005 msgid "Set Range Selection" msgstr "Создать выделение из области" -#: editor_actions.cc:2008 +#: editor_actions.cc:2012 msgid "Sequence Regions" msgstr "Выстроить области встык" -#: editor_actions.cc:2010 +#: editor_actions.cc:2014 msgid "Nudge Later by Capture Offset" msgstr "Толкнуть вперёд на смещение захвата" -#: editor_actions.cc:2012 +#: editor_actions.cc:2016 msgid "Nudge Earlier by Capture Offset" msgstr "Толкнуть назад на смещение захвата" -#: editor_actions.cc:2014 +#: editor_actions.cc:2018 msgid "Trim to Loop" msgstr "В петлю" -#: editor_actions.cc:2015 +#: editor_actions.cc:2019 msgid "Trim to Punch" msgstr "Во врезку" -#: editor_actions.cc:2017 +#: editor_actions.cc:2021 msgid "Trim to Previous" msgstr "До предыдущей области" -#: editor_actions.cc:2018 +#: editor_actions.cc:2022 msgid "Trim to Next" msgstr "До следующей области" -#: editor_actions.cc:2022 +#: editor_actions.cc:2026 msgid "Insert Region from Source List" msgstr "Вставить область из списка источников" -#: editor_actions.cc:2026 +#: editor_actions.cc:2030 msgid "Convert Region Cue Markers to CD Markers" msgstr "Превратить маркеры области в CD-маркеры" -#: editor_actions.cc:2027 +#: editor_actions.cc:2031 msgid "Convert Region Cue Markers to Location Markers" msgstr "Превратить маркеры области в маркеры местоположения" -#: editor_actions.cc:2028 +#: editor_actions.cc:2032 msgid "Add Region Cue Marker" msgstr "Добавить маркер области" -#: editor_actions.cc:2029 +#: editor_actions.cc:2033 msgid "Clear Region Cue Markers" msgstr "Удалить все маркеры области" -#: editor_actions.cc:2030 +#: editor_actions.cc:2034 msgid "Set Sync Position" msgstr "Установить синхронизатор области" -#: editor_actions.cc:2031 +#: editor_actions.cc:2035 msgid "Place Transient" msgstr "Вставить резкий переход" -#: editor_actions.cc:2032 +#: editor_actions.cc:2036 msgid "Trim Start at Edit Point" msgstr "Начало по точке редактирования" -#: editor_actions.cc:2033 +#: editor_actions.cc:2037 msgid "Trim End at Edit Point" msgstr "Конец по точке редактирования" -#: editor_actions.cc:2034 +#: editor_actions.cc:2038 msgid "Align Start" msgstr "Выровнять начала областей" -#: editor_actions.cc:2035 +#: editor_actions.cc:2039 msgid "Align Start Relative" msgstr "Выровнять относительно начал областей" -#: editor_actions.cc:2036 +#: editor_actions.cc:2040 msgid "Align End" msgstr "Выровнять концы областей" -#: editor_actions.cc:2037 +#: editor_actions.cc:2041 msgid "Align End Relative" msgstr "Выровнять относительно концов областей" -#: editor_actions.cc:2038 +#: editor_actions.cc:2042 msgid "Align Sync" msgstr "Выровнять по синхронизаторам областей" -#: editor_actions.cc:2039 +#: editor_actions.cc:2043 msgid "Align Sync Relative" msgstr "Выровнять относительно синхронизаторов областей" -#: editor_actions.cc:2040 editor_actions.cc:2041 +#: editor_actions.cc:2044 editor_actions.cc:2045 msgid "Choose Top..." msgstr "Выбрать верхнюю область..." @@ -6700,7 +6732,7 @@ msgid "Embed all without questions" msgstr "Встроить без лишних вопросов" #: editor_audio_import.cc:707 editor_audio_import.cc:733 -#: export_format_dialog.cc:79 session_dialog.cc:359 sfdb_ui.cc:843 +#: export_format_dialog.cc:79 session_dialog.cc:359 sfdb_ui.cc:846 msgid "Sample Rate" msgstr "Частота сэмплирования" @@ -6838,53 +6870,53 @@ msgstr "Смена длительности фейда нарастания" msgid "change fade out length" msgstr "Смена длительности фейда затухания" -#: editor_drag.cc:4712 +#: editor_drag.cc:4714 msgid "move marker" msgstr "смещение маркера" -#: editor_drag.cc:5013 editor_drag.cc:6483 +#: editor_drag.cc:5015 editor_drag.cc:6494 msgid "automation range move" msgstr "Смещение выделения автоматизации" -#: editor_drag.cc:5397 editor_drag.cc:5447 +#: editor_drag.cc:5399 editor_drag.cc:5449 msgid "An error occurred while executing time stretch operation" msgstr "Произошла ошибка при выполнении операции растяжения времени" -#: editor_drag.cc:5977 +#: editor_drag.cc:5979 msgid "programming_error: %1" msgstr "Ошибка в программе: %1" -#: editor_drag.cc:6042 +#: editor_drag.cc:6044 msgid "new skip marker" msgstr "Новый маркер пропуска" -#: editor_drag.cc:6043 +#: editor_drag.cc:6045 msgid "skip" msgstr "Пропустить" -#: editor_drag.cc:6047 location_ui.cc:67 +#: editor_drag.cc:6049 location_ui.cc:67 msgid "CD" msgstr "CD" -#: editor_drag.cc:6048 +#: editor_drag.cc:6050 msgid "new CD marker" msgstr "Новый CD-маркер" -#: editor_drag.cc:6052 editor_markers.cc:880 +#: editor_drag.cc:6054 editor_markers.cc:880 msgid "new range marker" msgstr "Новая пометка диапазона" -#: editor_drag.cc:6053 editor_route_groups.cc:429 mixer_ui.cc:2479 +#: editor_drag.cc:6055 editor_route_groups.cc:429 mixer_ui.cc:2479 msgid "unnamed" msgstr "Безымянный" -#: editor_drag.cc:6380 +#: editor_drag.cc:6391 msgid "Automation range drag created for invalid region type" msgstr "" "Перетаскивание области автоматизации предпринято для неправильного типа " "области" -#: editor_drag.cc:7140 +#: editor_drag.cc:7151 msgid "Edit Cue Marker Name" msgstr "Изменить название маркера очереди" @@ -6934,7 +6966,7 @@ msgid "Relative Gain Changes?" msgstr "Относительны ли изменения в усилении" #: editor_route_groups.cc:96 editor_regions.cc:87 mixer_strip.cc:1719 -#: meter_strip.cc:388 route_list_base.cc:197 route_time_axis.cc:2412 +#: meter_strip.cc:388 route_list_base.cc:197 route_time_axis.cc:2418 #: time_axis_view.cc:1221 track_record_axis.cc:258 vca_time_axis.cc:64 msgid "Mute|M" msgstr "М" @@ -6944,7 +6976,7 @@ msgid "Sharing Mute?" msgstr "Разделяется ли приглушение" #: editor_route_groups.cc:97 mixer_strip.cc:1733 meter_strip.cc:396 -#: route_list_base.cc:207 route_time_axis.cc:2409 vca_master_strip.cc:237 +#: route_list_base.cc:207 route_time_axis.cc:2415 vca_master_strip.cc:237 #: vca_time_axis.cc:282 msgid "Solo|S" msgstr "С" @@ -6989,7 +7021,7 @@ msgstr "Разделяется ли активный статус" #: editor_markers.cc:1486 editor_markers.cc:1516 editor_markers.cc:1550 #: editor_markers.cc:1581 editor_markers.cc:1606 editor_markers.cc:1656 #: editor_markers.cc:1775 editor_markers.cc:1801 editor_markers.cc:1823 -#: editor_mouse.cc:2552 +#: editor_mouse.cc:2557 msgid "programming error: marker canvas item has no marker object pointer!" msgstr "Ошибка в программе: marker canvas item has no marker object pointer!" @@ -7035,11 +7067,11 @@ msgstr "Префикс для сведенных областей:" msgid "Name for Bounced Region:" msgstr "Название сведенной области:" -#: editor_export_audio.cc:360 editor_ops.cc:4421 +#: editor_export_audio.cc:360 editor_ops.cc:4402 msgid "Bounce to Trigger Slot:" msgstr "Свести в триггерный слот" -#: editor_export_audio.cc:379 editor_ops.cc:4443 +#: editor_export_audio.cc:379 editor_ops.cc:4424 msgid "Bounce to Clip Library" msgstr "Свести в библиотеку клипов" @@ -7047,15 +7079,15 @@ msgstr "Свести в библиотеку клипов" msgid "Bounced Region will appear in the Source list" msgstr "Сведенная область появится в списке источников" -#: editor_export_audio.cc:425 editor_ops.cc:4489 +#: editor_export_audio.cc:425 editor_ops.cc:4470 msgid "Are you sure you want to overwrite the contents in slot %1?" msgstr "Вы действительно хотите перезаписать содержимое слота %1?" -#: editor_export_audio.cc:426 editor_ops.cc:4490 +#: editor_export_audio.cc:426 editor_ops.cc:4471 msgid "Overwriting slot" msgstr "Слот переписывается" -#: editor_export_audio.cc:427 editor_ops.cc:4491 +#: editor_export_audio.cc:427 editor_ops.cc:4472 msgid "One of your selected tracks has content in this slot." msgstr "" @@ -7079,11 +7111,11 @@ msgstr "создание петли из области" msgid "set punch range" msgstr "создание врезки из выделения" -#: editor_markers.cc:877 editor_ops.cc:4743 editor_ops.cc:7673 +#: editor_markers.cc:877 editor_ops.cc:4724 editor_ops.cc:7703 msgid "range" msgstr "Диапазон" -#: editor_markers.cc:933 editor_ops.cc:2481 location_ui.cc:874 +#: editor_markers.cc:933 editor_ops.cc:2447 location_ui.cc:874 msgid "remove marker" msgstr "Удаление пометки" @@ -7179,7 +7211,7 @@ msgstr "" msgid "set tempo to ramp to next" msgstr "" -#: editor_markers.cc:1850 editor_ops.cc:2287 +#: editor_markers.cc:1850 editor_ops.cc:2253 msgid "New Name:" msgstr "Новое название:" @@ -7195,8 +7227,8 @@ msgstr "Переименовать выделение" msgid "Rename Mark" msgstr "Переименовать пометку" -#: editor_markers.cc:1864 editor_mouse.cc:2569 mixer_ui.cc:4130 -#: mixer_ui.cc:4165 processor_box.cc:3608 processor_box.cc:4195 +#: editor_markers.cc:1864 editor_mouse.cc:2574 mixer_ui.cc:4130 +#: mixer_ui.cc:4165 processor_box.cc:3610 processor_box.cc:4197 #: route_ui.cc:1723 route_ui.cc:2856 template_dialog.cc:226 #: vca_master_strip.cc:482 msgid "Rename" @@ -7219,29 +7251,29 @@ msgstr "Экран недостаточно высок, чтобы показа msgid "Editor/Snap" msgstr "Редактор/Прилипание" -#: editor_mouse.cc:1572 editor_mouse.cc:1607 editor_tempodisplay.cc:585 +#: editor_mouse.cc:1577 editor_mouse.cc:1612 editor_tempodisplay.cc:585 msgid "" "programming error: tempo marker canvas item has no marker object pointer!" msgstr "" "Ошибка в программе: tempo marker canvas item has no marker object pointer!" -#: editor_mouse.cc:1577 editor_tempodisplay.cc:590 +#: editor_mouse.cc:1582 editor_tempodisplay.cc:590 msgid "programming error: marker for tempo is not a tempo marker!" msgstr "Ошибка в программе: marker for tempo is not a tempo marker!" -#: editor_mouse.cc:1589 editor_tempodisplay.cc:566 +#: editor_mouse.cc:1594 editor_tempodisplay.cc:566 msgid "programming error: bbt marker canvas item has no marker object pointer!" msgstr "" -#: editor_mouse.cc:1594 editor_tempodisplay.cc:571 +#: editor_mouse.cc:1599 editor_tempodisplay.cc:571 msgid "programming error: marker for bbt is not a bbt marker!" msgstr "" -#: editor_mouse.cc:1612 editor_tempodisplay.cc:808 +#: editor_mouse.cc:1617 editor_tempodisplay.cc:808 msgid "programming error: marker for meter is not a meter marker!" msgstr "Ошибка в программе: пометка размера таковым не является!" -#: editor_mouse.cc:2296 editor_mouse.cc:2321 editor_mouse.cc:2334 +#: editor_mouse.cc:2301 editor_mouse.cc:2326 editor_mouse.cc:2339 msgid "" "programming error: control point canvas item has no control point object " "pointer!" @@ -7249,23 +7281,23 @@ msgstr "" "ошибка в программе: у контр. точки пункта события нет управления точкой " "объектауказатель!" -#: editor_mouse.cc:2490 +#: editor_mouse.cc:2495 msgid "start point trim" msgstr "начальная точка обрезки" -#: editor_mouse.cc:2515 +#: editor_mouse.cc:2520 msgid "end point trim" msgstr "конечная точка обрезки" -#: editor_mouse.cc:2567 +#: editor_mouse.cc:2572 msgid "Name for region:" msgstr "Название области: " -#: editor_mouse.cc:2974 +#: editor_mouse.cc:2979 msgid "tempo mapping: end-stretch" msgstr "" -#: editor_mouse.cc:2980 editor_mouse.cc:2984 +#: editor_mouse.cc:2985 editor_mouse.cc:2989 msgid "tempo mapping: mid-twist" msgstr "" @@ -7309,190 +7341,204 @@ msgstr "Толчок назад" msgid "sequence regions" msgstr "выстраивание областей встык" -#: editor_ops.cc:2290 location_ui.cc:737 +#: editor_ops.cc:2256 location_ui.cc:737 msgid "New Range" msgstr "Создать диапазон" -#: editor_ops.cc:2292 +#: editor_ops.cc:2258 msgid "New Location Marker" msgstr "Новая пометка позиции" -#: editor_ops.cc:2340 editor_ops.cc:2383 editor_ops.cc:2520 editor_ops.cc:2557 +#: editor_ops.cc:2306 editor_ops.cc:2349 editor_ops.cc:2486 editor_ops.cc:2523 #: location_ui.cc:1074 msgid "add marker" msgstr "добавление маркера" -#: editor_ops.cc:2368 +#: editor_ops.cc:2334 msgid "cue %1" msgstr "очередь %1" -#: editor_ops.cc:2370 +#: editor_ops.cc:2336 msgid "section" msgstr "" -#: editor_ops.cc:2372 +#: editor_ops.cc:2338 msgid "cd trk" msgstr "" -#: editor_ops.cc:2374 +#: editor_ops.cc:2340 msgid "mark" msgstr "пометка" -#: editor_ops.cc:2409 +#: editor_ops.cc:2375 msgid "Set session start" msgstr "Установить начало сессии" -#: editor_ops.cc:2435 +#: editor_ops.cc:2401 msgid "Set session end" msgstr "Установить конец сессии" -#: editor_ops.cc:2520 +#: editor_ops.cc:2486 msgid "add markers" msgstr "добавление маркеров" -#: editor_ops.cc:2627 +#: editor_ops.cc:2593 msgid "clear markers" msgstr "Очистка пометок" -#: editor_ops.cc:2646 +#: editor_ops.cc:2612 msgid "clear xrun markers" msgstr "Очистка маркеров рассинхронизации" -#: editor_ops.cc:2666 +#: editor_ops.cc:2632 msgid "clear ranges" msgstr "Очистка диапазонов" -#: editor_ops.cc:2688 +#: editor_ops.cc:2654 msgid "clear cues" msgstr "" -#: editor_ops.cc:2706 editor_ops.cc:2722 +#: editor_ops.cc:2672 editor_ops.cc:2688 msgid "clear locations" msgstr "Очистка позиций" -#: editor_ops.cc:2795 +#: editor_ops.cc:2761 msgid "insert region" msgstr "Вставка области" -#: editor_ops.cc:3054 +#: editor_ops.cc:2786 +msgid "" +"Cut/Copy Section does not yet correctly include tempo/meter changes\n" +"Do you still want to proceed?" +msgstr "" + +#: editor_ops.cc:2787 +msgid "Cut/Copy Tempo Map" +msgstr "" + +#: editor_ops.cc:2788 +msgid "Do not show this dialog again." +msgstr "Больше не показывать этот диалог" + +#: editor_ops.cc:3035 msgid "raise regions" msgstr "Поднятие областей" -#: editor_ops.cc:3056 +#: editor_ops.cc:3037 msgid "raise region" msgstr "Поднятие области" -#: editor_ops.cc:3062 +#: editor_ops.cc:3043 msgid "raise regions to top" msgstr "Поднятие областей наверх" -#: editor_ops.cc:3064 +#: editor_ops.cc:3045 msgid "raise region to top" msgstr "Поднятие области наверх" -#: editor_ops.cc:3070 +#: editor_ops.cc:3051 msgid "lower regions" msgstr "Опускание областей" -#: editor_ops.cc:3072 editor_ops.cc:3080 +#: editor_ops.cc:3053 editor_ops.cc:3061 msgid "lower region" msgstr "Опускание области" -#: editor_ops.cc:3078 +#: editor_ops.cc:3059 msgid "lower regions to bottom" msgstr "Опускание областей вниз" -#: editor_ops.cc:3163 +#: editor_ops.cc:3144 msgid "Rename Region" msgstr "Переименовать область..." -#: editor_ops.cc:3165 processor_box.cc:3606 route_ui.cc:1721 +#: editor_ops.cc:3146 processor_box.cc:3608 route_ui.cc:1721 msgid "New name:" msgstr "Новое название:" -#: editor_ops.cc:3201 +#: editor_ops.cc:3182 msgid "Rename failed. Check for characters such as '/' or ':'" msgstr "" -#: editor_ops.cc:3230 +#: editor_ops.cc:3211 msgid "group regions" msgstr "группировка областей" -#: editor_ops.cc:3248 +#: editor_ops.cc:3229 msgid "ungroup regions" msgstr "разгруппировка областей" -#: editor_ops.cc:3486 +#: editor_ops.cc:3467 msgid "separate" msgstr "разделение" -#: editor_ops.cc:3623 +#: editor_ops.cc:3604 msgid "separate region under" msgstr "разделение области под курсором" -#: editor_ops.cc:3692 +#: editor_ops.cc:3673 msgid "Crop Regions to Edit Range" msgstr "Обрезать области по диапазону редактирования" -#: editor_ops.cc:3857 +#: editor_ops.cc:3838 msgid "set sync point" msgstr "Установка точки синхронизации" -#: editor_ops.cc:3881 +#: editor_ops.cc:3862 msgid "remove region sync" msgstr "Удаление синхронизатора области" -#: editor_ops.cc:3903 +#: editor_ops.cc:3884 msgid "move regions to original position" msgstr "Перемещение областей в исходную позицию" -#: editor_ops.cc:3905 +#: editor_ops.cc:3886 msgid "move region to original position" msgstr "Перемещение области в исходную позицию" -#: editor_ops.cc:3926 +#: editor_ops.cc:3907 msgid "align selection" msgstr "Выравнивание выделения" -#: editor_ops.cc:4000 +#: editor_ops.cc:3981 msgid "align selection (relative)" msgstr "Выравнивание выделения (относительное)" -#: editor_ops.cc:4034 +#: editor_ops.cc:4015 msgid "align region" msgstr "Выравнивание области" -#: editor_ops.cc:4085 +#: editor_ops.cc:4066 msgid "trim front" msgstr "Обрезка впереди" -#: editor_ops.cc:4085 +#: editor_ops.cc:4066 msgid "trim back" msgstr "Обрезка сзади" -#: editor_ops.cc:4113 +#: editor_ops.cc:4094 msgid "trim to loop" msgstr "Обрезка в петлю" -#: editor_ops.cc:4123 +#: editor_ops.cc:4104 msgid "trim to punch" msgstr "Обрезка во врезку" -#: editor_ops.cc:4230 +#: editor_ops.cc:4211 msgid "trim to region" msgstr "Обрезка в область" -#: editor_ops.cc:4287 +#: editor_ops.cc:4268 msgid "" "Transport cannot be stopped, likely due to external timecode sync.\n" "Freezing a track requires the transport to be stopped." msgstr "" -#: editor_ops.cc:4290 editor_ops.cc:4304 +#: editor_ops.cc:4271 editor_ops.cc:4285 msgid "Cannot freeze" msgstr "Невозможно заморозить" -#: editor_ops.cc:4301 +#: editor_ops.cc:4282 msgid "" "This track/bus cannot be frozen because the signal adds or loses channels " "before reaching the outputs.\n" @@ -7504,7 +7550,7 @@ msgstr "" "Это, как правило, вызвано плагинами, которые генерируют выходной " "стереосигнал из моновхода или наоборот." -#: editor_ops.cc:4310 +#: editor_ops.cc:4291 msgid "" "%1\n" "\n" @@ -7515,23 +7561,23 @@ msgid "" "sidechain." msgstr "" -#: editor_ops.cc:4314 +#: editor_ops.cc:4295 msgid "Freeze anyway" msgstr "Всё равно заморозить" -#: editor_ops.cc:4315 +#: editor_ops.cc:4296 msgid "Don't freeze" msgstr "Не замораживать" -#: editor_ops.cc:4316 +#: editor_ops.cc:4297 msgid "Freeze Limits" msgstr "Пределы заморозки" -#: editor_ops.cc:4331 +#: editor_ops.cc:4312 msgid "Cancel Freeze" msgstr "Отменить замораживание" -#: editor_ops.cc:4366 +#: editor_ops.cc:4347 msgid "" "You can't perform this operation because the processing of the signal will " "cause one or more of the tracks to end up with a region with more channels " @@ -7545,63 +7591,67 @@ msgstr "" "\n" "Вы можете сделать это без обработки, и это уже другая операция." -#: editor_ops.cc:4370 +#: editor_ops.cc:4351 msgid "Cannot bounce" msgstr "Невозможно выполнить сведение" -#: editor_ops.cc:4402 +#: editor_ops.cc:4383 msgid "Name for Bounced Range:" msgstr "Название сведенного выделения:" -#: editor_ops.cc:4450 +#: editor_ops.cc:4431 msgid "Bounced Range will appear in the Source list" msgstr "" -#: editor_ops.cc:4541 +#: editor_ops.cc:4522 msgid "bounce range" msgstr "сведение области" -#: editor_ops.cc:4601 +#: editor_ops.cc:4582 msgid "delete control points" msgstr "удаление контрольной точки" -#: editor_ops.cc:4663 +#: editor_ops.cc:4644 msgid "delete" msgstr "Удаление" -#: editor_ops.cc:4666 +#: editor_ops.cc:4647 msgid "cut" msgstr "Вырезать" -#: editor_ops.cc:4669 +#: editor_ops.cc:4650 msgid "copy" msgstr "Копировать" -#: editor_ops.cc:4672 +#: editor_ops.cc:4653 msgid "clear" msgstr "Очистить" -#: editor_ops.cc:4716 +#: editor_ops.cc:4697 msgid "objects" msgstr "объекты" -#: editor_ops.cc:4943 editor_ops.cc:5062 +#: editor_ops.cc:4924 msgid "remove region" msgstr "Удаление области" -#: editor_ops.cc:4966 +#: editor_ops.cc:4946 msgid "recover regions" msgstr "" -#: editor_ops.cc:5638 +#: editor_ops.cc:5009 +msgid "remove regions" +msgstr "" + +#: editor_ops.cc:5620 msgid "duplicate range selection" msgstr "повторить диапазон выделения" -#: editor_ops.cc:5730 +#: editor_ops.cc:5712 msgid "nudge track" msgstr "Смещение дорожки" -#: editor_ops.cc:5757 +#: editor_ops.cc:5739 msgid "" "Do you really want to destroy the last capture?\n" "(This is destructive and cannot be undone)" @@ -7609,181 +7659,189 @@ msgstr "" "Последнюю запись будет удалена. Вы уверены?\n" "(отмена операции невозможна)" -#: editor_ops.cc:5760 editor_ops.cc:8489 editor_regions.cc:275 +#: editor_ops.cc:5742 editor_ops.cc:8519 editor_regions.cc:275 #: editor_snapshots.cc:187 editor_sources.cc:186 vca_master_strip.cc:532 msgid "No, do nothing." msgstr "Нет, ничего не делать." -#: editor_ops.cc:5761 +#: editor_ops.cc:5743 msgid "Yes, destroy it." msgstr "Да, уничтожить." -#: editor_ops.cc:5763 +#: editor_ops.cc:5745 msgid "Destroy last capture" msgstr "Уничтожение последней записи" -#: editor_ops.cc:5781 +#: editor_ops.cc:5763 msgid "Tag:" msgstr "Метка:" -#: editor_ops.cc:5796 session_archive_dialog.cc:51 session_archive_dialog.cc:52 -#: session_archive_dialog.cc:225 sfdb_ui.cc:1991 sfdb_ui.cc:2111 +#: editor_ops.cc:5778 session_archive_dialog.cc:51 session_archive_dialog.cc:52 +#: session_archive_dialog.cc:225 sfdb_ui.cc:1996 sfdb_ui.cc:2116 msgid "Good" msgstr "Хорошее" -#: editor_ops.cc:5999 +#: editor_ops.cc:5981 msgid "normalize" msgstr "нормировка" -#: editor_ops.cc:6111 +#: editor_ops.cc:6093 msgid "reverse regions" msgstr "Разворот областей" -#: editor_ops.cc:6148 +#: editor_ops.cc:6130 msgid "strip silence" msgstr "Удаление тишины" -#: editor_ops.cc:6268 editor_ops.cc:6317 +#: editor_ops.cc:6250 editor_ops.cc:6299 msgid "Could not unlink %1" msgstr "Не удалось отсоединить %1" -#: editor_ops.cc:6310 +#: editor_ops.cc:6292 msgid "Fork Region(s)" msgstr "Ответвление областей" -#: editor_ops.cc:6393 +#: editor_ops.cc:6375 msgid "de-interlace midi" msgstr "" -#: editor_ops.cc:6636 +#: editor_ops.cc:6618 msgid "reset region gain" msgstr "Сброс усиления области" -#: editor_ops.cc:6695 +#: editor_ops.cc:6677 msgid "region polarity invert" msgstr "инвертирование полярности области" -#: editor_ops.cc:6729 +#: editor_ops.cc:6711 msgid "region gain envelope active" msgstr "Огибающая области активна" -#: editor_ops.cc:6754 +#: editor_ops.cc:6736 +msgid "region lock" +msgstr "блокировка области" + +#: editor_ops.cc:6760 +msgid "region unlock" +msgstr "разблокировка области" + +#: editor_ops.cc:6784 msgid "toggle region lock" msgstr "Переключение блокировки области" -#: editor_ops.cc:6778 +#: editor_ops.cc:6808 msgid "Toggle Video Lock" msgstr "Переключить видеоблокировку" -#: editor_ops.cc:6802 +#: editor_ops.cc:6832 msgid "change region opacity" msgstr "Смена прозрачности области" -#: editor_ops.cc:6955 +#: editor_ops.cc:6985 msgid "fade range" msgstr "Диапазон фейда" -#: editor_ops.cc:6993 +#: editor_ops.cc:7023 msgid "set fade in length" msgstr "Установка длины фейда нарастания" -#: editor_ops.cc:7000 +#: editor_ops.cc:7030 msgid "set fade out length" msgstr "Установка длины фейда затухания" -#: editor_ops.cc:7065 +#: editor_ops.cc:7095 msgid "set fade in shape" msgstr "Установка формы фейда нарастания" -#: editor_ops.cc:7100 +#: editor_ops.cc:7130 msgid "set fade out shape" msgstr "Установка формы фейда затухания" -#: editor_ops.cc:7136 +#: editor_ops.cc:7166 msgid "set fade in active" msgstr "Установка активности фейда нарастания" -#: editor_ops.cc:7170 +#: editor_ops.cc:7200 msgid "set fade out active" msgstr "Установка активности фейда затухания" -#: editor_ops.cc:7230 +#: editor_ops.cc:7260 msgid "toggle fade active" msgstr "переключение активности фейда" -#: editor_ops.cc:7397 +#: editor_ops.cc:7427 msgid "set loop range from selection" msgstr "Установка петли из выделения" -#: editor_ops.cc:7411 +#: editor_ops.cc:7441 msgid "set loop range from region" msgstr "Установка петли из области" -#: editor_ops.cc:7429 +#: editor_ops.cc:7459 msgid "set punch range from selection" msgstr "Установка врезки из выделения" -#: editor_ops.cc:7453 +#: editor_ops.cc:7483 msgid "Auto Punch In" msgstr "Начало автоврезки" -#: editor_ops.cc:7460 editor_ops.cc:7464 +#: editor_ops.cc:7490 editor_ops.cc:7494 msgid "Auto Punch In/Out" msgstr "Начало/конец автоврезки" -#: editor_ops.cc:7506 +#: editor_ops.cc:7536 msgid "set session start/end from selection" msgstr "Установка начала/конца сессии из выделения" -#: editor_ops.cc:7541 +#: editor_ops.cc:7571 msgid "set punch start from EP" msgstr "" -#: editor_ops.cc:7565 +#: editor_ops.cc:7595 msgid "set punch end from EP" msgstr "" -#: editor_ops.cc:7596 +#: editor_ops.cc:7626 msgid "set loop start from EP" msgstr "" -#: editor_ops.cc:7621 +#: editor_ops.cc:7651 msgid "set loop end from EP" msgstr "" -#: editor_ops.cc:7632 +#: editor_ops.cc:7662 msgid "set punch range from region" msgstr "Установка врезки из области" -#: editor_ops.cc:7665 +#: editor_ops.cc:7695 msgid "region" msgstr "область" -#: editor_ops.cc:7719 +#: editor_ops.cc:7749 msgid "Add new marker" msgstr "Создать пометку" -#: editor_ops.cc:7720 +#: editor_ops.cc:7750 msgid "Set global tempo" msgstr "Установить общий темп" -#: editor_ops.cc:7723 +#: editor_ops.cc:7753 msgid "Define one bar" msgstr "Определение такта" -#: editor_ops.cc:7724 +#: editor_ops.cc:7754 msgid "Do you want to set the global tempo or add a new tempo marker?" msgstr "Вы хотите установить общий темп или добавить новую пометку темпа?" -#: editor_ops.cc:7750 +#: editor_ops.cc:7780 msgid "set tempo from %1" msgstr "установить темп из %1" -#: editor_ops.cc:7774 +#: editor_ops.cc:7804 msgid "split regions" msgstr "разделение областей" -#: editor_ops.cc:7816 +#: editor_ops.cc:7846 msgid "" "You are about to split\n" "%1\n" @@ -7795,11 +7853,11 @@ msgstr "" "на %2 частей.\n" "Это может занять много времени." -#: editor_ops.cc:7823 +#: editor_ops.cc:7853 msgid "Call for the Ferret!" msgstr "Позвать Хорька!" -#: editor_ops.cc:7824 +#: editor_ops.cc:7854 msgid "" "Press OK to continue with this split operation\n" "or ask the Ferret dialog to tune the analysis" @@ -7807,47 +7865,47 @@ msgstr "" "Нажмите OK для выполнения разделения\n" "или попросите Хорька скорректировать анализ." -#: editor_ops.cc:7826 +#: editor_ops.cc:7856 msgid "Press OK to continue with this split operation" msgstr "Нажмите OK для выполнения разделения" -#: editor_ops.cc:7829 +#: editor_ops.cc:7859 msgid "Excessive split?" msgstr "Массовое разделение?" -#: editor_ops.cc:7988 +#: editor_ops.cc:8018 msgid "place transient" msgstr "вставка резкого перехода" -#: editor_ops.cc:8022 +#: editor_ops.cc:8052 msgid "snap regions to grid" msgstr "привязка областей к сетке" -#: editor_ops.cc:8063 +#: editor_ops.cc:8093 msgid "Close Region Gaps" msgstr "Закрытие интервалов между областями" -#: editor_ops.cc:8068 +#: editor_ops.cc:8098 msgid "Crossfade length" msgstr "Длительность кроссфейда" -#: editor_ops.cc:8079 +#: editor_ops.cc:8109 msgid "Pull-back length" msgstr "Растяжка длины назад" -#: editor_ops.cc:8092 +#: editor_ops.cc:8122 msgid "Ok" msgstr "ОК" -#: editor_ops.cc:8111 +#: editor_ops.cc:8141 msgid "close region gaps" msgstr "устранение пробелов области" -#: editor_ops.cc:8419 +#: editor_ops.cc:8449 msgid "That would be bad news ...." msgstr "Это было бы плохой новостью..." -#: editor_ops.cc:8423 +#: editor_ops.cc:8453 msgid "" "Removing the master or monitor bus is such a bad idea\n" "that %1 is not going to allow it.\n" @@ -7863,184 +7921,184 @@ msgstr "" "подобные вещи, в файле ardour.rc измените значение параметра\n" "\"allow-special-bus-removal\" на \"yes\"" -#: editor_ops.cc:8440 +#: editor_ops.cc:8470 msgid "track" msgid_plural "tracks" msgstr[0] "дорожка" msgstr[1] "дорожки" msgstr[2] "дорожек" -#: editor_ops.cc:8441 +#: editor_ops.cc:8471 msgid "bus" msgid_plural "busses" msgstr[0] "шина" msgstr[1] "шины" msgstr[2] "шин" -#: editor_ops.cc:8442 +#: editor_ops.cc:8472 msgid "VCA" msgid_plural "VCAs" msgstr[0] "VCA" msgstr[1] "VCA" msgstr[2] "VCA" -#: editor_ops.cc:8445 +#: editor_ops.cc:8475 msgid "Remove various strips" msgstr "" -#: editor_ops.cc:8446 +#: editor_ops.cc:8476 msgid "Do you really want to remove %1 %2, %3 %4 and %5 %6?" msgstr "Вы действительно хотите удалить %1 %2, %3 %4 и %5 %6?" -#: editor_ops.cc:8450 editor_ops.cc:8455 editor_ops.cc:8460 +#: editor_ops.cc:8480 editor_ops.cc:8485 editor_ops.cc:8490 msgid "Remove %1 and %2" msgstr "Удалить %1 и %2" -#: editor_ops.cc:8451 editor_ops.cc:8456 editor_ops.cc:8461 +#: editor_ops.cc:8481 editor_ops.cc:8486 editor_ops.cc:8491 msgid "Do you really want to remove %1 %2 and %3 %4?" msgstr "Вы действительно хотите удалить %1 %2 и %3 %4?" -#: editor_ops.cc:8465 editor_ops.cc:8470 editor_ops.cc:8475 +#: editor_ops.cc:8495 editor_ops.cc:8500 editor_ops.cc:8505 #: vca_master_strip.cc:527 msgid "Remove %1" msgstr "Удалить %1" -#: editor_ops.cc:8466 editor_ops.cc:8471 editor_ops.cc:8476 +#: editor_ops.cc:8496 editor_ops.cc:8501 editor_ops.cc:8506 msgid "Do you really want to remove %1 %2?" msgstr "Вы действительно хотите удалить %1 %2?" -#: editor_ops.cc:8484 +#: editor_ops.cc:8514 msgid "You may also lose the playlists associated with the %1" msgstr "Вы также можете потерять плейлисты, ассоциированные с %1" -#: editor_ops.cc:8487 +#: editor_ops.cc:8517 msgid "This action cannot be undone, and the session file will be overwritten!" msgstr "Это действие не может быть отменено, файл сессии будет перезаписан!" -#: editor_ops.cc:8491 +#: editor_ops.cc:8521 msgid "Yes, remove them." msgstr "Да, удалить их." -#: editor_ops.cc:8493 editor_snapshots.cc:188 vca_master_strip.cc:533 +#: editor_ops.cc:8523 editor_snapshots.cc:188 vca_master_strip.cc:533 msgid "Yes, remove it." msgstr "Да, удалить" -#: editor_ops.cc:8545 +#: editor_ops.cc:8575 msgid "You must first select some tracks to Insert Time." msgstr "Для вставки промежутка времени надо сначала выбрать дорожки." -#: editor_ops.cc:8552 +#: editor_ops.cc:8582 msgid "You cannot insert time in Lock Edit mode." msgstr "Невозможно вставить время в режиме блокировки редактирования" -#: editor_ops.cc:8591 editor_ops.cc:8624 editor_ops.cc:8646 editor_ops.cc:8685 -#: editor_ops.cc:8695 editor_ops.cc:8702 +#: editor_ops.cc:8621 editor_ops.cc:8654 editor_ops.cc:8676 editor_ops.cc:8715 +#: editor_ops.cc:8725 editor_ops.cc:8732 msgid "insert time" msgstr "Вставка времени" -#: editor_ops.cc:8716 +#: editor_ops.cc:8746 msgid "You must first select some tracks to Remove Time." msgstr "Для удаления промежутка времени надо сначала выбрать дорожки." -#: editor_ops.cc:8723 +#: editor_ops.cc:8753 msgid "You cannot remove time in Lock Edit mode." msgstr "Невозможно удалить время в режиме блокировки редактирования" -#: editor_ops.cc:8758 +#: editor_ops.cc:8788 msgid "Cannot insert or delete time when in Lock edit." msgstr "Невозможно вставить или удалить время в режиме блокировки." -#: editor_ops.cc:8772 editor_ops.cc:8791 editor_ops.cc:8862 editor_ops.cc:8876 -#: editor_ops.cc:8880 +#: editor_ops.cc:8802 editor_ops.cc:8821 editor_ops.cc:8892 editor_ops.cc:8906 +#: editor_ops.cc:8910 msgid "remove time" msgstr "удаление времени" -#: editor_ops.cc:8951 +#: editor_ops.cc:8981 msgid "There are too many tracks to fit in the current window" msgstr "Такое количество дорожек в окне не поместится" -#: editor_ops.cc:9016 +#: editor_ops.cc:9046 msgid "Sel" msgstr "Выб." -#: editor_ops.cc:9055 +#: editor_ops.cc:9085 #, c-format msgid "Saved view %u" msgstr "Сохраненный вид %u" -#: editor_ops.cc:9080 +#: editor_ops.cc:9110 msgid "mute regions" msgstr "приглушение областей" -#: editor_ops.cc:9082 +#: editor_ops.cc:9112 msgid "mute region" msgstr "приглушение области" -#: editor_ops.cc:9119 +#: editor_ops.cc:9149 msgid "combine regions" msgstr "Объединение областей" -#: editor_ops.cc:9157 +#: editor_ops.cc:9187 msgid "uncombine regions" msgstr "Разъединение областей" -#: editor_ops.cc:9196 +#: editor_ops.cc:9226 msgid "%1: Locked" msgstr "%1: заблокировано" -#: editor_ops.cc:9204 +#: editor_ops.cc:9234 msgid "Click to unlock" msgstr "Снять замок" -#: editor_ops.cc:9256 +#: editor_ops.cc:9286 msgid "Moving embedded files into session folder" msgstr "Перемещение встроенных файлов в папке сессии" -#: editor_ops.cc:9438 +#: editor_ops.cc:9468 msgid "New Cue Marker Name" msgstr "Название нового маркера очереди" -#: editor_ops.cc:9487 +#: editor_ops.cc:9517 msgid "add cue marker" msgstr "добавка маркера очереди" -#: editor_ops.cc:9529 +#: editor_ops.cc:9559 msgid "remove cue marker" msgstr "" -#: editor_ops.cc:9589 +#: editor_ops.cc:9619 msgid "clear cue markers" msgstr "очистка маркеров очередей" -#: editor_ops.cc:9641 +#: editor_ops.cc:9671 msgid "region markers -> global markers" msgstr "маркеры области -> глобальные маркеры" -#: editor_ops.cc:9653 +#: editor_ops.cc:9683 msgid "Smallest gap size to remove (seconds):" msgstr "Минимальный удаляемый интервал (секунды):" -#: editor_ops.cc:9662 +#: editor_ops.cc:9692 msgid "Leave a gap of(seconds):" msgstr "Оставить интервал (секунд):" -#: editor_ops.cc:9670 +#: editor_ops.cc:9700 msgid "Shift global markers too" msgstr "" -#: editor_ops.cc:9693 +#: editor_ops.cc:9723 msgid "The threshold value you entered is not a number" msgstr "" -#: editor_ops.cc:9699 editor_ops.cc:9715 +#: editor_ops.cc:9729 editor_ops.cc:9745 msgid "The threshold value must be larger than or equal to zero" msgstr "" -#: editor_ops.cc:9709 +#: editor_ops.cc:9739 msgid "The leave-gap value you entered is not a number" msgstr "Значение оставляемого интервала не является числом" -#: editor_ops.cc:9779 +#: editor_ops.cc:9809 msgid "remove gaps" msgstr "Удалить пробелы" @@ -8530,11 +8588,11 @@ msgstr "" "\n" "(Это ошибка сборки/упаковки/системы, она никогда не должна происходить.)" -#: engine_dialog.cc:162 rc_option_editor.cc:4348 +#: engine_dialog.cc:162 rc_option_editor.cc:4359 msgid "Audio Hardware" msgstr "Звуковое оборудование" -#: engine_dialog.cc:167 rc_option_editor.cc:4342 +#: engine_dialog.cc:167 rc_option_editor.cc:4353 msgid "Audio Driver" msgstr "Звуковой движок" @@ -9182,6 +9240,7 @@ msgid "Add silence at end:" msgstr "Добавить тишину в конец:" #: export_format_dialog.cc:73 +#, c-format msgid "" "Command to run post-export\n" "(%f=file path, %d=directory, %b=basename; see tooltip for more,\n" @@ -9538,7 +9597,7 @@ msgstr "Показать время как:" msgid "Realtime Export" msgstr "Экспорт в реальном времени" -#: export_timespan_selector.cc:60 processor_box.cc:4197 +#: export_timespan_selector.cc:60 processor_box.cc:4199 msgid "Select All" msgstr "Выделить всё" @@ -9633,7 +9692,7 @@ msgstr "Выходы..." msgid "Save As Template..." msgstr "Сохранить как шаблон..." -#: foldback_strip.cc:807 mixer_strip.cc:1103 route_group_dialog.cc:47 +#: foldback_strip.cc:807 mixer_strip.cc:1103 route_group_dialog.cc:48 #: route_time_axis.cc:876 trigger_strip.cc:271 msgid "Active" msgstr "Активно" @@ -9691,7 +9750,7 @@ msgstr "" msgid "programming error: %1\n" msgstr "" -#: sfdb_freesound_mootcher.cc:584 rc_option_editor.cc:4347 +#: sfdb_freesound_mootcher.cc:584 rc_option_editor.cc:4358 msgid "%1" msgstr "%1" @@ -10031,7 +10090,7 @@ msgstr "ВХОД в %1" msgid "OUTPUT from %1" msgstr "ВЫХОД из %1" -#: io_button.cc:246 rc_option_editor.cc:1410 transport_masters_dialog.cc:393 +#: io_button.cc:246 rc_option_editor.cc:1410 transport_masters_dialog.cc:394 msgid "Disconnected" msgstr "Нет соединения" @@ -10068,14 +10127,14 @@ msgstr "" "Сделайте двойной щелчок или щелчок правой\n" "клавишей мыши, чтобы добавить плагины I/O" -#: io_plugin_window.cc:281 region_editor.cc:953 +#: io_plugin_window.cc:281 region_editor.cc:958 msgid "" "%1\n" "Double-click to show GUI.\n" "%2+double-click to show generic GUI." msgstr "" -#: io_plugin_window.cc:283 processor_box.cc:560 processor_box.cc:1789 +#: io_plugin_window.cc:283 processor_box.cc:562 processor_box.cc:1791 msgid "" "%1\n" "Double-click to show generic GUI.%2" @@ -10083,7 +10142,7 @@ msgstr "" "%1\n" "Двойной щелчок, чтобы показать общий интерфейс.%2" -#: io_plugin_window.cc:358 processor_box.cc:4230 region_editor.cc:638 +#: io_plugin_window.cc:358 processor_box.cc:4232 region_editor.cc:639 msgid "Edit with generic controls..." msgstr "Изменить с интерфейсом хоста..." @@ -10174,7 +10233,7 @@ msgstr "сэмпл" msgid "period" msgstr "Период" -#: latency_gui.cc:177 rhythm_ferret.cc:312 sfdb_ui.cc:2173 +#: latency_gui.cc:177 rhythm_ferret.cc:312 sfdb_ui.cc:2178 msgid "programming error: %1 (%2)" msgstr "Ошибка в программе: %1 (%2)" @@ -10186,11 +10245,11 @@ msgstr "Скачивание лупов" msgid "Author" msgstr "Автор" -#: library_download_dialog.cc:55 sfdb_ui.cc:844 +#: library_download_dialog.cc:55 sfdb_ui.cc:847 msgid "License" msgstr "Лицензия" -#: library_download_dialog.cc:56 sfdb_ui.cc:842 +#: library_download_dialog.cc:56 sfdb_ui.cc:845 msgid "Size" msgstr "Размер" @@ -10471,7 +10530,7 @@ msgstr "Скрипты сессии" msgid "Action %1" msgstr "Действие %1" -#: lua_script_manager.cc:225 lua_script_manager.cc:307 rc_option_editor.cc:4848 +#: lua_script_manager.cc:225 lua_script_manager.cc:307 rc_option_editor.cc:4859 msgid "Unset" msgstr "Снять" @@ -10763,7 +10822,7 @@ msgstr "рисование автоматизации" #: midi_channel_selector.cc:435 rc_option_editor.cc:2713 recorder_ui.cc:83 #: session_archive_dialog.cc:44 session_archive_dialog.cc:49 #: session_archive_dialog.cc:197 session_archive_dialog.cc:208 -#: session_archive_dialog.cc:222 sfdb_ui.cc:793 trigger_ui.cc:706 +#: session_archive_dialog.cc:222 sfdb_ui.cc:796 trigger_ui.cc:706 #: trigger_ui.cc:739 msgid "None" msgstr "Нет" @@ -10988,7 +11047,7 @@ msgstr "Перемещение ноты" msgid "copy notes" msgstr "" -#: midi_region_view.cc:3415 velocity_ghost_region.cc:368 +#: midi_region_view.cc:3415 velocity_ghost_region.cc:374 msgid "draw velocities" msgstr "рисование силы нажатия" @@ -11411,7 +11470,7 @@ msgid "AudioUnit and VST" msgstr "AudioUnit и VST" #: missing_plugin_dialog.cc:60 plugin_selector.cc:1205 plugin_selector.cc:1213 -#: rc_option_editor.cc:4096 +#: rc_option_editor.cc:4107 msgid "VST" msgstr "VST" @@ -11550,7 +11609,7 @@ msgstr "Другие положения для записи и воспроиз msgid "Disk I/O..." msgstr "I/O диска…" -#: mixer_strip.cc:1133 processor_box.cc:4212 trigger_strip.cc:291 +#: mixer_strip.cc:1133 processor_box.cc:4214 trigger_strip.cc:291 msgid "Pin Connections..." msgstr "Порты плагина..." @@ -11566,7 +11625,7 @@ msgstr "Раскидать по дорожкам" msgid "Duplicate..." msgstr "Продублировать..." -#: mixer_strip.cc:1350 processor_box.cc:4222 +#: mixer_strip.cc:1350 processor_box.cc:4224 msgid "Custom LAN Amp Position" msgstr "Другое положение усилителя LAN" @@ -11632,7 +11691,7 @@ msgstr "Вх" msgid "MonitorDisk|D" msgstr "Д" -#: mixer_strip.cc:1737 meter_strip.cc:400 route_time_axis.cc:2400 +#: mixer_strip.cc:1737 meter_strip.cc:400 route_time_axis.cc:2406 #: vca_master_strip.cc:228 vca_time_axis.cc:273 msgid "AfterFader|A" msgstr "П" @@ -11969,7 +12028,7 @@ msgstr "" msgid "Reset Peak" msgstr "Сброс пик" -#: meter_strip.cc:403 route_time_axis.cc:2404 vca_master_strip.cc:232 +#: meter_strip.cc:403 route_time_axis.cc:2410 vca_master_strip.cc:232 #: vca_time_axis.cc:277 msgid "PreFader|P" msgstr "Д" @@ -12042,7 +12101,7 @@ msgstr "VU" msgid "SiP" msgstr "SiP" -#: monitor_section.cc:120 route_group_dialog.cc:51 +#: monitor_section.cc:120 route_group_dialog.cc:52 msgid "Soloing" msgstr "Солирование" @@ -12184,7 +12243,7 @@ msgstr "-30 Дб" msgid "Inv" msgstr "Инв." -#: monitor_section.cc:396 port_group.cc:664 +#: monitor_section.cc:396 port_group.cc:677 msgid "Monitor" msgstr "Монитор" @@ -12979,19 +13038,19 @@ msgstr "Ошб" msgid "Mis" msgstr "Отс" -#: plugin_manager_ui.cc:735 rc_option_editor.cc:5182 +#: plugin_manager_ui.cc:735 rc_option_editor.cc:5193 msgid "Re-scan Plugins now?" msgstr "Просканировать плагины заново прямо сейчас?" -#: plugin_manager_ui.cc:752 rc_option_editor.cc:4184 +#: plugin_manager_ui.cc:752 rc_option_editor.cc:4195 msgid "Set Windows VST2 Search Path" msgstr "" -#: plugin_manager_ui.cc:761 rc_option_editor.cc:4165 +#: plugin_manager_ui.cc:761 rc_option_editor.cc:4176 msgid "Set Linux VST2 Search Path" msgstr "" -#: plugin_manager_ui.cc:770 rc_option_editor.cc:4214 +#: plugin_manager_ui.cc:770 rc_option_editor.cc:4225 msgid "Set Additional VST3 Search Path" msgstr "" @@ -13130,7 +13189,7 @@ msgstr "Не удалось поменять конфигурацию выход msgid "Failed to alter plugin input configuration." msgstr "Не удалось поменять конфигурацию входа плагина." -#: plugin_pin_dialog.cc:1844 processor_box.cc:2907 +#: plugin_pin_dialog.cc:1844 processor_box.cc:2909 msgid "Cannot set up new send: %1" msgstr "Невозможно настроить новый посыл: %1" @@ -13487,7 +13546,7 @@ msgstr "Сохранить новый профиль" msgid "Save the current preset" msgstr "Сохранить текущий профиль" -#: plugin_ui.cc:552 processor_box.cc:909 +#: plugin_ui.cc:552 processor_box.cc:911 msgid "Delete the current preset" msgstr "Удалить текущий профиль" @@ -13530,7 +13589,7 @@ msgstr "Включить или отключить этот плагин" msgid "Edit Latency" msgstr "Изменить задержку" -#: plugin_ui.cc:763 processor_box.cc:857 +#: plugin_ui.cc:763 processor_box.cc:859 msgid "New Preset" msgstr "Создать пресет" @@ -13596,7 +13655,7 @@ msgstr "I/O после" msgid "No Plugins" msgstr "Нет плагинов" -#: plugin_window_proxy.cc:153 processor_box.cc:4606 +#: plugin_window_proxy.cc:153 processor_box.cc:4608 msgid "%1: %2 (by %3) [%4]" msgstr "%1: %2 (автор %3) [%4]" @@ -13628,23 +13687,27 @@ msgstr "Внешние" msgid "LTC Out" msgstr "LTC Out" -#: port_group.cc:569 +#: port_group.cc:526 +msgid "Control Surface" +msgstr "" + +#: port_group.cc:573 msgid "MMC in" msgstr "MMC in" -#: port_group.cc:574 +#: port_group.cc:578 msgid "MTC out" msgstr "MTC out" -#: port_group.cc:577 +#: port_group.cc:581 msgid "MIDI clock out" msgstr "MIDI clock out" -#: port_group.cc:580 +#: port_group.cc:584 msgid "MMC out" msgstr "MMC out" -#: port_group.cc:699 +#: port_group.cc:712 msgid "Scene " msgstr "Сцена" @@ -13781,15 +13844,15 @@ msgstr "Нет доступных портов." msgid "There are no %1 ports to connect." msgstr "Нет соединяемых портов %1." -#: processor_box.cc:255 +#: processor_box.cc:257 msgid "Return" msgstr "Возврат" -#: processor_box.cc:356 region_editor.cc:1010 +#: processor_box.cc:358 region_editor.cc:1015 msgid "New Favorite Preset for \"%1\"" msgstr "Новая избранная предустановка для \"%1\"" -#: processor_box.cc:548 processor_box.cc:1782 +#: processor_box.cc:550 processor_box.cc:1784 msgid "" "\n" "%1+double-click to toggle inline-display" @@ -13797,7 +13860,7 @@ msgstr "" "\n" "%1+двойной щелчок переключает видимость встроенного виджета" -#: processor_box.cc:552 +#: processor_box.cc:554 msgid "" "\n" "This plugin has been replicated %1 times." @@ -13805,7 +13868,7 @@ msgstr "" "\n" "Этот плагин был заменен %1 раз." -#: processor_box.cc:557 processor_box.cc:1786 +#: processor_box.cc:559 processor_box.cc:1788 msgid "" "%1\n" "Double-click to show GUI.\n" @@ -13815,7 +13878,7 @@ msgstr "" "Двойной щелчок открывает интерфейс плагина.\n" "%2+двойной щелчок открывает рисуемый хостом интерфейс.%3" -#: processor_box.cc:566 +#: processor_box.cc:568 msgid "" "%1\n" "The Plugin is not available on this system\n" @@ -13825,35 +13888,35 @@ msgstr "" "Этот плагин недоступен в \n" "системе и заменён на заглушку." -#: processor_box.cc:794 +#: processor_box.cc:796 msgid "Inline Display" msgstr "Встроенный дисплей" -#: processor_box.cc:807 +#: processor_box.cc:809 msgid "Show All Controls" msgstr "Показать все регуляторы" -#: processor_box.cc:811 +#: processor_box.cc:813 msgid "Hide All Controls" msgstr "Скрыть все регуляторы" -#: processor_box.cc:907 +#: processor_box.cc:909 msgid "New Preset..." msgstr "Создать пресет…" -#: processor_box.cc:912 +#: processor_box.cc:914 msgid "Reset Plugin" msgstr "Сбросить изменения" -#: processor_box.cc:962 +#: processor_box.cc:964 msgid "Link panner controls" msgstr "Связать регуляторы панорамирования" -#: processor_box.cc:970 +#: processor_box.cc:972 msgid "Allow Feedback Loop" msgstr "Разрешить цикл обратной связи" -#: processor_box.cc:1994 +#: processor_box.cc:1997 msgid "" "Right-click to add/remove/edit\n" "plugins,inserts,sends and more" @@ -13861,22 +13924,22 @@ msgstr "" "Щелчком правой клавишей мыши можно добавлять, \n" "изменять и удалять плагины, посылы, возвраты и пр." -#: processor_box.cc:2143 +#: processor_box.cc:2146 msgid "" "Processor Drag/Drop failed. Probably because\n" "the I/O configuration of the plugins could\n" "not match the configuration of this track." msgstr "" -#: processor_box.cc:2842 processor_box.cc:3379 +#: processor_box.cc:2844 processor_box.cc:3381 msgid "Plugin Incompatibility" msgstr "Несовместимость плагинов" -#: processor_box.cc:2845 +#: processor_box.cc:2847 msgid "You attempted to add the plugin \"%1\" in slot %2.\n" msgstr "Вы пытались добавить плагин \"%1\" в слот %2.\n" -#: processor_box.cc:2851 +#: processor_box.cc:2853 msgid "" "\n" "This plugin has:\n" @@ -13884,21 +13947,21 @@ msgstr "" "\n" "У этого плагина:\n" -#: processor_box.cc:2854 +#: processor_box.cc:2856 msgid "\t%1 MIDI input\n" msgid_plural "\t%1 MIDI inputs\n" msgstr[0] "\t%1 MIDI-вход\n" msgstr[1] "\t%1 MIDI-входа\n" msgstr[2] "\t%1 MIDI-входов\n" -#: processor_box.cc:2858 +#: processor_box.cc:2860 msgid "\t%1 audio input\n" msgid_plural "\t%1 audio inputs\n" msgstr[0] "\t%1 звуковой вход\n" msgstr[1] "\t%1 звуковых входа\n" msgstr[2] "\t%1 звуковых входов\n" -#: processor_box.cc:2861 +#: processor_box.cc:2863 msgid "" "\n" "but at the insertion point, there are:\n" @@ -13906,21 +13969,21 @@ msgstr "" "\n" "но в точке вставки сейчас:\n" -#: processor_box.cc:2864 +#: processor_box.cc:2866 msgid "\t%1 MIDI channel\n" msgid_plural "\t%1 MIDI channels\n" msgstr[0] "\t%1 MIDI-канал\n" msgstr[1] "\t%1 MIDI-канала\n" msgstr[2] "\t%1 MIDI-каналов\n" -#: processor_box.cc:2868 +#: processor_box.cc:2870 msgid "\t%1 audio channel\n" msgid_plural "\t%1 audio channels\n" msgstr[0] "\t%1 звуковой канал\n" msgstr[1] "\t%1 звуковых канала\n" msgstr[2] "\t%1 звуковых каналов\n" -#: processor_box.cc:2871 +#: processor_box.cc:2873 msgid "" "\n" "%1 is unable to insert this plugin here.\n" @@ -13928,7 +13991,7 @@ msgstr "" "\n" "%1 не может вставить сюда этот плагин.\n" -#: processor_box.cc:3382 +#: processor_box.cc:3384 msgid "" "You cannot reorder these plugins/sends/inserts\n" "in that way because the inputs and\n" @@ -13938,24 +14001,24 @@ msgstr "" "посылы и возвраты подобным образом, поскольку \n" "входы и выходы перестанут корректно работать." -#: processor_box.cc:3605 +#: processor_box.cc:3607 msgid "Rename Processor" msgstr "Переименовать обработчик" -#: processor_box.cc:3605 +#: processor_box.cc:3607 msgid "Rename Plugin" msgstr "Переименовать плагин" -#: processor_box.cc:3655 +#: processor_box.cc:3657 msgid "At least 100 IO objects exist with a name like %1 - name not changed" msgstr "" "Существует по крайней мере 100 объектов IO с именем, как %1 - имя не изменено" -#: processor_box.cc:3833 +#: processor_box.cc:3835 msgid "plugin insert constructor failed" msgstr "Сбой конструктора вставки плагина" -#: processor_box.cc:3844 +#: processor_box.cc:3846 msgid "" "Copying the set of processors on the clipboard failed,\n" "probably because the I/O configuration of the plugins\n" @@ -13965,7 +14028,7 @@ msgstr "" "буфер обмена. Вероятно, конфигурация входа и выхода\n" "плагинов не совпала с конфигурацией этой дорожки." -#: processor_box.cc:3904 +#: processor_box.cc:3906 msgid "" "Do you really want to remove all processors from %1?\n" "(this cannot be undone)" @@ -13974,15 +14037,15 @@ msgstr "" "обработчики из \"%1\" ?\n" "(отмена невозможна)" -#: processor_box.cc:3908 processor_box.cc:3933 +#: processor_box.cc:3910 processor_box.cc:3935 msgid "Yes, remove them all" msgstr "Да, удалить их все" -#: processor_box.cc:3910 processor_box.cc:3935 +#: processor_box.cc:3912 processor_box.cc:3937 msgid "Remove processors" msgstr "Удалить обработчики" -#: processor_box.cc:3925 +#: processor_box.cc:3927 msgid "" "Do you really want to remove all pre-fader processors from %1?\n" "(this cannot be undone)" @@ -13991,7 +14054,7 @@ msgstr "" "предфейдерные обработчики из \"%1\" ?\n" "(отмена невозможна)" -#: processor_box.cc:3928 +#: processor_box.cc:3930 msgid "" "Do you really want to remove all post-fader processors from %1?\n" "(this cannot be undone)" @@ -14000,79 +14063,79 @@ msgstr "" "послефейдерные обработчики из \"%1\" ?\n" "(отмена невозможна)" -#: processor_box.cc:4152 region_editor.cc:629 region_editor.cc:702 +#: processor_box.cc:4154 region_editor.cc:630 region_editor.cc:703 msgid "New Plugin" msgstr "Добавить плагин" -#: processor_box.cc:4155 +#: processor_box.cc:4157 msgid "New Insert" msgstr "Добавить возврат" -#: processor_box.cc:4158 +#: processor_box.cc:4160 msgid "New External Send ..." msgstr "Добавить внешний посыл с портом JACK..." -#: processor_box.cc:4162 +#: processor_box.cc:4164 msgid "New Aux Send ..." msgstr "Добавить внешний посыл без порта JACK..." -#: processor_box.cc:4163 +#: processor_box.cc:4165 msgid "New Foldback Send ..." msgstr "Добавить посыл сценического монитора…" -#: processor_box.cc:4164 +#: processor_box.cc:4166 msgid "Remove Foldback Send ..." msgstr "Удалить посыл сценического монитора…" -#: processor_box.cc:4166 +#: processor_box.cc:4168 msgid "Inline Controls" msgstr "Управление внутри канала" -#: processor_box.cc:4167 +#: processor_box.cc:4169 msgid "Send Options" msgstr "Параметры отправки" -#: processor_box.cc:4168 +#: processor_box.cc:4170 msgid "Presets" msgstr "Пресеты" -#: processor_box.cc:4170 +#: processor_box.cc:4172 msgid "Clear (all)" msgstr "Очистить (всё)" -#: processor_box.cc:4172 +#: processor_box.cc:4174 msgid "Clear (pre-fader)" msgstr "Очистить (до фейдера)" -#: processor_box.cc:4174 +#: processor_box.cc:4176 msgid "Clear (post-fader)" msgstr "Очистить (после фейдера)" -#: processor_box.cc:4204 +#: processor_box.cc:4206 msgid "Activate All" msgstr "Активировать все" -#: processor_box.cc:4206 +#: processor_box.cc:4208 msgid "Deactivate All" msgstr "Деактивировать все" -#: processor_box.cc:4208 +#: processor_box.cc:4210 msgid "A/B Plugins" msgstr "Отключить все" -#: processor_box.cc:4216 +#: processor_box.cc:4218 msgid "Disk I/O ..." msgstr "I/O диска…" -#: processor_box.cc:4217 +#: processor_box.cc:4219 msgid "Pre-Fader" msgstr "Предфейдер" -#: processor_box.cc:4218 +#: processor_box.cc:4220 msgid "Post-Fader" msgstr "Постфейдер" -#: processor_box.cc:4608 +#: processor_box.cc:4610 msgid "%1 (by %2) [%3]" msgstr "%1 (автор %2) [%3]" @@ -14572,8 +14635,8 @@ msgstr "" #: rc_option_editor.cc:2426 rc_option_editor.cc:2435 rc_option_editor.cc:2437 #: rc_option_editor.cc:2446 rc_option_editor.cc:2454 rc_option_editor.cc:2456 #: rc_option_editor.cc:2464 rc_option_editor.cc:2473 rc_option_editor.cc:2481 -#: rc_option_editor.cc:2636 rc_option_editor.cc:3629 rc_option_editor.cc:3978 -#: rc_option_editor.cc:5046 +#: rc_option_editor.cc:2636 rc_option_editor.cc:3629 rc_option_editor.cc:3989 +#: rc_option_editor.cc:5057 msgid "General" msgstr "Общие" @@ -14820,6 +14883,8 @@ msgid "" "Choose which part of long track names are hidden in the editor's track " "headers" msgstr "" +"Выбрать, какая часть длинных названий дорожек и шин скрывается за " +"многоточием в заголовках" # Примечания: # Добавить примечание @@ -14945,7 +15010,7 @@ msgstr "Использовать узкие полоски микшера по msgid "Limit inline-mixer-strip controls per plugin" msgstr "Ограничить число регуляторов внутри канала микшера на плагин" -#: rc_option_editor.cc:2882 rc_option_editor.cc:4987 +#: rc_option_editor.cc:2882 rc_option_editor.cc:4998 msgid "Unlimited" msgstr "Без ограничений" @@ -14957,11 +15022,11 @@ msgstr "16 параметров" msgid "32 parameters" msgstr "32 параметра" -#: rc_option_editor.cc:2885 rc_option_editor.cc:4988 +#: rc_option_editor.cc:2885 rc_option_editor.cc:4999 msgid "64 parameters" msgstr "64 параметра" -#: rc_option_editor.cc:2886 rc_option_editor.cc:4989 +#: rc_option_editor.cc:2886 rc_option_editor.cc:5000 msgid "128 parameters" msgstr "128 параметров" @@ -15853,12 +15918,12 @@ msgid "Reset xrun counter when starting to record" msgstr "Сбрасывать счетчик рассинхронихзаций в начале записи" #: rc_option_editor.cc:3813 rc_option_editor.cc:3815 rc_option_editor.cc:3823 -#: rc_option_editor.cc:3832 rc_option_editor.cc:3834 rc_option_editor.cc:3851 -#: rc_option_editor.cc:3867 rc_option_editor.cc:3868 +#: rc_option_editor.cc:3841 rc_option_editor.cc:3843 rc_option_editor.cc:3845 +#: rc_option_editor.cc:3862 rc_option_editor.cc:3878 rc_option_editor.cc:3879 msgid "Transport/Chase" msgstr "Транспорт/Слежение" -#: rc_option_editor.cc:3813 rc_option_editor.cc:3926 +#: rc_option_editor.cc:3813 rc_option_editor.cc:3937 msgid "MIDI Machine Control (MMC)" msgstr "MIDI Machine Control (MMC)" @@ -15870,15 +15935,31 @@ msgstr "Отвечать на команды MMC" msgid "Inbound MMC device ID" msgstr "Идентификатор входящего устройства MMC" -#: rc_option_editor.cc:3835 +#: rc_option_editor.cc:3834 +msgid "MMC Fast-wind behavior" +msgstr "" + +#: rc_option_editor.cc:3838 +msgid "Off (MMC fast-forward+rewind are ignored)" +msgstr "" + +#: rc_option_editor.cc:3839 varispeed_dialog.cc:33 +msgid "Varispeed" +msgstr "" + +#: rc_option_editor.cc:3840 +msgid "Marker Locate (MMC ffwd/rewd jumps to next/prior marker)" +msgstr "" + +#: rc_option_editor.cc:3846 msgid "Show Transport Masters Window" msgstr "Показать окно «Ведущие транспорта»" -#: rc_option_editor.cc:3840 +#: rc_option_editor.cc:3851 msgid "Match session video frame rate to external timecode" msgstr "Адаптировать частоту кадров видео в сессии к внешнему тайм-коду" -#: rc_option_editor.cc:3846 +#: rc_option_editor.cc:3857 msgid "" "This option controls the value of the video frame rate while chasing " "an external timecode source.\n" @@ -15901,15 +15982,15 @@ msgstr "" "этого индикатор частоты кадров в основном счётчике будет мерцать красным, а " "%1 будет конвертировать внешний тайм-код в тайм-код сессии." -#: rc_option_editor.cc:3855 +#: rc_option_editor.cc:3866 msgid "BPM Resolution for incoming MIDI Clock" msgstr "Разрешение в BPM для входящего MIDI Clock" -#: rc_option_editor.cc:3858 +#: rc_option_editor.cc:3869 msgid "quarters" msgstr "" -#: rc_option_editor.cc:3862 +#: rc_option_editor.cc:3873 msgid "" "This option can be used to quantize incoming MIDI clock to whole (or " "fractions of a) quarter note.\n" @@ -15924,31 +16005,31 @@ msgid "" "quarter note then adjust this setting to reflect that." msgstr "" -#: rc_option_editor.cc:3867 +#: rc_option_editor.cc:3878 msgid "MIDI Clock" msgstr "MIDI Clock" -#: rc_option_editor.cc:3870 rc_option_editor.cc:3872 rc_option_editor.cc:3889 -#: rc_option_editor.cc:3901 rc_option_editor.cc:3903 rc_option_editor.cc:3905 -#: rc_option_editor.cc:3907 rc_option_editor.cc:3924 rc_option_editor.cc:3926 -#: rc_option_editor.cc:3928 rc_option_editor.cc:3936 rc_option_editor.cc:3945 -#: rc_option_editor.cc:3947 +#: rc_option_editor.cc:3881 rc_option_editor.cc:3883 rc_option_editor.cc:3900 +#: rc_option_editor.cc:3912 rc_option_editor.cc:3914 rc_option_editor.cc:3916 +#: rc_option_editor.cc:3918 rc_option_editor.cc:3935 rc_option_editor.cc:3937 +#: rc_option_editor.cc:3939 rc_option_editor.cc:3947 rc_option_editor.cc:3956 +#: rc_option_editor.cc:3958 msgid "Transport/Generate" msgstr "Транспорт/Генераторы" -#: rc_option_editor.cc:3870 +#: rc_option_editor.cc:3881 msgid "Linear Timecode (LTC) Generator" msgstr "Генератор линейного таймкода (LTC)" -#: rc_option_editor.cc:3875 +#: rc_option_editor.cc:3886 msgid "Enable LTC generator" msgstr "Включить генератор LTC" -#: rc_option_editor.cc:3882 +#: rc_option_editor.cc:3893 msgid "Send LTC while stopped" msgstr "Отправлять LTC в остановленном состоянии" -#: rc_option_editor.cc:3888 +#: rc_option_editor.cc:3899 msgid "" "When enabled %1 will continue to send LTC information even when the " "transport (playhead) is not moving" @@ -15956,11 +16037,11 @@ msgstr "" "Когда включено, %1 продолжит передавать LTC даже когда транспорт " "(воспроизведение) не движется" -#: rc_option_editor.cc:3891 +#: rc_option_editor.cc:3902 msgid "LTC generator level [dBFS]" msgstr "Уровень генератора LYTC (dBFS)" -#: rc_option_editor.cc:3899 +#: rc_option_editor.cc:3910 msgid "" "Specify the Peak Volume of the generated LTC signal in dBFS. A good value " "is 0dBu ^= -18dBFS in an EBU calibrated system" @@ -15968,43 +16049,43 @@ msgstr "" "Укажите пиковую громкость генерируемого сигнала LTC в dbFS. Хорошее значение " "— это 0dBu ^ =-18dbFS в калиброванной системе EBU" -#: rc_option_editor.cc:3905 +#: rc_option_editor.cc:3916 msgid "MIDI Time Code (MTC) Generator" msgstr "Генератор MIDI-таймкода (MTC)" -#: rc_option_editor.cc:3910 +#: rc_option_editor.cc:3921 msgid "Enable MTC Generator" msgstr "Включить генератор MTC" -#: rc_option_editor.cc:3918 +#: rc_option_editor.cc:3929 msgid "Max MTC varispeed (%)" msgstr "Максимальная вариативность скорости MTC (%)" -#: rc_option_editor.cc:3923 +#: rc_option_editor.cc:3934 msgid "Percentage either side of normal transport speed to transmit MTC." msgstr "" -#: rc_option_editor.cc:3931 +#: rc_option_editor.cc:3942 msgid "Send MMC commands" msgstr "Передавать команды MMC" -#: rc_option_editor.cc:3939 +#: rc_option_editor.cc:3950 msgid "Outbound MMC device ID" msgstr "Идентификатор выходящего устройства MMC" -#: rc_option_editor.cc:3945 +#: rc_option_editor.cc:3956 msgid "MIDI Beat Clock (Mclk) Generator" msgstr "Генератор MIDI Beat Clock (Mclk)" -#: rc_option_editor.cc:3950 +#: rc_option_editor.cc:3961 msgid "Enable Mclk generator" msgstr "Включить генератор Mclk" -#: rc_option_editor.cc:3959 +#: rc_option_editor.cc:3970 msgid "Silence plugins when the transport is stopped" msgstr "Приглушать плагины при остановке транспорта" -#: rc_option_editor.cc:3965 +#: rc_option_editor.cc:3976 msgid "" "When enabled plugins will be reset at transport stop. When disabled " "plugins will be left unchanged at transport stop.\n" @@ -16017,30 +16098,30 @@ msgstr "" "\n" "По большей части это влияет на эффекты с «хвостом» вроде ревербераторов." -#: rc_option_editor.cc:3970 +#: rc_option_editor.cc:3981 msgid "Scan/Discover" msgstr "Сканирование и обнаружение" -#: rc_option_editor.cc:3972 rc_option_editor.cc:4099 rc_option_editor.cc:4253 +#: rc_option_editor.cc:3983 rc_option_editor.cc:4110 rc_option_editor.cc:4264 msgid "Scan for Plugins" msgstr "Просканировать плагины" -#: rc_option_editor.cc:3984 +#: rc_option_editor.cc:3995 msgid "Scan for [new] Plugins on Application Start" msgstr "Искать (новые) плагины при запуске программы" -#: rc_option_editor.cc:3990 +#: rc_option_editor.cc:4001 msgid "" "When enabled new plugins are searched, tested and added to the cache " "index on application start. When disabled new plugins will only be available " "after triggering a 'Scan' manually" msgstr "" -#: rc_option_editor.cc:3994 +#: rc_option_editor.cc:4005 msgid "Always Display Plugin Scan Progress" msgstr "Всегда показывать прогресс сканирования плагинов" -#: rc_option_editor.cc:4000 +#: rc_option_editor.cc:4011 msgid "" "When enabled a popup window showing plugin scan progress is displayed " "for indexing (cache load) and discovery (detect new plugins)" @@ -16048,33 +16129,33 @@ msgstr "" "Когда включено, всплывающее окно показывает прогресс поиска и " "индексации звуковых плагинов в системе" -#: rc_option_editor.cc:4004 +#: rc_option_editor.cc:4015 msgid "Verbose Plugin Scan" msgstr "Подробное сканирование плагинов" -#: rc_option_editor.cc:4010 +#: rc_option_editor.cc:4021 msgid "" "When enabled additional information for every plugin is shown to the " "Plugin Manager Log." msgstr "" -#: rc_option_editor.cc:4015 +#: rc_option_editor.cc:4026 msgid "Open Plugin Manager window when missing plugins are found" msgstr "" "Открывать окно управления плагинами, когда обнаружено отсутствие плагина" -#: rc_option_editor.cc:4021 +#: rc_option_editor.cc:4032 msgid "" "When enabled the Plugin Manager is display at session load if the " "session contains any plugins that are missing, or plugins have been updated " "and require a rescan." msgstr "" -#: rc_option_editor.cc:4025 +#: rc_option_editor.cc:4036 msgid "Make new plugins active" msgstr "Делать новые плагины активными" -#: rc_option_editor.cc:4031 +#: rc_option_editor.cc:4042 msgid "" "When enabled plugins will be activated when they are added to tracks/" "busses.\n" @@ -16082,11 +16163,11 @@ msgid "" "tracks/busses" msgstr "" -#: rc_option_editor.cc:4035 +#: rc_option_editor.cc:4046 msgid "Setup Sidechain ports when loading plugin with aux inputs" msgstr "Настраивать порты боковых цепей при загрузке плагинjd с Aux-входами" -#: rc_option_editor.cc:4041 +#: rc_option_editor.cc:4052 msgid "" "When enabled sidechain ports are created for plugins at instantiation " "time if a plugin has sidechain inputs. Note that the ports themselves will " @@ -16095,48 +16176,48 @@ msgid "" "When disabled sidechain input pins will remain unconnected." msgstr "" -#: rc_option_editor.cc:4043 rc_option_editor.cc:4044 rc_option_editor.cc:4058 -#: rc_option_editor.cc:4072 rc_option_editor.cc:4076 rc_option_editor.cc:4077 -#: rc_option_editor.cc:4091 +#: rc_option_editor.cc:4054 rc_option_editor.cc:4055 rc_option_editor.cc:4069 +#: rc_option_editor.cc:4083 rc_option_editor.cc:4087 rc_option_editor.cc:4088 +#: rc_option_editor.cc:4102 msgid "Plugins/GUI" msgstr "Плагины/Интерфейс" -#: rc_option_editor.cc:4043 +#: rc_option_editor.cc:4054 msgid "Plugin GUI" msgstr "Интерфейс плагинов" -#: rc_option_editor.cc:4047 +#: rc_option_editor.cc:4058 msgid "Automatically open the plugin GUI when adding a new plugin" msgstr "Автоматически открывать окно плагина после его добавления" -#: rc_option_editor.cc:4054 +#: rc_option_editor.cc:4065 msgid "Show only one plugin window at a time" msgstr "Показывать только одно окно плагина за один раз" -#: rc_option_editor.cc:4060 +#: rc_option_editor.cc:4071 msgid "" "When enabled at most one plugin GUI window can be on-screen at a " "time. When disabled, the number of visible plugin GUI windows is " "unlimited" msgstr "" -#: rc_option_editor.cc:4064 +#: rc_option_editor.cc:4075 msgid "Closing a Plugin GUI Window" msgstr "При закрытии окна с интерфейсом плагина" -#: rc_option_editor.cc:4068 +#: rc_option_editor.cc:4079 msgid "only hides the window" msgstr "Только скрывает окно" -#: rc_option_editor.cc:4069 +#: rc_option_editor.cc:4080 msgid "destroys the GUI instance, releasing resources" msgstr "разрушать копию интерфейса, высвобождать ресурсы" -#: rc_option_editor.cc:4070 +#: rc_option_editor.cc:4081 msgid "only destroys VST2/3 UIs, hides others" msgstr "разрушать только интерфейсы VST2/3, скрывать все остальные" -#: rc_option_editor.cc:4073 +#: rc_option_editor.cc:4084 msgid "" "Closing a plugin window, usually only hides it. This makes is fast to open " "the same plugin UI again at a later time.\n" @@ -16149,15 +16230,15 @@ msgid "" "issue." msgstr "" -#: rc_option_editor.cc:4076 +#: rc_option_editor.cc:4087 msgid "Mixer Strip Inline Display" msgstr "Интерфейсы, встраиваемые в каналы микшера" -#: rc_option_editor.cc:4080 +#: rc_option_editor.cc:4091 msgid "Show Plugin Inline Display on Mixer Strip by default" msgstr "Показывать встраиваемые интерфейсы плагинов по умолчанию" -#: rc_option_editor.cc:4087 +#: rc_option_editor.cc:4098 msgid "" "Don't automatically open the plugin GUI when the plugin has an inline " "display mode" @@ -16165,81 +16246,81 @@ msgstr "" "Не открывать GUI плагина автоматически, если у плагина есть встраиваемый " "интерфейс" -#: rc_option_editor.cc:4096 rc_option_editor.cc:4098 rc_option_editor.cc:4110 -#: rc_option_editor.cc:4122 rc_option_editor.cc:4132 rc_option_editor.cc:4142 -#: rc_option_editor.cc:4148 rc_option_editor.cc:4150 rc_option_editor.cc:4155 -#: rc_option_editor.cc:4162 rc_option_editor.cc:4172 rc_option_editor.cc:4181 -#: rc_option_editor.cc:4191 rc_option_editor.cc:4200 rc_option_editor.cc:4201 -#: rc_option_editor.cc:4206 rc_option_editor.cc:4224 rc_option_editor.cc:4227 -#: rc_option_editor.cc:4236 rc_option_editor.cc:4237 +#: rc_option_editor.cc:4107 rc_option_editor.cc:4109 rc_option_editor.cc:4121 +#: rc_option_editor.cc:4133 rc_option_editor.cc:4143 rc_option_editor.cc:4153 +#: rc_option_editor.cc:4159 rc_option_editor.cc:4161 rc_option_editor.cc:4166 +#: rc_option_editor.cc:4173 rc_option_editor.cc:4183 rc_option_editor.cc:4192 +#: rc_option_editor.cc:4202 rc_option_editor.cc:4211 rc_option_editor.cc:4212 +#: rc_option_editor.cc:4217 rc_option_editor.cc:4235 rc_option_editor.cc:4238 +#: rc_option_editor.cc:4247 rc_option_editor.cc:4248 msgid "Plugins/VST" msgstr "Плагины/VST" -#: rc_option_editor.cc:4106 +#: rc_option_editor.cc:4117 msgid "Enable Mac VST2 support (requires restart or re-scan)" msgstr "" "Включить поддержку Mac VST2 (требует перезапуска программы или повторного " "сканирования)" -#: rc_option_editor.cc:4118 +#: rc_option_editor.cc:4129 msgid "Enable Windows VST2 support (requires restart or re-scan)" msgstr "" "Включить поддержку Windows VST2 (требует перезапуска программы или " "повторного сканирования)" -#: rc_option_editor.cc:4128 +#: rc_option_editor.cc:4139 msgid "Enable Linux VST2 support (requires restart or re-scan)" msgstr "" "Включить поддержку Linux VST2 (требует перезапуска программы или повторного " "сканирования)" -#: rc_option_editor.cc:4138 +#: rc_option_editor.cc:4149 msgid "Enable VST3 support (requires restart or re-scan)" msgstr "" "Включить поддержку VST3 (требует перезапуска программы или повторного " "сканирования)" -#: rc_option_editor.cc:4148 +#: rc_option_editor.cc:4159 msgid "VST 2.x" msgstr "VST 2.x" -#: rc_option_editor.cc:4153 +#: rc_option_editor.cc:4164 msgid "VST 2 Cache:" msgstr "Кэш VST 2:" -#: rc_option_editor.cc:4158 +#: rc_option_editor.cc:4169 msgid "VST 2 Ignorelist:" -msgstr "" +msgstr "Список игнорируемых VST2:" -#: rc_option_editor.cc:4170 +#: rc_option_editor.cc:4181 msgid "Linux VST2 Path:" msgstr "Путь к Linux VST2:" -#: rc_option_editor.cc:4175 rc_option_editor.cc:4194 +#: rc_option_editor.cc:4186 rc_option_editor.cc:4205 msgid "Path:" msgstr "Расположение:" -#: rc_option_editor.cc:4189 +#: rc_option_editor.cc:4200 msgid "Windows VST2 Path:" msgstr "Путь к Windows VST2:" -#: rc_option_editor.cc:4200 +#: rc_option_editor.cc:4211 msgid "VST 3" msgstr "VST 3" -#: rc_option_editor.cc:4204 +#: rc_option_editor.cc:4215 msgid "VST 3 Cache:" msgstr "Кэш VST 3:" -#: rc_option_editor.cc:4209 +#: rc_option_editor.cc:4220 msgid "VST 3 Ignorelist:" -msgstr "" +msgstr "Список игнорируемых VST3:" -#: rc_option_editor.cc:4219 +#: rc_option_editor.cc:4230 msgid "Additional VST3 Path:" msgstr "Дополнительное расположение VST3:" -#: rc_option_editor.cc:4221 +#: rc_option_editor.cc:4232 msgid "" "Customizing VST3 paths is discouraged. Note that default VST3 paths as per " "спецификации, специально " "указывать их не надо." -#: rc_option_editor.cc:4230 +#: rc_option_editor.cc:4241 msgid "Automatically show 'Micro Edit' tagged controls on the mixer-strip" msgstr "" "Автоматически показывать элементы управления с тегом \"Micro Edit\" в микшере" -#: rc_option_editor.cc:4236 +#: rc_option_editor.cc:4247 msgid "VST2/VST3" msgstr "VST2/VST3" -#: rc_option_editor.cc:4240 +#: rc_option_editor.cc:4251 msgid "Conceal VST2 Plugin if matching VST3 exists" msgstr "Скрывать плагины VST2, когда есть аналогичные VST3" -#: rc_option_editor.cc:4250 rc_option_editor.cc:4252 rc_option_editor.cc:4263 -#: rc_option_editor.cc:4265 rc_option_editor.cc:4270 +#: rc_option_editor.cc:4261 rc_option_editor.cc:4263 rc_option_editor.cc:4274 +#: rc_option_editor.cc:4276 rc_option_editor.cc:4281 msgid "Plugins/Audio Unit" msgstr "Плагины/Audio Unit" -#: rc_option_editor.cc:4250 +#: rc_option_editor.cc:4261 msgid "Audio Unit" msgstr "Audio Unit" -#: rc_option_editor.cc:4259 +#: rc_option_editor.cc:4270 msgid "Enable Audio Unit support (requires restart or re-scan)" msgstr "" "Включить поддержку Audio Unit (нужен перезапуск или повторное сканирование)" -#: rc_option_editor.cc:4268 +#: rc_option_editor.cc:4279 msgid "AU Cache:" msgstr "Кэш AU:" -#: rc_option_editor.cc:4273 +#: rc_option_editor.cc:4284 msgid "AU Ignorelist:" msgstr "Список игнорируемых AU:" -#: rc_option_editor.cc:4276 +#: rc_option_editor.cc:4287 msgid "LV1/LV2" msgstr "LV1/LV2" -#: rc_option_editor.cc:4280 +#: rc_option_editor.cc:4291 msgid "Conceal LADSPA (LV1) Plugins if matching LV2 exists" msgstr "Скрывать плагины LADSPA (LV1), если доступны их версии в LV2" -#: rc_option_editor.cc:4284 +#: rc_option_editor.cc:4295 msgid "Instrument" msgstr "Виртуальные инструменты" -#: rc_option_editor.cc:4288 +#: rc_option_editor.cc:4299 msgid "Ask to replace existing instrument plugin" msgstr "Спрашивать о замене уже добавленного плагина виртуального инструмента" -#: rc_option_editor.cc:4296 +#: rc_option_editor.cc:4307 msgid "Interactively configure instrument plugins on insert" msgstr "Запрашивать параметры виртуальных инструментов на возврате" -#: rc_option_editor.cc:4302 +#: rc_option_editor.cc:4313 msgid "" "When enabled show a dialog to select instrument channel configuration " "before adding a multichannel plugin." msgstr "" -#: rc_option_editor.cc:4304 +#: rc_option_editor.cc:4315 msgid "Statistics" msgstr "Статистика" -#: rc_option_editor.cc:4307 +#: rc_option_editor.cc:4318 msgid "Reset Statistics" msgstr "Обнулить статистику" -#: rc_option_editor.cc:4313 +#: rc_option_editor.cc:4324 msgid "Plugin chart (use-count) length" msgstr "Позиций в чарте популярных плагинов" -#: rc_option_editor.cc:4322 +#: rc_option_editor.cc:4333 msgid "Plugin recent list length" msgstr "Длина списка недавних плагинов" -#: rc_option_editor.cc:4336 +#: rc_option_editor.cc:4347 msgid "Record monitoring handled by" msgstr "Где выполняется мониторинг записи" -#: rc_option_editor.cc:4354 +#: rc_option_editor.cc:4365 msgid "Auto Input does 'talkback'" msgstr "Всегда мониторить входы" -#: rc_option_editor.cc:4360 +#: rc_option_editor.cc:4371 msgid "" "When enabled, and Transport -> Auto-Input is enabled, %1 will always " "monitor audio inputs when transport is stopped, even if tracks aren't armed." msgstr "" -#: rc_option_editor.cc:4367 +#: rc_option_editor.cc:4378 msgid "Solo controls are Listen controls" msgstr "Управление солированием работает как управление прослушиванием" -#: rc_option_editor.cc:4377 +#: rc_option_editor.cc:4388 msgid "Exclusive solo" msgstr "Эксклюзивное солирование" -#: rc_option_editor.cc:4385 +#: rc_option_editor.cc:4396 msgid "Show solo muting" msgstr "Показывать приглушение при солировании" -#: rc_option_editor.cc:4393 +#: rc_option_editor.cc:4404 msgid "Soloing overrides muting" msgstr "Солирование приоритетнее приглушения" -#: rc_option_editor.cc:4401 +#: rc_option_editor.cc:4412 msgid "Solo-in-place mute cut (dB)" msgstr "Приглушение сигнала при солировании (dB)" -#: rc_option_editor.cc:4408 +#: rc_option_editor.cc:4419 msgid "Listen Position" msgstr "Положение прослушивания" -#: rc_option_editor.cc:4413 +#: rc_option_editor.cc:4424 msgid "after-fader (AFL)" msgstr "После фейдера (AFL)" -#: rc_option_editor.cc:4414 +#: rc_option_editor.cc:4425 msgid "pre-fader (PFL)" msgstr "До фейдера (PFL)" -#: rc_option_editor.cc:4420 +#: rc_option_editor.cc:4431 msgid "PFL signals come from" msgstr "Источник сигнала PFL" -#: rc_option_editor.cc:4425 +#: rc_option_editor.cc:4436 msgid "before pre-fader processors" msgstr "До послефейдерных обработчиков" -#: rc_option_editor.cc:4426 +#: rc_option_editor.cc:4437 msgid "pre-fader but after pre-fader processors" msgstr "До фейдера, но после предфейдерных обработчиков" -#: rc_option_editor.cc:4432 +#: rc_option_editor.cc:4443 msgid "AFL signals come from" msgstr "Источник сигнала AFL" -#: rc_option_editor.cc:4437 +#: rc_option_editor.cc:4448 msgid "immediately post-fader" msgstr "Сразу после фейдера" -#: rc_option_editor.cc:4438 +#: rc_option_editor.cc:4449 msgid "after post-fader processors (before pan)" msgstr "За послефейдерными обработчиками и до панорамирования" -#: rc_option_editor.cc:4444 +#: rc_option_editor.cc:4455 msgid "Master" msgstr "Мастер-шина" -#: rc_option_editor.cc:4448 +#: rc_option_editor.cc:4459 msgid "Enable master-bus output gain control" msgstr "Включить управление выходным усилением мастер-шины" -#: rc_option_editor.cc:4455 +#: rc_option_editor.cc:4466 msgid "I/O Resampler (vari-speed) quality" msgstr "Качество ввода-вывода ресэмплера (vari-speed)" -#: rc_option_editor.cc:4460 +#: rc_option_editor.cc:4471 msgid "Off (no vari-speed)" msgstr "Выключено (без vari-speed)" -#: rc_option_editor.cc:4461 +#: rc_option_editor.cc:4472 msgid "Low (16 samples latency)" msgstr "Низкое (задержка 16 сэмплов)" -#: rc_option_editor.cc:4462 +#: rc_option_editor.cc:4473 msgid "Moderate (32 samples latency), default" msgstr "Умеренное (задержка 32 сэмпла), по умолчанию" -#: rc_option_editor.cc:4463 +#: rc_option_editor.cc:4474 msgid "Medium (64 samples latency)" msgstr "Среднее (задержка 64 сэмпла)" -#: rc_option_editor.cc:4464 +#: rc_option_editor.cc:4475 msgid "High (96 samples latency)" msgstr "Высокое (задержка 96 сэмплов)" -#: rc_option_editor.cc:4465 +#: rc_option_editor.cc:4476 msgid "Very High (128 samples latency)" msgstr "Очень высокое (задержка 128 сэмплов)" -#: rc_option_editor.cc:4466 +#: rc_option_editor.cc:4477 msgid "Extreme (184 samples latency)" msgstr "Крайне высокое (задержка 184 сэмпла)" -#: rc_option_editor.cc:4478 +#: rc_option_editor.cc:4489 msgid "Custom (%1 samples latency)" msgstr "На заказ (задержка %1 сэмплов)" -#: rc_option_editor.cc:4481 +#: rc_option_editor.cc:4492 msgid "This setting will only take effect when the Audio Engine is restarted." msgstr "Изменение вступит в силу только после перезапуска звукового движка." -#: rc_option_editor.cc:4482 +#: rc_option_editor.cc:4493 msgid "" "To facilitate vari-speed playback/recording, audio is resampled to change " "pitch and speed. This introduces latency depending on the quality. For " @@ -16460,53 +16541,53 @@ msgid "" "trip latency)" msgstr "" -#: rc_option_editor.cc:4486 +#: rc_option_editor.cc:4497 msgid "Default Track / Bus Muting Options" msgstr "Параметры приглушения дорожек/шин по умолчанию" -#: rc_option_editor.cc:4491 +#: rc_option_editor.cc:4502 msgid "Mute affects pre-fader sends" msgstr "Предфейдерные посылы" -#: rc_option_editor.cc:4499 +#: rc_option_editor.cc:4510 msgid "Mute affects post-fader sends" msgstr "Послефейдерные посылы" -#: rc_option_editor.cc:4507 +#: rc_option_editor.cc:4518 msgid "Mute affects control outputs" msgstr "Выходы мониторинга" -#: rc_option_editor.cc:4515 +#: rc_option_editor.cc:4526 msgid "Mute affects main outputs" msgstr "Приглушение затрагивает основные выходы" -#: rc_option_editor.cc:4521 +#: rc_option_editor.cc:4532 msgid "Send Routing" msgstr "Маршрутизация посылов" -#: rc_option_editor.cc:4525 +#: rc_option_editor.cc:4536 msgid "Link panners of Aux and External Sends with main panner by default" msgstr "По умолчанию связывать внешние посылы с основным регулятором панорамы" -#: rc_option_editor.cc:4530 +#: rc_option_editor.cc:4541 msgid "Audio Regions" msgstr "Звуковые области" -#: rc_option_editor.cc:4535 +#: rc_option_editor.cc:4546 msgid "Replicate missing region channels" msgstr "Воссоздавать отсутствующие каналы области" -#: rc_option_editor.cc:4542 +#: rc_option_editor.cc:4553 msgid "Track and Bus Connections" msgstr "Соединения дорожек и шин" -#: rc_option_editor.cc:4546 +#: rc_option_editor.cc:4557 msgid "Auto-connect main output (master or monitor) bus to physical ports" msgstr "" "Автоматически соединить основную шину выхода (мастер или монитор) с " "физическими портами" -#: rc_option_editor.cc:4552 +#: rc_option_editor.cc:4563 msgid "" "When enabled the main output bus is auto-connected to the first N " "physical ports. If the session has a monitor-section, the monitor-bus output " @@ -16514,121 +16595,121 @@ msgid "" "is directly used for playback." msgstr "" -#: rc_option_editor.cc:4558 +#: rc_option_editor.cc:4569 msgid "Connect track inputs" msgstr "Соединять входы дорожек" -#: rc_option_editor.cc:4563 +#: rc_option_editor.cc:4574 msgid "automatically to physical inputs" msgstr "Автоматически с физическими входами" -#: rc_option_editor.cc:4564 rc_option_editor.cc:4577 +#: rc_option_editor.cc:4575 rc_option_editor.cc:4588 msgid "manually" msgstr "Вручную" -#: rc_option_editor.cc:4570 +#: rc_option_editor.cc:4581 msgid "Connect track and bus outputs" msgstr "Соединять выходы дорожек и шин" -#: rc_option_editor.cc:4575 +#: rc_option_editor.cc:4586 msgid "automatically to physical outputs" msgstr "Автоматически с физическими выходами" -#: rc_option_editor.cc:4576 +#: rc_option_editor.cc:4587 msgid "automatically to master bus" msgstr "Автоматически с общей шиной" -#: rc_option_editor.cc:4583 +#: rc_option_editor.cc:4594 msgid "Use 'Strict-I/O' for new tracks or busses" msgstr "Использовать строгий I/O для новых дорожек и шин" -#: rc_option_editor.cc:4603 +#: rc_option_editor.cc:4614 msgid "Enable metronome only while recording" msgstr "Включать метроном только при записи" -#: rc_option_editor.cc:4609 +#: rc_option_editor.cc:4620 msgid "" "When enabled the metronome will remain silent if %1 is not " "recording." msgstr "" -#: rc_option_editor.cc:4622 rc_option_editor.cc:4624 rc_option_editor.cc:4639 -#: rc_option_editor.cc:4656 rc_option_editor.cc:4672 rc_option_editor.cc:4688 -#: rc_option_editor.cc:4702 rc_option_editor.cc:4715 rc_option_editor.cc:4720 -#: rc_option_editor.cc:4738 rc_option_editor.cc:4756 rc_option_editor.cc:4774 -#: rc_option_editor.cc:4776 rc_option_editor.cc:4778 +#: rc_option_editor.cc:4633 rc_option_editor.cc:4635 rc_option_editor.cc:4650 +#: rc_option_editor.cc:4667 rc_option_editor.cc:4683 rc_option_editor.cc:4699 +#: rc_option_editor.cc:4713 rc_option_editor.cc:4726 rc_option_editor.cc:4731 +#: rc_option_editor.cc:4749 rc_option_editor.cc:4767 rc_option_editor.cc:4785 +#: rc_option_editor.cc:4787 rc_option_editor.cc:4789 msgid "Preferences|Metering" msgstr "Замер" -#: rc_option_editor.cc:4622 +#: rc_option_editor.cc:4633 msgid "Meterbridge meters" msgstr "Индикаторы Meterbridge" -#: rc_option_editor.cc:4629 +#: rc_option_editor.cc:4640 msgid "Peak hold time" msgstr "Удерживание пика" -#: rc_option_editor.cc:4635 +#: rc_option_editor.cc:4646 msgid "short" msgstr "Короткое" -#: rc_option_editor.cc:4636 +#: rc_option_editor.cc:4647 msgid "medium" msgstr "Среднее" -#: rc_option_editor.cc:4637 +#: rc_option_editor.cc:4648 msgid "long" msgstr "Долгое" -#: rc_option_editor.cc:4643 +#: rc_option_editor.cc:4654 msgid "DPM fall-off" msgstr "Скорость спадания" -#: rc_option_editor.cc:4649 +#: rc_option_editor.cc:4660 msgid "slowest [6.6dB/sec]" msgstr "Самое медленное [6,6 Дб/с]" -#: rc_option_editor.cc:4650 +#: rc_option_editor.cc:4661 msgid "slow [8.6dB/sec] (BBC PPM, EBU PPM)" msgstr "Медленное [8,6 Дб/с] (BBC PPM, EBU PPM)" -#: rc_option_editor.cc:4651 +#: rc_option_editor.cc:4662 msgid "moderate [12.0dB/sec] (DIN)" msgstr "Умеренное [12 Дб/с] (DIN)" -#: rc_option_editor.cc:4652 +#: rc_option_editor.cc:4663 msgid "medium [13.3dB/sec] (EBU Digi PPM, IRT Digi PPM)" msgstr "Среднее [13,3 Дб/с] (EBU Digi PPM, IRT Digi PPM)" -#: rc_option_editor.cc:4653 +#: rc_option_editor.cc:4664 msgid "fast [20dB/sec]" msgstr "Быстрое [20 Дб/с]" -#: rc_option_editor.cc:4654 +#: rc_option_editor.cc:4665 msgid "very fast [32dB/sec]" msgstr "Очень быстрое [32 Дб/с]" -#: rc_option_editor.cc:4660 +#: rc_option_editor.cc:4671 msgid "Meter line-up level; 0dBu" msgstr "Точка выравнивания; 0dBu" -#: rc_option_editor.cc:4665 rc_option_editor.cc:4681 +#: rc_option_editor.cc:4676 rc_option_editor.cc:4692 msgid "-24dBFS (SMPTE US: 4dBu = -20dBFS)" msgstr "-24dBFS (SMPTE US: 4dBu = -20dBFS)" -#: rc_option_editor.cc:4666 rc_option_editor.cc:4682 +#: rc_option_editor.cc:4677 rc_option_editor.cc:4693 msgid "-20dBFS (SMPTE RP.0155)" msgstr "-20dBFS (SMPTE RP.0155)" -#: rc_option_editor.cc:4667 rc_option_editor.cc:4683 +#: rc_option_editor.cc:4678 rc_option_editor.cc:4694 msgid "-18dBFS (EBU, BBC)" msgstr "-18dBFS (EBU, BBC)" -#: rc_option_editor.cc:4668 rc_option_editor.cc:4684 +#: rc_option_editor.cc:4679 rc_option_editor.cc:4695 msgid "-15dBFS (DIN)" msgstr "-15dBFS (DIN)" -#: rc_option_editor.cc:4670 +#: rc_option_editor.cc:4681 msgid "" "Configure meter-marks and color-knee point for dBFS scale DPM, set reference " "level for IEC1/Nordic, IEC2 PPM and VU meter." @@ -16636,39 +16717,39 @@ msgstr "" "Настройка измер. маркеров и цвета точки узла для dBFS масштабирования шкалы " "DPM, установка эталонного уровня для IEC1/Nordic, PPM и VU-метра." -#: rc_option_editor.cc:4676 +#: rc_option_editor.cc:4687 msgid "IEC1/DIN Meter line-up level; 0dBu" msgstr "Точка выравнивания IEC1/DIN; 0dBu" -#: rc_option_editor.cc:4686 +#: rc_option_editor.cc:4697 msgid "Reference level for IEC1/DIN meter." msgstr "Референсный уровень индикатора IEC1/DIN" -#: rc_option_editor.cc:4692 +#: rc_option_editor.cc:4703 msgid "VU Meter standard" msgstr "Стандарт индикатора VU" -#: rc_option_editor.cc:4697 +#: rc_option_editor.cc:4708 msgid "0VU = -2dBu (France)" msgstr "0VU = -2dBu (Франция)" -#: rc_option_editor.cc:4698 +#: rc_option_editor.cc:4709 msgid "0VU = 0dBu (North America, Australia)" msgstr "0VU = 0dBu (Северная Америка, Австралия)" -#: rc_option_editor.cc:4699 +#: rc_option_editor.cc:4710 msgid "0VU = +4dBu (standard)" msgstr "0VU = +4dBu (стандарт)" -#: rc_option_editor.cc:4700 +#: rc_option_editor.cc:4711 msgid "0VU = +8dBu" msgstr "0VU = +8dBu" -#: rc_option_editor.cc:4705 +#: rc_option_editor.cc:4716 msgid "Peak indicator threshold [dBFS]" msgstr "Порог индикатора пика [dBFS]" -#: rc_option_editor.cc:4713 +#: rc_option_editor.cc:4724 msgid "" "Specify the audio signal level in dBFS at and above which the meter-peak " "indicator will flash red." @@ -16676,11 +16757,11 @@ msgstr "" "Укажите номинальный и пиковый уровень звукового сигнала в в dbFS, когда " "индикатор мигает красным цветом." -#: rc_option_editor.cc:4717 +#: rc_option_editor.cc:4728 msgid "Default Meter Types" msgstr "Индикаторы по умолчанию" -#: rc_option_editor.cc:4718 +#: rc_option_editor.cc:4729 msgid "" "These settings apply to newly created tracks and busses. For the Master bus, " "this will be when a new session is created." @@ -16688,75 +16769,75 @@ msgstr "" "Эти параметры влияют на вновь созданные дорожки и шины. Изменение для мастер-" "шины вступит в силу после создания новой сессии." -#: rc_option_editor.cc:4724 +#: rc_option_editor.cc:4735 msgid "Default Meter Type for Master Bus" msgstr "Индикатор для мастер-шины" -#: rc_option_editor.cc:4742 +#: rc_option_editor.cc:4753 msgid "Default meter type for busses" msgstr "Индикатор для шин" -#: rc_option_editor.cc:4760 +#: rc_option_editor.cc:4771 msgid "Default meter type for tracks" msgstr "Индикатор для дорожек" -#: rc_option_editor.cc:4776 +#: rc_option_editor.cc:4787 msgid "Region Analysis" msgstr "Анализ области" -#: rc_option_editor.cc:4781 +#: rc_option_editor.cc:4792 msgid "Enable automatic analysis of audio" msgstr "Включить автоматический анализ звука" -#: rc_option_editor.cc:4792 rc_option_editor.cc:4810 rc_option_editor.cc:4861 -#: rc_option_editor.cc:4867 rc_option_editor.cc:4869 rc_option_editor.cc:4916 -#: rc_option_editor.cc:4919 rc_option_editor.cc:4921 rc_option_editor.cc:4941 -#: rc_option_editor.cc:4945 rc_option_editor.cc:4957 rc_option_editor.cc:4959 -#: rc_option_editor.cc:4961 rc_option_editor.cc:4970 rc_option_editor.cc:4979 -#: rc_option_editor.cc:4993 +#: rc_option_editor.cc:4803 rc_option_editor.cc:4821 rc_option_editor.cc:4872 +#: rc_option_editor.cc:4878 rc_option_editor.cc:4880 rc_option_editor.cc:4927 +#: rc_option_editor.cc:4930 rc_option_editor.cc:4932 rc_option_editor.cc:4952 +#: rc_option_editor.cc:4956 rc_option_editor.cc:4968 rc_option_editor.cc:4970 +#: rc_option_editor.cc:4972 rc_option_editor.cc:4981 rc_option_editor.cc:4990 +#: rc_option_editor.cc:5004 msgid "Performance" msgstr "Производительность" -#: rc_option_editor.cc:4792 +#: rc_option_editor.cc:4803 msgid "DSP CPU Utilization" msgstr "Использование ЦП" -#: rc_option_editor.cc:4796 +#: rc_option_editor.cc:4807 msgid "Signal processing uses" msgstr "При обработке используются" -#: rc_option_editor.cc:4801 rc_option_editor.cc:4932 +#: rc_option_editor.cc:4812 rc_option_editor.cc:4943 msgid "all but one processor" msgstr "Все процессоры кроме одного" -#: rc_option_editor.cc:4802 rc_option_editor.cc:4933 +#: rc_option_editor.cc:4813 rc_option_editor.cc:4944 msgid "all available processors" msgstr "Все доступные процессоры" -#: rc_option_editor.cc:4805 rc_option_editor.cc:4936 +#: rc_option_editor.cc:4816 rc_option_editor.cc:4947 msgid "%1 processor" msgid_plural "%1 processors" msgstr[0] "%1 процессор" msgstr[1] "%1 процессора" msgstr[2] "%1 процессоров" -#: rc_option_editor.cc:4808 rc_option_editor.cc:4939 +#: rc_option_editor.cc:4819 rc_option_editor.cc:4950 msgid "This setting will only take effect when %1 is restarted." msgstr "Это изменение вступит в силу при следующем запуске %1." -#: rc_option_editor.cc:4818 +#: rc_option_editor.cc:4829 msgid "Power Management, CPU DMA latency" msgstr "Управление питание, задержка CPU DMA" -#: rc_option_editor.cc:4849 +#: rc_option_editor.cc:4860 msgid "Lowest (prevent CPU sleep states)" msgstr "" -#: rc_option_editor.cc:4852 +#: rc_option_editor.cc:4863 msgid "%1 usec" msgstr "%1 мкс" -#: rc_option_editor.cc:4855 +#: rc_option_editor.cc:4866 msgid "" "This setting sets the maximum tolerable CPU DMA latency. This prevents the " "CPU from entering power-save states which can be beneficial for reliable low " @@ -16766,101 +16847,101 @@ msgstr "" "Это предотвращает переход процессора в состояние энергосбережения и может " "быть полезно для стабильно низкой задержки." -#: rc_option_editor.cc:4858 +#: rc_option_editor.cc:4869 msgid "This setting requires write access to `/dev/cpu_dma_latency'." msgstr "" "Для работы этой настройки нужны права на запись в `/dev/cpu_dma_latency'." -#: rc_option_editor.cc:4867 +#: rc_option_editor.cc:4878 msgid "CPU/FPU Denormals" msgstr "Денормализованные числа CPU/FPU" -#: rc_option_editor.cc:4872 +#: rc_option_editor.cc:4883 msgid "Use DC bias to protect against denormals" msgstr "Использовать смещение для защиты от денормировки" -#: rc_option_editor.cc:4879 +#: rc_option_editor.cc:4890 msgid "Processor handling" msgstr "Что делать с обработчиками" -#: rc_option_editor.cc:4885 +#: rc_option_editor.cc:4896 msgid "no processor handling" msgstr "Ничего не делать" -#: rc_option_editor.cc:4891 +#: rc_option_editor.cc:4902 msgid "use FlushToZero" msgstr "Использовать FlushToZero" -#: rc_option_editor.cc:4898 +#: rc_option_editor.cc:4909 msgid "use DenormalsAreZero" msgstr "Использовать DenormalsAreZero" -#: rc_option_editor.cc:4905 +#: rc_option_editor.cc:4916 msgid "use FlushToZero and DenormalsAreZero" msgstr "Использовать FlushToZero и DenormalsAreZero" -#: rc_option_editor.cc:4914 +#: rc_option_editor.cc:4925 msgid "Changes may not be effective until audio-engine restart." msgstr "Изменения могут не вступить в силу до перезапуска звукового движка." -#: rc_option_editor.cc:4919 +#: rc_option_editor.cc:4930 msgid "Disk I/O Buffering" msgstr "Буферизация чтения/записи диска" -#: rc_option_editor.cc:4926 +#: rc_option_editor.cc:4937 msgid "Disk I/O threads" msgstr "" -#: rc_option_editor.cc:4931 +#: rc_option_editor.cc:4942 msgid "all but two processor" msgstr "Все кроме двух процессоров" -#: rc_option_editor.cc:4945 +#: rc_option_editor.cc:4956 msgid "Memory Usage" msgstr "Использование памяти" -#: rc_option_editor.cc:4948 +#: rc_option_editor.cc:4959 msgid "Waveform image cache size (megabytes)" msgstr "Размер кэша для графики волновой формы (МБ)" -#: rc_option_editor.cc:4956 +#: rc_option_editor.cc:4967 msgid "" "Increasing the cache size uses more memory to store waveform images, which " "can improve graphical performance." msgstr "" -#: rc_option_editor.cc:4964 +#: rc_option_editor.cc:4975 msgid "Thinning factor (larger value => less data)" msgstr "Фактор разведения (большее значение => меньше данных)" -#: rc_option_editor.cc:4973 +#: rc_option_editor.cc:4984 msgid "Automation sampling interval (milliseconds)" msgstr "Интервал сэмплирования для автоматизации (мс)" -#: rc_option_editor.cc:4979 +#: rc_option_editor.cc:4990 msgid "Automatables" msgstr "Автоматизируемые параметры" -#: rc_option_editor.cc:4983 +#: rc_option_editor.cc:4994 msgid "Limit automatable parameters per plugin" msgstr "Ограничить число автоматизируемых параметров на плагин " -#: rc_option_editor.cc:4990 +#: rc_option_editor.cc:5001 msgid "256 parameters" msgstr "256 параметров" -#: rc_option_editor.cc:4991 +#: rc_option_editor.cc:5002 msgid "512 parameters" msgstr "512 параметров" -#: rc_option_editor.cc:4992 +#: rc_option_editor.cc:5003 msgid "999 parameters" msgstr "999 параметров" -#: rc_option_editor.cc:4995 +#: rc_option_editor.cc:5006 msgid "" "Some Plugins expose an unreasonable amount of control-inputs. This option " -"limits the number of parameters that are are listed as automatable without " +"limits the number of parameters that are listed as automatable without " "restricting the number of total controls.\n" "\n" "This reduces lag in the GUI and shortens excessively long drop-down lists " @@ -16870,20 +16951,20 @@ msgid "" "session-reload. Already automated parameters are retained." msgstr "" -#: rc_option_editor.cc:4998 rc_option_editor.cc:4999 +#: rc_option_editor.cc:5009 rc_option_editor.cc:5010 msgid "Video" msgstr "Видео" -#: rc_option_editor.cc:4998 +#: rc_option_editor.cc:5009 msgid "Video Server" msgstr "Видеосервер" -#: rc_option_editor.cc:5003 rc_option_editor.cc:5010 rc_option_editor.cc:5012 -#: rc_option_editor.cc:5014 rc_option_editor.cc:5021 +#: rc_option_editor.cc:5014 rc_option_editor.cc:5021 rc_option_editor.cc:5023 +#: rc_option_editor.cc:5025 rc_option_editor.cc:5032 msgid "Triggering" msgstr "Триггеры" -#: rc_option_editor.cc:5007 +#: rc_option_editor.cc:5018 msgid "" "If set, this identifies the input MIDI port that will be automatically " "connected to trigger boxes.\n" @@ -16894,15 +16975,15 @@ msgid "" "typical keyboard)" msgstr "" -#: rc_option_editor.cc:5012 +#: rc_option_editor.cc:5023 msgid "Clip Library" msgstr "Библиотека клипов" -#: rc_option_editor.cc:5016 +#: rc_option_editor.cc:5027 msgid "User writable Clip Library:" msgstr "Пользовательская библиотека клипов:" -#: rc_option_editor.cc:5022 +#: rc_option_editor.cc:5033 msgid "Reset Clip Library Dir" msgstr "Вернуть исходное расположение библиотеки" @@ -17137,11 +17218,11 @@ msgstr "Смена длительности области" msgid "change region sync point" msgstr "Смена синхронизатора областей" -#: region_editor.cc:777 +#: region_editor.cc:778 msgid "Failed to load Region Effect Plugin" msgstr "Не удалось загрузить плагин эффектов для области" -#: region_editor.cc:955 +#: region_editor.cc:960 msgid "" "%1\n" "Double-click to show generic GUI." @@ -17300,39 +17381,39 @@ msgstr "Действие" msgid "split regions (rhythm ferret)" msgstr "разделение областей (ритмический хорёк)" -#: route_group_dialog.cc:44 +#: route_group_dialog.cc:45 msgid "Track/bus Group" msgstr "Группа дорожек/шин" -#: route_group_dialog.cc:49 +#: route_group_dialog.cc:50 msgid "Relative" msgstr "Относительное" -#: route_group_dialog.cc:50 +#: route_group_dialog.cc:51 msgid "Muting" msgstr "Приглушение" -#: route_group_dialog.cc:52 +#: route_group_dialog.cc:53 msgid "Record enable" msgstr "Готовность к записи" -#: route_group_dialog.cc:53 +#: route_group_dialog.cc:54 msgid "Surround Send enable" msgstr "" -#: route_group_dialog.cc:55 +#: route_group_dialog.cc:56 msgid "Active state" msgstr "Активное состояние" -#: route_group_dialog.cc:61 +#: route_group_dialog.cc:62 msgid "RouteGroupDialog" msgstr "RouteGroupDialog" -#: route_group_dialog.cc:102 +#: route_group_dialog.cc:103 msgid "Sharing" msgstr "Разделяются:" -#: route_group_dialog.cc:196 +#: route_group_dialog.cc:199 msgid "The group name is not unique. Please use a different name." msgstr "Название группы не является уникальным. Используйте другое имя." @@ -17500,7 +17581,7 @@ msgstr "По времени захвата" msgid "Alignment" msgstr "Выравнивание" -#: route_time_axis.cc:787 route_time_axis.cc:1450 route_ui.cc:2547 +#: route_time_axis.cc:787 route_time_axis.cc:1456 route_ui.cc:2547 #: track_record_axis.cc:175 msgid "Playlist" msgstr "Плейлист" @@ -17521,27 +17602,27 @@ msgstr "" msgid "Time Domain" msgstr "Тип времени" -#: route_time_axis.cc:1263 +#: route_time_axis.cc:1269 msgid "The name \"%1\" is reserved for %2" msgstr "Название \"%1 зарезервировано для %2" -#: route_time_axis.cc:1440 route_ui.cc:2540 +#: route_time_axis.cc:1446 route_ui.cc:2540 msgid "Take: %1.%2" msgstr "Дубль: %1.%2" -#: route_time_axis.cc:1891 selection.cc:904 selection.cc:960 +#: route_time_axis.cc:1897 selection.cc:904 selection.cc:960 msgid "programming error: " msgstr "Ошибка в программе: " -#: route_time_axis.cc:2053 route_time_axis.cc:2080 +#: route_time_axis.cc:2059 route_time_axis.cc:2086 msgid "Parameters %1 - %2" msgstr "Параметры %1 - %2" -#: route_time_axis.cc:2401 vca_master_strip.cc:229 vca_time_axis.cc:274 +#: route_time_axis.cc:2407 vca_master_strip.cc:229 vca_time_axis.cc:274 msgid "After-fade listen (AFL)" msgstr "Прослушивание после фейдера (AFL)" -#: route_time_axis.cc:2405 vca_master_strip.cc:233 vca_time_axis.cc:278 +#: route_time_axis.cc:2411 vca_master_strip.cc:233 vca_time_axis.cc:278 msgid "Pre-fade listen (PFL)" msgstr "Прослушивание до фейдера (PFL)" @@ -18653,15 +18734,15 @@ msgstr "16-bit integer" msgid "32-bit floating point" msgstr "32-bit floating point" -#: sfdb_ui.cc:110 sfdb_ui.cc:1977 +#: sfdb_ui.cc:110 sfdb_ui.cc:1982 msgid "by track number" msgstr "По номеру дорожки" -#: sfdb_ui.cc:112 sfdb_ui.cc:1978 +#: sfdb_ui.cc:112 sfdb_ui.cc:1983 msgid "by track name" msgstr "По названию дорожки" -#: sfdb_ui.cc:114 sfdb_ui.cc:1979 +#: sfdb_ui.cc:114 sfdb_ui.cc:1984 msgid "by instrument name" msgstr "По названию инструмента" @@ -18701,7 +18782,7 @@ msgstr "Отметка времени:" msgid "Tempo Map:" msgstr "Карта темпа:" -#: sfdb_ui.cc:230 sfdb_ui.cc:778 +#: sfdb_ui.cc:230 sfdb_ui.cc:781 msgid "Tags:" msgstr "Метки:" @@ -18738,185 +18819,185 @@ msgid "SoundFileBox: Could not tokenize string: " msgstr "SoundFileBox: Не удалось разобрать строку: " #: sfdb_ui.cc:700 -msgid "Audio and MIDI files" -msgstr "Звуковые и MIDI-файлы" - -#: sfdb_ui.cc:703 msgid "Audio files" msgstr "Звуковые файлы" -#: sfdb_ui.cc:706 +#: sfdb_ui.cc:705 +msgid "Audio and MIDI files" +msgstr "Звуковые и MIDI-файлы" + +#: sfdb_ui.cc:708 msgid "MIDI files" msgstr "Файлы MIDI" -#: sfdb_ui.cc:709 add_video_dialog.cc:129 +#: sfdb_ui.cc:711 add_video_dialog.cc:129 msgid "All files" msgstr "Все файлы" -#: sfdb_ui.cc:728 add_video_dialog.cc:255 +#: sfdb_ui.cc:730 add_video_dialog.cc:255 msgid "Browse Files" msgstr "Обзор файлов" -#: sfdb_ui.cc:756 +#: sfdb_ui.cc:759 msgid "Paths" msgstr "Расположения" -#: sfdb_ui.cc:765 +#: sfdb_ui.cc:768 msgid "Search Tags" msgstr "Поиск по меткам" -#: sfdb_ui.cc:785 +#: sfdb_ui.cc:788 msgid "Sort:" msgstr "Критерий сортировки:" -#: sfdb_ui.cc:794 +#: sfdb_ui.cc:797 msgid "Longest" msgstr "Более длинные" -#: sfdb_ui.cc:795 +#: sfdb_ui.cc:798 msgid "Shortest" msgstr "Более короткие" -#: sfdb_ui.cc:796 +#: sfdb_ui.cc:799 msgid "Newest" msgstr "Более новые" -#: sfdb_ui.cc:797 +#: sfdb_ui.cc:800 msgid "Oldest" msgstr "Более старые" -#: sfdb_ui.cc:798 +#: sfdb_ui.cc:801 msgid "Most downloaded" msgstr "Чаще скачиваемые" -#: sfdb_ui.cc:799 +#: sfdb_ui.cc:802 msgid "Least downloaded" msgstr "Реже скачиваемые" -#: sfdb_ui.cc:800 +#: sfdb_ui.cc:803 msgid "Highest rated" msgstr "Выше оценённые" -#: sfdb_ui.cc:801 +#: sfdb_ui.cc:804 msgid "Lowest rated" msgstr "Ниже оценённые" -#: sfdb_ui.cc:807 +#: sfdb_ui.cc:810 msgid "License:" msgstr "Лицензия:" -#: sfdb_ui.cc:813 +#: sfdb_ui.cc:816 msgid "Any" msgstr "Любая" -#: sfdb_ui.cc:814 +#: sfdb_ui.cc:817 msgid "CC-BY" msgstr "CC-BY" -#: sfdb_ui.cc:815 +#: sfdb_ui.cc:818 msgid "CC-BY-NC" msgstr "CC-BY-NC" -#: sfdb_ui.cc:816 +#: sfdb_ui.cc:819 msgid "PD" msgstr "PD" -#: sfdb_ui.cc:823 +#: sfdb_ui.cc:826 msgid "More" msgstr "Ещё" -#: sfdb_ui.cc:827 +#: sfdb_ui.cc:830 msgid "Similar" msgstr "Подобное" -#: sfdb_ui.cc:839 +#: sfdb_ui.cc:842 msgid "ID" msgstr "ID" -#: sfdb_ui.cc:840 add_video_dialog.cc:88 +#: sfdb_ui.cc:843 add_video_dialog.cc:88 msgid "Filename" msgstr "Имя файла" -#: sfdb_ui.cc:841 time_fx_dialog.cc:159 +#: sfdb_ui.cc:844 time_fx_dialog.cc:159 msgid "Duration" msgstr "Длительность" -#: sfdb_ui.cc:872 +#: sfdb_ui.cc:875 msgid "Search Freesound" msgstr "Поиск по Freesound" -#: sfdb_ui.cc:886 +#: sfdb_ui.cc:890 msgid "Press to import selected files" msgstr "Нажмите для импорта выбранных файлов" -#: sfdb_ui.cc:1106 +#: sfdb_ui.cc:1110 msgid "SoundFileBrowser: Could not tokenize string: " msgstr "SoundFileBrowser: Не удалось разметить строку:" -#: sfdb_ui.cc:1336 +#: sfdb_ui.cc:1340 msgid "%1 more page of 100 results available" msgid_plural "%1 more pages of 100 results available" msgstr[0] "Ещё %1 страница из 100 доступных" msgstr[1] "Ещё %1 страницы из 100 доступных" msgstr[2] "Ещё %1 страниц из 100 доступных" -#: sfdb_ui.cc:1341 +#: sfdb_ui.cc:1345 msgid "No more results available" msgstr "Больше результатов нет" -#: sfdb_ui.cc:1416 +#: sfdb_ui.cc:1420 msgid "B" msgstr "Б" -#: sfdb_ui.cc:1418 +#: sfdb_ui.cc:1422 msgid "kB" msgstr "КБ" -#: sfdb_ui.cc:1420 sfdb_ui.cc:1422 +#: sfdb_ui.cc:1424 sfdb_ui.cc:1426 msgid "MB" msgstr "МБ" -#: sfdb_ui.cc:1424 +#: sfdb_ui.cc:1428 msgid "GB" msgstr "ГБ" -#: sfdb_ui.cc:1455 +#: sfdb_ui.cc:1459 msgid "Failed to retrieve XML for file" msgstr "Не удалось получить XML для файла" -#: sfdb_ui.cc:1673 sfdb_ui.cc:1984 sfdb_ui.cc:2018 sfdb_ui.cc:2036 +#: sfdb_ui.cc:1677 sfdb_ui.cc:1989 sfdb_ui.cc:2023 sfdb_ui.cc:2041 msgid "one track per file" msgstr "Одна дорожка на файл" -#: sfdb_ui.cc:1676 sfdb_ui.cc:2019 sfdb_ui.cc:2037 +#: sfdb_ui.cc:1680 sfdb_ui.cc:2024 sfdb_ui.cc:2042 msgid "one track per channel" msgstr "Одна дорожка на канал" -#: sfdb_ui.cc:1683 sfdb_ui.cc:2021 sfdb_ui.cc:2038 +#: sfdb_ui.cc:1687 sfdb_ui.cc:2026 sfdb_ui.cc:2043 msgid "sequence files" msgstr "Файлы последовательности" -#: sfdb_ui.cc:1685 sfdb_ui.cc:2026 +#: sfdb_ui.cc:1689 sfdb_ui.cc:2031 msgid "all files in one track" msgstr "Все файлы в одну дорожку" -#: sfdb_ui.cc:1686 sfdb_ui.cc:2020 +#: sfdb_ui.cc:1690 sfdb_ui.cc:2025 msgid "merge files" msgstr "Объединить файлы" -#: sfdb_ui.cc:1692 sfdb_ui.cc:2023 +#: sfdb_ui.cc:1696 sfdb_ui.cc:2028 msgid "one region per file" msgstr "Одна область на файл" -#: sfdb_ui.cc:1695 sfdb_ui.cc:2024 +#: sfdb_ui.cc:1699 sfdb_ui.cc:2029 msgid "one region per channel" msgstr "Одна область на канал" -#: sfdb_ui.cc:1700 sfdb_ui.cc:2025 sfdb_ui.cc:2039 +#: sfdb_ui.cc:1704 sfdb_ui.cc:2030 sfdb_ui.cc:2044 msgid "all files in one region" msgstr "Все файлы в одной области" -#: sfdb_ui.cc:1752 +#: sfdb_ui.cc:1756 msgid "" "One or more of the selected files\n" "cannot be used by %1" @@ -18924,87 +19005,87 @@ msgstr "" "Один или более выбранных файлов\n" "не могут быть использованы в %1" -#: sfdb_ui.cc:1890 +#: sfdb_ui.cc:1894 msgid "Copy audio files to session" msgstr "Скопировать звуковые файлы в сессию" -#: sfdb_ui.cc:1891 +#: sfdb_ui.cc:1895 msgid "Use MIDI Tempo Map" msgstr "Использовать карту темпа MIDI-файла" -#: sfdb_ui.cc:1892 +#: sfdb_ui.cc:1896 msgid "Import MIDI markers" msgstr "Импортировать MIDI-маркеры" -#: sfdb_ui.cc:1907 sfdb_ui.cc:2093 +#: sfdb_ui.cc:1911 sfdb_ui.cc:2098 msgid "file timestamp" msgstr "По отметке времени файла" -#: sfdb_ui.cc:1908 sfdb_ui.cc:2095 +#: sfdb_ui.cc:1912 sfdb_ui.cc:2100 msgid "edit point" msgstr "По точке редактирования" -#: sfdb_ui.cc:1909 sfdb_ui.cc:2097 +#: sfdb_ui.cc:1913 sfdb_ui.cc:2102 msgid "playhead" msgstr "По указателю воспр." -#: sfdb_ui.cc:1910 +#: sfdb_ui.cc:1914 msgid "session start" msgstr "В начало сессии" -#: sfdb_ui.cc:1919 +#: sfdb_ui.cc:1923 msgid "Add files:" msgstr "Добавить файлы:" -#: sfdb_ui.cc:1925 +#: sfdb_ui.cc:1929 msgid "Insert at:" msgstr "Куда вставить:" -#: sfdb_ui.cc:1931 +#: sfdb_ui.cc:1935 msgid "Mapping:" msgstr "Распределение:" -#: sfdb_ui.cc:1937 +#: sfdb_ui.cc:1941 msgid "Sort order:" msgstr "Порядок сортировки:" -#: sfdb_ui.cc:1949 +#: sfdb_ui.cc:1954 msgid "MIDI Instrument:" msgstr "MIDI-инструмент:" -#: sfdb_ui.cc:1955 +#: sfdb_ui.cc:1960 msgid "MIDI Track Names:" msgstr "Названия MIDI-дорожек:" -#: sfdb_ui.cc:1969 +#: sfdb_ui.cc:1974 msgid "Audio conversion quality:" msgstr "Качество конвертирования аудио:" -#: sfdb_ui.cc:1990 sfdb_ui.cc:2109 +#: sfdb_ui.cc:1995 sfdb_ui.cc:2114 msgid "Best" msgstr "Наилучшее" -#: sfdb_ui.cc:1992 sfdb_ui.cc:2113 +#: sfdb_ui.cc:1997 sfdb_ui.cc:2118 msgid "Quick" msgstr "Быстрое" -#: sfdb_ui.cc:1994 +#: sfdb_ui.cc:1999 msgid "Fastest" msgstr "Быстрее всего" -#: sfdb_ui.cc:2002 sfdb_ui.cc:2068 +#: sfdb_ui.cc:2007 sfdb_ui.cc:2073 msgid "by file name" msgstr "По имени файла" -#: sfdb_ui.cc:2003 sfdb_ui.cc:2070 +#: sfdb_ui.cc:2008 sfdb_ui.cc:2075 msgid "by modification time" msgstr "По дате изменения" -#: sfdb_ui.cc:2004 sfdb_ui.cc:2072 +#: sfdb_ui.cc:2009 sfdb_ui.cc:2077 msgid "by selection order" msgstr "По порядку сортировки" -#: sfdb_ui.cc:2075 +#: sfdb_ui.cc:2080 msgid "programming error: unknown import sort string %1" msgstr "Ошибка в коде: неизвестная строка сортировки %1" @@ -20305,7 +20386,7 @@ msgstr "" msgid "New transport master not added - check error log for details" msgstr "" -#: transport_masters_dialog.cc:624 +#: transport_masters_dialog.cc:625 msgid "%1 %2" msgstr "%1 %2" @@ -20573,15 +20654,15 @@ msgid "" "Right-click to select Launch Options for this clip" msgstr "" -#: ui_config.cc:266 ui_config.cc:456 +#: ui_config.cc:266 ui_config.cc:465 msgid "Loading default ui configuration file %1" msgstr "Загрузка файла конфигурации UI по умолчанию %1" -#: ui_config.cc:269 ui_config.cc:459 +#: ui_config.cc:269 ui_config.cc:468 msgid "cannot read default ui configuration file \"%1\"" msgstr "Невозможно прочитать основной файл конфигурации интерфейса \"%1\"" -#: ui_config.cc:272 ui_config.cc:464 +#: ui_config.cc:272 ui_config.cc:473 msgid "default ui configuration file \"%1\" not loaded successfully." msgstr "Основной файл конфигурации интерфейса \"%1\" не был успешно загружен" @@ -20589,65 +20670,65 @@ msgstr "Основной файл конфигурации интерфейса msgid "Could not find default UI configuration file %1" msgstr "Не удалось найти конфигурационный файл %1 для интерфейса" -#: ui_config.cc:327 +#: ui_config.cc:332 msgid "Loading color file %1" msgstr "Загружается файл %1 с описанием цветовой схемы" -#: ui_config.cc:330 +#: ui_config.cc:335 msgid "cannot read color file \"%1\"" msgstr "Невозможно прочитать файл %1 с описанием цветовой схемы" -#: ui_config.cc:335 +#: ui_config.cc:340 msgid "color file \"%1\" not loaded successfully." msgstr "Файл %1 с описанием цветовой схемы не был успешно загружен." -#: ui_config.cc:363 -msgid "Color file for %1 not found along %2" -msgstr "Файл цветовой схемы для %1 не найден по адресу %2" +#: ui_config.cc:399 +msgid "no theme file was found; colors will be odd" +msgstr "" -#: ui_config.cc:438 ui_config.cc:531 +#: ui_config.cc:447 ui_config.cc:540 msgid "Color file %1 not saved" msgstr "Файл %1 с описанием цветовой схемы не сохранён" -#: ui_config.cc:473 +#: ui_config.cc:482 msgid "Loading user ui configuration file %1" msgstr "Загрузка файла пользовательской конфигурации UI %1" -#: ui_config.cc:476 +#: ui_config.cc:485 msgid "cannot read ui configuration file \"%1\"" msgstr "Невозможно прочитать файл конфигурации UI \"%1\"" -#: ui_config.cc:481 +#: ui_config.cc:490 msgid "user ui configuration file \"%1\" not loaded successfully." msgstr "Конфигурация UI интерфейса файлa \"%1\" не загружена успешно." -#: ui_config.cc:489 +#: ui_config.cc:498 msgid "could not find any ui configuration file, canvas will look broken." msgstr "Невозможно найти файл конфигурации UI, это будет выглядеть сломаным." -#: ui_config.cc:510 +#: ui_config.cc:519 msgid "Config file %1 not saved" msgstr "Конфигурационный файл %1 не сохранён" -#: ui_config.cc:512 ui_config.cc:520 +#: ui_config.cc:521 ui_config.cc:529 msgid "Could not remove temporary ui-config file \"%1\" (%2)" msgstr "" -#: ui_config.cc:518 +#: ui_config.cc:527 msgid "could not rename temporary ui-config file %1 to %2 (%3)" msgstr "" -#: ui_config.cc:761 +#: ui_config.cc:770 msgid "Color %1 not found" msgstr "Цвет %1 не обнаружен" -#: ui_config.cc:831 +#: ui_config.cc:840 msgid "Unable to find UI style file %1 in search path %2. %3 will look strange" msgstr "" "Не удается найти файл стилей пользовательского интерфейса %1 в пути поиска " "%2. % 3 будет выглядеть странно" -#: ui_config.cc:837 +#: ui_config.cc:846 msgid "Loading ui configuration file %1" msgstr "Загрузка файла настройки пользовательского интерфейса %1" @@ -20690,10 +20771,6 @@ msgstr "Получено исключение при загрузке значк msgid "format_position: negative timecode position: %1" msgstr "" -#: varispeed_dialog.cc:33 -msgid "Varispeed" -msgstr "" - #: varispeed_dialog.cc:57 msgid "Percentage:" msgstr "В процентах:" @@ -21336,6 +21413,9 @@ msgstr "" msgid "Input Video File" msgstr "Исходный видеофайл" +#~ msgid "Color file for %1 not found along %2" +#~ msgstr "Файл цветовой схемы для %1 не найден по адресу %2" + #~ msgid "Only Other Ranges" #~ msgstr "Только прочие диапазоны" @@ -21495,9 +21575,6 @@ msgstr "Исходный видеофайл" #~ msgid "inactive" #~ msgstr "неактивно" -#~ msgid "Do not show this dialog again." -#~ msgstr "Больше не показывать этот диалог" - #~ msgid "MIDI Regions" #~ msgstr "MIDI-области" diff --git a/gtk2_ardour/port_insert_ui.cc b/gtk2_ardour/port_insert_ui.cc index 0b22f83d2a..7c88924da7 100644 --- a/gtk2_ardour/port_insert_ui.cc +++ b/gtk2_ardour/port_insert_ui.cc @@ -35,7 +35,7 @@ #include "context_menu_helper.h" #include "gui_thread.h" -#include "latency_gui.h" +#include "timectl_gui.h" #include "port_insert_ui.h" #include "timers.h" #include "utils.h" @@ -282,7 +282,7 @@ PortInsertUI::edit_latency_button_clicked () { assert (_pi); if (!_latency_gui) { - _latency_gui = new LatencyGUI (*(_pi.get ()), _pi->session ().sample_rate (), _pi->session ().get_block_size ()); + _latency_gui = new TimeCtlGUI (*(_pi.get ()), _pi->session ().sample_rate (), _pi->session ().get_block_size ()); _latency_dialog = new ArdourWindow (_("Edit Latency")); /* use both keep-above and transient for to try cover as many different WM's as possible. diff --git a/gtk2_ardour/port_insert_ui.h b/gtk2_ardour/port_insert_ui.h index 6538b7015b..94f4188419 100644 --- a/gtk2_ardour/port_insert_ui.h +++ b/gtk2_ardour/port_insert_ui.h @@ -31,7 +31,7 @@ namespace ARDOUR class PortInsert; } -class LatencyGUI; +class TimeCtlGUI; class MTDM; class PortInsertUI : public Gtk::VBox @@ -78,7 +78,7 @@ private: Gtk::HBox _latency_hbox; Gtk::Window* _parent; - LatencyGUI* _latency_gui; + TimeCtlGUI* _latency_gui; ArdourWindow* _latency_dialog; sigc::connection _latency_timeout; diff --git a/gtk2_ardour/region_editor.cc b/gtk2_ardour/region_editor.cc index 0c65eb9fb7..fd4e62546b 100644 --- a/gtk2_ardour/region_editor.cc +++ b/gtk2_ardour/region_editor.cc @@ -51,6 +51,7 @@ #include "new_plugin_preset_dialog.h" #include "region_editor.h" #include "region_view.h" +#include "timers.h" #include "plugin_selector.h" #include "plugin_window_proxy.h" #include "public_editor.h" @@ -188,7 +189,7 @@ RegionEditor::RegionEditor (Session* s, RegionView* rv) _table.attach (_sources, 1, 2, _table_row, _table_row + 1, Gtk::FILL | Gtk::EXPAND, Gtk::FILL); ++_table_row; -#ifndef MIXBUS // no region FX +#ifndef LIVETRAX // no region FX _table.attach (region_fx_label, 2, 3, 0, 1, Gtk::FILL, Gtk::FILL); _table.attach (_region_fx_box, 2, 3, 1, _table_row + 2, Gtk::FILL, Gtk::FILL); #endif @@ -233,7 +234,7 @@ RegionEditor::RegionEditor (Session* s, RegionView* rv) spin_arrow_grab = false; -#if 0 +#ifndef LIVETRAX // no region FX /* for now only audio region effects are supported */ if (std::dynamic_pointer_cast (_region)) { region_fx_label.show (); @@ -558,6 +559,8 @@ RegionEditor::RegionFxBox::RegionFxBox (std::shared_ptr r) _display.signal_key_press_event ().connect (sigc::mem_fun (*this, &RegionFxBox::on_key_press), false); + screen_update_connection = Timers::super_rapid_connect (sigc::mem_fun (*this, &RegionFxBox::update_controls)); + _scroller.show (); _display.show (); @@ -604,7 +607,8 @@ RegionEditor::RegionFxBox::add_fx_to_display (std::weak_ptr wfx) if (!fx) { return; } - RegionFxEntry* e = new RegionFxEntry (fx); + std::shared_ptr ar = std::dynamic_pointer_cast (_region); + RegionFxEntry* e = new RegionFxEntry (fx, ar && ar->fade_before_fx ()); _display.add_child (e, drag_targets ()); } @@ -638,50 +642,53 @@ RegionEditor::RegionFxBox::fxe_button_press_event (GdkEventButton* ev, RegionFxE std::shared_ptr plugin = child->region_fx_plugin ()->plugin (); - items.push_back (SeparatorElem ()); - items.push_back (MenuElem (_("Edit..."), sigc::bind (sigc::mem_fun (*this, &RegionFxBox::show_plugin_gui), wfx, true))); - items.back ().set_sensitive (plugin->has_editor ()); - items.push_back (MenuElem (_("Edit with generic controls..."), sigc::bind (sigc::mem_fun (*this, &RegionFxBox::show_plugin_gui), wfx, false))); - - Gtk::Menu* automation_menu = manage (new Gtk::Menu); - MenuList& ac_items (automation_menu->items ()); - - for (size_t i = 0; i < plugin->parameter_count (); ++i) { - if (!plugin->parameter_is_control (i) || !plugin->parameter_is_input (i)) { - continue; - } - const Evoral::Parameter param (PluginAutomation, 0, i); - std::string label = plugin->describe_parameter (param); - if (label == X_("latency") || label == X_("hidden")) { - continue; - } - std::shared_ptr c (std::dynamic_pointer_cast (child->region_fx_plugin ()->control (param))); - if (c && c->flags () & (Controllable::HiddenControl | Controllable::NotAutomatable)) { - continue; - } - - std::weak_ptr wac (c); - bool play = c->automation_state () == Play; - - ac_items.push_back (CheckMenuElem (label)); - Gtk::CheckMenuItem* cmi = static_cast (&ac_items.back ()); - cmi->set_active (play); - cmi->signal_activate ().connect ([wac, play] () { - std::shared_ptr ac = wac.lock (); - if (ac) { - ac->set_automation_state (play ? ARDOUR::Off : Play); - } - }); - } - - if (!ac_items.empty ()) { + if (plugin) { + items.push_back (SeparatorElem ()); + items.push_back (MenuElem (_("Edit..."), sigc::bind (sigc::mem_fun (*this, &RegionFxBox::show_plugin_gui), wfx, true))); + items.back ().set_sensitive (plugin->has_editor ()); + items.push_back (MenuElem (_("Edit with generic controls..."), sigc::bind (sigc::mem_fun (*this, &RegionFxBox::show_plugin_gui), wfx, false))); + + Gtk::Menu* automation_menu = manage (new Gtk::Menu); + MenuList& ac_items (automation_menu->items ()); + + for (size_t i = 0; i < plugin->parameter_count (); ++i) { + if (!plugin->parameter_is_control (i) || !plugin->parameter_is_input (i)) { + continue; + } + const Evoral::Parameter param (PluginAutomation, 0, i); + std::string label = plugin->describe_parameter (param); + if (label == X_("latency") || label == X_("hidden")) { + continue; + } + std::shared_ptr c (std::dynamic_pointer_cast (child->region_fx_plugin ()->control (param))); + if (c && c->flags () & (Controllable::HiddenControl | Controllable::NotAutomatable)) { + continue; + } + + std::weak_ptr wac (c); + bool play = c->automation_state () == Play; + + ac_items.push_back (CheckMenuElem (label)); + Gtk::CheckMenuItem* cmi = static_cast (&ac_items.back ()); + cmi->set_active (play); + cmi->signal_activate ().connect ([wac, play] () { + std::shared_ptr ac = wac.lock (); + if (ac) { + ac->set_automation_state (play ? ARDOUR::Off : Play); + } + }); + } + + if (!ac_items.empty ()) { + items.push_back (SeparatorElem ()); + items.push_back (MenuElem (_("Automation Enable"), *automation_menu)); + items.push_back (MenuElem (_("Clear All Automation"), sigc::bind (sigc::mem_fun (*this, &RegionFxBox::clear_automation), wfx))); + } else { + delete automation_menu; + } items.push_back (SeparatorElem ()); - items.push_back (MenuElem ("Automation Enable", *automation_menu)); - } else { - delete automation_menu; } - items.push_back (SeparatorElem ()); items.push_back (MenuElem (_("Delete"), sigc::bind (sigc::mem_fun (*this, &RegionFxBox::queue_delete_region_fx), wfx))); m->signal_unmap ().connect ([this, &npm] () { npm.remove_submenu (); _display.remove_placeholder (); }); @@ -749,6 +756,60 @@ RegionEditor::RegionFxBox::on_key_press (GdkEventKey* ev) return true; } +void +RegionEditor::RegionFxBox::update_controls () +{ + for (auto const& i : _display.children ()) { + std::shared_ptr rfx = i->region_fx_plugin (); + PluginWindowProxy* pwp = dynamic_cast (rfx->window_proxy ()); + if (!pwp || !pwp->get (false) || !pwp->get (false)->is_mapped ()) { + continue; + } + rfx->maybe_emit_changed_signals (); + } +} + +void +RegionEditor::RegionFxBox::clear_automation (std::weak_ptr wfx) +{ + std::shared_ptr fx (wfx.lock ()); + if (!fx) { + return; + } + bool in_command = false; + + timepos_t tas ((samplepos_t)_region->length().samples()); + + for (auto const& c : fx->controls ()) { + std::shared_ptr ac = std::dynamic_pointer_cast (c.second); + if (!ac) { + continue; + } + std::shared_ptr alist = ac->alist (); + if (!alist) { + continue; + } + + XMLNode& before (alist->get_state()); + + alist->freeze (); + alist->clear (); + fx->set_default_automation (tas); + alist->thaw (); + alist->set_automation_state (ARDOUR::Off); + + if (!in_command) { + _region->session ().begin_reversible_command (_("Clear region fx automation")); + in_command = true; + } + _region->session ().add_command (new MementoCommand(*alist.get(), &before, &alist->get_state())); + } + + if (in_command) { + _region->session ().commit_reversible_command (); + } +} + void RegionEditor::RegionFxBox::reordered () { @@ -909,7 +970,7 @@ void RegionEditor::RegionFxBox::show_plugin_gui (std::weak_ptr wfx, bool custom_ui) { std::shared_ptr rfx (wfx.lock ()); - if (!rfx) { + if (!rfx || !rfx->plugin ()) { return; } @@ -928,7 +989,7 @@ RegionEditor::RegionFxBox::show_plugin_gui (std::weak_ptr wfx, b rfx->set_window_proxy (pwp); WM::Manager::instance ().register_window (pwp); RegionView* rv = PublicEditor::instance ().regionview_from_region (_region); - rv->RegionViewGoingAway.connect_same_thread (*pwp, [pwp] (RegionView*) { pwp->hide (); }); + rv->RegionViewGoingAway.connect_same_thread (*pwp, [pwp, rv] (RegionView* srv) { if (rv == srv) { pwp->hide (); }}); } pwp->set_custom_ui_mode (custom_ui); @@ -942,20 +1003,35 @@ RegionEditor::RegionFxBox::show_plugin_gui (std::weak_ptr wfx, b /* ****************************************************************************/ -RegionEditor::RegionFxEntry::RegionFxEntry (std::shared_ptr rfx) +RegionEditor::RegionFxEntry::RegionFxEntry (std::shared_ptr rfx, bool pre) : _fx_btn (ArdourWidgets::ArdourButton::default_elements) , _rfx (rfx) { _box.pack_start (_fx_btn, true, true); - _plugin_preset_pointer = PluginPresetPtr (new PluginPreset (rfx->plugin ()->get_info ())); + if (rfx->plugin ()) { + _plugin_preset_pointer = PluginPresetPtr (new PluginPreset (rfx->plugin ()->get_info ())); + _selectable = true; + } else { + _plugin_preset_pointer = 0; + _selectable = false; + } _fx_btn.set_fallthrough_to_parent (true); _fx_btn.set_text (name ()); _fx_btn.set_active (true); - _fx_btn.set_name ("processor postfader"); - if (rfx->plugin ()->has_editor ()) { + if (!_selectable) { + _fx_btn.set_name ("processor stub"); + } else if (pre) { + _fx_btn.set_name ("processor prefader"); + } else { + _fx_btn.set_name ("processor postfader"); + } + + if (!rfx->plugin ()) { + set_tooltip (_fx_btn, string_compose (_("%1\nThe Plugin is not available on this system\nand has been replaced by a stub."), name ())); + } else if (rfx->plugin ()->has_editor ()) { set_tooltip (_fx_btn, string_compose (_("%1\nDouble-click to show GUI.\n%2+double-click to show generic GUI."), name (), Keyboard::secondary_modifier_name ())); } else { set_tooltip (_fx_btn, string_compose (_("%1\nDouble-click to show generic GUI."), name ())); @@ -986,7 +1062,7 @@ RegionEditor::RegionFxEntry::can_copy_state (Gtkmm2ext::DnDVBoxChild* o) const } std::shared_ptr my_p = self->plugin (); std::shared_ptr ot_p = othr->plugin (); - return my_p->unique_id () == ot_p->unique_id (); + return my_p && ot_p && my_p->unique_id () == ot_p->unique_id (); } void @@ -1008,7 +1084,9 @@ RegionEditor::RegionFxEntry::drag_data_get (Glib::RefPtr const } std::shared_ptr plugin = _rfx->plugin (); - assert (plugin); + if (!plugin) { + return false; + } PluginManager& manager (PluginManager::instance ()); bool fav = manager.get_status (_plugin_preset_pointer->_pip) == PluginManager::Favorite; diff --git a/gtk2_ardour/region_editor.h b/gtk2_ardour/region_editor.h index ecc5f9f6b1..86952592f3 100644 --- a/gtk2_ardour/region_editor.h +++ b/gtk2_ardour/region_editor.h @@ -75,12 +75,12 @@ private: class RegionFxEntry : public Gtkmm2ext::DnDVBoxChild, public sigc::trackable { public: - RegionFxEntry (std::shared_ptr); + RegionFxEntry (std::shared_ptr, bool pre); Gtk::EventBox& action_widget () { return _fx_btn; } Gtk::Widget& widget () { return _box; } std::string drag_text () const { return name (); } - bool is_selectable() const { return true; } + bool is_selectable() const { return _selectable; } bool can_copy_state (Gtkmm2ext::DnDVBoxChild*) const; void set_visual_state (Gtkmm2ext::VisualState, bool); bool drag_data_get (Glib::RefPtr const, Gtk::SelectionData &); @@ -93,6 +93,7 @@ private: ArdourWidgets::ArdourButton _fx_btn; std::shared_ptr _rfx; ARDOUR::PluginPresetPtr _plugin_preset_pointer; + bool _selectable; }; class RegionFxBox : public Gtk::VBox, public PluginInterestedObject //, public ARDOUR::SessionHandlePtr @@ -108,6 +109,8 @@ private: bool idle_delete_region_fx (std::weak_ptr); void notify_plugin_load_fail (uint32_t cnt = 1); bool on_key_press (GdkEventKey*); + void clear_automation (std::weak_ptr); + void update_controls (); /* PluginInterestedObject */ bool use_plugins (SelectedPlugins const&); @@ -129,6 +132,8 @@ private: Gtk::EventBox _base; bool _no_redisplay; int _placement; + + sigc::connection screen_update_connection; }; std::shared_ptr _region; diff --git a/gtk2_ardour/region_fx_line.cc b/gtk2_ardour/region_fx_line.cc index 44945d930a..8b86d67fb6 100644 --- a/gtk2_ardour/region_fx_line.cc +++ b/gtk2_ardour/region_fx_line.cc @@ -30,6 +30,7 @@ RegionFxLine::RegionFxLine (std::string const& name, RegionView& r, ArdourCanvas : AutomationLine (name, r.get_time_axis_view(), parent, l, d) , _rv (r) { + terminal_points_can_slide = false; init (); } @@ -38,6 +39,7 @@ RegionFxLine::RegionFxLine (std::string const& name, RegionView& r, ArdourCanvas , _rv (r) , _ac (ac) { + terminal_points_can_slide = false; init (); } @@ -56,7 +58,7 @@ RegionFxLine::get_origin() const } void -RegionFxLine::enable_autoation () +RegionFxLine::enable_automation () { std::shared_ptr ac = _ac.lock (); if (ac) { @@ -67,14 +69,14 @@ RegionFxLine::enable_autoation () void RegionFxLine::end_drag (bool with_push, uint32_t final_index) { - enable_autoation (); + enable_automation (); AutomationLine::end_drag (with_push, final_index); } void RegionFxLine::end_draw_merge () { - enable_autoation (); + enable_automation (); AutomationLine::end_draw_merge (); } diff --git a/gtk2_ardour/region_fx_line.h b/gtk2_ardour/region_fx_line.h index 64ae1e9afd..84aec3f0d7 100644 --- a/gtk2_ardour/region_fx_line.h +++ b/gtk2_ardour/region_fx_line.h @@ -36,7 +36,7 @@ public: void end_drag (bool with_push, uint32_t final_index); void end_draw_merge (); - virtual void enable_autoation (); + virtual void enable_automation (); private: void init (); diff --git a/gtk2_ardour/region_gain_line.cc b/gtk2_ardour/region_gain_line.cc index bfabb2604c..3a0d18776b 100644 --- a/gtk2_ardour/region_gain_line.cc +++ b/gtk2_ardour/region_gain_line.cc @@ -45,8 +45,6 @@ AudioRegionGainLine::AudioRegionGainLine (const string & name, AudioRegionView& : RegionFxLine (name, r, parent, l, l->parameter ()) , arv (r) { - - terminal_points_can_slide = false; } void @@ -119,12 +117,12 @@ AudioRegionGainLine::end_drag (bool with_push, uint32_t final_index) void AudioRegionGainLine::end_draw_merge () { - enable_autoation (); + enable_automation (); RegionFxLine::end_draw_merge (); } void -AudioRegionGainLine::enable_autoation () +AudioRegionGainLine::enable_automation () { if (!arv.audio_region()->envelope_active()) { XMLNode& before = arv.audio_region()->get_state(); diff --git a/gtk2_ardour/region_gain_line.h b/gtk2_ardour/region_gain_line.h index 6f6d724040..34780ff3e5 100644 --- a/gtk2_ardour/region_gain_line.h +++ b/gtk2_ardour/region_gain_line.h @@ -46,7 +46,7 @@ public: void start_drag_multiple (std::list, float, XMLNode*); void end_drag (bool with_push, uint32_t final_index); void end_draw_merge (); - void enable_autoation (); + void enable_automation (); void remove_point (ControlPoint&); private: diff --git a/gtk2_ardour/route_time_axis.cc b/gtk2_ardour/route_time_axis.cc index a99af947a0..ea87688325 100644 --- a/gtk2_ardour/route_time_axis.cc +++ b/gtk2_ardour/route_time_axis.cc @@ -346,6 +346,7 @@ RouteTimeAxisView::set_route (std::shared_ptr rt) plist->add (ARDOUR::Properties::group_mute, true); plist->add (ARDOUR::Properties::group_solo, true); + delete route_group_menu; route_group_menu = new RouteGroupMenu (_session, plist); gm.get_level_meter().signal_scroll_event().connect (sigc::mem_fun (*this, &RouteTimeAxisView::controls_ebox_scroll), false); @@ -426,6 +427,7 @@ RouteTimeAxisView::route_group_click (GdkEventButton *ev) WeakRouteList r; r.push_back (route ()); + route_group_menu->detach (); route_group_menu->build (r); if (ev->button == 1) { Gtkmm2ext::anchored_menu_popup(route_group_menu->menu(), @@ -650,7 +652,7 @@ RouteTimeAxisView::build_display_menu () TimeAxisView::build_display_menu (); - /* now fill it with our stuff */ + bool active = _route->active (); MenuList& items = display_menu->items(); @@ -680,24 +682,27 @@ RouteTimeAxisView::build_display_menu () return; } - items.push_back (MenuElem (_("Color..."), sigc::mem_fun (*this, &RouteUI::choose_color))); + /* now fill it with our stuff */ + if (active) { + items.push_back (MenuElem (_("Color..."), sigc::mem_fun (*this, &RouteUI::choose_color))); - items.push_back (MenuElem (_("Comments..."), sigc::mem_fun (*this, &RouteUI::open_comment_editor))); + items.push_back (MenuElem (_("Comments..."), sigc::mem_fun (*this, &RouteUI::open_comment_editor))); - items.push_back (MenuElem (_("Inputs..."), sigc::mem_fun (*this, &RouteUI::edit_input_configuration))); + items.push_back (MenuElem (_("Inputs..."), sigc::mem_fun (*this, &RouteUI::edit_input_configuration))); - items.push_back (MenuElem (_("Outputs..."), sigc::mem_fun (*this, &RouteUI::edit_output_configuration))); + items.push_back (MenuElem (_("Outputs..."), sigc::mem_fun (*this, &RouteUI::edit_output_configuration))); - items.push_back (SeparatorElem()); + items.push_back (SeparatorElem()); - build_size_menu (); - items.push_back (MenuElem (_("Height"), *_size_menu)); - items.push_back (SeparatorElem()); + build_size_menu (); + items.push_back (MenuElem (_("Height"), *_size_menu)); + items.push_back (SeparatorElem()); - // Hook for derived classes to add type specific stuff - append_extra_display_menu_items (); + /* Hook for derived classes to add type specific stuff */ + append_extra_display_menu_items (); + } - if (is_track()) { + if (active && is_track()) { Menu* layers_menu = manage (new Menu); MenuList &layers_items = layers_menu->items(); @@ -841,32 +846,34 @@ RouteTimeAxisView::build_display_menu () items.push_back (SeparatorElem()); } - route_group_menu->detach (); - WeakRouteList r; - for (TrackSelection::iterator i = _editor.get_selection().tracks.begin(); i != _editor.get_selection().tracks.end(); ++i) { - RouteTimeAxisView* rtv = dynamic_cast (*i); - if (rtv) { - r.push_back (rtv->route ()); + if (active) { + WeakRouteList r; + for (TrackSelection::iterator i = _editor.get_selection().tracks.begin(); i != _editor.get_selection().tracks.end(); ++i) { + RouteTimeAxisView* rtv = dynamic_cast (*i); + if (rtv) { + r.push_back (rtv->route ()); + } } + + if (r.empty ()) { + r.push_back (route ()); + } + + if (!_route->is_singleton ()) { + route_group_menu->detach (); + route_group_menu->build (r); + items.push_back (MenuElem (_("Group"), *route_group_menu->menu ())); + } + + build_automation_action_menu (true); + items.push_back (MenuElem (_("Automation"), *automation_action_menu)); + items.push_back (SeparatorElem()); } - if (r.empty ()) { - r.push_back (route ()); - } - if (!_route->is_singleton ()) { - route_group_menu->build (r); - items.push_back (MenuElem (_("Group"), *route_group_menu->menu ())); - } - - build_automation_action_menu (true); - items.push_back (MenuElem (_("Automation"), *automation_action_menu)); - - items.push_back (SeparatorElem()); - - int active = 0; - int inactive = 0; + int n_active = 0; + int n_inactive = 0; bool always_active = false; TrackSelection const & s = _editor.get_selection().tracks; for (TrackSelection::const_iterator i = s.begin(); i != s.end(); ++i) { @@ -879,9 +886,9 @@ RouteTimeAxisView::build_display_menu () always_active |= r->route()->mixbus() != 0; #endif if (r->route()->active()) { - ++active; + ++n_active; } else { - ++inactive; + ++n_inactive; } } @@ -922,19 +929,19 @@ RouteTimeAxisView::build_display_menu () items.push_back (CheckMenuElem (_("Active"))); i = dynamic_cast (&items.back()); bool click_sets_active = true; - if (active > 0 && inactive == 0) { + if (n_active > 0 && n_inactive == 0) { i->set_active (true); click_sets_active = false; - } else if (active > 0 && inactive > 0) { + } else if (n_active > 0 && n_inactive > 0) { i->set_inconsistent (true); } i->set_sensitive(! _session->transport_rolling() && ! always_active); - i->signal_activate().connect (sigc::bind (sigc::mem_fun (*this, &RouteUI::set_route_active), click_sets_active, true)); + i->signal_activate().connect (sigc::bind (sigc::mem_fun (*this, &RouteUI::set_route_active), click_sets_active, !_editor.get_selection().tracks.empty ())); items.push_back (SeparatorElem()); - items.push_back (MenuElem (_("Hide"), sigc::bind (sigc::mem_fun(_editor, &PublicEditor::hide_track_in_display), this, true))); + items.push_back (MenuElem (_("Hide"), sigc::bind (sigc::mem_fun(_editor, &PublicEditor::hide_track_in_display), this, !_editor.get_selection().tracks.empty ()))); - if (_route && !_route->is_singleton ()) { + if (active && _route && !_route->is_singleton ()) { items.push_back (SeparatorElem()); items.push_back (MenuElem (_("Duplicate..."), boost::bind (&ARDOUR_UI::start_duplicate_routes, ARDOUR_UI::instance()))); diff --git a/gtk2_ardour/latency_gui.cc b/gtk2_ardour/timectl_gui.cc similarity index 59% rename from gtk2_ardour/latency_gui.cc rename to gtk2_ardour/timectl_gui.cc index e3c8d12dc2..f5f3ae6af1 100644 --- a/gtk2_ardour/latency_gui.cc +++ b/gtk2_ardour/timectl_gui.cc @@ -29,10 +29,12 @@ #include "pbd/unwind.h" #include "ardour/latent.h" +#include "ardour/rc_configuration.h" +#include "ardour/tailtime.h" #include "gtkmm2ext/utils.h" -#include "latency_gui.h" +#include "timectl_gui.h" #include "utils.h" #include "pbd/i18n.h" @@ -50,46 +52,66 @@ static const gchar *_unit_strings[] = { 0 }; -std::vector LatencyGUI::unit_strings; +std::vector TimeCtlGUI::unit_strings; std::string -LatencyBarController::get_label (double&) +TimeCtlBarController::get_label (double&) { return ARDOUR_UI_UTILS::samples_as_time_string ( - _latency_gui->adjustment.get_value(), _latency_gui->sample_rate, true); + _timectl_gui->adjustment.get_value(), _timectl_gui->sample_rate, true); } void -LatencyGUIControllable::set_value (double v, PBD::Controllable::GroupControlDisposition group_override) +TimeCtlGUIControllable::set_value (double v, PBD::Controllable::GroupControlDisposition group_override) { - _latency_gui->adjustment.set_value (v); + _timectl_gui->adjustment.set_value (v); } double -LatencyGUIControllable::get_value () const +TimeCtlGUIControllable::get_value () const { - return _latency_gui->adjustment.get_value (); + return _timectl_gui->adjustment.get_value (); } double -LatencyGUIControllable::lower() const +TimeCtlGUIControllable::lower() const { - return _latency_gui->adjustment.get_lower (); + return _timectl_gui->adjustment.get_lower (); } double -LatencyGUIControllable::upper() const +TimeCtlGUIControllable::upper() const { - return _latency_gui->adjustment.get_upper (); + return _timectl_gui->adjustment.get_upper (); } -LatencyGUI::LatencyGUI (Latent& l, samplepos_t sr, samplepos_t psz) - : _latent (l) +TimeCtlGUI::TimeCtlGUI (Latent& l, samplepos_t sr, samplepos_t psz) + : _latent (&l) + , _tailtime (0) , sample_rate (sr) , period_size (psz) , _ignore_change (false) , adjustment (0, 0.0, sample_rate, 1.0, sample_rate / 1000.0f) /* max 1 second, step by samples, page by msecs */ , bc (adjustment, this) , reset_button (_("Reset")) +{ + init (); +} + +TimeCtlGUI::TimeCtlGUI (TailTime& t, samplepos_t sr, samplepos_t psz) + : _latent (0) + , _tailtime (&t) + , sample_rate (sr) + , period_size (psz) + , _ignore_change (false) + , adjustment (0, 0.0, 20 * sample_rate, sample_rate / 1000.f, 1.0, sample_rate / 2.0f) /* max 20 second, step by msec, page by 0.5 sec */ + , bc (adjustment, this) + , reset_button (_("Reset")) +{ + init (); +} + +void +TimeCtlGUI::init () { Widget* w; @@ -116,17 +138,21 @@ LatencyGUI::LatencyGUI (Latent& l, samplepos_t sr, samplepos_t psz) hbox2.pack_start (plus_button); hbox2.pack_start (units_combo, true, true); - minus_button.signal_clicked().connect (sigc::bind (sigc::mem_fun (*this, &LatencyGUI::change_latency_from_button), -1)); - plus_button.signal_clicked().connect (sigc::bind (sigc::mem_fun (*this, &LatencyGUI::change_latency_from_button), 1)); - reset_button.signal_clicked().connect (sigc::mem_fun (*this, &LatencyGUI::reset)); + minus_button.signal_clicked().connect (sigc::bind (sigc::mem_fun (*this, &TimeCtlGUI::change_from_button), -1)); + plus_button.signal_clicked().connect (sigc::bind (sigc::mem_fun (*this, &TimeCtlGUI::change_from_button), 1)); + reset_button.signal_clicked().connect (sigc::mem_fun (*this, &TimeCtlGUI::reset)); /* Limit value to adjustment range (max = sample_rate). * Otherwise if the signal_latency() is larger than the adjustment's max, - * LatencyGUI::finish() would set the adjustment's max value as custom-latency. + * TimeCtlGUI::finish() would set the adjustment's max value as custom-latency. */ - adjustment.set_value (std::min (sample_rate, _latent.signal_latency ())); + if (_latent) { + adjustment.set_value (std::min (sample_rate, _latent->signal_latency ())); + } else if (_tailtime) { + adjustment.set_value (std::min (sample_rate, _tailtime->signal_tailtime ())); + } - adjustment.signal_value_changed().connect (sigc::mem_fun (*this, &LatencyGUI::finish)); + adjustment.signal_value_changed().connect (sigc::mem_fun (*this, &TimeCtlGUI::finish)); bc.set_size_request (-1, 25); bc.set_name (X_("ProcessorControlSlider")); @@ -137,32 +163,46 @@ LatencyGUI::LatencyGUI (Latent& l, samplepos_t sr, samplepos_t psz) } void -LatencyGUI::finish () +TimeCtlGUI::finish () { if (_ignore_change) { return; } samplepos_t new_value = (samplepos_t) adjustment.get_value(); - _latent.set_user_latency (new_value); + if (_latent) { + _latent->set_user_latency (new_value); + } else if (_tailtime) { + _tailtime->set_user_tailtime (new_value); + } } void -LatencyGUI::reset () +TimeCtlGUI::reset () { - _latent.unset_user_latency (); - PBD::Unwinder uw (_ignore_change, true); - adjustment.set_value (std::min (sample_rate, _latent.signal_latency ())); + if (_latent) { + _latent->unset_user_latency (); + PBD::Unwinder uw (_ignore_change, true); + adjustment.set_value (std::min (sample_rate, _latent->signal_latency ())); + } else if (_tailtime) { + _tailtime->unset_user_tailtime (); + PBD::Unwinder uw (_ignore_change, true); + adjustment.set_value (std::min (Config->get_max_tail_samples (), _tailtime->signal_tailtime ())); + } } void -LatencyGUI::refresh () +TimeCtlGUI::refresh () { PBD::Unwinder uw (_ignore_change, true); - adjustment.set_value (std::min (sample_rate, _latent.effective_latency ())); + if (_latent) { + adjustment.set_value (std::min (sample_rate, _latent->effective_latency ())); + } else if (_tailtime) { + adjustment.set_value (std::min (Config->get_max_tail_samples (),_tailtime->effective_tailtime ())); + } } void -LatencyGUI::change_latency_from_button (int dir) +TimeCtlGUI::change_from_button (int dir) { std::string unitstr = units_combo.get_active_text(); double shift = 0.0; diff --git a/gtk2_ardour/latency_gui.h b/gtk2_ardour/timectl_gui.h similarity index 69% rename from gtk2_ardour/latency_gui.h rename to gtk2_ardour/timectl_gui.h index 838e01d848..b20d253ac0 100644 --- a/gtk2_ardour/latency_gui.h +++ b/gtk2_ardour/timectl_gui.h @@ -2,7 +2,7 @@ * Copyright (C) 2007-2017 Paul Davis * Copyright (C) 2009 Carl Hetherington * Copyright (C) 2009 David Robillard - * Copyright (C) 2017-2019 Robin Gareus + * Copyright (C) 2017-2024 Robin Gareus * * 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 @@ -19,8 +19,8 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#ifndef __gtk2_ardour_latency_gui_h__ -#define __gtk2_ardour_latency_gui_h__ +#ifndef __gtk2_ardour_timectl_gui_h__ +#define __gtk2_ardour_timectl_gui_h__ #include #include @@ -38,16 +38,17 @@ namespace ARDOUR { class Latent; + class TailTime; } -class LatencyGUI; +class TimeCtlGUI; -class LatencyGUIControllable : public PBD::Controllable +class TimeCtlGUIControllable : public PBD::Controllable { public: - LatencyGUIControllable (LatencyGUI* g) + TimeCtlGUIControllable (TimeCtlGUI* g) : PBD::Controllable ("ignoreMe") - , _latency_gui (g) + , _timectl_gui (g) {} void set_value (double v, PBD::Controllable::GroupControlDisposition group_override); @@ -62,44 +63,48 @@ public: } private: - LatencyGUI* _latency_gui; + TimeCtlGUI* _timectl_gui; }; -class LatencyBarController : public ArdourWidgets::BarController +class TimeCtlBarController : public ArdourWidgets::BarController { public: - LatencyBarController (Gtk::Adjustment& adj, LatencyGUI* g) - : BarController (adj, std::shared_ptr (new LatencyGUIControllable (g))) - , _latency_gui (g) + TimeCtlBarController (Gtk::Adjustment& adj, TimeCtlGUI* g) + : BarController (adj, std::shared_ptr (new TimeCtlGUIControllable (g))) + , _timectl_gui (g) { set_digits (0); } private: - LatencyGUI* _latency_gui; + TimeCtlGUI* _timectl_gui; std::string get_label (double&); }; -class LatencyGUI : public Gtk::VBox +class TimeCtlGUI : public Gtk::VBox { public: - LatencyGUI (ARDOUR::Latent&, samplepos_t sample_rate, samplepos_t period_size); - ~LatencyGUI() { } + TimeCtlGUI (ARDOUR::Latent&, samplepos_t sample_rate, samplepos_t period_size); + TimeCtlGUI (ARDOUR::TailTime&, samplepos_t sample_rate, samplepos_t period_size); + ~TimeCtlGUI() { } void refresh (); private: + void init (); void reset (); void finish (); - ARDOUR::Latent& _latent; + ARDOUR::Latent* _latent; + ARDOUR::TailTime* _tailtime; + samplepos_t sample_rate; samplepos_t period_size; bool _ignore_change; Gtk::Adjustment adjustment; - LatencyBarController bc; + TimeCtlBarController bc; Gtk::HBox hbox1; Gtk::HBox hbox2; Gtk::HButtonBox hbbox; @@ -108,12 +113,12 @@ private: Gtk::Button reset_button; Gtk::ComboBoxText units_combo; - void change_latency_from_button (int dir); + void change_from_button (int dir); - friend class LatencyBarController; - friend class LatencyGUIControllable; + friend class TimeCtlBarController; + friend class TimeCtlGUIControllable; static std::vector unit_strings; }; -#endif /* __gtk2_ardour_latency_gui_h__ */ +#endif /* __gtk2_ardour_timectl_gui_h__ */ diff --git a/gtk2_ardour/trigger_strip.cc b/gtk2_ardour/trigger_strip.cc index 6fe8a1eca9..67e5f91d57 100644 --- a/gtk2_ardour/trigger_strip.cc +++ b/gtk2_ardour/trigger_strip.cc @@ -258,8 +258,6 @@ TriggerStrip::build_route_ops_menu () /* do not allow rename if the track is record-enabled */ items.back().set_sensitive (!is_track() || !track()->rec_enable_control()->get_value()); } - - items.push_back (SeparatorElem()); } if ((!_route->is_singleton () || !active) @@ -268,11 +266,18 @@ TriggerStrip::build_route_ops_menu () #endif ) { + if (active) { + items.push_back (SeparatorElem()); + } items.push_back (CheckMenuElem (_("Active"))); Gtk::CheckMenuItem* i = dynamic_cast (&items.back()); i->set_active (active); i->set_sensitive (!_session->transport_rolling()); i->signal_activate().connect (sigc::bind (sigc::mem_fun (*this, &RouteUI::set_route_active), !_route->active(), false)); + } + + /* Plugin / Processor related */ + if (active) { items.push_back (SeparatorElem()); } @@ -286,12 +291,20 @@ TriggerStrip::build_route_ops_menu () uint32_t plugin_insert_cnt = 0; _route->foreach_processor (boost::bind (RouteUI::help_count_plugins, _1, & plugin_insert_cnt)); - if (active && plugin_insert_cnt > 0) { items.push_back (MenuElem (_("Pin Connections..."), sigc::mem_fun (*this, &RouteUI::manage_pins))); } + if (active) { + items.push_back (CheckMenuElem (_("Protect Against Denormals"), sigc::mem_fun (*this, &RouteUI::toggle_denormal_protection))); + denormal_menu_item = dynamic_cast (&items.back()); + denormal_menu_item->set_active (_route->denormal_protection()); + } + + /* MIDI */ + if (active && (std::dynamic_pointer_cast(_route) || _route->the_instrument ())) { + items.push_back (SeparatorElem()); items.push_back (MenuElem (_("Patch Selector..."), sigc::mem_fun(*this, &RouteUI::select_midi_patch))); } @@ -300,13 +313,8 @@ TriggerStrip::build_route_ops_menu () // TODO ..->n_audio() > 1 && separate_output_groups) hard to check here every time. items.push_back (MenuElem (_("Fan out to Busses"), sigc::bind (sigc::mem_fun (*this, &RouteUI::fan_out), true, true))); items.push_back (MenuElem (_("Fan out to Tracks"), sigc::bind (sigc::mem_fun (*this, &RouteUI::fan_out), false, true))); - items.push_back (SeparatorElem()); } - items.push_back (CheckMenuElem (_("Protect Against Denormals"), sigc::mem_fun (*this, &RouteUI::toggle_denormal_protection))); - denormal_menu_item = dynamic_cast (&items.back()); - denormal_menu_item->set_active (_route->denormal_protection()); - /* note that this relies on selection being shared across editor and * mixer (or global to the backend, in the future), which is the only * sane thing for users anyway. diff --git a/gtk2_ardour/wscript b/gtk2_ardour/wscript index fcff17ef88..9d8af89fe5 100644 --- a/gtk2_ardour/wscript +++ b/gtk2_ardour/wscript @@ -139,7 +139,6 @@ gtk2_ardour_sources = [ 'hit.cc', 'keyboard.cc', 'keyeditor.cc', - 'latency_gui.cc', 'led.cc', 'level_meter.cc', 'library_download_dialog.cc', @@ -304,6 +303,7 @@ gtk2_ardour_sources = [ 'time_fx_dialog.cc', 'time_info_box.cc', 'time_selection.cc', + 'timectl_gui.cc', 'timers.cc', 'track_record_axis.cc', 'track_selection.cc', diff --git a/libs/ardour/ardour/audio_unit.h b/libs/ardour/ardour/audio_unit.h index 757a771282..6ce63b00c9 100644 --- a/libs/ardour/ardour/audio_unit.h +++ b/libs/ardour/ardour/audio_unit.h @@ -81,6 +81,7 @@ class LIBARDOUR_API AUPlugin : public ARDOUR::Plugin void deactivate (); void flush (); int set_block_size (pframes_t nframes); + void set_non_realtime (bool); int connect_and_run (BufferSet& bufs, samplepos_t start, samplepos_t end, double speed, @@ -166,6 +167,7 @@ class LIBARDOUR_API AUPlugin : public ARDOUR::Plugin std::shared_ptr unit; bool initialized; + bool process_offline; int32_t input_channels; int32_t output_channels; std::vector > io_configs; diff --git a/libs/ardour/ardour/audioanalyser.h b/libs/ardour/ardour/audioanalyser.h index 8755082d75..283910ea61 100644 --- a/libs/ardour/ardour/audioanalyser.h +++ b/libs/ardour/ardour/audioanalyser.h @@ -23,7 +23,7 @@ #include #include -#include +#include #include #include "ardour/libardour_visibility.h" #include "ardour/types.h" diff --git a/libs/ardour/ardour/audioregion.h b/libs/ardour/ardour/audioregion.h index 92a7b8b284..07a6e99dec 100644 --- a/libs/ardour/ardour/audioregion.h +++ b/libs/ardour/ardour/audioregion.h @@ -50,6 +50,7 @@ namespace Properties { LIBARDOUR_API extern PBD::PropertyDescriptor default_fade_out; LIBARDOUR_API extern PBD::PropertyDescriptor fade_in_active; LIBARDOUR_API extern PBD::PropertyDescriptor fade_out_active; + LIBARDOUR_API extern PBD::PropertyDescriptor fade_before_fx; LIBARDOUR_API extern PBD::PropertyDescriptor scale_amplitude; LIBARDOUR_API extern PBD::PropertyDescriptor > fade_in; LIBARDOUR_API extern PBD::PropertyDescriptor > inverse_fade_in; @@ -100,6 +101,7 @@ class LIBARDOUR_API AudioRegion : public Region, public AudioReadable bool envelope_active () const { return _envelope_active; } bool fade_in_active () const { return _fade_in_active; } bool fade_out_active () const { return _fade_out_active; } + bool fade_before_fx () const { return _fade_before_fx; } std::shared_ptr fade_in() { return _fade_in.val (); } std::shared_ptr inverse_fade_in() { return _inverse_fade_in.val (); } @@ -162,11 +164,15 @@ class LIBARDOUR_API AudioRegion : public Region, public AudioReadable void set_envelope_active (bool yn); void set_default_envelope (); + void set_fade_before_fx (bool yn); + int separate_by_channel (std::vector >&) const; bool remove_plugin (std::shared_ptr); void reorder_plugins (RegionFxList const&); + timecnt_t tail () const; + /* automation */ std::shared_ptr @@ -220,6 +226,7 @@ class LIBARDOUR_API AudioRegion : public Region, public AudioReadable PBD::Property _default_fade_out; PBD::Property _fade_in_active; PBD::Property _fade_out_active; + PBD::Property _fade_before_fx; /** linear gain to apply to the whole region */ PBD::Property _scale_amplitude; @@ -259,6 +266,7 @@ class LIBARDOUR_API AudioRegion : public Region, public AudioReadable void apply_region_fx (BufferSet&, samplepos_t, samplepos_t, samplecnt_t); void fx_latency_changed (bool no_emit); + void fx_tail_changed (bool no_emit); void copy_plugin_state (std::shared_ptr); mutable samplepos_t _fx_pos; @@ -269,6 +277,7 @@ class LIBARDOUR_API AudioRegion : public Region, public AudioReadable mutable BufferSet _readcache; mutable samplepos_t _cache_start; mutable samplepos_t _cache_end; + mutable samplecnt_t _cache_tail; mutable std::atomic _invalidated; protected: diff --git a/libs/ardour/ardour/buffer.h b/libs/ardour/ardour/buffer.h index e262ecf821..14a406d736 100644 --- a/libs/ardour/ardour/buffer.h +++ b/libs/ardour/ardour/buffer.h @@ -24,7 +24,7 @@ #include -#include +#include #include "ardour/libardour_visibility.h" #include "ardour/types.h" diff --git a/libs/ardour/ardour/clip_library.h b/libs/ardour/ardour/clip_library.h index 1ad9937f9f..4dfa031784 100644 --- a/libs/ardour/ardour/clip_library.h +++ b/libs/ardour/ardour/clip_library.h @@ -25,8 +25,6 @@ #include -#include "boost/shared_ptr.hpp" - #include "pbd/signals.h" #include "ardour/libardour_visibility.h" diff --git a/libs/ardour/ardour/debug.h b/libs/ardour/ardour/debug.h index 18aaa74ae7..3e6f2661fa 100644 --- a/libs/ardour/ardour/debug.h +++ b/libs/ardour/ardour/debug.h @@ -34,6 +34,7 @@ namespace PBD { namespace DEBUG { LIBARDOUR_API extern DebugBits AudioEngine; LIBARDOUR_API extern DebugBits AudioPlayback; + LIBARDOUR_API extern DebugBits AudioCacheRefill; LIBARDOUR_API extern DebugBits AudioUnitConfig; LIBARDOUR_API extern DebugBits AudioUnitGUI; LIBARDOUR_API extern DebugBits AudioUnitProcess; @@ -68,6 +69,7 @@ namespace PBD { LIBARDOUR_API extern DebugBits LatencyRoute; LIBARDOUR_API extern DebugBits LaunchControlXL; LIBARDOUR_API extern DebugBits Launchpad; + LIBARDOUR_API extern DebugBits Launchkey; LIBARDOUR_API extern DebugBits Layering; LIBARDOUR_API extern DebugBits MIDISurface; LIBARDOUR_API extern DebugBits MTC; diff --git a/libs/ardour/ardour/disk_writer.h b/libs/ardour/ardour/disk_writer.h index 669c064ed7..bef6d77966 100644 --- a/libs/ardour/ardour/disk_writer.h +++ b/libs/ardour/ardour/disk_writer.h @@ -26,6 +26,7 @@ #include #include "ardour/disk_io.h" +#include "ardour/event_ring_buffer.h" #include "ardour/midi_buffer.h" namespace ARDOUR @@ -201,8 +202,8 @@ private: /** A buffer that we use to put newly-arrived MIDI data in for * the GUI to read (so that it can update itself). */ - MidiBuffer _gui_feed_buffer; - mutable Glib::Threads::Mutex _gui_feed_buffer_mutex; + mutable EventRingBuffer _gui_feed_fifo; + mutable Glib::Threads::Mutex _gui_feed_reset_mutex; }; } // namespace diff --git a/libs/ardour/ardour/export_channel_configuration.h b/libs/ardour/ardour/export_channel_configuration.h index 64b906a680..80d89c439d 100644 --- a/libs/ardour/ardour/export_channel_configuration.h +++ b/libs/ardour/ardour/export_channel_configuration.h @@ -26,8 +26,6 @@ #include #include -#include - #include "ardour/export_channel.h" #include "ardour/export_pointers.h" diff --git a/libs/ardour/ardour/export_smf_writer.h b/libs/ardour/ardour/export_smf_writer.h index 5c46f72bbd..9d86d1b832 100644 --- a/libs/ardour/ardour/export_smf_writer.h +++ b/libs/ardour/ardour/export_smf_writer.h @@ -19,8 +19,6 @@ #ifndef _libardour_export_smf_writer_h_ #define _libardour_export_smf_writer_h_ -#include - #include "evoral/SMF.h" #include "ardour/libardour_visibility.h" diff --git a/libs/ardour/ardour/graph_edges.h b/libs/ardour/ardour/graph_edges.h index 433e67fe79..98faf2ace0 100644 --- a/libs/ardour/ardour/graph_edges.h +++ b/libs/ardour/ardour/graph_edges.h @@ -22,8 +22,6 @@ #include #include -#include - #include "ardour/libardour_visibility.h" #include "ardour/types.h" diff --git a/libs/ardour/ardour/io_plug.h b/libs/ardour/ardour/io_plug.h index 40458577ff..6421fee466 100644 --- a/libs/ardour/ardour/io_plug.h +++ b/libs/ardour/ardour/io_plug.h @@ -95,6 +95,9 @@ public: virtual bool get_stats (PBD::microseconds_t&, PBD::microseconds_t&, double&, double&) const; virtual void clear_stats (); + ChanMapping input_map (uint32_t num) const; + ChanMapping output_map (uint32_t num) const; + /* ControlSet */ std::shared_ptr control_factory (const Evoral::Parameter& id); diff --git a/libs/ardour/ardour/lua_api.h b/libs/ardour/ardour/lua_api.h index 835937e7df..527879bedb 100644 --- a/libs/ardour/ardour/lua_api.h +++ b/libs/ardour/ardour/lua_api.h @@ -23,7 +23,6 @@ #include #include -#include #include #include diff --git a/libs/ardour/ardour/lv2_plugin.h b/libs/ardour/ardour/lv2_plugin.h index 0c006787dd..8f06b62b4c 100644 --- a/libs/ardour/ardour/lv2_plugin.h +++ b/libs/ardour/ardour/lv2_plugin.h @@ -28,7 +28,6 @@ #include #include #include -#include #include "temporal/tempo.h" diff --git a/libs/ardour/ardour/midi_cursor.h b/libs/ardour/ardour/midi_cursor.h index f0894663f1..1fdd60b729 100644 --- a/libs/ardour/ardour/midi_cursor.h +++ b/libs/ardour/ardour/midi_cursor.h @@ -21,8 +21,6 @@ #include -#include - #include "pbd/signals.h" diff --git a/libs/ardour/ardour/midi_model.h b/libs/ardour/ardour/midi_model.h index 3e7680fc98..dd7cfa1f1d 100644 --- a/libs/ardour/ardour/midi_model.h +++ b/libs/ardour/ardour/midi_model.h @@ -29,7 +29,6 @@ #include #include -#include #include #include "pbd/command.h" diff --git a/libs/ardour/ardour/midi_playlist.h b/libs/ardour/ardour/midi_playlist.h index e1d50616fe..4277cb5049 100644 --- a/libs/ardour/ardour/midi_playlist.h +++ b/libs/ardour/ardour/midi_playlist.h @@ -26,8 +26,6 @@ #include #include -#include - #include "evoral/Parameter.h" #include "ardour/ardour.h" diff --git a/libs/ardour/ardour/mixer_scene.h b/libs/ardour/ardour/mixer_scene.h index 8a6d866dbc..d152e0af6d 100644 --- a/libs/ardour/ardour/mixer_scene.h +++ b/libs/ardour/ardour/mixer_scene.h @@ -19,8 +19,6 @@ #ifndef _libardour_mixer_scene_h_ #define _libardour_mixer_scene_h_ -#include - #include "pbd/stateful.h" #include "ardour/libardour_visibility.h" diff --git a/libs/ardour/ardour/monitor_control.h b/libs/ardour/ardour/monitor_control.h index e6a7a1d42d..151bcc2225 100644 --- a/libs/ardour/ardour/monitor_control.h +++ b/libs/ardour/ardour/monitor_control.h @@ -22,8 +22,6 @@ #include #include -#include - #include "ardour/slavable_automation_control.h" #include "ardour/monitorable.h" diff --git a/libs/ardour/ardour/playlist.h b/libs/ardour/ardour/playlist.h index 8405fd197c..882c886845 100644 --- a/libs/ardour/ardour/playlist.h +++ b/libs/ardour/ardour/playlist.h @@ -37,7 +37,6 @@ #include #include -#include #include @@ -402,7 +401,7 @@ protected: void _set_sort_id (); - std::shared_ptr regions_touched_locked (timepos_t const & start, timepos_t const & end); + std::shared_ptr regions_touched_locked (timepos_t const & start, timepos_t const & end, bool with_tail); void notify_region_removed (std::shared_ptr); void notify_region_added (std::shared_ptr); diff --git a/libs/ardour/ardour/plug_insert_base.h b/libs/ardour/ardour/plug_insert_base.h index 63f486701b..fcc4ff254e 100644 --- a/libs/ardour/ardour/plug_insert_base.h +++ b/libs/ardour/ardour/plug_insert_base.h @@ -25,6 +25,7 @@ #include "ardour/ardour.h" #include "ardour/automation_control.h" +#include "ardour/chan_mapping.h" #include "ardour/plugin.h" #include "ardour/plugin_types.h" @@ -68,18 +69,22 @@ public: virtual bool get_stats (PBD::microseconds_t&, PBD::microseconds_t&, double&, double&) const = 0; virtual void clear_stats () = 0; + virtual ChanMapping input_map (uint32_t num) const = 0; + virtual ChanMapping output_map (uint32_t num) const = 0; + /** A control that manipulates a plugin parameter (control port). */ - struct PluginControl : public AutomationControl { + class PluginControl : public AutomationControl { + public: PluginControl (Session& s, PlugInsertBase* p, const Evoral::Parameter& param, const ParameterDescriptor& desc, std::shared_ptr list = std::shared_ptr ()); - double get_value (void) const; - void catch_up_with_external_value (double val); - XMLNode& get_state () const; - std::string get_user_string () const; + virtual double get_value (void) const; + void catch_up_with_external_value (double val); + XMLNode& get_state () const; + std::string get_user_string () const; protected: virtual void actually_set_value (double val, PBD::Controllable::GroupControlDisposition group_override); @@ -94,8 +99,8 @@ public: const ParameterDescriptor& desc, std::shared_ptr list = std::shared_ptr ()); - double get_value (void) const; - XMLNode& get_state () const; + virtual double get_value (void) const; + XMLNode& get_state () const; protected: virtual void actually_set_value (double value, PBD::Controllable::GroupControlDisposition); diff --git a/libs/ardour/ardour/plugin.h b/libs/ardour/ardour/plugin.h index 1addcf645d..01ad34f8a7 100644 --- a/libs/ardour/ardour/plugin.h +++ b/libs/ardour/ardour/plugin.h @@ -40,6 +40,7 @@ #include "ardour/midi_ring_buffer.h" #include "ardour/midi_state_tracker.h" #include "ardour/parameter_descriptor.h" +#include "ardour/tailtime.h" #include "ardour/types.h" #include "ardour/variant.h" @@ -74,7 +75,7 @@ typedef std::set PluginOutputConfiguration; * * Plugins are not used directly in Ardour but always wrapped by a PluginInsert. */ -class LIBARDOUR_API Plugin : public PBD::StatefulDestructible, public HasLatency +class LIBARDOUR_API Plugin : public PBD::StatefulDestructible, public HasLatency, public HasTailTime { public: Plugin (ARDOUR::AudioEngine&, ARDOUR::Session&); @@ -87,12 +88,12 @@ public: virtual void set_insert_id (PBD::ID id) {} virtual void set_state_dir (const std::string& d = "") {} - void set_insert (PluginInsert* pi, uint32_t num) { - _pi = pi; + void set_insert (PlugInsertBase* pib, uint32_t num) { + _pib = pib; _num = num; } - PluginInsert* plugin_insert () const { return _pi; } + PlugInsertBase* plugin_insert () const { return _pib; } uint32_t plugin_number () const { return _num; } virtual std::string unique_id () const = 0; @@ -172,6 +173,11 @@ public: return plugin_latency (); } + samplecnt_t signal_tailtime () const + { + return plugin_tailtime (); + } + /** the max possible latency a plugin will have */ virtual samplecnt_t max_latency () const { return 0; } @@ -423,6 +429,11 @@ protected: private: virtual samplecnt_t plugin_latency () const = 0; + /** tail duration in samples. e.g. for reverb or delay plugins. + * + * The default when unknown is 2 sec */ + virtual samplecnt_t plugin_tailtime () const; + /** Fill _presets with our presets */ virtual void find_presets () = 0; @@ -443,8 +454,8 @@ private: void invalidate_preset_cache (std::string const&, Plugin*, bool); void resolve_midi (); - PluginInsert* _pi; - uint32_t _num; + PlugInsertBase* _pib; + uint32_t _num; PBD::ScopedConnection _preset_connection; }; diff --git a/libs/ardour/ardour/plugin_insert.h b/libs/ardour/ardour/plugin_insert.h index 4e893e6f57..5ad4192080 100644 --- a/libs/ardour/ardour/plugin_insert.h +++ b/libs/ardour/ardour/plugin_insert.h @@ -34,7 +34,6 @@ #include "ardour/ardour.h" #include "ardour/libardour_visibility.h" -#include "ardour/chan_mapping.h" #include "ardour/fixed_delay.h" #include "ardour/io.h" #include "ardour/types.h" @@ -323,9 +322,7 @@ private: /** details of the match currently being used */ Match _match; - /* ordered map [plugin instance ID] => ARDOUR::ChanMapping - * TODO: consider replacing with boost::flat_map<> or std::vector<>. - */ + /* ordered map [plugin instance ID] => ARDOUR::ChanMapping */ #if defined(_MSC_VER) /* && (_MSC_VER < 1900) * Regarding the note (below) it was initially * thought that this got fixed in VS2015 - but diff --git a/libs/ardour/ardour/plugin_manager.h b/libs/ardour/ardour/plugin_manager.h index 393b07e38b..4c93a7e7f6 100644 --- a/libs/ardour/ardour/plugin_manager.h +++ b/libs/ardour/ardour/plugin_manager.h @@ -32,7 +32,6 @@ #include #include #include -#include #include #include "ardour/libardour_visibility.h" diff --git a/libs/ardour/ardour/port.h b/libs/ardour/ardour/port.h index b2fdc27c62..3037b8f9da 100644 --- a/libs/ardour/ardour/port.h +++ b/libs/ardour/ardour/port.h @@ -29,7 +29,7 @@ #include #include #include -#include +#include #include "pbd/signals.h" #include "ardour/data_type.h" diff --git a/libs/ardour/ardour/port_set.h b/libs/ardour/ardour/port_set.h index 413160825b..98df046b0f 100644 --- a/libs/ardour/ardour/port_set.h +++ b/libs/ardour/ardour/port_set.h @@ -23,8 +23,6 @@ #include #include "ardour/chan_count.h" -#include - namespace ARDOUR { class Port; diff --git a/libs/ardour/ardour/rc_configuration_vars.h b/libs/ardour/ardour/rc_configuration_vars.h index db7469150b..850961787c 100644 --- a/libs/ardour/ardour/rc_configuration_vars.h +++ b/libs/ardour/ardour/rc_configuration_vars.h @@ -260,6 +260,9 @@ CONFIG_VARIABLE (uint32_t, plugin_scan_timeout, "plugin-scan-timeout", 150) /* d CONFIG_VARIABLE (uint32_t, limit_n_automatables, "limit-n-automatables", 512) CONFIG_VARIABLE (uint32_t, plugin_cache_version, "plugin-cache-version", 0) +CONFIG_VARIABLE (float, tail_duration_sec, "tail-duration-sec", 2.0) +CONFIG_VARIABLE (uint32_t, max_tail_samples, "max-tail-samples", 0xffffffff) // aka kInfiniteTail + /* custom user plugin paths */ CONFIG_VARIABLE (std::string, plugin_path_vst, "plugin-path-vst", "@default@") CONFIG_VARIABLE (std::string, plugin_path_lxvst, "plugin-path-lxvst", "@default@") diff --git a/libs/ardour/ardour/readonly_control.h b/libs/ardour/ardour/readonly_control.h index 0e130938af..3a0dd6ebf1 100644 --- a/libs/ardour/ardour/readonly_control.h +++ b/libs/ardour/ardour/readonly_control.h @@ -35,11 +35,11 @@ class LIBARDOUR_API ReadOnlyControl : public PBD::Destructible public: ReadOnlyControl (std::shared_ptr, const ParameterDescriptor&, uint32_t pnum); - double get_parameter () const; + virtual double get_parameter () const; std::string describe_parameter (); const ParameterDescriptor& desc() const { return _desc; } -private: +protected: std::weak_ptr _plugin; const ParameterDescriptor _desc; uint32_t _parameter_num; diff --git a/libs/ardour/ardour/record_enable_control.h b/libs/ardour/ardour/record_enable_control.h index 8a79667610..3e64cdbc4e 100644 --- a/libs/ardour/ardour/record_enable_control.h +++ b/libs/ardour/ardour/record_enable_control.h @@ -22,8 +22,6 @@ #include #include -#include - #include "ardour/slavable_automation_control.h" #include "ardour/recordable.h" diff --git a/libs/ardour/ardour/region.h b/libs/ardour/ardour/region.h index a66dc036f0..9313d84a2d 100644 --- a/libs/ardour/ardour/region.h +++ b/libs/ardour/ardour/region.h @@ -27,8 +27,6 @@ #include #include -#include - #include "temporal/domain_swap.h" #include "temporal/timeline.h" #include "temporal/range.h" @@ -145,6 +143,8 @@ public: timepos_t end() const; timepos_t nt_last() const { return end().decrement(); } + virtual timecnt_t tail () const { return timecnt_t (0); } + timepos_t source_position () const; timecnt_t source_relative_position (Temporal::timepos_t const &) const; timecnt_t region_relative_position (Temporal::timepos_t const &) const; @@ -301,8 +301,8 @@ public: * OverlapEnd: the range overlaps the end of this region. * OverlapExternal: the range overlaps all of this region. */ - Temporal::OverlapType coverage (timepos_t const & start, timepos_t const & end) const { - return Temporal::coverage_exclusive_ends (position(), nt_last(), start, end); + Temporal::OverlapType coverage (timepos_t const & start, timepos_t const & end, bool with_tail = false) const { + return Temporal::coverage_exclusive_ends (position(), with_tail ? nt_last() + tail() : nt_last(), start, end); } bool exact_equivalent (std::shared_ptr) const; @@ -564,6 +564,7 @@ protected: protected: virtual bool _add_plugin (std::shared_ptr, std::shared_ptr, bool) { return false; } virtual void fx_latency_changed (bool no_emit); + virtual void fx_tail_changed (bool no_emit); virtual void send_change (const PBD::PropertyChange&); virtual int _set_state (const XMLNode&, int version, PBD::PropertyChange& what_changed, bool send_signal); @@ -584,6 +585,7 @@ protected: mutable Glib::Threads::RWLock _fx_lock; uint32_t _fx_latency; + uint32_t _fx_tail; RegionFxList _plugins; PBD::Property _sync_marked; diff --git a/libs/ardour/ardour/region_fx_plugin.h b/libs/ardour/ardour/region_fx_plugin.h index c52cd755ce..74e8c0fd50 100644 --- a/libs/ardour/ardour/region_fx_plugin.h +++ b/libs/ardour/ardour/region_fx_plugin.h @@ -43,7 +43,7 @@ namespace ARDOUR { class ReadOnlyControl; -class LIBARDOUR_API RegionFxPlugin : public SessionObject, public PlugInsertBase, public Latent, public Temporal::TimeDomainProvider +class LIBARDOUR_API RegionFxPlugin : public SessionObject, public PlugInsertBase, public Latent, public TailTime, public Temporal::TimeDomainProvider { public: RegionFxPlugin (Session&, Temporal::TimeDomain const, std::shared_ptr = std::shared_ptr ()); @@ -61,22 +61,22 @@ public: /* Latent */ samplecnt_t signal_latency () const; + /* TailTime */ + samplecnt_t signal_tailtime () const; /* PlugInsertBase */ uint32_t get_count () const { return _plugins.size (); } - PluginType type () const - { - return plugin ()->get_info ()->type; - } + PluginType type () const; + std::shared_ptr plugin (uint32_t num = 0) const { if (num < _plugins.size ()) { return _plugins[num]; } else { - return _plugins[0]; + return std::shared_ptr(); } } @@ -91,6 +91,8 @@ public: bool reset_parameters_to_default (); bool can_reset_all_parameters (); + void maybe_emit_changed_signals () const; + std::string describe_parameter (Evoral::Parameter param); bool provides_stats () const @@ -103,6 +105,22 @@ public: } void clear_stats () {} + ChanMapping input_map (uint32_t num) const { + if (num < _in_map.size()) { + return _in_map.find (num)->second; + } else { + return ChanMapping (); + } + } + + ChanMapping output_map (uint32_t num) const { + if (num < _out_map.size()) { + return _out_map.find (num)->second; + } else { + return ChanMapping (); + } + } + /* Stateful */ XMLNode& get_state (void) const; int set_state (const XMLNode&, int version); @@ -158,7 +176,8 @@ private: /** details of the match currently being used */ Match _match; - uint32_t _plugin_signal_latency; + samplecnt_t _plugin_signal_latency; + samplecnt_t _plugin_signal_tailtime; typedef std::vector> Plugins; Plugins _plugins; @@ -173,11 +192,17 @@ private: bool _configured; bool _no_inplace; + mutable samplepos_t _last_emit; + typedef std::map> CtrlOutMap; CtrlOutMap _control_outputs; Gtkmm2ext::WindowProxy* _window_proxy; std::atomic _flush; + + XMLNode* _state; + + mutable Glib::Threads::Mutex _process_lock; }; } // namespace ARDOUR diff --git a/libs/ardour/ardour/session.h b/libs/ardour/ardour/session.h index d4bd0ac566..188673da09 100644 --- a/libs/ardour/ardour/session.h +++ b/libs/ardour/ardour/session.h @@ -49,8 +49,6 @@ #include #include -#include - #include #include diff --git a/libs/ardour/ardour/source.h b/libs/ardour/ardour/source.h index b4a430cced..a74a90a4c2 100644 --- a/libs/ardour/ardour/source.h +++ b/libs/ardour/ardour/source.h @@ -30,8 +30,6 @@ #include -#include - #include "pbd/statefuldestructible.h" #include "ardour/ardour.h" diff --git a/libs/ardour/ardour/step_sequencer.h b/libs/ardour/ardour/step_sequencer.h index 37e9dba275..33d34885b3 100644 --- a/libs/ardour/ardour/step_sequencer.h +++ b/libs/ardour/ardour/step_sequencer.h @@ -23,7 +23,6 @@ #include #include -#include #include #include diff --git a/libs/ardour/ardour/stripable.h b/libs/ardour/ardour/stripable.h index d7d76821f9..c21e9cc7f2 100644 --- a/libs/ardour/ardour/stripable.h +++ b/libs/ardour/ardour/stripable.h @@ -26,8 +26,6 @@ #include #include -#include - #include "pbd/signals.h" #include "ardour/automatable.h" diff --git a/libs/ardour/ardour/tailtime.h b/libs/ardour/ardour/tailtime.h new file mode 100644 index 0000000000..f0e6c038cf --- /dev/null +++ b/libs/ardour/ardour/tailtime.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 Robin Gareus + * + * 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. + */ + +#ifndef __ardour_tailtime_h__ +#define __ardour_tailtime_h__ + +#include "pbd/signals.h" + +#include "ardour/libardour_visibility.h" +#include "ardour/types.h" + +namespace ARDOUR { + +class LIBARDOUR_API HasTailTime { +public: + virtual ~HasTailTime () {} + virtual samplecnt_t signal_tailtime () const = 0; +}; + +class LIBARDOUR_API TailTime : public HasTailTime { +public: + TailTime (); + TailTime (TailTime const&); + virtual ~TailTime() {} + + samplecnt_t effective_tailtime () const; + + samplecnt_t user_latency () const { + if (_use_user_tailtime) { + return _user_tailtime; + } else { + return 0; + } + } + + void unset_user_tailtime (); + void set_user_tailtime (samplecnt_t val); + + PBD::Signal0 TailTimeChanged; + +protected: + int set_state (const XMLNode& node, int version); + void add_state (XMLNode*) const; + +private: + samplecnt_t _use_user_tailtime; + samplecnt_t _user_tailtime; +}; + +} /* namespace */ + + +#endif /* __ardour_tailtime_h__*/ + diff --git a/libs/ardour/ardour/uri_map.h b/libs/ardour/ardour/uri_map.h index d109aa6867..8b80627daf 100644 --- a/libs/ardour/ardour/uri_map.h +++ b/libs/ardour/ardour/uri_map.h @@ -23,8 +23,6 @@ #include -#include - #include #ifdef HAVE_LV2_1_18_6 diff --git a/libs/ardour/ardour/utils.h b/libs/ardour/ardour/utils.h index 32c27fc26a..337df374d2 100644 --- a/libs/ardour/ardour/utils.h +++ b/libs/ardour/ardour/utils.h @@ -32,8 +32,6 @@ #include #include -#include "boost/shared_ptr.hpp" - #if __APPLE__ #include #endif /* __APPLE__ */ diff --git a/libs/ardour/ardour/vst3_plugin.h b/libs/ardour/ardour/vst3_plugin.h index fa8dfb6cbc..3ac40037b6 100644 --- a/libs/ardour/ardour/vst3_plugin.h +++ b/libs/ardour/ardour/vst3_plugin.h @@ -167,6 +167,8 @@ public: Vst::ParamID index_to_id (uint32_t) const; Glib::Threads::Mutex& process_lock () { return _process_lock; } + bool& component_is_synced () { return _restart_component_is_synced; } + enum ParameterChange { BeginGesture, EndGesture, @@ -180,6 +182,7 @@ public: /* API for Ardour -- Setup/Processing */ uint32_t plugin_latency (); + uint32_t plugin_tailtime (); bool set_block_size (int32_t); bool activate (); bool deactivate (); @@ -326,6 +329,7 @@ private: bool _add_to_selection; boost::optional _plugin_latency; + boost::optional _plugin_tail; int _n_bus_in; int _n_bus_out; @@ -440,6 +444,7 @@ public: private: samplecnt_t plugin_latency () const; + samplecnt_t plugin_tailtime () const; void init (); void find_presets (); void forward_resize_view (int w, int h); diff --git a/libs/ardour/audio_playlist.cc b/libs/ardour/audio_playlist.cc index d751152d4c..b3a2e3bfc2 100644 --- a/libs/ardour/audio_playlist.cc +++ b/libs/ardour/audio_playlist.cc @@ -176,7 +176,14 @@ ARDOUR::timecnt_t AudioPlaylist::read (Sample *buf, Sample *mixdown_buffer, float *gain_buffer, timepos_t const & start, timecnt_t const & cnt, uint32_t chan_n) { DEBUG_TRACE (DEBUG::AudioPlayback, string_compose ("Playlist %1 read @ %2 for %3, channel %4, regions %5 mixdown @ %6 gain @ %7\n", - name(), start, cnt, chan_n, regions.size(), mixdown_buffer, gain_buffer)); + name(), start.samples(), cnt.samples(), chan_n, regions.size(), mixdown_buffer, gain_buffer)); + + DEBUG_TRACE (DEBUG::AudioCacheRefill, string_compose ("Playlist '%1' chn: %2 from %3 to %4 [s] PH@ %5\n", + name (), chan_n, + std::setprecision (3), std::fixed, + start.samples() / (float)_session.sample_rate (), + (start.samples() + cnt.samples()) / (float)_session.sample_rate (), + _session.transport_sample () / (float)_session.sample_rate ())); samplecnt_t const scnt (cnt.samples ()); @@ -204,7 +211,7 @@ AudioPlaylist::read (Sample *buf, Sample *mixdown_buffer, float *gain_buffer, ti /* Find all the regions that are involved in the bit we are reading, and sort them by descending layer and ascending position. */ - std::shared_ptr all = regions_touched_locked (start, start + cnt); + std::shared_ptr all = regions_touched_locked (start, start + cnt, true); all->sort (ReadSorter ()); /* This will be a list of the bits of our read range that we have @@ -236,7 +243,7 @@ AudioPlaylist::read (Sample *buf, Sample *mixdown_buffer, float *gain_buffer, ti */ Temporal::Range rrange = ar->range_samples (); Temporal::Range region_range (max (rrange.start(), start), - min (rrange.end(), start + cnt)); + min (rrange.end() + ar->tail (), start + cnt)); /* ... and then remove the bits that are already done */ @@ -256,9 +263,9 @@ AudioPlaylist::read (Sample *buf, Sample *mixdown_buffer, float *gain_buffer, ti /* Cut this range down to just the body and mark it done */ Temporal::Range body = ar->body_range (); - if (body.start() < d.end() && body.end() > d.start()) { + if (body.start() < d.end().earlier (ar->tail ()) && body.end() > d.start()) { d.set_start (max (d.start(), body.start())); - d.set_end (min (d.end(), body.end())); + d.set_end (min (d.end().earlier (ar->tail ()), body.end())); done.add (d); } } @@ -285,8 +292,13 @@ AudioPlaylist::read (Sample *buf, Sample *mixdown_buffer, float *gain_buffer, ti assert (soffset + read_cnt <= scnt); samplecnt_t nread = i->region->read_at (buf + soffset, mixdown_buffer, gain_buffer, read_pos, read_cnt, chan_n); if (nread != read_cnt) { - std::cerr << name() << " tried to read " << read_cnt << " from " << nread << " in " << i->region->name() << " using range " - << i->range.start() << " .. " << i->range.end() << " len " << i->range.length() << std::endl; + std::cerr << name() << " tried to read " << read_cnt + << " got " << nread + << " in " << i->region->name() + << " for chn " << chan_n + << " to offset " << soffset + << " using range " << i->range.start().samples() << " .. " << i->range.end().samples() + << " len " << i->range.length().samples() << std::endl; #ifndef NDEBUG /* forward error to DiskReader::audio_read. This does 2 things: * - error "DiskReader %1: when refilling, cannot read ..." diff --git a/libs/ardour/audio_unit.cc b/libs/ardour/audio_unit.cc index 781a4e8783..f94f7a1be9 100644 --- a/libs/ardour/audio_unit.cc +++ b/libs/ardour/audio_unit.cc @@ -360,6 +360,7 @@ AUPlugin::AUPlugin (AudioEngine& engine, Session& session, std::shared_ptrSetProperty (/*kAudioUnitProperty_OfflineRender*/ 37, kAudioUnitScope_Global, 0, &isOffline, sizeof (isOffline))) != noErr) { + info << string_compose (_("AU: cannot set offline rendering(err = %1)"), err) << endmsg; + } + if (yn) { + UInt32 numSamples = _session.get_block_size(); + unit->SetProperty (/*kAudioUnitOfflineProperty_InputSize*/ 3020, kAudioUnitScope_Global, 0, &numSamples, sizeof(numSamples)); + } + + if (was_initialized) { + activate (); + } +} int AUPlugin::set_block_size (pframes_t nframes) @@ -888,6 +917,10 @@ AUPlugin::set_block_size (pframes_t nframes) return -1; } + if (process_offline) { + unit->SetProperty (/*kAudioUnitOfflineProperty_InputSize*/ 3020, kAudioUnitScope_Global, 0, &numSamples, sizeof(numSamples)); + } + if (was_initialized) { activate (); } diff --git a/libs/ardour/audioregion.cc b/libs/ardour/audioregion.cc index 85be9e78f3..90a68f0e0f 100644 --- a/libs/ardour/audioregion.cc +++ b/libs/ardour/audioregion.cc @@ -47,6 +47,7 @@ #include "ardour/analysis_graph.h" #include "ardour/audioregion.h" #include "ardour/buffer_manager.h" +#include "ardour/butler.h" #include "ardour/session.h" #include "ardour/dB.h" #include "ardour/debug.h" @@ -81,6 +82,7 @@ namespace ARDOUR { PBD::PropertyDescriptor default_fade_out; PBD::PropertyDescriptor fade_in_active; PBD::PropertyDescriptor fade_out_active; + PBD::PropertyDescriptor fade_before_fx; PBD::PropertyDescriptor scale_amplitude; PBD::PropertyDescriptor > fade_in; PBD::PropertyDescriptor > inverse_fade_in; @@ -175,6 +177,8 @@ AudioRegion::make_property_quarks () DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for fade-in-active = %1\n", Properties::fade_in_active.property_id)); Properties::fade_out_active.property_id = g_quark_from_static_string (X_("fade-out-active")); DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for fade-out-active = %1\n", Properties::fade_out_active.property_id)); + Properties::fade_before_fx.property_id = g_quark_from_static_string (X_("fade-before-fx")); + DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for fade-before-fx = %1\n", Properties::fade_before_fx.property_id)); Properties::scale_amplitude.property_id = g_quark_from_static_string (X_("scale-amplitude")); DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for scale-amplitude = %1\n", Properties::scale_amplitude.property_id)); Properties::fade_in.property_id = g_quark_from_static_string (X_("FadeIn")); @@ -199,6 +203,7 @@ AudioRegion::register_properties () add_property (_default_fade_out); add_property (_fade_in_active); add_property (_fade_out_active); + add_property (_fade_before_fx); add_property (_scale_amplitude); add_property (_fade_in); add_property (_inverse_fade_in); @@ -213,6 +218,7 @@ AudioRegion::register_properties () , _default_fade_out (Properties::default_fade_out, true) \ , _fade_in_active (Properties::fade_in_active, true) \ , _fade_out_active (Properties::fade_out_active, true) \ + , _fade_before_fx (Properties::fade_before_fx, false) \ , _scale_amplitude (Properties::scale_amplitude, 1.0) \ , _fade_in (Properties::fade_in, std::shared_ptr (new AutomationList (Evoral::Parameter (FadeInAutomation), tdp))) \ , _inverse_fade_in (Properties::inverse_fade_in, std::shared_ptr (new AutomationList (Evoral::Parameter (FadeInAutomation), tdp))) \ @@ -225,6 +231,7 @@ AudioRegion::register_properties () , _default_fade_out (Properties::default_fade_out, other->_default_fade_out) \ , _fade_in_active (Properties::fade_in_active, other->_fade_in_active) \ , _fade_out_active (Properties::fade_out_active, other->_fade_out_active) \ + , _fade_before_fx (Properties::fade_before_fx, other->_fade_before_fx) \ , _scale_amplitude (Properties::scale_amplitude, other->_scale_amplitude) \ , _fade_in (Properties::fade_in, std::shared_ptr (new AutomationList (*other->_fade_in.val()))) \ , _inverse_fade_in (Properties::fade_in, std::shared_ptr (new AutomationList (*other->_inverse_fade_in.val()))) \ @@ -247,6 +254,7 @@ AudioRegion::init () connect_to_header_position_offset_changed (); _fx_pos = _cache_start = _cache_end = -1; + _cache_tail = 0; _fx_block_size = 0; _fx_latent_read = false; } @@ -340,6 +348,7 @@ AudioRegion::AudioRegion (std::shared_ptr other) connect_to_header_position_offset_changed (); _fx_pos = _cache_start = _cache_end = -1; + _cache_tail = 0; _fx_block_size = 0; _fx_latent_read = false; @@ -368,6 +377,7 @@ AudioRegion::AudioRegion (std::shared_ptr other, timecnt_t co connect_to_header_position_offset_changed (); _fx_pos = _cache_start = _cache_end = -1; + _cache_tail = 0; _fx_block_size = 0; _fx_latent_read = false; @@ -394,6 +404,7 @@ AudioRegion::AudioRegion (std::shared_ptr other, const Source connect_to_header_position_offset_changed (); _fx_pos = _cache_start = _cache_end = -1; + _cache_tail = 0; _fx_block_size = 0; _fx_latent_read = false; @@ -498,6 +509,32 @@ AudioRegion::set_envelope_active (bool yn) } } +void +AudioRegion::set_fade_before_fx (bool yn) +{ + if (fade_before_fx() != yn) { + _fade_before_fx = yn; + send_change (PropertyChange (Properties::fade_before_fx)); + if (!has_region_fx ()) { + return; + } + if (!_invalidated.exchange (true)) { + send_change (PropertyChange (Properties::region_fx)); // trigger DiskReader overwrite + } + RegionFxChanged (); /* EMIT SIGNAL */ + } +} + +timecnt_t +AudioRegion::tail () const +{ + if (_fade_before_fx && has_region_fx ()) { + return timecnt_t ((samplecnt_t)_fx_tail); + } else { + return timecnt_t (0); + } +} + /** @param buf Buffer to put peak data in. * @param npeaks Number of peaks to read (ie the number of PeakDatas in buf) * @param offset Start position, as an offset from the start of this region's source. @@ -573,6 +610,11 @@ AudioRegion::read_at (Sample* buf, The caller has verified that we cover the desired section. */ + DEBUG_TRACE (DEBUG::AudioCacheRefill, string_compose ("- Region '%1' chn: %2 from %3 to %4 [s]\n", + name(), chan_n, + std::setprecision (3), std::fixed, + pos / (float)_session.sample_rate (), (pos + cnt) / (float)_session.sample_rate ())); + /* See doc/region_read.svg for a drawing which might help to explain what is going on. */ @@ -584,49 +626,62 @@ AudioRegion::read_at (Sample* buf, return 0; } - /* WORK OUT WHERE TO GET DATA FROM */ - - samplecnt_t to_read; - const samplepos_t psamples = position().samples(); - const samplecnt_t lsamples = _length.val().samples(); - - assert (pos >= psamples); - sampleoffset_t const internal_offset = pos - psamples; - - if (internal_offset >= lsamples) { - return 0; /* read nothing */ - } - - const samplecnt_t esamples = lsamples - internal_offset; - assert (esamples >= 0); - - if ((to_read = min (cnt, esamples)) == 0) { - return 0; /* read nothing */ - } - std::shared_ptr pl (playlist()); if (!pl){ return 0; } - /* COMPUTE DETAILS OF ANY FADES INVOLVED IN THIS READ */ + /* WORK OUT WHERE TO GET DATA FROM */ + + const samplepos_t psamples = position().samples(); + const samplecnt_t lsamples = _length.val().samples(); + const samplecnt_t tsamples = tail ().samples (); + + assert (pos >= psamples); + sampleoffset_t internal_offset = pos - psamples; + sampleoffset_t suffix = 0; + + if (internal_offset >= lsamples + tsamples) { + return 0; /* read nothing */ + } + + if (internal_offset > lsamples) { + suffix = internal_offset - lsamples; + internal_offset = lsamples; + } + + const samplecnt_t esamples = lsamples - internal_offset; + assert (esamples >= 0); + + if (min (cnt, esamples + tsamples) <= 0) { + return 0; /* read nothing */ + } + + /* does not include tail */ + samplecnt_t const to_read = max (0, min (cnt, esamples)); + samplecnt_t const can_read = max (0, min (cnt, esamples + tsamples)); + + /* COMPUTE DETAILS OF ANY FADES INVOLVED IN THIS READ + * + * This information is also used for inverse fades to fade out + * layered regions below this one. + */ + bool const use_region_fades = _session.config.get_use_region_fades(); /* Amount (length) of fade in that we are dealing with in this read */ samplecnt_t fade_in_limit = 0; - /* Offset from buf / mixdown_buffer of the start - of any fade out that we are dealing with - */ + /* Offset from buf / mixdown_buffer of the start of any fade out that we are dealing with */ sampleoffset_t fade_out_offset = 0; /* Amount (length) of fade out that we are dealing with in this read */ samplecnt_t fade_out_limit = 0; + /* offset for fade-out curve data */ samplecnt_t fade_interval_start = 0; /* Fade in */ - - if (_fade_in_active && _session.config.get_use_region_fades()) { + if (_fade_in_active && use_region_fades) { samplecnt_t fade_in_length = _fade_in->when(false).samples(); @@ -638,15 +693,13 @@ AudioRegion::read_at (Sample* buf, } /* Fade out */ - - if (_fade_out_active && _session.config.get_use_region_fades()) { + if (_fade_out_active && use_region_fades) { /* see if some part of this read is within the fade out */ - - /* ................. >| REGION + /* ................. >| REGION * _length * - * { } FADE + * { } FADE * fade_out_length * ^ * _length - fade_out_length @@ -654,18 +707,19 @@ AudioRegion::read_at (Sample* buf, * |--------------| * ^internal_offset * ^internal_offset + to_read - * - * we need the intersection of [internal_offset,internal_offset+to_read] with - * [_length - fade_out_length, _length] - * + */ + /* we need the intersection of + * [internal_offset, internal_offset + to_read] + * with + * [_length - fade_out_length, _length] */ - fade_interval_start = max (internal_offset, lsamples - _fade_out->when(false).samples()); - samplecnt_t fade_interval_end = min(internal_offset + to_read, lsamples); + fade_interval_start = max (internal_offset, lsamples - _fade_out->when(false).samples()); + samplecnt_t fade_interval_end = min (internal_offset + to_read, lsamples); if (fade_interval_end > fade_interval_start) { /* (part of the) the fade out is in this buffer */ - fade_out_limit = fade_interval_end - fade_interval_start; + fade_out_limit = fade_interval_end - fade_interval_start; fade_out_offset = fade_interval_start - internal_offset; } } @@ -673,16 +727,17 @@ AudioRegion::read_at (Sample* buf, Glib::Threads::Mutex::Lock cl (_cache_lock); if (chan_n == 0 && _invalidated.exchange (false)) { _cache_start = _cache_end = -1; + _cache_tail = 0; } boost::scoped_array gain_array; boost::scoped_array mixdown_array; // TODO optimize mono reader, w/o plugins -> old code - if (n_chn > 1 && _cache_start < _cache_end && internal_offset >= _cache_start && internal_offset + to_read <= _cache_end) { - DEBUG_TRACE (DEBUG::AudioPlayback, string_compose ("Region '%1' channel: %2 copy from cache %3 - %4 to_read: %5\n", - name(), chan_n, internal_offset, internal_offset + to_read, to_read)); - copy_vector (mixdown_buffer, _readcache.get_audio (chan_n).data (internal_offset - _cache_start), to_read); + if (n_chn > 1 && _cache_start < _cache_end && internal_offset + suffix >= _cache_start && internal_offset + suffix + can_read <= _cache_end) { + DEBUG_TRACE (DEBUG::AudioPlayback, string_compose ("Region '%1' channel: %2 copy from cache %3 - %4 to_read: %5 can_read: %6\n", + name(), chan_n, internal_offset + suffix, internal_offset + suffix + can_read, to_read, can_read)); + copy_vector (mixdown_buffer, _readcache.get_audio (chan_n).data (internal_offset + suffix - _cache_start), can_read); cl.release (); } else { Glib::Threads::RWLock::ReaderLock lm (_fx_lock); @@ -690,24 +745,21 @@ AudioRegion::read_at (Sample* buf, uint32_t fx_latency = _fx_latency; lm.release (); - ChanCount cc (DataType::AUDIO, n_channels ()); - _readcache.ensure_buffers (cc, to_read + _fx_latency); - samplecnt_t n_read = to_read; //< data to read from disk samplecnt_t n_proc = to_read; //< silence pad data to process + samplepos_t n_tail = 0; // further silence pad, read tail from FX samplepos_t readat = pos; sampleoffset_t offset = internal_offset; - //printf ("READ Cache end %ld pos %ld\n", _cache_end, readat); - if (_cache_end != readat && fx_latency > 0) { + if (tsamples > 0 && cnt >= esamples) { + n_tail = can_read - n_read; + n_proc += n_tail; + } + + if (_cache_end != internal_offset + suffix && fx_latency > 0) { _fx_latent_read = true; n_proc += fx_latency; n_read = min (to_read + fx_latency, esamples); - - mixdown_array.reset (new Sample[n_proc]); - mixdown_buffer = mixdown_array.get (); - gain_array.reset (new gain_t[n_proc]); - gain_buffer = gain_array.get (); } if (!_fx_latent_read && fx_latency > 0) { @@ -716,13 +768,20 @@ AudioRegion::read_at (Sample* buf, n_read = max (0, min (to_read, lsamples - offset)); } - DEBUG_TRACE (DEBUG::AudioPlayback, string_compose ("Region '%1' channel: %2 read: %3 - %4 (%5) to_read: %6 offset: %7 with fx: %8 fx_latency: %9\n", - name(), chan_n, readat, readat + n_read, n_read, to_read, internal_offset, have_fx, fx_latency)); + if (n_proc > to_read) { + mixdown_array.reset (new Sample[n_proc]); + mixdown_buffer = mixdown_array.get (); + gain_array.reset (new gain_t[n_proc]); + gain_buffer = gain_array.get (); + } + DEBUG_TRACE (DEBUG::AudioPlayback, string_compose ("Region '%1' channel: %2 read: %3 - %4 (%5) to_read: %6 offset: %7 with fx: %8 fx_latency: %9 fx_tail %10\n", + name(), chan_n, readat, readat + n_read, n_read, to_read, internal_offset, have_fx, fx_latency, n_tail)); + + ChanCount cc (DataType::AUDIO, n_channels ()); _readcache.ensure_buffers (cc, n_proc); if (n_read < n_proc) { - //printf ("SILENCE PAD rd: %ld proc: %ld\n", n_read, n_proc); /* silence pad, process tail of latent effects */ memset (&mixdown_buffer[n_read], 0, sizeof (Sample)* (n_proc - n_read)); _readcache.silence (n_proc - n_read, n_read); @@ -730,6 +789,7 @@ AudioRegion::read_at (Sample* buf, /* reset in case read fails we return early */ _cache_start = _cache_end = -1; + _cache_tail = 0; for (uint32_t chn = 0; chn < n_chn; ++chn) { /* READ DATA FROM THE SOURCE INTO mixdown_buffer. @@ -759,6 +819,49 @@ AudioRegion::read_at (Sample* buf, apply_gain_to_buffer (mixdown_buffer, n_read, _scale_amplitude); } + /* Apply Region Fades before processing. */ + + /* Fade in. Precomputed data from above may not apply here. + * latent FX may have increasded to_read -> n_read, + * or internal_offset. + */ + if (_fade_before_fx && use_region_fades && _fade_in_active) { + samplecnt_t fade_in_length = _fade_in->when(false).samples(); + if (offset < fade_in_length) { + samplecnt_t fade_in_limit = min (n_read, fade_in_length - offset); + + //fade_in_limit = min (fade_in_limit, n_read); + assert (fade_in_limit <= n_read); + _fade_in->curve().get_vector (timepos_t (offset), timepos_t (offset + fade_in_limit), gain_buffer, fade_in_limit); + for (samplecnt_t n = 0; n < fade_in_limit; ++n) { + mixdown_buffer[n] *= gain_buffer[n]; + } + } + } + + /* Fade out. Precomputed data from above may not apply here. + * If there are latent FX: internal_offset != offset + */ + if (_fade_before_fx && use_region_fades && _fade_out_active) { + samplecnt_t fade_interval_start = max (offset, lsamples - _fade_out->when(false).samples()); + samplecnt_t fade_interval_end = min (offset + n_read, lsamples); + + if (fade_interval_end > fade_interval_start) { + /* (part of the) the fade out is in this buffer */ + samplecnt_t fade_out_limit = fade_interval_end - fade_interval_start; + sampleoffset_t fade_out_offset = fade_interval_start - offset; + + assert (fade_out_offset + fade_out_limit <= n_read); + + /* apply fade out */ + samplecnt_t const curve_offset = fade_interval_start - _fade_out->when(false).distance (len_as_tpos ()).samples(); + _fade_out->curve().get_vector (timepos_t (curve_offset), timepos_t (curve_offset + fade_out_limit), gain_buffer, fade_out_limit); + for (samplecnt_t n = 0, m = fade_out_offset; n < fade_out_limit; ++n, ++m) { + mixdown_buffer[m] *= gain_buffer[n]; + } + } + } + /* for mono regions no cache is required, unless there are * regionFX, which use the _readcache BufferSet. */ @@ -769,17 +872,39 @@ AudioRegion::read_at (Sample* buf, /* apply region FX to all channels */ if (have_fx) { - const_cast(this)->apply_region_fx (_readcache, offset, offset + n_proc, n_proc); +#ifndef NDEBUG + microseconds_t t_start = get_microseconds (); +#endif + const_cast(this)->apply_region_fx (_readcache, offset + suffix, offset + suffix + n_proc, n_proc); +#ifndef NDEBUG + if (DEBUG_ENABLED (DEBUG::AudioCacheRefill)) { + microseconds_t t_end = get_microseconds (); + int nsecs_per_sample = lrintf ((t_end - t_start) * 1000 / std::max (1.0, n_proc * n_chn)); + float load = (t_end - t_start) / (10000.f * n_proc / (float) _session.sample_rate ()); + DEBUG_TRACE (DEBUG::AudioCacheRefill, string_compose ("- RegionFx '%1' took %2 us, frames: %3, nchn: %4, ns/spl: %5 load: %6%%\n", + name (), (t_end - t_start), n_proc, n_chn, nsecs_per_sample, load)); + } +#endif } /* for mono regions without plugins, mixdown_buffer is valid as-is */ if (n_chn > 1 || have_fx) { /* copy data for current channel */ - copy_vector (mixdown_buffer, _readcache.get_audio (chan_n).data (), to_read); + if (chan_n < n_channels()) { + copy_vector (mixdown_buffer, _readcache.get_audio (chan_n).data (), to_read + n_tail); + } else { + if (Config->get_replicate_missing_region_channels()) { + chan_n = chan_n % n_channels (); + copy_vector (mixdown_buffer, _readcache.get_audio (chan_n).data (), to_read + n_tail); + } else { + memset (mixdown_buffer, 0, sizeof (Sample) * (to_read + n_tail)); + } + } } - _cache_start = internal_offset; - _cache_end = internal_offset + to_read; + _cache_start = internal_offset + suffix; + _cache_end = internal_offset + suffix + to_read + n_tail; + _cache_tail = n_tail; cl.release (); } @@ -828,9 +953,13 @@ AudioRegion::read_at (Sample* buf, _fade_in->curve().get_vector (timepos_t (internal_offset), timepos_t (internal_offset + fade_in_limit), gain_buffer, fade_in_limit); } - /* Mix our newly-read data in, with the fade */ - for (samplecnt_t n = 0; n < fade_in_limit; ++n) { - buf[n] += mixdown_buffer[n] * gain_buffer[n]; + if (!_fade_before_fx) { + /* Mix our newly-read data in, with the fade */ + for (samplecnt_t n = 0; n < fade_in_limit; ++n) { + buf[n] += mixdown_buffer[n] * gain_buffer[n]; + } + } else { + fade_in_limit = 0; } } @@ -869,11 +998,13 @@ AudioRegion::read_at (Sample* buf, _fade_out->curve().get_vector (timepos_t (curve_offset), timepos_t (curve_offset + fade_out_limit), gain_buffer, fade_out_limit); } - /* Mix our newly-read data with whatever was already there, - with the fade out applied to our data. - */ - for (samplecnt_t n = 0, m = fade_out_offset; n < fade_out_limit; ++n, ++m) { - buf[m] += mixdown_buffer[m] * gain_buffer[n]; + if (!_fade_before_fx) { + /* Mix our newly-read data with whatever was already there, with the fade out applied to our data. */ + for (samplecnt_t n = 0, m = fade_out_offset; n < fade_out_limit; ++n, ++m) { + buf[m] += mixdown_buffer[m] * gain_buffer[n]; + } + } else { + fade_out_limit = 0; } } @@ -890,8 +1021,16 @@ AudioRegion::read_at (Sample* buf, mix_buffers_no_gain (buf + fade_in_limit, mixdown_buffer + fade_in_limit, N); } } + samplecnt_t T = _cache_tail; + if (T > 0) { + T = min (T, can_read); + DEBUG_TRACE (DEBUG::AudioPlayback, string_compose ("Region %1 adding FX tail of %2 cut to_read %3 at %4 total len = %5 cnt was %6\n", + name (), _cache_tail, T, to_read, to_read + T, cnt)); + /* AudioPlaylist::read reads regions in reverse order, so we can add the tail here */ + mix_buffers_no_gain (buf + to_read, mixdown_buffer + to_read, T); + } - return to_read; + return to_read + T; } /** Read data directly from one of our sources, accounting for the situation when the track has a different channel @@ -2341,7 +2480,12 @@ AudioRegion::_add_plugin (std::shared_ptr rfx, std::shared_ptrdelegate (boost::bind (&AudioRegion::send_change, this, PropertyChange (Properties::region_fx))); + } } }); if (!ac->alist ()) { @@ -2356,6 +2500,7 @@ AudioRegion::_add_plugin (std::shared_ptr rfx, std::shared_ptrLatencyChanged.connect_same_thread (*this, boost::bind (&AudioRegion::fx_latency_changed, this, false)); + rfx->TailTimeChanged.connect_same_thread (*this, boost::bind (&AudioRegion::fx_tail_changed, this, false)); rfx->set_block_size (_session.get_block_size ()); if (from_set_state) { @@ -2374,6 +2519,8 @@ AudioRegion::_add_plugin (std::shared_ptr rfx, std::shared_ptrset_default_automation (len_as_tpos ()); fx_latency_changed (true); + fx_tail_changed (true); + if (!_invalidated.exchange (true)) { send_change (PropertyChange (Properties::region_fx)); // trigger DiskReader overwrite } @@ -2395,11 +2542,13 @@ AudioRegion::remove_plugin (std::shared_ptr fx) fx->drop_references (); fx_latency_changed (true); + fx_tail_changed (true); if (!_invalidated.exchange (true)) { send_change (PropertyChange (Properties::region_fx)); // trigger DiskReader overwrite } RegionFxChanged (); /* EMIT SIGNAL */ + _session.set_dirty (); return true; } @@ -2434,6 +2583,27 @@ AudioRegion::fx_latency_changed (bool no_emit) } } +void +AudioRegion::fx_tail_changed (bool no_emit) +{ + uint32_t t = 0; + for (auto const& rfx : _plugins) { + t = max (t, rfx->effective_tailtime ()); + } + if (t == _fx_tail) { + return; + } + _fx_tail = t; + + if (no_emit) { + return; + } + + if (!_invalidated.exchange (true)) { + send_change (PropertyChange (Properties::region_fx)); // trigger DiskReader overwrite + } +} + void AudioRegion::apply_region_fx (BufferSet& bufs, samplepos_t start_sample, samplepos_t end_sample, samplecnt_t n_samples) { @@ -2463,7 +2633,8 @@ AudioRegion::apply_region_fx (BufferSet& bufs, samplepos_t start_sample, samplep while (remain > 0) { pframes_t run = std::min (remain, block_size); - if (!rfx->run (bufs, start_sample + offset - latency_offset, end_sample + offset - latency_offset, position().samples(), run, offset)) { + samplepos_t cycle_start = start_sample + offset - latency_offset; + if (!rfx->run (bufs, cycle_start, cycle_start + run, position().samples(), run, offset)) { lm.release (); /* this triggers a re-read */ const_cast(this)->remove_plugin (rfx); diff --git a/libs/ardour/debug.cc b/libs/ardour/debug.cc index 99a8dbce20..bc715bcacc 100644 --- a/libs/ardour/debug.cc +++ b/libs/ardour/debug.cc @@ -29,6 +29,7 @@ using namespace std; PBD::DebugBits PBD::DEBUG::AudioEngine = PBD::new_debug_bit ("AudioEngine"); PBD::DebugBits PBD::DEBUG::AudioPlayback = PBD::new_debug_bit ("audioplayback"); +PBD::DebugBits PBD::DEBUG::AudioCacheRefill = PBD::new_debug_bit ("audiocacherefill"); PBD::DebugBits PBD::DEBUG::AudioUnitConfig = PBD::new_debug_bit ("AudioUnitConfig"); PBD::DebugBits PBD::DEBUG::AudioUnitGUI = PBD::new_debug_bit ("AudioUnitGUI"); PBD::DebugBits PBD::DEBUG::AudioUnitProcess = PBD::new_debug_bit ("AudioUnitProcess"); @@ -63,6 +64,7 @@ PBD::DebugBits PBD::DEBUG::LatencyIO = PBD::new_debug_bit ("latencyio"); PBD::DebugBits PBD::DEBUG::LatencyRoute = PBD::new_debug_bit ("latencyroute"); PBD::DebugBits PBD::DEBUG::LaunchControlXL = PBD::new_debug_bit("launchcontrolxl"); PBD::DebugBits PBD::DEBUG::Launchpad = PBD::new_debug_bit ("launchpad"); +PBD::DebugBits PBD::DEBUG::Launchkey = PBD::new_debug_bit ("launchkey"); PBD::DebugBits PBD::DEBUG::Layering = PBD::new_debug_bit ("layering"); PBD::DebugBits PBD::DEBUG::MIDISurface = PBD::new_debug_bit ("midisurface"); PBD::DebugBits PBD::DEBUG::MTC = PBD::new_debug_bit ("mtc"); diff --git a/libs/ardour/disk_reader.cc b/libs/ardour/disk_reader.cc index d812648759..57976725cb 100644 --- a/libs/ardour/disk_reader.cc +++ b/libs/ardour/disk_reader.cc @@ -423,6 +423,10 @@ DiskReader::run (BufferSet& bufs, samplepos_t start_sample, samplepos_t end_samp cerr << "underrun for " << _name << " Available samples: " << available << " required: " << disk_samples_to_consume << endl; #endif DEBUG_TRACE (DEBUG::Butler, string_compose ("%1 underrun in %2, total space = %3 vs %4\n", DEBUG_THREAD_SELF, name (), available, disk_samples_to_consume)); + DEBUG_TRACE (DEBUG::AudioCacheRefill, string_compose ("DR '%1' underrun have %2 need %3 samples at pos %4\n", + name (), available, disk_samples_to_consume, + std::setprecision (3), std::fixed, + start_sample / (float)_session.sample_rate ())); Underrun (); return; } diff --git a/libs/ardour/disk_writer.cc b/libs/ardour/disk_writer.cc index 0c8d197480..9ee351e17f 100644 --- a/libs/ardour/disk_writer.cc +++ b/libs/ardour/disk_writer.cc @@ -60,7 +60,7 @@ DiskWriter::DiskWriter (Session& s, Track& t, string const & str, DiskIOProcesso , _accumulated_capture_offset (0) , _transport_looped (false) , _transport_loop_sample (0) - , _gui_feed_buffer(AudioEngine::instance()->raw_buffer_size (DataType::MIDI)) + , _gui_feed_fifo (min (64000, max (s.sample_rate() / 10, 2 * AudioEngine::instance()->raw_buffer_size (DataType::MIDI)))) { DiskIOProcessor::init (); _xruns.reserve (128); @@ -684,25 +684,18 @@ DiskWriter::run (BufferSet& bufs, samplepos_t start_sample, samplepos_t end_samp _samples_pending_write.fetch_add ((int) nframes); if (buf.size() != 0) { - Glib::Threads::Mutex::Lock lm (_gui_feed_buffer_mutex, Glib::Threads::TRY_LOCK); - - if (lm.locked ()) { - /* Copy this data into our GUI feed buffer and tell the GUI - that it can read it if it likes. - */ - _gui_feed_buffer.clear (); - - for (MidiBuffer::iterator i = buf.begin(); i != buf.end(); ++i) { - /* This may fail if buf is larger than _gui_feed_buffer, but it's not really - * the end of the world if it does. - */ - samplepos_t mpos = (*i).time() + start_sample - _accumulated_capture_offset; - if (mpos >= _first_recordable_sample) { - _gui_feed_buffer.push_back (mpos, Evoral::MIDI_EVENT, (*i).size(), (*i).buffer()); - } + /* Copy this data into our GUI feed buffer and tell the GUI + * that it can read it if it likes. + */ + for (MidiBuffer::iterator i = buf.begin(); i != buf.end(); ++i) { + /* This may fail if buf is larger than _gui_feed_fifo, but it's not really + * the end of the world if it does. + */ + samplepos_t mpos = (*i).time() + start_sample - _accumulated_capture_offset; + if (mpos >= _first_recordable_sample) { + _gui_feed_fifo.write (mpos, Evoral::MIDI_EVENT, (*i).size(), (*i).buffer()); } } - } if (cnt) { @@ -807,10 +800,18 @@ DiskWriter::finish_capture (std::shared_ptr c) std::shared_ptr DiskWriter::get_gui_feed_buffer () const { + Glib::Threads::Mutex::Lock lm (_gui_feed_reset_mutex); std::shared_ptr b (new MidiBuffer (AudioEngine::instance()->raw_buffer_size (DataType::MIDI))); - Glib::Threads::Mutex::Lock lm (_gui_feed_buffer_mutex); - b->copy (_gui_feed_buffer); + vector buffer (_gui_feed_fifo.capacity()); + samplepos_t time; + Evoral::EventType type; + uint32_t size; + + while (_gui_feed_fifo.read (&time, &type, &size, &buffer[0])) { + b->push_back (time, type, size, &buffer[0]); + } + return b; } @@ -1191,6 +1192,11 @@ DiskWriter::transport_stopped_wallclock (struct tm& when, time_t twhen, bool abo finish_capture (c); + { + Glib::Threads::Mutex::Lock lm (_gui_feed_reset_mutex); + _gui_feed_fifo.reset (); + } + /* butler is already stopped, but there may be work to do to flush remaining data to disk. */ diff --git a/libs/ardour/element_importer.cc b/libs/ardour/element_importer.cc index a2ba8aa22f..a3940b08c8 100644 --- a/libs/ardour/element_importer.cc +++ b/libs/ardour/element_importer.cc @@ -34,8 +34,8 @@ using namespace std; using namespace PBD; using namespace ARDOUR; -Signal2,string, string> ElementImporter::Rename; -Signal1 ElementImporter::Prompt; +PBD::Signal2,string, string> ElementImporter::Rename; +PBD::Signal1 ElementImporter::Prompt; ElementImporter::ElementImporter (XMLTree const & source, ARDOUR::Session & session) : source (source), diff --git a/libs/ardour/export_handler.cc b/libs/ardour/export_handler.cc index d90c9cfb46..46fbc9564e 100644 --- a/libs/ardour/export_handler.cc +++ b/libs/ardour/export_handler.cc @@ -32,6 +32,7 @@ #include "ardour/audiofile_tagger.h" #include "ardour/audio_port.h" #include "ardour/debug.h" +#include "ardour/disk_reader.h" #include "ardour/export_graph_builder.h" #include "ardour/export_handler.h" #include "ardour/export_timespan.h" @@ -380,7 +381,10 @@ ExportHandler::start_timespan_bg (void* eh) ExportHandler* self = static_cast (eh); self->process_connection.disconnect (); Glib::Threads::Mutex::Lock l (self->export_status->lock()); + SessionEvent::create_per_thread_pool (name, 512); + DiskReader::allocate_working_buffers (); self->start_timespan (); + DiskReader::free_working_buffers (); return 0; } @@ -447,17 +451,8 @@ ExportHandler::finish_timespan () if (!fmt->command().empty()) { SessionMetadata const & metadata (*SessionMetadata::Metadata()); -#if 0 // would be nicer with C++11 initialiser... - std::map subs { - { 'f', filename }, - { 'd', Glib::path_get_dirname(filename) + G_DIR_SEPARATOR }, - { 'b', PBD::basename_nosuffix(filename) }, - ... - }; -#endif export_status->active_job = ExportStatus::Command; PBD::ScopedConnection command_connection; - std::map subs; std::stringstream track_number; track_number << metadata.track_number (); @@ -466,30 +461,32 @@ ExportHandler::finish_timespan () std::stringstream year; year << metadata.year (); - subs.insert (std::pair ('a', metadata.artist ())); - subs.insert (std::pair ('b', PBD::basename_nosuffix (filename))); - subs.insert (std::pair ('c', metadata.copyright ())); - subs.insert (std::pair ('d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR)); - subs.insert (std::pair ('f', filename)); - subs.insert (std::pair ('l', metadata.lyricist ())); - subs.insert (std::pair ('n', session.name ())); - subs.insert (std::pair ('s', session.path ())); - subs.insert (std::pair ('o', metadata.conductor ())); - subs.insert (std::pair ('t', metadata.title ())); - subs.insert (std::pair ('z', metadata.organization ())); - subs.insert (std::pair ('A', metadata.album ())); - subs.insert (std::pair ('C', metadata.comment ())); - subs.insert (std::pair ('E', metadata.engineer ())); - subs.insert (std::pair ('G', metadata.genre ())); - subs.insert (std::pair ('L', total_tracks.str ())); - subs.insert (std::pair ('M', metadata.mixer ())); - subs.insert (std::pair ('N', current_timespan->name())); // =?= config_map.begin()->first->name () - subs.insert (std::pair ('O', metadata.composer ())); - subs.insert (std::pair ('P', metadata.producer ())); - subs.insert (std::pair ('S', metadata.disc_subtitle ())); - subs.insert (std::pair ('T', track_number.str ())); - subs.insert (std::pair ('Y', year.str ())); - subs.insert (std::pair ('Z', metadata.country ())); + std::map subs { + {'a', metadata.artist ()}, + {'b', PBD::basename_nosuffix (filename)}, + {'c', metadata.copyright ()}, + {'d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR}, + {'f', filename}, + {'l', metadata.lyricist ()}, + {'n', session.name ()}, + {'s', session.path ()}, + {'o', metadata.conductor ()}, + {'t', metadata.title ()}, + {'z', metadata.organization ()}, + {'A', metadata.album ()}, + {'C', metadata.comment ()}, + {'E', metadata.engineer ()}, + {'G', metadata.genre ()}, + {'L', total_tracks.str ()}, + {'M', metadata.mixer ()}, + {'N', current_timespan->name()}, // =?= config_map.begin()->first->name () + {'O', metadata.composer ()}, + {'P', metadata.producer ()}, + {'S', metadata.disc_subtitle ()}, + {'T', track_number.str ()}, + {'Y', year.str ()}, + {'Z', metadata.country ()} + }; ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs, true); info << "Post-export command line : {" << se->to_s () << "}" << endmsg; diff --git a/libs/ardour/io_plug.cc b/libs/ardour/io_plug.cc index ae97c0b1cd..18df41dd26 100644 --- a/libs/ardour/io_plug.cc +++ b/libs/ardour/io_plug.cc @@ -257,6 +257,7 @@ IOPlug::setup () _plugin->reconfigure_io (_n_in, aux_in, _n_out); _plugin->ParameterChangedExternally.connect_same_thread (*this, boost::bind (&IOPlug::parameter_changed_externally, this, _1, _2)); _plugin->activate (); + _plugin->set_insert (this, 0); } samplecnt_t @@ -436,6 +437,26 @@ IOPlug::ensure_io () return true; } +ChanMapping +IOPlug::input_map (uint32_t num) const +{ + if (num == 1) { + return ChanMapping (_n_in); + } else { + return ChanMapping (); + } +} + +ChanMapping +IOPlug::output_map (uint32_t num) const +{ + if (num == 1) { + return ChanMapping (_n_out); + } else { + return ChanMapping (); + } +} + void IOPlug::process () { diff --git a/libs/ardour/luabindings.cc b/libs/ardour/luabindings.cc index f10e97de01..29d4110445 100644 --- a/libs/ardour/luabindings.cc +++ b/libs/ardour/luabindings.cc @@ -1638,7 +1638,7 @@ LuaBindings::common (lua_State* L) .addFunction ("has_transients", &Region::has_transients) .addFunction ("transients", (AnalysisFeatureList (Region::*)())&Region::transients) -#if 0 +#ifndef LIVETRAX // disable region FX .addFunction ("load_plugin", &Region::load_plugin) .addFunction ("add_plugin", &Region::add_plugin) .addFunction ("remove_plugin", &Region::add_plugin) @@ -1694,7 +1694,9 @@ LuaBindings::common (lua_State* L) .addFunction ("envelope_active", &AudioRegion::envelope_active) .addFunction ("fade_in_active", &AudioRegion::fade_in_active) .addFunction ("fade_out_active", &AudioRegion::fade_out_active) + .addFunction ("fade_before_fx", &AudioRegion::fade_before_fx) .addFunction ("set_envelope_active", &AudioRegion::set_envelope_active) + .addFunction ("set_fade_before_fx", &AudioRegion::set_fade_before_fx) .addFunction ("set_fade_in_active", &AudioRegion::set_fade_in_active) .addFunction ("set_fade_in_shape", &AudioRegion::set_fade_in_shape) .addFunction ("set_fade_in_length", &AudioRegion::set_fade_in_length) diff --git a/libs/ardour/lv2_plugin.cc b/libs/ardour/lv2_plugin.cc index 3aa41cf9ce..ce753c9d7f 100644 --- a/libs/ardour/lv2_plugin.cc +++ b/libs/ardour/lv2_plugin.cc @@ -38,8 +38,6 @@ #include #include -#include - #include "pbd/file_utils.h" #include "pbd/stl_delete.h" #include "pbd/compose.h" diff --git a/libs/ardour/monitor_port.cc b/libs/ardour/monitor_port.cc index 45db2addb1..6dd6588155 100644 --- a/libs/ardour/monitor_port.cc +++ b/libs/ardour/monitor_port.cc @@ -338,6 +338,11 @@ MonitorPort::clear_ports (bool instantly) MonitorInputChanged (i->first, false); /* EMIT SIGNAL */ } + if (instantly) { + /* release shared_ptr references */ + _monitor_ports.flush (); + } + if (!s) { return; } diff --git a/libs/ardour/playlist.cc b/libs/ardour/playlist.cc index 5389d30cb7..4b78a677d6 100644 --- a/libs/ardour/playlist.cc +++ b/libs/ardour/playlist.cc @@ -1989,16 +1989,16 @@ std::shared_ptr Playlist::regions_touched (timepos_t const & start, timepos_t const & end) { RegionReadLock rlock (this); - return regions_touched_locked (start, end); + return regions_touched_locked (start, end, false); } std::shared_ptr -Playlist::regions_touched_locked (timepos_t const & start, timepos_t const & end) +Playlist::regions_touched_locked (timepos_t const & start, timepos_t const & end, bool with_tail) { std::shared_ptr rlist (new RegionList); for (auto & r : regions) { - if (r->coverage (start, end) != Temporal::OverlapNone) { + if (r->coverage (start, end, with_tail) != Temporal::OverlapNone) { rlist->push_back (r); } } diff --git a/libs/ardour/plugin.cc b/libs/ardour/plugin.cc index 52ceb21dc3..06e442d91a 100644 --- a/libs/ardour/plugin.cc +++ b/libs/ardour/plugin.cc @@ -96,7 +96,7 @@ Plugin::Plugin (AudioEngine& e, Session& s) , _parameter_changed_since_last_preset (false) , _immediate_events(6096) // FIXME: size? , _resolve_midi (false) - , _pi (0) + , _pib (0) , _num (0) { _pending_stop_events.ensure_buffers (DataType::MIDI, 1, 4096); @@ -118,7 +118,7 @@ Plugin::Plugin (const Plugin& other) , _parameter_changed_since_last_preset (false) , _immediate_events(6096) // FIXME: size? , _resolve_midi (false) - , _pi (other._pi) + , _pib (other._pib) , _num (other._num) { _pending_stop_events.ensure_buffers (DataType::MIDI, 1, 4096); @@ -317,6 +317,12 @@ Plugin::input_streams () const return ChanCount::ZERO; } +samplecnt_t +Plugin::plugin_tailtime () const +{ + return _session.sample_rate () * Config->get_tail_duration_sec (); +} + Plugin::IOPortDescription Plugin::describe_io_port (ARDOUR::DataType dt, bool input, uint32_t id) const { diff --git a/libs/ardour/port_manager.cc b/libs/ardour/port_manager.cc index 37308ed53f..625006dcb3 100644 --- a/libs/ardour/port_manager.cc +++ b/libs/ardour/port_manager.cc @@ -1160,6 +1160,9 @@ PortManager::update_input_ports (bool clear) * do this when called from ::reestablish_ports() * "JACK: Cannot connect ports owned by inactive clients" */ + /* .. but take the opportunity to clear out dead wood */ + _audio_input_ports.flush (); + _midi_input_ports.flush (); return; } diff --git a/libs/ardour/region.cc b/libs/ardour/region.cc index 6393eac02e..eced85fec7 100644 --- a/libs/ardour/region.cc +++ b/libs/ardour/region.cc @@ -296,6 +296,7 @@ Region::Region (Session& s, timepos_t const & start, timecnt_t const & length, c : SessionObject(s, name) , _type (type) , _fx_latency (0) + , _fx_tail (0) , REGION_DEFAULT_STATE (start,length) , _last_length (length) , _first_edit (EditChangesNothing) @@ -312,6 +313,7 @@ Region::Region (const SourceList& srcs) : SessionObject(srcs.front()->session(), "toBeRenamed") , _type (srcs.front()->type()) , _fx_latency (0) + , _fx_tail (0) , REGION_DEFAULT_STATE(_type == DataType::MIDI ? timepos_t (Temporal::Beats()) : timepos_t::from_superclock (0), _type == DataType::MIDI ? timecnt_t (Temporal::Beats()) : timecnt_t::from_superclock (0)) , _last_length (_type == DataType::MIDI ? timecnt_t (Temporal::Beats()) : timecnt_t::from_superclock (0)) @@ -332,6 +334,7 @@ Region::Region (std::shared_ptr other) : SessionObject(other->session(), other->name()) , _type (other->data_type()) , _fx_latency (0) + , _fx_tail (0) , REGION_COPY_STATE (other) , _last_length (other->_last_length) , _first_edit (EditChangesNothing) @@ -391,6 +394,7 @@ Region::Region (std::shared_ptr other, timecnt_t const & offset) : SessionObject(other->session(), other->name()) , _type (other->data_type()) , _fx_latency (0) + , _fx_tail (0) , REGION_COPY_STATE (other) , _last_length (other->_last_length) , _first_edit (EditChangesNothing) @@ -437,6 +441,7 @@ Region::Region (std::shared_ptr other, const SourceList& srcs) : SessionObject (other->session(), other->name()) , _type (srcs.front()->type()) , _fx_latency (0) + , _fx_tail (0) , REGION_COPY_STATE (other) , _last_length (other->_last_length) , _first_edit (EditChangesID) @@ -1574,12 +1579,20 @@ Region::_set_state (const XMLNode& node, int version, PropertyChange& what_chang Glib::Threads::RWLock::WriterLock lm (_fx_lock); bool changed = !_plugins.empty (); + for (auto const& rfx : _plugins) { + rfx->drop_references (); + } + _plugins.clear (); for (auto const& child : node.children ()) { if (child->name() == X_("RegionFXPlugin")) { std::shared_ptr rfx (new RegionFxPlugin (_session, time_domain ())); - rfx->set_state (*child, version); + if (rfx->set_state (*child, version)) { + PBD::warning << string_compose (_("Failed to load RegionFx Plugin for region `%1'"), name()) << endmsg; + // TODO replace w/stub, retain config + continue; + } if (!_add_plugin (rfx, std::shared_ptr(), true)) { continue; } @@ -1587,8 +1600,10 @@ Region::_set_state (const XMLNode& node, int version, PropertyChange& what_chang changed = true; } } + lm.release (); if (changed) { fx_latency_changed (true); + fx_tail_changed (true); send_change (PropertyChange (Properties::region_fx)); // trigger DiskReader overwrite RegionFxChanged (); /* EMIT SIGNAL */ } @@ -2408,7 +2423,11 @@ Region::load_plugin (ARDOUR::PluginType type, std::string const& name) bool Region::add_plugin (std::shared_ptr rfx, std::shared_ptr pos) { - return _add_plugin (rfx, pos, false); + bool rv = _add_plugin (rfx, pos, false); + if (rv) { + _session.set_dirty (); + } + return rv; } void @@ -2435,6 +2454,7 @@ Region::reorder_plugins (RegionFxList const& new_order) oiter = _plugins.erase (oiter); } _plugins.insert (oiter, as_it_will_be.begin (), as_it_will_be.end ()); + _session.set_dirty (); } void @@ -2449,3 +2469,16 @@ Region::fx_latency_changed (bool) } _fx_latency = l; } + +void +Region::fx_tail_changed (bool) +{ + uint32_t t = 0; + for (auto const& rfx : _plugins) { + t = max (t, rfx->effective_tailtime ()); + } + if (t == _fx_tail) { + return; + } + _fx_tail = t; +} diff --git a/libs/ardour/region_fx_plugin.cc b/libs/ardour/region_fx_plugin.cc index ed5ec43f9d..b0547e94d5 100644 --- a/libs/ardour/region_fx_plugin.cc +++ b/libs/ardour/region_fx_plugin.cc @@ -37,18 +37,189 @@ using namespace std; using namespace ARDOUR; using namespace PBD; +class TimedReadOnlyControl : public ReadOnlyControl { +public: + TimedReadOnlyControl (std::shared_ptr p, const ParameterDescriptor& desc, uint32_t pnum) + : ReadOnlyControl (p, desc, pnum) + , _flush (false) + {} + + double get_parameter () const { + std::shared_ptr p = _plugin.lock(); + + if (!p) { + return 0; + } + samplepos_t when = p->session().audible_sample (); + + Glib::Threads::Mutex::Lock lm (_history_mutex); + auto it = _history.lower_bound (when); + if (it != _history.begin ()) { + --it; + } + if (it == _history.end ()) { + return p->get_parameter (_parameter_num); + } else { + return it->second; + } + } + + void flush () { + _flush = true; + } + + void store_value (samplepos_t start, samplepos_t end) { + std::shared_ptr p = _plugin.lock(); + if (!p) { + return; + } + double value = p->get_parameter (_parameter_num); + Glib::Threads::Mutex::Lock lm (_history_mutex); + if (_flush) { + _flush = false; + _history.clear (); + } + auto it = _history.lower_bound (start); + if (it != _history.begin ()) { + --it; + if (it->second == value) { + return; + } + assert (start > it->first); + if (start - it->first < 512) { + return; + } + } + _history[start] = value; + + if (_history.size () > 2000 && std::distance (_history.begin(), it) > 1500) { + samplepos_t when = min (start, p->session().audible_sample ()); + auto io = _history.lower_bound (when - p->session().sample_rate ()); + if (std::distance (io, it) > 1) { + _history.erase (_history.begin(), io); + } + } + } + +private: + std::map _history; + mutable Glib::Threads::Mutex _history_mutex; + bool _flush; +}; + +class TimedPluginControl : public PlugInsertBase::PluginControl +{ +public: + TimedPluginControl (Session& s, + PlugInsertBase* p, + const Evoral::Parameter& param, + const ParameterDescriptor& desc, + std::shared_ptr list, + bool replay_param) + : PlugInsertBase::PluginControl (s, p, param, desc, list) + , _last_value (desc.lower - 1) + , _replay_param (replay_param) + , _flush (false) + { + } + + double get_value (void) const + { + samplepos_t when = _session.audible_sample (); + Glib::Threads::Mutex::Lock lm (_history_mutex); + auto it = _history.lower_bound (when); + if (it != _history.begin ()) { + --it; + } + if (it == _history.end ()) { + return PlugInsertBase::PluginControl::get_value (); + } else { + return it->second; + } + } + + bool maybe_emit_changed () + { + double current = get_value (); + if (current == _last_value) { + return false; + } + _last_value = current; + if (_replay_param) { // AU, VST2 + /* this is only called for automated parameters. + * Next call to ::run() will set the actual value before + * running the plugin (via automation_run). + */ + actually_set_value (current, PBD::Controllable::NoGroup); + } else { // generic UI, LV2 + Changed (true, Controllable::NoGroup); + } + return true; + } + + void flush () { + if (automation_playback ()) { + _flush = true; + } else { + Glib::Threads::Mutex::Lock lm (_history_mutex); + _history.clear (); + } + } + + void store_value (samplepos_t start, samplepos_t end) + { + double value = PlugInsertBase::PluginControl::get_value (); + Glib::Threads::Mutex::Lock lm (_history_mutex); + if (_flush) { + _flush = false; + _history.clear (); + } + auto it = _history.lower_bound (start); + if (it != _history.begin ()) { + --it; + if (it->second == value) { + return; + } + assert (start > it->first); + if (start - it->first < 512) { + return; + } + } + _history[start] = value; + + /* do not accumulate */ + if (_history.size () > 2000 && std::distance (_history.begin(), it) > 1500) { + samplepos_t when = min (start, _session.audible_sample ()); + auto io = _history.lower_bound (when - _session.sample_rate ()); + if (std::distance (io, it) > 1) { + _history.erase (_history.begin(), io); + } + } + } + +private: + std::map _history; + mutable Glib::Threads::Mutex _history_mutex; + double _last_value; + bool _replay_param; + bool _flush; +}; + RegionFxPlugin::RegionFxPlugin (Session& s, Temporal::TimeDomain const td, std::shared_ptr plug) : SessionObject (s, (plug ? plug->name () : string ("toBeRenamed"))) , TimeDomainProvider (td) , _plugin_signal_latency (0) , _configured (false) , _no_inplace (false) + , _last_emit (0) , _window_proxy (0) + , _state (0) { _flush.store (0); if (plug) { add_plugin (plug); + plug->activate (); create_parameters (); } } @@ -64,14 +235,22 @@ RegionFxPlugin::~RegionFxPlugin () std::dynamic_pointer_cast(i.second)->drop_references (); } _controls.clear (); + + delete _state; } XMLNode& RegionFxPlugin::get_state () const { + if (_plugins.empty ()) { + assert (_state); + return *(new XMLNode (*_state)); + } + XMLNode* node = new XMLNode (/*state_node_name*/ "RegionFXPlugin"); Latent::add_state (node); + TailTime::add_state (node); node->set_property ("type", _plugins[0]->state_node_name ()); node->set_property ("unique-id", _plugins[0]->unique_id ()); @@ -124,7 +303,15 @@ RegionFxPlugin::set_state (const XMLNode& node, int version) std::shared_ptr plugin = find_and_load_plugin (_session, node, type, unique_id, any_vst); if (!plugin) { - return -1; + delete _state; + _state = new XMLNode (node); + string name; + if (node.get_property ("name", name)) { + set_name (name); + } else { + set_name ("Unknown Plugin"); + } + return 0; } add_plugin (plugin); @@ -197,9 +384,29 @@ RegionFxPlugin::set_state (const XMLNode& node, int version) ac->Changed (false, Controllable::NoGroup); /* EMIT SIGNAL */ } } + + Latent::set_state (node, version); + TailTime::set_state (node, version); + return 0; } +PluginType +RegionFxPlugin::type () const +{ + if (!_plugins.empty ()) { + return plugin ()->get_info ()->type; + } + if (_state) { + ARDOUR::PluginType type; + std::string unique_id; + if (parse_plugin_type (*_state, type, unique_id)) { + return type; + } + } + return LXVST; /* whatever */ +} + void RegionFxPlugin::update_id (PBD::ID id) { @@ -222,6 +429,8 @@ RegionFxPlugin::add_plugin (std::shared_ptr plugin) plugin->EndTouch.connect_same_thread (*this, boost::bind (&RegionFxPlugin::end_touch, this, _1)); } + plugin->set_insert (this, _plugins.size ()); + _plugins.push_back (plugin); if (_plugins.size () > 1) { @@ -291,9 +500,21 @@ RegionFxPlugin::drop_references () ARDOUR::samplecnt_t RegionFxPlugin::signal_latency () const { + if (_plugins.empty ()) { + return 0; + } return _plugins.front ()->signal_latency (); } +ARDOUR::samplecnt_t +RegionFxPlugin::signal_tailtime () const +{ + if (_plugins.empty ()) { + return 0; + } + return _plugins.front ()->signal_tailtime (); +} + PlugInsertBase::UIElements RegionFxPlugin::ui_elements () const { @@ -308,6 +529,18 @@ RegionFxPlugin::create_parameters () std::shared_ptr plugin = _plugins.front (); set a = _plugins.front ()->automatable (); + bool replay_param = false; + switch (_plugins.front ()->get_info ()->type) { + case AudioUnit: + case LXVST: + case MacVST: + case Windows_VST: + replay_param = true; + break; + default: + break; + } + for (uint32_t i = 0; i < plugin->parameter_count (); ++i) { if (!plugin->parameter_is_control (i)) { continue; @@ -317,7 +550,7 @@ RegionFxPlugin::create_parameters () plugin->get_parameter_descriptor (i, desc); if (!plugin->parameter_is_input (i)) { - _control_outputs[i] = std::shared_ptr (new ReadOnlyControl (plugin, desc, i)); + _control_outputs[i] = std::shared_ptr (new TimedReadOnlyControl (plugin, desc, i)); continue; } @@ -325,7 +558,7 @@ RegionFxPlugin::create_parameters () const bool automatable = a.find(param) != a.end(); std::shared_ptr list (new AutomationList (param, desc, *this)); - std::shared_ptr c (new PluginControl (_session, this, param, desc, list)); + std::shared_ptr c (new TimedPluginControl (_session, this, param, desc, list, replay_param)); if (!automatable) { c->set_flag (Controllable::NotAutomatable); } @@ -448,6 +681,7 @@ RegionFxPlugin::parameter_changed_externally (uint32_t which, float val) std::string RegionFxPlugin::describe_parameter (Evoral::Parameter param) { + assert (!_plugins.empty ()); if (param.type () == PluginAutomation) { return _plugins[0]->describe_parameter (param); } else if (param.type () == PluginPropertyAutomation) { @@ -462,7 +696,6 @@ RegionFxPlugin::describe_parameter (Evoral::Parameter param) void RegionFxPlugin::start_touch (uint32_t param_id) { - assert (0); // touch is N/A std::shared_ptr ac = std::dynamic_pointer_cast (control (Evoral::Parameter (PluginAutomation, 0, param_id))); if (ac) { ac->start_touch (timepos_t (_session.audible_sample ())); // XXX subtract region position @@ -472,7 +705,6 @@ RegionFxPlugin::start_touch (uint32_t param_id) void RegionFxPlugin::end_touch (uint32_t param_id) { - assert (0); // touch is N/A std::shared_ptr ac = std::dynamic_pointer_cast (control (Evoral::Parameter (PluginAutomation, 0, param_id))); if (ac) { ac->stop_touch (timepos_t (_session.audible_sample ())); // XXX subtract region position @@ -482,6 +714,10 @@ RegionFxPlugin::end_touch (uint32_t param_id) bool RegionFxPlugin::can_reset_all_parameters () { + if (_plugins.empty ()) { + return false; + } + bool all = true; uint32_t params = 0; std::shared_ptr plugin = _plugins.front (); @@ -511,6 +747,8 @@ RegionFxPlugin::can_reset_all_parameters () bool RegionFxPlugin::reset_parameters_to_default () { + assert (!_plugins.empty ()); + bool all = true; std::shared_ptr plugin = _plugins.front (); @@ -548,11 +786,24 @@ void RegionFxPlugin::flush () { _flush.store (1); + + for (auto const& i : _control_outputs) { + shared_ptr toc = std::dynamic_pointer_cast(i.second); + toc->flush (); + } + for (auto const& i : _controls) { + shared_ptr tpc = std::dynamic_pointer_cast(i.second); + tpc->flush (); + } } bool RegionFxPlugin::can_support_io_configuration (const ChanCount& in, ChanCount& out) { + if (_plugins.empty ()) { + out = ChanCount::min (in, out); + return true; + } return private_can_support_io_configuration (in, out).method != Impossible; } @@ -706,6 +957,10 @@ RegionFxPlugin::configure_io (ChanCount in, ChanCount out) _configured_in = in; _configured_out = out; + if (_plugins.empty ()) { + return true; + } + ChanCount natural_input_streams = _plugins[0]->get_info ()->n_inputs; ChanCount natural_output_streams = _plugins[0]->get_info ()->n_outputs; @@ -733,10 +988,13 @@ RegionFxPlugin::configure_io (ChanCount in, ChanCount out) if (_plugins.front ()->reconfigure_io (din, daux, dout) == false) { return false; } - DEBUG_TRACE (DEBUG::RegionFx, string_compose ("Delegate configured in: %1, out: %2 for in: %3 out: %4", din, dout, in, _configured_out)); + DEBUG_TRACE (DEBUG::RegionFx, string_compose ("Delegate configured in: %1, out: %2 for in: %3 out: %4\n", din, dout, in, _configured_out)); if (din < in || dout < _configured_out) { return false; } + /* update after match_variable_io sets info */ + natural_input_streams = _plugins[0]->get_info ()->n_inputs; + natural_output_streams = _plugins[0]->get_info ()->n_outputs; } break; case Replicate: @@ -830,6 +1088,7 @@ RegionFxPlugin::configure_io (ChanCount in, ChanCount out) DEBUG_STR_APPEND(a, _out_map[pc]); } DEBUG_STR_APPEND(a, "-------->>--------\n"); + DEBUG_TRACE (DEBUG::RegionFx, DEBUG_STR(a).str()); } #endif @@ -989,6 +1248,12 @@ RegionFxPlugin::find_next_event (timepos_t const& start, timepos_t const& end, E bool RegionFxPlugin::run (BufferSet& bufs, samplepos_t start, samplepos_t end, samplepos_t pos, pframes_t nframes, sampleoffset_t off) { + if (_plugins.empty ()) { + return true; + } + + Glib::Threads::Mutex::Lock lp (_process_lock); + int canderef (1); if (_flush.compare_exchange_strong (canderef, 0)) { for (auto const& i : _plugins) { @@ -1162,10 +1427,47 @@ RegionFxPlugin::connect_and_run (BufferSet& bufs, samplepos_t start, samplepos_t } } + for (auto const& i : _control_outputs) { + shared_ptr toc = std::dynamic_pointer_cast(i.second); + toc->store_value (start + pos, end + pos); + } + + for (auto const& i : _controls) { + shared_ptr tpc = std::dynamic_pointer_cast(i.second); + if (tpc->automation_playback ()) { + tpc->store_value (start + pos, end + pos); + } + } + const samplecnt_t l = effective_latency (); if (_plugin_signal_latency != l) { _plugin_signal_latency= l; LatencyChanged (); /* EMIT SIGNAL */ } + const samplecnt_t t = effective_latency (); + if (_plugin_signal_tailtime != l) { + _plugin_signal_tailtime = t; + TailTimeChanged (); /* EMIT SIGNAL */ + } return true; } + +void +RegionFxPlugin::maybe_emit_changed_signals () const +{ + if (!_session.transport_rolling ()) { + samplepos_t when = _session.audible_sample (); + if (_last_emit == when) { + return; + } + _last_emit = when; + } + + Glib::Threads::Mutex::Lock lp (_process_lock); + for (auto const& i : _controls) { + shared_ptr tpc = std::dynamic_pointer_cast(i.second); + if (tpc->automation_playback ()) { + tpc->maybe_emit_changed (); + } + } +} diff --git a/libs/ardour/route.cc b/libs/ardour/route.cc index 3d5c614ddd..bd76093790 100644 --- a/libs/ardour/route.cc +++ b/libs/ardour/route.cc @@ -400,31 +400,48 @@ Route::process_output_buffers (BufferSet& bufs, return; } - /* We should offset the route-owned ctrls by the given latency, however - * this only affects Mute. Other route-owned controls (solo, polarity..) - * are not automatable. - * - * Mute has its own issues since there's not a single mute-point, - * but in general - */ - automation_run (start_sample, nframes); - if (_pannable) { - _pannable->automation_run (start_sample + _signal_latency, nframes); + /* this is only for the benfit of updating the UI. + * + * Panner's `::distribute_one_automated()` evalualte + * a sample-accurate curve using start/end of the + * delivery processor. + */ + _pannable->automation_run (start_sample, nframes); } + const int speed = (is_auditioner() ? 1 : _session.transport_speed ()); + assert (speed == -1 || speed == 0 || speed == 1); + + const samplecnt_t output_latency = speed * _output_latency; + const samplecnt_t latency_offset = speed * (_signal_latency + _output_latency); + + /* Mute is the only actual route-owned control (solo, solo safe, polarity + * are not automatable). + * + * Here we offset mute automation to align to output/master bus + * to be consistent with the fader. This applied to the + * "Main outs" mute point. + * + * Other mute points in the middle of signal flow flow + * will not be handled correctly. That would mean to add + * _signal_latency - accumulated processor effective_latency() at mute mute + */ + + automation_run (start_sample + output_latency, nframes); + /* figure out if we're going to use gain automation */ if (gain_automation_ok) { _amp->set_gain_automation_buffer (_session.gain_automation_buffer ()); _amp->setup_gain_automation ( - start_sample + _amp->output_latency (), - end_sample + _amp->output_latency (), + start_sample + _amp->output_latency () + output_latency, + end_sample + _amp->output_latency () + output_latency, nframes); _trim->set_gain_automation_buffer (_session.trim_automation_buffer ()); _trim->setup_gain_automation ( - start_sample + _trim->output_latency (), - end_sample + _trim->output_latency (), + start_sample + _trim->output_latency () + output_latency, + end_sample + _trim->output_latency () + output_latency, nframes); } @@ -438,18 +455,8 @@ Route::process_output_buffers (BufferSet& bufs, * -> at Time T= -15, the disk-reader reads sample T=0. * By the Time T=0 is reached (dt=15 later) that sample is audible. */ - - const double speed = (is_auditioner() ? 1.0 : _session.transport_speed ()); - - const sampleoffset_t latency_offset = _signal_latency + _output_latency; - if (speed < 0) { - /* when rolling backwards this can become negative */ - start_sample -= latency_offset; - end_sample -= latency_offset; - } else { - start_sample += latency_offset; - end_sample += latency_offset; - } + start_sample += latency_offset; + end_sample += latency_offset; /* Note: during initial pre-roll 'start_sample' as passed as argument can be negative. * Functions calling process_output_buffers() will set "run_disk_reader" diff --git a/libs/ardour/session.cc b/libs/ardour/session.cc index 299de6f949..d8a0313c42 100644 --- a/libs/ardour/session.cc +++ b/libs/ardour/session.cc @@ -44,8 +44,6 @@ #include #include -#include - #include "pbd/atomic.h" #include "pbd/basename.h" #include "pbd/convert.h" @@ -5574,6 +5572,7 @@ Session::unload_io_plugin (std::shared_ptr ioplugin) } IOPluginsChanged (); /* EMIT SIGNAL */ set_dirty(); + _io_plugins.flush (); return true; } diff --git a/libs/ardour/session_bundles.cc b/libs/ardour/session_bundles.cc index e0bd838460..558c63716c 100644 --- a/libs/ardour/session_bundles.cc +++ b/libs/ardour/session_bundles.cc @@ -75,6 +75,7 @@ Session::remove_bundle (std::shared_ptr bundle) if (removed) { BundleAddedOrRemoved (); /* EMIT SIGNAL */ + _bundles.flush (); } set_dirty(); diff --git a/libs/ardour/session_export.cc b/libs/ardour/session_export.cc index 2310a83002..0e2168c69c 100644 --- a/libs/ardour/session_export.cc +++ b/libs/ardour/session_export.cc @@ -173,6 +173,8 @@ Session::start_audio_export (samplepos_t position, bool realtime, bool region_ex /* get everyone to the right position */ std::shared_ptr rl = routes.reader(); + ARDOUR::ProcessThread* pt = new ProcessThread (); + pt->get_buffers (); for (auto const& i : *rl) { std::shared_ptr tr = std::dynamic_pointer_cast (i); @@ -182,6 +184,8 @@ Session::start_audio_export (samplepos_t position, bool realtime, bool region_ex return -1; } } + pt->drop_buffers (); + delete pt; } /* we just did the core part of a locate call above, but diff --git a/libs/ardour/session_process.cc b/libs/ardour/session_process.cc index 33b188161c..9d2116192e 100644 --- a/libs/ardour/session_process.cc +++ b/libs/ardour/session_process.cc @@ -26,8 +26,6 @@ #include #include -#include - #include "pbd/i18n.h" #include "pbd/error.h" #include "pbd/enumwriter.h" @@ -1258,6 +1256,12 @@ Session::plan_master_strategy_engine (pframes_t nframes, double master_speed, sa DEBUG_TRACE (DEBUG::Slave, "JACK transport: not moving\n"); + if (!transport_stopped_or_stopping()) { + DEBUG_TRACE (DEBUG::Slave, "JACK Transport: jack is stopped, we are not, so stop ...\n"); + TFSM_STOP (false, false); + return 1.0; + } + const samplecnt_t wlp = worst_latency_preroll_buffer_size_ceil (); if (delta != wlp) { diff --git a/libs/ardour/session_state.cc b/libs/ardour/session_state.cc index 59edb7b767..b8120b3427 100644 --- a/libs/ardour/session_state.cc +++ b/libs/ardour/session_state.cc @@ -425,10 +425,10 @@ Session::post_engine_init () void Session::session_loaded () { - SessionLoaded(); - set_clean (); + SessionLoaded(); + if (_is_new) { save_state (""); } diff --git a/libs/ardour/session_transport.cc b/libs/ardour/session_transport.cc index 6a345cb221..fcbd280ff1 100644 --- a/libs/ardour/session_transport.cc +++ b/libs/ardour/session_transport.cc @@ -32,8 +32,6 @@ #include #include -#include - #include "pbd/atomic.h" #include "pbd/error.h" #include "pbd/enumwriter.h" @@ -1728,7 +1726,7 @@ Session::worst_latency_preroll () const samplecnt_t Session::worst_latency_preroll_buffer_size_ceil () const { - return lrintf (ceil ((_worst_output_latency + _worst_input_latency) / (float) current_block_size) * current_block_size); + return lrintf (ceil ((_worst_output_latency + max (_worst_route_latency, _worst_input_latency)) / (float) current_block_size) * current_block_size); } void diff --git a/libs/ardour/session_vst.cc b/libs/ardour/session_vst.cc index 2374ccf4d9..349b836ca6 100644 --- a/libs/ardour/session_vst.cc +++ b/libs/ardour/session_vst.cc @@ -33,7 +33,7 @@ #include "ardour/debug.h" #include "ardour/session.h" #include "ardour/tempo.h" -#include "ardour/plugin_insert.h" +#include "ardour/plug_insert_base.h" #include "ardour/windows_vst_plugin.h" #include "ardour/vestige/vestige.h" #include "ardour/vst_types.h" @@ -529,7 +529,7 @@ intptr_t Session::vst_callback ( SHOW_CALLBACK ("audioMasterBeginEdit"); // begin of automation session (when mouse down), parameter index in if (plug && plug->plugin_insert ()) { - std::shared_ptr ac = plug->plugin_insert ()->automation_control (Evoral::Parameter (PluginAutomation, 0, index)); + std::shared_ptr ac = std::dynamic_pointer_cast(plug->plugin_insert ()->control (Evoral::Parameter (PluginAutomation, 0, index))); if (ac) { ac->start_touch (timepos_t (ac->session().transport_sample())); } @@ -540,7 +540,7 @@ intptr_t Session::vst_callback ( SHOW_CALLBACK ("audioMasterEndEdit"); // end of automation session (when mouse up), parameter index in if (plug && plug->plugin_insert ()) { - std::shared_ptr ac = plug->plugin_insert ()->automation_control (Evoral::Parameter (PluginAutomation, 0, index)); + std::shared_ptr ac = std::dynamic_pointer_cast(plug->plugin_insert ()->control (Evoral::Parameter (PluginAutomation, 0, index))); if (ac) { ac->stop_touch (timepos_t (ac->session().transport_sample())); } diff --git a/libs/ardour/tailtime.cc b/libs/ardour/tailtime.cc new file mode 100644 index 0000000000..fca81206d2 --- /dev/null +++ b/libs/ardour/tailtime.cc @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2024 Robin Gareus + * + * 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 "pbd/xml++.h" + +#include "ardour/tailtime.h" +#include "ardour/rc_configuration.h" + +using namespace ARDOUR; + +TailTime::TailTime () + : HasTailTime () + , _use_user_tailtime (false) + , _user_tailtime (0) +{} + +TailTime::TailTime (TailTime const& other) + : HasTailTime () + , _use_user_tailtime (other._use_user_tailtime) + , _user_tailtime (other._user_tailtime) +{} + +samplecnt_t +TailTime::effective_tailtime () const +{ + if (_use_user_tailtime) { + return _user_tailtime; + } else { + return std::max (0, std::min (signal_tailtime (), Config->get_max_tail_samples ())); + } +} + +void +TailTime::set_user_tailtime (samplecnt_t val) +{ + if (_use_user_tailtime && _user_tailtime == val) { + return; + } + _use_user_tailtime = true; + _user_tailtime = val; + TailTimeChanged (); /* EMIT SIGNAL */ +} + +void +TailTime::unset_user_tailtime () +{ + if (!_use_user_tailtime) { + return; + } + _use_user_tailtime = false; + _user_tailtime = 0; + TailTimeChanged (); /* EMIT SIGNAL */ +} + + + +int +TailTime::set_state (const XMLNode& node, int version) +{ + node.get_property ("user-tailtime", _user_tailtime); + if (!node.get_property ("use-user-tailtime", _use_user_tailtime)) { + _use_user_tailtime = _user_tailtime > 0; + } + return 0; +} + +void +TailTime::add_state (XMLNode* node) const +{ + node->set_property ("user-tailtime", _user_tailtime); + node->set_property ("use-user-tailtime", _use_user_tailtime); +} diff --git a/libs/ardour/vst3_host.cc b/libs/ardour/vst3_host.cc index 0ba39bc82f..f690d95a09 100644 --- a/libs/ardour/vst3_host.cc +++ b/libs/ardour/vst3_host.cc @@ -29,6 +29,11 @@ #include "pbd/compose.h" #endif +#if SMTG_OS_LINUX +#include +#include +#endif + using namespace Steinberg; DEF_CLASS_IID (FUnknown) @@ -77,6 +82,143 @@ DEF_CLASS_IID (Presonus::IPlugInViewScaling) #if SMTG_OS_LINUX DEF_CLASS_IID (Linux::IRunLoop); + +class AVST3Runloop : public Linux::IRunLoop +{ +private: + struct EventHandler + { + EventHandler (Linux::IEventHandler* handler = 0, GIOChannel* gio_channel = 0, guint source_id = 0) + : _handler (handler) + , _gio_channel (gio_channel) + , _source_id (source_id) + {} + + bool operator== (EventHandler const& other) { + return other._handler == _handler && other._gio_channel == _gio_channel && other._source_id == _source_id; + } + Linux::IEventHandler* _handler; + GIOChannel* _gio_channel; + guint _source_id; + }; + + boost::unordered_map _event_handlers; + boost::unordered_map _timer_handlers; + + static gboolean event (GIOChannel* source, GIOCondition condition, gpointer data) + { + Linux::IEventHandler* handler = reinterpret_cast (data); + handler->onFDIsSet (g_io_channel_unix_get_fd (source)); + if (condition & ~G_IO_IN) { + /* remove on error */ + return false; + } else { + return true; + } + } + + static gboolean timeout (gpointer data) + { + Linux::ITimerHandler* handler = reinterpret_cast (data); + handler->onTimer (); + return true; + } + +public: + ~AVST3Runloop () + { + clear (); + } + + void clear () { + Glib::Threads::Mutex::Lock lm (_lock); + for (boost::unordered_map::const_iterator it = _event_handlers.begin (); it != _event_handlers.end (); ++it) { + g_source_remove (it->second._source_id); + g_io_channel_unref (it->second._gio_channel); + } + for (boost::unordered_map::const_iterator it = _timer_handlers.begin (); it != _timer_handlers.end (); ++it) { + g_source_remove (it->first); + } + _event_handlers.clear (); + _timer_handlers.clear (); + } + + /* VST3 IRunLoop interface */ + tresult registerEventHandler (Linux::IEventHandler* handler, FileDescriptor fd) SMTG_OVERRIDE + { + if (!handler || _event_handlers.find(fd) != _event_handlers.end()) { + return kInvalidArgument; + } + + Glib::Threads::Mutex::Lock lm (_lock); + GIOChannel* gio_channel = g_io_channel_unix_new (fd); + guint id = g_io_add_watch (gio_channel, (GIOCondition) (G_IO_IN /*| G_IO_OUT*/ | G_IO_ERR | G_IO_HUP), event, handler); + _event_handlers[fd] = EventHandler (handler, gio_channel, id); + return kResultTrue; + } + + tresult unregisterEventHandler (Linux::IEventHandler* handler) SMTG_OVERRIDE + { + if (!handler) { + return kInvalidArgument; + } + + tresult rv = false; + Glib::Threads::Mutex::Lock lm (_lock); + for (boost::unordered_map::const_iterator it = _event_handlers.begin (); it != _event_handlers.end ();) { + if (it->second._handler == handler) { + g_source_remove (it->second._source_id); + g_io_channel_unref (it->second._gio_channel); + it = _event_handlers.erase (it); + rv = kResultTrue; + } else { + ++it; + } + } + return rv; + } + + tresult registerTimer (Linux::ITimerHandler* handler, TimerInterval milliseconds) SMTG_OVERRIDE + { + if (!handler || milliseconds == 0) { + return kInvalidArgument; + } + Glib::Threads::Mutex::Lock lm (_lock); + guint id = g_timeout_add_full (G_PRIORITY_HIGH_IDLE, milliseconds, timeout, handler, NULL); + _timer_handlers[id] = handler; + return kResultTrue; + + } + + tresult unregisterTimer (Linux::ITimerHandler* handler) SMTG_OVERRIDE + { + if (!handler) { + return kInvalidArgument; + } + + tresult rv = false; + Glib::Threads::Mutex::Lock lm (_lock); + for (boost::unordered_map::const_iterator it = _timer_handlers.begin (); it != _timer_handlers.end ();) { + if (it->second == handler) { + g_source_remove (it->first); + it = _timer_handlers.erase (it); + rv = kResultTrue; + } else { + ++it; + } + } + return rv; + } + + uint32 PLUGIN_API addRef () SMTG_OVERRIDE { return 1; } + uint32 PLUGIN_API release () SMTG_OVERRIDE { return 1; } + tresult queryInterface (const TUID, void**) SMTG_OVERRIDE { return kNoInterface; } + +private: + Glib::Threads::Mutex _lock; +}; + +AVST3Runloop static_runloop; #endif std::string @@ -467,6 +609,13 @@ HostApplication::queryInterface (const char* _iid, void** obj) QUERY_INTERFACE (_iid, obj, FUnknown::iid, IHostApplication) QUERY_INTERFACE (_iid, obj, IHostApplication::iid, IHostApplication) +#if SMTG_OS_LINUX + if (FUnknownPrivate::iidEqual (_iid, Linux::IRunLoop::iid)) { + *obj = &static_runloop; + return kResultOk; + } +#endif + if (_plug_interface_support && _plug_interface_support->queryInterface (_iid, obj) == kResultTrue) { return kResultOk; } diff --git a/libs/ardour/vst3_plugin.cc b/libs/ardour/vst3_plugin.cc index a7629b185c..27c5f691a5 100644 --- a/libs/ardour/vst3_plugin.cc +++ b/libs/ardour/vst3_plugin.cc @@ -21,8 +21,6 @@ #include "pbd/gstdio_compat.h" #include -#include - #include "pbd/basename.h" #include "pbd/compose.h" #include "pbd/convert.h" @@ -58,146 +56,6 @@ using namespace Temporal; using namespace Steinberg; using namespace Presonus; -#if SMTG_OS_LINUX -class AVST3Runloop : public Linux::IRunLoop -{ -private: - struct EventHandler - { - EventHandler (Linux::IEventHandler* handler = 0, GIOChannel* gio_channel = 0, guint source_id = 0) - : _handler (handler) - , _gio_channel (gio_channel) - , _source_id (source_id) - {} - - bool operator== (EventHandler const& other) { - return other._handler == _handler && other._gio_channel == _gio_channel && other._source_id == _source_id; - } - Linux::IEventHandler* _handler; - GIOChannel* _gio_channel; - guint _source_id; - }; - - boost::unordered_map _event_handlers; - boost::unordered_map _timer_handlers; - - static gboolean event (GIOChannel* source, GIOCondition condition, gpointer data) - { - Linux::IEventHandler* handler = reinterpret_cast (data); - handler->onFDIsSet (g_io_channel_unix_get_fd (source)); - if (condition & ~G_IO_IN) { - /* remove on error */ - return false; - } else { - return true; - } - } - - static gboolean timeout (gpointer data) - { - Linux::ITimerHandler* handler = reinterpret_cast (data); - handler->onTimer (); - return true; - } - -public: - ~AVST3Runloop () - { - clear (); - } - - void clear () { - Glib::Threads::Mutex::Lock lm (_lock); - for (boost::unordered_map::const_iterator it = _event_handlers.begin (); it != _event_handlers.end (); ++it) { - g_source_remove (it->second._source_id); - g_io_channel_unref (it->second._gio_channel); - } - for (boost::unordered_map::const_iterator it = _timer_handlers.begin (); it != _timer_handlers.end (); ++it) { - g_source_remove (it->first); - } - _event_handlers.clear (); - _timer_handlers.clear (); - } - - /* VST3 IRunLoop interface */ - tresult registerEventHandler (Linux::IEventHandler* handler, FileDescriptor fd) SMTG_OVERRIDE - { - if (!handler || _event_handlers.find(fd) != _event_handlers.end()) { - return kInvalidArgument; - } - - Glib::Threads::Mutex::Lock lm (_lock); - GIOChannel* gio_channel = g_io_channel_unix_new (fd); - guint id = g_io_add_watch (gio_channel, (GIOCondition) (G_IO_IN /*| G_IO_OUT*/ | G_IO_ERR | G_IO_HUP), event, handler); - _event_handlers[fd] = EventHandler (handler, gio_channel, id); - return kResultTrue; - } - - tresult unregisterEventHandler (Linux::IEventHandler* handler) SMTG_OVERRIDE - { - if (!handler) { - return kInvalidArgument; - } - - tresult rv = false; - Glib::Threads::Mutex::Lock lm (_lock); - for (boost::unordered_map::const_iterator it = _event_handlers.begin (); it != _event_handlers.end ();) { - if (it->second._handler == handler) { - g_source_remove (it->second._source_id); - g_io_channel_unref (it->second._gio_channel); - it = _event_handlers.erase (it); - rv = kResultTrue; - } else { - ++it; - } - } - return rv; - } - - tresult registerTimer (Linux::ITimerHandler* handler, TimerInterval milliseconds) SMTG_OVERRIDE - { - if (!handler || milliseconds == 0) { - return kInvalidArgument; - } - Glib::Threads::Mutex::Lock lm (_lock); - guint id = g_timeout_add_full (G_PRIORITY_HIGH_IDLE, milliseconds, timeout, handler, NULL); - _timer_handlers[id] = handler; - return kResultTrue; - - } - - tresult unregisterTimer (Linux::ITimerHandler* handler) SMTG_OVERRIDE - { - if (!handler) { - return kInvalidArgument; - } - - tresult rv = false; - Glib::Threads::Mutex::Lock lm (_lock); - for (boost::unordered_map::const_iterator it = _timer_handlers.begin (); it != _timer_handlers.end ();) { - if (it->second == handler) { - g_source_remove (it->first); - it = _timer_handlers.erase (it); - rv = kResultTrue; - } else { - ++it; - } - } - return rv; - } - - uint32 PLUGIN_API addRef () SMTG_OVERRIDE { return 1; } - uint32 PLUGIN_API release () SMTG_OVERRIDE { return 1; } - tresult queryInterface (const TUID, void**) SMTG_OVERRIDE { return kNoInterface; } - -private: - Glib::Threads::Mutex _lock; -}; - -AVST3Runloop static_runloop; - -#endif - VST3Plugin::VST3Plugin (AudioEngine& engine, Session& session, VST3PI* plug) : Plugin (engine, session) , _plug (plug) @@ -819,6 +677,12 @@ VST3Plugin::set_block_size (pframes_t n_samples) return 0; } +samplecnt_t +VST3Plugin::plugin_tailtime () const +{ + return _plug->plugin_tailtime (); +} + samplecnt_t VST3Plugin::plugin_latency () const { @@ -1019,9 +883,10 @@ VST3Plugin::load_preset (PresetRecord r) return false; } - Glib::Threads::Mutex::Lock lx (_plug->process_lock ()); if (tmp[0] == "VST3-P") { + Glib::Threads::Mutex::Lock lx (_plug->process_lock ()); + PBD::Unwinder uw (_plug->component_is_synced (), true); int program = PBD::atoi (tmp[2]); assert (!r.user); if (!_plug->set_program (program, 0)) { @@ -1038,14 +903,14 @@ VST3Plugin::load_preset (PresetRecord r) std::string const& fn = _preset_uri_map[r.uri]; if (Glib::file_test (fn, Glib::FILE_TEST_EXISTS)) { + Glib::Threads::Mutex::Lock lx (_plug->process_lock ()); + PBD::Unwinder uw (_plug->component_is_synced (), true); RAMStream stream (fn); ok = _plug->load_state (stream); DEBUG_TRACE (DEBUG::VST3Config, string_compose ("VST3Plugin::load_preset: file %1 status %2\n", fn, ok ? "OK" : "error")); } } - lx.release (); - if (ok) { Plugin::load_preset (r); } @@ -1214,6 +1079,14 @@ VST3PluginInfo::load (Session& session) if (!m) { DEBUG_TRACE (DEBUG::VST3Config, string_compose ("VST3 Loading: %1\n", path)); m = VST3PluginModule::load (path); +#if SMTG_OS_LINUX + IPluginFactory* factory = m->factory (); + IPtr factory3 = FUnknownPtr (factory); + if (factory3) { + DEBUG_TRACE (DEBUG::VST3Config, "VST3 detected IPluginFactory3, setting Linux runloop host context\n"); + factory3->setHostContext ((FUnknown*) HostApplication::getHostContext ()); + } +#endif } PluginPtr plugin; Steinberg::VST3PI* plug = new VST3PI (m, unique_id); @@ -1418,14 +1291,6 @@ VST3PI::VST3PI (std::shared_ptr m, std::string unique_ throw failed_constructor (); } -#if SMTG_OS_LINUX - IPtr factory3 = FUnknownPtr (factory); - if (factory3) { - Vst::IComponentHandler* ctx = this; - factory3->setHostContext ((FUnknown*) ctx); - } -#endif - /* prepare process context */ memset (&_context, 0, sizeof (Vst::ProcessContext)); @@ -1654,8 +1519,7 @@ VST3PI::queryInterface (const TUID _iid, void** obj) #if SMTG_OS_LINUX if (FUnknownPrivate::iidEqual (_iid, Linux::IRunLoop::iid)) { - *obj = &static_runloop; - return kResultOk; + return HostApplication::getHostContext()->queryInterface (_iid, obj); } #endif @@ -1951,6 +1815,15 @@ VST3PI::plugin_latency () return _plugin_latency.value (); } +uint32_t +VST3PI::plugin_tailtime () +{ + if (!_plugin_tail) { // XXX this is currently never reset + _plugin_tail = _processor->getTailSamples (); + } + return _plugin_tail.value (); +} + void VST3PI::set_owner (SessionObject* o) { @@ -1970,7 +1843,11 @@ VST3PI::set_owner (SessionObject* o) void VST3PI::set_non_realtime (bool yn) { + if (_process_offline == yn) { + return; + } _process_offline = yn; + update_processor (); } int32 diff --git a/libs/ardour/vst_plugin.cc b/libs/ardour/vst_plugin.cc index 9887e74680..ce845071b2 100644 --- a/libs/ardour/vst_plugin.cc +++ b/libs/ardour/vst_plugin.cc @@ -460,7 +460,8 @@ VSTPlugin::load_plugin_preset (PresetRecord r) sscanf (r.uri.c_str(), "VST:%d:%d", &id, &index); #endif _state->want_program = index; - if (!has_editor () || 0 == plugin_insert ()->window_proxy ()) { + PluginInsert* pi = dynamic_cast (plugin_insert ()); + if (!has_editor () || (!pi || 0 == pi->window_proxy ())) { vststate_maybe_set_program (_state); _state->want_chunk = 0; _state->want_program = -1; @@ -508,7 +509,8 @@ VSTPlugin::load_user_preset (PresetRecord r) _state->wanted_chunk = raw_data; _state->wanted_chunk_size = size; _state->want_chunk = 1; - if (!has_editor () || (plugin_insert () && 0 == plugin_insert ()->window_proxy ())) { + PluginInsert* pi = dynamic_cast (plugin_insert ()); + if (!has_editor () || (!pi || 0 == pi->window_proxy ())) { vststate_maybe_set_program (_state); _state->want_chunk = 0; _state->want_program = -1; diff --git a/libs/ardour/windows_vst_plugin.cc b/libs/ardour/windows_vst_plugin.cc index f1e7c442da..ef91287a31 100644 --- a/libs/ardour/windows_vst_plugin.cc +++ b/libs/ardour/windows_vst_plugin.cc @@ -108,7 +108,7 @@ WindowsVSTPluginInfo::get_presets (bool user_only) const { std::vector p; - if (!Config->get_use_lxvst()) { + if (!Config->get_use_windows_vst()) { return p; } diff --git a/libs/ardour/wscript b/libs/ardour/wscript index 5f5f10e65b..e2635dc527 100644 --- a/libs/ardour/wscript +++ b/libs/ardour/wscript @@ -256,6 +256,7 @@ libardour_sources = [ 'system_exec.cc', 'revision.cc', 'rt_midibuffer.cc', + 'tailtime.cc', 'template_utils.cc', 'tempo_map_importer.cc', 'thawlist.cc', diff --git a/libs/audiographer/audiographer/process_context.h b/libs/audiographer/audiographer/process_context.h index 3b54accc06..8aec7fa9b2 100644 --- a/libs/audiographer/audiographer/process_context.h +++ b/libs/audiographer/audiographer/process_context.h @@ -2,7 +2,6 @@ #define AUDIOGRAPHER_PROCESS_CONTEXT_H #include -#include #include #include "audiographer/visibility.h" diff --git a/libs/audiographer/wscript b/libs/audiographer/wscript index ad8594d4d2..5d726d4d04 100644 --- a/libs/audiographer/wscript +++ b/libs/audiographer/wscript @@ -28,7 +28,6 @@ def configure(conf): autowaf.check_pkg(conf, 'fftw3f', uselib_store='FFTW3F', mandatory=True) # Boost headers - autowaf.check_header(conf, 'cxx', 'boost/shared_ptr.hpp') autowaf.check_header(conf, 'cxx', 'boost/format.hpp') def build(bld): diff --git a/libs/ctrl-interface/control_protocol/control_protocol.cc b/libs/ctrl-interface/control_protocol/control_protocol.cc index b2c7de5296..bbf307b1d1 100644 --- a/libs/ctrl-interface/control_protocol/control_protocol.cc +++ b/libs/ctrl-interface/control_protocol/control_protocol.cc @@ -41,15 +41,15 @@ using namespace ARDOUR; using namespace std; using namespace PBD; -Signal0 ControlProtocol::ZoomToSession; -Signal0 ControlProtocol::ZoomOut; -Signal0 ControlProtocol::ZoomIn; -Signal0 ControlProtocol::Enter; -Signal0 ControlProtocol::Undo; -Signal0 ControlProtocol::Redo; -Signal1 ControlProtocol::ScrollTimeline; -Signal1 ControlProtocol::GotoView; -Signal0 ControlProtocol::CloseDialog; +PBD::Signal0 ControlProtocol::ZoomToSession; +PBD::Signal0 ControlProtocol::ZoomOut; +PBD::Signal0 ControlProtocol::ZoomIn; +PBD::Signal0 ControlProtocol::Enter; +PBD::Signal0 ControlProtocol::Undo; +PBD::Signal0 ControlProtocol::Redo; +PBD::Signal1 ControlProtocol::ScrollTimeline; +PBD::Signal1 ControlProtocol::GotoView; +PBD::Signal0 ControlProtocol::CloseDialog; PBD::Signal0 ControlProtocol::VerticalZoomInAll; PBD::Signal0 ControlProtocol::VerticalZoomOutAll; PBD::Signal0 ControlProtocol::VerticalZoomInSelected; diff --git a/libs/ctrl-interface/control_protocol/control_protocol/types.h b/libs/ctrl-interface/control_protocol/control_protocol/types.h index 5e5335c79a..7285fc1bde 100644 --- a/libs/ctrl-interface/control_protocol/control_protocol/types.h +++ b/libs/ctrl-interface/control_protocol/control_protocol/types.h @@ -20,7 +20,6 @@ #define __ardour_control_protocol_types_h__ #include -#include namespace ARDOUR { class Route; diff --git a/libs/ctrl-interface/midi_surface/midi_surface.cc b/libs/ctrl-interface/midi_surface/midi_surface.cc index 5081697de2..e451d3299f 100644 --- a/libs/ctrl-interface/midi_surface/midi_surface.cc +++ b/libs/ctrl-interface/midi_surface/midi_surface.cc @@ -42,6 +42,7 @@ MIDISurface::MIDISurface (ARDOUR::Session& s, std::string const & namestr, std:: , AbstractUI (namestr) , with_pad_filter (use_pad_filter) , _in_use (false) + , _data_required (false) , port_name_prefix (port_prefix) , _connection_state (ConnectionState (0)) { @@ -372,10 +373,10 @@ MIDISurface::midi_input_handler (IOCondition ioc, MIDI::Port* port) } DEBUG_TRACE (DEBUG::MIDISurface, string_compose ("data available on %1\n", port->name())); - if (_in_use) { + if (_in_use || _data_required) { samplepos_t now = AudioEngine::instance()->sample_time(); port->parse (now); - } + } } return true; diff --git a/libs/ctrl-interface/midi_surface/midi_surface/midi_surface.h b/libs/ctrl-interface/midi_surface/midi_surface/midi_surface.h index 9248c39343..5bb8c0d1de 100644 --- a/libs/ctrl-interface/midi_surface/midi_surface/midi_surface.h +++ b/libs/ctrl-interface/midi_surface/midi_surface/midi_surface.h @@ -90,6 +90,7 @@ class MIDISurface : public ARDOUR::ControlProtocol protected: bool with_pad_filter; bool _in_use; + bool _data_required; std::string port_name_prefix; MIDI::Port* _input_port; MIDI::Port* _output_port; diff --git a/libs/evoral/evoral/ControlSet.h b/libs/evoral/evoral/ControlSet.h index 20614e5527..44a69fb6db 100644 --- a/libs/evoral/evoral/ControlSet.h +++ b/libs/evoral/evoral/ControlSet.h @@ -26,7 +26,7 @@ #include #include -#include +#include #include #include "pbd/signals.h" diff --git a/libs/evoral/evoral/Curve.h b/libs/evoral/evoral/Curve.h index 167bacea71..9bd574e2d5 100644 --- a/libs/evoral/evoral/Curve.h +++ b/libs/evoral/evoral/Curve.h @@ -20,7 +20,7 @@ #define EVORAL_CURVE_HPP #include -#include +#include #include "temporal/timeline.h" diff --git a/libs/gtkmm2ext/gtkapplication_quartz.mm b/libs/gtkmm2ext/gtkapplication_quartz.mm index 0b0083efdd..f83180f4c9 100644 --- a/libs/gtkmm2ext/gtkapplication_quartz.mm +++ b/libs/gtkmm2ext/gtkapplication_quartz.mm @@ -1,4 +1,4 @@ -/* GTK+ application-level integration for the Mac OS X/Cocoa +/* GTK+ application-level integration for the Mac OS X/Cocoa * * Copyright (C) 2007 Pioneer Research Center USA, Inc. * Copyright (C) 2007 Imendio AB @@ -41,9 +41,11 @@ #import #import +#include + #define UNUSED_PARAMETER(a) (void) (a) -// #define DEBUG(format, ...) g_printerr ("%s: " format, G_STRFUNC, ## __VA_ARGS__) +//#define DEBUG(format, ...) g_printerr ("%s: " format, G_STRFUNC, ## __VA_ARGS__) #define DEBUG(format, ...) /* TODO @@ -56,6 +58,8 @@ */ static gint _exiting = 0; +static std::vector global_menu_items; +static gint _modal_state = 0; static guint gdk_quartz_keyval_to_ns_keyval (guint keyval) @@ -309,7 +313,7 @@ keyval_keypad_nonkeypad_equivalent (guint keyval) return GDK_VoidSymbol; } -static const gchar* +static const gchar* gdk_quartz_keyval_to_string (guint keyval) { switch (keyval) { @@ -521,7 +525,7 @@ keyval_is_uppercase (guint keyval) } /* gtk/osx has a problem in that mac main menu events - are handled using an "internal" event handling system that + are handled using an "internal" event handling system that doesn't pass things back to the glib/gtk main loop. if we call gtk_main_iteration() block while in a menu event handler, then glib gets confused and thinks there are two threads running @@ -531,7 +535,7 @@ keyval_is_uppercase (guint keyval) static int _in_menu_event_handler = 0; -int +int gdk_quartz_in_menu_event_handler () { return _in_menu_event_handler; @@ -544,14 +548,16 @@ idle_call_activate (gpointer data) return FALSE; } -@interface GNSMenuItem : NSMenuItem +@interface GNSMenuItem : NSMenuItem { @public GtkMenuItem* gtk_menu_item; - GClosure *accel_closure; + GClosure* accel_closure; + bool premodal; } - (id) initWithTitle:(NSString*) title andGtkWidget:(GtkMenuItem*) w; - (void) activate:(id) sender; +- (BOOL) validateMenuItem:(NSMenuItem*) menuItem; @end @implementation GNSMenuItem @@ -577,6 +583,20 @@ idle_call_activate (gpointer data) g_idle_add_full (G_PRIORITY_HIGH_IDLE, idle_call_activate, gtk_menu_item, NULL); // g_idle_add (idle_call_activate, gtk_menu_item); } +- (BOOL) validateMenuItem:(NSMenuItem*) menuItem +{ + if (_modal_state > 0) { + return false; + } + + GtkAction* act = gtk_activatable_get_related_action (GTK_ACTIVATABLE(gtk_menu_item)); + + if (act) { + return gtk_action_get_sensitive (act); + } else { + return true; + } +} @end static void push_menu_shell_to_nsmenu (GtkMenuShell *menu_shell, @@ -668,7 +688,7 @@ cocoa_menu_connect (GtkWidget *menu, if (cocoa_menu_quark == 0) cocoa_menu_quark = g_quark_from_static_string ("NSMenu"); - + g_object_set_qdata_full (G_OBJECT (menu), cocoa_menu_quark, cocoa_menu, (GDestroyNotify) cocoa_menu_free); @@ -708,10 +728,11 @@ cocoa_menu_item_update_state (NSMenuItem* cocoa_item, "visible", &visible, NULL); - if (!sensitive) + if (!sensitive) { [cocoa_item setEnabled:NO]; - else + } else { [cocoa_item setEnabled:YES]; + } #if 0 // requires OS X 10.5 or later @@ -730,7 +751,7 @@ cocoa_menu_item_update_active (NSMenuItem *cocoa_item, g_object_get (widget, "active", &active, NULL); - if (active) + if (active) [cocoa_item setState:NSOnState]; else [cocoa_item setState:NSOffState]; @@ -741,7 +762,7 @@ cocoa_menu_item_update_submenu (NSMenuItem *cocoa_item, GtkWidget *widget) { GtkWidget *submenu; - + g_return_if_fail (cocoa_item != NULL); g_return_if_fail (widget != NULL); @@ -757,12 +778,12 @@ cocoa_menu_item_update_submenu (NSMenuItem *cocoa_item, /* create a new nsmenu to hold the GTK menu */ - if (label_text) + if (label_text) cocoa_submenu = [ [ NSMenu alloc ] initWithTitle:[ [ NSString alloc] initWithCString:label_text encoding:NSUTF8StringEncoding]]; else cocoa_submenu = [ [ NSMenu alloc ] initWithTitle:@""]; - [cocoa_submenu setAutoenablesItems:NO]; + [cocoa_submenu setAutoenablesItems:YES]; cocoa_menu_connect (submenu, cocoa_submenu); /* connect the new nsmenu to the passed-in item (which lives in @@ -790,7 +811,7 @@ cocoa_menu_item_update_label (NSMenuItem *cocoa_item, label_text = get_menu_label_text (widget, NULL); if (label_text) [cocoa_item setTitle:[ [ NSString alloc] initWithCString:label_text encoding:NSUTF8StringEncoding]]; - else + else [cocoa_item setTitle:@""]; } @@ -806,29 +827,29 @@ cocoa_menu_item_update_accelerator (NSMenuItem *cocoa_item, /* important note: this function doesn't do anything to actually change key handling. Its goal is to get Cocoa to display the correct accelerator as part of a menu item. Actual accelerator handling - is still done by GTK, so this is more cosmetic than it may + is still done by GTK, so this is more cosmetic than it may appear. */ - get_menu_label_text (widget, &label); + get_menu_label_text (widget, &label); if (GTK_IS_ACCEL_LABEL (label) && GTK_ACCEL_LABEL (label)->accel_closure) { GtkAccelKey *key; - + key = gtk_accel_group_find (GTK_ACCEL_LABEL (label)->accel_group, accel_find_func, GTK_ACCEL_LABEL (label)->accel_closure); - + if (key && key->accel_key && key->accel_flags & GTK_ACCEL_VISIBLE) { - guint modifiers = 0; + guint modifiers = 0; const gchar* str = NULL; - guint actual_key = key->accel_key; - + guint actual_key = key->accel_key; + if (keyval_is_keypad (actual_key)) { if ((actual_key = keyval_keypad_nonkeypad_equivalent (actual_key)) == GDK_VoidSymbol) { /* GDK_KP_Separator */ @@ -837,17 +858,17 @@ cocoa_menu_item_update_accelerator (NSMenuItem *cocoa_item, } modifiers |= NSNumericPadKeyMask; } - + /* if we somehow got here with GDK_A ... GDK_Z rather than GDK_a ... GDK_z, then take note of that and make sure we use a shift modifier. */ - + if (keyval_is_uppercase (actual_key)) { modifiers |= NSShiftKeyMask; } - + str = gdk_quartz_keyval_to_string (actual_key); - + if (str) { unichar ukey = str[0]; [cocoa_item setKeyEquivalent:[NSString stringWithCharacters:&ukey length:1]]; @@ -860,31 +881,31 @@ cocoa_menu_item_update_accelerator (NSMenuItem *cocoa_item, [cocoa_item setKeyEquivalent:@""]; return; } - } - + } + if (key->accel_mods || modifiers) { if (key->accel_mods & GDK_SHIFT_MASK) { modifiers |= NSShiftKeyMask; } - + /* gdk/quartz maps Alt/Option to Mod1 */ - + if (key->accel_mods & (GDK_MOD1_MASK)) { modifiers |= NSAlternateKeyMask; } - + if (key->accel_mods & GDK_CONTROL_MASK) { modifiers |= NSControlKeyMask; } - + /* our modified gdk/quartz maps Command to Mod2 */ - + if (key->accel_mods & GDK_MOD2_MASK) { modifiers |= NSCommandKeyMask; } - } - + } + [cocoa_item setKeyEquivalentModifierMask:modifiers]; return; } @@ -904,7 +925,7 @@ cocoa_menu_item_accel_changed (GtkAccelGroup* /*accel_group*/, GNSMenuItem *cocoa_item; GtkWidget *label; - if (_exiting) + if (_exiting) return; cocoa_item = cocoa_menu_item_get (widget); @@ -961,7 +982,7 @@ cocoa_menu_item_notify_label (GObject *object, { GNSMenuItem *cocoa_item; - if (_exiting) + if (_exiting) return; cocoa_item = cocoa_menu_item_get (GTK_WIDGET (object)); @@ -1016,13 +1037,13 @@ cocoa_menu_item_connect (GtkWidget* menu_item, g_object_set_qdata_full (G_OBJECT (menu_item), cocoa_menu_item_quark, cocoa_item, (GDestroyNotify) cocoa_menu_item_free); - + if (!old_item) { g_signal_connect (menu_item, "notify", G_CALLBACK (cocoa_menu_item_notify), cocoa_item); - + if (label) g_signal_connect_swapped (label, "notify::label", G_CALLBACK (cocoa_menu_item_notify_label), @@ -1035,14 +1056,14 @@ add_menu_item (NSMenu* cocoa_menu, GtkWidget* menu_item, int index) { GtkWidget* label = NULL; GNSMenuItem *cocoa_item; - - DEBUG ("add %s to menu %s separator ? %d\n", get_menu_label_text (menu_item, NULL), + + DEBUG ("add %s to menu %s separator ? %d\n", get_menu_label_text (menu_item, NULL), [[cocoa_menu title] cStringUsingEncoding:NSUTF8StringEncoding], GTK_IS_SEPARATOR_MENU_ITEM(menu_item)); cocoa_item = cocoa_menu_item_get (menu_item); - if (cocoa_item) + if (cocoa_item) return; if (GTK_IS_SEPARATOR_MENU_ITEM (menu_item)) { @@ -1056,7 +1077,7 @@ add_menu_item (NSMenu* cocoa_menu, GtkWidget* menu_item, int index) } const gchar* label_text = get_menu_label_text (menu_item, &label); - + if (label_text) cocoa_item = [ [ GNSMenuItem alloc] initWithTitle:[ [ NSString alloc] initWithCString:label_text encoding:NSUTF8StringEncoding] andGtkWidget:(GtkMenuItem*)menu_item]; @@ -1064,16 +1085,16 @@ add_menu_item (NSMenu* cocoa_menu, GtkWidget* menu_item, int index) cocoa_item = [ [ GNSMenuItem alloc] initWithTitle:@"" andGtkWidget:(GtkMenuItem*)menu_item]; DEBUG ("\tan item\n"); } - + /* connect GtkMenuItem and NSMenuItem so that we can notice changes to accel/label/submenu etc. */ cocoa_menu_item_connect (menu_item, (GNSMenuItem*) cocoa_item, label); cocoa_menu_item_update_state (cocoa_item, menu_item); - if (index >= 0) + if (index >= 0) [ cocoa_menu insertItem:cocoa_item atIndex:index]; - else + else [ cocoa_menu addItem:cocoa_item]; - + if (!GTK_WIDGET_IS_SENSITIVE (menu_item)) [cocoa_item setState:NSOffState]; @@ -1081,19 +1102,24 @@ add_menu_item (NSMenu* cocoa_menu, GtkWidget* menu_item, int index) if (!GTK_WIDGET_VISIBLE (menu_item)) [cocoa_item setHidden:YES]; #endif - + if (GTK_IS_CHECK_MENU_ITEM (menu_item)) cocoa_menu_item_update_active (cocoa_item, menu_item); - + if (!GTK_IS_SEPARATOR_MENU_ITEM (menu_item)) cocoa_menu_item_update_accel_closure (cocoa_item, menu_item); - - if (gtk_menu_item_get_submenu (GTK_MENU_ITEM (menu_item))) + + if (gtk_menu_item_get_submenu (GTK_MENU_ITEM (menu_item))) cocoa_menu_item_update_submenu (cocoa_item, menu_item); [ cocoa_item release]; + + if (GTK_IS_CHECK_MENU_ITEM (menu_item)) { + GtkMenuItem* mitem = GTK_MENU_ITEM(menu_item); + global_menu_items.push_back (mitem); + } } - + static void push_menu_shell_to_nsmenu (GtkMenuShell *menu_shell, NSMenu* cocoa_menu, @@ -1117,7 +1143,7 @@ push_menu_shell_to_nsmenu (GtkMenuShell *menu_shell, add_menu_item (cocoa_menu, menu_item, -1); } - + g_list_free (children); } @@ -1264,7 +1290,7 @@ add_to_window_menu (NSMenu *menu) static int create_window_menu () -{ +{ _window_menu = [[NSMenu alloc] initWithTitle: @"Window"]; [_window_menu addItemWithTitle:@"Minimize" @@ -1277,7 +1303,7 @@ create_window_menu () add_to_menubar(_window_menu); return 0; -} +} #endif /* @@ -1303,7 +1329,7 @@ gtk_application_set_menu_bar (GtkMenuShell *menu_shell) doesn't really make sense for a Gtk/Cocoa hybrid menu. */ - [cocoa_menubar setAutoenablesItems:NO]; + [cocoa_menubar setAutoenablesItems:YES]; emission_hook_id = g_signal_add_emission_hook (g_signal_lookup ("parent-set", @@ -1355,7 +1381,7 @@ gtk_application_add_app_menu_item (GtkApplicationMenuGroup *group, /* add a separator before adding the first item, but not * for the first group */ - + if (!group->items && list->prev) { [appMenu insertItem:[NSMenuItem separatorItem] atIndex:index+1]; @@ -1389,7 +1415,7 @@ namespace Gtk { } @interface GtkApplicationNotificationObject : NSObject {} -- (GtkApplicationNotificationObject*) init; +- (GtkApplicationNotificationObject*) init; @end @implementation GtkApplicationNotificationObject @@ -1405,7 +1431,7 @@ namespace Gtk { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidBecomeInactive:) - name:NSApplicationWillResignActiveNotification + name:NSApplicationWillResignActiveNotification object:[NSApplication sharedApplication]]; } @@ -1429,7 +1455,6 @@ namespace Gtk { @interface GtkApplicationDelegate : NSObject -(BOOL) application:(NSApplication*) app openFile:(NSString*) file; - (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *) app; -- (void) startApp; @end @implementation GtkApplicationDelegate @@ -1443,20 +1468,44 @@ namespace Gtk { - (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *) app { UNUSED_PARAMETER(app); + if (_modal_state > 0) { + return NSTerminateCancel; + } Gtkmm2ext::Application::instance()->ShouldQuit (); return NSTerminateCancel; } @end +static void +gdk_quartz_modal_notify (GdkWindow*, gboolean modal) +{ + /* this global will control sensitivity of our app menu items, via validateMenuItem */ + if (modal) { + ++_modal_state; + } else if (_modal_state > 0) { + --_modal_state; + } + + /* Need to notify GTK that actions are insensitive where necessary */ + + for (auto & mitem : global_menu_items) { + GtkAction* act = gtk_activatable_get_related_action (GTK_ACTIVATABLE(mitem)); + if (act) { + gtk_action_set_sensitive (act, 0 == _modal_state); + } + } +} /* Basic setup */ extern "C" int gtk_application_init () { + gdk_window_set_modal_notify (gdk_quartz_modal_notify); + _main_menubar = [[NSMenu alloc] initWithTitle: @""]; - if (!_main_menubar) + if (!_main_menubar) return -1; [NSApp setMainMenu: _main_menubar]; diff --git a/libs/hidapi/README b/libs/hidapi/README index eda4740b10..bcbe1d66fd 100644 --- a/libs/hidapi/README +++ b/libs/hidapi/README @@ -1,2 +1 @@ -http://www.signal11.us/oss/hidapi/ -hidapi-0.8.0-rc1-21-ga6a622f (2016-01-08) from https://github.com/signal11/hidapi +hidapi-0.14.0-35-gc3c79a7 (2024-08-21) from https://github.com/libusb/hidapi diff --git a/libs/hidapi/hidapi/hidapi.h b/libs/hidapi/hidapi/hidapi.h index a75dc5ac3e..62ca8a80e6 100644 --- a/libs/hidapi/hidapi/hidapi.h +++ b/libs/hidapi/hidapi/hidapi.h @@ -5,9 +5,9 @@ Alan Ott Signal 11 Software - 8/22/2009 + libusb/hidapi Team - Copyright 2009, All Rights Reserved. + Copyright 2023, All Rights Reserved. At the discretion of the user of this library, this software may be licensed under the terms of the @@ -17,7 +17,7 @@ files located at the root of the source distribution. These files may also be found in the public source code repository located at: - http://github.com/signal11/hidapi . + https://github.com/libusb/hidapi . ********************************************************/ /** @file @@ -29,22 +29,123 @@ #include -#if 0 // XXX we compile hidapi as static library +/* #480: this is to be refactored properly for v1.0 */ +#ifdef _WIN32 + #ifndef HID_API_NO_EXPORT_DEFINE #define HID_API_EXPORT __declspec(dllexport) - #define HID_API_CALL -#else - #define HID_API_EXPORT /**< API export macro */ - #define HID_API_CALL /**< API call macro */ + #endif #endif +#ifndef HID_API_EXPORT + #define HID_API_EXPORT /**< API export macro */ +#endif +/* To be removed in v1.0 */ +#define HID_API_CALL /**< API call macro */ #define HID_API_EXPORT_CALL HID_API_EXPORT HID_API_CALL /**< API export and call macro*/ +/** @brief Static/compile-time major version of the library. + + @ingroup API +*/ +#define HID_API_VERSION_MAJOR 0 +/** @brief Static/compile-time minor version of the library. + + @ingroup API +*/ +#define HID_API_VERSION_MINOR 15 +/** @brief Static/compile-time patch version of the library. + + @ingroup API +*/ +#define HID_API_VERSION_PATCH 0 + +/* Helper macros */ +#define HID_API_AS_STR_IMPL(x) #x +#define HID_API_AS_STR(x) HID_API_AS_STR_IMPL(x) +#define HID_API_TO_VERSION_STR(v1, v2, v3) HID_API_AS_STR(v1.v2.v3) + +/** @brief Coverts a version as Major/Minor/Patch into a number: + <8 bit major><16 bit minor><8 bit patch>. + + This macro was added in version 0.12.0. + + Convenient function to be used for compile-time checks, like: + @code{.c} + #if HID_API_VERSION >= HID_API_MAKE_VERSION(0, 12, 0) + @endcode + + @ingroup API +*/ +#define HID_API_MAKE_VERSION(mj, mn, p) (((mj) << 24) | ((mn) << 8) | (p)) + +/** @brief Static/compile-time version of the library. + + This macro was added in version 0.12.0. + + @see @ref HID_API_MAKE_VERSION. + + @ingroup API +*/ +#define HID_API_VERSION HID_API_MAKE_VERSION(HID_API_VERSION_MAJOR, HID_API_VERSION_MINOR, HID_API_VERSION_PATCH) + +/** @brief Static/compile-time string version of the library. + + @ingroup API +*/ +#define HID_API_VERSION_STR HID_API_TO_VERSION_STR(HID_API_VERSION_MAJOR, HID_API_VERSION_MINOR, HID_API_VERSION_PATCH) + +/** @brief Maximum expected HID Report descriptor size in bytes. + + Since version 0.13.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 13, 0) + + @ingroup API +*/ +#define HID_API_MAX_REPORT_DESCRIPTOR_SIZE 4096 + #ifdef __cplusplus extern "C" { #endif + /** A structure to hold the version numbers. */ + struct hid_api_version { + int major; /**< major version number */ + int minor; /**< minor version number */ + int patch; /**< patch version number */ + }; + struct hid_device_; typedef struct hid_device_ hid_device; /**< opaque hidapi structure */ + /** @brief HID underlying bus types. + + @ingroup API + */ + typedef enum { + /** Unknown bus type */ + HID_API_BUS_UNKNOWN = 0x00, + + /** USB bus + Specifications: + https://usb.org/hid */ + HID_API_BUS_USB = 0x01, + + /** Bluetooth or Bluetooth LE bus + Specifications: + https://www.bluetooth.com/specifications/specs/human-interface-device-profile-1-1-1/ + https://www.bluetooth.com/specifications/specs/hid-service-1-0/ + https://www.bluetooth.com/specifications/specs/hid-over-gatt-profile-1-0/ */ + HID_API_BUS_BLUETOOTH = 0x02, + + /** I2C bus + Specifications: + https://docs.microsoft.com/previous-versions/windows/hardware/design/dn642101(v=vs.85) */ + HID_API_BUS_I2C = 0x03, + + /** SPI bus + Specifications: + https://www.microsoft.com/download/details.aspx?id=103325 */ + HID_API_BUS_SPI = 0x04, + } hid_bus_type; + /** hidapi info structure */ struct hid_device_info { /** Platform-specific device path */ @@ -63,19 +164,26 @@ extern "C" { /** Product string */ wchar_t *product_string; /** Usage Page for this Device/Interface - (Windows/Mac only). */ + (Windows/Mac/hidraw only) */ unsigned short usage_page; /** Usage for this Device/Interface - (Windows/Mac only).*/ + (Windows/Mac/hidraw only) */ unsigned short usage; /** The USB interface which this logical device - represents. Valid on both Linux implementations - in all cases, and valid on the Windows implementation - only if the device contains more than one interface. */ + represents. + + Valid only if the device is a USB HID device. + Set to -1 in all other cases. + */ int interface_number; /** Pointer to the next device */ struct hid_device_info *next; + + /** Underlying bus type + Since version 0.13.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 13, 0) + */ + hid_bus_type bus_type; }; @@ -87,11 +195,12 @@ extern "C" { needed. This function should be called at the beginning of execution however, if there is a chance of HIDAPI handles being opened by different threads simultaneously. - + @ingroup API @returns This function returns 0 on success and -1 on error. + Call hid_error(NULL) to get the failure reason. */ int HID_API_EXPORT HID_API_CALL hid_init(void); @@ -103,7 +212,7 @@ extern "C" { @ingroup API - @returns + @returns This function returns 0 on success and -1 on error. */ int HID_API_EXPORT HID_API_CALL hid_exit(void); @@ -123,21 +232,25 @@ extern "C" { @param product_id The Product ID (PID) of the types of device to open. - @returns - This function returns a pointer to a linked list of type - struct #hid_device, containing information about the HID devices - attached to the system, or NULL in the case of failure. Free - this linked list by calling hid_free_enumeration(). + @returns + This function returns a pointer to a linked list of type + struct #hid_device_info, containing information about the HID devices + attached to the system, + or NULL in the case of failure or if no HID devices present in the system. + Call hid_error(NULL) to get the failure reason. + + @note The returned value by this function must to be freed by calling hid_free_enumeration(), + when not needed anymore. */ struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned short vendor_id, unsigned short product_id); /** @brief Free an enumeration Linked List - This function frees a linked list created by hid_enumerate(). + This function frees a linked list created by hid_enumerate(). @ingroup API - @param devs Pointer to a list of struct_device returned from - hid_enumerate(). + @param devs Pointer to a list of struct_device returned from + hid_enumerate(). */ void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *devs); @@ -151,11 +264,15 @@ extern "C" { @param vendor_id The Vendor ID (VID) of the device to open. @param product_id The Product ID (PID) of the device to open. @param serial_number The Serial Number of the device to open - (Optionally NULL). + (Optionally NULL). @returns This function returns a pointer to a #hid_device object on success or NULL on failure. + Call hid_error(NULL) to get the failure reason. + + @note The returned object must be freed by calling hid_close(), + when not needed anymore. */ HID_API_EXPORT hid_device * HID_API_CALL hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number); @@ -166,11 +283,15 @@ extern "C" { Linux). @ingroup API - @param path The path name of the device to open + @param path The path name of the device to open @returns This function returns a pointer to a #hid_device object on success or NULL on failure. + Call hid_error(NULL) to get the failure reason. + + @note The returned object must be freed by calling hid_close(), + when not needed anymore. */ HID_API_EXPORT hid_device * HID_API_CALL hid_open_path(const char *path); @@ -186,12 +307,12 @@ extern "C" { single report), followed by the report data (16 bytes). In this example, the length passed in would be 17. - hid_write() will send the data on the first OUT endpoint, if - one exists. If it does not, it will send the data through - the Control Endpoint (Endpoint 0). + hid_write() will send the data on the first interrupt OUT + endpoint, if one exists. If it does not the behaviour is as + @ref hid_send_output_report @ingroup API - @param device A device handle returned from hid_open(). + @param dev A device handle returned from hid_open(). @param data The data to send, including the report number as the first byte. @param length The length in bytes of the data to send. @@ -199,8 +320,9 @@ extern "C" { @returns This function returns the actual number of bytes written and -1 on error. + Call hid_error(dev) to get the failure reason. */ - int HID_API_EXPORT HID_API_CALL hid_write(hid_device *device, const unsigned char *data, size_t length); + int HID_API_EXPORT HID_API_CALL hid_write(hid_device *dev, const unsigned char *data, size_t length); /** @brief Read an Input report from a HID device with timeout. @@ -209,7 +331,7 @@ extern "C" { contain the Report number if the device uses numbered reports. @ingroup API - @param device A device handle returned from hid_open(). + @param dev A device handle returned from hid_open(). @param data A buffer to put the read data into. @param length The number of bytes to read. For devices with multiple reports, make sure to read an extra byte for @@ -218,7 +340,9 @@ extern "C" { @returns This function returns the actual number of bytes read and - -1 on error. If no packet was available to be read within + -1 on error. + Call hid_error(dev) to get the failure reason. + If no packet was available to be read within the timeout period, this function returns 0. */ int HID_API_EXPORT HID_API_CALL hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds); @@ -226,11 +350,11 @@ extern "C" { /** @brief Read an Input report from a HID device. Input reports are returned - to the host through the INTERRUPT IN endpoint. The first byte will + to the host through the INTERRUPT IN endpoint. The first byte will contain the Report number if the device uses numbered reports. @ingroup API - @param device A device handle returned from hid_open(). + @param dev A device handle returned from hid_open(). @param data A buffer to put the read data into. @param length The number of bytes to read. For devices with multiple reports, make sure to read an extra byte for @@ -238,10 +362,12 @@ extern "C" { @returns This function returns the actual number of bytes read and - -1 on error. If no packet was available to be read and + -1 on error. + Call hid_error(dev) to get the failure reason. + If no packet was available to be read and the handle is in non-blocking mode, this function returns 0. */ - int HID_API_EXPORT HID_API_CALL hid_read(hid_device *device, unsigned char *data, size_t length); + int HID_API_EXPORT HID_API_CALL hid_read(hid_device *dev, unsigned char *data, size_t length); /** @brief Set the device handle to be non-blocking. @@ -253,15 +379,16 @@ extern "C" { Nonblocking can be turned on and off at any time. @ingroup API - @param device A device handle returned from hid_open(). + @param dev A device handle returned from hid_open(). @param nonblock enable or not the nonblocking reads - 1 to enable nonblocking - 0 to disable nonblocking. @returns This function returns 0 on success and -1 on error. + Call hid_error(dev) to get the failure reason. */ - int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *device, int nonblock); + int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *dev, int nonblock); /** @brief Send a Feature report to the device. @@ -279,7 +406,7 @@ extern "C" { in would be 17. @ingroup API - @param device A device handle returned from hid_open(). + @param dev A device handle returned from hid_open(). @param data The data to send, including the report number as the first byte. @param length The length in bytes of the data to send, including @@ -288,8 +415,9 @@ extern "C" { @returns This function returns the actual number of bytes written and -1 on error. + Call hid_error(dev) to get the failure reason. */ - int HID_API_EXPORT HID_API_CALL hid_send_feature_report(hid_device *device, const unsigned char *data, size_t length); + int HID_API_EXPORT HID_API_CALL hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length); /** @brief Get a feature report from a HID device. @@ -300,7 +428,7 @@ extern "C" { start in data[1]. @ingroup API - @param device A device handle returned from hid_open(). + @param dev A device handle returned from hid_open(). @param data A buffer to put the read data into, including the Report ID. Set the first byte of @p data[] to the Report ID of the report to be read, or set it to zero @@ -313,79 +441,218 @@ extern "C" { This function returns the number of bytes read plus one for the report ID (which is still in the first byte), or -1 on error. + Call hid_error(dev) to get the failure reason. */ - int HID_API_EXPORT HID_API_CALL hid_get_feature_report(hid_device *device, unsigned char *data, size_t length); + int HID_API_EXPORT HID_API_CALL hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length); + + /** @brief Send a Output report to the device. + + Since version 0.15.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 15, 0) + + Output reports are sent over the Control endpoint as a + Set_Report transfer. The first byte of @p data[] must + contain the Report ID. For devices which only support a + single report, this must be set to 0x0. The remaining bytes + contain the report data. Since the Report ID is mandatory, + calls to hid_send_output_report() will always contain one + more byte than the report contains. For example, if a hid + report is 16 bytes long, 17 bytes must be passed to + hid_send_output_report(): the Report ID (or 0x0, for + devices which do not use numbered reports), followed by the + report data (16 bytes). In this example, the length passed + in would be 17. + + This function sets the return value of hid_error(). + + @ingroup API + @param dev A device handle returned from hid_open(). + @param data The data to send, including the report number as + the first byte. + @param length The length in bytes of the data to send, including + the report number. + + @returns + This function returns the actual number of bytes written and + -1 on error. + + @see @ref hid_write + */ + int HID_API_EXPORT HID_API_CALL hid_send_output_report(hid_device* dev, const unsigned char* data, size_t length); + + /** @brief Get a input report from a HID device. + + Since version 0.10.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 10, 0) + + Set the first byte of @p data[] to the Report ID of the + report to be read. Make sure to allow space for this + extra byte in @p data[]. Upon return, the first byte will + still contain the Report ID, and the report data will + start in data[1]. + + @ingroup API + @param dev A device handle returned from hid_open(). + @param data A buffer to put the read data into, including + the Report ID. Set the first byte of @p data[] to the + Report ID of the report to be read, or set it to zero + if your device does not use numbered reports. + @param length The number of bytes to read, including an + extra byte for the report ID. The buffer can be longer + than the actual report. + + @returns + This function returns the number of bytes read plus + one for the report ID (which is still in the first + byte), or -1 on error. + Call hid_error(dev) to get the failure reason. + */ + int HID_API_EXPORT HID_API_CALL hid_get_input_report(hid_device *dev, unsigned char *data, size_t length); /** @brief Close a HID device. @ingroup API - @param device A device handle returned from hid_open(). + @param dev A device handle returned from hid_open(). */ - void HID_API_EXPORT HID_API_CALL hid_close(hid_device *device); + void HID_API_EXPORT HID_API_CALL hid_close(hid_device *dev); /** @brief Get The Manufacturer String from a HID device. @ingroup API - @param device A device handle returned from hid_open(). + @param dev A device handle returned from hid_open(). @param string A wide string buffer to put the data into. @param maxlen The length of the buffer in multiples of wchar_t. @returns This function returns 0 on success and -1 on error. + Call hid_error(dev) to get the failure reason. */ - int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *device, wchar_t *string, size_t maxlen); + int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen); /** @brief Get The Product String from a HID device. @ingroup API - @param device A device handle returned from hid_open(). + @param dev A device handle returned from hid_open(). @param string A wide string buffer to put the data into. @param maxlen The length of the buffer in multiples of wchar_t. @returns This function returns 0 on success and -1 on error. + Call hid_error(dev) to get the failure reason. */ - int HID_API_EXPORT_CALL hid_get_product_string(hid_device *device, wchar_t *string, size_t maxlen); + int HID_API_EXPORT_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen); /** @brief Get The Serial Number String from a HID device. @ingroup API - @param device A device handle returned from hid_open(). + @param dev A device handle returned from hid_open(). @param string A wide string buffer to put the data into. @param maxlen The length of the buffer in multiples of wchar_t. @returns This function returns 0 on success and -1 on error. + Call hid_error(dev) to get the failure reason. */ - int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *device, wchar_t *string, size_t maxlen); + int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen); + + /** @brief Get The struct #hid_device_info from a HID device. + + Since version 0.13.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 13, 0) + + @ingroup API + @param dev A device handle returned from hid_open(). + + @returns + This function returns a pointer to the struct #hid_device_info + for this hid_device, or NULL in the case of failure. + Call hid_error(dev) to get the failure reason. + This struct is valid until the device is closed with hid_close(). + + @note The returned object is owned by the @p dev, and SHOULD NOT be freed by the user. + */ + struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_get_device_info(hid_device *dev); /** @brief Get a string from a HID device, based on its string index. @ingroup API - @param device A device handle returned from hid_open(). + @param dev A device handle returned from hid_open(). @param string_index The index of the string to get. @param string A wide string buffer to put the data into. @param maxlen The length of the buffer in multiples of wchar_t. @returns This function returns 0 on success and -1 on error. + Call hid_error(dev) to get the failure reason. */ - int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *device, int string_index, wchar_t *string, size_t maxlen); + int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen); + + /** @brief Get a report descriptor from a HID device. + + Since version 0.14.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 14, 0) + + User has to provide a preallocated buffer where descriptor will be copied to. + The recommended size for preallocated buffer is @ref HID_API_MAX_REPORT_DESCRIPTOR_SIZE bytes. + + @ingroup API + @param dev A device handle returned from hid_open(). + @param buf The buffer to copy descriptor into. + @param buf_size The size of the buffer in bytes. + + @returns + This function returns non-negative number of bytes actually copied, or -1 on error. + */ + int HID_API_EXPORT_CALL hid_get_report_descriptor(hid_device *dev, unsigned char *buf, size_t buf_size); /** @brief Get a string describing the last error which occurred. + This function is intended for logging/debugging purposes. + + This function guarantees to never return NULL. + If there was no error in the last function call - + the returned string clearly indicates that. + + Any HIDAPI function that can explicitly indicate an execution failure + (e.g. by an error code, or by returning NULL) - may set the error string, + to be returned by this function. + + Strings returned from hid_error() must not be freed by the user, + i.e. owned by HIDAPI library. + Device-specific error string may remain allocated at most until hid_close() is called. + Global error string may remain allocated at most until hid_exit() is called. + @ingroup API - @param device A device handle returned from hid_open(). + @param dev A device handle returned from hid_open(), + or NULL to get the last non-device-specific error + (e.g. for errors in hid_open() or hid_enumerate()). @returns - This function returns a string containing the last error - which occurred or NULL if none has occurred. + A string describing the last error (if any). */ - HID_API_EXPORT const wchar_t* HID_API_CALL hid_error(hid_device *device); + HID_API_EXPORT const wchar_t* HID_API_CALL hid_error(hid_device *dev); + + /** @brief Get a runtime version of the library. + + This function is thread-safe. + + @ingroup API + + @returns + Pointer to statically allocated struct, that contains version. + */ + HID_API_EXPORT const struct hid_api_version* HID_API_CALL hid_version(void); + + + /** @brief Get a runtime version string of the library. + + This function is thread-safe. + + @ingroup API + + @returns + Pointer to statically allocated string, that contains version string. + */ + HID_API_EXPORT const char* HID_API_CALL hid_version_str(void); #ifdef __cplusplus } #endif #endif - diff --git a/libs/hidapi/linux/hid.c b/libs/hidapi/linux/hid.c index bc6429e4e1..cb8a78fd11 100644 --- a/libs/hidapi/linux/hid.c +++ b/libs/hidapi/linux/hid.c @@ -5,10 +5,9 @@ Alan Ott Signal 11 Software - 8/22/2009 - Linux Version - 6/2/2009 + libusb/hidapi Team - Copyright 2009, All Rights Reserved. + Copyright 2022, All Rights Reserved. At the discretion of the user of this library, this software may be licensed under the terms of the @@ -18,11 +17,9 @@ files located at the root of the source distribution. These files may also be found in the public source code repository located at: - http://github.com/signal11/hidapi . + https://github.com/libusb/hidapi . ********************************************************/ -#define _POSIX_C_SOURCE 200809L - /* C */ #include #include @@ -47,8 +44,13 @@ #include "hidapi.h" -/* Definitions from linux/hidraw.h. Since these are new, some distros - may not have header files which contain them. */ +#ifdef HIDAPI_ALLOW_BUILD_WORKAROUND_KERNEL_2_6_39 +/* This definitions first appeared in Linux Kernel 2.6.39 in linux/hidraw.h. + hidapi doesn't support kernels older than that, + so we don't define macros below explicitly, to fail builds on old kernels. + For those who really need this as a workaround (e.g. to be able to build on old build machines), + can workaround by defining the macro above. +*/ #ifndef HIDIOCSFEATURE #define HIDIOCSFEATURE(len) _IOC(_IOC_WRITE|_IOC_READ, 'H', 0x06, len) #endif @@ -56,59 +58,46 @@ #define HIDIOCGFEATURE(len) _IOC(_IOC_WRITE|_IOC_READ, 'H', 0x07, len) #endif +#endif -/* USB HID device property names */ -const char *device_string_names[] = { - "manufacturer", - "product", - "serial", -}; -/* Symbolic names for the properties above */ -enum device_string_id { - DEVICE_STRING_MANUFACTURER, - DEVICE_STRING_PRODUCT, - DEVICE_STRING_SERIAL, - - DEVICE_STRING_COUNT, -}; +// HIDIOCGINPUT and HIDIOCSOUTPUT are not defined in Linux kernel headers < 5.11. +// These definitions are from hidraw.h in Linux >= 5.11. +// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=f43d3870cafa2a0f3854c1819c8385733db8f9ae +#ifndef HIDIOCGINPUT +#define HIDIOCGINPUT(len) _IOC(_IOC_WRITE|_IOC_READ, 'H', 0x0A, len) +#endif +#ifndef HIDIOCSOUTPUT +#define HIDIOCSOUTPUT(len) _IOC(_IOC_WRITE|_IOC_READ, 'H', 0x0B, len) +#endif struct hid_device_ { int device_handle; int blocking; - int uses_numbered_reports; + wchar_t *last_error_str; + struct hid_device_info* device_info; }; +static struct hid_api_version api_version = { + .major = HID_API_VERSION_MAJOR, + .minor = HID_API_VERSION_MINOR, + .patch = HID_API_VERSION_PATCH +}; -static __u32 kernel_version = 0; +static wchar_t *last_global_error_str = NULL; -static __u32 detect_kernel_version(void) -{ - struct utsname name; - int major, minor, release; - int ret; - - uname(&name); - ret = sscanf(name.release, "%d.%d.%d", &major, &minor, &release); - if (ret == 3) { - return KERNEL_VERSION(major, minor, release); - } - - ret = sscanf(name.release, "%d.%d", &major, &minor); - if (ret == 2) { - return KERNEL_VERSION(major, minor, 0); - } - - printf("Couldn't determine kernel version from version string \"%s\"\n", name.release); - return 0; -} static hid_device *new_hid_device(void) { - hid_device *dev = calloc(1, sizeof(hid_device)); + hid_device *dev = (hid_device*) calloc(1, sizeof(hid_device)); + if (dev == NULL) { + return NULL; + } + dev->device_handle = -1; dev->blocking = 1; - dev->uses_numbered_reports = 0; + dev->last_error_str = NULL; + dev->device_info = NULL; return dev; } @@ -124,7 +113,11 @@ static wchar_t *utf8_to_wchar_t(const char *utf8) if ((size_t) -1 == wlen) { return wcsdup(L""); } - ret = calloc(wlen+1, sizeof(wchar_t)); + ret = (wchar_t*) calloc(wlen+1, sizeof(wchar_t)); + if (ret == NULL) { + /* as much as we can do at this point */ + return NULL; + } mbstowcs(ret, utf8, wlen+1); ret[wlen] = 0x0000; } @@ -132,6 +125,64 @@ static wchar_t *utf8_to_wchar_t(const char *utf8) return ret; } + +/* Makes a copy of the given error message (and decoded according to the + * currently locale) into the wide string pointer pointed by error_str. + * The last stored error string is freed. + * Use register_error_str(NULL) to free the error message completely. */ +static void register_error_str(wchar_t **error_str, const char *msg) +{ + free(*error_str); + *error_str = utf8_to_wchar_t(msg); +} + +/* Semilar to register_error_str, but allows passing a format string with va_list args into this function. */ +static void register_error_str_vformat(wchar_t **error_str, const char *format, va_list args) +{ + char msg[256]; + vsnprintf(msg, sizeof(msg), format, args); + + register_error_str(error_str, msg); +} + +/* Set the last global error to be reported by hid_error(NULL). + * The given error message will be copied (and decoded according to the + * currently locale, so do not pass in string constants). + * The last stored global error message is freed. + * Use register_global_error(NULL) to indicate "no error". */ +static void register_global_error(const char *msg) +{ + register_error_str(&last_global_error_str, msg); +} + +/* Similar to register_global_error, but allows passing a format string into this function. */ +static void register_global_error_format(const char *format, ...) +{ + va_list args; + va_start(args, format); + register_error_str_vformat(&last_global_error_str, format, args); + va_end(args); +} + +/* Set the last error for a device to be reported by hid_error(dev). + * The given error message will be copied (and decoded according to the + * currently locale, so do not pass in string constants). + * The last stored device error message is freed. + * Use register_device_error(dev, NULL) to indicate "no error". */ +static void register_device_error(hid_device *dev, const char *msg) +{ + register_error_str(&dev->last_error_str, msg); +} + +/* Similar to register_device_error, but you can pass a format string into this function. */ +static void register_device_error_format(hid_device *dev, const char *format, ...) +{ + va_list args; + va_start(args, format); + register_error_str_vformat(&dev->last_error_str, format, args); + va_end(args); +} + /* Get an attribute value from a udev_device and return it as a whar_t string. The returned string must be freed with free() when done.*/ static wchar_t *copy_udev_string(struct udev_device *dev, const char *udev_name) @@ -139,78 +190,397 @@ static wchar_t *copy_udev_string(struct udev_device *dev, const char *udev_name) return utf8_to_wchar_t(udev_device_get_sysattr_value(dev, udev_name)); } -/* uses_numbered_reports() returns 1 if report_descriptor describes a device - which contains numbered reports. */ -static int uses_numbered_reports(__u8 *report_descriptor, __u32 size) { - unsigned int i = 0; +/* + * Gets the size of the HID item at the given position + * Returns 1 if successful, 0 if an invalid key + * Sets data_len and key_size when successful + */ +static int get_hid_item_size(const __u8 *report_descriptor, __u32 size, unsigned int pos, int *data_len, int *key_size) +{ + int key = report_descriptor[pos]; int size_code; - int data_len, key_size; - while (i < size) { - int key = report_descriptor[i]; + /* + * This is a Long Item. The next byte contains the + * length of the data section (value) for this key. + * See the HID specification, version 1.11, section + * 6.2.2.3, titled "Long Items." + */ + if ((key & 0xf0) == 0xf0) { + if (pos + 1 < size) + { + *data_len = report_descriptor[pos + 1]; + *key_size = 3; + return 1; + } + *data_len = 0; /* malformed report */ + *key_size = 0; + } - /* Check for the Report ID key */ - if (key == 0x85/*Report ID*/) { - /* This device has a Report ID, which means it uses - numbered reports. */ + /* + * This is a Short Item. The bottom two bits of the + * key contain the size code for the data section + * (value) for this key. Refer to the HID + * specification, version 1.11, section 6.2.2.2, + * titled "Short Items." + */ + size_code = key & 0x3; + switch (size_code) { + case 0: + case 1: + case 2: + *data_len = size_code; + *key_size = 1; + return 1; + case 3: + *data_len = 4; + *key_size = 1; + return 1; + default: + /* Can't ever happen since size_code is & 0x3 */ + *data_len = 0; + *key_size = 0; + break; + }; + + /* malformed report */ + return 0; +} + +/* + * Get bytes from a HID Report Descriptor. + * Only call with a num_bytes of 0, 1, 2, or 4. + */ +static __u32 get_hid_report_bytes(const __u8 *rpt, size_t len, size_t num_bytes, size_t cur) +{ + /* Return if there aren't enough bytes. */ + if (cur + num_bytes >= len) + return 0; + + if (num_bytes == 0) + return 0; + else if (num_bytes == 1) + return rpt[cur + 1]; + else if (num_bytes == 2) + return (rpt[cur + 2] * 256 + rpt[cur + 1]); + else if (num_bytes == 4) + return ( + rpt[cur + 4] * 0x01000000 + + rpt[cur + 3] * 0x00010000 + + rpt[cur + 2] * 0x00000100 + + rpt[cur + 1] * 0x00000001 + ); + else + return 0; +} + +/* + * Iterates until the end of a Collection. + * Assumes that *pos is exactly at the beginning of a Collection. + * Skips all nested Collection, i.e. iterates until the end of current level Collection. + * + * The return value is non-0 when an end of current Collection is found, + * 0 when error is occured (broken Descriptor, end of a Collection is found before its begin, + * or no Collection is found at all). + */ +static int hid_iterate_over_collection(const __u8 *report_descriptor, __u32 size, unsigned int *pos, int *data_len, int *key_size) +{ + int collection_level = 0; + + while (*pos < size) { + int key = report_descriptor[*pos]; + int key_cmd = key & 0xfc; + + /* Determine data_len and key_size */ + if (!get_hid_item_size(report_descriptor, size, *pos, data_len, key_size)) + return 0; /* malformed report */ + + switch (key_cmd) { + case 0xa0: /* Collection 6.2.2.4 (Main) */ + collection_level++; + break; + case 0xc0: /* End Collection 6.2.2.4 (Main) */ + collection_level--; + break; + } + + if (collection_level < 0) { + /* Broken descriptor or someone is using this function wrong, + * i.e. should be called exactly at the collection start */ + return 0; + } + + if (collection_level == 0) { + /* Found it! + * Also possible when called not at the collection start, but should not happen if used correctly */ return 1; } - //printf("key: %02hhx\n", key); - - if ((key & 0xf0) == 0xf0) { - /* This is a Long Item. The next byte contains the - length of the data section (value) for this key. - See the HID specification, version 1.11, section - 6.2.2.3, titled "Long Items." */ - if (i+1 < size) - data_len = report_descriptor[i+1]; - else - data_len = 0; /* malformed report */ - key_size = 3; - } - else { - /* This is a Short Item. The bottom two bits of the - key contain the size code for the data section - (value) for this key. Refer to the HID - specification, version 1.11, section 6.2.2.2, - titled "Short Items." */ - size_code = key & 0x3; - switch (size_code) { - case 0: - case 1: - case 2: - data_len = size_code; - break; - case 3: - data_len = 4; - break; - default: - /* Can't ever happen since size_code is & 0x3 */ - data_len = 0; - break; - }; - key_size = 1; - } - - /* Skip over this key and it's associated data */ - i += data_len + key_size; + *pos += *data_len + *key_size; } - /* Didn't find a Report ID key. Device doesn't use numbered reports. */ + return 0; /* Did not find the end of a Collection */ +} + +struct hid_usage_iterator { + unsigned int pos; + int usage_page_found; + unsigned short usage_page; +}; + +/* + * Retrieves the device's Usage Page and Usage from the report descriptor. + * The algorithm returns the current Usage Page/Usage pair whenever a new + * Collection is found and a Usage Local Item is currently in scope. + * Usage Local Items are consumed by each Main Item (See. 6.2.2.8). + * The algorithm should give similar results as Apple's: + * https://developer.apple.com/documentation/iokit/kiohiddeviceusagepairskey?language=objc + * Physical Collections are also matched (macOS does the same). + * + * This function can be called repeatedly until it returns non-0 + * Usage is found. pos is the starting point (initially 0) and will be updated + * to the next search position. + * + * The return value is 0 when a pair is found. + * 1 when finished processing descriptor. + * -1 on a malformed report. + */ +static int get_next_hid_usage(const __u8 *report_descriptor, __u32 size, struct hid_usage_iterator *ctx, unsigned short *usage_page, unsigned short *usage) +{ + int data_len, key_size; + int initial = ctx->pos == 0; /* Used to handle case where no top-level application collection is defined */ + + int usage_found = 0; + + while (ctx->pos < size) { + int key = report_descriptor[ctx->pos]; + int key_cmd = key & 0xfc; + + /* Determine data_len and key_size */ + if (!get_hid_item_size(report_descriptor, size, ctx->pos, &data_len, &key_size)) + return -1; /* malformed report */ + + switch (key_cmd) { + case 0x4: /* Usage Page 6.2.2.7 (Global) */ + ctx->usage_page = get_hid_report_bytes(report_descriptor, size, data_len, ctx->pos); + ctx->usage_page_found = 1; + break; + + case 0x8: /* Usage 6.2.2.8 (Local) */ + if (data_len == 4) { /* Usages 5.5 / Usage Page 6.2.2.7 */ + ctx->usage_page = get_hid_report_bytes(report_descriptor, size, 2, ctx->pos + 2); + ctx->usage_page_found = 1; + *usage = get_hid_report_bytes(report_descriptor, size, 2, ctx->pos); + usage_found = 1; + } + else { + *usage = get_hid_report_bytes(report_descriptor, size, data_len, ctx->pos); + usage_found = 1; + } + break; + + case 0xa0: /* Collection 6.2.2.4 (Main) */ + if (!hid_iterate_over_collection(report_descriptor, size, &ctx->pos, &data_len, &key_size)) { + return -1; + } + + /* A pair is valid - to be reported when Collection is found */ + if (usage_found && ctx->usage_page_found) { + *usage_page = ctx->usage_page; + return 0; + } + + break; + } + + /* Skip over this key and its associated data */ + ctx->pos += data_len + key_size; + } + + /* If no top-level application collection is found and usage page/usage pair is found, pair is valid + https://docs.microsoft.com/en-us/windows-hardware/drivers/hid/top-level-collections */ + if (initial && usage_found && ctx->usage_page_found) { + *usage_page = ctx->usage_page; + return 0; /* success */ + } + + return 1; /* finished processing */ +} + +/* + * Retrieves the hidraw report descriptor from a file. + * When using this form, /device/report_descriptor, elevated privileges are not required. + */ +static int get_hid_report_descriptor(const char *rpt_path, struct hidraw_report_descriptor *rpt_desc) +{ + int rpt_handle; + ssize_t res; + + rpt_handle = open(rpt_path, O_RDONLY | FD_CLOEXEC); + if (rpt_handle < 0) { + register_global_error_format("open failed (%s): %s", rpt_path, strerror(errno)); + return -1; + } + + /* + * Read in the Report Descriptor + * The sysfs file has a maximum size of 4096 (which is the same as HID_MAX_DESCRIPTOR_SIZE) so we should always + * be ok when reading the descriptor. + * In practice if the HID descriptor is any larger I suspect many other things will break. + */ + memset(rpt_desc, 0x0, sizeof(*rpt_desc)); + res = read(rpt_handle, rpt_desc->value, HID_MAX_DESCRIPTOR_SIZE); + if (res < 0) { + register_global_error_format("read failed (%s): %s", rpt_path, strerror(errno)); + } + rpt_desc->size = (__u32) res; + + close(rpt_handle); + return (int) res; +} + +/* return size of the descriptor, or -1 on failure */ +static int get_hid_report_descriptor_from_sysfs(const char *sysfs_path, struct hidraw_report_descriptor *rpt_desc) +{ + int res = -1; + /* Construct /device/report_descriptor */ + size_t rpt_path_len = strlen(sysfs_path) + 25 + 1; + char* rpt_path = (char*) calloc(1, rpt_path_len); + snprintf(rpt_path, rpt_path_len, "%s/device/report_descriptor", sysfs_path); + + res = get_hid_report_descriptor(rpt_path, rpt_desc); + free(rpt_path); + + return res; +} + +/* return non-zero if successfully parsed */ +static int parse_hid_vid_pid_from_uevent(const char *uevent, unsigned *bus_type, unsigned short *vendor_id, unsigned short *product_id) +{ + char tmp[1024]; + size_t uevent_len = strlen(uevent); + if (uevent_len > sizeof(tmp) - 1) + uevent_len = sizeof(tmp) - 1; + memcpy(tmp, uevent, uevent_len); + tmp[uevent_len] = '\0'; + + char *saveptr = NULL; + char *line; + char *key; + char *value; + + line = strtok_r(tmp, "\n", &saveptr); + while (line != NULL) { + /* line: "KEY=value" */ + key = line; + value = strchr(line, '='); + if (!value) { + goto next_line; + } + *value = '\0'; + value++; + + if (strcmp(key, "HID_ID") == 0) { + /** + * type vendor product + * HID_ID=0003:000005AC:00008242 + **/ + int ret = sscanf(value, "%x:%hx:%hx", bus_type, vendor_id, product_id); + if (ret == 3) { + return 1; + } + } + +next_line: + line = strtok_r(NULL, "\n", &saveptr); + } + + register_global_error("Couldn't find/parse HID_ID"); return 0; } +/* return non-zero if successfully parsed */ +static int parse_hid_vid_pid_from_uevent_path(const char *uevent_path, unsigned *bus_type, unsigned short *vendor_id, unsigned short *product_id) +{ + int handle; + ssize_t res; + + handle = open(uevent_path, O_RDONLY | FD_CLOEXEC); + if (handle < 0) { + register_global_error_format("open failed (%s): %s", uevent_path, strerror(errno)); + return 0; + } + + char buf[1024]; + res = read(handle, buf, sizeof(buf) - 1); /* -1 for '\0' at the end */ + close(handle); + + if (res < 0) { + register_global_error_format("read failed (%s): %s", uevent_path, strerror(errno)); + return 0; + } + + buf[res] = '\0'; + return parse_hid_vid_pid_from_uevent(buf, bus_type, vendor_id, product_id); +} + +/* return non-zero if successfully read/parsed */ +static int parse_hid_vid_pid_from_sysfs(const char *sysfs_path, unsigned *bus_type, unsigned short *vendor_id, unsigned short *product_id) +{ + int res = 0; + /* Construct /device/uevent */ + size_t uevent_path_len = strlen(sysfs_path) + 14 + 1; + char* uevent_path = (char*) calloc(1, uevent_path_len); + snprintf(uevent_path, uevent_path_len, "%s/device/uevent", sysfs_path); + + res = parse_hid_vid_pid_from_uevent_path(uevent_path, bus_type, vendor_id, product_id); + free(uevent_path); + + return res; +} + +static int get_hid_report_descriptor_from_hidraw(hid_device *dev, struct hidraw_report_descriptor *rpt_desc) +{ + int desc_size = 0; + + /* Get Report Descriptor Size */ + int res = ioctl(dev->device_handle, HIDIOCGRDESCSIZE, &desc_size); + if (res < 0) { + register_device_error_format(dev, "ioctl(GRDESCSIZE): %s", strerror(errno)); + return res; + } + + /* Get Report Descriptor */ + memset(rpt_desc, 0x0, sizeof(*rpt_desc)); + rpt_desc->size = desc_size; + res = ioctl(dev->device_handle, HIDIOCGRDESC, rpt_desc); + if (res < 0) { + register_device_error_format(dev, "ioctl(GRDESC): %s", strerror(errno)); + } + + return res; +} + /* * The caller is responsible for free()ing the (newly-allocated) character * strings pointed to by serial_number_utf8 and product_name_utf8 after use. */ -static int -parse_uevent_info(const char *uevent, int *bus_type, +static int parse_uevent_info(const char *uevent, unsigned *bus_type, unsigned short *vendor_id, unsigned short *product_id, char **serial_number_utf8, char **product_name_utf8) { - char *tmp = strdup(uevent); + char tmp[1024]; + + if (!uevent) { + return 0; + } + + size_t uevent_len = strlen(uevent); + if (uevent_len > sizeof(tmp) - 1) + uevent_len = sizeof(tmp) - 1; + memcpy(tmp, uevent, uevent_len); + tmp[uevent_len] = '\0'; + char *saveptr = NULL; char *line; char *key; @@ -254,134 +624,288 @@ next_line: line = strtok_r(NULL, "\n", &saveptr); } - free(tmp); return (found_id && found_name && found_serial); } -static int get_device_string(hid_device *dev, enum device_string_id key, wchar_t *string, size_t maxlen) +static struct hid_device_info * create_device_info_for_device(struct udev_device *raw_dev) { - struct udev *udev; - struct udev_device *udev_dev, *parent, *hid_dev; - struct stat s; - int ret = -1; - char *serial_number_utf8 = NULL; - char *product_name_utf8 = NULL; + struct hid_device_info *root = NULL; + struct hid_device_info *cur_dev = NULL; - /* Create the udev object */ - udev = udev_new(); - if (!udev) { - printf("Can't create udev\n"); - return -1; + const char *sysfs_path; + const char *dev_path; + const char *str; + struct udev_device *hid_dev; /* The device's HID udev node. */ + struct udev_device *usb_dev; /* The device's USB udev node. */ + struct udev_device *intf_dev; /* The device's interface (in the USB sense). */ + unsigned short dev_vid; + unsigned short dev_pid; + char *serial_number_utf8 = NULL; + char *product_name_utf8 = NULL; + unsigned bus_type; + int result; + struct hidraw_report_descriptor report_desc; + + sysfs_path = udev_device_get_syspath(raw_dev); + dev_path = udev_device_get_devnode(raw_dev); + + hid_dev = udev_device_get_parent_with_subsystem_devtype( + raw_dev, + "hid", + NULL); + + if (!hid_dev) { + /* Unable to find parent hid device. */ + goto end; } - /* Get the dev_t (major/minor numbers) from the file handle. */ - ret = fstat(dev->device_handle, &s); - if (-1 == ret) - return ret; - /* Open a udev device from the dev_t. 'c' means character device. */ - udev_dev = udev_device_new_from_devnum(udev, 'c', s.st_rdev); - if (udev_dev) { - hid_dev = udev_device_get_parent_with_subsystem_devtype( - udev_dev, - "hid", - NULL); - if (hid_dev) { - unsigned short dev_vid; - unsigned short dev_pid; - int bus_type; - size_t retm; + result = parse_uevent_info( + udev_device_get_sysattr_value(hid_dev, "uevent"), + &bus_type, + &dev_vid, + &dev_pid, + &serial_number_utf8, + &product_name_utf8); - ret = parse_uevent_info( - udev_device_get_sysattr_value(hid_dev, "uevent"), - &bus_type, - &dev_vid, - &dev_pid, - &serial_number_utf8, - &product_name_utf8); + if (!result) { + /* parse_uevent_info() failed for at least one field. */ + goto end; + } - if (bus_type == BUS_BLUETOOTH) { - switch (key) { - case DEVICE_STRING_MANUFACTURER: - wcsncpy(string, L"", maxlen); - ret = 0; - break; - case DEVICE_STRING_PRODUCT: - retm = mbstowcs(string, product_name_utf8, maxlen); - ret = (retm == (size_t)-1)? -1: 0; - break; - case DEVICE_STRING_SERIAL: - retm = mbstowcs(string, serial_number_utf8, maxlen); - ret = (retm == (size_t)-1)? -1: 0; - break; - case DEVICE_STRING_COUNT: - default: - ret = -1; - break; - } + /* Filter out unhandled devices right away */ + switch (bus_type) { + case BUS_BLUETOOTH: + case BUS_I2C: + case BUS_USB: + case BUS_SPI: + break; + + default: + goto end; + } + + /* Create the record. */ + root = (struct hid_device_info*) calloc(1, sizeof(struct hid_device_info)); + if (!root) + goto end; + + cur_dev = root; + + /* Fill out the record */ + cur_dev->next = NULL; + cur_dev->path = dev_path? strdup(dev_path): NULL; + + /* VID/PID */ + cur_dev->vendor_id = dev_vid; + cur_dev->product_id = dev_pid; + + /* Serial Number */ + cur_dev->serial_number = utf8_to_wchar_t(serial_number_utf8); + + /* Release Number */ + cur_dev->release_number = 0x0; + + /* Interface Number */ + cur_dev->interface_number = -1; + + switch (bus_type) { + case BUS_USB: + /* The device pointed to by raw_dev contains information about + the hidraw device. In order to get information about the + USB device, get the parent device with the + subsystem/devtype pair of "usb"/"usb_device". This will + be several levels up the tree, but the function will find + it. */ + usb_dev = udev_device_get_parent_with_subsystem_devtype( + raw_dev, + "usb", + "usb_device"); + + /* uhid USB devices + * Since this is a virtual hid interface, no USB information will + * be available. */ + if (!usb_dev) { + /* Manufacturer and Product strings */ + cur_dev->manufacturer_string = wcsdup(L""); + cur_dev->product_string = utf8_to_wchar_t(product_name_utf8); + break; } - else { - /* This is a USB device. Find its parent USB Device node. */ - parent = udev_device_get_parent_with_subsystem_devtype( - udev_dev, - "usb", - "usb_device"); - if (parent) { - const char *str; - const char *key_str = NULL; - if (key >= 0 && key < DEVICE_STRING_COUNT) { - key_str = device_string_names[key]; - } else { - ret = -1; - goto end; - } + cur_dev->manufacturer_string = copy_udev_string(usb_dev, "manufacturer"); + cur_dev->product_string = copy_udev_string(usb_dev, "product"); - str = udev_device_get_sysattr_value(parent, key_str); - if (str) { - /* Convert the string from UTF-8 to wchar_t */ - retm = mbstowcs(string, str, maxlen); - ret = (retm == (size_t)-1)? -1: 0; - goto end; - } - } + cur_dev->bus_type = HID_API_BUS_USB; + + str = udev_device_get_sysattr_value(usb_dev, "bcdDevice"); + cur_dev->release_number = (str)? strtol(str, NULL, 16): 0x0; + + /* Get a handle to the interface's udev node. */ + intf_dev = udev_device_get_parent_with_subsystem_devtype( + raw_dev, + "usb", + "usb_interface"); + if (intf_dev) { + str = udev_device_get_sysattr_value(intf_dev, "bInterfaceNumber"); + cur_dev->interface_number = (str)? strtol(str, NULL, 16): -1; } + + break; + + case BUS_BLUETOOTH: + cur_dev->manufacturer_string = wcsdup(L""); + cur_dev->product_string = utf8_to_wchar_t(product_name_utf8); + + cur_dev->bus_type = HID_API_BUS_BLUETOOTH; + + break; + case BUS_I2C: + cur_dev->manufacturer_string = wcsdup(L""); + cur_dev->product_string = utf8_to_wchar_t(product_name_utf8); + + cur_dev->bus_type = HID_API_BUS_I2C; + + break; + + case BUS_SPI: + cur_dev->manufacturer_string = wcsdup(L""); + cur_dev->product_string = utf8_to_wchar_t(product_name_utf8); + + cur_dev->bus_type = HID_API_BUS_SPI; + + break; + + default: + /* Unknown device type - this should never happen, as we + * check for USB and Bluetooth devices above */ + break; + } + + /* Usage Page and Usage */ + result = get_hid_report_descriptor_from_sysfs(sysfs_path, &report_desc); + if (result >= 0) { + unsigned short page = 0, usage = 0; + struct hid_usage_iterator usage_iterator; + memset(&usage_iterator, 0, sizeof(usage_iterator)); + + /* + * Parse the first usage and usage page + * out of the report descriptor. + */ + if (!get_next_hid_usage(report_desc.value, report_desc.size, &usage_iterator, &page, &usage)) { + cur_dev->usage_page = page; + cur_dev->usage = usage; + } + + /* + * Parse any additional usage and usage pages + * out of the report descriptor. + */ + while (!get_next_hid_usage(report_desc.value, report_desc.size, &usage_iterator, &page, &usage)) { + /* Create new record for additional usage pairs */ + struct hid_device_info *tmp = (struct hid_device_info*) calloc(1, sizeof(struct hid_device_info)); + struct hid_device_info *prev_dev = cur_dev; + + if (!tmp) + continue; + cur_dev->next = tmp; + cur_dev = tmp; + + /* Update fields */ + cur_dev->path = dev_path? strdup(dev_path): NULL; + cur_dev->vendor_id = dev_vid; + cur_dev->product_id = dev_pid; + cur_dev->serial_number = prev_dev->serial_number? wcsdup(prev_dev->serial_number): NULL; + cur_dev->release_number = prev_dev->release_number; + cur_dev->interface_number = prev_dev->interface_number; + cur_dev->manufacturer_string = prev_dev->manufacturer_string? wcsdup(prev_dev->manufacturer_string): NULL; + cur_dev->product_string = prev_dev->product_string? wcsdup(prev_dev->product_string): NULL; + cur_dev->usage_page = page; + cur_dev->usage = usage; + cur_dev->bus_type = prev_dev->bus_type; } } end: - free(serial_number_utf8); - free(product_name_utf8); + free(serial_number_utf8); + free(product_name_utf8); + + return root; +} + +static struct hid_device_info * create_device_info_for_hid_device(hid_device *dev) { + struct udev *udev; + struct udev_device *udev_dev; + struct stat s; + int ret = -1; + struct hid_device_info *root = NULL; + + register_device_error(dev, NULL); + + /* Get the dev_t (major/minor numbers) from the file handle. */ + ret = fstat(dev->device_handle, &s); + if (-1 == ret) { + register_device_error(dev, "Failed to stat device handle"); + return NULL; + } + + /* Create the udev object */ + udev = udev_new(); + if (!udev) { + register_device_error(dev, "Couldn't create udev context"); + return NULL; + } + + /* Open a udev device from the dev_t. 'c' means character device. */ + udev_dev = udev_device_new_from_devnum(udev, 'c', s.st_rdev); + if (udev_dev) { + root = create_device_info_for_device(udev_dev); + } + + if (!root) { + /* TODO: have a better error reporting via create_device_info_for_device */ + register_device_error(dev, "Couldn't create hid_device_info"); + } udev_device_unref(udev_dev); - /* parent and hid_dev don't need to be (and can't be) unref'd. - I'm not sure why, but they'll throw double-free() errors. */ udev_unref(udev); - return ret; + return root; +} + +HID_API_EXPORT const struct hid_api_version* HID_API_CALL hid_version(void) +{ + return &api_version; +} + +HID_API_EXPORT const char* HID_API_CALL hid_version_str(void) +{ + return HID_API_VERSION_STR; } int HID_API_EXPORT hid_init(void) { const char *locale; + /* indicate no error */ + register_global_error(NULL); + /* Set the locale if it's not set. */ locale = setlocale(LC_CTYPE, NULL); if (!locale) setlocale(LC_CTYPE, ""); - kernel_version = detect_kernel_version(); - return 0; } int HID_API_EXPORT hid_exit(void) { - /* Nothing to do for this in the Linux/hidraw implementation. */ + /* Free global error message */ + register_global_error(NULL); + return 0; } - struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) { struct udev *udev; @@ -390,14 +914,14 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, struct hid_device_info *root = NULL; /* return object */ struct hid_device_info *cur_dev = NULL; - struct hid_device_info *prev_dev = NULL; /* previous device */ hid_init(); + /* register_global_error: global error is reset by hid_init */ /* Create the udev object */ udev = udev_new(); if (!udev) { - printf("Can't create udev\n"); + register_global_error("Couldn't create udev context"); return NULL; } @@ -410,163 +934,62 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, create a udev_device record for it */ udev_list_entry_foreach(dev_list_entry, devices) { const char *sysfs_path; - const char *dev_path; - const char *str; + unsigned short dev_vid = 0; + unsigned short dev_pid = 0; + unsigned bus_type = 0; struct udev_device *raw_dev; /* The device's hidraw udev node. */ - struct udev_device *hid_dev; /* The device's HID udev node. */ - struct udev_device *usb_dev; /* The device's USB udev node. */ - struct udev_device *intf_dev; /* The device's interface (in the USB sense). */ - unsigned short dev_vid; - unsigned short dev_pid; - char *serial_number_utf8 = NULL; - char *product_name_utf8 = NULL; - int bus_type; - int result; + struct hid_device_info * tmp; /* Get the filename of the /sys entry for the device and create a udev_device object (dev) representing it */ sysfs_path = udev_list_entry_get_name(dev_list_entry); + if (!sysfs_path) + continue; + + if (vendor_id != 0 || product_id != 0) { + if (!parse_hid_vid_pid_from_sysfs(sysfs_path, &bus_type, &dev_vid, &dev_pid)) + continue; + + if (vendor_id != 0 && vendor_id != dev_vid) + continue; + if (product_id != 0 && product_id != dev_pid) + continue; + } + raw_dev = udev_device_new_from_syspath(udev, sysfs_path); - dev_path = udev_device_get_devnode(raw_dev); + if (!raw_dev) + continue; - hid_dev = udev_device_get_parent_with_subsystem_devtype( - raw_dev, - "hid", - NULL); - - if (!hid_dev) { - /* Unable to find parent hid device. */ - goto next; - } - - result = parse_uevent_info( - udev_device_get_sysattr_value(hid_dev, "uevent"), - &bus_type, - &dev_vid, - &dev_pid, - &serial_number_utf8, - &product_name_utf8); - - if (!result) { - /* parse_uevent_info() failed for at least one field. */ - goto next; - } - - if (bus_type != BUS_USB && bus_type != BUS_BLUETOOTH) { - /* We only know how to handle USB and BT devices. */ - goto next; - } - - /* Check the VID/PID against the arguments */ - if ((vendor_id == 0x0 || vendor_id == dev_vid) && - (product_id == 0x0 || product_id == dev_pid)) { - struct hid_device_info *tmp; - - /* VID/PID match. Create the record. */ - tmp = malloc(sizeof(struct hid_device_info)); + tmp = create_device_info_for_device(raw_dev); + if (tmp) { if (cur_dev) { cur_dev->next = tmp; } else { root = tmp; } - prev_dev = cur_dev; cur_dev = tmp; - /* Fill out the record */ - cur_dev->next = NULL; - cur_dev->path = dev_path? strdup(dev_path): NULL; - - /* VID/PID */ - cur_dev->vendor_id = dev_vid; - cur_dev->product_id = dev_pid; - - /* Serial Number */ - cur_dev->serial_number = utf8_to_wchar_t(serial_number_utf8); - - /* Release Number */ - cur_dev->release_number = 0x0; - - /* Interface Number */ - cur_dev->interface_number = -1; - - switch (bus_type) { - case BUS_USB: - /* The device pointed to by raw_dev contains information about - the hidraw device. In order to get information about the - USB device, get the parent device with the - subsystem/devtype pair of "usb"/"usb_device". This will - be several levels up the tree, but the function will find - it. */ - usb_dev = udev_device_get_parent_with_subsystem_devtype( - raw_dev, - "usb", - "usb_device"); - - if (!usb_dev) { - /* Free this device */ - free(cur_dev->serial_number); - free(cur_dev->path); - free(cur_dev); - - /* Take it off the device list. */ - if (prev_dev) { - prev_dev->next = NULL; - cur_dev = prev_dev; - } - else { - cur_dev = root = NULL; - } - - goto next; - } - - /* Manufacturer and Product strings */ - cur_dev->manufacturer_string = copy_udev_string(usb_dev, device_string_names[DEVICE_STRING_MANUFACTURER]); - cur_dev->product_string = copy_udev_string(usb_dev, device_string_names[DEVICE_STRING_PRODUCT]); - - /* Release Number */ - str = udev_device_get_sysattr_value(usb_dev, "bcdDevice"); - cur_dev->release_number = (str)? strtol(str, NULL, 16): 0x0; - - /* Get a handle to the interface's udev node. */ - intf_dev = udev_device_get_parent_with_subsystem_devtype( - raw_dev, - "usb", - "usb_interface"); - if (intf_dev) { - str = udev_device_get_sysattr_value(intf_dev, "bInterfaceNumber"); - cur_dev->interface_number = (str)? strtol(str, NULL, 16): -1; - } - - break; - - case BUS_BLUETOOTH: - /* Manufacturer and Product strings */ - cur_dev->manufacturer_string = wcsdup(L""); - cur_dev->product_string = utf8_to_wchar_t(product_name_utf8); - - break; - - default: - /* Unknown device type - this should never happen, as we - * check for USB and Bluetooth devices above */ - break; + /* move the pointer to the tail of returned list */ + while (cur_dev->next != NULL) { + cur_dev = cur_dev->next; } } - next: - free(serial_number_utf8); - free(product_name_utf8); udev_device_unref(raw_dev); - /* hid_dev, usb_dev and intf_dev don't need to be (and can't be) - unref()d. It will cause a double-free() error. I'm not - sure why. */ } /* Free the enumerator and udev objects. */ udev_enumerate_unref(enumerate); udev_unref(udev); + if (root == NULL) { + if (vendor_id == 0 && product_id == 0) { + register_global_error("No HID devices found in the system."); + } else { + register_global_error("No HID devices with requested VID/PID found in the system."); + } + } + return root; } @@ -590,7 +1013,13 @@ hid_device * hid_open(unsigned short vendor_id, unsigned short product_id, const const char *path_to_open = NULL; hid_device *handle = NULL; + /* register_global_error: global error is reset by hid_enumerate/hid_init */ devs = hid_enumerate(vendor_id, product_id); + if (devs == NULL) { + /* register_global_error: global error is already set by hid_enumerate */ + return NULL; + } + cur_dev = devs; while (cur_dev) { if (cur_dev->vendor_id == vendor_id && @@ -612,6 +1041,8 @@ hid_device * hid_open(unsigned short vendor_id, unsigned short product_id, const if (path_to_open) { /* Open the device */ handle = hid_open_path(path_to_open); + } else { + register_global_error("Device with requested VID/PID/(SerialNumber) not found"); } hid_free_enumeration(devs); @@ -624,44 +1055,33 @@ hid_device * HID_API_EXPORT hid_open_path(const char *path) hid_device *dev = NULL; hid_init(); + /* register_global_error: global error is reset by hid_init */ dev = new_hid_device(); + if (!dev) { + register_global_error("Couldn't allocate memory"); + return NULL; + } - /* OPEN HERE */ - dev->device_handle = open(path, O_RDWR); + dev->device_handle = open(path, O_RDWR | FD_CLOEXEC); - /* If we have a good handle, return it. */ - if (dev->device_handle > 0) { - - /* Get the report descriptor */ + if (dev->device_handle >= 0) { int res, desc_size = 0; - struct hidraw_report_descriptor rpt_desc; - memset(&rpt_desc, 0x0, sizeof(rpt_desc)); - - /* Get Report Descriptor Size */ + /* Make sure this is a HIDRAW device - responds to HIDIOCGRDESCSIZE */ res = ioctl(dev->device_handle, HIDIOCGRDESCSIZE, &desc_size); - if (res < 0) - perror("HIDIOCGRDESCSIZE"); - - - /* Get Report Descriptor */ - rpt_desc.size = desc_size; - res = ioctl(dev->device_handle, HIDIOCGRDESC, &rpt_desc); if (res < 0) { - perror("HIDIOCGRDESC"); - } else { - /* Determine if this device uses numbered reports. */ - dev->uses_numbered_reports = - uses_numbered_reports(rpt_desc.value, - rpt_desc.size); + hid_close(dev); + register_global_error_format("ioctl(GRDESCSIZE) error for '%s', not a HIDRAW device?: %s", path, strerror(errno)); + return NULL; } return dev; } else { - /* Unable to open any devices. */ + /* Unable to open a device. */ free(dev); + register_global_error_format("Failed to open a device with path '%s': %s", path, strerror(errno)); return NULL; } } @@ -671,14 +1091,25 @@ int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t { int bytes_written; + if (!data || (length == 0)) { + errno = EINVAL; + register_device_error(dev, strerror(errno)); + return -1; + } + bytes_written = write(dev->device_handle, data, length); + register_device_error(dev, (bytes_written == -1)? strerror(errno): NULL); + return bytes_written; } int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds) { + /* Set device error to none */ + register_device_error(dev, NULL); + int bytes_read; if (milliseconds >= 0) { @@ -695,29 +1126,32 @@ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t fds.events = POLLIN; fds.revents = 0; ret = poll(&fds, 1, milliseconds); - if (ret == -1 || ret == 0) { - /* Error or timeout */ + if (ret == 0) { + /* Timeout */ + return ret; + } + if (ret == -1) { + /* Error */ + register_device_error(dev, strerror(errno)); return ret; } else { /* Check for errors on the file descriptor. This will indicate a device disconnection. */ - if (fds.revents & (POLLERR | POLLHUP | POLLNVAL)) + if (fds.revents & (POLLERR | POLLHUP | POLLNVAL)) { + // We cannot use strerror() here as no -1 was returned from poll(). + register_device_error(dev, "hid_read_timeout: unexpected poll error (device disconnected)"); return -1; + } } } bytes_read = read(dev->device_handle, data, length); - if (bytes_read < 0 && (errno == EAGAIN || errno == EINPROGRESS)) - bytes_read = 0; - - if (bytes_read >= 0 && - kernel_version != 0 && - kernel_version < KERNEL_VERSION(2,6,34) && - dev->uses_numbered_reports) { - /* Work around a kernel bug. Chop off the first byte. */ - memmove(data, data+1, bytes_read); - bytes_read--; + if (bytes_read < 0) { + if (errno == EAGAIN || errno == EINPROGRESS) + bytes_read = 0; + else + register_device_error(dev, strerror(errno)); } return bytes_read; @@ -743,9 +1177,11 @@ int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char { int res; + register_device_error(dev, NULL); + res = ioctl(dev->device_handle, HIDIOCSFEATURE(length), data); if (res < 0) - perror("ioctl (SFEATURE)"); + register_device_error_format(dev, "ioctl (SFEATURE): %s", strerror(errno)); return res; } @@ -754,46 +1190,181 @@ int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, { int res; + register_device_error(dev, NULL); + res = ioctl(dev->device_handle, HIDIOCGFEATURE(length), data); if (res < 0) - perror("ioctl (GFEATURE)"); - + register_device_error_format(dev, "ioctl (GFEATURE): %s", strerror(errno)); return res; } +int HID_API_EXPORT HID_API_CALL hid_send_output_report(hid_device *dev, const unsigned char *data, size_t length) +{ + int res; + + register_device_error(dev, NULL); + + res = ioctl(dev->device_handle, HIDIOCSOUTPUT(length), data); + if (res < 0) + register_device_error_format(dev, "ioctl (SOUTPUT): %s", strerror(errno)); + + return res; +} + +int HID_API_EXPORT HID_API_CALL hid_get_input_report(hid_device *dev, unsigned char *data, size_t length) +{ + int res; + + register_device_error(dev, NULL); + + res = ioctl(dev->device_handle, HIDIOCGINPUT(length), data); + if (res < 0) + register_device_error_format(dev, "ioctl (GINPUT): %s", strerror(errno)); + + return res; +} void HID_API_EXPORT hid_close(hid_device *dev) { if (!dev) return; + close(dev->device_handle); + + /* Free the device error message */ + register_device_error(dev, NULL); + + hid_free_enumeration(dev->device_info); + free(dev); } int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen) { - return get_device_string(dev, DEVICE_STRING_MANUFACTURER, string, maxlen); + if (!string || !maxlen) { + register_device_error(dev, "Zero buffer/length"); + return -1; + } + + struct hid_device_info *info = hid_get_device_info(dev); + if (!info) { + // hid_get_device_info will have set an error already + return -1; + } + + if (info->manufacturer_string) { + wcsncpy(string, info->manufacturer_string, maxlen); + string[maxlen - 1] = L'\0'; + } + else { + string[0] = L'\0'; + } + + return 0; } int HID_API_EXPORT_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen) { - return get_device_string(dev, DEVICE_STRING_PRODUCT, string, maxlen); + if (!string || !maxlen) { + register_device_error(dev, "Zero buffer/length"); + return -1; + } + + struct hid_device_info *info = hid_get_device_info(dev); + if (!info) { + // hid_get_device_info will have set an error already + return -1; + } + + if (info->product_string) { + wcsncpy(string, info->product_string, maxlen); + string[maxlen - 1] = L'\0'; + } + else { + string[0] = L'\0'; + } + + return 0; } int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen) { - return get_device_string(dev, DEVICE_STRING_SERIAL, string, maxlen); + if (!string || !maxlen) { + register_device_error(dev, "Zero buffer/length"); + return -1; + } + + struct hid_device_info *info = hid_get_device_info(dev); + if (!info) { + // hid_get_device_info will have set an error already + return -1; + } + + if (info->serial_number) { + wcsncpy(string, info->serial_number, maxlen); + string[maxlen - 1] = L'\0'; + } + else { + string[0] = L'\0'; + } + + return 0; +} + + +HID_API_EXPORT struct hid_device_info *HID_API_CALL hid_get_device_info(hid_device *dev) { + if (!dev->device_info) { + // Lazy initialize device_info + dev->device_info = create_device_info_for_hid_device(dev); + } + + // create_device_info_for_hid_device will set an error if needed + return dev->device_info; } int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen) { + (void)string_index; + (void)string; + (void)maxlen; + + register_device_error(dev, "hid_get_indexed_string: not supported by hidraw"); + return -1; } +int HID_API_EXPORT_CALL hid_get_report_descriptor(hid_device *dev, unsigned char *buf, size_t buf_size) +{ + struct hidraw_report_descriptor rpt_desc; + int res = get_hid_report_descriptor_from_hidraw(dev, &rpt_desc); + if (res < 0) { + /* error already registered */ + return res; + } + + if (rpt_desc.size < buf_size) { + buf_size = (size_t) rpt_desc.size; + } + + memcpy(buf, rpt_desc.value, buf_size); + + return (int) buf_size; +} + + +/* Passing in NULL means asking for the last global error message. */ HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) { - return NULL; + if (dev) { + if (dev->last_error_str == NULL) + return L"Success"; + return dev->last_error_str; + } + + if (last_global_error_str == NULL) + return L"Success"; + return last_global_error_str; } diff --git a/libs/hidapi/mac/hid.c b/libs/hidapi/mac/hid.c index e0756a1588..e2b365c4f3 100644 --- a/libs/hidapi/mac/hid.c +++ b/libs/hidapi/mac/hid.c @@ -5,9 +5,9 @@ Alan Ott Signal 11 Software - 2010-07-03 + libusb/hidapi Team - Copyright 2010, All Rights Reserved. + Copyright 2022, All Rights Reserved. At the discretion of the user of this library, this software may be licensed under the terms of the @@ -17,7 +17,7 @@ files located at the root of the source distribution. These files may also be found in the public source code repository located at: - http://github.com/signal11/hidapi . + https://github.com/libusb/hidapi . ********************************************************/ /* See Apple Technical Note TN2187 for details on IOHidManager. */ @@ -25,7 +25,10 @@ #include #include #include +#include #include +#include +#include #include #include #include @@ -33,7 +36,7 @@ #include #include -#include "hidapi.h" +#include "hidapi_darwin.h" /* Barrier implementation because Mac OSX doesn't have pthread_barrier. It also doesn't have clock_gettime(). So much for POSIX and SUSv2. @@ -49,15 +52,17 @@ typedef struct pthread_barrier { static int pthread_barrier_init(pthread_barrier_t *barrier, const pthread_barrierattr_t *attr, unsigned int count) { - if(count == 0) { + (void) attr; + + if (count == 0) { errno = EINVAL; return -1; } - if(pthread_mutex_init(&barrier->mutex, 0) < 0) { + if (pthread_mutex_init(&barrier->mutex, 0) < 0) { return -1; } - if(pthread_cond_init(&barrier->cond, 0) < 0) { + if (pthread_cond_init(&barrier->cond, 0) < 0) { pthread_mutex_destroy(&barrier->mutex); return -1; } @@ -78,16 +83,18 @@ static int pthread_barrier_wait(pthread_barrier_t *barrier) { pthread_mutex_lock(&barrier->mutex); ++(barrier->count); - if(barrier->count >= barrier->trip_count) - { + if (barrier->count >= barrier->trip_count) { barrier->count = 0; - pthread_cond_broadcast(&barrier->cond); pthread_mutex_unlock(&barrier->mutex); + pthread_cond_broadcast(&barrier->cond); return 1; } - else - { - pthread_cond_wait(&barrier->cond, &(barrier->mutex)); + else { + do { + pthread_cond_wait(&barrier->cond, &(barrier->mutex)); + } + while (barrier->count != 0); + pthread_mutex_unlock(&barrier->mutex); return 0; } @@ -102,10 +109,23 @@ struct input_report { struct input_report *next; }; +static struct hid_api_version api_version = { + .major = HID_API_VERSION_MAJOR, + .minor = HID_API_VERSION_MINOR, + .patch = HID_API_VERSION_PATCH +}; + +/* - Run context - */ +static IOHIDManagerRef hid_mgr = 0x0; +static int is_macos_10_10_or_greater = 0; +static IOOptionBits device_open_options = 0; +static wchar_t *last_global_error_str = NULL; +/* --- */ + struct hid_device_ { IOHIDDeviceRef device_handle; + IOOptionBits open_options; int blocking; - int uses_numbered_reports; int disconnected; CFStringRef run_loop_mode; CFRunLoopRef run_loop; @@ -113,6 +133,7 @@ struct hid_device_ { uint8_t *input_report_buf; CFIndex max_input_report_len; struct input_report *input_reports; + struct hid_device_info* device_info; pthread_t thread; pthread_mutex_t mutex; /* Protects input_reports */ @@ -120,21 +141,28 @@ struct hid_device_ { pthread_barrier_t barrier; /* Ensures correct startup sequence */ pthread_barrier_t shutdown_barrier; /* Ensures correct shutdown sequence */ int shutdown_thread; + wchar_t *last_error_str; }; static hid_device *new_hid_device(void) { - hid_device *dev = calloc(1, sizeof(hid_device)); + hid_device *dev = (hid_device*) calloc(1, sizeof(hid_device)); + if (dev == NULL) { + return NULL; + } + dev->device_handle = NULL; + dev->open_options = device_open_options; dev->blocking = 1; - dev->uses_numbered_reports = 0; dev->disconnected = 0; dev->run_loop_mode = NULL; dev->run_loop = NULL; dev->source = NULL; dev->input_report_buf = NULL; dev->input_reports = NULL; + dev->device_info = NULL; dev->shutdown_thread = 0; + dev->last_error_str = NULL; /* Thread objects */ pthread_mutex_init(&dev->mutex, NULL); @@ -167,6 +195,7 @@ static void free_hid_device(hid_device *dev) if (dev->source) CFRelease(dev->source); free(dev->input_report_buf); + hid_free_enumeration(dev->device_info); /* Clean up the thread objects */ pthread_barrier_destroy(&dev->shutdown_barrier); @@ -178,21 +207,102 @@ static void free_hid_device(hid_device *dev) free(dev); } -static IOHIDManagerRef hid_mgr = 0x0; - -#if 0 -static void register_error(hid_device *device, const char *op) +/* The caller must free the returned string with free(). */ +static wchar_t *utf8_to_wchar_t(const char *utf8) { + wchar_t *ret = NULL; + if (utf8) { + size_t wlen = mbstowcs(NULL, utf8, 0); + if ((size_t) -1 == wlen) { + return wcsdup(L""); + } + ret = (wchar_t*) calloc(wlen+1, sizeof(wchar_t)); + if (ret == NULL) { + /* as much as we can do at this point */ + return NULL; + } + mbstowcs(ret, utf8, wlen+1); + ret[wlen] = 0x0000; + } + + return ret; } -#endif +/* Makes a copy of the given error message (and decoded according to the + * currently locale) into the wide string pointer pointed by error_str. + * The last stored error string is freed. + * Use register_error_str(NULL) to free the error message completely. */ +static void register_error_str(wchar_t **error_str, const char *msg) +{ + free(*error_str); + *error_str = utf8_to_wchar_t(msg); +} + +/* Similar to register_error_str, but allows passing a format string with va_list args into this function. */ +static void register_error_str_vformat(wchar_t **error_str, const char *format, va_list args) +{ + char msg[1024]; + vsnprintf(msg, sizeof(msg), format, args); + + register_error_str(error_str, msg); +} + +/* Set the last global error to be reported by hid_error(NULL). + * The given error message will be copied (and decoded according to the + * currently locale, so do not pass in string constants). + * The last stored global error message is freed. + * Use register_global_error(NULL) to indicate "no error". */ +static void register_global_error(const char *msg) +{ + register_error_str(&last_global_error_str, msg); +} + +/* Similar to register_global_error, but allows passing a format string into this function. */ +static void register_global_error_format(const char *format, ...) +{ + va_list args; + va_start(args, format); + register_error_str_vformat(&last_global_error_str, format, args); + va_end(args); +} + +/* Set the last error for a device to be reported by hid_error(dev). + * The given error message will be copied (and decoded according to the + * currently locale, so do not pass in string constants). + * The last stored device error message is freed. + * Use register_device_error(dev, NULL) to indicate "no error". */ +static void register_device_error(hid_device *dev, const char *msg) +{ + register_error_str(&dev->last_error_str, msg); +} + +/* Similar to register_device_error, but you can pass a format string into this function. */ +static void register_device_error_format(hid_device *dev, const char *format, ...) +{ + va_list args; + va_start(args, format); + register_error_str_vformat(&dev->last_error_str, format, args); + va_end(args); +} + + +static CFArrayRef get_array_property(IOHIDDeviceRef device, CFStringRef key) +{ + CFTypeRef ref = IOHIDDeviceGetProperty(device, key); + if (ref != NULL && CFGetTypeID(ref) == CFArrayGetTypeID()) { + return (CFArrayRef)ref; + } else { + return NULL; + } +} + static int32_t get_int_property(IOHIDDeviceRef device, CFStringRef key) { CFTypeRef ref; - int32_t value; + int32_t value = 0; ref = IOHIDDeviceGetProperty(device, key); if (ref) { @@ -204,6 +314,41 @@ static int32_t get_int_property(IOHIDDeviceRef device, CFStringRef key) return 0; } +static bool try_get_int_property(IOHIDDeviceRef device, CFStringRef key, int32_t *out_val) +{ + bool result = false; + CFTypeRef ref; + + ref = IOHIDDeviceGetProperty(device, key); + if (ref) { + if (CFGetTypeID(ref) == CFNumberGetTypeID()) { + result = CFNumberGetValue((CFNumberRef) ref, kCFNumberSInt32Type, out_val); + } + } + return result; +} + +static bool try_get_ioregistry_int_property(io_service_t service, CFStringRef property, int32_t *out_val) +{ + bool result = false; + CFTypeRef ref = IORegistryEntryCreateCFProperty(service, property, kCFAllocatorDefault, 0); + + if (ref) { + if (CFGetTypeID(ref) == CFNumberGetTypeID()) { + result = CFNumberGetValue(ref, kCFNumberSInt32Type, out_val); + } + + CFRelease(ref); + } + + return result; +} + +static CFArrayRef get_usage_pairs(IOHIDDeviceRef device) +{ + return get_array_property(device, CFSTR(kIOHIDDeviceUsagePairsKey)); +} + static unsigned short get_vendor_id(IOHIDDeviceRef device) { return get_int_property(device, CFSTR(kIOHIDVendorIDKey)); @@ -226,11 +371,11 @@ static int get_string_property(IOHIDDeviceRef device, CFStringRef prop, wchar_t if (!len) return 0; - str = IOHIDDeviceGetProperty(device, prop); + str = (CFStringRef) IOHIDDeviceGetProperty(device, prop); buf[0] = 0; - if (str) { + if (str && CFGetTypeID(str) == CFStringGetTypeID()) { CFIndex str_len = CFStringGetLength(str); CFRange range; CFIndex used_buf_len; @@ -239,18 +384,18 @@ static int get_string_property(IOHIDDeviceRef device, CFStringRef prop, wchar_t len --; range.location = 0; - range.length = ((size_t)str_len > len)? len: (size_t)str_len; + range.length = ((size_t) str_len > len)? len: (size_t) str_len; chars_copied = CFStringGetBytes(str, range, kCFStringEncodingUTF32LE, - (char)'?', + (char) '?', FALSE, (UInt8*)buf, len * sizeof(wchar_t), &used_buf_len); - if (chars_copied == len) - buf[len] = 0; /* len is decremented above */ + if (chars_copied <= 0) + buf[0] = 0; else buf[chars_copied] = 0; @@ -281,69 +426,12 @@ static int get_product_string(IOHIDDeviceRef device, wchar_t *buf, size_t len) static wchar_t *dup_wcs(const wchar_t *s) { size_t len = wcslen(s); - wchar_t *ret = malloc((len+1)*sizeof(wchar_t)); + wchar_t *ret = (wchar_t*) malloc((len+1)*sizeof(wchar_t)); wcscpy(ret, s); return ret; } -/* hidapi_IOHIDDeviceGetService() - * - * Return the io_service_t corresponding to a given IOHIDDeviceRef, either by: - * - on OS X 10.6 and above, calling IOHIDDeviceGetService() - * - on OS X 10.5, extract it from the IOHIDDevice struct - */ -static io_service_t hidapi_IOHIDDeviceGetService(IOHIDDeviceRef device) -{ - static void *iokit_framework = NULL; - static io_service_t (*dynamic_IOHIDDeviceGetService)(IOHIDDeviceRef device) = NULL; - - /* Use dlopen()/dlsym() to get a pointer to IOHIDDeviceGetService() if it exists. - * If any of these steps fail, dynamic_IOHIDDeviceGetService will be left NULL - * and the fallback method will be used. - */ - if (iokit_framework == NULL) { - iokit_framework = dlopen("/System/Library/IOKit.framework/IOKit", RTLD_LAZY); - - if (iokit_framework != NULL) - dynamic_IOHIDDeviceGetService = dlsym(iokit_framework, "IOHIDDeviceGetService"); - } - - if (dynamic_IOHIDDeviceGetService != NULL) { - /* Running on OS X 10.6 and above: IOHIDDeviceGetService() exists */ - return dynamic_IOHIDDeviceGetService(device); - } - else - { - /* Running on OS X 10.5: IOHIDDeviceGetService() doesn't exist. - * - * Be naughty and pull the service out of the IOHIDDevice. - * IOHIDDevice is an opaque struct not exposed to applications, but its - * layout is stable through all available versions of OS X. - * Tested and working on OS X 10.5.8 i386, x86_64, and ppc. - */ - struct IOHIDDevice_internal { - /* The first field of the IOHIDDevice struct is a - * CFRuntimeBase (which is a private CF struct). - * - * a, b, and c are the 3 fields that make up a CFRuntimeBase. - * See http://opensource.apple.com/source/CF/CF-476.18/CFRuntime.h - * - * The second field of the IOHIDDevice is the io_service_t we're looking for. - */ - uintptr_t a; - uint8_t b[4]; -#if __LP64__ - uint32_t c; -#endif - io_service_t service; - }; - struct IOHIDDevice_internal *tmp = (struct IOHIDDevice_internal *)device; - - return tmp->service; - } -} - /* Initialize the IOHIDManager. Return 0 for success and -1 for failure. */ static int init_hid_manager(void) { @@ -355,15 +443,30 @@ static int init_hid_manager(void) return 0; } + register_global_error("Failed to create IOHIDManager"); return -1; } +HID_API_EXPORT const struct hid_api_version* HID_API_CALL hid_version(void) +{ + return &api_version; +} + +HID_API_EXPORT const char* HID_API_CALL hid_version_str(void) +{ + return HID_API_VERSION_STR; +} + /* Initialize the IOHIDManager if necessary. This is the public function, and it is safe to call this function repeatedly. Return 0 for success and -1 for failure. */ int HID_API_EXPORT hid_init(void) { + register_global_error(NULL); + if (!hid_mgr) { + is_macos_10_10_or_greater = (kCFCoreFoundationVersionNumber >= 1151.16); /* kCFCoreFoundationVersionNumber10_10 */ + hid_darwin_set_open_exclusive(1); /* Backward compatibility */ return init_hid_manager(); } @@ -380,6 +483,9 @@ int HID_API_EXPORT hid_exit(void) hid_mgr = NULL; } + /* Free global error message */ + register_global_error(NULL); + return 0; } @@ -390,6 +496,206 @@ static void process_pending_events(void) { } while(res != kCFRunLoopRunFinished && res != kCFRunLoopRunTimedOut); } +static int read_usb_interface_from_hid_service_parent(io_service_t hid_service) +{ + int32_t result = -1; + bool success = false; + io_registry_entry_t current = IO_OBJECT_NULL; + kern_return_t res; + int parent_number = 0; + + res = IORegistryEntryGetParentEntry(hid_service, kIOServicePlane, ¤t); + while (KERN_SUCCESS == res + /* Only search up to 3 parent entries. + * With the default driver - the parent-of-interest supposed to be the first one, + * but lets assume some custom drivers or so, with deeper tree. */ + && parent_number < 3) { + io_registry_entry_t parent = IO_OBJECT_NULL; + int32_t interface_number = -1; + parent_number++; + + success = try_get_ioregistry_int_property(current, CFSTR(kUSBInterfaceNumber), &interface_number); + if (success) { + result = interface_number; + break; + } + + res = IORegistryEntryGetParentEntry(current, kIOServicePlane, &parent); + if (parent) { + IOObjectRelease(current); + current = parent; + } + + } + + if (current) { + IOObjectRelease(current); + current = IO_OBJECT_NULL; + } + + return result; +} + +static struct hid_device_info *create_device_info_with_usage(IOHIDDeviceRef dev, int32_t usage_page, int32_t usage) +{ + unsigned short dev_vid; + unsigned short dev_pid; + int BUF_LEN = 256; + wchar_t buf[BUF_LEN]; + CFTypeRef transport_prop; + + struct hid_device_info *cur_dev; + io_service_t hid_service; + kern_return_t res; + uint64_t entry_id = 0; + + if (dev == NULL) { + return NULL; + } + + cur_dev = (struct hid_device_info *)calloc(1, sizeof(struct hid_device_info)); + if (cur_dev == NULL) { + return NULL; + } + + dev_vid = get_vendor_id(dev); + dev_pid = get_product_id(dev); + + cur_dev->usage_page = usage_page; + cur_dev->usage = usage; + + /* Fill out the record */ + cur_dev->next = NULL; + + /* Fill in the path (as a unique ID of the service entry) */ + cur_dev->path = NULL; + hid_service = IOHIDDeviceGetService(dev); + if (hid_service != MACH_PORT_NULL) { + res = IORegistryEntryGetRegistryEntryID(hid_service, &entry_id); + } + else { + res = KERN_INVALID_ARGUMENT; + } + + if (res == KERN_SUCCESS) { + /* max value of entry_id(uint64_t) is 18446744073709551615 which is 20 characters long, + so for (max) "path" string 'DevSrvsID:18446744073709551615' we would need + 9+1+20+1=31 bytes buffer, but allocate 32 for simple alignment */ + const size_t path_len = 32; + cur_dev->path = calloc(1, path_len); + if (cur_dev->path != NULL) { + snprintf(cur_dev->path, path_len, "DevSrvsID:%llu", entry_id); + } + } + + if (cur_dev->path == NULL) { + /* for whatever reason, trying to keep it a non-NULL string */ + cur_dev->path = strdup(""); + } + + /* Serial Number */ + get_serial_number(dev, buf, BUF_LEN); + cur_dev->serial_number = dup_wcs(buf); + + /* Manufacturer and Product strings */ + get_manufacturer_string(dev, buf, BUF_LEN); + cur_dev->manufacturer_string = dup_wcs(buf); + get_product_string(dev, buf, BUF_LEN); + cur_dev->product_string = dup_wcs(buf); + + /* VID/PID */ + cur_dev->vendor_id = dev_vid; + cur_dev->product_id = dev_pid; + + /* Release Number */ + cur_dev->release_number = get_int_property(dev, CFSTR(kIOHIDVersionNumberKey)); + + /* Interface Number. + * We can only retrieve the interface number for USB HID devices. + * See below */ + cur_dev->interface_number = -1; + + /* Bus Type */ + transport_prop = IOHIDDeviceGetProperty(dev, CFSTR(kIOHIDTransportKey)); + + if (transport_prop != NULL && CFGetTypeID(transport_prop) == CFStringGetTypeID()) { + if (CFStringCompare((CFStringRef)transport_prop, CFSTR(kIOHIDTransportUSBValue), 0) == kCFCompareEqualTo) { + int32_t interface_number = -1; + cur_dev->bus_type = HID_API_BUS_USB; + + /* A IOHIDDeviceRef used to have this simple property, + * until macOS 13.3 - we will try to use it. */ + if (try_get_int_property(dev, CFSTR(kUSBInterfaceNumber), &interface_number)) { + cur_dev->interface_number = interface_number; + } else { + /* Otherwise fallback to io_service_t property. + * (of one of the parent services). */ + cur_dev->interface_number = read_usb_interface_from_hid_service_parent(hid_service); + + /* If the above doesn't work - + * no (known) fallback exists at this point. */ + } + + /* Match "Bluetooth", "BluetoothLowEnergy" and "Bluetooth Low Energy" strings */ + } else if (CFStringHasPrefix((CFStringRef)transport_prop, CFSTR(kIOHIDTransportBluetoothValue))) { + cur_dev->bus_type = HID_API_BUS_BLUETOOTH; + } else if (CFStringCompare((CFStringRef)transport_prop, CFSTR(kIOHIDTransportI2CValue), 0) == kCFCompareEqualTo) { + cur_dev->bus_type = HID_API_BUS_I2C; + } else if (CFStringCompare((CFStringRef)transport_prop, CFSTR(kIOHIDTransportSPIValue), 0) == kCFCompareEqualTo) { + cur_dev->bus_type = HID_API_BUS_SPI; + } + } + + return cur_dev; +} + +static struct hid_device_info *create_device_info(IOHIDDeviceRef device) +{ + const int32_t primary_usage_page = get_int_property(device, CFSTR(kIOHIDPrimaryUsagePageKey)); + const int32_t primary_usage = get_int_property(device, CFSTR(kIOHIDPrimaryUsageKey)); + + /* Primary should always be first, to match previous behavior. */ + struct hid_device_info *root = create_device_info_with_usage(device, primary_usage_page, primary_usage); + struct hid_device_info *cur = root; + + if (!root) + return NULL; + + CFArrayRef usage_pairs = get_usage_pairs(device); + + if (usage_pairs != NULL) { + struct hid_device_info *next = NULL; + for (CFIndex i = 0; i < CFArrayGetCount(usage_pairs); i++) { + CFTypeRef dict = CFArrayGetValueAtIndex(usage_pairs, i); + if (CFGetTypeID(dict) != CFDictionaryGetTypeID()) { + continue; + } + + CFTypeRef usage_page_ref, usage_ref; + int32_t usage_page, usage; + + if (!CFDictionaryGetValueIfPresent((CFDictionaryRef)dict, CFSTR(kIOHIDDeviceUsagePageKey), &usage_page_ref) || + !CFDictionaryGetValueIfPresent((CFDictionaryRef)dict, CFSTR(kIOHIDDeviceUsageKey), &usage_ref) || + CFGetTypeID(usage_page_ref) != CFNumberGetTypeID() || + CFGetTypeID(usage_ref) != CFNumberGetTypeID() || + !CFNumberGetValue((CFNumberRef)usage_page_ref, kCFNumberSInt32Type, &usage_page) || + !CFNumberGetValue((CFNumberRef)usage_ref, kCFNumberSInt32Type, &usage)) { + continue; + } + if (usage_page == primary_usage_page && usage == primary_usage) + continue; /* Already added. */ + + next = create_device_info_with_usage(device, usage_page, usage); + cur->next = next; + if (next != NULL) { + cur = next; + } + } + } + + return root; +} + struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) { struct hid_device_info *root = NULL; /* return object */ @@ -398,93 +704,87 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, int i; /* Set up the HID Manager if it hasn't been done */ - if (hid_init() < 0) + if (hid_init() < 0) { return NULL; + } + /* register_global_error: global error is set/reset by hid_init */ /* give the IOHIDManager a chance to update itself */ process_pending_events(); /* Get a list of the Devices */ - IOHIDManagerSetDeviceMatching(hid_mgr, NULL); + CFMutableDictionaryRef matching = NULL; + if (vendor_id != 0 || product_id != 0) { + matching = CFDictionaryCreateMutable(kCFAllocatorDefault, kIOHIDOptionsTypeNone, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + + if (matching && vendor_id != 0) { + CFNumberRef v = CFNumberCreate(kCFAllocatorDefault, kCFNumberShortType, &vendor_id); + CFDictionarySetValue(matching, CFSTR(kIOHIDVendorIDKey), v); + CFRelease(v); + } + + if (matching && product_id != 0) { + CFNumberRef p = CFNumberCreate(kCFAllocatorDefault, kCFNumberShortType, &product_id); + CFDictionarySetValue(matching, CFSTR(kIOHIDProductIDKey), p); + CFRelease(p); + } + } + IOHIDManagerSetDeviceMatching(hid_mgr, matching); + if (matching != NULL) { + CFRelease(matching); + } + CFSetRef device_set = IOHIDManagerCopyDevices(hid_mgr); - /* Convert the list into a C array so we can iterate easily. */ - num_devices = CFSetGetCount(device_set); - IOHIDDeviceRef *device_array = calloc(num_devices, sizeof(IOHIDDeviceRef)); - CFSetGetValues(device_set, (const void **) device_array); + IOHIDDeviceRef *device_array = NULL; + + if (device_set != NULL) { + /* Convert the list into a C array so we can iterate easily. */ + num_devices = CFSetGetCount(device_set); + device_array = (IOHIDDeviceRef*) calloc(num_devices, sizeof(IOHIDDeviceRef)); + CFSetGetValues(device_set, (const void **) device_array); + } else { + num_devices = 0; + } /* Iterate over each device, making an entry for it. */ for (i = 0; i < num_devices; i++) { - unsigned short dev_vid; - unsigned short dev_pid; - #define BUF_LEN 256 - wchar_t buf[BUF_LEN]; IOHIDDeviceRef dev = device_array[i]; + if (!dev) { + continue; + } - if (!dev) { - continue; - } - dev_vid = get_vendor_id(dev); - dev_pid = get_product_id(dev); + struct hid_device_info *tmp = create_device_info(dev); + if (tmp == NULL) { + continue; + } - /* Check the VID/PID against the arguments */ - if ((vendor_id == 0x0 || vendor_id == dev_vid) && - (product_id == 0x0 || product_id == dev_pid)) { - struct hid_device_info *tmp; - io_object_t iokit_dev; - kern_return_t res; - io_string_t path; + if (cur_dev) { + cur_dev->next = tmp; + } + else { + root = tmp; + } + cur_dev = tmp; - /* VID/PID match. Create the record. */ - tmp = malloc(sizeof(struct hid_device_info)); - if (cur_dev) { - cur_dev->next = tmp; - } - else { - root = tmp; - } - cur_dev = tmp; - - /* Get the Usage Page and Usage for this device. */ - cur_dev->usage_page = get_int_property(dev, CFSTR(kIOHIDPrimaryUsagePageKey)); - cur_dev->usage = get_int_property(dev, CFSTR(kIOHIDPrimaryUsageKey)); - - /* Fill out the record */ - cur_dev->next = NULL; - - /* Fill in the path (IOService plane) */ - iokit_dev = hidapi_IOHIDDeviceGetService(dev); - res = IORegistryEntryGetPath(iokit_dev, kIOServicePlane, path); - if (res == KERN_SUCCESS) - cur_dev->path = strdup(path); - else - cur_dev->path = strdup(""); - - /* Serial Number */ - get_serial_number(dev, buf, BUF_LEN); - cur_dev->serial_number = dup_wcs(buf); - - /* Manufacturer and Product strings */ - get_manufacturer_string(dev, buf, BUF_LEN); - cur_dev->manufacturer_string = dup_wcs(buf); - get_product_string(dev, buf, BUF_LEN); - cur_dev->product_string = dup_wcs(buf); - - /* VID/PID */ - cur_dev->vendor_id = dev_vid; - cur_dev->product_id = dev_pid; - - /* Release Number */ - cur_dev->release_number = get_int_property(dev, CFSTR(kIOHIDVersionNumberKey)); - - /* Interface Number (Unsupported on Mac)*/ - cur_dev->interface_number = -1; + /* move the pointer to the tail of returned list */ + while (cur_dev->next != NULL) { + cur_dev = cur_dev->next; } } free(device_array); - CFRelease(device_set); + if (device_set != NULL) + CFRelease(device_set); + + if (root == NULL) { + if (vendor_id == 0 && product_id == 0) { + register_global_error("No HID devices found in the system."); + } else { + register_global_error("No HID devices with requested VID/PID found in the system."); + } + } return root; } @@ -507,11 +807,18 @@ void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) hid_device * HID_API_EXPORT hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) { /* This function is identical to the Linux version. Platform independent. */ + struct hid_device_info *devs, *cur_dev; const char *path_to_open = NULL; hid_device * handle = NULL; + /* register_global_error: global error is reset by hid_enumerate/hid_init */ devs = hid_enumerate(vendor_id, product_id); + if (devs == NULL) { + /* register_global_error: global error is already set by hid_enumerate */ + return NULL; + } + cur_dev = devs; while (cur_dev) { if (cur_dev->vendor_id == vendor_id && @@ -531,8 +838,9 @@ hid_device * HID_API_EXPORT hid_open(unsigned short vendor_id, unsigned short pr } if (path_to_open) { - /* Open the device */ handle = hid_open_path(path_to_open); + } else { + register_global_error("Device with requested VID/PID/(SerialNumber) not found"); } hid_free_enumeration(devs); @@ -543,8 +851,11 @@ hid_device * HID_API_EXPORT hid_open(unsigned short vendor_id, unsigned short pr static void hid_device_removal_callback(void *context, IOReturn result, void *sender) { + (void) result; + (void) sender; + /* Stop the Run Loop for this device. */ - hid_device *d = context; + hid_device *d = (hid_device*) context; d->disconnected = 1; CFRunLoopStop(d->run_loop); @@ -557,12 +868,17 @@ static void hid_report_callback(void *context, IOReturn result, void *sender, IOHIDReportType report_type, uint32_t report_id, uint8_t *report, CFIndex report_length) { + (void) result; + (void) sender; + (void) report_type; + (void) report_id; + struct input_report *rpt; - hid_device *dev = context; + hid_device *dev = (hid_device*) context; /* Make a new Input Report object */ - rpt = calloc(1, sizeof(struct input_report)); - rpt->data = calloc(1, report_length); + rpt = (struct input_report*) calloc(1, sizeof(struct input_report)); + rpt->data = (uint8_t*) calloc(1, report_length); memcpy(rpt->data, report, report_length); rpt->len = report_length; rpt->next = NULL; @@ -605,13 +921,13 @@ static void hid_report_callback(void *context, IOReturn result, void *sender, hid_close(), and serves to stop the read_thread's run loop. */ static void perform_signal_callback(void *context) { - hid_device *dev = context; + hid_device *dev = (hid_device*) context; CFRunLoopStop(dev->run_loop); /*TODO: CFRunLoopGetCurrent()*/ } static void *read_thread(void *param) { - hid_device *dev = param; + hid_device *dev = (hid_device*) param; SInt32 code; /* Move the device's run loop to this thread. */ @@ -639,7 +955,7 @@ static void *read_thread(void *param) while (!dev->shutdown_thread && !dev->disconnected) { code = CFRunLoopRunInMode(dev->run_loop_mode, 1000/*sec*/, FALSE); /* Return if the device has been disconnected */ - if (code == kCFRunLoopRunFinished) { + if (code == kCFRunLoopRunFinished || code == kCFRunLoopRunStopped) { dev->disconnected = 1; break; } @@ -673,26 +989,57 @@ static void *read_thread(void *param) return NULL; } -/* hid_open_path() - * - * path must be a valid path to an IOHIDDevice in the IOService plane - * Example: "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/EHC1@1D,7/AppleUSBEHCI/PLAYSTATION(R)3 Controller@fd120000/IOUSBInterface@0/IOUSBHIDDriver" - */ +/* \p path must be one of: + - in format 'DevSrvsID:' (as returned by hid_enumerate); + - a valid path to an IOHIDDevice in the IOService plane (as returned by IORegistryEntryGetPath, + e.g.: "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/EHC1@1D,7/AppleUSBEHCI/PLAYSTATION(R)3 Controller@fd120000/IOUSBInterface@0/IOUSBHIDDriver"); + Second format is for compatibility with paths accepted by older versions of HIDAPI. +*/ +static io_registry_entry_t hid_open_service_registry_from_path(const char *path) +{ + if (path == NULL) + return MACH_PORT_NULL; + + /* Get the IORegistry entry for the given path */ + if (strncmp("DevSrvsID:", path, 10) == 0) { + char *endptr; + uint64_t entry_id = strtoull(path + 10, &endptr, 10); + if (*endptr == '\0') { + return IOServiceGetMatchingService((mach_port_t) 0, IORegistryEntryIDMatching(entry_id)); + } + } + else { + /* Fallback to older format of the path */ + return IORegistryEntryFromPath((mach_port_t) 0, path); + } + + return MACH_PORT_NULL; +} + hid_device * HID_API_EXPORT hid_open_path(const char *path) { hid_device *dev = NULL; io_registry_entry_t entry = MACH_PORT_NULL; - - dev = new_hid_device(); + IOReturn ret = kIOReturnInvalid; + char str[32]; /* Set up the HID Manager if it hasn't been done */ - if (hid_init() < 0) + if (hid_init() < 0) { return NULL; + } + /* register_global_error: global error is set/reset by hid_init */ + + dev = new_hid_device(); + if (!dev) { + register_global_error("Couldn't allocate memory"); + return NULL; + } /* Get the IORegistry entry for the given path */ - entry = IORegistryEntryFromPath(kIOMasterPortDefault, path); + entry = hid_open_service_registry_from_path(path); if (entry == MACH_PORT_NULL) { /* Path wasn't valid (maybe device was removed?) */ + register_global_error("hid_open_path: device mach entry not found with the given path"); goto return_error; } @@ -700,43 +1047,42 @@ hid_device * HID_API_EXPORT hid_open_path(const char *path) dev->device_handle = IOHIDDeviceCreate(kCFAllocatorDefault, entry); if (dev->device_handle == NULL) { /* Error creating the HID device */ + register_global_error("hid_open_path: failed to create IOHIDDevice from the mach entry"); goto return_error; } /* Open the IOHIDDevice */ - IOReturn ret = IOHIDDeviceOpen(dev->device_handle, kIOHIDOptionsTypeSeizeDevice); - if (ret == kIOReturnSuccess) { - char str[32]; - - /* Create the buffers for receiving data */ - dev->max_input_report_len = (CFIndex) get_max_report_length(dev->device_handle); - dev->input_report_buf = calloc(dev->max_input_report_len, sizeof(uint8_t)); - - /* Create the Run Loop Mode for this device. - printing the reference seems to work. */ - sprintf(str, "HIDAPI_%p", dev->device_handle); - dev->run_loop_mode = - CFStringCreateWithCString(NULL, str, kCFStringEncodingASCII); - - /* Attach the device to a Run Loop */ - IOHIDDeviceRegisterInputReportCallback( - dev->device_handle, dev->input_report_buf, dev->max_input_report_len, - &hid_report_callback, dev); - IOHIDDeviceRegisterRemovalCallback(dev->device_handle, hid_device_removal_callback, dev); - - /* Start the read thread */ - pthread_create(&dev->thread, NULL, read_thread, dev); - - /* Wait here for the read thread to be initialized. */ - pthread_barrier_wait(&dev->barrier); - - IOObjectRelease(entry); - return dev; - } - else { + ret = IOHIDDeviceOpen(dev->device_handle, dev->open_options); + if (ret != kIOReturnSuccess) { + register_global_error_format("hid_open_path: failed to open IOHIDDevice from mach entry: (0x%08X) %s", ret, mach_error_string(ret)); goto return_error; } + /* Create the buffers for receiving data */ + dev->max_input_report_len = (CFIndex) get_max_report_length(dev->device_handle); + dev->input_report_buf = (uint8_t*) calloc(dev->max_input_report_len, sizeof(uint8_t)); + + /* Create the Run Loop Mode for this device. + printing the reference seems to work. */ + snprintf(str, sizeof(str), "HIDAPI_%p", (void*) dev->device_handle); + dev->run_loop_mode = + CFStringCreateWithCString(NULL, str, kCFStringEncodingASCII); + + /* Attach the device to a Run Loop */ + IOHIDDeviceRegisterInputReportCallback( + dev->device_handle, dev->input_report_buf, dev->max_input_report_len, + &hid_report_callback, dev); + IOHIDDeviceRegisterRemovalCallback(dev->device_handle, hid_device_removal_callback, dev); + + /* Start the read thread */ + pthread_create(&dev->thread, NULL, read_thread, dev); + + /* Wait here for the read thread to be initialized. */ + pthread_barrier_wait(&dev->barrier); + + IOObjectRelease(entry); + return dev; + return_error: if (dev->device_handle != NULL) CFRelease(dev->device_handle); @@ -750,41 +1096,83 @@ return_error: static int set_report(hid_device *dev, IOHIDReportType type, const unsigned char *data, size_t length) { - const unsigned char *data_to_send; - size_t length_to_send; + const unsigned char *data_to_send = data; + CFIndex length_to_send = length; IOReturn res; + unsigned char report_id; - /* Return if the device has been disconnected. */ - if (dev->disconnected) + register_device_error(dev, NULL); + + if (!data || (length == 0)) { + register_device_error(dev, strerror(EINVAL)); return -1; + } - if (data[0] == 0x0) { + report_id = data[0]; + + if (report_id == 0x0) { /* Not using numbered Reports. Don't send the report number. */ data_to_send = data+1; length_to_send = length-1; } - else { - /* Using numbered Reports. - Send the Report Number */ - data_to_send = data; - length_to_send = length; + + /* Avoid crash if the device has been unplugged. */ + if (dev->disconnected) { + register_device_error(dev, "Device is disconnected"); + return -1; } - if (!dev->disconnected) { - res = IOHIDDeviceSetReport(dev->device_handle, - type, - data[0], /* Report ID*/ - data_to_send, length_to_send); + res = IOHIDDeviceSetReport(dev->device_handle, + type, + report_id, + data_to_send, length_to_send); - if (res == kIOReturnSuccess) { - return length; - } - else - return -1; + if (res != kIOReturnSuccess) { + register_device_error_format(dev, "IOHIDDeviceSetReport failed: (0x%08X) %s", res, mach_error_string(res)); + return -1; } - return -1; + return (int) length; +} + +static int get_report(hid_device *dev, IOHIDReportType type, unsigned char *data, size_t length) +{ + unsigned char *report = data; + CFIndex report_length = length; + IOReturn res = kIOReturnSuccess; + const unsigned char report_id = data[0]; + + register_device_error(dev, NULL); + + if (report_id == 0x0) { + /* Not using numbered Reports. + Don't send the report number. */ + report = data+1; + report_length = length-1; + } + + /* Avoid crash if the device has been unplugged. */ + if (dev->disconnected) { + register_device_error(dev, "Device is disconnected"); + return -1; + } + + res = IOHIDDeviceGetReport(dev->device_handle, + type, + report_id, + report, &report_length); + + if (res != kIOReturnSuccess) { + register_device_error_format(dev, "IOHIDDeviceGetReport failed: (0x%08X) %s", res, mach_error_string(res)); + return -1; + } + + if (report_id == 0x0) { /* 0 report number still present at the beginning */ + report_length++; + } + + return (int) report_length; } int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t length) @@ -799,14 +1187,16 @@ static int return_data(hid_device *dev, unsigned char *data, size_t length) return buffer (data), and delete the liked list item. */ struct input_report *rpt = dev->input_reports; size_t len = (length < rpt->len)? length: rpt->len; - memcpy(data, rpt->data, len); + if (data != NULL) { + memcpy(data, rpt->data, len); + } dev->input_reports = rpt->next; free(rpt->data); free(rpt); - return len; + return (int) len; } -static int cond_wait(const hid_device *dev, pthread_cond_t *cond, pthread_mutex_t *mutex) +static int cond_wait(hid_device *dev, pthread_cond_t *cond, pthread_mutex_t *mutex) { while (!dev->input_reports) { int res = pthread_cond_wait(cond, mutex); @@ -814,19 +1204,20 @@ static int cond_wait(const hid_device *dev, pthread_cond_t *cond, pthread_mutex_ return res; /* A res of 0 means we may have been signaled or it may - be a spurious wakeup. Check to see that there's acutally + be a spurious wakeup. Check to see that there's actually data in the queue before returning, and if not, go back to sleep. See the pthread_cond_timedwait() man page for details. */ - if (dev->shutdown_thread || dev->disconnected) + if (dev->shutdown_thread || dev->disconnected) { return -1; + } } return 0; } -static int cond_timedwait(const hid_device *dev, pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime) +static int cond_timedwait(hid_device *dev, pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime) { while (!dev->input_reports) { int res = pthread_cond_timedwait(cond, mutex, abstime); @@ -834,13 +1225,14 @@ static int cond_timedwait(const hid_device *dev, pthread_cond_t *cond, pthread_m return res; /* A res of 0 means we may have been signaled or it may - be a spurious wakeup. Check to see that there's acutally + be a spurious wakeup. Check to see that there's actually data in the queue before returning, and if not, go back to sleep. See the pthread_cond_timedwait() man page for details. */ - if (dev->shutdown_thread || dev->disconnected) + if (dev->shutdown_thread || dev->disconnected) { return -1; + } } return 0; @@ -864,6 +1256,7 @@ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t /* Return if the device has been disconnected. */ if (dev->disconnected) { bytes_read = -1; + register_device_error(dev, "hid_read_timeout: device disconnected"); goto ret; } @@ -872,6 +1265,7 @@ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t has been an error. An error code of -1 should be returned. */ bytes_read = -1; + register_device_error(dev, "hid_read_timeout: thread shutdown"); goto ret; } @@ -885,6 +1279,7 @@ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t bytes_read = return_data(dev, data, length); else { /* There was an error, or a device disconnection. */ + register_device_error(dev, "hid_read_timeout: error waiting for more data"); bytes_read = -1; } } @@ -903,12 +1298,14 @@ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t } res = cond_timedwait(dev, &dev->condition, &dev->mutex, &ts); - if (res == 0) + if (res == 0) { bytes_read = return_data(dev, data, length); - else if (res == ETIMEDOUT) + } else if (res == ETIMEDOUT) { bytes_read = 0; - else + } else { + register_device_error(dev, "hid_read_timeout: error waiting for more data"); bytes_read = -1; + } } else { /* Purely non-blocking */ @@ -941,31 +1338,28 @@ int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) { - CFIndex len = length; - IOReturn res; - - /* Return if the device has been unplugged. */ - if (dev->disconnected) - return -1; - - res = IOHIDDeviceGetReport(dev->device_handle, - kIOHIDReportTypeFeature, - data[0], /* Report ID */ - data, &len); - if (res == kIOReturnSuccess) - return len; - else - return -1; + return get_report(dev, kIOHIDReportTypeFeature, data, length); } +int HID_API_EXPORT hid_send_output_report(hid_device *dev, const unsigned char *data, size_t length) +{ + return set_report(dev, kIOHIDReportTypeOutput, data, length); +} + +int HID_API_EXPORT HID_API_CALL hid_get_input_report(hid_device *dev, unsigned char *data, size_t length) +{ + return get_report(dev, kIOHIDReportTypeInput, data, length); +} void HID_API_EXPORT hid_close(hid_device *dev) { if (!dev) return; - /* Disconnect the report callback before close. */ - if (!dev->disconnected) { + /* Disconnect the report callback before close. + See comment below. + */ + if (is_macos_10_10_or_greater || !dev->disconnected) { IOHIDDeviceRegisterInputReportCallback( dev->device_handle, dev->input_report_buf, dev->max_input_report_len, NULL, dev); @@ -989,9 +1383,15 @@ void HID_API_EXPORT hid_close(hid_device *dev) /* Close the OS handle to the device, but only if it's not been unplugged. If it's been unplugged, then calling - IOHIDDeviceClose() will crash. */ - if (!dev->disconnected) { - IOHIDDeviceClose(dev->device_handle, kIOHIDOptionsTypeSeizeDevice); + IOHIDDeviceClose() will crash. + + UPD: The crash part was true in/until some version of macOS. + Starting with macOS 10.15, there is an opposite effect in some environments: + crash happenes if IOHIDDeviceClose() is not called. + Not leaking a resource in all tested environments. + */ + if (is_macos_10_10_or_greater || !dev->disconnected) { + IOHIDDeviceClose(dev->device_handle, dev->open_options); } /* Clear out the queue of received reports. */ @@ -1007,104 +1407,151 @@ void HID_API_EXPORT hid_close(hid_device *dev) int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen) { - return get_manufacturer_string(dev->device_handle, string, maxlen); + if (!string || !maxlen) + { + register_device_error(dev, "Zero buffer/length"); + return -1; + } + + struct hid_device_info *info = hid_get_device_info(dev); + if (!info) + { + // hid_get_device_info will have set an error already + return -1; + } + + wcsncpy(string, info->manufacturer_string, maxlen); + string[maxlen - 1] = L'\0'; + + return 0; } int HID_API_EXPORT_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen) { - return get_product_string(dev->device_handle, string, maxlen); + if (!string || !maxlen) { + register_device_error(dev, "Zero buffer/length"); + return -1; + } + + struct hid_device_info *info = hid_get_device_info(dev); + if (!info) { + // hid_get_device_info will have set an error already + return -1; + } + + wcsncpy(string, info->product_string, maxlen); + string[maxlen - 1] = L'\0'; + + return 0; } int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen) { - return get_serial_number(dev->device_handle, string, maxlen); + if (!string || !maxlen) { + register_device_error(dev, "Zero buffer/length"); + return -1; + } + + struct hid_device_info *info = hid_get_device_info(dev); + if (!info) { + // hid_get_device_info will have set an error already + return -1; + } + + wcsncpy(string, info->serial_number, maxlen); + string[maxlen - 1] = L'\0'; + + return 0; +} + +HID_API_EXPORT struct hid_device_info *HID_API_CALL hid_get_device_info(hid_device *dev) { + if (!dev->device_info) { + dev->device_info = create_device_info(dev->device_handle); + if (!dev->device_info) { + register_device_error(dev, "Failed to create hid_device_info"); + } + } + + return dev->device_info; } int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen) { - /* TODO: */ + (void) dev; + (void) string_index; + (void) string; + (void) maxlen; - return 0; + register_device_error(dev, "hid_get_indexed_string: not available on this platform"); + return -1; } +int HID_API_EXPORT_CALL hid_darwin_get_location_id(hid_device *dev, uint32_t *location_id) +{ + int res = get_int_property(dev->device_handle, CFSTR(kIOHIDLocationIDKey)); + if (res != 0) { + *location_id = (uint32_t) res; + return 0; + } else { + register_device_error(dev, "Failed to get IOHIDLocationID property"); + return -1; + } +} + +void HID_API_EXPORT_CALL hid_darwin_set_open_exclusive(int open_exclusive) +{ + device_open_options = (open_exclusive == 0) ? kIOHIDOptionsTypeNone : kIOHIDOptionsTypeSeizeDevice; +} + +int HID_API_EXPORT_CALL hid_darwin_get_open_exclusive(void) +{ + return (device_open_options == kIOHIDOptionsTypeSeizeDevice) ? 1 : 0; +} + +int HID_API_EXPORT_CALL hid_darwin_is_device_open_exclusive(hid_device *dev) +{ + if (!dev) + return -1; + + return (dev->open_options == kIOHIDOptionsTypeSeizeDevice) ? 1 : 0; +} + +int HID_API_EXPORT_CALL hid_get_report_descriptor(hid_device *dev, unsigned char *buf, size_t buf_size) +{ + CFTypeRef ref = IOHIDDeviceGetProperty(dev->device_handle, CFSTR(kIOHIDReportDescriptorKey)); + if (ref != NULL && CFGetTypeID(ref) == CFDataGetTypeID()) { + CFDataRef report_descriptor = (CFDataRef) ref; + const UInt8 *descriptor_buf = CFDataGetBytePtr(report_descriptor); + CFIndex descriptor_buf_len = CFDataGetLength(report_descriptor); + size_t copy_len = (size_t) descriptor_buf_len; + + if (descriptor_buf == NULL || descriptor_buf_len < 0) { + register_device_error(dev, "Zero buffer/length"); + return -1; + } + + if (buf_size < copy_len) { + copy_len = buf_size; + } + + memcpy(buf, descriptor_buf, copy_len); + return copy_len; + } + else { + register_device_error(dev, "Failed to get kIOHIDReportDescriptorKey property"); + return -1; + } +} HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) { - /* TODO: */ - - return NULL; -} - - - - - - - -#if 0 -static int32_t get_location_id(IOHIDDeviceRef device) -{ - return get_int_property(device, CFSTR(kIOHIDLocationIDKey)); -} - -static int32_t get_usage(IOHIDDeviceRef device) -{ - int32_t res; - res = get_int_property(device, CFSTR(kIOHIDDeviceUsageKey)); - if (!res) - res = get_int_property(device, CFSTR(kIOHIDPrimaryUsageKey)); - return res; -} - -static int32_t get_usage_page(IOHIDDeviceRef device) -{ - int32_t res; - res = get_int_property(device, CFSTR(kIOHIDDeviceUsagePageKey)); - if (!res) - res = get_int_property(device, CFSTR(kIOHIDPrimaryUsagePageKey)); - return res; -} - -static int get_transport(IOHIDDeviceRef device, wchar_t *buf, size_t len) -{ - return get_string_property(device, CFSTR(kIOHIDTransportKey), buf, len); -} - - -int main(void) -{ - IOHIDManagerRef mgr; - int i; - - mgr = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); - IOHIDManagerSetDeviceMatching(mgr, NULL); - IOHIDManagerOpen(mgr, kIOHIDOptionsTypeNone); - - CFSetRef device_set = IOHIDManagerCopyDevices(mgr); - - CFIndex num_devices = CFSetGetCount(device_set); - IOHIDDeviceRef *device_array = calloc(num_devices, sizeof(IOHIDDeviceRef)); - CFSetGetValues(device_set, (const void **) device_array); - - for (i = 0; i < num_devices; i++) { - IOHIDDeviceRef dev = device_array[i]; - printf("Device: %p\n", dev); - printf(" %04hx %04hx\n", get_vendor_id(dev), get_product_id(dev)); - - wchar_t serial[256], buf[256]; - char cbuf[256]; - get_serial_number(dev, serial, 256); - - - printf(" Serial: %ls\n", serial); - printf(" Loc: %ld\n", get_location_id(dev)); - get_transport(dev, buf, 256); - printf(" Trans: %ls\n", buf); - make_path(dev, cbuf, 256); - printf(" Path: %s\n", cbuf); - + if (dev) { + if (dev->last_error_str == NULL) + return L"Success"; + return dev->last_error_str; } - return 0; + if (last_global_error_str == NULL) + return L"Success"; + return last_global_error_str; } -#endif diff --git a/libs/hidapi/mac/hidapi_darwin.h b/libs/hidapi/mac/hidapi_darwin.h new file mode 100644 index 0000000000..34c30a07de --- /dev/null +++ b/libs/hidapi/mac/hidapi_darwin.h @@ -0,0 +1,98 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2022, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ + +/** @file + * @defgroup API hidapi API + + * Since version 0.12.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 12, 0) + */ + +#ifndef HIDAPI_DARWIN_H__ +#define HIDAPI_DARWIN_H__ + +#include + +#include "hidapi.h" + +#ifdef __cplusplus +extern "C" { +#endif + + /** @brief Get the location ID for a HID device. + + Since version 0.12.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 12, 0) + + @ingroup API + @param dev A device handle returned from hid_open(). + @param location_id The device's location ID on return. + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT_CALL hid_darwin_get_location_id(hid_device *dev, uint32_t *location_id); + + + /** @brief Changes the behavior of all further calls to @ref hid_open or @ref hid_open_path. + + By default on Darwin platform all devices opened by HIDAPI with @ref hid_open or @ref hid_open_path + are opened in exclusive mode (see kIOHIDOptionsTypeSeizeDevice). + + Since version 0.12.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 12, 0) + + @ingroup API + @param open_exclusive When set to 0 - all further devices will be opened + in non-exclusive mode. Otherwise - all further devices will be opened + in exclusive mode. + + @note During the initialisation by @ref hid_init - this property is set to 1 (TRUE). + This is done to preserve full backward compatibility with previous behavior. + + @note Calling this function before @ref hid_init or after @ref hid_exit has no effect. + */ + void HID_API_EXPORT_CALL hid_darwin_set_open_exclusive(int open_exclusive); + + /** @brief Getter for option set by @ref hid_darwin_set_open_exclusive. + + Since version 0.12.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 12, 0) + + @ingroup API + @return 1 if all further devices will be opened in exclusive mode. + + @note Value returned by this function before calling to @ref hid_init or after @ref hid_exit + is not reliable. + */ + int HID_API_EXPORT_CALL hid_darwin_get_open_exclusive(void); + + /** @brief Check how the device was opened. + + Since version 0.12.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 12, 0) + + @ingroup API + @param dev A device to get property from. + + @return 1 if the device is opened in exclusive mode, 0 - opened in non-exclusive, + -1 - if dev is invalid. + */ + int HID_API_EXPORT_CALL hid_darwin_is_device_open_exclusive(hid_device *dev); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/libs/hidapi/windows/hid.c b/libs/hidapi/windows/hid.c index 86810d7e56..35c2de04e1 100755 --- a/libs/hidapi/windows/hid.c +++ b/libs/hidapi/windows/hid.c @@ -5,10 +5,10 @@ Alan Ott Signal 11 Software - 8/22/2009 + libusb/hidapi Team + + Copyright 2022, All Rights Reserved. - Copyright 2009, All Rights Reserved. - At the discretion of the user of this library, this software may be licensed under the terms of the GNU General Public License v3, a BSD-Style license, or the @@ -17,9 +17,21 @@ files located at the root of the source distribution. These files may also be found in the public source code repository located at: - http://github.com/signal11/hidapi . + https://github.com/libusb/hidapi . ********************************************************/ +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +/* Do not warn about wcsncpy usage. + https://docs.microsoft.com/cpp/c-runtime-library/security-features-in-the-crt */ +#define _CRT_SECURE_NO_WARNINGS +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#include "hidapi_winapi.h" + #include #ifndef _NTDEF_ @@ -29,133 +41,184 @@ typedef LONG NTSTATUS; #ifdef __MINGW32__ #include #include +#define WC_ERR_INVALID_CHARS 0x00000080 #endif #ifdef __CYGWIN__ #include +#include #define _wcsdup wcsdup #endif -/* The maximum number of characters that can be passed into the - HidD_Get*String() functions without it failing.*/ -#define MAX_STRING_WCHARS 0xFFF - /*#define HIDAPI_USE_DDK*/ -#ifdef __cplusplus -extern "C" { -#endif - #include - #include - #ifdef HIDAPI_USE_DDK - #include - #endif - - /* Copied from inc/ddk/hidclass.h, part of the Windows DDK. */ - #define HID_OUT_CTL_CODE(id) \ - CTL_CODE(FILE_DEVICE_KEYBOARD, (id), METHOD_OUT_DIRECT, FILE_ANY_ACCESS) - #define IOCTL_HID_GET_FEATURE HID_OUT_CTL_CODE(100) - -#ifdef __cplusplus -} /* extern "C" */ -#endif +#include "hidapi_cfgmgr32.h" +#include "hidapi_hidclass.h" +#include "hidapi_hidsdi.h" #include #include +#include - -#include "hidapi.h" - +#ifdef MIN #undef MIN +#endif #define MIN(x,y) ((x) < (y)? (x): (y)) -#ifdef _MSC_VER - /* Thanks Microsoft, but I know how to use strncpy(). */ - #pragma warning(disable:4996) -#endif +/* MAXIMUM_USB_STRING_LENGTH from usbspec.h is 255 */ +/* BLUETOOTH_DEVICE_NAME_SIZE from bluetoothapis.h is 256 */ +#define MAX_STRING_WCHARS 256 -#ifdef __cplusplus -extern "C" { -#endif +/* For certain USB devices, using a buffer larger or equal to 127 wchars results + in successful completion of HID API functions, but a broken string is stored + in the output buffer. This behaviour persists even if HID API is bypassed and + HID IOCTLs are passed to the HID driver directly. Therefore, for USB devices, + the buffer MUST NOT exceed 126 WCHARs. +*/ + +#define MAX_STRING_WCHARS_USB 126 + +static struct hid_api_version api_version = { + .major = HID_API_VERSION_MAJOR, + .minor = HID_API_VERSION_MINOR, + .patch = HID_API_VERSION_PATCH +}; #ifndef HIDAPI_USE_DDK - /* Since we're not building with the DDK, and the HID header - files aren't part of the SDK, we have to define all this - stuff here. In lookup_functions(), the function pointers - defined below are set. */ - typedef struct _HIDD_ATTRIBUTES{ - ULONG Size; - USHORT VendorID; - USHORT ProductID; - USHORT VersionNumber; - } HIDD_ATTRIBUTES, *PHIDD_ATTRIBUTES; +/* Since we're not building with the DDK, and the HID header + files aren't part of the Windows SDK, we define what we need ourselves. + In lookup_functions(), the function pointers + defined below are set. */ - typedef USHORT USAGE; - typedef struct _HIDP_CAPS { - USAGE Usage; - USAGE UsagePage; - USHORT InputReportByteLength; - USHORT OutputReportByteLength; - USHORT FeatureReportByteLength; - USHORT Reserved[17]; - USHORT fields_not_used_by_hidapi[10]; - } HIDP_CAPS, *PHIDP_CAPS; - typedef void* PHIDP_PREPARSED_DATA; - #define HIDP_STATUS_SUCCESS 0x110000 +static HidD_GetHidGuid_ HidD_GetHidGuid; +static HidD_GetAttributes_ HidD_GetAttributes; +static HidD_GetSerialNumberString_ HidD_GetSerialNumberString; +static HidD_GetManufacturerString_ HidD_GetManufacturerString; +static HidD_GetProductString_ HidD_GetProductString; +static HidD_SetFeature_ HidD_SetFeature; +static HidD_GetFeature_ HidD_GetFeature; +static HidD_SetOutputReport_ HidD_SetOutputReport; +static HidD_GetInputReport_ HidD_GetInputReport; +static HidD_GetIndexedString_ HidD_GetIndexedString; +static HidD_GetPreparsedData_ HidD_GetPreparsedData; +static HidD_FreePreparsedData_ HidD_FreePreparsedData; +static HidP_GetCaps_ HidP_GetCaps; +static HidD_SetNumInputBuffers_ HidD_SetNumInputBuffers; - typedef BOOLEAN (__stdcall *HidD_GetAttributes_)(HANDLE device, PHIDD_ATTRIBUTES attrib); - typedef BOOLEAN (__stdcall *HidD_GetSerialNumberString_)(HANDLE device, PVOID buffer, ULONG buffer_len); - typedef BOOLEAN (__stdcall *HidD_GetManufacturerString_)(HANDLE handle, PVOID buffer, ULONG buffer_len); - typedef BOOLEAN (__stdcall *HidD_GetProductString_)(HANDLE handle, PVOID buffer, ULONG buffer_len); - typedef BOOLEAN (__stdcall *HidD_SetFeature_)(HANDLE handle, PVOID data, ULONG length); - typedef BOOLEAN (__stdcall *HidD_GetFeature_)(HANDLE handle, PVOID data, ULONG length); - typedef BOOLEAN (__stdcall *HidD_GetIndexedString_)(HANDLE handle, ULONG string_index, PVOID buffer, ULONG buffer_len); - typedef BOOLEAN (__stdcall *HidD_GetPreparsedData_)(HANDLE handle, PHIDP_PREPARSED_DATA *preparsed_data); - typedef BOOLEAN (__stdcall *HidD_FreePreparsedData_)(PHIDP_PREPARSED_DATA preparsed_data); - typedef NTSTATUS (__stdcall *HidP_GetCaps_)(PHIDP_PREPARSED_DATA preparsed_data, HIDP_CAPS *caps); - typedef BOOLEAN (__stdcall *HidD_SetNumInputBuffers_)(HANDLE handle, ULONG number_buffers); +static CM_Locate_DevNodeW_ CM_Locate_DevNodeW = NULL; +static CM_Get_Parent_ CM_Get_Parent = NULL; +static CM_Get_DevNode_PropertyW_ CM_Get_DevNode_PropertyW = NULL; +static CM_Get_Device_Interface_PropertyW_ CM_Get_Device_Interface_PropertyW = NULL; +static CM_Get_Device_Interface_List_SizeW_ CM_Get_Device_Interface_List_SizeW = NULL; +static CM_Get_Device_Interface_ListW_ CM_Get_Device_Interface_ListW = NULL; - static HidD_GetAttributes_ HidD_GetAttributes; - static HidD_GetSerialNumberString_ HidD_GetSerialNumberString; - static HidD_GetManufacturerString_ HidD_GetManufacturerString; - static HidD_GetProductString_ HidD_GetProductString; - static HidD_SetFeature_ HidD_SetFeature; - static HidD_GetFeature_ HidD_GetFeature; - static HidD_GetIndexedString_ HidD_GetIndexedString; - static HidD_GetPreparsedData_ HidD_GetPreparsedData; - static HidD_FreePreparsedData_ HidD_FreePreparsedData; - static HidP_GetCaps_ HidP_GetCaps; - static HidD_SetNumInputBuffers_ HidD_SetNumInputBuffers; +static HMODULE hid_lib_handle = NULL; +static HMODULE cfgmgr32_lib_handle = NULL; +static BOOLEAN hidapi_initialized = FALSE; + +static void free_library_handles() +{ + if (hid_lib_handle) + FreeLibrary(hid_lib_handle); + hid_lib_handle = NULL; + if (cfgmgr32_lib_handle) + FreeLibrary(cfgmgr32_lib_handle); + cfgmgr32_lib_handle = NULL; +} + +static int lookup_functions() +{ + hid_lib_handle = LoadLibraryW(L"hid.dll"); + if (hid_lib_handle == NULL) { + goto err; + } + + cfgmgr32_lib_handle = LoadLibraryW(L"cfgmgr32.dll"); + if (cfgmgr32_lib_handle == NULL) { + goto err; + } + +#if defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wcast-function-type" +#endif +#define RESOLVE(lib_handle, x) x = (x##_)GetProcAddress(lib_handle, #x); if (!x) goto err; + + RESOLVE(hid_lib_handle, HidD_GetHidGuid); + RESOLVE(hid_lib_handle, HidD_GetAttributes); + RESOLVE(hid_lib_handle, HidD_GetSerialNumberString); + RESOLVE(hid_lib_handle, HidD_GetManufacturerString); + RESOLVE(hid_lib_handle, HidD_GetProductString); + RESOLVE(hid_lib_handle, HidD_SetFeature); + RESOLVE(hid_lib_handle, HidD_GetFeature); + RESOLVE(hid_lib_handle, HidD_SetOutputReport); + RESOLVE(hid_lib_handle, HidD_GetInputReport); + RESOLVE(hid_lib_handle, HidD_GetIndexedString); + RESOLVE(hid_lib_handle, HidD_GetPreparsedData); + RESOLVE(hid_lib_handle, HidD_FreePreparsedData); + RESOLVE(hid_lib_handle, HidP_GetCaps); + RESOLVE(hid_lib_handle, HidD_SetNumInputBuffers); + + RESOLVE(cfgmgr32_lib_handle, CM_Locate_DevNodeW); + RESOLVE(cfgmgr32_lib_handle, CM_Get_Parent); + RESOLVE(cfgmgr32_lib_handle, CM_Get_DevNode_PropertyW); + RESOLVE(cfgmgr32_lib_handle, CM_Get_Device_Interface_PropertyW); + RESOLVE(cfgmgr32_lib_handle, CM_Get_Device_Interface_List_SizeW); + RESOLVE(cfgmgr32_lib_handle, CM_Get_Device_Interface_ListW); + +#undef RESOLVE +#if defined(__GNUC__) +# pragma GCC diagnostic pop +#endif + + return 0; + +err: + free_library_handles(); + return -1; +} - static HMODULE lib_handle = NULL; - static BOOLEAN initialized = FALSE; #endif /* HIDAPI_USE_DDK */ struct hid_device_ { HANDLE device_handle; BOOL blocking; USHORT output_report_length; + unsigned char *write_buf; size_t input_report_length; - void *last_error_str; - DWORD last_error_num; + USHORT feature_report_length; + unsigned char *feature_buf; + wchar_t *last_error_str; BOOL read_pending; char *read_buf; OVERLAPPED ol; + OVERLAPPED write_ol; + struct hid_device_info* device_info; }; static hid_device *new_hid_device() { hid_device *dev = (hid_device*) calloc(1, sizeof(hid_device)); + + if (dev == NULL) { + return NULL; + } + dev->device_handle = INVALID_HANDLE_VALUE; dev->blocking = TRUE; dev->output_report_length = 0; + dev->write_buf = NULL; dev->input_report_length = 0; + dev->feature_report_length = 0; + dev->feature_buf = NULL; dev->last_error_str = NULL; - dev->last_error_num = 0; dev->read_pending = FALSE; dev->read_buf = NULL; memset(&dev->ol, 0, sizeof(dev->ol)); dev->ol.hEvent = CreateEvent(NULL, FALSE, FALSE /*initial state f=nonsignaled*/, NULL); + memset(&dev->write_ol, 0, sizeof(dev->write_ol)); + dev->write_ol.hEvent = CreateEvent(NULL, FALSE, FALSE /*initial state f=nonsignaled*/, NULL); + dev->device_info = NULL; return dev; } @@ -163,75 +226,125 @@ static hid_device *new_hid_device() static void free_hid_device(hid_device *dev) { CloseHandle(dev->ol.hEvent); + CloseHandle(dev->write_ol.hEvent); CloseHandle(dev->device_handle); - LocalFree(dev->last_error_str); + free(dev->last_error_str); + dev->last_error_str = NULL; + free(dev->write_buf); + free(dev->feature_buf); free(dev->read_buf); + hid_free_enumeration(dev->device_info); free(dev); } -static void register_error(hid_device *device, const char *op) +static void register_winapi_error_to_buffer(wchar_t **error_buffer, const WCHAR *op) { - WCHAR *ptr, *msg; + free(*error_buffer); + *error_buffer = NULL; - FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | - FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, + /* Only clear out error messages if NULL is passed into op */ + if (!op) { + return; + } + + WCHAR system_err_buf[1024]; + DWORD error_code = GetLastError(); + + DWORD system_err_len = FormatMessageW( + FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, - GetLastError(), + error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPVOID)&msg, 0/*sz*/, + system_err_buf, ARRAYSIZE(system_err_buf), NULL); - + + DWORD op_len = (DWORD)wcslen(op); + + DWORD op_prefix_len = + op_len + + 15 /*: (0x00000000) */ + ; + DWORD msg_len = + + op_prefix_len + + system_err_len + ; + + *error_buffer = (WCHAR *)calloc(msg_len + 1, sizeof (WCHAR)); + WCHAR *msg = *error_buffer; + + if (!msg) + return; + + int printf_written = swprintf(msg, msg_len + 1, L"%.*ls: (0x%08X) %.*ls", (int)op_len, op, error_code, (int)system_err_len, system_err_buf); + + if (printf_written < 0) + { + /* Highly unlikely */ + msg[0] = L'\0'; + return; + } + /* Get rid of the CR and LF that FormatMessage() sticks at the end of the message. Thanks Microsoft! */ - ptr = msg; - while (*ptr) { - if (*ptr == '\r') { - *ptr = 0x0000; - break; - } - ptr++; + while(msg[msg_len-1] == L'\r' || msg[msg_len-1] == L'\n' || msg[msg_len-1] == L' ') + { + msg[msg_len-1] = L'\0'; + msg_len--; } - - /* Store the message off in the Device entry so that - the hid_error() function can pick it up. */ - LocalFree(device->last_error_str); - device->last_error_str = msg; } -#ifndef HIDAPI_USE_DDK -static int lookup_functions() +#if defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Warray-bounds" +#endif +/* A bug in GCC/mingw gives: + * error: array subscript 0 is outside array bounds of 'wchar_t *[0]' {aka 'short unsigned int *[]'} [-Werror=array-bounds] + * | free(*error_buffer); + * Which doesn't make sense in this context. */ + +static void register_string_error_to_buffer(wchar_t **error_buffer, const WCHAR *string_error) { - lib_handle = LoadLibraryA("hid.dll"); - if (lib_handle) { -#define RESOLVE(x) x = (x##_)GetProcAddress(lib_handle, #x); if (!x) return -1; - RESOLVE(HidD_GetAttributes); - RESOLVE(HidD_GetSerialNumberString); - RESOLVE(HidD_GetManufacturerString); - RESOLVE(HidD_GetProductString); - RESOLVE(HidD_SetFeature); - RESOLVE(HidD_GetFeature); - RESOLVE(HidD_GetIndexedString); - RESOLVE(HidD_GetPreparsedData); - RESOLVE(HidD_FreePreparsedData); - RESOLVE(HidP_GetCaps); - RESOLVE(HidD_SetNumInputBuffers); -#undef RESOLVE - } - else - return -1; + free(*error_buffer); + *error_buffer = NULL; - return 0; + if (string_error) { + *error_buffer = _wcsdup(string_error); + } } + +#if defined(__GNUC__) +# pragma GCC diagnostic pop #endif -static HANDLE open_device(const char *path, BOOL enumerate) +static void register_winapi_error(hid_device *dev, const WCHAR *op) +{ + register_winapi_error_to_buffer(&dev->last_error_str, op); +} + +static void register_string_error(hid_device *dev, const WCHAR *string_error) +{ + register_string_error_to_buffer(&dev->last_error_str, string_error); +} + +static wchar_t *last_global_error_str = NULL; + +static void register_global_winapi_error(const WCHAR *op) +{ + register_winapi_error_to_buffer(&last_global_error_str, op); +} + +static void register_global_error(const WCHAR *string_error) +{ + register_string_error_to_buffer(&last_global_error_str, string_error); +} + +static HANDLE open_device(const wchar_t *path, BOOL open_rw) { HANDLE handle; - DWORD desired_access = (enumerate)? 0: (GENERIC_WRITE | GENERIC_READ); + DWORD desired_access = (open_rw)? (GENERIC_WRITE | GENERIC_READ): 0; DWORD share_mode = FILE_SHARE_READ|FILE_SHARE_WRITE; - handle = CreateFileA(path, + handle = CreateFileW(path, desired_access, share_mode, NULL, @@ -242,15 +355,26 @@ static HANDLE open_device(const char *path, BOOL enumerate) return handle; } +HID_API_EXPORT const struct hid_api_version* HID_API_CALL hid_version(void) +{ + return &api_version; +} + +HID_API_EXPORT const char* HID_API_CALL hid_version_str(void) +{ + return HID_API_VERSION_STR; +} + int HID_API_EXPORT hid_init(void) { + register_global_error(NULL); #ifndef HIDAPI_USE_DDK - if (!initialized) { + if (!hidapi_initialized) { if (lookup_functions() < 0) { - hid_exit(); + register_global_winapi_error(L"resolve DLL functions"); return -1; } - initialized = TRUE; + hidapi_initialized = TRUE; } #endif return 0; @@ -259,149 +383,507 @@ int HID_API_EXPORT hid_init(void) int HID_API_EXPORT hid_exit(void) { #ifndef HIDAPI_USE_DDK - if (lib_handle) - FreeLibrary(lib_handle); - lib_handle = NULL; - initialized = FALSE; + free_library_handles(); + hidapi_initialized = FALSE; #endif + register_global_error(NULL); return 0; } +static void* hid_internal_get_devnode_property(DEVINST dev_node, const DEVPROPKEY* property_key, DEVPROPTYPE expected_property_type) +{ + ULONG len = 0; + CONFIGRET cr; + DEVPROPTYPE property_type; + PBYTE property_value = NULL; + + cr = CM_Get_DevNode_PropertyW(dev_node, property_key, &property_type, NULL, &len, 0); + if (cr != CR_BUFFER_SMALL || property_type != expected_property_type) + return NULL; + + property_value = (PBYTE)calloc(len, sizeof(BYTE)); + cr = CM_Get_DevNode_PropertyW(dev_node, property_key, &property_type, property_value, &len, 0); + if (cr != CR_SUCCESS) { + free(property_value); + return NULL; + } + + return property_value; +} + +static void* hid_internal_get_device_interface_property(const wchar_t* interface_path, const DEVPROPKEY* property_key, DEVPROPTYPE expected_property_type) +{ + ULONG len = 0; + CONFIGRET cr; + DEVPROPTYPE property_type; + PBYTE property_value = NULL; + + cr = CM_Get_Device_Interface_PropertyW(interface_path, property_key, &property_type, NULL, &len, 0); + if (cr != CR_BUFFER_SMALL || property_type != expected_property_type) + return NULL; + + property_value = (PBYTE)calloc(len, sizeof(BYTE)); + cr = CM_Get_Device_Interface_PropertyW(interface_path, property_key, &property_type, property_value, &len, 0); + if (cr != CR_SUCCESS) { + free(property_value); + return NULL; + } + + return property_value; +} + +static void hid_internal_towupper(wchar_t* string) +{ + for (wchar_t* p = string; *p; ++p) *p = towupper(*p); +} + +static int hid_internal_extract_int_token_value(wchar_t* string, const wchar_t* token) +{ + int token_value; + wchar_t* startptr, * endptr; + + startptr = wcsstr(string, token); + if (!startptr) + return -1; + + startptr += wcslen(token); + token_value = wcstol(startptr, &endptr, 16); + if (endptr == startptr) + return -1; + + return token_value; +} + +static void hid_internal_get_usb_info(struct hid_device_info* dev, DEVINST dev_node) +{ + wchar_t *device_id = NULL, *hardware_ids = NULL; + + device_id = hid_internal_get_devnode_property(dev_node, &DEVPKEY_Device_InstanceId, DEVPROP_TYPE_STRING); + if (!device_id) + goto end; + + /* Normalize to upper case */ + hid_internal_towupper(device_id); + + /* Check for Xbox Common Controller class (XUSB) device. + https://docs.microsoft.com/windows/win32/xinput/directinput-and-xusb-devices + https://docs.microsoft.com/windows/win32/xinput/xinput-and-directinput + */ + if (hid_internal_extract_int_token_value(device_id, L"IG_") != -1) { + /* Get devnode parent to reach out USB device. */ + if (CM_Get_Parent(&dev_node, dev_node, 0) != CR_SUCCESS) + goto end; + } + + /* Get the hardware ids from devnode */ + hardware_ids = hid_internal_get_devnode_property(dev_node, &DEVPKEY_Device_HardwareIds, DEVPROP_TYPE_STRING_LIST); + if (!hardware_ids) + goto end; + + /* Get additional information from USB device's Hardware ID + https://docs.microsoft.com/windows-hardware/drivers/install/standard-usb-identifiers + https://docs.microsoft.com/windows-hardware/drivers/usbcon/enumeration-of-interfaces-not-grouped-in-collections + */ + for (wchar_t* hardware_id = hardware_ids; *hardware_id; hardware_id += wcslen(hardware_id) + 1) { + /* Normalize to upper case */ + hid_internal_towupper(hardware_id); + + if (dev->release_number == 0) { + /* USB_DEVICE_DESCRIPTOR.bcdDevice value. */ + int release_number = hid_internal_extract_int_token_value(hardware_id, L"REV_"); + if (release_number != -1) { + dev->release_number = (unsigned short)release_number; + } + } + + if (dev->interface_number == -1) { + /* USB_INTERFACE_DESCRIPTOR.bInterfaceNumber value. */ + int interface_number = hid_internal_extract_int_token_value(hardware_id, L"MI_"); + if (interface_number != -1) { + dev->interface_number = interface_number; + } + } + } + + /* Try to get USB device manufacturer string if not provided by HidD_GetManufacturerString. */ + if (wcslen(dev->manufacturer_string) == 0) { + wchar_t* manufacturer_string = hid_internal_get_devnode_property(dev_node, &DEVPKEY_Device_Manufacturer, DEVPROP_TYPE_STRING); + if (manufacturer_string) { + free(dev->manufacturer_string); + dev->manufacturer_string = manufacturer_string; + } + } + + /* Try to get USB device serial number if not provided by HidD_GetSerialNumberString. */ + if (wcslen(dev->serial_number) == 0) { + DEVINST usb_dev_node = dev_node; + if (dev->interface_number != -1) { + /* Get devnode parent to reach out composite parent USB device. + https://docs.microsoft.com/windows-hardware/drivers/usbcon/enumeration-of-the-composite-parent-device + */ + if (CM_Get_Parent(&usb_dev_node, dev_node, 0) != CR_SUCCESS) + goto end; + } + + /* Get the device id of the USB device. */ + free(device_id); + device_id = hid_internal_get_devnode_property(usb_dev_node, &DEVPKEY_Device_InstanceId, DEVPROP_TYPE_STRING); + if (!device_id) + goto end; + + /* Extract substring after last '\\' of Instance ID. + For USB devices it may contain device's serial number. + https://docs.microsoft.com/windows-hardware/drivers/install/instance-ids + */ + for (wchar_t *ptr = device_id + wcslen(device_id); ptr > device_id; --ptr) { + /* Instance ID is unique only within the scope of the bus. + For USB devices it means that serial number is not available. Skip. */ + if (*ptr == L'&') + break; + + if (*ptr == L'\\') { + free(dev->serial_number); + dev->serial_number = _wcsdup(ptr + 1); + break; + } + } + } + + /* If we can't get the interface number, it means that there is only one interface. */ + if (dev->interface_number == -1) + dev->interface_number = 0; + +end: + free(device_id); + free(hardware_ids); +} + +/* HidD_GetProductString/HidD_GetManufacturerString/HidD_GetSerialNumberString is not working for BLE HID devices + Request this info via dev node properties instead. + https://docs.microsoft.com/answers/questions/401236/hidd-getproductstring-with-ble-hid-device.html +*/ +static void hid_internal_get_ble_info(struct hid_device_info* dev, DEVINST dev_node) +{ + if (wcslen(dev->manufacturer_string) == 0) { + /* Manufacturer Name String (UUID: 0x2A29) */ + wchar_t* manufacturer_string = hid_internal_get_devnode_property(dev_node, (const DEVPROPKEY*)&PKEY_DeviceInterface_Bluetooth_Manufacturer, DEVPROP_TYPE_STRING); + if (manufacturer_string) { + free(dev->manufacturer_string); + dev->manufacturer_string = manufacturer_string; + } + } + + if (wcslen(dev->serial_number) == 0) { + /* Serial Number String (UUID: 0x2A25) */ + wchar_t* serial_number = hid_internal_get_devnode_property(dev_node, (const DEVPROPKEY*)&PKEY_DeviceInterface_Bluetooth_DeviceAddress, DEVPROP_TYPE_STRING); + if (serial_number) { + free(dev->serial_number); + dev->serial_number = serial_number; + } + } + + if (wcslen(dev->product_string) == 0) { + /* Model Number String (UUID: 0x2A24) */ + wchar_t* product_string = hid_internal_get_devnode_property(dev_node, (const DEVPROPKEY*)&PKEY_DeviceInterface_Bluetooth_ModelNumber, DEVPROP_TYPE_STRING); + if (!product_string) { + DEVINST parent_dev_node = 0; + /* Fallback: Get devnode grandparent to reach out Bluetooth LE device node */ + if (CM_Get_Parent(&parent_dev_node, dev_node, 0) == CR_SUCCESS) { + /* Device Name (UUID: 0x2A00) */ + product_string = hid_internal_get_devnode_property(parent_dev_node, &DEVPKEY_NAME, DEVPROP_TYPE_STRING); + } + } + + if (product_string) { + free(dev->product_string); + dev->product_string = product_string; + } + } +} + +/* Unfortunately, HID_API_BUS_xxx constants alone aren't enough to distinguish between BLUETOOTH and BLE */ + +#define HID_API_BUS_FLAG_BLE 0x01 + +typedef struct hid_internal_detect_bus_type_result_ { + DEVINST dev_node; + hid_bus_type bus_type; + unsigned int bus_flags; +} hid_internal_detect_bus_type_result; + +static hid_internal_detect_bus_type_result hid_internal_detect_bus_type(const wchar_t* interface_path) +{ + wchar_t *device_id = NULL, *compatible_ids = NULL; + CONFIGRET cr; + DEVINST dev_node; + hid_internal_detect_bus_type_result result = { 0 }; + + /* Get the device id from interface path */ + device_id = hid_internal_get_device_interface_property(interface_path, &DEVPKEY_Device_InstanceId, DEVPROP_TYPE_STRING); + if (!device_id) + goto end; + + /* Open devnode from device id */ + cr = CM_Locate_DevNodeW(&dev_node, (DEVINSTID_W)device_id, CM_LOCATE_DEVNODE_NORMAL); + if (cr != CR_SUCCESS) + goto end; + + /* Get devnode parent */ + cr = CM_Get_Parent(&dev_node, dev_node, 0); + if (cr != CR_SUCCESS) + goto end; + + /* Get the compatible ids from parent devnode */ + compatible_ids = hid_internal_get_devnode_property(dev_node, &DEVPKEY_Device_CompatibleIds, DEVPROP_TYPE_STRING_LIST); + if (!compatible_ids) + goto end; + + /* Now we can parse parent's compatible IDs to find out the device bus type */ + for (wchar_t* compatible_id = compatible_ids; *compatible_id; compatible_id += wcslen(compatible_id) + 1) { + /* Normalize to upper case */ + hid_internal_towupper(compatible_id); + + /* USB devices + https://docs.microsoft.com/windows-hardware/drivers/hid/plug-and-play-support + https://docs.microsoft.com/windows-hardware/drivers/install/standard-usb-identifiers */ + if (wcsstr(compatible_id, L"USB") != NULL) { + result.bus_type = HID_API_BUS_USB; + break; + } + + /* Bluetooth devices + https://docs.microsoft.com/windows-hardware/drivers/bluetooth/installing-a-bluetooth-device */ + if (wcsstr(compatible_id, L"BTHENUM") != NULL) { + result.bus_type = HID_API_BUS_BLUETOOTH; + break; + } + + /* Bluetooth LE devices */ + if (wcsstr(compatible_id, L"BTHLEDEVICE") != NULL) { + result.bus_type = HID_API_BUS_BLUETOOTH; + result.bus_flags |= HID_API_BUS_FLAG_BLE; + break; + } + + /* I2C devices + https://docs.microsoft.com/windows-hardware/drivers/hid/plug-and-play-support-and-power-management */ + if (wcsstr(compatible_id, L"PNP0C50") != NULL) { + result.bus_type = HID_API_BUS_I2C; + break; + } + + /* SPI devices + https://docs.microsoft.com/windows-hardware/drivers/hid/plug-and-play-for-spi */ + if (wcsstr(compatible_id, L"PNP0C51") != NULL) { + result.bus_type = HID_API_BUS_SPI; + break; + } + } + + result.dev_node = dev_node; + +end: + free(device_id); + free(compatible_ids); + return result; +} + +static char *hid_internal_UTF16toUTF8(const wchar_t *src) +{ + char *dst = NULL; + int len = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, src, -1, NULL, 0, NULL, NULL); + if (len) { + dst = (char*)calloc(len, sizeof(char)); + if (dst == NULL) { + return NULL; + } + WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, src, -1, dst, len, NULL, NULL); + } + + return dst; +} + +static wchar_t *hid_internal_UTF8toUTF16(const char *src) +{ + wchar_t *dst = NULL; + int len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, -1, NULL, 0); + if (len) { + dst = (wchar_t*)calloc(len, sizeof(wchar_t)); + if (dst == NULL) { + return NULL; + } + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, -1, dst, len); + } + + return dst; +} + +static struct hid_device_info *hid_internal_get_device_info(const wchar_t *path, HANDLE handle) +{ + struct hid_device_info *dev = NULL; /* return object */ + HIDD_ATTRIBUTES attrib; + PHIDP_PREPARSED_DATA pp_data = NULL; + HIDP_CAPS caps; + wchar_t string[MAX_STRING_WCHARS + 1]; + ULONG len; + ULONG size; + hid_internal_detect_bus_type_result detect_bus_type_result; + + /* Create the record. */ + dev = (struct hid_device_info*)calloc(1, sizeof(struct hid_device_info)); + + if (dev == NULL) { + return NULL; + } + + /* Fill out the record */ + dev->next = NULL; + dev->path = hid_internal_UTF16toUTF8(path); + dev->interface_number = -1; + + attrib.Size = sizeof(HIDD_ATTRIBUTES); + if (HidD_GetAttributes(handle, &attrib)) { + /* VID/PID */ + dev->vendor_id = attrib.VendorID; + dev->product_id = attrib.ProductID; + + /* Release Number */ + dev->release_number = attrib.VersionNumber; + } + + /* Get the Usage Page and Usage for this device. */ + if (HidD_GetPreparsedData(handle, &pp_data)) { + if (HidP_GetCaps(pp_data, &caps) == HIDP_STATUS_SUCCESS) { + dev->usage_page = caps.UsagePage; + dev->usage = caps.Usage; + } + + HidD_FreePreparsedData(pp_data); + } + + /* detect bus type before reading string descriptors */ + detect_bus_type_result = hid_internal_detect_bus_type(path); + dev->bus_type = detect_bus_type_result.bus_type; + + len = dev->bus_type == HID_API_BUS_USB ? MAX_STRING_WCHARS_USB : MAX_STRING_WCHARS; + string[len] = L'\0'; + size = len * sizeof(wchar_t); + + /* Serial Number */ + string[0] = L'\0'; + HidD_GetSerialNumberString(handle, string, size); + dev->serial_number = _wcsdup(string); + + /* Manufacturer String */ + string[0] = L'\0'; + HidD_GetManufacturerString(handle, string, size); + dev->manufacturer_string = _wcsdup(string); + + /* Product String */ + string[0] = L'\0'; + HidD_GetProductString(handle, string, size); + dev->product_string = _wcsdup(string); + + /* now, the portion that depends on string descriptors */ + switch (dev->bus_type) { + case HID_API_BUS_USB: + hid_internal_get_usb_info(dev, detect_bus_type_result.dev_node); + break; + + case HID_API_BUS_BLUETOOTH: + if (detect_bus_type_result.bus_flags & HID_API_BUS_FLAG_BLE) + hid_internal_get_ble_info(dev, detect_bus_type_result.dev_node); + break; + + case HID_API_BUS_UNKNOWN: + case HID_API_BUS_SPI: + case HID_API_BUS_I2C: + /* shut down -Wswitch */ + break; + } + + return dev; +} + struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned short vendor_id, unsigned short product_id) { - BOOL res; struct hid_device_info *root = NULL; /* return object */ struct hid_device_info *cur_dev = NULL; + GUID interface_class_guid; + CONFIGRET cr; + wchar_t* device_interface_list = NULL; + DWORD len; - /* Windows objects for interacting with the driver. */ - GUID InterfaceClassGuid = {0x4d1e55b2, 0xf16f, 0x11cf, {0x88, 0xcb, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30} }; - SP_DEVINFO_DATA devinfo_data; - SP_DEVICE_INTERFACE_DATA device_interface_data; - SP_DEVICE_INTERFACE_DETAIL_DATA_A *device_interface_detail_data = NULL; - HDEVINFO device_info_set = INVALID_HANDLE_VALUE; - int device_index = 0; - int i; - - if (hid_init() < 0) + if (hid_init() < 0) { + /* register_global_error: global error is reset by hid_init */ return NULL; + } - /* Initialize the Windows objects. */ - memset(&devinfo_data, 0x0, sizeof(devinfo_data)); - devinfo_data.cbSize = sizeof(SP_DEVINFO_DATA); - device_interface_data.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA); + /* Retrieve HID Interface Class GUID + https://docs.microsoft.com/windows-hardware/drivers/install/guid-devinterface-hid */ + HidD_GetHidGuid(&interface_class_guid); - /* Get information for all the devices belonging to the HID class. */ - device_info_set = SetupDiGetClassDevsA(&InterfaceClassGuid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); - - /* Iterate over each device in the HID class, looking for the right one. */ - - for (;;) { - HANDLE write_handle = INVALID_HANDLE_VALUE; - DWORD required_size = 0; - HIDD_ATTRIBUTES attrib; - - res = SetupDiEnumDeviceInterfaces(device_info_set, - NULL, - &InterfaceClassGuid, - device_index, - &device_interface_data); - - if (!res) { - /* A return of FALSE from this function means that - there are no more devices. */ + /* Get the list of all device interfaces belonging to the HID class. */ + /* Retry in case of list was changed between calls to + CM_Get_Device_Interface_List_SizeW and CM_Get_Device_Interface_ListW */ + do { + cr = CM_Get_Device_Interface_List_SizeW(&len, &interface_class_guid, NULL, CM_GET_DEVICE_INTERFACE_LIST_PRESENT); + if (cr != CR_SUCCESS) { + register_global_error(L"Failed to get size of HID device interface list"); break; } - /* Call with 0-sized detail size, and let the function - tell us how long the detail struct needs to be. The - size is put in &required_size. */ - res = SetupDiGetDeviceInterfaceDetailA(device_info_set, - &device_interface_data, - NULL, - 0, - &required_size, - NULL); - - /* Allocate a long enough structure for device_interface_detail_data. */ - device_interface_detail_data = (SP_DEVICE_INTERFACE_DETAIL_DATA_A*) malloc(required_size); - device_interface_detail_data->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_A); - - /* Get the detailed data for this device. The detail data gives us - the device path for this device, which is then passed into - CreateFile() to get a handle to the device. */ - res = SetupDiGetDeviceInterfaceDetailA(device_info_set, - &device_interface_data, - device_interface_detail_data, - required_size, - NULL, - NULL); - - if (!res) { - /* register_error(dev, "Unable to call SetupDiGetDeviceInterfaceDetail"); - Continue to the next device. */ - goto cont; + if (device_interface_list != NULL) { + free(device_interface_list); } - /* Make sure this device is of Setup Class "HIDClass" and has a - driver bound to it. */ - for (i = 0; ; i++) { - char driver_name[256]; - - /* Populate devinfo_data. This function will return failure - when there are no more interfaces left. */ - res = SetupDiEnumDeviceInfo(device_info_set, i, &devinfo_data); - if (!res) - goto cont; - - res = SetupDiGetDeviceRegistryPropertyA(device_info_set, &devinfo_data, - SPDRP_CLASS, NULL, (PBYTE)driver_name, sizeof(driver_name), NULL); - if (!res) - goto cont; - - if (strcmp(driver_name, "HIDClass") == 0) { - /* See if there's a driver bound. */ - res = SetupDiGetDeviceRegistryPropertyA(device_info_set, &devinfo_data, - SPDRP_DRIVER, NULL, (PBYTE)driver_name, sizeof(driver_name), NULL); - if (res) - break; - } + device_interface_list = (wchar_t*)calloc(len, sizeof(wchar_t)); + if (device_interface_list == NULL) { + register_global_error(L"Failed to allocate memory for HID device interface list"); + return NULL; } + cr = CM_Get_Device_Interface_ListW(&interface_class_guid, NULL, device_interface_list, len, CM_GET_DEVICE_INTERFACE_LIST_PRESENT); + if (cr != CR_SUCCESS && cr != CR_BUFFER_SMALL) { + register_global_error(L"Failed to get HID device interface list"); + } + } while (cr == CR_BUFFER_SMALL); - //wprintf(L"HandleName: %s\n", device_interface_detail_data->DevicePath); + if (cr != CR_SUCCESS) { + goto end_of_function; + } - /* Open a handle to the device */ - write_handle = open_device(device_interface_detail_data->DevicePath, TRUE); + /* Iterate over each device interface in the HID class, looking for the right one. */ + for (wchar_t* device_interface = device_interface_list; *device_interface; device_interface += wcslen(device_interface) + 1) { + HANDLE device_handle = INVALID_HANDLE_VALUE; + HIDD_ATTRIBUTES attrib; - /* Check validity of write_handle. */ - if (write_handle == INVALID_HANDLE_VALUE) { + /* Open read-only handle to the device */ + device_handle = open_device(device_interface, FALSE); + + /* Check validity of device_handle. */ + if (device_handle == INVALID_HANDLE_VALUE) { /* Unable to open the device. */ - //register_error(dev, "CreateFile"); - goto cont_close; - } - + continue; + } /* Get the Vendor ID and Product ID for this device. */ attrib.Size = sizeof(HIDD_ATTRIBUTES); - HidD_GetAttributes(write_handle, &attrib); - //wprintf(L"Product/Vendor: %x %x\n", attrib.ProductID, attrib.VendorID); + if (!HidD_GetAttributes(device_handle, &attrib)) { + goto cont_close; + } /* Check the VID/PID to see if we should add this device to the enumeration list. */ if ((vendor_id == 0x0 || attrib.VendorID == vendor_id) && (product_id == 0x0 || attrib.ProductID == product_id)) { - #define WSTR_LEN 512 - const char *str; - struct hid_device_info *tmp; - PHIDP_PREPARSED_DATA pp_data = NULL; - HIDP_CAPS caps; - BOOLEAN res; - NTSTATUS nt_res; - wchar_t wstr[WSTR_LEN]; /* TODO: Determine Size */ - size_t len; - /* VID/PID match. Create the record. */ - tmp = (struct hid_device_info*) calloc(1, sizeof(struct hid_device_info)); + struct hid_device_info *tmp = hid_internal_get_device_info(device_interface, device_handle); + + if (tmp == NULL) { + goto cont_close; + } + if (cur_dev) { cur_dev->next = tmp; } @@ -409,94 +891,24 @@ struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned shor root = tmp; } cur_dev = tmp; - - /* Get the Usage Page and Usage for this device. */ - res = HidD_GetPreparsedData(write_handle, &pp_data); - if (res) { - nt_res = HidP_GetCaps(pp_data, &caps); - if (nt_res == HIDP_STATUS_SUCCESS) { - cur_dev->usage_page = caps.UsagePage; - cur_dev->usage = caps.Usage; - } - - HidD_FreePreparsedData(pp_data); - } - - /* Fill out the record */ - cur_dev->next = NULL; - str = device_interface_detail_data->DevicePath; - if (str) { - len = strlen(str); - cur_dev->path = (char*) calloc(len+1, sizeof(char)); - strncpy(cur_dev->path, str, len+1); - cur_dev->path[len] = '\0'; - } - else - cur_dev->path = NULL; - - /* Serial Number */ - res = HidD_GetSerialNumberString(write_handle, wstr, sizeof(wstr)); - wstr[WSTR_LEN-1] = 0x0000; - if (res) { - cur_dev->serial_number = _wcsdup(wstr); - } - - /* Manufacturer String */ - res = HidD_GetManufacturerString(write_handle, wstr, sizeof(wstr)); - wstr[WSTR_LEN-1] = 0x0000; - if (res) { - cur_dev->manufacturer_string = _wcsdup(wstr); - } - - /* Product String */ - res = HidD_GetProductString(write_handle, wstr, sizeof(wstr)); - wstr[WSTR_LEN-1] = 0x0000; - if (res) { - cur_dev->product_string = _wcsdup(wstr); - } - - /* VID/PID */ - cur_dev->vendor_id = attrib.VendorID; - cur_dev->product_id = attrib.ProductID; - - /* Release Number */ - cur_dev->release_number = attrib.VersionNumber; - - /* Interface Number. It can sometimes be parsed out of the path - on Windows if a device has multiple interfaces. See - http://msdn.microsoft.com/en-us/windows/hardware/gg487473 or - search for "Hardware IDs for HID Devices" at MSDN. If it's not - in the path, it's set to -1. */ - cur_dev->interface_number = -1; - if (cur_dev->path) { - char *interface_component = strstr(cur_dev->path, "&mi_"); - if (interface_component) { - char *hex_str = interface_component + 4; - char *endptr = NULL; - cur_dev->interface_number = strtol(hex_str, &endptr, 16); - if (endptr == hex_str) { - /* The parsing failed. Set interface_number to -1. */ - cur_dev->interface_number = -1; - } - } - } } cont_close: - CloseHandle(write_handle); -cont: - /* We no longer need the detail data. It can be freed */ - free(device_interface_detail_data); - - device_index++; - + CloseHandle(device_handle); } - /* Close the device information handle. */ - SetupDiDestroyDeviceInfoList(device_info_set); + if (root == NULL) { + if (vendor_id == 0 && product_id == 0) { + register_global_error(L"No HID devices found in the system."); + } else { + register_global_error(L"No HID devices with requested VID/PID found in the system."); + } + } + +end_of_function: + free(device_interface_list); return root; - } void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *devs) @@ -514,21 +926,26 @@ void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *d } } - HID_API_EXPORT hid_device * HID_API_CALL hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) { /* TODO: Merge this functions with the Linux version. This function should be platform independent. */ struct hid_device_info *devs, *cur_dev; const char *path_to_open = NULL; hid_device *handle = NULL; - + + /* register_global_error: global error is reset by hid_enumerate/hid_init */ devs = hid_enumerate(vendor_id, product_id); + if (!devs) { + /* register_global_error: global error is already set by hid_enumerate */ + return NULL; + } + cur_dev = devs; while (cur_dev) { if (cur_dev->vendor_id == vendor_id && cur_dev->product_id == product_id) { if (serial_number) { - if (wcscmp(serial_number, cur_dev->serial_number) == 0) { + if (cur_dev->serial_number && wcscmp(serial_number, cur_dev->serial_number) == 0) { path_to_open = cur_dev->path; break; } @@ -544,123 +961,166 @@ HID_API_EXPORT hid_device * HID_API_CALL hid_open(unsigned short vendor_id, unsi if (path_to_open) { /* Open the device */ handle = hid_open_path(path_to_open); + } else { + register_global_error(L"Device with requested VID/PID/(SerialNumber) not found"); } hid_free_enumeration(devs); - + return handle; } HID_API_EXPORT hid_device * HID_API_CALL hid_open_path(const char *path) { - hid_device *dev; - HIDP_CAPS caps; + hid_device *dev = NULL; + wchar_t* interface_path = NULL; + HANDLE device_handle = INVALID_HANDLE_VALUE; PHIDP_PREPARSED_DATA pp_data = NULL; - BOOLEAN res; - NTSTATUS nt_res; + HIDP_CAPS caps; if (hid_init() < 0) { - return NULL; + /* register_global_error: global error is reset by hid_init */ + goto end_of_function; + } + + interface_path = hid_internal_UTF8toUTF16(path); + if (!interface_path) { + register_global_error(L"Path conversion failure"); + goto end_of_function; + } + + /* Open a handle to the device */ + device_handle = open_device(interface_path, TRUE); + + /* Check validity of write_handle. */ + if (device_handle == INVALID_HANDLE_VALUE) { + /* System devices, such as keyboards and mice, cannot be opened in + read-write mode, because the system takes exclusive control over + them. This is to prevent keyloggers. However, feature reports + can still be sent and received. Retry opening the device, but + without read/write access. */ + device_handle = open_device(interface_path, FALSE); + + /* Check the validity of the limited device_handle. */ + if (device_handle == INVALID_HANDLE_VALUE) { + register_global_winapi_error(L"open_device"); + goto end_of_function; + } + } + + /* Set the Input Report buffer size to 64 reports. */ + if (!HidD_SetNumInputBuffers(device_handle, 64)) { + register_global_winapi_error(L"set input buffers"); + goto end_of_function; + } + + /* Get the Input Report length for the device. */ + if (!HidD_GetPreparsedData(device_handle, &pp_data)) { + register_global_winapi_error(L"get preparsed data"); + goto end_of_function; + } + + if (HidP_GetCaps(pp_data, &caps) != HIDP_STATUS_SUCCESS) { + register_global_error(L"HidP_GetCaps"); + goto end_of_function; } dev = new_hid_device(); - /* Open a handle to the device */ - dev->device_handle = open_device(path, FALSE); - - /* Check validity of write_handle. */ - if (dev->device_handle == INVALID_HANDLE_VALUE) { - /* Unable to open the device. */ - register_error(dev, "CreateFile"); - goto err; + if (dev == NULL) { + register_global_error(L"hid_device allocation error"); + goto end_of_function; } - /* Set the Input Report buffer size to 64 reports. */ - res = HidD_SetNumInputBuffers(dev->device_handle, 64); - if (!res) { - register_error(dev, "HidD_SetNumInputBuffers"); - goto err; - } + dev->device_handle = device_handle; + device_handle = INVALID_HANDLE_VALUE; - /* Get the Input Report length for the device. */ - res = HidD_GetPreparsedData(dev->device_handle, &pp_data); - if (!res) { - register_error(dev, "HidD_GetPreparsedData"); - goto err; - } - nt_res = HidP_GetCaps(pp_data, &caps); - if (nt_res != HIDP_STATUS_SUCCESS) { - register_error(dev, "HidP_GetCaps"); - goto err_pp_data; - } dev->output_report_length = caps.OutputReportByteLength; dev->input_report_length = caps.InputReportByteLength; - HidD_FreePreparsedData(pp_data); - + dev->feature_report_length = caps.FeatureReportByteLength; dev->read_buf = (char*) malloc(dev->input_report_length); + dev->device_info = hid_internal_get_device_info(interface_path, dev->device_handle); + +end_of_function: + free(interface_path); + CloseHandle(device_handle); + + if (pp_data) { + HidD_FreePreparsedData(pp_data); + } return dev; - -err_pp_data: - HidD_FreePreparsedData(pp_data); -err: - free_hid_device(dev); - return NULL; } int HID_API_EXPORT HID_API_CALL hid_write(hid_device *dev, const unsigned char *data, size_t length) { - DWORD bytes_written; + DWORD bytes_written = 0; + int function_result = -1; BOOL res; + BOOL overlapped = FALSE; - OVERLAPPED ol; unsigned char *buf; - memset(&ol, 0, sizeof(ol)); + + if (!data || !length) { + register_string_error(dev, L"Zero buffer/length"); + return function_result; + } + + register_string_error(dev, NULL); /* Make sure the right number of bytes are passed to WriteFile. Windows expects the number of bytes which are in the _longest_ report (plus one for the report number) bytes even if the data is a report which is shorter than that. Windows gives us this value in caps.OutputReportByteLength. If a user passes in fewer bytes than this, - create a temporary buffer which is the proper size. */ + use cached temporary buffer which is the proper size. */ if (length >= dev->output_report_length) { /* The user passed the right number of bytes. Use the buffer as-is. */ buf = (unsigned char *) data; } else { - /* Create a temporary buffer and copy the user's data - into it, padding the rest with zeros. */ - buf = (unsigned char *) malloc(dev->output_report_length); + if (dev->write_buf == NULL) + dev->write_buf = (unsigned char *) malloc(dev->output_report_length); + buf = dev->write_buf; memcpy(buf, data, length); memset(buf + length, 0, dev->output_report_length - length); length = dev->output_report_length; } - res = WriteFile(dev->device_handle, buf, length, NULL, &ol); - + res = WriteFile(dev->device_handle, buf, (DWORD) length, NULL, &dev->write_ol); + if (!res) { if (GetLastError() != ERROR_IO_PENDING) { /* WriteFile() failed. Return error. */ - register_error(dev, "WriteFile"); - bytes_written = -1; + register_winapi_error(dev, L"WriteFile"); + goto end_of_function; + } + overlapped = TRUE; + } + + if (overlapped) { + /* Wait for the transaction to complete. This makes + hid_write() synchronous. */ + res = WaitForSingleObject(dev->write_ol.hEvent, 1000); + if (res != WAIT_OBJECT_0) { + /* There was a Timeout. */ + register_winapi_error(dev, L"hid_write/WaitForSingleObject"); + goto end_of_function; + } + + /* Get the result. */ + res = GetOverlappedResult(dev->device_handle, &dev->write_ol, &bytes_written, FALSE/*wait*/); + if (res) { + function_result = bytes_written; + } + else { + /* The Write operation failed. */ + register_winapi_error(dev, L"hid_write/GetOverlappedResult"); goto end_of_function; } } - /* Wait here until the write is done. This makes - hid_write() synchronous. */ - res = GetOverlappedResult(dev->device_handle, &ol, &bytes_written, TRUE/*wait*/); - if (!res) { - /* The Write operation failed. */ - register_error(dev, "WriteFile"); - bytes_written = -1; - goto end_of_function; - } - end_of_function: - if (buf != data) - free(buf); - - return bytes_written; + return function_result; } @@ -668,7 +1128,15 @@ int HID_API_EXPORT HID_API_CALL hid_read_timeout(hid_device *dev, unsigned char { DWORD bytes_read = 0; size_t copy_len = 0; - BOOL res; + BOOL res = FALSE; + BOOL overlapped = FALSE; + + if (!data || !length) { + register_string_error(dev, L"Zero buffer/length"); + return -1; + } + + register_string_error(dev, NULL); /* Copy the handle for convenience. */ HANDLE ev = dev->ol.hEvent; @@ -678,34 +1146,39 @@ int HID_API_EXPORT HID_API_CALL hid_read_timeout(hid_device *dev, unsigned char dev->read_pending = TRUE; memset(dev->read_buf, 0, dev->input_report_length); ResetEvent(ev); - res = ReadFile(dev->device_handle, dev->read_buf, dev->input_report_length, &bytes_read, &dev->ol); - + res = ReadFile(dev->device_handle, dev->read_buf, (DWORD) dev->input_report_length, &bytes_read, &dev->ol); + if (!res) { if (GetLastError() != ERROR_IO_PENDING) { /* ReadFile() has failed. Clean up and return error. */ + register_winapi_error(dev, L"ReadFile"); CancelIo(dev->device_handle); dev->read_pending = FALSE; goto end_of_function; } + overlapped = TRUE; } } + else { + overlapped = TRUE; + } - if (milliseconds >= 0) { + if (overlapped) { /* See if there is any data yet. */ - res = WaitForSingleObject(ev, milliseconds); + res = WaitForSingleObject(ev, milliseconds >= 0 ? (DWORD)milliseconds : INFINITE); if (res != WAIT_OBJECT_0) { /* There was no data this time. Return zero bytes available, but leave the Overlapped I/O running. */ return 0; } - } - /* Either WaitForSingleObject() told us that ReadFile has completed, or - we are in non-blocking mode. Get the number of bytes read. The actual - data has been copied to the data[] array which was passed to ReadFile(). */ - res = GetOverlappedResult(dev->device_handle, &dev->ol, &bytes_read, TRUE/*wait*/); - + /* Get the number of bytes read. The actual data has been copied to the data[] + array which was passed to ReadFile(). We must not wait here because we've + already waited on our event above, and since it's auto-reset, it will have + been reset back to unsignalled by now. */ + res = GetOverlappedResult(dev->device_handle, &dev->ol, &bytes_read, FALSE/*don't wait now - already did on the prev step*/); + } /* Set pending back to false, even if GetOverlappedResult() returned error. */ dev->read_pending = FALSE; @@ -725,14 +1198,16 @@ int HID_API_EXPORT HID_API_CALL hid_read_timeout(hid_device *dev, unsigned char memcpy(data, dev->read_buf, copy_len); } } - + if (!res) { + register_winapi_error(dev, L"hid_read_timeout/GetOverlappedResult"); + } + end_of_function: if (!res) { - register_error(dev, "GetOverlappedResult"); return -1; } - - return copy_len; + + return (int) copy_len; } int HID_API_EXPORT HID_API_CALL hid_read(hid_device *dev, unsigned char *data, size_t length) @@ -748,42 +1223,69 @@ int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *dev, int nonbloc int HID_API_EXPORT HID_API_CALL hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length) { - BOOL res = HidD_SetFeature(dev->device_handle, (PVOID)data, length); - if (!res) { - register_error(dev, "HidD_SetFeature"); + BOOL res = FALSE; + unsigned char *buf; + size_t length_to_send; + + if (!data || !length) { + register_string_error(dev, L"Zero buffer/length"); return -1; } - return length; + register_string_error(dev, NULL); + + /* Windows expects at least caps.FeatureReportByteLength bytes passed + to HidD_SetFeature(), even if the report is shorter. Any less sent and + the function fails with error ERROR_INVALID_PARAMETER set. Any more + and HidD_SetFeature() silently truncates the data sent in the report + to caps.FeatureReportByteLength. */ + if (length >= dev->feature_report_length) { + buf = (unsigned char *) data; + length_to_send = length; + } else { + if (dev->feature_buf == NULL) + dev->feature_buf = (unsigned char *) malloc(dev->feature_report_length); + buf = dev->feature_buf; + memcpy(buf, data, length); + memset(buf + length, 0, dev->feature_report_length - length); + length_to_send = dev->feature_report_length; + } + + res = HidD_SetFeature(dev->device_handle, (PVOID)buf, (DWORD) length_to_send); + + if (!res) { + register_winapi_error(dev, L"HidD_SetFeature"); + return -1; + } + + return (int) length; } - -int HID_API_EXPORT HID_API_CALL hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) +static int hid_get_report(hid_device *dev, DWORD report_type, unsigned char *data, size_t length) { BOOL res; -#if 0 - res = HidD_GetFeature(dev->device_handle, data, length); - if (!res) { - register_error(dev, "HidD_GetFeature"); - return -1; - } - return 0; /* HidD_GetFeature() doesn't give us an actual length, unfortunately */ -#else - DWORD bytes_returned; + DWORD bytes_returned = 0; OVERLAPPED ol; memset(&ol, 0, sizeof(ol)); + if (!data || !length) { + register_string_error(dev, L"Zero buffer/length"); + return -1; + } + + register_string_error(dev, NULL); + res = DeviceIoControl(dev->device_handle, - IOCTL_HID_GET_FEATURE, - data, length, - data, length, + report_type, + data, (DWORD) length, + data, (DWORD) length, &bytes_returned, &ol); if (!res) { if (GetLastError() != ERROR_IO_PENDING) { /* DeviceIoControl() failed. Return error. */ - register_error(dev, "Send Feature Report DeviceIoControl"); + register_winapi_error(dev, L"Get Input/Feature Report DeviceIoControl"); return -1; } } @@ -793,150 +1295,253 @@ int HID_API_EXPORT HID_API_CALL hid_get_feature_report(hid_device *dev, unsigned res = GetOverlappedResult(dev->device_handle, &ol, &bytes_returned, TRUE/*wait*/); if (!res) { /* The operation failed. */ - register_error(dev, "Send Feature Report GetOverLappedResult"); + register_winapi_error(dev, L"Get Input/Feature Report GetOverLappedResult"); return -1; } - /* bytes_returned does not include the first byte which contains the - report ID. The data buffer actually contains one more byte than - bytes_returned. */ - bytes_returned++; + /* When numbered reports aren't used, + bytes_returned seem to include only what is actually received from the device + (not including the first byte with 0, as an indication "no numbered reports"). */ + if (data[0] == 0x0) { + bytes_returned++; + } return bytes_returned; -#endif +} + +int HID_API_EXPORT HID_API_CALL hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) +{ + /* We could use HidD_GetFeature() instead, but it doesn't give us an actual length, unfortunately */ + return hid_get_report(dev, IOCTL_HID_GET_FEATURE, data, length); +} + +int HID_API_EXPORT HID_API_CALL hid_send_output_report(hid_device* dev, const unsigned char* data, size_t length) +{ + BOOL res = FALSE; + unsigned char *buf; + size_t length_to_send; + + if (!data || !length) { + register_string_error(dev, L"Zero buffer/length"); + return -1; + } + + register_string_error(dev, NULL); + + /* Windows expects at least caps.OutputeportByteLength bytes passed + to HidD_SetOutputReport(), even if the report is shorter. Any less sent and + the function fails with error ERROR_INVALID_PARAMETER set. Any more + and HidD_SetOutputReport() silently truncates the data sent in the report + to caps.OutputReportByteLength. */ + if (length >= dev->output_report_length) { + buf = (unsigned char *) data; + length_to_send = length; + } else { + if (dev->write_buf == NULL) + dev->write_buf = (unsigned char *) malloc(dev->output_report_length); + buf = dev->write_buf; + memcpy(buf, data, length); + memset(buf + length, 0, dev->output_report_length - length); + length_to_send = dev->output_report_length; + } + + res = HidD_SetOutputReport(dev->device_handle, (PVOID)buf, (DWORD) length_to_send); + if (!res) { + register_string_error(dev, L"HidD_SetOutputReport"); + return -1; + } + + return (int) length; +} + +int HID_API_EXPORT HID_API_CALL hid_get_input_report(hid_device *dev, unsigned char *data, size_t length) +{ + /* We could use HidD_GetInputReport() instead, but it doesn't give us an actual length, unfortunately */ + return hid_get_report(dev, IOCTL_HID_GET_INPUT_REPORT, data, length); } void HID_API_EXPORT HID_API_CALL hid_close(hid_device *dev) { if (!dev) return; + CancelIo(dev->device_handle); free_hid_device(dev); } int HID_API_EXPORT_CALL HID_API_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen) { - BOOL res; - - res = HidD_GetManufacturerString(dev->device_handle, string, sizeof(wchar_t) * MIN(maxlen, MAX_STRING_WCHARS)); - if (!res) { - register_error(dev, "HidD_GetManufacturerString"); + if (!string || !maxlen) { + register_string_error(dev, L"Zero buffer/length"); return -1; } + if (!dev->device_info) { + register_string_error(dev, L"NULL device info"); + return -1; + } + + wcsncpy(string, dev->device_info->manufacturer_string, maxlen); + string[maxlen - 1] = L'\0'; + + register_string_error(dev, NULL); + return 0; } int HID_API_EXPORT_CALL HID_API_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen) { - BOOL res; - - res = HidD_GetProductString(dev->device_handle, string, sizeof(wchar_t) * MIN(maxlen, MAX_STRING_WCHARS)); - if (!res) { - register_error(dev, "HidD_GetProductString"); + if (!string || !maxlen) { + register_string_error(dev, L"Zero buffer/length"); return -1; } + if (!dev->device_info) { + register_string_error(dev, L"NULL device info"); + return -1; + } + + wcsncpy(string, dev->device_info->product_string, maxlen); + string[maxlen - 1] = L'\0'; + + register_string_error(dev, NULL); + return 0; } int HID_API_EXPORT_CALL HID_API_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen) { - BOOL res; - - res = HidD_GetSerialNumberString(dev->device_handle, string, sizeof(wchar_t) * MIN(maxlen, MAX_STRING_WCHARS)); - if (!res) { - register_error(dev, "HidD_GetSerialNumberString"); + if (!string || !maxlen) { + register_string_error(dev, L"Zero buffer/length"); return -1; } + if (!dev->device_info) { + register_string_error(dev, L"NULL device info"); + return -1; + } + + wcsncpy(string, dev->device_info->serial_number, maxlen); + string[maxlen - 1] = L'\0'; + + register_string_error(dev, NULL); + return 0; } +HID_API_EXPORT struct hid_device_info * HID_API_CALL hid_get_device_info(hid_device *dev) { + if (!dev->device_info) + { + register_string_error(dev, L"NULL device info"); + return NULL; + } + + return dev->device_info; +} + int HID_API_EXPORT_CALL HID_API_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen) { BOOL res; - res = HidD_GetIndexedString(dev->device_handle, string_index, string, sizeof(wchar_t) * MIN(maxlen, MAX_STRING_WCHARS)); + if (dev->device_info && dev->device_info->bus_type == HID_API_BUS_USB && maxlen > MAX_STRING_WCHARS_USB) { + string[MAX_STRING_WCHARS_USB] = L'\0'; + maxlen = MAX_STRING_WCHARS_USB; + } + + res = HidD_GetIndexedString(dev->device_handle, string_index, string, (ULONG)maxlen * sizeof(wchar_t)); if (!res) { - register_error(dev, "HidD_GetIndexedString"); + register_winapi_error(dev, L"HidD_GetIndexedString"); return -1; } + register_string_error(dev, NULL); + return 0; } +int HID_API_EXPORT_CALL hid_winapi_get_container_id(hid_device *dev, GUID *container_id) +{ + wchar_t *interface_path = NULL, *device_id = NULL; + CONFIGRET cr = CR_FAILURE; + DEVINST dev_node; + DEVPROPTYPE property_type; + ULONG len; + + if (!container_id) { + register_string_error(dev, L"Invalid Container ID"); + return -1; + } + + register_string_error(dev, NULL); + + interface_path = hid_internal_UTF8toUTF16(dev->device_info->path); + if (!interface_path) { + register_string_error(dev, L"Path conversion failure"); + goto end; + } + + /* Get the device id from interface path */ + device_id = hid_internal_get_device_interface_property(interface_path, &DEVPKEY_Device_InstanceId, DEVPROP_TYPE_STRING); + if (!device_id) { + register_string_error(dev, L"Failed to get device interface property InstanceId"); + goto end; + } + + /* Open devnode from device id */ + cr = CM_Locate_DevNodeW(&dev_node, (DEVINSTID_W)device_id, CM_LOCATE_DEVNODE_NORMAL); + if (cr != CR_SUCCESS) { + register_string_error(dev, L"Failed to locate device node"); + goto end; + } + + /* Get the container id from devnode */ + len = sizeof(*container_id); + cr = CM_Get_DevNode_PropertyW(dev_node, &DEVPKEY_Device_ContainerId, &property_type, (PBYTE)container_id, &len, 0); + if (cr == CR_SUCCESS && property_type != DEVPROP_TYPE_GUID) + cr = CR_FAILURE; + + if (cr != CR_SUCCESS) + register_string_error(dev, L"Failed to read ContainerId property from device node"); + +end: + free(interface_path); + free(device_id); + + return cr == CR_SUCCESS ? 0 : -1; +} + + +int HID_API_EXPORT_CALL hid_get_report_descriptor(hid_device *dev, unsigned char *buf, size_t buf_size) +{ + PHIDP_PREPARSED_DATA pp_data = NULL; + + if (!HidD_GetPreparsedData(dev->device_handle, &pp_data) || pp_data == NULL) { + register_string_error(dev, L"HidD_GetPreparsedData"); + return -1; + } + + int res = hid_winapi_descriptor_reconstruct_pp_data(pp_data, buf, buf_size); + + HidD_FreePreparsedData(pp_data); + + return res; +} HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) { - return (wchar_t*)dev->last_error_str; + if (dev) { + if (dev->last_error_str == NULL) + return L"Success"; + return (wchar_t*)dev->last_error_str; + } + + if (last_global_error_str == NULL) + return L"Success"; + return last_global_error_str; } - -/*#define PICPGM*/ -/*#define S11*/ -#define P32 -#ifdef S11 - unsigned short VendorID = 0xa0a0; - unsigned short ProductID = 0x0001; -#endif - -#ifdef P32 - unsigned short VendorID = 0x04d8; - unsigned short ProductID = 0x3f; -#endif - - -#ifdef PICPGM - unsigned short VendorID = 0x04d8; - unsigned short ProductID = 0x0033; -#endif - - -#if 0 -int __cdecl main(int argc, char* argv[]) -{ - int res; - unsigned char buf[65]; - - UNREFERENCED_PARAMETER(argc); - UNREFERENCED_PARAMETER(argv); - - /* Set up the command buffer. */ - memset(buf,0x00,sizeof(buf)); - buf[0] = 0; - buf[1] = 0x81; - - - /* Open the device. */ - int handle = open(VendorID, ProductID, L"12345"); - if (handle < 0) - printf("unable to open device\n"); - - - /* Toggle LED (cmd 0x80) */ - buf[1] = 0x80; - res = write(handle, buf, 65); - if (res < 0) - printf("Unable to write()\n"); - - /* Request state (cmd 0x81) */ - buf[1] = 0x81; - write(handle, buf, 65); - if (res < 0) - printf("Unable to write() (2)\n"); - - /* Read requested state */ - read(handle, buf, 65); - if (res < 0) - printf("Unable to read()\n"); - - /* Print out the returned buffer. */ - for (int i = 0; i < 4; i++) - printf("buf[%d]: %d\n", i, buf[i]); - - return 0; -} +#ifndef hidapi_winapi_EXPORTS +#include "hidapi_descriptor_reconstruct.c" #endif #ifdef __cplusplus diff --git a/libs/hidapi/windows/hidapi_cfgmgr32.h b/libs/hidapi/windows/hidapi_cfgmgr32.h new file mode 100644 index 0000000000..638512a8b4 --- /dev/null +++ b/libs/hidapi/windows/hidapi_cfgmgr32.h @@ -0,0 +1,75 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2022, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ + +#ifndef HIDAPI_CFGMGR32_H +#define HIDAPI_CFGMGR32_H + +#ifdef HIDAPI_USE_DDK + +#include +#include +#include +#include + +#else + +/* This part of the header mimics cfgmgr32.h, + but only what is used by HIDAPI */ + +#include +#include +#include + +typedef DWORD RETURN_TYPE; +typedef RETURN_TYPE CONFIGRET; +typedef DWORD DEVNODE, DEVINST; +typedef DEVNODE* PDEVNODE, * PDEVINST; +typedef WCHAR* DEVNODEID_W, * DEVINSTID_W; + +#define CR_SUCCESS (0x00000000) +#define CR_BUFFER_SMALL (0x0000001A) +#define CR_FAILURE (0x00000013) + +#define CM_LOCATE_DEVNODE_NORMAL 0x00000000 + +#define CM_GET_DEVICE_INTERFACE_LIST_PRESENT (0x00000000) + +typedef CONFIGRET(__stdcall* CM_Locate_DevNodeW_)(PDEVINST pdnDevInst, DEVINSTID_W pDeviceID, ULONG ulFlags); +typedef CONFIGRET(__stdcall* CM_Get_Parent_)(PDEVINST pdnDevInst, DEVINST dnDevInst, ULONG ulFlags); +typedef CONFIGRET(__stdcall* CM_Get_DevNode_PropertyW_)(DEVINST dnDevInst, CONST DEVPROPKEY* PropertyKey, DEVPROPTYPE* PropertyType, PBYTE PropertyBuffer, PULONG PropertyBufferSize, ULONG ulFlags); +typedef CONFIGRET(__stdcall* CM_Get_Device_Interface_PropertyW_)(LPCWSTR pszDeviceInterface, CONST DEVPROPKEY* PropertyKey, DEVPROPTYPE* PropertyType, PBYTE PropertyBuffer, PULONG PropertyBufferSize, ULONG ulFlags); +typedef CONFIGRET(__stdcall* CM_Get_Device_Interface_List_SizeW_)(PULONG pulLen, LPGUID InterfaceClassGuid, DEVINSTID_W pDeviceID, ULONG ulFlags); +typedef CONFIGRET(__stdcall* CM_Get_Device_Interface_ListW_)(LPGUID InterfaceClassGuid, DEVINSTID_W pDeviceID, PZZWSTR Buffer, ULONG BufferLen, ULONG ulFlags); + +// from devpkey.h +DEFINE_DEVPROPKEY(DEVPKEY_NAME, 0xb725f130, 0x47ef, 0x101a, 0xa5, 0xf1, 0x02, 0x60, 0x8c, 0x9e, 0xeb, 0xac, 10); // DEVPROP_TYPE_STRING +DEFINE_DEVPROPKEY(DEVPKEY_Device_Manufacturer, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 13); // DEVPROP_TYPE_STRING +DEFINE_DEVPROPKEY(DEVPKEY_Device_InstanceId, 0x78c34fc8, 0x104a, 0x4aca, 0x9e, 0xa4, 0x52, 0x4d, 0x52, 0x99, 0x6e, 0x57, 256); // DEVPROP_TYPE_STRING +DEFINE_DEVPROPKEY(DEVPKEY_Device_HardwareIds, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 3); // DEVPROP_TYPE_STRING_LIST +DEFINE_DEVPROPKEY(DEVPKEY_Device_CompatibleIds, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 4); // DEVPROP_TYPE_STRING_LIST +DEFINE_DEVPROPKEY(DEVPKEY_Device_ContainerId, 0x8c7ed206, 0x3f8a, 0x4827, 0xb3, 0xab, 0xae, 0x9e, 0x1f, 0xae, 0xfc, 0x6c, 2); // DEVPROP_TYPE_GUID + +// from propkey.h +DEFINE_PROPERTYKEY(PKEY_DeviceInterface_Bluetooth_DeviceAddress, 0x2BD67D8B, 0x8BEB, 0x48D5, 0x87, 0xE0, 0x6C, 0xDA, 0x34, 0x28, 0x04, 0x0A, 1); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_DeviceInterface_Bluetooth_Manufacturer, 0x2BD67D8B, 0x8BEB, 0x48D5, 0x87, 0xE0, 0x6C, 0xDA, 0x34, 0x28, 0x04, 0x0A, 4); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_DeviceInterface_Bluetooth_ModelNumber, 0x2BD67D8B, 0x8BEB, 0x48D5, 0x87, 0xE0, 0x6C, 0xDA, 0x34, 0x28, 0x04, 0x0A, 5); // DEVPROP_TYPE_STRING + +#endif + +#endif /* HIDAPI_CFGMGR32_H */ diff --git a/libs/hidapi/windows/hidapi_descriptor_reconstruct.c b/libs/hidapi/windows/hidapi_descriptor_reconstruct.c new file mode 100644 index 0000000000..c76d4ea68c --- /dev/null +++ b/libs/hidapi/windows/hidapi_descriptor_reconstruct.c @@ -0,0 +1,987 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2022, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ +#include "hidapi_descriptor_reconstruct.h" + +/** + * @brief References to report descriptor buffer. + * + */ +struct rd_buffer { + unsigned char* buf; /* Pointer to the array which stores the reconstructed descriptor */ + size_t buf_size; /* Size of the buffer in bytes */ + size_t byte_idx; /* Index of the next report byte to write to buf array */ +}; + +/** + * @brief Function that appends a byte to encoded report descriptor buffer. + * + * @param[in] byte Single byte to append. + * @param rpt_desc Pointer to report descriptor buffer struct. + */ +static void rd_append_byte(unsigned char byte, struct rd_buffer* rpt_desc) { + if (rpt_desc->byte_idx < rpt_desc->buf_size) { + rpt_desc->buf[rpt_desc->byte_idx] = byte; + rpt_desc->byte_idx++; + } +} + +/** + * @brief Writes a short report descriptor item according USB HID spec 1.11 chapter 6.2.2.2. + * + * @param[in] rd_item Enumeration identifying type (Main, Global, Local) and function (e.g Usage or Report Count) of the item. + * @param[in] data Data (Size depends on rd_item 0,1,2 or 4bytes). + * @param rpt_desc Pointer to report descriptor buffer struct. + * + * @return Returns 0 if successful, -1 for error. + */ +static int rd_write_short_item(rd_items rd_item, LONG64 data, struct rd_buffer* rpt_desc) { + if (rd_item & 0x03) { + // Invalid input data, last to bits are reserved for data size + return -1; + } + + if (rd_item == rd_main_collection_end) { + // Item without data (1Byte prefix only) + unsigned char oneBytePrefix = (unsigned char) rd_item + 0x00; + rd_append_byte(oneBytePrefix, rpt_desc); + } + else if ((rd_item == rd_global_logical_minimum) || + (rd_item == rd_global_logical_maximum) || + (rd_item == rd_global_physical_minimum) || + (rd_item == rd_global_physical_maximum)) { + // Item with signed integer data + if ((data >= -128) && (data <= 127)) { + // 1Byte prefix + 1Byte data + unsigned char oneBytePrefix = (unsigned char) rd_item + 0x01; + char localData = (char)data; + rd_append_byte(oneBytePrefix, rpt_desc); + rd_append_byte(localData & 0xFF, rpt_desc); + } + else if ((data >= -32768) && (data <= 32767)) { + // 1Byte prefix + 2Byte data + unsigned char oneBytePrefix = (unsigned char) rd_item + 0x02; + INT16 localData = (INT16)data; + rd_append_byte(oneBytePrefix, rpt_desc); + rd_append_byte(localData & 0xFF, rpt_desc); + rd_append_byte(localData >> 8 & 0xFF, rpt_desc); + } + else if ((data >= -2147483648LL) && (data <= 2147483647)) { + // 1Byte prefix + 4Byte data + unsigned char oneBytePrefix = (unsigned char) rd_item + 0x03; + INT32 localData = (INT32)data; + rd_append_byte(oneBytePrefix, rpt_desc); + rd_append_byte(localData & 0xFF, rpt_desc); + rd_append_byte(localData >> 8 & 0xFF, rpt_desc); + rd_append_byte(localData >> 16 & 0xFF, rpt_desc); + rd_append_byte(localData >> 24 & 0xFF, rpt_desc); + } + else { + // Data out of 32 bit signed integer range + return -1; + } + } + else { + // Item with unsigned integer data + if ((data >= 0) && (data <= 0xFF)) { + // 1Byte prefix + 1Byte data + unsigned char oneBytePrefix = (unsigned char) rd_item + 0x01; + unsigned char localData = (unsigned char)data; + rd_append_byte(oneBytePrefix, rpt_desc); + rd_append_byte(localData & 0xFF, rpt_desc); + } + else if ((data >= 0) && (data <= 0xFFFF)) { + // 1Byte prefix + 2Byte data + unsigned char oneBytePrefix = (unsigned char) rd_item + 0x02; + UINT16 localData = (UINT16)data; + rd_append_byte(oneBytePrefix, rpt_desc); + rd_append_byte(localData & 0xFF, rpt_desc); + rd_append_byte(localData >> 8 & 0xFF, rpt_desc); + } + else if ((data >= 0) && (data <= 0xFFFFFFFF)) { + // 1Byte prefix + 4Byte data + unsigned char oneBytePrefix = (unsigned char) rd_item + 0x03; + UINT32 localData = (UINT32)data; + rd_append_byte(oneBytePrefix, rpt_desc); + rd_append_byte(localData & 0xFF, rpt_desc); + rd_append_byte(localData >> 8 & 0xFF, rpt_desc); + rd_append_byte(localData >> 16 & 0xFF, rpt_desc); + rd_append_byte(localData >> 24 & 0xFF, rpt_desc); + } + else { + // Data out of 32 bit unsigned integer range + return -1; + } + } + return 0; +} + +static struct rd_main_item_node * rd_append_main_item_node(int first_bit, int last_bit, rd_node_type type_of_node, int caps_index, int collection_index, rd_main_items main_item_type, unsigned char report_id, struct rd_main_item_node **list) { + struct rd_main_item_node *new_list_node; + + // Determine last node in the list + while (*list != NULL) + { + list = &(*list)->next; + } + + new_list_node = malloc(sizeof(*new_list_node)); // Create new list entry + new_list_node->FirstBit = first_bit; + new_list_node->LastBit = last_bit; + new_list_node->TypeOfNode = type_of_node; + new_list_node->CapsIndex = caps_index; + new_list_node->CollectionIndex = collection_index; + new_list_node->MainItemType = main_item_type; + new_list_node->ReportID = report_id; + new_list_node->next = NULL; // NULL marks last node in the list + + *list = new_list_node; + return new_list_node; +} + +static struct rd_main_item_node * rd_insert_main_item_node(int first_bit, int last_bit, rd_node_type type_of_node, int caps_index, int collection_index, rd_main_items main_item_type, unsigned char report_id, struct rd_main_item_node **list) { + // Insert item after the main item node referenced by list + struct rd_main_item_node *next_item = (*list)->next; + (*list)->next = NULL; + rd_append_main_item_node(first_bit, last_bit, type_of_node, caps_index, collection_index, main_item_type, report_id, list); + (*list)->next->next = next_item; + return (*list)->next; +} + +static struct rd_main_item_node * rd_search_main_item_list_for_bit_position(int search_bit, rd_main_items main_item_type, unsigned char report_id, struct rd_main_item_node **list) { + // Determine first INPUT/OUTPUT/FEATURE main item, where the last bit position is equal or greater than the search bit position + + while (((*list)->next->MainItemType != rd_collection) && + ((*list)->next->MainItemType != rd_collection_end) && + !(((*list)->next->LastBit >= search_bit) && + ((*list)->next->ReportID == report_id) && + ((*list)->next->MainItemType == main_item_type)) + ) + { + list = &(*list)->next; + } + return *list; +} + +int hid_winapi_descriptor_reconstruct_pp_data(void *preparsed_data, unsigned char *buf, size_t buf_size) +{ + hidp_preparsed_data *pp_data = (hidp_preparsed_data *) preparsed_data; + + // Check if MagicKey is correct, to ensure that pp_data points to an valid preparse data structure + if (memcmp(pp_data->MagicKey, "HidP KDR", 8) != 0) { + return -1; + } + + struct rd_buffer rpt_desc = { + .buf = buf, + .buf_size = buf_size, + .byte_idx = 0 + }; + + // Set pointer to the first node of link_collection_nodes + phid_pp_link_collection_node link_collection_nodes = (phid_pp_link_collection_node)(((unsigned char*)&pp_data->caps[0]) + pp_data->FirstByteOfLinkCollectionArray); + + // **************************************************************************************************************************** + // Create lookup tables for the bit range of each report per collection (position of first bit and last bit in each collection) + // coll_bit_range[COLLECTION_INDEX][REPORT_ID][INPUT/OUTPUT/FEATURE] + // **************************************************************************************************************************** + + // Allocate memory and initialize lookup table + rd_bit_range ****coll_bit_range; + coll_bit_range = malloc(pp_data->NumberLinkCollectionNodes * sizeof(*coll_bit_range)); + for (USHORT collection_node_idx = 0; collection_node_idx < pp_data->NumberLinkCollectionNodes; collection_node_idx++) { + coll_bit_range[collection_node_idx] = malloc(256 * sizeof(*coll_bit_range[0])); // 256 possible report IDs (incl. 0x00) + for (int reportid_idx = 0; reportid_idx < 256; reportid_idx++) { + coll_bit_range[collection_node_idx][reportid_idx] = malloc(NUM_OF_HIDP_REPORT_TYPES * sizeof(*coll_bit_range[0][0])); + for (HIDP_REPORT_TYPE rt_idx = 0; rt_idx < NUM_OF_HIDP_REPORT_TYPES; rt_idx++) { + coll_bit_range[collection_node_idx][reportid_idx][rt_idx] = malloc(sizeof(rd_bit_range)); + coll_bit_range[collection_node_idx][reportid_idx][rt_idx]->FirstBit = -1; + coll_bit_range[collection_node_idx][reportid_idx][rt_idx]->LastBit = -1; + } + } + } + + // Fill the lookup table where caps exist + for (HIDP_REPORT_TYPE rt_idx = 0; rt_idx < NUM_OF_HIDP_REPORT_TYPES; rt_idx++) { + for (USHORT caps_idx = pp_data->caps_info[rt_idx].FirstCap; caps_idx < pp_data->caps_info[rt_idx].LastCap; caps_idx++) { + int first_bit, last_bit; + first_bit = (pp_data->caps[caps_idx].BytePosition - 1) * 8 + + pp_data->caps[caps_idx].BitPosition; + last_bit = first_bit + pp_data->caps[caps_idx].ReportSize + * pp_data->caps[caps_idx].ReportCount - 1; + if (coll_bit_range[pp_data->caps[caps_idx].LinkCollection][pp_data->caps[caps_idx].ReportID][rt_idx]->FirstBit == -1 || + coll_bit_range[pp_data->caps[caps_idx].LinkCollection][pp_data->caps[caps_idx].ReportID][rt_idx]->FirstBit > first_bit) { + coll_bit_range[pp_data->caps[caps_idx].LinkCollection][pp_data->caps[caps_idx].ReportID][rt_idx]->FirstBit = first_bit; + } + if (coll_bit_range[pp_data->caps[caps_idx].LinkCollection][pp_data->caps[caps_idx].ReportID][rt_idx]->LastBit < last_bit) { + coll_bit_range[pp_data->caps[caps_idx].LinkCollection][pp_data->caps[caps_idx].ReportID][rt_idx]->LastBit = last_bit; + } + } + } + + // ************************************************************************* + // -Determine hierarchy levels of each collections and store it in: + // coll_levels[COLLECTION_INDEX] + // -Determine number of direct childs of each collections and store it in: + // coll_number_of_direct_childs[COLLECTION_INDEX] + // ************************************************************************* + int max_coll_level = 0; + int *coll_levels = malloc(pp_data->NumberLinkCollectionNodes * sizeof(coll_levels[0])); + int *coll_number_of_direct_childs = malloc(pp_data->NumberLinkCollectionNodes * sizeof(coll_number_of_direct_childs[0])); + for (USHORT collection_node_idx = 0; collection_node_idx < pp_data->NumberLinkCollectionNodes; collection_node_idx++) { + coll_levels[collection_node_idx] = -1; + coll_number_of_direct_childs[collection_node_idx] = 0; + } + + { + int actual_coll_level = 0; + USHORT collection_node_idx = 0; + while (actual_coll_level >= 0) { + coll_levels[collection_node_idx] = actual_coll_level; + if ((link_collection_nodes[collection_node_idx].NumberOfChildren > 0) && + (coll_levels[link_collection_nodes[collection_node_idx].FirstChild] == -1)) { + actual_coll_level++; + coll_levels[collection_node_idx] = actual_coll_level; + if (max_coll_level < actual_coll_level) { + max_coll_level = actual_coll_level; + } + coll_number_of_direct_childs[collection_node_idx]++; + collection_node_idx = link_collection_nodes[collection_node_idx].FirstChild; + } + else if (link_collection_nodes[collection_node_idx].NextSibling != 0) { + coll_number_of_direct_childs[link_collection_nodes[collection_node_idx].Parent]++; + collection_node_idx = link_collection_nodes[collection_node_idx].NextSibling; + } + else { + actual_coll_level--; + if (actual_coll_level >= 0) { + collection_node_idx = link_collection_nodes[collection_node_idx].Parent; + } + } + } + } + + // ********************************************************************************* + // Propagate the bit range of each report from the child collections to their parent + // and store the merged result for the parent + // ********************************************************************************* + for (int actual_coll_level = max_coll_level - 1; actual_coll_level >= 0; actual_coll_level--) { + for (USHORT collection_node_idx = 0; collection_node_idx < pp_data->NumberLinkCollectionNodes; collection_node_idx++) { + if (coll_levels[collection_node_idx] == actual_coll_level) { + USHORT child_idx = link_collection_nodes[collection_node_idx].FirstChild; + while (child_idx) { + for (int reportid_idx = 0; reportid_idx < 256; reportid_idx++) { + for (HIDP_REPORT_TYPE rt_idx = 0; rt_idx < NUM_OF_HIDP_REPORT_TYPES; rt_idx++) { + // Merge bit range from childs + if ((coll_bit_range[child_idx][reportid_idx][rt_idx]->FirstBit != -1) && + (coll_bit_range[collection_node_idx][reportid_idx][rt_idx]->FirstBit > coll_bit_range[child_idx][reportid_idx][rt_idx]->FirstBit)) { + coll_bit_range[collection_node_idx][reportid_idx][rt_idx]->FirstBit = coll_bit_range[child_idx][reportid_idx][rt_idx]->FirstBit; + } + if (coll_bit_range[collection_node_idx][reportid_idx][rt_idx]->LastBit < coll_bit_range[child_idx][reportid_idx][rt_idx]->LastBit) { + coll_bit_range[collection_node_idx][reportid_idx][rt_idx]->LastBit = coll_bit_range[child_idx][reportid_idx][rt_idx]->LastBit; + } + child_idx = link_collection_nodes[child_idx].NextSibling; + } + } + } + } + } + } + + // ************************************************************************************************** + // Determine child collection order of the whole hierarchy, based on previously determined bit ranges + // and store it this index coll_child_order[COLLECTION_INDEX][DIRECT_CHILD_INDEX] + // ************************************************************************************************** + USHORT **coll_child_order; + coll_child_order = malloc(pp_data->NumberLinkCollectionNodes * sizeof(*coll_child_order)); + { + BOOLEAN *coll_parsed_flag; + coll_parsed_flag = malloc(pp_data->NumberLinkCollectionNodes * sizeof(coll_parsed_flag[0])); + for (USHORT collection_node_idx = 0; collection_node_idx < pp_data->NumberLinkCollectionNodes; collection_node_idx++) { + coll_parsed_flag[collection_node_idx] = FALSE; + } + int actual_coll_level = 0; + USHORT collection_node_idx = 0; + while (actual_coll_level >= 0) { + if ((coll_number_of_direct_childs[collection_node_idx] != 0) && + (coll_parsed_flag[link_collection_nodes[collection_node_idx].FirstChild] == FALSE)) { + coll_parsed_flag[link_collection_nodes[collection_node_idx].FirstChild] = TRUE; + coll_child_order[collection_node_idx] = malloc((coll_number_of_direct_childs[collection_node_idx]) * sizeof(*coll_child_order[0])); + + { + // Create list of child collection indices + // sorted reverse to the order returned to HidP_GetLinkCollectionNodeschild + // which seems to match the original order, as long as no bit position needs to be considered + USHORT child_idx = link_collection_nodes[collection_node_idx].FirstChild; + int child_count = coll_number_of_direct_childs[collection_node_idx] - 1; + coll_child_order[collection_node_idx][child_count] = child_idx; + while (link_collection_nodes[child_idx].NextSibling) { + child_count--; + child_idx = link_collection_nodes[child_idx].NextSibling; + coll_child_order[collection_node_idx][child_count] = child_idx; + } + } + + if (coll_number_of_direct_childs[collection_node_idx] > 1) { + // Sort child collections indices by bit positions + for (HIDP_REPORT_TYPE rt_idx = 0; rt_idx < NUM_OF_HIDP_REPORT_TYPES; rt_idx++) { + for (int reportid_idx = 0; reportid_idx < 256; reportid_idx++) { + for (int child_idx = 1; child_idx < coll_number_of_direct_childs[collection_node_idx]; child_idx++) { + // since the coll_bit_range array is not sorted, we need to reference the collection index in + // our sorted coll_child_order array, and look up the corresponding bit ranges for comparing values to sort + int prev_coll_idx = coll_child_order[collection_node_idx][child_idx - 1]; + int cur_coll_idx = coll_child_order[collection_node_idx][child_idx]; + if ((coll_bit_range[prev_coll_idx][reportid_idx][rt_idx]->FirstBit != -1) && + (coll_bit_range[cur_coll_idx][reportid_idx][rt_idx]->FirstBit != -1) && + (coll_bit_range[prev_coll_idx][reportid_idx][rt_idx]->FirstBit > coll_bit_range[cur_coll_idx][reportid_idx][rt_idx]->FirstBit)) { + // Swap position indices of the two compared child collections + USHORT idx_latch = coll_child_order[collection_node_idx][child_idx - 1]; + coll_child_order[collection_node_idx][child_idx - 1] = coll_child_order[collection_node_idx][child_idx]; + coll_child_order[collection_node_idx][child_idx] = idx_latch; + } + } + } + } + } + actual_coll_level++; + collection_node_idx = link_collection_nodes[collection_node_idx].FirstChild; + } + else if (link_collection_nodes[collection_node_idx].NextSibling != 0) { + collection_node_idx = link_collection_nodes[collection_node_idx].NextSibling; + } + else { + actual_coll_level--; + if (actual_coll_level >= 0) { + collection_node_idx = link_collection_nodes[collection_node_idx].Parent; + } + } + } + free(coll_parsed_flag); + } + + + // *************************************************************************************** + // Create sorted main_item_list containing all the Collection and CollectionEnd main items + // *************************************************************************************** + struct rd_main_item_node *main_item_list = NULL; // List root + // Lookup table to find the Collection items in the list by index + struct rd_main_item_node **coll_begin_lookup = malloc(pp_data->NumberLinkCollectionNodes * sizeof(*coll_begin_lookup)); + struct rd_main_item_node **coll_end_lookup = malloc(pp_data->NumberLinkCollectionNodes * sizeof(*coll_end_lookup)); + { + int *coll_last_written_child = malloc(pp_data->NumberLinkCollectionNodes * sizeof(coll_last_written_child[0])); + for (USHORT collection_node_idx = 0; collection_node_idx < pp_data->NumberLinkCollectionNodes; collection_node_idx++) { + coll_last_written_child[collection_node_idx] = -1; + } + + int actual_coll_level = 0; + USHORT collection_node_idx = 0; + struct rd_main_item_node *firstDelimiterNode = NULL; + struct rd_main_item_node *delimiterCloseNode = NULL; + coll_begin_lookup[0] = rd_append_main_item_node(0, 0, rd_item_node_collection, 0, collection_node_idx, rd_collection, 0, &main_item_list); + while (actual_coll_level >= 0) { + if ((coll_number_of_direct_childs[collection_node_idx] != 0) && + (coll_last_written_child[collection_node_idx] == -1)) { + // Collection has child collections, but none is written to the list yet + + coll_last_written_child[collection_node_idx] = coll_child_order[collection_node_idx][0]; + collection_node_idx = coll_child_order[collection_node_idx][0]; + + // In a HID Report Descriptor, the first usage declared is the most preferred usage for the control. + // While the order in the WIN32 capabiliy strutures is the opposite: + // Here the preferred usage is the last aliased usage in the sequence. + + if (link_collection_nodes[collection_node_idx].IsAlias && (firstDelimiterNode == NULL)) { + // Alliased Collection (First node in link_collection_nodes -> Last entry in report descriptor output) + firstDelimiterNode = main_item_list; + coll_begin_lookup[collection_node_idx] = rd_append_main_item_node(0, 0, rd_item_node_collection, 0, collection_node_idx, rd_delimiter_usage, 0, &main_item_list); + coll_begin_lookup[collection_node_idx] = rd_append_main_item_node(0, 0, rd_item_node_collection, 0, collection_node_idx, rd_delimiter_close, 0, &main_item_list); + delimiterCloseNode = main_item_list; + } + else { + // Normal not aliased collection + coll_begin_lookup[collection_node_idx] = rd_append_main_item_node(0, 0, rd_item_node_collection, 0, collection_node_idx, rd_collection, 0, &main_item_list); + actual_coll_level++; + } + + + } + else if ((coll_number_of_direct_childs[collection_node_idx] > 1) && + (coll_last_written_child[collection_node_idx] != coll_child_order[collection_node_idx][coll_number_of_direct_childs[collection_node_idx] - 1])) { + // Collection has child collections, and this is not the first child + + int nextChild = 1; + while (coll_last_written_child[collection_node_idx] != coll_child_order[collection_node_idx][nextChild - 1]) { + nextChild++; + } + coll_last_written_child[collection_node_idx] = coll_child_order[collection_node_idx][nextChild]; + collection_node_idx = coll_child_order[collection_node_idx][nextChild]; + + if (link_collection_nodes[collection_node_idx].IsAlias && (firstDelimiterNode == NULL)) { + // Alliased Collection (First node in link_collection_nodes -> Last entry in report descriptor output) + firstDelimiterNode = main_item_list; + coll_begin_lookup[collection_node_idx] = rd_append_main_item_node(0, 0, rd_item_node_collection, 0, collection_node_idx, rd_delimiter_usage, 0, &main_item_list); + coll_begin_lookup[collection_node_idx] = rd_append_main_item_node(0, 0, rd_item_node_collection, 0, collection_node_idx, rd_delimiter_close, 0, &main_item_list); + delimiterCloseNode = main_item_list; + } + else if (link_collection_nodes[collection_node_idx].IsAlias && (firstDelimiterNode != NULL)) { + coll_begin_lookup[collection_node_idx] = rd_insert_main_item_node(0, 0, rd_item_node_collection, 0, collection_node_idx, rd_delimiter_usage, 0, &firstDelimiterNode); + } + else if (!link_collection_nodes[collection_node_idx].IsAlias && (firstDelimiterNode != NULL)) { + coll_begin_lookup[collection_node_idx] = rd_insert_main_item_node(0, 0, rd_item_node_collection, 0, collection_node_idx, rd_delimiter_usage, 0, &firstDelimiterNode); + coll_begin_lookup[collection_node_idx] = rd_insert_main_item_node(0, 0, rd_item_node_collection, 0, collection_node_idx, rd_delimiter_open, 0, &firstDelimiterNode); + firstDelimiterNode = NULL; + main_item_list = delimiterCloseNode; + delimiterCloseNode = NULL; // Last entry of alias has .IsAlias == FALSE + } + if (!link_collection_nodes[collection_node_idx].IsAlias) { + coll_begin_lookup[collection_node_idx] = rd_append_main_item_node(0, 0, rd_item_node_collection, 0, collection_node_idx, rd_collection, 0, &main_item_list); + actual_coll_level++; + } + } + else { + actual_coll_level--; + coll_end_lookup[collection_node_idx] = rd_append_main_item_node(0, 0, rd_item_node_collection, 0, collection_node_idx, rd_collection_end, 0, &main_item_list); + collection_node_idx = link_collection_nodes[collection_node_idx].Parent; + } + } + free(coll_last_written_child); + } + + + // **************************************************************** + // Inserted Input/Output/Feature main items into the main_item_list + // in order of reconstructed bit positions + // **************************************************************** + for (HIDP_REPORT_TYPE rt_idx = 0; rt_idx < NUM_OF_HIDP_REPORT_TYPES; rt_idx++) { + // Add all value caps to node list + struct rd_main_item_node *firstDelimiterNode = NULL; + struct rd_main_item_node *delimiterCloseNode = NULL; + for (USHORT caps_idx = pp_data->caps_info[rt_idx].FirstCap; caps_idx < pp_data->caps_info[rt_idx].LastCap; caps_idx++) { + struct rd_main_item_node *coll_begin = coll_begin_lookup[pp_data->caps[caps_idx].LinkCollection]; + int first_bit, last_bit; + first_bit = (pp_data->caps[caps_idx].BytePosition - 1) * 8 + + pp_data->caps[caps_idx].BitPosition; + last_bit = first_bit + pp_data->caps[caps_idx].ReportSize * + pp_data->caps[caps_idx].ReportCount - 1; + + for (int child_idx = 0; child_idx < coll_number_of_direct_childs[pp_data->caps[caps_idx].LinkCollection]; child_idx++) { + // Determine in which section before/between/after child collection the item should be inserted + if (first_bit < coll_bit_range[coll_child_order[pp_data->caps[caps_idx].LinkCollection][child_idx]][pp_data->caps[caps_idx].ReportID][rt_idx]->FirstBit) + { + // Note, that the default value for undefined coll_bit_range is -1, which can't be greater than the bit position + break; + } + coll_begin = coll_end_lookup[coll_child_order[pp_data->caps[caps_idx].LinkCollection][child_idx]]; + } + struct rd_main_item_node *list_node; + list_node = rd_search_main_item_list_for_bit_position(first_bit, (rd_main_items) rt_idx, pp_data->caps[caps_idx].ReportID, &coll_begin); + + // In a HID Report Descriptor, the first usage declared is the most preferred usage for the control. + // While the order in the WIN32 capabiliy strutures is the opposite: + // Here the preferred usage is the last aliased usage in the sequence. + + if (pp_data->caps[caps_idx].IsAlias && (firstDelimiterNode == NULL)) { + // Alliased Usage (First node in pp_data->caps -> Last entry in report descriptor output) + firstDelimiterNode = list_node; + rd_insert_main_item_node(first_bit, last_bit, rd_item_node_cap, caps_idx, pp_data->caps[caps_idx].LinkCollection, rd_delimiter_usage, pp_data->caps[caps_idx].ReportID, &list_node); + rd_insert_main_item_node(first_bit, last_bit, rd_item_node_cap, caps_idx, pp_data->caps[caps_idx].LinkCollection, rd_delimiter_close, pp_data->caps[caps_idx].ReportID, &list_node); + delimiterCloseNode = list_node; + } else if (pp_data->caps[caps_idx].IsAlias && (firstDelimiterNode != NULL)) { + rd_insert_main_item_node(first_bit, last_bit, rd_item_node_cap, caps_idx, pp_data->caps[caps_idx].LinkCollection, rd_delimiter_usage, pp_data->caps[caps_idx].ReportID, &list_node); + } + else if (!pp_data->caps[caps_idx].IsAlias && (firstDelimiterNode != NULL)) { + // Alliased Collection (Last node in pp_data->caps -> First entry in report descriptor output) + rd_insert_main_item_node(first_bit, last_bit, rd_item_node_cap, caps_idx, pp_data->caps[caps_idx].LinkCollection, rd_delimiter_usage, pp_data->caps[caps_idx].ReportID, &list_node); + rd_insert_main_item_node(first_bit, last_bit, rd_item_node_cap, caps_idx, pp_data->caps[caps_idx].LinkCollection, rd_delimiter_open, pp_data->caps[caps_idx].ReportID, &list_node); + firstDelimiterNode = NULL; + list_node = delimiterCloseNode; + delimiterCloseNode = NULL; // Last entry of alias has .IsAlias == FALSE + } + if (!pp_data->caps[caps_idx].IsAlias) { + rd_insert_main_item_node(first_bit, last_bit, rd_item_node_cap, caps_idx, pp_data->caps[caps_idx].LinkCollection, (rd_main_items) rt_idx, pp_data->caps[caps_idx].ReportID, &list_node); + } + } + } + + + // *********************************************************** + // Add const main items for padding to main_item_list + // -To fill all bit gaps + // -At each report end for 8bit padding + // Note that information about the padding at the report end, + // is not stored in the preparsed data, but in practice all + // report descriptors seem to have it, as assumed here. + // *********************************************************** + { + int last_bit_position[NUM_OF_HIDP_REPORT_TYPES][256]; + struct rd_main_item_node *last_report_item_lookup[NUM_OF_HIDP_REPORT_TYPES][256]; + for (HIDP_REPORT_TYPE rt_idx = 0; rt_idx < NUM_OF_HIDP_REPORT_TYPES; rt_idx++) { + for (int reportid_idx = 0; reportid_idx < 256; reportid_idx++) { + last_bit_position[rt_idx][reportid_idx] = -1; + last_report_item_lookup[rt_idx][reportid_idx] = NULL; + } + } + + struct rd_main_item_node *list = main_item_list; // List root; + + while (list->next != NULL) + { + if ((list->MainItemType >= rd_input) && + (list->MainItemType <= rd_feature)) { + // INPUT, OUTPUT or FEATURE + if (list->FirstBit != -1) { + if ((last_bit_position[list->MainItemType][list->ReportID] + 1 != list->FirstBit) && + (last_report_item_lookup[list->MainItemType][list->ReportID] != NULL) && + (last_report_item_lookup[list->MainItemType][list->ReportID]->FirstBit != list->FirstBit) // Happens in case of IsMultipleItemsForArray for multiple dedicated usages for a multi-button array + ) { + struct rd_main_item_node *list_node = rd_search_main_item_list_for_bit_position(last_bit_position[list->MainItemType][list->ReportID], list->MainItemType, list->ReportID, &last_report_item_lookup[list->MainItemType][list->ReportID]); + rd_insert_main_item_node(last_bit_position[list->MainItemType][list->ReportID] + 1, list->FirstBit - 1, rd_item_node_padding, -1, 0, list->MainItemType, list->ReportID, &list_node); + } + last_bit_position[list->MainItemType][list->ReportID] = list->LastBit; + last_report_item_lookup[list->MainItemType][list->ReportID] = list; + } + } + list = list->next; + } + // Add 8 bit padding at each report end + for (HIDP_REPORT_TYPE rt_idx = 0; rt_idx < NUM_OF_HIDP_REPORT_TYPES; rt_idx++) { + for (int reportid_idx = 0; reportid_idx < 256; reportid_idx++) { + if (last_bit_position[rt_idx][reportid_idx] != -1) { + int padding = 8 - ((last_bit_position[rt_idx][reportid_idx] + 1) % 8); + if (padding < 8) { + // Insert padding item after item referenced in last_report_item_lookup + rd_insert_main_item_node(last_bit_position[rt_idx][reportid_idx] + 1, last_bit_position[rt_idx][reportid_idx] + padding, rd_item_node_padding, -1, 0, (rd_main_items) rt_idx, (unsigned char) reportid_idx, &last_report_item_lookup[rt_idx][reportid_idx]); + } + } + } + } + } + + + // *********************************** + // Encode the report descriptor output + // *********************************** + UCHAR last_report_id = 0; + USAGE last_usage_page = 0; + LONG last_physical_min = 0;// If both, Physical Minimum and Physical Maximum are 0, the logical limits should be taken as physical limits according USB HID spec 1.11 chapter 6.2.2.7 + LONG last_physical_max = 0; + ULONG last_unit_exponent = 0; // If Unit Exponent is Undefined it should be considered as 0 according USB HID spec 1.11 chapter 6.2.2.7 + ULONG last_unit = 0; // If the first nibble is 7, or second nibble of Unit is 0, the unit is None according USB HID spec 1.11 chapter 6.2.2.7 + BOOLEAN inhibit_write_of_usage = FALSE; // Needed in case of delimited usage print, before the normal collection or cap + int report_count = 0; + while (main_item_list != NULL) + { + int rt_idx = main_item_list->MainItemType; + int caps_idx = main_item_list->CapsIndex; + if (main_item_list->MainItemType == rd_collection) { + if (last_usage_page != link_collection_nodes[main_item_list->CollectionIndex].LinkUsagePage) { + // Write "Usage Page" at the begin of a collection - except it refers the same table as wrote last + rd_write_short_item(rd_global_usage_page, link_collection_nodes[main_item_list->CollectionIndex].LinkUsagePage, &rpt_desc); + last_usage_page = link_collection_nodes[main_item_list->CollectionIndex].LinkUsagePage; + } + if (inhibit_write_of_usage) { + // Inhibit only once after DELIMITER statement + inhibit_write_of_usage = FALSE; + } + else { + // Write "Usage" of collection + rd_write_short_item(rd_local_usage, link_collection_nodes[main_item_list->CollectionIndex].LinkUsage, &rpt_desc); + } + // Write begin of "Collection" + rd_write_short_item(rd_main_collection, link_collection_nodes[main_item_list->CollectionIndex].CollectionType, &rpt_desc); + } + else if (main_item_list->MainItemType == rd_collection_end) { + // Write "End Collection" + rd_write_short_item(rd_main_collection_end, 0, &rpt_desc); + } + else if (main_item_list->MainItemType == rd_delimiter_open) { + if (main_item_list->CollectionIndex != -1) { + // Write "Usage Page" inside of a collection delmiter section + if (last_usage_page != link_collection_nodes[main_item_list->CollectionIndex].LinkUsagePage) { + rd_write_short_item(rd_global_usage_page, link_collection_nodes[main_item_list->CollectionIndex].LinkUsagePage, &rpt_desc); + last_usage_page = link_collection_nodes[main_item_list->CollectionIndex].LinkUsagePage; + } + } + else if (main_item_list->CapsIndex != 0) { + // Write "Usage Page" inside of a main item delmiter section + if (pp_data->caps[caps_idx].UsagePage != last_usage_page) { + rd_write_short_item(rd_global_usage_page, pp_data->caps[caps_idx].UsagePage, &rpt_desc); + last_usage_page = pp_data->caps[caps_idx].UsagePage; + } + } + // Write "Delimiter Open" + rd_write_short_item(rd_local_delimiter, 1, &rpt_desc); // 1 = open set of aliased usages + } + else if (main_item_list->MainItemType == rd_delimiter_usage) { + if (main_item_list->CollectionIndex != -1) { + // Write aliased collection "Usage" + rd_write_short_item(rd_local_usage, link_collection_nodes[main_item_list->CollectionIndex].LinkUsage, &rpt_desc); + } if (main_item_list->CapsIndex != 0) { + // Write aliased main item range from "Usage Minimum" to "Usage Maximum" + if (pp_data->caps[caps_idx].IsRange) { + rd_write_short_item(rd_local_usage_minimum, pp_data->caps[caps_idx].Range.UsageMin, &rpt_desc); + rd_write_short_item(rd_local_usage_maximum, pp_data->caps[caps_idx].Range.UsageMax, &rpt_desc); + } + else { + // Write single aliased main item "Usage" + rd_write_short_item(rd_local_usage, pp_data->caps[caps_idx].NotRange.Usage, &rpt_desc); + } + } + } + else if (main_item_list->MainItemType == rd_delimiter_close) { + // Write "Delimiter Close" + rd_write_short_item(rd_local_delimiter, 0, &rpt_desc); // 0 = close set of aliased usages + // Inhibit next usage write + inhibit_write_of_usage = TRUE; + } + else if (main_item_list->TypeOfNode == rd_item_node_padding) { + // Padding + // The preparsed data doesn't contain any information about padding. Therefore all undefined gaps + // in the reports are filled with the same style of constant padding. + + // Write "Report Size" with number of padding bits + rd_write_short_item(rd_global_report_size, (main_item_list->LastBit - main_item_list->FirstBit + 1), &rpt_desc); + + // Write "Report Count" for padding always as 1 + rd_write_short_item(rd_global_report_count, 1, &rpt_desc); + + if (rt_idx == HidP_Input) { + // Write "Input" main item - We know it's Constant - We can only guess the other bits, but they don't matter in case of const + rd_write_short_item(rd_main_input, 0x03, &rpt_desc); // Const / Abs + } + else if (rt_idx == HidP_Output) { + // Write "Output" main item - We know it's Constant - We can only guess the other bits, but they don't matter in case of const + rd_write_short_item(rd_main_output, 0x03, &rpt_desc); // Const / Abs + } + else if (rt_idx == HidP_Feature) { + // Write "Feature" main item - We know it's Constant - We can only guess the other bits, but they don't matter in case of const + rd_write_short_item(rd_main_feature, 0x03, &rpt_desc); // Const / Abs + } + report_count = 0; + } + else if (pp_data->caps[caps_idx].IsButtonCap) { + // Button + // (The preparsed data contain different data for 1 bit Button caps, than for parametric Value caps) + + if (last_report_id != pp_data->caps[caps_idx].ReportID) { + // Write "Report ID" if changed + rd_write_short_item(rd_global_report_id, pp_data->caps[caps_idx].ReportID, &rpt_desc); + last_report_id = pp_data->caps[caps_idx].ReportID; + } + + // Write "Usage Page" when changed + if (pp_data->caps[caps_idx].UsagePage != last_usage_page) { + rd_write_short_item(rd_global_usage_page, pp_data->caps[caps_idx].UsagePage, &rpt_desc); + last_usage_page = pp_data->caps[caps_idx].UsagePage; + } + + // Write only local report items for each cap, if ReportCount > 1 + if (pp_data->caps[caps_idx].IsRange) { + report_count += (pp_data->caps[caps_idx].Range.DataIndexMax - pp_data->caps[caps_idx].Range.DataIndexMin); + } + + if (inhibit_write_of_usage) { + // Inhibit only once after Delimiter - Reset flag + inhibit_write_of_usage = FALSE; + } + else { + if (pp_data->caps[caps_idx].IsRange) { + // Write range from "Usage Minimum" to "Usage Maximum" + rd_write_short_item(rd_local_usage_minimum, pp_data->caps[caps_idx].Range.UsageMin, &rpt_desc); + rd_write_short_item(rd_local_usage_maximum, pp_data->caps[caps_idx].Range.UsageMax, &rpt_desc); + } + else { + // Write single "Usage" + rd_write_short_item(rd_local_usage, pp_data->caps[caps_idx].NotRange.Usage, &rpt_desc); + } + } + + if (pp_data->caps[caps_idx].IsDesignatorRange) { + // Write physical descriptor indices range from "Designator Minimum" to "Designator Maximum" + rd_write_short_item(rd_local_designator_minimum, pp_data->caps[caps_idx].Range.DesignatorMin, &rpt_desc); + rd_write_short_item(rd_local_designator_maximum, pp_data->caps[caps_idx].Range.DesignatorMax, &rpt_desc); + } + else if (pp_data->caps[caps_idx].NotRange.DesignatorIndex != 0) { + // Designator set 0 is a special descriptor set (of the HID Physical Descriptor), + // that specifies the number of additional descriptor sets. + // Therefore Designator Index 0 can never be a useful reference for a control and we can inhibit it. + // Write single "Designator Index" + rd_write_short_item(rd_local_designator_index, pp_data->caps[caps_idx].NotRange.DesignatorIndex, &rpt_desc); + } + + if (pp_data->caps[caps_idx].IsStringRange) { + // Write range of indices of the USB string descriptor, from "String Minimum" to "String Maximum" + rd_write_short_item(rd_local_string_minimum, pp_data->caps[caps_idx].Range.StringMin, &rpt_desc); + rd_write_short_item(rd_local_string_maximum, pp_data->caps[caps_idx].Range.StringMax, &rpt_desc); + } + else if (pp_data->caps[caps_idx].NotRange.StringIndex != 0) { + // String Index 0 is a special entry of the USB string descriptor, that contains a list of supported languages, + // therefore Designator Index 0 can never be a useful reference for a control and we can inhibit it. + // Write single "String Index" + rd_write_short_item(rd_local_string, pp_data->caps[caps_idx].NotRange.StringIndex, &rpt_desc); + } + + if ((main_item_list->next != NULL) && + ((int)main_item_list->next->MainItemType == rt_idx) && + (main_item_list->next->TypeOfNode == rd_item_node_cap) && + (pp_data->caps[main_item_list->next->CapsIndex].IsButtonCap) && + (!pp_data->caps[caps_idx].IsRange) && // This node in list is no array + (!pp_data->caps[main_item_list->next->CapsIndex].IsRange) && // Next node in list is no array + (pp_data->caps[main_item_list->next->CapsIndex].UsagePage == pp_data->caps[caps_idx].UsagePage) && + (pp_data->caps[main_item_list->next->CapsIndex].ReportID == pp_data->caps[caps_idx].ReportID) && + (pp_data->caps[main_item_list->next->CapsIndex].BitField == pp_data->caps[caps_idx].BitField) + ) { + if (main_item_list->next->FirstBit != main_item_list->FirstBit) { + // In case of IsMultipleItemsForArray for multiple dedicated usages for a multi-button array, the report count should be incremented + + // Skip global items until any of them changes, than use ReportCount item to write the count of identical report fields + report_count++; + } + } + else { + + if ((pp_data->caps[caps_idx].Button.LogicalMin == 0) && + (pp_data->caps[caps_idx].Button.LogicalMax == 0)) { + // While a HID report descriptor must always contain LogicalMinimum and LogicalMaximum, + // the preparsed data contain both fields set to zero, for the case of simple buttons + // Write "Logical Minimum" set to 0 and "Logical Maximum" set to 1 + rd_write_short_item(rd_global_logical_minimum, 0, &rpt_desc); + rd_write_short_item(rd_global_logical_maximum, 1, &rpt_desc); + } + else { + // Write logical range from "Logical Minimum" to "Logical Maximum" + rd_write_short_item(rd_global_logical_minimum, pp_data->caps[caps_idx].Button.LogicalMin, &rpt_desc); + rd_write_short_item(rd_global_logical_maximum, pp_data->caps[caps_idx].Button.LogicalMax, &rpt_desc); + } + + // Write "Report Size" + rd_write_short_item(rd_global_report_size, pp_data->caps[caps_idx].ReportSize, &rpt_desc); + + // Write "Report Count" + if (!pp_data->caps[caps_idx].IsRange) { + // Variable bit field with one bit per button + // In case of multiple usages with the same items, only "Usage" is written per cap, and "Report Count" is incremented + rd_write_short_item(rd_global_report_count, pp_data->caps[caps_idx].ReportCount + report_count, &rpt_desc); + } + else { + // Button array of "Report Size" x "Report Count + rd_write_short_item(rd_global_report_count, pp_data->caps[caps_idx].ReportCount, &rpt_desc); + } + + + // Buttons have only 1 bit and therefore no physical limits/units -> Set to undefined state + if (last_physical_min != 0) { + // Write "Physical Minimum", but only if changed + last_physical_min = 0; + rd_write_short_item(rd_global_physical_minimum, last_physical_min, &rpt_desc); + } + if (last_physical_max != 0) { + // Write "Physical Maximum", but only if changed + last_physical_max = 0; + rd_write_short_item(rd_global_physical_maximum, last_physical_max, &rpt_desc); + } + if (last_unit_exponent != 0) { + // Write "Unit Exponent", but only if changed + last_unit_exponent = 0; + rd_write_short_item(rd_global_unit_exponent, last_unit_exponent, &rpt_desc); + } + if (last_unit != 0) { + // Write "Unit",but only if changed + last_unit = 0; + rd_write_short_item(rd_global_unit, last_unit, &rpt_desc); + } + + // Write "Input" main item + if (rt_idx == HidP_Input) { + rd_write_short_item(rd_main_input, pp_data->caps[caps_idx].BitField, &rpt_desc); + } + // Write "Output" main item + else if (rt_idx == HidP_Output) { + rd_write_short_item(rd_main_output, pp_data->caps[caps_idx].BitField, &rpt_desc); + } + // Write "Feature" main item + else if (rt_idx == HidP_Feature) { + rd_write_short_item(rd_main_feature, pp_data->caps[caps_idx].BitField, &rpt_desc); + } + report_count = 0; + } + } + else { + + if (last_report_id != pp_data->caps[caps_idx].ReportID) { + // Write "Report ID" if changed + rd_write_short_item(rd_global_report_id, pp_data->caps[caps_idx].ReportID, &rpt_desc); + last_report_id = pp_data->caps[caps_idx].ReportID; + } + + // Write "Usage Page" if changed + if (pp_data->caps[caps_idx].UsagePage != last_usage_page) { + rd_write_short_item(rd_global_usage_page, pp_data->caps[caps_idx].UsagePage, &rpt_desc); + last_usage_page = pp_data->caps[caps_idx].UsagePage; + } + + if (inhibit_write_of_usage) { + // Inhibit only once after Delimiter - Reset flag + inhibit_write_of_usage = FALSE; + } + else { + if (pp_data->caps[caps_idx].IsRange) { + // Write usage range from "Usage Minimum" to "Usage Maximum" + rd_write_short_item(rd_local_usage_minimum, pp_data->caps[caps_idx].Range.UsageMin, &rpt_desc); + rd_write_short_item(rd_local_usage_maximum, pp_data->caps[caps_idx].Range.UsageMax, &rpt_desc); + } + else { + // Write single "Usage" + rd_write_short_item(rd_local_usage, pp_data->caps[caps_idx].NotRange.Usage, &rpt_desc); + } + } + + if (pp_data->caps[caps_idx].IsDesignatorRange) { + // Write physical descriptor indices range from "Designator Minimum" to "Designator Maximum" + rd_write_short_item(rd_local_designator_minimum, pp_data->caps[caps_idx].Range.DesignatorMin, &rpt_desc); + rd_write_short_item(rd_local_designator_maximum, pp_data->caps[caps_idx].Range.DesignatorMax, &rpt_desc); + } + else if (pp_data->caps[caps_idx].NotRange.DesignatorIndex != 0) { + // Designator set 0 is a special descriptor set (of the HID Physical Descriptor), + // that specifies the number of additional descriptor sets. + // Therefore Designator Index 0 can never be a useful reference for a control and we can inhibit it. + // Write single "Designator Index" + rd_write_short_item(rd_local_designator_index, pp_data->caps[caps_idx].NotRange.DesignatorIndex, &rpt_desc); + } + + if (pp_data->caps[caps_idx].IsStringRange) { + // Write range of indices of the USB string descriptor, from "String Minimum" to "String Maximum" + rd_write_short_item(rd_local_string_minimum, pp_data->caps[caps_idx].Range.StringMin, &rpt_desc); + rd_write_short_item(rd_local_string_maximum, pp_data->caps[caps_idx].Range.StringMax, &rpt_desc); + } + else if (pp_data->caps[caps_idx].NotRange.StringIndex != 0) { + // String Index 0 is a special entry of the USB string descriptor, that contains a list of supported languages, + // therefore Designator Index 0 can never be a useful reference for a control and we can inhibit it. + // Write single "String Index" + rd_write_short_item(rd_local_string, pp_data->caps[caps_idx].NotRange.StringIndex, &rpt_desc); + } + + if ((pp_data->caps[caps_idx].BitField & 0x02) != 0x02) { + // In case of an value array overwrite "Report Count" + pp_data->caps[caps_idx].ReportCount = pp_data->caps[caps_idx].Range.DataIndexMax - pp_data->caps[caps_idx].Range.DataIndexMin + 1; + } + + + // Print only local report items for each cap, if ReportCount > 1 + if ((main_item_list->next != NULL) && + ((int) main_item_list->next->MainItemType == rt_idx) && + (main_item_list->next->TypeOfNode == rd_item_node_cap) && + (!pp_data->caps[main_item_list->next->CapsIndex].IsButtonCap) && + (!pp_data->caps[caps_idx].IsRange) && // This node in list is no array + (!pp_data->caps[main_item_list->next->CapsIndex].IsRange) && // Next node in list is no array + (pp_data->caps[main_item_list->next->CapsIndex].UsagePage == pp_data->caps[caps_idx].UsagePage) && + (pp_data->caps[main_item_list->next->CapsIndex].NotButton.LogicalMin == pp_data->caps[caps_idx].NotButton.LogicalMin) && + (pp_data->caps[main_item_list->next->CapsIndex].NotButton.LogicalMax == pp_data->caps[caps_idx].NotButton.LogicalMax) && + (pp_data->caps[main_item_list->next->CapsIndex].NotButton.PhysicalMin == pp_data->caps[caps_idx].NotButton.PhysicalMin) && + (pp_data->caps[main_item_list->next->CapsIndex].NotButton.PhysicalMax == pp_data->caps[caps_idx].NotButton.PhysicalMax) && + (pp_data->caps[main_item_list->next->CapsIndex].UnitsExp == pp_data->caps[caps_idx].UnitsExp) && + (pp_data->caps[main_item_list->next->CapsIndex].Units == pp_data->caps[caps_idx].Units) && + (pp_data->caps[main_item_list->next->CapsIndex].ReportSize == pp_data->caps[caps_idx].ReportSize) && + (pp_data->caps[main_item_list->next->CapsIndex].ReportID == pp_data->caps[caps_idx].ReportID) && + (pp_data->caps[main_item_list->next->CapsIndex].BitField == pp_data->caps[caps_idx].BitField) && + (pp_data->caps[main_item_list->next->CapsIndex].ReportCount == 1) && + (pp_data->caps[caps_idx].ReportCount == 1) + ) { + // Skip global items until any of them changes, than use ReportCount item to write the count of identical report fields + report_count++; + } + else { + // Value + + // Write logical range from "Logical Minimum" to "Logical Maximum" + rd_write_short_item(rd_global_logical_minimum, pp_data->caps[caps_idx].NotButton.LogicalMin, &rpt_desc); + rd_write_short_item(rd_global_logical_maximum, pp_data->caps[caps_idx].NotButton.LogicalMax, &rpt_desc); + + if ((last_physical_min != pp_data->caps[caps_idx].NotButton.PhysicalMin) || + (last_physical_max != pp_data->caps[caps_idx].NotButton.PhysicalMax)) { + // Write range from "Physical Minimum" to " Physical Maximum", but only if one of them changed + rd_write_short_item(rd_global_physical_minimum, pp_data->caps[caps_idx].NotButton.PhysicalMin, &rpt_desc); + last_physical_min = pp_data->caps[caps_idx].NotButton.PhysicalMin; + rd_write_short_item(rd_global_physical_maximum, pp_data->caps[caps_idx].NotButton.PhysicalMax, &rpt_desc); + last_physical_max = pp_data->caps[caps_idx].NotButton.PhysicalMax; + } + + + if (last_unit_exponent != pp_data->caps[caps_idx].UnitsExp) { + // Write "Unit Exponent", but only if changed + rd_write_short_item(rd_global_unit_exponent, pp_data->caps[caps_idx].UnitsExp, &rpt_desc); + last_unit_exponent = pp_data->caps[caps_idx].UnitsExp; + } + + if (last_unit != pp_data->caps[caps_idx].Units) { + // Write physical "Unit", but only if changed + rd_write_short_item(rd_global_unit, pp_data->caps[caps_idx].Units, &rpt_desc); + last_unit = pp_data->caps[caps_idx].Units; + } + + // Write "Report Size" + rd_write_short_item(rd_global_report_size, pp_data->caps[caps_idx].ReportSize, &rpt_desc); + + // Write "Report Count" + rd_write_short_item(rd_global_report_count, pp_data->caps[caps_idx].ReportCount + report_count, &rpt_desc); + + if (rt_idx == HidP_Input) { + // Write "Input" main item + rd_write_short_item(rd_main_input, pp_data->caps[caps_idx].BitField, &rpt_desc); + } + else if (rt_idx == HidP_Output) { + // Write "Output" main item + rd_write_short_item(rd_main_output, pp_data->caps[caps_idx].BitField, &rpt_desc); + } + else if (rt_idx == HidP_Feature) { + // Write "Feature" main item + rd_write_short_item(rd_main_feature, pp_data->caps[caps_idx].BitField, &rpt_desc); + } + report_count = 0; + } + } + + // Go to next item in main_item_list and free the memory of the actual item + struct rd_main_item_node *main_item_list_prev = main_item_list; + main_item_list = main_item_list->next; + free(main_item_list_prev); + } + + // Free multidimensionable array: coll_bit_range[COLLECTION_INDEX][REPORT_ID][INPUT/OUTPUT/FEATURE] + // Free multidimensionable array: coll_child_order[COLLECTION_INDEX][DIRECT_CHILD_INDEX] + for (USHORT collection_node_idx = 0; collection_node_idx < pp_data->NumberLinkCollectionNodes; collection_node_idx++) { + for (int reportid_idx = 0; reportid_idx < 256; reportid_idx++) { + for (HIDP_REPORT_TYPE rt_idx = 0; rt_idx < NUM_OF_HIDP_REPORT_TYPES; rt_idx++) { + free(coll_bit_range[collection_node_idx][reportid_idx][rt_idx]); + } + free(coll_bit_range[collection_node_idx][reportid_idx]); + } + free(coll_bit_range[collection_node_idx]); + if (coll_number_of_direct_childs[collection_node_idx] != 0) free(coll_child_order[collection_node_idx]); + } + free(coll_bit_range); + free(coll_child_order); + + // Free one dimensional arrays + free(coll_begin_lookup); + free(coll_end_lookup); + free(coll_levels); + free(coll_number_of_direct_childs); + + return (int) rpt_desc.byte_idx; +} diff --git a/libs/hidapi/windows/hidapi_descriptor_reconstruct.h b/libs/hidapi/windows/hidapi_descriptor_reconstruct.h new file mode 100644 index 0000000000..4b8ca83fbc --- /dev/null +++ b/libs/hidapi/windows/hidapi_descriptor_reconstruct.h @@ -0,0 +1,247 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2022, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ +#ifndef HIDAPI_DESCRIPTOR_RECONSTRUCT_H__ +#define HIDAPI_DESCRIPTOR_RECONSTRUCT_H__ + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +/* Do not warn about wcsncpy usage. + https://docs.microsoft.com/cpp/c-runtime-library/security-features-in-the-crt */ +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include "hidapi_winapi.h" + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable: 4200) +#pragma warning(disable: 4201) +#pragma warning(disable: 4214) +#endif + +#include + +#include "hidapi_hidsdi.h" +#include + +#define NUM_OF_HIDP_REPORT_TYPES 3 + +typedef enum rd_items_ { + rd_main_input = 0x80, /* 1000 00 nn */ + rd_main_output = 0x90, /* 1001 00 nn */ + rd_main_feature = 0xB0, /* 1011 00 nn */ + rd_main_collection = 0xA0, /* 1010 00 nn */ + rd_main_collection_end = 0xC0, /* 1100 00 nn */ + rd_global_usage_page = 0x04, /* 0000 01 nn */ + rd_global_logical_minimum = 0x14, /* 0001 01 nn */ + rd_global_logical_maximum = 0x24, /* 0010 01 nn */ + rd_global_physical_minimum = 0x34, /* 0011 01 nn */ + rd_global_physical_maximum = 0x44, /* 0100 01 nn */ + rd_global_unit_exponent = 0x54, /* 0101 01 nn */ + rd_global_unit = 0x64, /* 0110 01 nn */ + rd_global_report_size = 0x74, /* 0111 01 nn */ + rd_global_report_id = 0x84, /* 1000 01 nn */ + rd_global_report_count = 0x94, /* 1001 01 nn */ + rd_global_push = 0xA4, /* 1010 01 nn */ + rd_global_pop = 0xB4, /* 1011 01 nn */ + rd_local_usage = 0x08, /* 0000 10 nn */ + rd_local_usage_minimum = 0x18, /* 0001 10 nn */ + rd_local_usage_maximum = 0x28, /* 0010 10 nn */ + rd_local_designator_index = 0x38, /* 0011 10 nn */ + rd_local_designator_minimum = 0x48, /* 0100 10 nn */ + rd_local_designator_maximum = 0x58, /* 0101 10 nn */ + rd_local_string = 0x78, /* 0111 10 nn */ + rd_local_string_minimum = 0x88, /* 1000 10 nn */ + rd_local_string_maximum = 0x98, /* 1001 10 nn */ + rd_local_delimiter = 0xA8 /* 1010 10 nn */ +} rd_items; + +typedef enum rd_main_items_ { + rd_input = HidP_Input, + rd_output = HidP_Output, + rd_feature = HidP_Feature, + rd_collection, + rd_collection_end, + rd_delimiter_open, + rd_delimiter_usage, + rd_delimiter_close, +} rd_main_items; + +typedef struct rd_bit_range_ { + int FirstBit; + int LastBit; +} rd_bit_range; + +typedef enum rd_item_node_type_ { + rd_item_node_cap, + rd_item_node_padding, + rd_item_node_collection, +} rd_node_type; + +struct rd_main_item_node { + int FirstBit; /* Position of first bit in report (counting from 0) */ + int LastBit; /* Position of last bit in report (counting from 0) */ + rd_node_type TypeOfNode; /* Information if caps index refers to the array of button caps, value caps, + or if the node is just a padding element to fill unused bit positions. + The node can also be a collection node without any bits in the report. */ + int CapsIndex; /* Index in the array of caps */ + int CollectionIndex; /* Index in the array of link collections */ + rd_main_items MainItemType; /* Input, Output, Feature, Collection or Collection End */ + unsigned char ReportID; + struct rd_main_item_node* next; +}; + +typedef struct hid_pp_caps_info_ { + USHORT FirstCap; + USHORT NumberOfCaps; // Includes empty caps after LastCap + USHORT LastCap; + USHORT ReportByteLength; +} hid_pp_caps_info, *phid_pp_caps_info; + +typedef struct hid_pp_link_collection_node_ { + USAGE LinkUsage; + USAGE LinkUsagePage; + USHORT Parent; + USHORT NumberOfChildren; + USHORT NextSibling; + USHORT FirstChild; + ULONG CollectionType : 8; + ULONG IsAlias : 1; + ULONG Reserved : 23; + // Same as the public API structure HIDP_LINK_COLLECTION_NODE, but without PVOID UserContext at the end +} hid_pp_link_collection_node, *phid_pp_link_collection_node; + +// Note: This is risk-reduction-measure for this specific struct, as it has ULONG bit-field. +// Although very unlikely, it might still be possible that the compiler creates a memory layout that is +// not binary compatile. +// Other structs are not checked at the time of writing. +static_assert(sizeof(struct hid_pp_link_collection_node_) == 16, + "Size of struct hid_pp_link_collection_node_ not as expected. This might break binary compatibility"); + +typedef struct hidp_unknown_token_ { + UCHAR Token; /* Specifies the one-byte prefix of a global item. */ + UCHAR Reserved[3]; + ULONG BitField; /* Specifies the data part of the global item. */ +} hidp_unknown_token, * phidp_unknown_token; + +typedef struct hid_pp_cap_ { + USAGE UsagePage; + UCHAR ReportID; + UCHAR BitPosition; + USHORT ReportSize; // WIN32 term for this is BitSize + USHORT ReportCount; + USHORT BytePosition; + USHORT BitCount; + ULONG BitField; + USHORT NextBytePosition; + USHORT LinkCollection; + USAGE LinkUsagePage; + USAGE LinkUsage; + + // Start of 8 Flags in one byte + BOOLEAN IsMultipleItemsForArray:1; + + BOOLEAN IsPadding:1; + BOOLEAN IsButtonCap:1; + BOOLEAN IsAbsolute:1; + BOOLEAN IsRange:1; + BOOLEAN IsAlias:1; // IsAlias is set to TRUE in the first n-1 capability structures added to the capability array. IsAlias set to FALSE in the nth capability structure. + BOOLEAN IsStringRange:1; + BOOLEAN IsDesignatorRange:1; + // End of 8 Flags in one byte + BOOLEAN Reserved1[3]; + + hidp_unknown_token UnknownTokens[4]; // 4 x 8 Byte + + union { + struct { + USAGE UsageMin; + USAGE UsageMax; + USHORT StringMin; + USHORT StringMax; + USHORT DesignatorMin; + USHORT DesignatorMax; + USHORT DataIndexMin; + USHORT DataIndexMax; + } Range; + struct { + USAGE Usage; + USAGE Reserved1; + USHORT StringIndex; + USHORT Reserved2; + USHORT DesignatorIndex; + USHORT Reserved3; + USHORT DataIndex; + USHORT Reserved4; + } NotRange; + }; + union { + struct { + LONG LogicalMin; + LONG LogicalMax; + } Button; + struct { + BOOLEAN HasNull; + UCHAR Reserved4[3]; + LONG LogicalMin; + LONG LogicalMax; + LONG PhysicalMin; + LONG PhysicalMax; + } NotButton; + }; + ULONG Units; + ULONG UnitsExp; + +} hid_pp_cap, *phid_pp_cap; + +typedef struct hidp_preparsed_data_ { + UCHAR MagicKey[8]; + USAGE Usage; + USAGE UsagePage; + USHORT Reserved[2]; + + // CAPS structure for Input, Output and Feature + hid_pp_caps_info caps_info[3]; + + USHORT FirstByteOfLinkCollectionArray; + USHORT NumberLinkCollectionNodes; + +#ifndef _MSC_VER + // MINGW fails with: Flexible array member in union not supported + // Solution: https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html + union { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" + hid_pp_cap caps[0]; + hid_pp_link_collection_node LinkCollectionArray[0]; +#pragma GCC diagnostic pop + }; +#else + union { + hid_pp_cap caps[]; + hid_pp_link_collection_node LinkCollectionArray[]; + }; +#endif + +} hidp_preparsed_data; + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +#endif diff --git a/libs/hidapi/windows/hidapi_hidclass.h b/libs/hidapi/windows/hidapi_hidclass.h new file mode 100644 index 0000000000..13bd6f22bd --- /dev/null +++ b/libs/hidapi/windows/hidapi_hidclass.h @@ -0,0 +1,38 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2022, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ + +#ifndef HIDAPI_HIDCLASS_H +#define HIDAPI_HIDCLASS_H + +#ifdef HIDAPI_USE_DDK + +#include + +#else + +/* This part of the header mimics hidclass.h, + but only what is used by HIDAPI */ + +#define HID_OUT_CTL_CODE(id) CTL_CODE(FILE_DEVICE_KEYBOARD, (id), METHOD_OUT_DIRECT, FILE_ANY_ACCESS) +#define IOCTL_HID_GET_FEATURE HID_OUT_CTL_CODE(100) +#define IOCTL_HID_GET_INPUT_REPORT HID_OUT_CTL_CODE(104) + +#endif + +#endif /* HIDAPI_HIDCLASS_H */ diff --git a/libs/hidapi/windows/hidapi_hidpi.h b/libs/hidapi/windows/hidapi_hidpi.h new file mode 100644 index 0000000000..75a5812c94 --- /dev/null +++ b/libs/hidapi/windows/hidapi_hidpi.h @@ -0,0 +1,72 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2022, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ + +#ifndef HIDAPI_HIDPI_H +#define HIDAPI_HIDPI_H + +#ifdef HIDAPI_USE_DDK + +#include + +#else + +/* This part of the header mimics hidpi.h, + but only what is used by HIDAPI */ + +typedef enum _HIDP_REPORT_TYPE +{ + HidP_Input, + HidP_Output, + HidP_Feature +} HIDP_REPORT_TYPE; + +typedef struct _HIDP_PREPARSED_DATA * PHIDP_PREPARSED_DATA; + +typedef struct _HIDP_CAPS +{ + USAGE Usage; + USAGE UsagePage; + USHORT InputReportByteLength; + USHORT OutputReportByteLength; + USHORT FeatureReportByteLength; + USHORT Reserved[17]; + + USHORT NumberLinkCollectionNodes; + + USHORT NumberInputButtonCaps; + USHORT NumberInputValueCaps; + USHORT NumberInputDataIndices; + + USHORT NumberOutputButtonCaps; + USHORT NumberOutputValueCaps; + USHORT NumberOutputDataIndices; + + USHORT NumberFeatureButtonCaps; + USHORT NumberFeatureValueCaps; + USHORT NumberFeatureDataIndices; +} HIDP_CAPS, *PHIDP_CAPS; + +#define HIDP_STATUS_SUCCESS 0x00110000 +#define HIDP_STATUS_INVALID_PREPARSED_DATA 0xc0110001 + +typedef NTSTATUS (__stdcall *HidP_GetCaps_)(PHIDP_PREPARSED_DATA preparsed_data, PHIDP_CAPS caps); + +#endif + +#endif /* HIDAPI_HIDPI_H */ diff --git a/libs/hidapi/windows/hidapi_hidsdi.h b/libs/hidapi/windows/hidapi_hidsdi.h new file mode 100644 index 0000000000..ffed5b2f88 --- /dev/null +++ b/libs/hidapi/windows/hidapi_hidsdi.h @@ -0,0 +1,59 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2022, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ + +#ifndef HIDAPI_HIDSDI_H +#define HIDAPI_HIDSDI_H + +#ifdef HIDAPI_USE_DDK + +#include + +#else + +/* This part of the header mimics hidsdi.h, + but only what is used by HIDAPI */ + +typedef USHORT USAGE; + +#include "hidapi_hidpi.h" + +typedef struct _HIDD_ATTRIBUTES{ + ULONG Size; + USHORT VendorID; + USHORT ProductID; + USHORT VersionNumber; +} HIDD_ATTRIBUTES, *PHIDD_ATTRIBUTES; + +typedef void (__stdcall *HidD_GetHidGuid_)(LPGUID hid_guid); +typedef BOOLEAN (__stdcall *HidD_GetAttributes_)(HANDLE device, PHIDD_ATTRIBUTES attrib); +typedef BOOLEAN (__stdcall *HidD_GetSerialNumberString_)(HANDLE device, PVOID buffer, ULONG buffer_len); +typedef BOOLEAN (__stdcall *HidD_GetManufacturerString_)(HANDLE handle, PVOID buffer, ULONG buffer_len); +typedef BOOLEAN (__stdcall *HidD_GetProductString_)(HANDLE handle, PVOID buffer, ULONG buffer_len); +typedef BOOLEAN (__stdcall *HidD_SetFeature_)(HANDLE handle, PVOID data, ULONG length); +typedef BOOLEAN (__stdcall *HidD_GetFeature_)(HANDLE handle, PVOID data, ULONG length); +typedef BOOLEAN (__stdcall* HidD_SetOutputReport_)(HANDLE handle, PVOID data, ULONG length); +typedef BOOLEAN (__stdcall *HidD_GetInputReport_)(HANDLE handle, PVOID data, ULONG length); +typedef BOOLEAN (__stdcall *HidD_GetIndexedString_)(HANDLE handle, ULONG string_index, PVOID buffer, ULONG buffer_len); +typedef BOOLEAN (__stdcall *HidD_GetPreparsedData_)(HANDLE handle, PHIDP_PREPARSED_DATA *preparsed_data); +typedef BOOLEAN (__stdcall *HidD_FreePreparsedData_)(PHIDP_PREPARSED_DATA preparsed_data); +typedef BOOLEAN (__stdcall *HidD_SetNumInputBuffers_)(HANDLE handle, ULONG number_buffers); + +#endif + +#endif /* HIDAPI_HIDSDI_H */ diff --git a/libs/hidapi/windows/hidapi_winapi.h b/libs/hidapi/windows/hidapi_winapi.h new file mode 100644 index 0000000000..a9919923c7 --- /dev/null +++ b/libs/hidapi/windows/hidapi_winapi.h @@ -0,0 +1,74 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2022, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ + +/** @file + * @defgroup API hidapi API + * + * Since version 0.12.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 12, 0) + */ + +#ifndef HIDAPI_WINAPI_H__ +#define HIDAPI_WINAPI_H__ + +#include + +#include + +#include "hidapi.h" + +#ifdef __cplusplus +extern "C" { +#endif + + /** @brief Get the container ID for a HID device. + + Since version 0.12.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 12, 0) + + This function returns the `DEVPKEY_Device_ContainerId` property of + the given device. This can be used to correlate different + interfaces/ports on the same hardware device. + + @ingroup API + @param dev A device handle returned from hid_open(). + @param container_id The device's container ID on return. + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT_CALL hid_winapi_get_container_id(hid_device *dev, GUID *container_id); + + /** + * @brief Reconstructs a HID Report Descriptor from a Win32 HIDP_PREPARSED_DATA structure. + * This reconstructed report descriptor is logical identical to the real report descriptor, + * but not byte wise identical. + * + * @param[in] hidp_preparsed_data Pointer to the HIDP_PREPARSED_DATA to read, i.e.: the value of PHIDP_PREPARSED_DATA, + * as returned by HidD_GetPreparsedData WinAPI function. + * @param buf Pointer to the buffer where the report descriptor should be stored. + * @param[in] buf_size Size of the buffer. The recommended size for the buffer is @ref HID_API_MAX_REPORT_DESCRIPTOR_SIZE bytes. + * + * @return Returns size of reconstructed report descriptor if successful, -1 for error. + */ + int HID_API_EXPORT_CALL hid_winapi_descriptor_reconstruct_pp_data(void *hidp_preparsed_data, unsigned char *buf, size_t buf_size); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/libs/hidapi/wscript b/libs/hidapi/wscript index cf04057d9b..70ea8b158c 100644 --- a/libs/hidapi/wscript +++ b/libs/hidapi/wscript @@ -50,6 +50,7 @@ def build(bld): obj.source = 'mac/hid.c' obj.framework = [ 'IOKit', 'CoreFoundation' ] else: + # with '-strict' this needs "-std=gnu99" to compile w/o warnings obj.source = 'linux/hid.c' if re.search ("linux", sys.platform) != None: obj.uselib = 'UDEV' diff --git a/libs/panners/vbap/vbap_speakers.h b/libs/panners/vbap/vbap_speakers.h index 7af8f49741..b30ba5f76d 100644 --- a/libs/panners/vbap/vbap_speakers.h +++ b/libs/panners/vbap/vbap_speakers.h @@ -22,8 +22,6 @@ #include #include -#include - #include #include "ardour/panner.h" diff --git a/libs/pbd/boost-debug/shared_ptr.hpp b/libs/pbd/boost-debug/shared_ptr.hpp deleted file mode 100644 index f64b582d99..0000000000 --- a/libs/pbd/boost-debug/shared_ptr.hpp +++ /dev/null @@ -1,491 +0,0 @@ -#define DEBUG_SHARED_PTR -#ifndef DEBUG_SHARED_PTR - -#include - -#else - -#include "pbd/stacktrace.h" - -#ifndef BOOST_SHARED_PTR_HPP_INCLUDED -#define BOOST_SHARED_PTR_HPP_INCLUDED - -// -// shared_ptr.hpp -// -// (C) Copyright Greg Colvin and Beman Dawes 1998, 1999. -// Copyright (c) 2001, 2002, 2003 Peter Dimov -// -// Distributed under the Boost Software License, Version 1.0. (See -// accompanying file LICENSE_1_0.txt or copy at -// http://www.boost.org/LICENSE_1_0.txt) -// -// See http://www.boost.org/libs/smart_ptr/shared_ptr.htm for documentation. -// - -#include "pbd/stacktrace.h" - -#include // for broken compiler workarounds - -#if defined(BOOST_NO_MEMBER_TEMPLATES) && !defined(BOOST_MSVC6_MEMBER_TEMPLATES) -#include -#else - -#include -#include -#include -#include -#include - -#include // for std::auto_ptr -#include // for std::swap -#include // for std::less -#include // for std::bad_cast -#include // for std::basic_ostream - -#ifdef BOOST_MSVC // moved here to work around VC++ compiler crash -# pragma warning(push) -# pragma warning(disable:4284) // odd return type for operator-> -#endif - -namespace boost -{ - -template class weak_ptr; -template class enable_shared_from_this; - -namespace detail -{ - -struct static_cast_tag {}; -struct const_cast_tag {}; -struct dynamic_cast_tag {}; -struct polymorphic_cast_tag {}; - -template struct shared_ptr_traits -{ - typedef T & reference; -}; - -template<> struct shared_ptr_traits -{ - typedef void reference; -}; - -#if !defined(BOOST_NO_CV_VOID_SPECIALIZATIONS) - -template<> struct shared_ptr_traits -{ - typedef void reference; -}; - -template<> struct shared_ptr_traits -{ - typedef void reference; -}; - -template<> struct shared_ptr_traits -{ - typedef void reference; -}; - -#endif - -// enable_shared_from_this support - -template void sp_enable_shared_from_this( shared_count const & pn, boost::enable_shared_from_this const * pe, Y const * px ) -{ - if(pe != 0) pe->_internal_weak_this._internal_assign(const_cast(px), pn); -} - -inline void sp_enable_shared_from_this( shared_count const & /*pn*/, ... ) -{ -} - -} // namespace detail - - -// -// shared_ptr -// -// An enhanced relative of scoped_ptr with reference counted copy semantics. -// The object pointed to is deleted when the last shared_ptr pointing to it -// is destroyed or reset. -// - -template class shared_ptr -{ -private: - - // Borland 5.5.1 specific workaround - typedef shared_ptr this_type; - -public: - - typedef T element_type; - typedef T value_type; - typedef T * pointer; - typedef typename detail::shared_ptr_traits::reference reference; - - shared_ptr(): px(0), pn() // never throws in 1.30+ - { - } - - template - explicit shared_ptr( Y * p ): px( p ), pn( p ) // Y must be complete - { - detail::sp_enable_shared_from_this( pn, p, p ); - } - - // - // Requirements: D's copy constructor must not throw - // - // shared_ptr will release p by calling d(p) - // - - template shared_ptr(Y * p, D d): px(p), pn(p, d) - { - detail::sp_enable_shared_from_this( pn, p, p ); - } - -// generated copy constructor, assignment, destructor are fine... - -// except that Borland C++ has a bug, and g++ with -Wsynth warns -#if defined(__BORLANDC__) || defined(__GNUC__) - - shared_ptr & operator=(shared_ptr const & r) // never throws - { - px = r.px; - pn = r.pn; // shared_count::op= doesn't throw - return *this; - } - -#endif - - template - explicit shared_ptr(weak_ptr const & r): pn(r.pn) // may throw - { - // it is now safe to copy r.px, as pn(r.pn) did not throw - px = r.px; - } - - template - shared_ptr(shared_ptr const & r): px(r.px), pn(r.pn) // never throws - { - } - - template - shared_ptr(shared_ptr const & r, detail::static_cast_tag): px(static_cast(r.px)), pn(r.pn) - { - } - - template - shared_ptr(shared_ptr const & r, detail::const_cast_tag): px(const_cast(r.px)), pn(r.pn) - { - } - - template - shared_ptr(shared_ptr const & r, detail::dynamic_cast_tag): px(dynamic_cast(r.px)), pn(r.pn) - { - if(px == 0) // need to allocate new counter -- the cast failed - { - pn = detail::shared_count(); - } - } - - template - shared_ptr(shared_ptr const & r, detail::polymorphic_cast_tag): px(dynamic_cast(r.px)), pn(r.pn) - { - if(px == 0) - { - boost::throw_exception(std::bad_cast()); - } - } - -#ifndef BOOST_NO_AUTO_PTR - - template - explicit shared_ptr(std::auto_ptr & r): px(r.get()), pn() - { - Y * tmp = r.get(); - pn = detail::shared_count(r); - detail::sp_enable_shared_from_this( pn, tmp, tmp ); - } - -#endif - -#if !defined(BOOST_MSVC) || (BOOST_MSVC > 1200) - - template - shared_ptr & operator=(shared_ptr const & r) // never throws - { - px = r.px; - pn = r.pn; // shared_count::op= doesn't throw - return *this; - } - -#endif - -#ifndef BOOST_NO_AUTO_PTR - - template - shared_ptr & operator=(std::auto_ptr & r) - { - this_type(r).swap(*this); - return *this; - } - -#endif - - void reset() // never throws in 1.30+ - { - this_type().swap(*this); - } - - template void reset(Y * p) // Y must be complete - { - BOOST_ASSERT(p == 0 || p != px); // catch self-reset errors - this_type(p).swap(*this); - } - - template void reset(Y * p, D d) - { - this_type(p, d).swap(*this); - } - - reference operator* () const // never throws - { - BOOST_ASSERT(px != 0); - return *px; - } - - T * operator-> () const // never throws - { - BOOST_ASSERT(px != 0); - return px; - } - - T * get() const // never throws - { - return px; - } - - // implicit conversion to "bool" - -#if defined(__SUNPRO_CC) && BOOST_WORKAROUND(__SUNPRO_CC, <= 0x530) - - operator bool () const - { - return px != 0; - } - -#elif \ - ( defined(__MWERKS__) && BOOST_WORKAROUND(__MWERKS__, < 0x3200) ) || \ - ( defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__ < 304) ) - - typedef T * (this_type::*unspecified_bool_type)() const; - - operator unspecified_bool_type() const // never throws - { - return px == 0? 0: &this_type::get; - } - -#else - - typedef T * this_type::*unspecified_bool_type; - - operator unspecified_bool_type() const // never throws - { - return px == 0? 0: &this_type::px; - } - -#endif - - // operator! is redundant, but some compilers need it - - bool operator! () const // never throws - { - return px == 0; - } - - bool unique() const // never throws - { - return pn.unique(); - } - - long use_count() const // never throws - { - return pn.use_count(); - } - - void swap(shared_ptr & other) // never throws - { - std::swap(px, other.px); - pn.swap(other.pn); - } - - template bool _internal_less(shared_ptr const & rhs) const - { - return pn < rhs.pn; - } - - void * _internal_get_deleter(std::type_info const & ti) const - { - return pn.get_deleter(ti); - } - -// Tasteless as this may seem, making all members public allows member templates -// to work in the absence of member template friends. (Matthew Langston) - -#ifndef BOOST_NO_MEMBER_TEMPLATE_FRIENDS - -private: - - template friend class shared_ptr; - template friend class weak_ptr; - -#endif - - T * px; // contained pointer - detail::shared_count pn; // reference counter - typename PBD::thing_with_backtrace bt; // backtrace - -}; // shared_ptr - -template inline bool operator==(shared_ptr const & a, shared_ptr const & b) -{ - return a.get() == b.get(); -} - -template inline bool operator!=(shared_ptr const & a, shared_ptr const & b) -{ - return a.get() != b.get(); -} - -#if __GNUC__ == 2 && __GNUC_MINOR__ <= 96 - -// Resolve the ambiguity between our op!= and the one in rel_ops - -template inline bool operator!=(shared_ptr const & a, shared_ptr const & b) -{ - return a.get() != b.get(); -} - -#endif - -template inline bool operator<(shared_ptr const & a, shared_ptr const & b) -{ - return a._internal_less(b); -} - -template inline void swap(shared_ptr & a, shared_ptr & b) -{ - a.swap(b); -} - -template shared_ptr static_pointer_cast(shared_ptr const & r) -{ - return shared_ptr(r, detail::static_cast_tag()); -} - -template shared_ptr const_pointer_cast(shared_ptr const & r) -{ - return shared_ptr(r, detail::const_cast_tag()); -} - -template shared_ptr dynamic_pointer_cast(shared_ptr const & r) -{ - return shared_ptr(r, detail::dynamic_cast_tag()); -} - -// shared_*_cast names are deprecated. Use *_pointer_cast instead. - -template shared_ptr shared_static_cast(shared_ptr const & r) -{ - return shared_ptr(r, detail::static_cast_tag()); -} - -template shared_ptr shared_dynamic_cast(shared_ptr const & r) -{ - return shared_ptr(r, detail::dynamic_cast_tag()); -} - -template shared_ptr shared_polymorphic_cast(shared_ptr const & r) -{ - return shared_ptr(r, detail::polymorphic_cast_tag()); -} - -template shared_ptr shared_polymorphic_downcast(shared_ptr const & r) -{ - BOOST_ASSERT(dynamic_cast(r.get()) == r.get()); - return shared_static_cast(r); -} - -// get_pointer() enables boost::mem_fn to recognize shared_ptr - -template inline T * get_pointer(shared_ptr const & p) -{ - return p.get(); -} - -// operator<< - -#if defined(__GNUC__) && (__GNUC__ < 3) - -template std::ostream & operator<< (std::ostream & os, shared_ptr const & p) -{ - os << p.get(); - return os; -} - -#else - -# if defined(BOOST_MSVC) && BOOST_WORKAROUND(BOOST_MSVC, <= 1200 && __SGI_STL_PORT) -// MSVC6 has problems finding std::basic_ostream through the using declaration in namespace _STL -using std::basic_ostream; -template basic_ostream & operator<< (basic_ostream & os, shared_ptr const & p) -# else -template std::basic_ostream & operator<< (std::basic_ostream & os, shared_ptr const & p) -# endif -{ - os << p.get(); - return os; -} - -#endif - -// get_deleter (experimental) - -#if ( defined(__GNUC__) && BOOST_WORKAROUND(__GNUC__, < 3) ) || \ - ( defined(__EDG_VERSION__) && BOOST_WORKAROUND(__EDG_VERSION__, <= 238) ) || \ - ( defined(__HP_aCC) && BOOST_WORKAROUND(__HP_aCC, <= 33500) ) - -// g++ 2.9x doesn't allow static_cast(void *) -// apparently EDG 2.38 and HP aCC A.03.35 also don't accept it - -template D * get_deleter(shared_ptr const & p) -{ - void const * q = p._internal_get_deleter(typeid(D)); - return const_cast(static_cast(q)); -} - -#else - -template D * get_deleter(shared_ptr const & p) -{ - return static_cast(p._internal_get_deleter(typeid(D))); -} - -#endif - -} // namespace boost - -#ifdef BOOST_MSVC -# pragma warning(pop) -#endif - -#endif // #if defined(BOOST_NO_MEMBER_TEMPLATES) && !defined(BOOST_MSVC6_MEMBER_TEMPLATES) - -#endif // #ifndef BOOST_SHARED_PTR_HPP_INCLUDED - -#endif // #ifndef DEBUG_SHARED_PTR diff --git a/libs/pbd/boost_debug.cc b/libs/pbd/boost_debug.cc index b60a7b7336..c9a9f58a30 100644 --- a/libs/pbd/boost_debug.cc +++ b/libs/pbd/boost_debug.cc @@ -29,7 +29,6 @@ #include #include #include -#include #include "pbd/stacktrace.h" #include "pbd/boost_debug.h" diff --git a/libs/pbd/pbd/stl_functors.h b/libs/pbd/pbd/stl_functors.h deleted file mode 100644 index 306c0376f4..0000000000 --- a/libs/pbd/pbd/stl_functors.h +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 1998-2015 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., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef __stl_functors_h__ -#define __stl_functors_h__ - -#include - -#include "pbd/libpbd_visibility.h" - -#ifndef LESS_STRING_P -struct LIBPBD_API less { - bool operator()(std::string *s1, std::string *s2) const { - return *s1 < *s2; - } -}; -#define LESS_STRING_P -#endif // LESS_STRING_P - -#ifndef LESS_CONST_STRING_P -struct LIBPBD_API less { - bool operator()(const std::string *s1, const std::string *s2) const { - return *s1 < *s2; - } -}; -#define LESS_CONST_STRING_P -#endif // LESS_CONST_STRING_P - -#ifndef LESS_CONST_CHAR_P -struct LIBPBD_API less -{ - bool operator()(const char* s1, const char* s2) const { - return strcmp(s1, s2) < 0; - } -}; -#define LESS_CONST_CHAR_P -#endif // LESS_CONST_CHAR_P - -#ifndef LESS_CONST_FLOAT_P -struct LIBPBD_API less -{ - bool operator()(const float *n1, const float *n2) const { - return *n1 < *n2; - } -}; -#define LESS_CONST_FLOAT_P -#endif // LESS_CONST_FLOAT_P - -#ifndef EQUAL_TO_CONST_CHAR_P -struct LIBPBD_API equal_to -{ - bool operator()(const char *s1, const char *s2) const { - return strcmp (s1, s2) == 0; - } -}; -#define EQUAL_TO_CONST_CHAR_P -#endif // EQUAL_TO_CONST_CHAR_P - -#ifndef EQUAL_TO_STRING_P -struct LIBPBD_API equal_to -{ - bool operator()(const std::string *s1, const std::string *s2) const { - return *s1 == *s2; - } -}; -#define EQUAL_TO_STRING_P -#endif // EQUAL_TO_STRING_P - -#ifndef LESS_CONST_STRING_R -struct LIBPBD_API less { - bool operator() (const std::string &s1, const std::string &s2) { - return s1 < s2; - } -}; -#define LESS_CONST_STRING_R -#endif // EQUAL_TO_STRING_P - -#endif // __stl_functors_h__ diff --git a/libs/surfaces/console1/c1_plugin_operations.cc b/libs/surfaces/console1/c1_plugin_operations.cc index fd441ded37..dfa8971464 100644 --- a/libs/surfaces/console1/c1_plugin_operations.cc +++ b/libs/surfaces/console1/c1_plugin_operations.cc @@ -20,8 +20,6 @@ #include #include -#include - #include "glib-2.0/gio/gio.h" #include "glib-2.0/glib/gstdio.h" #include "glibmm-2.4/glibmm/main.h" diff --git a/libs/surfaces/launchkey_4/gui.cc b/libs/surfaces/launchkey_4/gui.cc new file mode 100644 index 0000000000..bd87dd6800 --- /dev/null +++ b/libs/surfaces/launchkey_4/gui.cc @@ -0,0 +1,280 @@ +/* + * 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include +#include +#include + +#include "pbd/unwind.h" +#include "pbd/strsplit.h" +#include "pbd/file_utils.h" + +#include "gtkmm2ext/bindings.h" +#include "gtkmm2ext/gui_thread.h" +#include "gtkmm2ext/utils.h" + +#include "ardour/audioengine.h" +#include "ardour/filesystem_paths.h" +#include "ardour/parameter_descriptor.h" + +#include "launchkey_4.h" +#include "gui.h" + +#include "pbd/i18n.h" + +#ifdef LAUNCHPAD_MINI +#define LAUNCHPAD_NAMESPACE LP_MINI +#else +#define LAUNCHPAD_NAMESPACE LP_X +#endif + +using namespace PBD; +using namespace ARDOUR; +using namespace ArdourSurface; +using namespace ArdourSurface::LAUNCHPAD_NAMESPACE; +using namespace Gtk; +using namespace Gtkmm2ext; + +void* +LaunchKey4::get_gui () const +{ + if (!_gui) { + const_cast(this)->build_gui (); + } + + static_cast(_gui)->show_all(); + return _gui; +} + +void +LaunchKey4::tear_down_gui () +{ + if (_gui) { + Gtk::Widget *w = static_cast(_gui)->get_parent(); + if (w) { + w->hide(); + delete w; + } + } + delete _gui; + _gui = 0; +} + +void +LaunchKey4::build_gui () +{ + _gui = new LK4_GUI (*this); +} + +/*--------------------*/ + +LK4_GUI::LK4_GUI (LaunchKey4& p) + : _lp (p) + , _table (2, 5) + , _action_table (5, 4) + , _ignore_active_change (false) +{ + set_border_width (12); + + _table.set_row_spacings (4); + _table.set_col_spacings (6); + _table.set_border_width (12); + _table.set_homogeneous (false); + + std::string data_file_path; + std::string name = "lkmk4.png"; + + Searchpath spath(ARDOUR::ardour_data_search_path()); + spath.add_subdirectory_to_paths ("icons"); + find_file (spath, name, data_file_path); + if (!data_file_path.empty()) { + _image.set (data_file_path); + _hpacker.pack_start (_image, false, false); + } + + Gtk::Label* l; + int row = 0; + + _input_combo.pack_start (_midi_port_columns.short_name); + _output_combo.pack_start (_midi_port_columns.short_name); + + _input_combo.signal_changed().connect (sigc::bind (sigc::mem_fun (*this, &LK4_GUI::active_port_changed), &_input_combo, true)); + _output_combo.signal_changed().connect (sigc::bind (sigc::mem_fun (*this, &LK4_GUI::active_port_changed), &_output_combo, false)); + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Incoming MIDI on:"))); + l->set_alignment (1.0, 0.5); + _table.attach (*l, 0, 1, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions(0)); + _table.attach (_input_combo, 1, 2, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions(0), 0, 0); + row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Outgoing MIDI on:"))); + l->set_alignment (1.0, 0.5); + _table.attach (*l, 0, 1, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions(0)); + _table.attach (_output_combo, 1, 2, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions(0), 0, 0); + row++; + + _hpacker.pack_start (_table, true, true); + + set_spacing (12); + + pack_start (_hpacker, false, false); + + /* update the port connection combos */ + + update_port_combos (); + + /* catch future changes to connection state */ + + ARDOUR::AudioEngine::instance()->PortRegisteredOrUnregistered.connect (_port_connections, invalidator (*this), boost::bind (&LK4_GUI::connection_handler, this), gui_context()); + ARDOUR::AudioEngine::instance()->PortPrettyNameChanged.connect (_port_connections, invalidator (*this), boost::bind (&LK4_GUI::connection_handler, this), gui_context()); + _lp.ConnectionChange.connect (_port_connections, invalidator (*this), boost::bind (&LK4_GUI::connection_handler, this), gui_context()); +} + +LK4_GUI::~LK4_GUI () +{ +} + +void +LK4_GUI::connection_handler () +{ + /* ignore all changes to combobox active strings here, because we're + updating them to match a new ("external") reality - we were called + because port connections have changed. + */ + + PBD::Unwinder ici (_ignore_active_change, true); + + update_port_combos (); +} + +void +LK4_GUI::update_port_combos () +{ + std::vector midi_inputs; + std::vector midi_outputs; + + if (!_lp.input_port() || !_lp.output_port()) { + return; + } + + ARDOUR::AudioEngine::instance()->get_ports ("", ARDOUR::DataType::MIDI, ARDOUR::PortFlags (ARDOUR::IsOutput|ARDOUR::IsTerminal), midi_inputs); + ARDOUR::AudioEngine::instance()->get_ports ("", ARDOUR::DataType::MIDI, ARDOUR::PortFlags (ARDOUR::IsInput|ARDOUR::IsTerminal), midi_outputs); + + Glib::RefPtr input = build_midi_port_list (midi_inputs, true); + Glib::RefPtr output = build_midi_port_list (midi_outputs, false); + bool input_found = false; + bool output_found = false; + int n; + + _input_combo.set_model (input); + _output_combo.set_model (output); + + Gtk::TreeModel::Children children = input->children(); + Gtk::TreeModel::Children::iterator i; + i = children.begin(); + ++i; /* skip "Disconnected" */ + + + for (n = 1; i != children.end(); ++i, ++n) { + std::string port_name = (*i)[_midi_port_columns.full_name]; + if (_lp.input_port()->connected_to (port_name)) { + _input_combo.set_active (n); + input_found = true; + break; + } + } + + if (!input_found) { + _input_combo.set_active (0); /* disconnected */ + } + + children = output->children(); + i = children.begin(); + ++i; /* skip "Disconnected" */ + + for (n = 1; i != children.end(); ++i, ++n) { + std::string port_name = (*i)[_midi_port_columns.full_name]; + if (_lp.output_port()->connected_to (port_name)) { + _output_combo.set_active (n); + output_found = true; + break; + } + } + + if (!output_found) { + _output_combo.set_active (0); /* disconnected */ + } +} + +Glib::RefPtr +LK4_GUI::build_midi_port_list (std::vector const & ports, bool for_input) +{ + Glib::RefPtr store = ListStore::create (_midi_port_columns); + TreeModel::Row row; + + row = *store->append (); + row[_midi_port_columns.full_name] = std::string(); + row[_midi_port_columns.short_name] = _("Disconnected"); + + for (std::vector::const_iterator p = ports.begin(); p != ports.end(); ++p) { + row = *store->append (); + row[_midi_port_columns.full_name] = *p; + std::string pn = ARDOUR::AudioEngine::instance()->get_pretty_name_by_name (*p); + if (pn.empty ()) { + pn = (*p).substr ((*p).find (':') + 1); + } + row[_midi_port_columns.short_name] = pn; + } + + return store; +} + +void +LK4_GUI::active_port_changed (Gtk::ComboBox* combo, bool for_input) +{ + if (_ignore_active_change) { + return; + } + + TreeModel::iterator active = combo->get_active (); + std::string new_port = (*active)[_midi_port_columns.full_name]; + + if (new_port.empty()) { + if (for_input) { + _lp.input_port()->disconnect_all (); + } else { + _lp.output_port()->disconnect_all (); + } + + return; + } + + if (for_input) { + if (!_lp.input_port()->connected_to (new_port)) { + _lp.input_port()->disconnect_all (); + _lp.input_port()->connect (new_port); + } + } else { + if (!_lp.output_port()->connected_to (new_port)) { + _lp.output_port()->disconnect_all (); + _lp.output_port()->connect (new_port); + } + } +} diff --git a/libs/surfaces/launchkey_4/gui.h b/libs/surfaces/launchkey_4/gui.h new file mode 100644 index 0000000000..e88b2451b3 --- /dev/null +++ b/libs/surfaces/launchkey_4/gui.h @@ -0,0 +1,101 @@ +/* + * 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __ardour_lpx_gui_h__ +#define __ardour_lpx_gui_h__ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Gtk { + class ListStore; +} + +#include "ardour/mode.h" + +#include "launchkey_4.h" + +namespace ArdourSurface { namespace LAUNCHPAD_NAMESPACE { + +class LK4_GUI : public Gtk::VBox +{ +public: + LK4_GUI (LaunchKey4&); + ~LK4_GUI (); + +private: + LaunchKey4& _lp; + Gtk::HBox _hpacker; + Gtk::Table _table; + Gtk::Table _action_table; + Gtk::ComboBox _input_combo; + Gtk::ComboBox _output_combo; + Gtk::Image _image; + + void update_port_combos (); + void connection_handler (); + + PBD::ScopedConnectionList _port_connections; + + struct MidiPortColumns : public Gtk::TreeModel::ColumnRecord { + MidiPortColumns() { + add (short_name); + add (full_name); + } + Gtk::TreeModelColumn short_name; + Gtk::TreeModelColumn full_name; + }; + + MidiPortColumns _midi_port_columns; + bool _ignore_active_change; + + Glib::RefPtr build_midi_port_list (std::vector const & ports, bool for_input); + + void active_port_changed (Gtk::ComboBox*,bool for_input); + +#if 0 + struct PressureModeColumns : public Gtk::TreeModel::ColumnRecord { + PressureModeColumns() { + add (mode); + add (name); + } + Gtk::TreeModelColumn mode; + Gtk::TreeModelColumn name; + }; + + PressureModeColumns _pressure_mode_columns; + Glib::RefPtr build_pressure_mode_columns (); + Gtk::ComboBox _pressure_mode_selector; + Gtk::Label _pressure_mode_label; + + void reprogram_pressure_mode (); +#endif +}; + +} } /* namespaces */ + +#endif /* __ardour_lpx_gui_h__ */ diff --git a/libs/surfaces/launchkey_4/launchkey_4.cc b/libs/surfaces/launchkey_4/launchkey_4.cc new file mode 100644 index 0000000000..9f95a534e7 --- /dev/null +++ b/libs/surfaces/launchkey_4/launchkey_4.cc @@ -0,0 +1,2715 @@ +/* + * Copyright (C) 2016-2018 Paul Davis + * Copyright (C) 2017-2018 Robin Gareus + * + * 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 +#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 "temporal/time.h" +#include "temporal/bbt_time.h" + +#include "ardour/amp.h" +#include "ardour/async_midi_port.h" +#include "ardour/audioengine.h" +#include "ardour/dB.h" +#include "ardour/debug.h" +#include "ardour/internal_send.h" +#include "ardour/midiport_manager.h" +#include "ardour/midi_track.h" +#include "ardour/midi_port.h" +#include "ardour/plugin.h" +#include "ardour/plugin_insert.h" +#include "ardour/selection.h" +#include "ardour/session.h" +#include "ardour/tempo.h" +#include "ardour/triggerbox.h" +#include "ardour/types_convert.h" +#include "ardour/utils.h" + +#include "gtkmm2ext/gui_thread.h" +#include "gtkmm2ext/rgb_macros.h" + +#include "gtkmm2ext/colors.h" + +#include "gui.h" +#include "launchkey_4.h" + +#include "pbd/i18n.h" + +#ifdef PLATFORM_WINDOWS +#define random() rand() +#endif + +using namespace ARDOUR; +using namespace PBD; +using namespace Glib; +using namespace ArdourSurface; +using namespace ArdourSurface::LAUNCHPAD_NAMESPACE; +using namespace Gtkmm2ext; + +#include "pbd/abstract_ui.cc" // instantiate template + +/* USB IDs */ + +#define NOVATION 0x1235 + +#define LAUNCHKEY4_MINI_25 0x0141 +#define LAUNCHKEY4_MINI_37 0x0142 +#define LAUNCHKEY4_25 0x0143 +#define LAUNCHKEY4_37 0x0144 +#define LAUNCHKEY4_49 0x0145 +#define LAUNCHKEY4_61 0x0146 + +static int first_fader = 0x9; +static const int PAD_COLUMNS = 8; +static const int PAD_ROWS = 2; +static const int NFADERS = 9; +static int last_detected = 0x0; + +bool +LaunchKey4::available () +{ + /* no preconditions other than the device being present */ + return true; +} + +bool +LaunchKey4::match_usb (uint16_t vendor, uint16_t device) +{ + if (vendor != NOVATION) { + return false; + } + + switch (device) { + case LAUNCHKEY4_MINI_25: + case LAUNCHKEY4_MINI_37: + case LAUNCHKEY4_25: + case LAUNCHKEY4_37: + case LAUNCHKEY4_49: + case LAUNCHKEY4_61: + last_detected = device; + return true; + } + + + return false; +} + +bool +LaunchKey4::probe (std::string& i, std::string& o) +{ + vector midi_inputs; + vector midi_outputs; + + AudioEngine::instance()->get_ports ("", DataType::MIDI, PortFlags (IsOutput|IsTerminal), midi_inputs); + AudioEngine::instance()->get_ports("", DataType::MIDI, PortFlags(IsInput | IsTerminal), midi_outputs); + + if (midi_inputs.empty() || midi_outputs.empty()) { + return false; + } + + std::regex rx (X_("Launchkey (Mini MK4|MK4).*MI"), std::regex::extended); + + auto has_lppro = [&rx](string const &s) { + std::string pn = AudioEngine::instance()->get_hardware_port_name_by_name(s); + return std::regex_search (pn, rx); + }; + + auto pi = std::find_if (midi_inputs.begin(), midi_inputs.end(), has_lppro); + auto po = std::find_if (midi_outputs.begin (), midi_outputs.end (), has_lppro); + + if (pi == midi_inputs.end () || po == midi_outputs.end ()) { + return false; + } + + i = *pi; + o = *po; + return true; +} + +LaunchKey4::LaunchKey4 (ARDOUR::Session& s) +#ifdef LAUNCHPAD_MINI + : MIDISurface (s, X_("Novation Launchkey Mini"), X_("Launchkey Mini"), true) +#else + : MIDISurface (s, X_("Novation Launchkey 4"), X_("Launchkey MK4"), true) +#endif + , _daw_out_port (nullptr) + , _gui (nullptr) + , scroll_x_offset (0) + , scroll_y_offset (0) + , device_pid (0x0) + , mode_channel (0xf) + , pad_function (MuteSolo) + , shift_pressed (false) + , layer_pressed (false) + , bank_start (0) + , button_mode (ButtonsRecEnable) // reset via toggle later + , encoder_mode (EncoderMixer) + , num_plugin_controls (0) +{ + run_event_loop (); + port_setup (); + + std::string pn_in, pn_out; + if (probe (pn_in, pn_out)) { + _async_in->connect (pn_in); + _async_out->connect (pn_out); + } + + build_color_map (); + build_pad_map (); + + Trigger::TriggerPropertyChange.connect (trigger_connections, invalidator (*this), boost::bind (&LaunchKey4::trigger_property_change, this, _1, _2), this); + ControlProtocol::PluginSelected.connect (session_connections, invalidator (*this), boost::bind (&LaunchKey4::plugin_selected, this, _1), this); + + session->RecordStateChanged.connect (session_connections, invalidator(*this), boost::bind (&LaunchKey4::record_state_changed, this), this); + session->TransportStateChange.connect (session_connections, invalidator(*this), boost::bind (&LaunchKey4::transport_state_changed, this), this); + session->RouteAdded.connect (session_connections, invalidator(*this), boost::bind (&LaunchKey4::stripables_added, this), this); + session->SoloChanged.connect (session_connections, invalidator(*this), boost::bind (&LaunchKey4::solo_changed, this), this); +} + +LaunchKey4::~LaunchKey4 () +{ + DEBUG_TRACE (DEBUG::Launchkey, "launchkey control surface object being destroyed\n"); + + trigger_connections.drop_connections (); + route_connections.drop_connections (); + session_connections.drop_connections (); + + for (size_t n = 0; n < sizeof (pads) / sizeof (pads[0]); ++n) { + pads[n].timeout_connection.disconnect (); + } + + stop_event_loop (); + tear_down_gui (); + + MIDISurface::drop (); + +} + +void +LaunchKey4::transport_state_changed () +{ + MIDI::byte msg[9]; + + msg[0] = 0xb0 | mode_channel; + msg[1] = 0x73; + + msg[3] = 0xb0 | mode_channel; + msg[4] = Play; + + msg[6] = 0xb0 | mode_channel; + msg[7] = Stop; + + if (session->transport_rolling()) { + msg[2] = 0x7f; + msg[5] = 0x0; + } else { + msg[2] = 0x0; + msg[5] = 0x7f; + } + + if (session->get_play_loop()) { + msg[8] = 0x7f; + } else { + msg[8] = 0x0; + } + + daw_write (msg, 9); + + map_rec_enable (); +} + +void +LaunchKey4::record_state_changed () +{ + map_rec_enable(); +} + +void +LaunchKey4::map_rec_enable () +{ + if (button_mode != ButtonsRecEnable) { + return; + } + + MIDI::byte msg[3]; + int channel = session->actively_recording() ? 0x0 : 0x2; + const int rec_color_index = 0x5; /* bright red */ + const int norec_color_index = 0x0; + + /* The global rec-enable button */ + + msg[0] = 0xb0 | channel; + msg[1] = 0x75; + msg[2] = session->get_record_enabled() ? rec_color_index : norec_color_index; + + daw_write (msg, 3); + + /* Now all the tracks */ + + for (int i = 0; i < NFADERS-1; ++i) { + show_rec_enable (i); + } +} + +void +LaunchKey4::show_rec_enable (int n) +{ + LightingMode mode = session->actively_recording() ? Solid : Pulse; + const int rec_color_index = 0x5; /* bright red */ + const int norec_color_index = 0x0; + + if (stripable[n]) { + std::shared_ptr ac = stripable[n]->rec_enable_control(); + if (ac) { + light_button (Button1 + n, mode, ac->get_value() ? rec_color_index : norec_color_index); + } else { + light_button (Button1 + n, Solid, 0x0); + } + } else { + light_button (Button1 + n, Solid, 0x0); + } +} + +int +LaunchKey4::set_active (bool yn) +{ + DEBUG_TRACE (DEBUG::Launchkey, string_compose("Launchpad X::set_active init with yn: %1\n", yn)); + + if (yn == active()) { + return 0; + } + + if (yn) { + + if (device_acquire ()) { + return -1; + } + + } else { + /* Control Protocol Manager never calls us with false, but + * insteads destroys us. + */ + } + + ControlProtocol::set_active (yn); + + DEBUG_TRACE (DEBUG::Launchkey, string_compose("Launchpad X::set_active done with yn: '%1'\n", yn)); + + return 0; +} + +void +LaunchKey4::run_event_loop () +{ + DEBUG_TRACE (DEBUG::Launchkey, "start event loop\n"); + BaseUI::run (); +} + +void +LaunchKey4::stop_event_loop () +{ + DEBUG_TRACE (DEBUG::Launchkey, "stop event loop\n"); + BaseUI::quit (); +} + +int +LaunchKey4::begin_using_device () +{ + DEBUG_TRACE (DEBUG::Launchkey, "begin using device\n"); + + /* get device model */ + + _data_required = true; + MidiByteArray device_inquiry (6, 0xf0, 0x7e, 0x7f, 0x06, 0x01, 0xf7); + write (device_inquiry); + + return 0; +} + +void +LaunchKey4::finish_begin_using_device () +{ + DEBUG_TRACE (DEBUG::Launchkey, "finish begin using device\n"); + + _data_required = false; + + if (MIDISurface::begin_using_device ()) { + return; + } + + connect_daw_ports (); + + /* enter DAW mode */ + + set_daw_mode (true); + set_pad_function (MuteSolo); + + /* catch current selection, if any so that we can wire up the pads if appropriate */ + stripable_selection_changed (); + switch_bank (0); + toggle_button_mode (); + use_encoders (true); + set_encoder_bank (0); + + /* Set configuration for fader displays, which is never altered */ + + MIDI::byte display_config[10]; + + display_config[0] = 0xf0; + display_config[1] = 0x0; + display_config[2] = 0x20; + display_config[3] = 0x29; + display_config[4] = (device_pid>>8) & 0x7f; + display_config[5] = device_pid & 0x7f; + display_config[6] = 0x4; + + display_config[8] = 0x61; + display_config[9] = 0xf7; + + for (int fader = 0; fader < 9; ++fader) { + /* 2 line display for all faders */ + display_config[7] = 0x5 + fader; + daw_write (display_config, 10); + } + std::cerr << "Configuring displays now\n"; + configure_display (StationaryDisplay, 0x1); + set_display_target (StationaryDisplay, 0, "ardour", true); + set_display_target (StationaryDisplay, 1, string(), true); + + configure_display (DAWPadFunctionDisplay, 0x1); + + /* In this DAW, mixer mode controls pan */ + set_display_target (MixerPotMode, 1, "Level", false); +} + +void +LaunchKey4::set_daw_mode (bool yn) +{ + MidiByteArray msg; + + msg.push_back (0x9f); + msg.push_back (0xc); + msg.push_back (yn ? 0x7f : 0x0); + daw_write (msg); + + if (yn) { + mode_channel = 0x0; + } else { + mode_channel = 0xf; + } + + if (yn) { + all_pads_out (); + } +} + +void +LaunchKey4::all_pads (int color_index) +{ + MIDI::byte msg[3]; + + msg[0] = 0x90; + msg[2] = color_index; + + /* top row */ + for (int i = 0; i < 8; ++i) { + msg[1] = 0x60 + i; + daw_write (msg, 3); + } + for (int i = 0; i < 8; ++i) { + msg[1] = 0x70 + i; + daw_write (msg, 3); + } +} + +void +LaunchKey4::all_pads_out () +{ + all_pads (0x0); +} + +int +LaunchKey4::stop_using_device () +{ + DEBUG_TRACE (DEBUG::Launchkey, "stop using device\n"); + + if (!_in_use) { + DEBUG_TRACE (DEBUG::Launchkey, "nothing to do, device not in use\n"); + return 0; + } + + set_daw_mode (false); + + return MIDISurface::stop_using_device (); +} + +XMLNode& +LaunchKey4::get_state() const +{ + XMLNode& node (MIDISurface::get_state()); + + XMLNode* child = new XMLNode (X_("DAWInput")); + child->add_child_nocopy (_daw_in->get_state()); + node.add_child_nocopy (*child); + child = new XMLNode (X_("DAWOutput")); + child->add_child_nocopy (_daw_out->get_state()); + node.add_child_nocopy (*child); + + return node; +} + +int +LaunchKey4::set_state (const XMLNode & node, int version) +{ + DEBUG_TRACE (DEBUG::Launchkey, string_compose ("LaunchKey4::set_state: active %1\n", active())); + + int retval = 0; + + if (MIDISurface::set_state (node, version)) { + return -1; + } + + return retval; +} + +std::string +LaunchKey4::input_port_name () const +{ + switch (last_detected) { + case LAUNCHKEY4_MINI_25: + case LAUNCHKEY4_MINI_37: + return X_(":Launchpad Mini MK3.*MIDI (In|2)"); + default: + break; + } + return X_(":Launchpad X MK3.*MIDI (In|2)"); +} + +std::string +LaunchKey4::output_port_name () const +{ + switch (last_detected) { + case LAUNCHKEY4_MINI_25: + case LAUNCHKEY4_MINI_37: + return X_(":Launchpad Mini MK3.*MIDI (Out|2)"); + default: + break; + } + + return X_(":Launchpad X MK3.*MIDI (Out|2)"); +} + +void +LaunchKey4::relax (Pad & pad) +{ +} + +void +LaunchKey4::relax (Pad & pad, int) +{ +} + +void +LaunchKey4::build_pad_map () +{ + for (int n = 0; n < 8; ++n) { + int pid = 0x60 + n; + pads[n] = Pad (pid, n, 0); + } + for (int n = 0; n < 8; ++n) { + int pid = 0x70 + n; + pads[8+n] = Pad (pid, n, 1); + } +} + +void +LaunchKey4::use_encoders (bool onoff) +{ + MIDI::byte msg[3]; + msg[0] = 0xb6; + msg[1] = 0x45; + msg[2] = (onoff ? 0x7f : 0x0); + daw_write (msg, 3); + + if (!onoff) { + return; + } + + MIDI::byte display_config[10]; + + display_config[0] = 0xf0; + display_config[1] = 0x0; + display_config[2] = 0x20; + display_config[3] = 0x29; + display_config[4] = (device_pid>>8) & 0x7f; + display_config[5] = device_pid & 0x7f; + display_config[6] = 0x4; + + display_config[8] = 0x62; + display_config[9] = 0xf7; + + for (int encoder = 0; encoder < 8; ++encoder) { + /* 2 line display for all encoders */ + display_config[7] = 0x15 + encoder; + daw_write (display_config, 10); + } +} + +void +LaunchKey4::handle_midi_sysex (MIDI::Parser& parser, MIDI::byte* raw_bytes, size_t sz) +{ +#ifndef NDEBUG + if (DEBUG_ENABLED(DEBUG::Launchkey)) { + std::stringstream str; + str << "Sysex received, size " << sz << std::endl; + str << hex; + for (size_t n = 0; n < sz; ++n) { + str << "0x" << (int) raw_bytes[n] << ' '; + } + str << std::endl; + std::cerr << str.str(); + } +#endif + + if (sz != 17) { + return; + } + + MIDI::byte dp_lsb; + MIDI::byte dp_msb; + + if (raw_bytes[1] == 0x7e && + raw_bytes[2] == 0x0 && + raw_bytes[3] == 0x6 && + raw_bytes[4] == 0x2 && + raw_bytes[5] == 0x0 && + raw_bytes[6] == 0x20 && + raw_bytes[7] == 0x29) { + dp_lsb = raw_bytes[8]; + dp_msb = raw_bytes[9]; + + const int family = (dp_msb<<8)|dp_lsb; + switch (family) { + case LAUNCHKEY4_MINI_25: + case LAUNCHKEY4_MINI_37: + device_pid = 0x0213; + break; + case LAUNCHKEY4_25: + case LAUNCHKEY4_37: + case LAUNCHKEY4_49: + case LAUNCHKEY4_61: + device_pid = 0x0214; + break; + default: + return; + } + + finish_begin_using_device (); + return; + } +} + +void +LaunchKey4::handle_midi_controller_message_chnF (MIDI::Parser& parser, MIDI::EventTwoBytes* ev) +{ + if (ev->controller_number < 0x05 || ev->controller_number > 0xd) { + return; + } + + int fader_number = ev->controller_number - 0x5; + fader_move (fader_number, ev->value); +} + +void +LaunchKey4::handle_midi_controller_message (MIDI::Parser& parser, MIDI::EventTwoBytes* ev) +{ + /* Remember: fader controller events are delivered via if (ev->controller_::handle_midi_controller_message_chnF() */ + if (&parser != _daw_in_port->parser()) { + if (ev->controller_number == 0x69 && ev->value == 0x7f) { + DEBUG_TRACE (DEBUG::Launchkey, string_compose ("function button press on non-DAW port, CC %1 (value %2)\n", (int) ev->controller_number, (int) ev->value)); + function_press (); + return; + } + /* we don't process CC messages from the regular port */ + DEBUG_TRACE (DEBUG::Launchkey, string_compose ("skip non-DAW CC %1 (value %2)\n", (int) ev->controller_number, (int) ev->value)); + return; + } + +#ifndef NDEBUG + std::stringstream ss; + ss << "CC 0x" << std::hex << (int) ev->controller_number << std::dec << " value (" << (int) ev->value << ")\n"; + DEBUG_TRACE (DEBUG::Launchkey, ss.str()); +#endif + + /* Shift being pressed can change everything */ + + if (ev->controller_number == 0x48) { + if (ev->value) { + shift_pressed = true; + } else { + shift_pressed = false; + } + return; + } + + /* Scene launch */ + if (ev->controller_number == 0x68) { + if (ev->value) { + scene_press (); + } + return; + } + + /* Button 9 (below fader 9 */ + + if (ev->controller_number == Button9) { + /* toggle on press only */ + + if (ev->value) { + toggle_button_mode (); + } + return; + } + + /* Encoder Mode button */ + + if (ev->controller_number == 0x41) { + switch (ev->value) { + case 2: + set_encoder_mode (EncoderPlugins); + break; + case 1: + set_encoder_mode (EncoderMixer); + break; + case 4: + set_encoder_mode (EncoderSendA); + break; + case 5: + set_encoder_mode (EncoderTransport); + break; + default: + break; + } + return; + } + + /* Encoder Bank Buttons */ + + if (ev->controller_number == 0x33) { + /* up'; use press only */ + if (ev->value && encoder_bank > 0) { + set_encoder_bank (encoder_bank - 1); + } + return; + } + + if (ev->controller_number == 0x34) { + /* down; use press only */ + if (ev->value && encoder_bank < 2) { + set_encoder_bank (encoder_bank + 1); + } + return; + } + + switch (ev->controller_number) { + case 0x6a: + if (ev->value) button_up (); + return; + case 0x6b: + if (ev->value) button_down (); + return; + case 0x67: + if (ev->value) button_left (); + return; + case 0x66: + if (ev->value) button_right (); + return; + } + + /* Buttons below faders */ + + if (ev->controller_number >= Button1 && ev->controller_number <= Button8) { + + if (ev->value == 0x7f) { + button_press (ev->controller_number - Button1); + } else { + button_release (ev->controller_number - Button1); + } + + return; + + } else if (ev->controller_number >= Knob1 && ev->controller_number <= Knob8) { + + encoder (ev->controller_number - Knob1, ev->value - 64); + return; + + } else if (ev->controller_number >= 0x55 && ev->controller_number <= 0x5c) { + + encoder (ev->controller_number - Knob1, ev->value - 64); + return; + } + + if (ev->value == 0x7f) { + switch (ev->controller_number) { + case Function: + function_press (); + break; + case Undo: + undo_press (); + break; + case Play: + if (device_pid == 0x213) { + /* Mini version only play button, so toggle */ + if (session->transport_rolling()) { + transport_stop(); + } else { + transport_play(); + } + } else { + transport_play (); + } + break; + case Stop: + transport_stop (); + break; + case RecEnable: + set_record_enable (!get_record_enabled()); + break; + case Loop: + loop_toggle (); + break; + default: + break; + } + } +} + +void +LaunchKey4::handle_midi_note_on_message (MIDI::Parser& parser, MIDI::EventTwoBytes* ev) +{ + if (ev->velocity == 0) { + handle_midi_note_off_message (parser, ev); + return; + } + + if (&parser != _daw_in_port->parser()) { + /* we don't process note messages from the regular port */ + DEBUG_TRACE (DEBUG::Launchkey, string_compose ("skip non-DAW Note On %1/0x%3%4%5 (velocity %2)\n", (int) ev->note_number, (int) ev->velocity, std::hex, (int) ev->note_number, std::dec)); + return; + } + + int pad_number; + + switch (ev->note_number) { + case 0x60: + pad_number = 0; + break; + case 0x61: + pad_number = 1; + break; + case 0x62: + pad_number = 2; + break; + case 0x63: + pad_number = 3; + break; + case 0x64: + pad_number = 4; + break; + case 0x65: + pad_number = 5; + break; + case 0x66: + pad_number = 6; + break; + case 0x67: + pad_number = 7; + break; + + case 0x70: + pad_number = 8; + break; + case 0x71: + pad_number = 9; + break; + case 0x72: + pad_number = 10; + break; + case 0x73: + pad_number = 11; + break; + case 0x74: + pad_number = 12; + break; + case 0x75: + pad_number = 13; + break; + case 0x76: + pad_number = 14; + break; + case 0x77: + pad_number = 15; + break; + default: + return; + } + + DEBUG_TRACE (DEBUG::Launchkey, string_compose ("Note On %1/0x%3%4%5 (velocity %2) => pad %6\n", (int) ev->note_number, (int) ev->velocity, std::hex, (int) ev->note_number, std::dec, pad_number)); + + Pad& pad = pads[pad_number]; + + switch (pad_function) { + case MuteSolo: + pad_mute_solo (pad); + break; + case Triggers: + pad_trigger (pad, ev->velocity); + break; + default: + break; + } +} + +void +LaunchKey4::handle_midi_note_off_message (MIDI::Parser&, MIDI::EventTwoBytes* ev) +{ + int pad_number; + + switch (ev->note_number) { + case 0x60: + pad_number = 0; + break; + case 0x61: + pad_number = 1; + break; + case 0x62: + pad_number = 2; + break; + case 0x63: + pad_number = 3; + break; + case 0x64: + pad_number = 4; + break; + case 0x65: + pad_number = 5; + break; + case 0x66: + pad_number = 6; + break; + case 0x67: + pad_number = 7; + break; + + case 0x70: + pad_number = 8; + break; + case 0x71: + pad_number = 9; + break; + case 0x72: + pad_number = 10; + break; + case 0x73: + pad_number = 11; + break; + case 0x74: + pad_number = 12; + break; + case 0x75: + pad_number = 13; + break; + case 0x76: + pad_number = 14; + break; + case 0x77: + pad_number = 15; + break; + default: + return; + } + + DEBUG_TRACE (DEBUG::Launchkey, string_compose ("Note Off %1/0x%3%4%5 (velocity %2)\n", (int) ev->note_number, (int) ev->velocity, std::hex, (int) ev->note_number, std::dec)); + pad_release (pads[pad_number]); +} + +void +LaunchKey4::pad_trigger (Pad& pad, int velocity) +{ + if (shift_pressed) { + trigger_stop_col (pad.x, true); /* immediate */ + } else { + TriggerPtr trigger = session->trigger_at (pad.x, pad.y + scroll_y_offset); + switch (trigger->state()) { + case Trigger::Stopped: + trigger->bang (velocity / 127.0f); + break; + default: + break; + } + start_press_timeout (pad); + } +} + +void +LaunchKey4::pad_release (Pad& pad) +{ + pad.timeout_connection.disconnect (); +} + +void +LaunchKey4::start_press_timeout (Pad& pad) +{ + Glib::RefPtr timeout = Glib::TimeoutSource::create (250); // milliseconds + pad.timeout_connection = timeout->connect (sigc::bind (sigc::mem_fun (*this, &LaunchKey4::long_press_timeout), pad.x)); + timeout->attach (main_loop()->get_context()); +} + +bool +LaunchKey4::long_press_timeout (int col) +{ + std::cerr << "timeout!\n"; + trigger_stop_col (col, false); /* non-immediate */ + return false; /* don't get called again */ +} + +void +LaunchKey4::trigger_property_change (PropertyChange pc, Trigger* t) +{ + if (pad_function != Triggers) { + return; + } + + int x = t->box().order(); + int y = t->index(); + + DEBUG_TRACE (DEBUG::Launchpad, string_compose ("prop change %1 for trigger at %2, %3\n", pc, x, y)); + + if (y < scroll_y_offset || y > scroll_y_offset + 1) { + /* not visible at present */ + return; + } + + if (x < scroll_x_offset || x > scroll_x_offset + 7) { + /* not visible at present */ + return; + } + + y -= scroll_y_offset; + x -= scroll_x_offset; + + /* name property change is sent when slots are loaded or unloaded */ + + PropertyChange our_interests; + our_interests.add (Properties::running); + our_interests.add (Properties::name);; + + if (pc.contains (our_interests)) { + + Pad& pad (pads[(y*8) + x]); + std::shared_ptr r = session->get_remote_nth_route (scroll_x_offset + x); + + trigger_pad_light (pad, r, t); + } +} + +void +LaunchKey4::trigger_pad_light (Pad& pad, std::shared_ptr r, Trigger* t) +{ + if (!r || !t || !t->region()) { + unlight_pad (pad.id); + return; + } + + MIDI::byte msg[3]; + + msg[0] = 0x90; + msg[1] = pad.id; + + switch (t->state()) { + case Trigger::Stopped: + msg[2] = find_closest_palette_color (r->presentation_info().color()); + break; + + case Trigger::WaitingToStart: + msg[0] |= 0x2; /* channel 2=> pulsing */ + msg[2] = 0x17; // find_closest_palette_color (r->presentation_info().color())); + break; + + case Trigger::Running: + /* choose contrasting color from the base one */ + msg[2] = find_closest_palette_color (HSV(r->presentation_info().color()).opposite()); + break; + + case Trigger::WaitingForRetrigger: + case Trigger::WaitingToStop: + case Trigger::WaitingToSwitch: + case Trigger::Stopping: + msg[0] |= 0x2; /* pulse */ + msg[2] = find_closest_palette_color (HSV(r->presentation_info().color()).opposite()); + } + + daw_write (msg, 3); +} + +void +LaunchKey4::map_triggers () +{ + for (int x = 0; x < PAD_COLUMNS; ++x) { + map_triggerbox (x); + } +} + +void +LaunchKey4::map_triggerbox (int x) +{ + std::shared_ptr r = session->get_remote_nth_route (x + scroll_x_offset); + + for (int y = 0; y < PAD_ROWS; ++y) { + Pad& pad (pads[(y*8) + x]); + TriggerPtr t = session->trigger_at (x + scroll_x_offset, y + scroll_y_offset); + trigger_pad_light (pad, r, t.get()); + } +} + +void +LaunchKey4::pad_mute_solo (Pad& pad) +{ + if (!stripable[pad.x]) { + return; + } + + if (pad.y == 0) { + session->set_control (stripable[pad.x]->mute_control(), !stripable[pad.x]->mute_control()->get_value(), PBD::Controllable::UseGroup); + } else { + session->set_control (stripable[pad.x]->solo_control(), !stripable[pad.x]->solo_control()->get_value(), PBD::Controllable::UseGroup); + } +} + +void +LaunchKey4::port_registration_handler () +{ + MIDISurface::port_registration_handler (); + connect_daw_ports (); +} + +void +LaunchKey4::connect_daw_ports () +{ + if (!_daw_in || !_daw_out) { + /* ports not registered yet */ + return; + } + + if (_daw_in->connected() && _daw_out->connected()) { + /* don't waste cycles here */ + return; + } + + std::vector midi_inputs; + std::vector midi_outputs; + + /* get all MIDI Ports */ + + AudioEngine::instance()->get_ports ("", DataType::MIDI, PortFlags (IsOutput|IsTerminal), midi_inputs); + AudioEngine::instance()->get_ports("", DataType::MIDI, PortFlags(IsInput | IsTerminal), midi_outputs); + + if (midi_inputs.empty() || midi_outputs.empty()) { + return; + } + + /* Try to find the DAW port, whose pretty name varies on Linux + * depending on the version of ALSA, but is fairly consistent across + * newer ALSA and other platforms. + */ + + std::string regex_str; + + if (device_pid == 0x213) { + regex_str = X_("Launchkey Mini MK4.*(DAW|MIDI 2|DA$)"); + } else { + regex_str = X_("Launchkey MK4.*(DAW|MIDI 2|DA$)"); + } + + std::regex rx (regex_str, std::regex::extended); + + auto is_dawport = [&rx](string const &s) { + std::string pn = AudioEngine::instance()->get_hardware_port_name_by_name(s); + return std::regex_search (pn, rx); + }; + + auto pi = std::find_if (midi_inputs.begin(), midi_inputs.end(), is_dawport); + auto po = std::find_if (midi_outputs.begin (), midi_outputs.end (), is_dawport); + + if (pi == midi_inputs.end() || po == midi_inputs.end()) { + std::cerr << "daw port not found\n"; + return; + } + + if (!_daw_in->connected()) { + AudioEngine::instance()->connect (_daw_in->name(), *pi); + } + + if (!_daw_out->connected()) { + AudioEngine::instance()->connect (_daw_out->name(), *po); + } + + + connect_to_port_parser (*_daw_in_port); + + MIDI::Parser* p = _daw_in_port->parser(); + /* fader messages are controllers but always on channel 0xf */ + p->channel_controller[15].connect_same_thread (*this, boost::bind (&LaunchKey4::handle_midi_controller_message_chnF, this, _1, _2)); + + /* Connect DAW input port to event loop */ + + AsyncMIDIPort* asp; + + asp = dynamic_cast (_daw_in_port); + asp->xthread().set_receive_handler (sigc::bind (sigc::mem_fun (this, &MIDISurface::midi_input_handler), _daw_in_port)); + asp->xthread().attach (main_loop()->get_context()); +} + +int +LaunchKey4::ports_acquire () +{ + int ret = MIDISurface::ports_acquire (); + + if (!ret) { + _daw_in = AudioEngine::instance()->register_input_port (DataType::MIDI, string_compose (X_("%1 daw in"), port_name_prefix), true); + if (_daw_in) { + _daw_in_port = std::dynamic_pointer_cast(_daw_in).get(); + _daw_out = AudioEngine::instance()->register_output_port (DataType::MIDI, string_compose (X_("%1 daw out"), port_name_prefix), true); + } + if (_daw_out) { + _daw_out_port = std::dynamic_pointer_cast(_daw_out).get(); + return 0; + } + + ret = -1; + } + + return ret; +} + +void +LaunchKey4::ports_release () +{ + /* wait for button data to be flushed */ + MIDI::Port* daw_port = std::dynamic_pointer_cast(_daw_out).get(); + AsyncMIDIPort* asp; + asp = dynamic_cast (daw_port); + asp->drain (10000, 500000); + + { + Glib::Threads::Mutex::Lock em (AudioEngine::instance()->process_lock()); + AudioEngine::instance()->unregister_port (_daw_in); + AudioEngine::instance()->unregister_port (_daw_out); + } + + _daw_in.reset ((ARDOUR::Port*) 0); + _daw_out.reset ((ARDOUR::Port*) 0); + + MIDISurface::ports_release (); +} + +void +LaunchKey4::daw_write (const MidiByteArray& data) +{ + DEBUG_TRACE (DEBUG::Launchkey, string_compose ("daw write %1 %2\n", data.size(), data)); + _daw_out_port->write (&data[0], data.size(), 0); +} + +void +LaunchKey4::daw_write (MIDI::byte const * data, size_t size) +{ + +#ifndef NDEBUG + std::stringstream str; + + if (DEBUG_ENABLED(DEBUG::Launchkey)) { + str << hex; + for (size_t n = 0; n < size; ++n) { + str << (int) data[n] << ' '; + } + } +#endif + + DEBUG_TRACE (DEBUG::Launchkey, string_compose ("daw write %1 [%2]\n", size, str.str())); + _daw_out_port->write (data, size, 0); +} + +void +LaunchKey4::stripable_selection_changed () +{ + map_selection (); + + if (session->selection().first_selected_stripable()) { + set_display_target (GlobalTemporaryDisplay, 0, session->selection().first_selected_stripable()->name(), true); + } +} + +void +LaunchKey4::show_scene_ids () +{ + set_display_target (DAWPadFunctionDisplay, 0, string_compose ("Scenes %1 + %2", scroll_y_offset + 1, scroll_y_offset + 2), true); +} + +void +LaunchKey4::button_up () +{ + if (pad_function != Triggers) { + return; + } + + if (scroll_y_offset >= 1) { + scroll_y_offset -= 1; + show_scene_ids (); + } +} + +void +LaunchKey4::button_down() +{ + if (pad_function != Triggers) { + return; + } + + scroll_y_offset += 1; + show_scene_ids (); +} + +void +LaunchKey4::build_color_map () +{ + /* RGB values taken from using color picker on PDF of LP manual, page + * 10, but without zero (off) + */ + + static uint32_t novation_color_chart_left_side[] = { + 0xb3b3b3ff, + 0xddddddff, + 0xffffffff, + 0xffb3b3ff, + 0xff6161ff, + 0xdd6161ff, + 0xb36161ff, + 0xfff3d5ff, + 0xffb361ff, + 0xdd8c61ff, + 0xb37661ff, + 0xffeea1ff, + 0xffff61ff, + 0xdddd61ff, + 0xb3b361ff, + 0xddffa1ff, + 0xc2ff61ff, + 0xa1dd61ff, + 0x81b361ff, + 0xc2ffb3ff, + 0x61ff61ff, + 0x61dd61ff, + 0x61b361ff, + 0xc2ffc2ff, + 0x61ff8cff, + 0x61dd76ff, + 0x61b36bff, + 0xc2ffccff, + 0x61ffccff, + 0x61dda1ff, + 0x61b381ff, + 0xc2fff3ff, + 0x61ffe9ff, + 0x61ddc2ff, + 0x61b396ff, + 0xc2f3ffff, + 0x61eeffff, + 0x61c7ddff, + 0x61a1b3ff, + 0xc2ddffff, + 0x61c7ffff, + 0x61a1ddff, + 0x6181b3ff, + 0xa18cffff, + 0x6161ffff, + 0x6161ddff, + 0x6161b3ff, + 0xccb3ffff, + 0xa161ffff, + 0x8161ddff, + 0x7661b3ff, + 0xffb3ffff, + 0xff61ffff, + 0xdd61ddff, + 0xb361b3ff, + 0xffb3d5ff, + 0xff61c2ff, + 0xdd61a1ff, + 0xb3618cff, + 0xff7661ff, + 0xe9b361ff, + 0xddc261ff, + 0xa1a161ff, + }; + + static uint32_t novation_color_chart_right_side[] = { + 0x61b361ff, + 0x61b38cff, + 0x618cd5ff, + 0x6161ffff, + 0x61b3b3ff, + 0x8c61f3ff, + 0xccb3c2ff, + 0x8c7681ff, + /**/ + 0xff6161ff, + 0xf3ffa1ff, + 0xeefc61ff, + 0xccff61ff, + 0x76dd61ff, + 0x61ffccff, + 0x61e9ffff, + 0x61a1ffff, + /**/ + 0x8c61ffff, + 0xcc61fcff, + 0xcc61fcff, + 0xa17661ff, + 0xffa161ff, + 0xddf961ff, + 0xd5ff8cff, + 0x61ff61ff, + /**/ + 0xb3ffa1ff, + 0xccfcd5ff, + 0xb3fff6ff, + 0xcce4ffff, + 0xa1c2f6ff, + 0xd5c2f9ff, + 0xf98cffff, + 0xff61ccff, + /**/ + 0xff61ccff, + 0xf3ee61ff, + 0xe4ff61ff, + 0xddcc61ff, + 0xb3a161ff, + 0x61ba76ff, + 0x76c28cff, + 0x8181a1ff, + /**/ + 0x818cccff, + 0xccaa81ff, + 0xdd6161ff, + 0xf9b3a1ff, + 0xf9ba76ff, + 0xfff38cff, + 0xe9f9a1ff, + 0xd5ee76ff, + /**/ + 0x8181a1ff, + 0xf9f9d5ff, + 0xddfce4ff, + 0xe9e9ffff, + 0xe4d5ffff, + 0xb3b3b3ff, + 0xd5d5d5ff, + 0xf9ffffff, + /**/ + 0xe96161ff, + 0xe96161ff, + 0x81f661ff, + 0x61b361ff, + 0xf3ee61ff, + 0xb3a161ff, + 0xeec261ff, + 0xc27661ff + }; + + for (size_t n = 0; n < sizeof (novation_color_chart_left_side) / sizeof (novation_color_chart_left_side[0]); ++n) { + uint32_t color = novation_color_chart_left_side[n]; + /* Add 1 to account for missing zero at zero in the table */ + std::pair p (1 + n, color); + color_map.insert (p); + } + + for (size_t n = 0; n < sizeof (novation_color_chart_right_side) / sizeof (novation_color_chart_right_side[0]); ++n) { + uint32_t color = novation_color_chart_right_side[n]; + /* Add 40 to account for start offset number shown in page 10 of the LP manual */ + std::pair p (40 + n, color); + color_map.insert (p); + } +} + +int +LaunchKey4::find_closest_palette_color (uint32_t color) +{ + auto distance = std::numeric_limits::max(); + int index = -1; + + NearestMap::iterator n = nearest_map.find (color); + if (n != nearest_map.end()) { + return n->second; + } + + HSV hsv_c (color); + + for (auto const & c : color_map) { + + HSV hsv_p (c.second); + + double chr = M_PI * (hsv_c.h / 180.0); + double phr = M_PI * (hsv_p.h /180.0); + double t1 = (sin (chr) * hsv_c.s * hsv_c.v) - (sin (phr) * hsv_p.s* hsv_p.v); + double t2 = (cos (chr) * hsv_c.s * hsv_c.v) - (cos (phr) * hsv_p.s * hsv_p.v); + double t3 = hsv_c.v - hsv_p.v; + double d = (t1 * t1) + (t2 * t2) + (0.5 * (t3 * t3)); + + + if (d < distance) { + index = c.first; + distance = d; + } + } + + nearest_map.insert (std::pair (color, index)); + + return index; +} + +void +LaunchKey4::route_property_change (PropertyChange const & pc, int col) +{ + if (pc.contains (Properties::color)) { + map_triggerbox (col); + } + + + if (pc.contains (Properties::selected)) { + } +} + +void +LaunchKey4::fader_move (int which, int val) +{ + std::shared_ptr ac; + + if (which == 8) { + std::shared_ptr monitor = session->monitor_out(); + + if (monitor) { + ac = monitor->gain_control(); + } else { + std::shared_ptr master = session->master_out(); + if (!master) { + return; + } + ac = master->gain_control(); + } + + } else { + if (!stripable[which]) { + return; + } + + ac = stripable[which]->gain_control(); + } + + if (ac) { + gain_t gain = ARDOUR::slider_position_to_gain_with_max (val/127.0, ARDOUR::Config->get_max_gain()); + session->set_control (ac, gain, PBD::Controllable::NoGroup); + + char buf[16]; + snprintf (buf, sizeof (buf), "%.1f dB", accurate_coefficient_to_dB (gain)); + set_display_target (DisplayTarget (0x5 + which), 1, buf, true); + } +} + +void +LaunchKey4::automation_control_change (int n, std::weak_ptr wac) +{ + std::shared_ptr ac = wac.lock(); + if (!ac) { + return; + } + + MIDI::byte msg[3]; + msg[0] = 0xb4; + msg[1] = first_fader + n; + + switch (current_fader_bank) { + case VolumeFaders: + case SendAFaders: + case SendBFaders: + msg[2] = (MIDI::byte) (ARDOUR::gain_to_slider_position_with_max (ac->get_value(), ARDOUR::Config->get_max_gain()) * 127.0); + break; + case PanFaders: + msg[2] = (MIDI::byte) (ac->get_value() * 127.0); + break; + default: + break; + } + + daw_write (msg, 3); +} + +void +LaunchKey4::encoder (int which, int step) +{ + switch (encoder_mode) { + case EncoderPlugins: + encoder_plugin (which, step); + break; + case EncoderMixer: + encoder_mixer (which, step); + break; + case EncoderSendA: + encoder_senda (which, step); + break; + case EncoderTransport: + encoder_transport (which, step); + break; + } +} + +void +LaunchKey4::plugin_selected (std::weak_ptr wpi) +{ + std::shared_ptr pi (wpi.lock()); + if (!pi) { + return; + } + + current_plugin = pi->plugin(); + uint32_t n = 0; + + while (n < 24) { + + Evoral::ParameterDescriptor pd; + Evoral::Parameter param (PluginAutomation, 0, n); + + std::shared_ptr ac = pi->automation_control (param, false); + if (ac) { + controls[n] = ac; + } else { + break; + } + + ++n; + } + + num_plugin_controls = n; + + while (n < 24) { + controls[n].reset (); + ++n; + } + + if (encoder_mode == EncoderPlugins) { + label_encoders (); + /* light up/down arrows appropriately */ + set_encoder_bank (encoder_bank); + } +} + +void +LaunchKey4::show_encoder_value (int n, std::shared_ptr plugin, int control, std::shared_ptr ac, bool display) +{ + bool ok; + std::string str; + uint32_t p = plugin->nth_parameter (control, ok); + + if (!ok || !plugin->print_parameter (p, str)) { + char buf[32]; + double val = ac->get_value (); + snprintf (buf, sizeof (buf), "%.2f", val); + set_display_target (DisplayTarget (0x15 + n), 2, buf, display); + return; + } + + set_display_target (DisplayTarget (0x15 + n), 2, str, true); +} + +void +LaunchKey4::setup_screen_for_encoder_plugins () +{ + uint32_t n = 0; + + std::shared_ptr plugin = current_plugin.lock(); + + if (plugin) { + while (n < 8) { + uint32_t ctrl = (encoder_bank * 8) + n; + + std::shared_ptr ac = controls[ctrl].lock(); + bool ok; + + if (!ac) { + break; + } + int p = plugin->nth_parameter (n, ok); + if (!ok) { + break; + } + + std::string label = plugin->parameter_label (p); + + set_display_target (DisplayTarget (0x15+n), 0, plugin->name(), (n == 0)); + set_display_target (DisplayTarget (0x15+n), 1, label,(n == 0)); + show_encoder_value (n, plugin, ctrl, ac, (n == 0)); + ++n; + } + } + + while (n < 8) { + set_display_target (DisplayTarget (0x15+n), 0, plugin->name(), (n == 0)); + set_display_target (DisplayTarget (0x15+n), 1, "--", (n == 0)); + set_display_target (DisplayTarget (0x15+n), 2, string(), (n == 0)); + ++n; + } +} + +void +LaunchKey4::encoder_plugin (int which, int step) +{ + std::shared_ptr plugin (current_plugin.lock()); + if (!plugin) { + return; + } + + int control = which + (encoder_bank * 8); + std::shared_ptr ac (controls[control].lock()); + + if (!ac) { + return; + } + + double val = ac->internal_to_interface (ac->get_value()); + val += step/127.0; + ac->set_value (ac->interface_to_internal (val), PBD::Controllable::NoGroup); + + show_encoder_value (which, plugin, control, ac, true); +} + +void +LaunchKey4::encoder_mixer (int which, int step) +{ + switch (encoder_bank) { + case 0: + encoder_level (which, step); + break; + case 1: + encoder_pan (which, step); + break; + default: + break; + } +} + +void +LaunchKey4::encoder_pan (int which, int step) +{ + if (!stripable[which]) { + return; + } + + std::shared_ptr ac (stripable[which]->pan_azimuth_control()); + + if (!ac) { + return; + } + + double val = ac->internal_to_interface (ac->get_value()); + session->set_control (ac, ac->interface_to_internal (val - (step/127.0)), Controllable::NoGroup); + + char buf[64]; + snprintf (buf, sizeof (buf), _("L:%3d R:%3d"), (int) rint (100.0 * (1.0 - val)), (int) rint (100.0 * val)); + set_display_target (DisplayTarget (0x15 + which), 2, buf, true); +} + + +void +LaunchKey4::encoder_level (int which, int step) +{ + if (!stripable[which]) { + return; + } + + std::shared_ptr gc (stripable[which]->gain_control()); + + if (!gc) { + return; + } + + gain_t gain; + + if (shift_pressed) { + gain = gc->get_value(); + } else { + double pos = ARDOUR::gain_to_slider_position_with_max (gc->get_value(), ARDOUR::Config->get_max_gain()); + pos += (step/127.0); + gain = ARDOUR::slider_position_to_gain_with_max (pos, ARDOUR::Config->get_max_gain()); + session->set_control (gc, gain, Controllable::NoGroup); + } + + char buf[16]; + snprintf (buf, sizeof (buf), "%.1f dB", accurate_coefficient_to_dB (gain)); + set_display_target (DisplayTarget (0x15 + which), 2, buf, true); +} + +void +LaunchKey4::encoder_senda (int which, int step) +{ + std::shared_ptr s = session->selection().first_selected_stripable(); + if (!s) { + return; + } + + std::shared_ptr target_bus = std::dynamic_pointer_cast (s); + if (!target_bus) { + return; + } + + if (!stripable[which]) { + return; + } + + std::shared_ptr route = std::dynamic_pointer_cast (stripable[which]); + if (!route) { + return; + } + + std::shared_ptr send = std::dynamic_pointer_cast (route->internal_send_for (target_bus)); + if (!send) { + return; + } + + std::shared_ptr gc = send->gain_control(); + if (!gc) { + return; + } + gain_t gain; + + if (shift_pressed) { + /* Just display current value */ + gain = gc->get_value(); + } else { + double pos = ARDOUR::gain_to_slider_position_with_max (gc->get_value(), ARDOUR::Config->get_max_gain()); + pos += (step/127.0); + gain = ARDOUR::slider_position_to_gain_with_max (pos, ARDOUR::Config->get_max_gain()); + session->set_control (gc, gain, Controllable::NoGroup); + } + + char buf[16]; + snprintf (buf, sizeof (buf), "%.1f dB", accurate_coefficient_to_dB (gain)); + set_display_target (DisplayTarget (0x15 + which), 1, string_compose ("> %1", send->target_route()->name()), true); + set_display_target (DisplayTarget (0x15 + which), 2, buf, true); +} + +void +LaunchKey4::encoder_transport (int which, int step) +{ + switch (which) { + case 0: + transport_shuttle (step); + break; + case 1: + zoom (step); + break; + case 2: + loop_start_move (step); + break; + case 3: + loop_end_move (step); + break; + case 4: + jump_to_marker (step); + break; + case 5: + break; + case 6: + break; + case 7: + break; + } +} + +void +LaunchKey4::transport_shuttle (int step) +{ + using namespace Temporal; + + /* 1 step == 1/10th current page */ + timepos_t pos (session->transport_sample()); + + if (pos == 0 && step < 0) { + return; + } + + Beats b = pos.beats(); + + if (step > 0) { + b = b.round_up_to_beat (); + b += Beats (1, 0) * step; + } else { + b = b.round_down_to_beat (); + b += Beats (1, 0) * step; // step is negative, so add + if (b < Beats()) { + b = Beats(); + } + } + + BBT_Time bbt = TempoMap::use()->bbt_at (b); + std::stringstream str; + str << bbt; + + set_display_target (DisplayTarget (0x15), 2, str.str(), true); + + session->request_locate (timepos_t (b).samples()); +} + +void +LaunchKey4::zoom (int step) +{ + if (step > 0) { + while (step--) { + temporal_zoom_in (); + } + } else { + while (step++ < 0) { + temporal_zoom_out (); + } + } + set_display_target (DisplayTarget (0x15 + 1), 2, string(), true); +} + +void +LaunchKey4::loop_start_move (int step) +{ + using namespace Temporal; + + Location* l = session->locations()->auto_loop_location (); + BBT_Offset dur; + + if (!l) { + /* XXX NEEDS WRAPPING IN REVERSIBLE COMMAND */ + timepos_t ph (session->transport_sample()); + timepos_t beat_later ((ph.beats() + Beats (1,0)).round_to_beat()); + + Location* loc = new Location (*session, timepos_t (ph.beats()), beat_later, _("Loop"), Location::IsAutoLoop); + session->locations()->add (loc, true); + session->set_auto_loop_location (loc); + + dur = BBT_Offset (0, 1, 0); + + } else { + timepos_t start = l->start(); + start = start.beats() + Beats (step, 0); + if (start.is_zero() || start.is_negative()) { + return; + } + l->set_start (start); + + TempoMap::SharedPtr map (TempoMap::use()); + BBT_Time bbt_start = map->bbt_at (start); + BBT_Time bbt_end = map->bbt_at (l->end()); + + dur = bbt_delta (bbt_end, bbt_start); + } + + std::stringstream str; + str << dur; + set_display_target (DisplayTarget (0x15 + 2), 2, str.str(), true); +} + +void +LaunchKey4::loop_end_move (int step) +{ + using namespace Temporal; + + Location* l = session->locations()->auto_loop_location (); + BBT_Offset dur; + + if (!l) { + /* XXX NEEDS WRAPPING IN REVERSIBLE COMMAND */ + timepos_t ph (session->transport_sample()); + timepos_t beat_later ((ph.beats() + Beats (1,0)).round_to_beat()); + + Location* loc = new Location (*session, timepos_t (ph.beats()), beat_later, _("Loop"), Location::IsAutoLoop); + session->locations()->add (loc, true); + session->set_auto_loop_location (loc); + dur = BBT_Offset (0, 1, 0); + } else { + timepos_t end = l->end(); + end = end.beats() + Beats (step, 0); + if (end.is_zero() || end.is_negative()) { + return; + } + l->set_end (end); + + TempoMap::SharedPtr map (TempoMap::use()); + BBT_Time bbt_start = map->bbt_at (l->start()); + BBT_Time bbt_end = map->bbt_at (end); + + dur = bbt_delta (bbt_end, bbt_start); + } + + std::stringstream str; + str << dur; + set_display_target (DisplayTarget (0x15 + 3), 2, str.str(), true); +} + +void +LaunchKey4::jump_to_marker (int step) +{ + timepos_t pos; + Location::Flags noflags = Location::Flags (0); + Location* loc; + + if (step > 0) { + pos = session->locations()->first_mark_after_flagged (timepos_t (session->audible_sample()+1), true, noflags, noflags, noflags, &loc); + + if (pos == timepos_t::max (Temporal::AudioTime)) { + return; + } + + } else { + pos = session->locations()->first_mark_before_flagged (timepos_t (session->audible_sample()), true, noflags, noflags, noflags, &loc); + + //handle the case where we are rolling, and we're less than one-half second past the mark, we want to go to the prior mark... + if (session->transport_rolling()) { + if ((session->audible_sample() - pos.samples()) < session->sample_rate()/2) { + timepos_t prior = session->locations()->first_mark_before (pos); + pos = prior; + } + } + + if (pos == timepos_t::max (Temporal::AudioTime)) { + return; + } + } + + session->request_locate (pos.samples()); + + set_display_target (DisplayTarget (0x15+4), 2, loc->name(), true); +} + +void +LaunchKey4::set_pad_function (PadFunction f) +{ + MIDI::byte msg[3]; + std::string str; + + /* make the LK forget about any currently lit pads, because we overload + mode 0x2 and it gets confusing when it tries to restore lighting. + */ + all_pads (0x5); + all_pads_out (); + + msg[0] = 0xb6; + msg[1] = 0x40; /* set pad layout */ + + switch (f) { + case MuteSolo: + str = "Mute/Solo"; + break; + case Triggers: + str = "Cues & Scenes"; + break; + } + + pad_function = f; + + if (pad_function == Triggers) { + map_triggers (); + } else if (pad_function == MuteSolo) { + map_mute_solo (); + } + + /* Turn up/down arrows on/off depending on pad mode, also scene mode */ + + msg[0] = 0xb0; + msg[2] = (pad_function == Triggers ? 0x3 : 0x0); + + msg[1] = 0x6a; /* upper */ + daw_write (msg, 3); + msg[1] = 0x6b; /* lower */ + daw_write (msg, 3); + msg[1] = 0x68; /* scene */ + daw_write (msg, 3); + + configure_display (DAWPadFunctionDisplay, 0x1); + set_display_target (DAWPadFunctionDisplay, 0, str, true); +} + +void +LaunchKey4::select_display_target (DisplayTarget dt) +{ + MidiByteArray msg; + + msg.push_back (0xf0); + msg.push_back (0x0); + msg.push_back (0x20); + msg.push_back (0x29); + msg.push_back ((device_pid >> 8) & 0x7f); + msg.push_back (device_pid & 0x7f); + msg.push_back (0x4); + msg.push_back (dt); + msg.push_back (0x7f); + msg.push_back (0xf7); + + daw_write (msg); +} + +void +LaunchKey4::set_plugin_encoder_name (int encoder, int field, std::string const & str) +{ + set_display_target (PluginPotMode, field, str, true); +} + +void +LaunchKey4::set_display_target (DisplayTarget dt, int field, std::string const & str, bool display) +{ + MidiByteArray msg; + + msg.push_back (0xf0); + msg.push_back (0x0); + msg.push_back (0x20); + msg.push_back (0x29); + msg.push_back ((device_pid >> 8) & 0x7f); + msg.push_back (device_pid & 0x7f); + msg.push_back (0x6); + msg.push_back (dt); + msg.push_back (display ? ((1<<6) | (field & 0x7f)) : (field & 0x7f)); + + for (auto c : str) { + msg.push_back (c & 0x7f); + } + + msg.push_back (0xf7); + + daw_write (msg); + write (msg); +} + +void +LaunchKey4::configure_display (DisplayTarget target, int config) +{ + MidiByteArray msg (9, 0xf0, 0x00, 0x29, 0xff, 0xff, 0x04, 0xff, 0xff, 0xf7); + + msg[3] = (device_pid >> 8) & 0x7f; + msg[4] = device_pid & 0x7f; + + msg[6] = target; + msg[7] = config & 0x7f; + + daw_write (msg); +} + +void +LaunchKey4::function_press () +{ + switch (pad_function) { + case MuteSolo: + set_pad_function (Triggers); + break; + case Triggers: + set_pad_function (MuteSolo); + break; + } +} + +void +LaunchKey4::undo_press () +{ + if (shift_pressed) { + redo (); + } else { + undo (); + } +} + +void +LaunchKey4::button_press (int n) +{ + std::shared_ptr ac; + + if (!stripable[n]) { + return; + } + + switch (button_mode) { + case ButtonsSelect: + session->selection().select_stripable_and_maybe_group (stripable[n], SelectionSet); + break; + case ButtonsRecEnable: + ac = stripable[n]->rec_enable_control(); + if (ac) { + ac->set_value (!ac->get_value(), Controllable::NoGroup); + } + break; + } +} + +void +LaunchKey4::button_release (int n) +{ +} + +void +LaunchKey4::solo_changed () +{ + map_mute_solo (); +} + +void +LaunchKey4::mute_changed (uint32_t n) +{ + show_mute (n); +} + +void +LaunchKey4::rec_enable_changed (uint32_t n) +{ + show_rec_enable (n); +} + +void +LaunchKey4::switch_bank (uint32_t base) +{ + stripable_connections.drop_connections (); + + /* work backwards so we can tell if we should actually switch banks */ + + std::shared_ptr s[8]; + + for (int n = 0; n < 8; ++n) { + s[n] = session->get_remote_nth_stripable (base+n, PresentationInfo::Flag (PresentationInfo::Route|PresentationInfo::VCA)); + } + + if (!s[0]) { + /* not even the first stripable exists, do nothing */ + return; + } + + for (int n = 0; n < 8; ++n) { + stripable[n] = s[n]; + } + + /* at least one stripable in this bank */ + + bank_start = base; + + for (int n = 0; n < 8; ++n) { + + if (stripable[n]) { + + /* stripable goes away? refill the bank, starting at the same point */ + + stripable[n]->DropReferences.connect (stripable_connections, invalidator (*this), boost::bind (&LaunchKey4::switch_bank, this, bank_start), this); + stripable[n]->presentation_info().PropertyChanged.connect (stripable_connections, invalidator (*this), boost::bind (&LaunchKey4::stripable_property_change, this, _1, n), this); + stripable[n]->mute_control()->Changed.connect (stripable_connections, invalidator (*this), boost::bind (&LaunchKey4::mute_changed, this, n), this); + std::shared_ptr ac = stripable[n]->rec_enable_control(); + if (ac) { + ac->Changed.connect (stripable_connections, invalidator (*this), boost::bind (&LaunchKey4::rec_enable_changed, this, n), this); + } + } + + /* Set fader "title" fields to show current bank */ + + for (int n = 0; n < 8; ++n) { + if (stripable[n]) { + set_display_target (DisplayTarget (0x5 + n), 0, stripable[n]->name(), true); + } else { + set_display_target (DisplayTarget (0x5 + n), 0, string(), true); + } + } + + if (session->monitor_out()) { + set_display_target (DisplayTarget (0x5 + 8), 0, session->monitor_out()->name(), true); + } else if (session->master_out()) { + set_display_target (DisplayTarget (0x5 + 8), 0, session->master_out()->name(), true); + } + } + + if (button_mode == ButtonsSelect) { + map_selection (); + } else { + map_rec_enable (); + } + + switch (pad_function) { + case Triggers: + map_triggers (); + break; + case MuteSolo: + map_mute_solo (); + break; + default: + break; + } + + if (encoder_mode != EncoderTransport) { + set_encoder_titles_to_route_names (); + } +} + +void +LaunchKey4::stripable_property_change (PropertyChange const& what_changed, uint32_t which) +{ + if (what_changed.contains (Properties::color)) { + show_selection (which); + } + + if (what_changed.contains (Properties::hidden)) { + switch_bank (bank_start); + } + + if (what_changed.contains (Properties::selected)) { + + if (!stripable[which]) { + return; + } + } + +} + +void +LaunchKey4::stripables_added () +{ + /* reload current bank */ + switch_bank (bank_start); +} + +void +LaunchKey4::button_right () +{ + if (pad_function == Triggers) { + switch_bank (bank_start + 1); + scroll_x_offset = bank_start; + } else { + switch_bank (bank_start + 8); + } + std::cerr << "rright to " << bank_start << std::endl; + + if (stripable[0]) { + set_display_target (GlobalTemporaryDisplay, 0, stripable[0]->name(), true); + } +} + +void +LaunchKey4::button_left () +{ + if (pad_function == Triggers) { + if (bank_start > 0) { + switch_bank (bank_start - 1); + scroll_x_offset = bank_start; + } + } else { + if (bank_start > 7) { + switch_bank (bank_start - 8); + } + } + + std::cerr << "left to " << bank_start << std::endl; + + if (stripable[0]) { + set_display_target (GlobalTemporaryDisplay, 0, stripable[0]->name(), true); + } +} + +void +LaunchKey4::toggle_button_mode () +{ + switch (button_mode) { + case ButtonsSelect: + button_mode = ButtonsRecEnable; + map_rec_enable (); + break; + case ButtonsRecEnable: + button_mode = ButtonsSelect; + map_selection (); + break; + } + + MIDI::byte msg[3]; + msg[0] = 0xb0; + msg[1] = Button9; + + if (button_mode == ButtonsSelect) { + msg[2] = 0x3; /* brght white */ + } else { + msg[2] = 0x5; /* red */ + } + + daw_write (msg, 3); +} + +void +LaunchKey4::map_selection () +{ + for (int n = 0; n < 8; ++n) { + show_selection (n); + } +} + +void +LaunchKey4::show_selection (int n) +{ + const int first_button = 0x25; + const int selection_color = 0xd; /* bright yellow */ + + if (!stripable[n]) { + light_button (first_button + n, Off, 0); + } else if (stripable[n]->is_selected()) { + light_button (first_button + n, Solid, selection_color); + } else { + light_button (first_button + n, Solid, find_closest_palette_color (stripable[n]->presentation_info().color ())); + } +} + +void +LaunchKey4::map_mute_solo () +{ + for (int n = 0; n < 8; ++n) { + show_mute (n); + show_solo (n); + } +} + +void +LaunchKey4::show_mute (int n) +{ + if (!stripable[n]) { + return; + } + + std::shared_ptr mc (stripable[n]->mute_control()); + if (!mc) { + return; + } + MIDI::byte msg[3]; + msg[0] = 0x90; + msg[1] = 0x60 + n; + if (mc->muted_by_self()) { + // std::cerr << stripable[n]->name() << " muted by self\n"; + msg[2] = 0xd; /* bright yellow */ + } else if (mc->muted_by_others_soloing() || mc->muted_by_masters()) { + // std::cerr << stripable[n]->name() << " muted by others\n"; + msg[2] = 0x49; /* soft yellow */ + } else { + // std::cerr << stripable[n]->name() << " not muted\n"; + msg[2] = 0x0;; + } + + daw_write (msg, 3); +} + +void +LaunchKey4::show_solo (int n) +{ + if (!stripable[n]) { + return; + } + + std::shared_ptr sc (stripable[n]->solo_control()); + if (!sc) { + return; + } + MIDI::byte msg[3]; + msg[0] = 0x90; + msg[1] = 0x70 + n; + if (sc->soloed_by_self_or_masters()) { + msg[2] = 0x15; /* bright green */ + } else if (sc->soloed_by_others()) { + msg[2] = 0x4b; /* soft green */ + } else { + msg[2] = 0x0; + } + + daw_write (msg, 3); +} + +void +LaunchKey4::light_button (int which, LightingMode mode, int color_index) +{ + MIDI::byte msg[3]; + + msg[1] = which; + + switch (mode) { + case Off: + msg[0] = 0xb0; + msg[2] = 0x0; + break; + + case Solid: + msg[0] = 0xb0; + msg[2] = color_index & 0x7f; + break; + + case Flash: + msg[0] = 0xb1; + msg[2] = color_index & 0x7f; + break; + + case Pulse: + msg[0] = 0xb2; + msg[2] = color_index & 0x7f; + break; + } + + daw_write (msg, 3); +} + + +void +LaunchKey4::light_pad (int pid, LightingMode mode, int color_index) +{ + MIDI::byte msg[3]; + + msg[1] = pid; + + switch (mode) { + case Off: + msg[0] = 0x90; + msg[2] = 0x0; + break; + + case Solid: + msg[0] = 0x90; + msg[2] = color_index & 0x7f; + break; + + case Flash: + msg[0] = 0x91; + msg[2] = color_index & 0x7f; + break; + + case Pulse: + msg[0] = 0x92; + msg[2] = color_index & 0x7f; + break; + } + + daw_write (msg, 3); +} + +void +LaunchKey4::unlight_pad (int pad_id) +{ + light_pad (pad_id, Solid, 0x0); +} + +void +LaunchKey4::set_encoder_bank (int n) +{ + bool light_up_arrow = false; + bool light_down_arrow = false; + + encoder_bank = n; + + /* Ordering: + + 9 + 1 + 2 + */ + + if (encoder_mode == EncoderPlugins) { + + switch (encoder_bank) { + case 0: + if (num_plugin_controls > 8) { + light_down_arrow = true; + } + break; + case 1: + if (num_plugin_controls > 8) { + light_up_arrow = true; + } + if (num_plugin_controls > 16) { + light_down_arrow = true; + } + break; + case 2: + if (num_plugin_controls > 16) { + light_up_arrow = true; + } + break; + } + + } else if (encoder_mode == EncoderMixer) { + + switch (encoder_bank) { + case 0: + light_down_arrow = true; + break; + case 1: + light_down_arrow = true; + light_up_arrow = true; + break; + case 2: + light_up_arrow = true; + break; + default: + return; + } + } + + MIDI::byte msg[6]; + /* Color doesn't really matter, these LEDs are single-color. Just turn + it on or off. + */ + const int color_index = 0x3; + + msg[0] = 0xb0; + msg[1] = 0x33; /* top */ + msg[3] = 0xb0; + msg[4] = 0x34; /* bottom */ + + if (light_up_arrow) { + msg[2] = color_index; + } else { + msg[2] = 0x0; + } + + if (light_down_arrow) { + msg[5] = color_index; + } else { + msg[5] = 0x0; + } + + /* Stupid device doesn't seem to like both messages "at once" */ + daw_write (msg, 3); + daw_write (&msg[3], 3); + + label_encoders (); +} + +void +LaunchKey4::label_encoders () +{ + std::shared_ptr plugin (current_plugin.lock()); + + switch (encoder_mode) { + case EncoderMixer: + case EncoderSendA: + set_encoder_titles_to_route_names (); + switch (encoder_bank) { + case 0: + for (int n = 0; n < 8; ++n) { + set_display_target (DisplayTarget (0x15 + n), 1, "Level", false); + } + set_display_target (GlobalTemporaryDisplay, 0, "Levels", true); + break; + case 1: + for (int n = 0; n < 8; ++n) { + set_display_target (DisplayTarget (0x15 + n), 1, "Pan", false); + } + set_display_target (GlobalTemporaryDisplay, 0, "Panning", true); + break; + default: + break; + } + break; + case EncoderPlugins: + setup_screen_for_encoder_plugins (); + break; + case EncoderTransport: + set_display_target (DisplayTarget (0x15), 1, "Shuttle", true); + set_display_target (DisplayTarget (0x16), 1, "Zoom", true); + set_display_target (DisplayTarget (0x17), 1, "Loop Start", true); + set_display_target (DisplayTarget (0x18), 1, "Loop End", true); + set_display_target (DisplayTarget (0x19), 1, "Jump to Marker", true); + set_display_target (DisplayTarget (0x1a), 1, string(), true); + set_display_target (DisplayTarget (0x1b), 1, string(), true); + set_display_target (DisplayTarget (0x1c), 1, string(), true); + for (int n = 0; n < 8; ++n) { + set_display_target (DisplayTarget (0x15 + n), 0, "Transport", true); + } + set_display_target (GlobalTemporaryDisplay, 0, "Transport", true); + break; + } +} + +void +LaunchKey4::set_encoder_mode (EncoderMode m) +{ + encoder_mode = m; + set_encoder_bank (0); + + /* device firmware reset to continuous controller mode, so switch back + * if (ev->controller_to encoders + */ + + use_encoders (true); + label_encoders (); +} + +void +LaunchKey4::set_encoder_titles_to_route_names () +{ + /* Set encoder "title" fields to show current bank */ + bool first = true; + + for (int n = 0; n < 8; ++n) { + if (stripable[n]) { + set_display_target (DisplayTarget (0x15 + n), 0, stripable[n]->name(), first); + first = false; + } else { + set_display_target (DisplayTarget (0x15 + n), 0, string(), true); + } + } +} + +void +LaunchKey4::in_msecs (int msecs, std::function func) +{ + Glib::RefPtr timeout = Glib::TimeoutSource::create (msecs); // milliseconds + timeout->connect (sigc::bind_return (func, false)); + timeout->attach (main_loop()->get_context()); +} + +void +LaunchKey4::scene_press () +{ + if (shift_pressed) { + trigger_stop_all (true); /* immediate stop */ + } else { + trigger_cue_row (scroll_y_offset); + } +} diff --git a/libs/surfaces/launchkey_4/launchkey_4.h b/libs/surfaces/launchkey_4/launchkey_4.h new file mode 100644 index 0000000000..68d2b412f0 --- /dev/null +++ b/libs/surfaces/launchkey_4/launchkey_4.h @@ -0,0 +1,408 @@ +/* + * Copyright (C) 2016-2018 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __ardour_lk4_h__ +#define __ardour_lk4_h__ + +#include +#include +#include +#include +#include +#include + +#include + +#define ABSTRACT_UI_EXPORTS +#include "pbd/abstract_ui.h" + +#include "midi++/types.h" + +#include "ardour/mode.h" +#include "ardour/types.h" + +#include "control_protocol/control_protocol.h" +#include "control_protocol/types.h" + +#include "gtkmm2ext/colors.h" + +#include "midi_surface/midi_byte_array.h" +#include "midi_surface/midi_surface.h" + +namespace MIDI { + class Parser; + class Port; +} + +namespace ARDOUR { + class AutomationControl; + class Plugin; + class PluginInsert; + class Port; + class MidiBuffer; + class MidiTrack; + class Trigger; +} + +#ifdef LAUNCHPAD_MINI +#define LAUNCHPAD_NAMESPACE LP_MINI +#else +#define LAUNCHPAD_NAMESPACE LP_X +#endif + +namespace ArdourSurface { namespace LAUNCHPAD_NAMESPACE { + +class LK4_GUI; + +class LaunchKey4 : public MIDISurface +{ + public: + /* use hex for these constants, because we'll see them (as note numbers + and CC numbers) in hex within MIDI messages when debugging. + */ + + enum ButtonID { + Button1 = 0x25, + Button2 = 0x26, + Button3 = 0x27, + Button4 = 0x28, + Button5 = 0x29, + Button6 = 0x2a, + Button7 = 0x2b, + Button8 = 0x2c, + Button9 = 0x2d, + + Volume = 0x0b, + Custom1 = 0x0c, + Custom2 = 0x0d, + Custom3 = 0x0e, + Custom4 = 0x0f, + PartA = 0x10, + PartB = 0x11, + Split = 0x12, + Layer = 0x13, + Shift = 0x13, + // Settings = 0x23, + TrackLeft = 0x67, + TrackRight =0x66, + Up = 0x6a, + Down = 0x6b, + CaptureMidi = 0x3, + Undo = 0x4d, + Quantize = 0x4b, + Metronome = 0x4c, + // Stop = 0x34 .. sends Stop + // Play = 0x36 .. sends Play + Play = 0x73, + Stop = 0x74, + RecEnable = 0x75, + Loop = 0x76, + Function = 0x69, + Scene = 0x68, + EncUp = 0x33, + EncDown = 0x44, + }; + + enum KnobID { + Knob1 = 0x55, + Knob2 = 0x56, + Knob3 = 0x57, + Knob4 = 0x58, + Knob5 = 0x59, + Knob6 = 0x5a, + Knob7 = 0x5b, + Knob8 = 0x5c, + }; + + LaunchKey4 (ARDOUR::Session&); + ~LaunchKey4 (); + + static bool available (); + static bool match_usb (uint16_t, uint16_t); + static bool probe (std::string&, std::string&); + + std::string input_port_name () const; + std::string output_port_name () const; + + bool has_editor () const { return true; } + void* get_gui () const; + void tear_down_gui (); + + int set_active (bool yn); + XMLNode& get_state() const; + int set_state (const XMLNode & node, int version); + + private: + enum FaderBank { + VolumeFaders, + PanFaders, + SendAFaders, + SendBFaders, + }; + + struct Pad { + + enum ColorMode { + Static = 0x0, + Flashing = 0x1, + Pulsing = 0x2 + }; + + typedef void (LaunchKey4::*PadMethod)(Pad&, int velocity); + + Pad (int pid, int xx, int yy) + : id (pid) + , x (xx) + , y (yy) + { + } + + Pad () : id (-1), x (-1), y (-1) + { + } + + + int id; + int x; + int y; + + sigc::connection timeout_connection; + }; + + void relax (Pad& p); + void relax (Pad&, int); + + std::set consumed; + + Pad pads[16]; + void build_pad_map (); + + typedef std::map ColorMap; + ColorMap color_map; + void build_color_map (); + int find_closest_palette_color (uint32_t); + + typedef std::map NearestMap; + NearestMap nearest_map; + + int begin_using_device (); + int stop_using_device (); + int device_acquire () { return 0; } + void device_release () { } + void run_event_loop (); + void stop_event_loop (); + + void finish_begin_using_device (); + + void stripable_selection_changed (); + void select_stripable (int col); + std::weak_ptr _current_pad_target; + + void handle_midi_controller_message (MIDI::Parser&, MIDI::EventTwoBytes*); + void handle_midi_controller_message_chnF (MIDI::Parser&, MIDI::EventTwoBytes*); + void handle_midi_note_on_message (MIDI::Parser&, MIDI::EventTwoBytes*); + void handle_midi_note_off_message (MIDI::Parser&, MIDI::EventTwoBytes*); + void handle_midi_sysex (MIDI::Parser&, MIDI::byte *, size_t count); + + MIDI::Port* _daw_in_port; + MIDI::Port* _daw_out_port; + std::shared_ptr _daw_in; + std::shared_ptr _daw_out; + + void port_registration_handler (); + int ports_acquire (); + void ports_release (); + void connect_daw_ports (); + + void daw_write (const MidiByteArray&); + void daw_write (MIDI::byte const *, size_t); + + void reconnect_for_programmer (); + void reconnect_for_session (); + + mutable LK4_GUI* _gui; + void build_gui (); + + void maybe_start_press_timeout (Pad& pad); + void start_press_timeout (Pad& pad); + bool long_press_timeout (int pad_id); + + void button_press (int button); + void button_release (int button); + + void trigger_property_change (PBD::PropertyChange, ARDOUR::Trigger*); + void trigger_pad_light (Pad& pad, std::shared_ptr r, ARDOUR::Trigger* t); + PBD::ScopedConnectionList trigger_connections; + + void display_session_layout (); + void transport_state_changed (); + void record_state_changed (); + + void map_selection (); + void map_mute_solo (); + void map_rec_enable (); + void map_triggers (); + + void map_triggerbox (int col); + + void route_property_change (PBD::PropertyChange const &, int x); + PBD::ScopedConnectionList route_connections; + + void fader_move (int which, int val); + void automation_control_change (int n, std::weak_ptr); + PBD::ScopedConnectionList control_connections; + FaderBank current_fader_bank; + bool revert_layout_on_fader_release; + + void use_encoders (bool); + void encoder (int which, int step); + void knob (int which, int value); + + int scroll_x_offset; + int scroll_y_offset; + + uint16_t device_pid; + + enum DisplayTarget { + StationaryDisplay = 0x20, + GlobalTemporaryDisplay = 0x21, + DAWPadFunctionDisplay = 0x22, + DawDrumrackModeDisplay = 0x23, + MixerPotMode = 0x24, + PluginPotMode = 0x25, + SendPotMode = 0x26, + TransportPotMode = 0x27, + FaderMode = 0x28, + }; + + void select_display_target (DisplayTarget dt); + void set_display_target (DisplayTarget dt, int field, std::string const &, bool display = false); + void configure_display (DisplayTarget dt, int config); + void set_plugin_encoder_name (int encoder, int field, std::string const &); + + void set_daw_mode (bool); + int mode_channel; + + enum PadFunction { + MuteSolo, + Triggers, + }; + + PadFunction pad_function; + void set_pad_function (PadFunction); + void pad_mute_solo (Pad&); + void pad_trigger (Pad&, int velocity); + void pad_release (Pad&); + + bool shift_pressed; + bool layer_pressed; + + void function_press (); + void undo_press (); + void metronome_press (); + void quantize_press (); + void button_left (); + void button_right (); + void button_down (); + void button_up (); + + /* stripables */ + + int32_t bank_start; + PBD::ScopedConnectionList stripable_connections; + std::shared_ptr stripable[8]; + void stripables_added (); + void stripable_property_change (PBD::PropertyChange const& what_changed, uint32_t which); + void switch_bank (uint32_t); + + void solo_changed (); + void mute_changed (uint32_t which); + void rec_enable_changed (uint32_t which); + + enum LightingMode { + Off, + Solid, + Flash, + Pulse, + }; + + void light_button (int which, LightingMode, int color_index); + void light_pad (int pid, LightingMode, int color_index); + + enum ButtonMode { + ButtonsRecEnable, + ButtonsSelect + }; + + ButtonMode button_mode; + + void toggle_button_mode (); + void show_selection (int which); + void show_rec_enable (int which); + void show_mute (int which); + void show_solo (int which); + + enum EncoderMode { + EncoderPlugins, + EncoderMixer, + EncoderSendA, + EncoderTransport + }; + + EncoderMode encoder_mode; + int encoder_bank; + void set_encoder_bank (int); + void set_encoder_mode (EncoderMode); + void set_encoder_titles_to_route_names (); + void setup_screen_for_encoder_plugins (); + void label_encoders (); + void show_encoder_value (int which, std::shared_ptr plugin, int control, std::shared_ptr ac, bool display); + + void encoder_plugin (int which, int step); + void encoder_mixer (int which, int step); + void encoder_pan (int which, int step); + void encoder_level (int which, int step); + void encoder_senda (int which, int step); + void encoder_transport (int which, int step); + + void transport_shuttle (int step); + void zoom (int step); + void loop_start_move (int step); + void loop_end_move (int step); + void jump_to_marker (int step); + + void light_pad (int pad_id, int color_index); + void unlight_pad (int pad_id); + void all_pads (int color_index); + void all_pads_out (); + + void show_scene_ids (); + void scene_press (); + + void in_msecs (int msecs, std::function func); + + std::weak_ptr controls[24]; + std::weak_ptr current_plugin; + void plugin_selected (std::weak_ptr); + uint32_t num_plugin_controls; +}; + + +} } /* namespaces */ + +#endif /* __ardour_lk4_h__ */ diff --git a/libs/surfaces/launchkey_4/launchkey_4_interface.cc b/libs/surfaces/launchkey_4/launchkey_4_interface.cc new file mode 100644 index 0000000000..b958da3d6b --- /dev/null +++ b/libs/surfaces/launchkey_4/launchkey_4_interface.cc @@ -0,0 +1,88 @@ +/* + * 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include + +#include "pbd/error.h" + +#include "ardour/rc_configuration.h" + +#include "control_protocol/control_protocol.h" +#include "launchkey_4.h" + +#ifdef LAUNCHPAD_MINI +#define LAUNCHPAD_NAMESPACE LP_MINI +#else +#define LAUNCHPAD_NAMESPACE LP_X +#endif + +using namespace ARDOUR; +using namespace PBD; +using namespace ArdourSurface::LAUNCHPAD_NAMESPACE; + +static ControlProtocol* +new_lk4 (Session* s) +{ + LaunchKey4 * lk4 = nullptr; + + try { + lk4 = new LaunchKey4 (*s); + /* do not set active here - wait for set_state() */ + } + catch (std::exception & e) { + error << "Error instantiating LaunchKey 4 support: " << e.what() << endmsg; + delete lk4; + lk4 = nullptr; + } + + return lk4; +} + +static void +delete_lk4 (ControlProtocol* cp) +{ + try + { + delete cp; + } + catch ( std::exception & e ) + { + std::cout << "Exception caught trying to finalize LaunchKey 4 support: " << e.what() << std::endl; + } +} + +static bool +probe_lk4_midi_protocol () +{ + std::string i, o; + return LaunchKey4::probe (i, o); +} + + +static ControlProtocolDescriptor lk4_descriptor = { + /* name */ "Novation LaunchKey 4", + /* id */ "uri://ardour.org/surfaces/launchkey4:0", + /* module */ 0, + /* available */ 0, + /* probe_port */ probe_lk4_midi_protocol, + /* match usb */ 0, // LaunchKey4::match_usb, + /* initialize */ new_lk4, + /* destroy */ delete_lk4, +}; + +extern "C" ARDOURSURFACE_API ControlProtocolDescriptor* protocol_descriptor () { return &lk4_descriptor; } diff --git a/libs/surfaces/launchkey_4/wscript b/libs/surfaces/launchkey_4/wscript new file mode 100644 index 0000000000..8397377cd2 --- /dev/null +++ b/libs/surfaces/launchkey_4/wscript @@ -0,0 +1,52 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf +import os + +lpxm_sources = [ + 'launchkey_4.cc', + 'gui.cc', +] + +def options(opt): + pass + +def configure(conf): + autowaf.check_pkg(conf, 'pangomm-1.4', uselib_store='PANGOMM', atleast_version='1.4', mandatory=True) + autowaf.check_pkg(conf, 'cairomm-1.0', uselib_store='CAIROMM', atleast_version='1.8.4', mandatory=True) + + +def build(bld): +# obj = bld(features = 'cxx cxxshlib') +# obj.source = list(lpxm_sources) +# obj.source += [ 'launchkey_4_interface.cc' ] +# obj.defines = [ 'PACKAGE="ardour_launchkey_mini"' ] +# obj.defines += [ 'ARDOURSURFACE_DLL_EXPORTS' ] +# obj.defines += [ 'LAUNCHKEY_MINI' ] +# obj.includes = ['.', ] +# obj.name = 'libardour_launchkey_mini' +# obj.target = 'ardour_launchkey_mini' +# obj.uselib = 'CAIROMM PANGOMM USB SIGCPP XML OSX' +# obj.use = 'libardour libardour_cp libardour_midisurface libgtkmm2ext libpbd libevoral libcanvas libtemporal' +# obj.install_path = os.path.join(bld.env['LIBDIR'], 'surfaces') +# if bld.is_defined('YTK'): +# obj.use += ' libytkmm' +# obj.uselib += ' GLIBMM GIOMM' +# else: +# obj.uselib += ' GTKMM' + + obj = bld(features = 'cxx cxxshlib') + obj.source = list(lpxm_sources) + obj.source += [ 'launchkey_4_interface.cc' ] + obj.defines = [ 'PACKAGE="ardour_launchpad_x"' ] + obj.defines += [ 'ARDOURSURFACE_DLL_EXPORTS' ] + obj.includes = ['.', ] + obj.name = 'libardour_launchkey_4' + obj.target = 'ardour_launchkey_4' + obj.uselib = 'CAIROMM PANGOMM USB SIGCPP XML OSX' + obj.use = 'libardour libardour_cp libardour_midisurface libgtkmm2ext libpbd libevoral libcanvas libtemporal' + obj.install_path = os.path.join(bld.env['LIBDIR'], 'surfaces') + if bld.is_defined('YTK'): + obj.use += ' libytkmm' + obj.uselib += ' GLIBMM GIOMM' + else: + obj.uselib += ' GTKMM' diff --git a/libs/surfaces/mackie/controls.h b/libs/surfaces/mackie/controls.h index a0f4eaf36c..b7a3cdc018 100644 --- a/libs/surfaces/mackie/controls.h +++ b/libs/surfaces/mackie/controls.h @@ -27,8 +27,6 @@ #include #include -#include - #include "pbd/controllable.h" #include "pbd/signals.h" diff --git a/libs/surfaces/mackie/gui.cc b/libs/surfaces/mackie/gui.cc index 9b1cc2fc70..ba824544cf 100644 --- a/libs/surfaces/mackie/gui.cc +++ b/libs/surfaces/mackie/gui.cc @@ -99,7 +99,6 @@ MackieControlProtocolGUI::MackieControlProtocolGUI (MackieControlProtocol& p) , touch_sensitivity_scale (touch_sensitivity_adjustment) , recalibrate_fader_button (_("Recalibrate Faders")) , ipmidi_base_port_adjustment (_cp.ipmidi_base(), 0, 32767, 1, 1000) - , discover_button (_("Discover Mackie Devices")) , _device_dependent_widget (0) , _ignore_profile_changed (false) , ignore_active_change (false) @@ -222,11 +221,6 @@ MackieControlProtocolGUI::MackieControlProtocolGUI (MackieControlProtocol& p) table.attach (recalibrate_fader_button, 1, 2, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); row++; - - table.attach (discover_button, 1, 2, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); - discover_button.signal_clicked().connect (sigc::mem_fun (*this, &MackieControlProtocolGUI::discover_clicked)); - row++; - vector profiles; for (std::map::iterator i = DeviceProfile::device_profiles.begin(); i != DeviceProfile::device_profiles.end(); ++i) { @@ -776,13 +770,6 @@ MackieControlProtocolGUI::ipmidi_spinner_changed () _cp.set_ipmidi_base ((int16_t) lrintf (ipmidi_base_port_adjustment.get_value())); } -void -MackieControlProtocolGUI::discover_clicked () -{ - /* this should help to get things started */ - _cp.ping_devices (); -} - void MackieControlProtocolGUI::recalibrate_faders () { diff --git a/libs/surfaces/mackie/gui.h b/libs/surfaces/mackie/gui.h index e8c2132708..41b77eb75e 100644 --- a/libs/surfaces/mackie/gui.h +++ b/libs/surfaces/mackie/gui.h @@ -122,12 +122,10 @@ class MackieControlProtocolGUI : public Gtk::Notebook Gtk::HScale touch_sensitivity_scale; Gtk::Button recalibrate_fader_button; Gtk::Adjustment ipmidi_base_port_adjustment; - Gtk::Button discover_button; Gtk::HBox hpacker; Gtk::Image image; - void discover_clicked (); void recalibrate_faders (); void toggle_backlight (); void touch_sensitive_change (); diff --git a/libs/surfaces/mackie/mackie_control_protocol.cc b/libs/surfaces/mackie/mackie_control_protocol.cc index b40f9c0423..7e34712c1c 100644 --- a/libs/surfaces/mackie/mackie_control_protocol.cc +++ b/libs/surfaces/mackie/mackie_control_protocol.cc @@ -202,21 +202,6 @@ MackieControlProtocol::thread_init () set_thread_priority (); } -void -MackieControlProtocol::ping_devices () -{ - /* should not be called if surfaces are not connected, but will not - * malfunction if it is. - */ - - { - Glib::Threads::Mutex::Lock lm (surfaces_lock); - for (auto const& si : surfaces) { - si->connected (); - } - } -} - // go to the previous track. void MackieControlProtocol::prev_track() diff --git a/libs/surfaces/mackie/mackie_control_protocol.h b/libs/surfaces/mackie/mackie_control_protocol.h index c6ab3745d5..9a8011c68b 100644 --- a/libs/surfaces/mackie/mackie_control_protocol.h +++ b/libs/surfaces/mackie/mackie_control_protocol.h @@ -31,8 +31,6 @@ #include #include -#include - #define ABSTRACT_UI_EXPORTS #include "pbd/abstract_ui.h" #include "midi++/types.h" @@ -234,8 +232,6 @@ class MackieControlProtocol int16_t ipmidi_base() const { return _ipmidi_base; } void set_ipmidi_base (int16_t); - void ping_devices (); - protected: // shut down the surface void close(); diff --git a/libs/surfaces/mackie/subview.h b/libs/surfaces/mackie/subview.h index cc31042717..5a5135cd82 100644 --- a/libs/surfaces/mackie/subview.h +++ b/libs/surfaces/mackie/subview.h @@ -20,8 +20,6 @@ #ifndef __ardour_mackie_control_protocol_subview_h__ #define __ardour_mackie_control_protocol_subview_h__ -#include - #include "pbd/signals.h" #include "ardour/types.h" diff --git a/libs/surfaces/osc/osc.cc b/libs/surfaces/osc/osc.cc index 11e7c5fae9..f1b6a8f9d6 100644 --- a/libs/surfaces/osc/osc.cc +++ b/libs/surfaces/osc/osc.cc @@ -1873,14 +1873,15 @@ OSC::set_surface (uint32_t b_size, uint32_t strips, uint32_t fb, uint32_t gm, ui s->bank_size = b_size; s->strip_types = strips; s->feedback = fb; + if (s->sel_obs) { + s->sel_obs->set_feedback(fb); + } s->gainmode = gm; if (s->strip_types[10]) { s->usegroup = PBD::Controllable::UseGroup; } else { s->usegroup = PBD::Controllable::NoGroup; } - s->send_page_size = se_size; - s->plug_page_size = pi_size; if (s->temp_mode) { s->temp_mode = TempOff; } @@ -1953,6 +1954,9 @@ OSC::set_surface_feedback (uint32_t fb, lo_message msg) } OSCSurface *s = get_surface(get_address (msg), true); s->feedback = fb; + if (s->sel_obs) { + s->sel_obs->set_feedback(fb); + } strip_feedback (s, true); global_feedback (s); diff --git a/libs/surfaces/osc/osc_select_observer.cc b/libs/surfaces/osc/osc_select_observer.cc index a965b2f322..5efb441ebc 100644 --- a/libs/surfaces/osc/osc_select_observer.cc +++ b/libs/surfaces/osc/osc_select_observer.cc @@ -68,8 +68,7 @@ OSCSelectObserver::OSCSelectObserver (OSC& o, ARDOUR::Session& s, ArdourSurface: session = &s; addr = lo_address_new_from_url (sur->remote_url.c_str()); gainmode = sur->gainmode; - feedback = sur->feedback; - in_line = feedback[2]; + set_feedback(sur->feedback); send_page_size = sur->send_page_size; send_size = send_page_size; send_page = sur->send_page; @@ -93,6 +92,15 @@ OSCSelectObserver::~OSCSelectObserver () lo_address_free (addr); } +void +OSCSelectObserver::set_feedback (std::bitset<32> fb) +{ + feedback = fb; + in_line = fb[2]; + // No explicit refresh, callers should take care of that to + // prevent duplicate refreshing +} + void OSCSelectObserver::no_strip () { diff --git a/libs/surfaces/osc/osc_select_observer.h b/libs/surfaces/osc/osc_select_observer.h index 171a985ddc..0206bec2c6 100644 --- a/libs/surfaces/osc/osc_select_observer.h +++ b/libs/surfaces/osc/osc_select_observer.h @@ -54,6 +54,7 @@ class OSCSelectObserver void set_plugin_id (int id, uint32_t page); void set_plugin_page (uint32_t page); void set_plugin_size (uint32_t size); + void set_feedback (std::bitset<32> fb); private: std::shared_ptr _strip; diff --git a/libs/surfaces/us2400/controls.h b/libs/surfaces/us2400/controls.h index 73b2b187f7..21063ac25b 100644 --- a/libs/surfaces/us2400/controls.h +++ b/libs/surfaces/us2400/controls.h @@ -24,8 +24,6 @@ #include #include -#include - #include "pbd/controllable.h" #include "pbd/signals.h" diff --git a/libs/surfaces/us2400/us2400_control_protocol.h b/libs/surfaces/us2400/us2400_control_protocol.h index 4c576c4469..e45465eb91 100644 --- a/libs/surfaces/us2400/us2400_control_protocol.h +++ b/libs/surfaces/us2400/us2400_control_protocol.h @@ -26,8 +26,6 @@ #include #include -#include - #define ABSTRACT_UI_EXPORTS #include "pbd/abstract_ui.h" #include "midi++/types.h" diff --git a/libs/surfaces/websockets/state.cc b/libs/surfaces/websockets/state.cc index cc18886af0..ec58f46bdd 100644 --- a/libs/surfaces/websockets/state.cc +++ b/libs/surfaces/websockets/state.cc @@ -16,7 +16,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include +#include #include #include "state.h" diff --git a/libs/surfaces/wscript b/libs/surfaces/wscript index b0ca408cdb..5a5fe03564 100644 --- a/libs/surfaces/wscript +++ b/libs/surfaces/wscript @@ -22,7 +22,8 @@ children = [ 'osc', 'console1', 'launchpad_pro', - 'launchpad_x' + 'launchpad_x', + 'launchkey_4' ] def options(opt): @@ -81,6 +82,7 @@ def build(bld): bld.recurse('console1') bld.recurse('launchpad_pro') bld.recurse('launchpad_x') + bld.recurse('launchkey_4') if bld.is_defined('BUILD_WIIMOTE'): bld.recurse('wiimote') diff --git a/libs/tk/ydk/gdkglobals.c b/libs/tk/ydk/gdkglobals.c index 4c3ad80956..03b807bc54 100644 --- a/libs/tk/ydk/gdkglobals.c +++ b/libs/tk/ydk/gdkglobals.c @@ -40,6 +40,7 @@ gchar *_gdk_display_name = NULL; gint _gdk_screen_number = -1; gchar *_gdk_display_arg_name = NULL; gboolean _gdk_native_windows = FALSE; +void (*_gdk_modal_notify)(GdkWindow*,gboolean) = 0; GSList *_gdk_displays = NULL; diff --git a/libs/tk/ydk/gdkwindow.c b/libs/tk/ydk/gdkwindow.c index 4fd12edaa0..ce1370aa40 100644 --- a/libs/tk/ydk/gdkwindow.c +++ b/libs/tk/ydk/gdkwindow.c @@ -159,7 +159,6 @@ typedef struct { int dx, dy; /* The amount that the source was moved to reach dest_region */ } GdkWindowRegionMove; - /* Global info */ static GdkGC *gdk_window_create_gc (GdkDrawable *drawable, @@ -11408,6 +11407,11 @@ gdk_window_get_height (GdkWindow *window) return height; } +void +gdk_window_set_modal_notify (void (*modal_notify)(GdkWindow*,gboolean)) +{ + _gdk_modal_notify = modal_notify; +} #define __GDK_WINDOW_C__ #include "gdkaliasdef.c" diff --git a/libs/tk/ydk/quartz/gdkwindow-quartz.c b/libs/tk/ydk/quartz/gdkwindow-quartz.c index 4cab5aab9a..facb9b1e4b 100644 --- a/libs/tk/ydk/quartz/gdkwindow-quartz.c +++ b/libs/tk/ydk/quartz/gdkwindow-quartz.c @@ -23,6 +23,7 @@ #include #include "gdk.h" +#include "gdkinternals.h" #include "gdkwindowimpl.h" #include "gdkprivate-quartz.h" #include "gdkscreen-quartz.h" @@ -196,7 +197,15 @@ gdk_window_impl_quartz_finalize (GObject *object) { GdkWindowImplQuartz *impl = GDK_WINDOW_IMPL_QUARTZ (object); - check_grab_destroy (GDK_DRAWABLE_IMPL_QUARTZ (object)->wrapper); + GdkWindow *window = GDK_DRAWABLE_IMPL_QUARTZ (object)->wrapper; + GdkWindowObject *private = (GdkWindowObject*) window; + + check_grab_destroy (window); + + if (private->modal_hint && _gdk_modal_notify) + { + _gdk_modal_notify (GDK_DRAWABLE_IMPL_QUARTZ (object)->wrapper, false); + } if (impl->paint_clip_region) gdk_region_destroy (impl->paint_clip_region); @@ -2385,11 +2394,19 @@ void gdk_window_set_modal_hint (GdkWindow *window, gboolean modal) { + GdkWindowObject *private; + if (GDK_WINDOW_DESTROYED (window) || !WINDOW_IS_TOPLEVEL (window)) return; - /* FIXME: Implement */ + private = (GdkWindowObject*) window; + + if (_gdk_modal_notify && private->modal_hint != modal) { + _gdk_modal_notify (window, modal); + } + + private->modal_hint = modal; } void diff --git a/libs/tk/ydk/ydk/gdk/gdkinternals.h b/libs/tk/ydk/ydk/gdk/gdkinternals.h index 289878b1d4..895b2587ce 100644 --- a/libs/tk/ydk/ydk/gdk/gdkinternals.h +++ b/libs/tk/ydk/ydk/gdk/gdkinternals.h @@ -709,6 +709,8 @@ void _gdk_offscreen_window_new (GdkWindow *window, void _gdk_image_exit (void); void _gdk_windowing_exit (void); +extern void (*_gdk_modal_notify)(GdkWindow*, gboolean); + G_END_DECLS #endif /* __GDK_INTERNALS_H__ */ diff --git a/libs/tk/ydk/ydk/gdk/gdkwindow.h b/libs/tk/ydk/ydk/gdk/gdkwindow.h index 572797b955..d060ba0e0a 100644 --- a/libs/tk/ydk/ydk/gdk/gdkwindow.h +++ b/libs/tk/ydk/ydk/gdk/gdkwindow.h @@ -406,7 +406,7 @@ void gdk_window_move_region (GdkWindow *window, gint dy); gboolean gdk_window_ensure_native (GdkWindow *window); -/* +/* * This allows for making shaped (partially transparent) windows * - cool feature, needed for Drag and Drag for example. * The shape_mask can be the mask @@ -508,6 +508,9 @@ gboolean gdk_window_get_modal_hint (GdkWindow *window); void gdk_window_set_modal_hint (GdkWindow *window, gboolean modal); +typedef void (*GdkWindowModalNotify)(GdkWindow*,gboolean); +void gdk_window_set_modal_notify (GdkWindowModalNotify); + void gdk_window_set_skip_taskbar_hint (GdkWindow *window, gboolean skips_taskbar); void gdk_window_set_skip_pager_hint (GdkWindow *window, diff --git a/libs/tk/ytk/gtkfilechooserdefault.c b/libs/tk/ytk/gtkfilechooserdefault.c index ce9802cf62..b0435f658d 100644 --- a/libs/tk/ytk/gtkfilechooserdefault.c +++ b/libs/tk/ytk/gtkfilechooserdefault.c @@ -321,6 +321,8 @@ static void gtk_file_chooser_default_get_default_size (GtkFileCh static gboolean gtk_file_chooser_default_should_respond (GtkFileChooserEmbed *chooser_embed); static void gtk_file_chooser_default_initial_focus (GtkFileChooserEmbed *chooser_embed); +static void gtk_file_chooser_activate_location_entry (GtkWidget *item, gpointer user_data); + static void add_selection_to_recent_list (GtkFileChooserDefault *impl); static void location_popup_handler (GtkFileChooserDefault *impl, @@ -4442,7 +4444,10 @@ location_entry_create (GtkFileChooserDefault *impl) _gtk_file_chooser_entry_set_local_only (GTK_FILE_CHOOSER_ENTRY (impl->location_entry), impl->local_only); _gtk_file_chooser_entry_set_action (GTK_FILE_CHOOSER_ENTRY (impl->location_entry), impl->action); gtk_entry_set_width_chars (GTK_ENTRY (impl->location_entry), 45); - gtk_entry_set_activates_default (GTK_ENTRY (impl->location_entry), TRUE); + if (impl->action == GTK_FILE_CHOOSER_ACTION_OPEN) + g_signal_connect (impl->location_entry, "activate", G_CALLBACK (gtk_file_chooser_activate_location_entry), impl); + else + gtk_entry_set_activates_default (GTK_ENTRY (impl->location_entry), TRUE); } /* Creates the widgets specific to Save mode */ @@ -8520,7 +8525,10 @@ file_exists_get_info_cb (GCancellable *cancellable, else { if (file_exists) - request_response_and_add_to_recent_list (data->impl); /* user typed an existing filename; we are done */ + { + gtk_file_chooser_default_select_file (GTK_FILE_CHOOSER (data->impl), data->file, NULL); + request_response_and_add_to_recent_list (data->impl); /* user typed an existing filename; we are done */ + } else needs_parent_check = TRUE; /* file doesn't exist; see if its parent exists */ } @@ -8917,6 +8925,68 @@ gtk_file_chooser_default_should_respond (GtkFileChooserEmbed *chooser_embed) return retval; } +static void +gtk_file_chooser_activate_location_entry (GtkWidget *item, gpointer user_data) +{ + /* This is similar to gtk_file_chooser_default_should_respond, + * and used in case the default handler is not activated by + * the location entry. + */ + GtkFileChooserDefault *impl = GTK_FILE_CHOOSER_DEFAULT (user_data); + GFile *file; + gboolean is_well_formed, is_empty, is_file_part_empty; + gboolean is_folder; + GtkFileChooserEntry *entry; + GError *error; + + g_assert (impl->action == GTK_FILE_CHOOSER_ACTION_OPEN); + + entry = GTK_FILE_CHOOSER_ENTRY (impl->location_entry); + check_save_entry (impl, &file, &is_well_formed, &is_empty, &is_file_part_empty, &is_folder); + + if (!is_well_formed || is_empty) + return; + + g_assert (file != NULL); + + error = NULL; + if (is_folder) + { + change_folder_and_display_error (impl, file, TRUE); + } + else + { + struct FileExistsData *data; + + /* We need to check whether file exists and whether it is a folder - + * the GtkFileChooserEntry *does* report is_folder==FALSE as a false + * negative (it doesn't know yet if your last path component is a + * folder). + */ + + data = g_new0 (struct FileExistsData, 1); + data->impl = g_object_ref (impl); + data->file = g_object_ref (file); + data->parent_file = _gtk_file_chooser_entry_get_current_folder (entry); + + if (impl->file_exists_get_info_cancellable) + g_cancellable_cancel (impl->file_exists_get_info_cancellable); + + impl->file_exists_get_info_cancellable = + _gtk_file_system_get_info (impl->file_system, file, + "standard::type", + file_exists_get_info_cb, + data); + + set_busy_cursor (impl, TRUE); + + if (error != NULL) + g_error_free (error); + } + + g_object_unref (file); +} + /* Implementation for GtkFileChooserEmbed::initial_focus() */ static void gtk_file_chooser_default_initial_focus (GtkFileChooserEmbed *chooser_embed) diff --git a/libs/vamp-pyin/LocalCandidatePYIN.cpp b/libs/vamp-pyin/LocalCandidatePYIN.cpp index e90e819191..f724578221 100644 --- a/libs/vamp-pyin/LocalCandidatePYIN.cpp +++ b/libs/vamp-pyin/LocalCandidatePYIN.cpp @@ -27,8 +27,6 @@ #include #include -#include - using std::string; using std::vector; using std::map; diff --git a/libs/vamp-pyin/MonoNoteHMM.cpp b/libs/vamp-pyin/MonoNoteHMM.cpp index 202467064e..9d9be8c03b 100644 --- a/libs/vamp-pyin/MonoNoteHMM.cpp +++ b/libs/vamp-pyin/MonoNoteHMM.cpp @@ -13,8 +13,6 @@ #include "MonoNoteHMM.h" -#include - #include #include diff --git a/libs/vamp-pyin/MonoPitchHMM.cpp b/libs/vamp-pyin/MonoPitchHMM.cpp index b5ab2984f8..97f1ef8147 100644 --- a/libs/vamp-pyin/MonoPitchHMM.cpp +++ b/libs/vamp-pyin/MonoPitchHMM.cpp @@ -13,8 +13,6 @@ #include "MonoPitchHMM.h" -#include - #include #include diff --git a/libs/vamp-pyin/YinUtil.cpp b/libs/vamp-pyin/YinUtil.cpp index b6d5a10fd8..16227bb31c 100644 --- a/libs/vamp-pyin/YinUtil.cpp +++ b/libs/vamp-pyin/YinUtil.cpp @@ -19,8 +19,6 @@ #include #include -#include - void YinUtil::slowDifference(const double *in, double *yinBuffer, const size_t yinBufferSize) { diff --git a/libs/widgets/ardour_icon.cc b/libs/widgets/ardour_icon.cc index e66b5697bb..0e5343c406 100644 --- a/libs/widgets/ardour_icon.cc +++ b/libs/widgets/ardour_icon.cc @@ -1344,6 +1344,45 @@ icon_latency_clock (cairo_t* cr, const int width, const int height, const uint32 cairo_fill (cr); } +static void +icon_tailtime_clock (cairo_t* cr, const int width, const int height, const uint32_t fg_color) +{ + const double x = width * .5; + const double y = height * .5; + const double d = std::min (x, y) * .4; + const double r = std::min (x, y) * .66; + + const double lw = DEFAULT_LINE_WIDTH; + const double lc = fmod (lw * .5, 1.0); + const double x0 = rint (x) - lc; + const double yl = rint (y) - lc; + + cairo_move_to (cr, x0, y - d); + cairo_line_to (cr, x0, y - r); + VECTORICONSTROKE (lw, fg_color); + + cairo_move_to (cr, x0, y + d); + cairo_line_to (cr, x0, y + r); + VECTORICONSTROKE (lw, fg_color); + + cairo_move_to (cr, x - d , yl); + cairo_line_to (cr, x - r, yl); + VECTORICONSTROKE (lw, fg_color); + + cairo_move_to (cr, x + d , yl); + cairo_line_to (cr, x + r, yl); + VECTORICONSTROKE (lw, fg_color); + + cairo_move_to (cr, x , y); + cairo_close_path (cr); + VECTORICONSTROKE (lw, fg_color); + + cairo_arc (cr, x, y, r, 0, 2 * M_PI); + VECTORICONSTROKE (lw, fg_color); + + //cairo_fill (cr); +} + static void icon_file_folder (cairo_t* cr, const int width, const int height, const uint32_t fg_color) { @@ -1689,6 +1728,9 @@ ArdourWidgets::ArdourIcon::render (cairo_t* cr case TrackWaveform: icon_waveform (cr, width, height, fg_color); break; + case TailTimeClock: + icon_tailtime_clock (cr, width, height, fg_color); + break; case NoIcon: rv = false; break; diff --git a/libs/widgets/widgets/ardour_icon.h b/libs/widgets/widgets/ardour_icon.h index 7a04c4593c..d58062f51f 100644 --- a/libs/widgets/widgets/ardour_icon.h +++ b/libs/widgets/widgets/ardour_icon.h @@ -77,6 +77,7 @@ namespace ArdourWidgets { namespace ArdourIcon { Mixer, Meters, TrackWaveform, + TailTimeClock, NoIcon //< Last }; diff --git a/share/midi_maps/AKAI_MPKmini_mk3.map b/share/midi_maps/AKAI_MPKmini_mk3.map new file mode 100644 index 0000000000..cb152382ae --- /dev/null +++ b/share/midi_maps/AKAI_MPKmini_mk3.map @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/share/midi_maps/DDX3216.map b/share/midi_maps/DDX3216.map index 1e39dbdf9b..308ddf4124 100644 --- a/share/midi_maps/DDX3216.map +++ b/share/midi_maps/DDX3216.map @@ -1,5 +1,5 @@ - + diff --git a/share/midi_maps/xboard-61.map b/share/midi_maps/xboard-61.map index f990460e4a..4d9e330226 100644 --- a/share/midi_maps/xboard-61.map +++ b/share/midi_maps/xboard-61.map @@ -1,5 +1,5 @@ - +