From 1b6868f69cbcad71a83a1da16e2ea87730b045c5 Mon Sep 17 00:00:00 2001 From: Alexandre Prokoudine Date: Wed, 14 Aug 2024 20:18:00 +0200 Subject: [PATCH 001/111] Rename the DDX3216 map to clarify who the vendor is --- share/midi_maps/DDX3216.map | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 @@ - + From 803ff507ab0a119b6a8a14c00d72a1a17ab9eadd Mon Sep 17 00:00:00 2001 From: Alexandre Prokoudine Date: Wed, 14 Aug 2024 20:42:36 +0200 Subject: [PATCH 002/111] Add MIDI map for Akai MPK mini mk3, contributed by Peter Zenk --- share/midi_maps/AKAI_MPKmini_mk3.map | 50 ++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 share/midi_maps/AKAI_MPKmini_mk3.map 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 8a8ae7069ef8909ed79c54df44eb30784da9aecb Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Wed, 14 Aug 2024 21:50:04 +0200 Subject: [PATCH 003/111] Fix edge-case pre-roll required for looping This is mainly for the benefit of Mixbus, where input_latency is not propagated upwards from the master bus (no direct connection). In Ardour's case _worst_input_latency >= _worst_route_latency unless a given track with latent plugin is not connected. Previously looping became out of sync (normal playback was not affected) when a track had a latent plugin. --- libs/ardour/session_transport.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/ardour/session_transport.cc b/libs/ardour/session_transport.cc index a965c127cf..354f38e7a4 100644 --- a/libs/ardour/session_transport.cc +++ b/libs/ardour/session_transport.cc @@ -1728,7 +1728,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 From d4b71a777759ef8915098475f95f831282535648 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Thu, 15 Aug 2024 14:49:41 +0200 Subject: [PATCH 004/111] VST3: correctly set offline processing --- libs/ardour/vst3_plugin.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/ardour/vst3_plugin.cc b/libs/ardour/vst3_plugin.cc index a7629b185c..7912c0f808 100644 --- a/libs/ardour/vst3_plugin.cc +++ b/libs/ardour/vst3_plugin.cc @@ -1970,7 +1970,11 @@ VST3PI::set_owner (SessionObject* o) void VST3PI::set_non_realtime (bool yn) { + if (_process_offline == yn) { + return; + } _process_offline = yn; + update_processor (); } int32 From 2411a6a62d0d52825bd66ffb6aa0b10a90708078 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Fri, 16 Aug 2024 16:52:20 +0200 Subject: [PATCH 005/111] Windows: unregister fonts on crash This allows to cleanly un/reinstall Ardour after a crash. Previously registered fonts remained in-use, and uninstall could not remove the files. --- gtk2_ardour/bundle_env_mingw.cc | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/gtk2_ardour/bundle_env_mingw.cc b/gtk2_ardour/bundle_env_mingw.cc index f540fe9e1b..bb2736bead 100644 --- a/gtk2_ardour/bundle_env_mingw.cc +++ b/gtk2_ardour/bundle_env_mingw.cc @@ -110,24 +110,31 @@ 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()); + printf ("unload_custom_fonts\n"); + 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 +151,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 +166,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); } } From d0994dbfcc8bfac2f7e16ad9bd11b40c7909ec67 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Fri, 16 Aug 2024 17:25:19 +0200 Subject: [PATCH 006/111] Remove debug message --- gtk2_ardour/bundle_env_mingw.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/gtk2_ardour/bundle_env_mingw.cc b/gtk2_ardour/bundle_env_mingw.cc index bb2736bead..d4b3fbe8b9 100644 --- a/gtk2_ardour/bundle_env_mingw.cc +++ b/gtk2_ardour/bundle_env_mingw.cc @@ -116,7 +116,6 @@ static std::string ardour_sans_file; static __cdecl void unload_custom_fonts() { - printf ("unload_custom_fonts\n"); if (!ardour_mono_file.empty ()) { RemoveFontResource(ardour_mono_file.c_str()); } From 95d1ae595a825129a1c0b613e38e46f551b3fef2 Mon Sep 17 00:00:00 2001 From: Alexandre Prokoudine Date: Fri, 16 Aug 2024 18:16:15 +0200 Subject: [PATCH 007/111] Normalize the name of the E-MU Xboard 61 map --- share/midi_maps/xboard-61.map | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 @@ - + >--------\n"); + DEBUG_TRACE (DEBUG::RegionFx, DEBUG_STR(a).str()); } #endif From d8725ff3c8a984fb40ea787269e8d60bd71c4ae5 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Sat, 17 Aug 2024 05:29:04 +0200 Subject: [PATCH 016/111] RegionFX: fix crash when custom GUI thread sends change requests This can happen with VST2s (e.g gvst) and some JUCE based plugins. Previously that lead to a "programming error: no per-thread pool" when the DR queues a overwrite buffer session-event. --- libs/ardour/audioregion.cc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/ardour/audioregion.cc b/libs/ardour/audioregion.cc index 93262909c2..ddcaacadd5 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" @@ -2350,7 +2351,12 @@ AudioRegion::_add_plugin (std::shared_ptr rfx, std::shared_ptrdelegate (boost::bind (&AudioRegion::send_change, this, PropertyChange (Properties::region_fx))); + } } }); if (!ac->alist ()) { From 8f5d6295b3817ffb256d9663c9068fcaa1e3ac25 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Sun, 18 Aug 2024 00:41:33 +0200 Subject: [PATCH 017/111] RegionFX: fix plugin cycle times --- libs/ardour/audioregion.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/ardour/audioregion.cc b/libs/ardour/audioregion.cc index ddcaacadd5..5b81448a72 100644 --- a/libs/ardour/audioregion.cc +++ b/libs/ardour/audioregion.cc @@ -2478,7 +2478,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); From 4bcf1d31c6bbf1918598b1edcc431f09eafd871b Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Tue, 13 Aug 2024 19:43:13 +0200 Subject: [PATCH 018/111] RegionFX: implement pre/post region-fade FX --- libs/ardour/ardour/audioregion.h | 5 ++ libs/ardour/audioregion.cc | 134 ++++++++++++++++++++++++------- libs/ardour/luabindings.cc | 2 + 3 files changed, 111 insertions(+), 30 deletions(-) diff --git a/libs/ardour/ardour/audioregion.h b/libs/ardour/ardour/audioregion.h index 92a7b8b284..c2de836097 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,6 +164,8 @@ 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); @@ -220,6 +224,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; diff --git a/libs/ardour/audioregion.cc b/libs/ardour/audioregion.cc index 5b81448a72..ab7bf64766 100644 --- a/libs/ardour/audioregion.cc +++ b/libs/ardour/audioregion.cc @@ -82,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; @@ -176,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")); @@ -200,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); @@ -214,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))) \ @@ -226,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()))) \ @@ -499,6 +505,22 @@ 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 */ + } +} + /** @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. @@ -585,6 +607,11 @@ AudioRegion::read_at (Sample* buf, return 0; } + std::shared_ptr pl (playlist()); + if (!pl){ + return 0; + } + /* WORK OUT WHERE TO GET DATA FROM */ samplecnt_t to_read; @@ -605,29 +632,28 @@ AudioRegion::read_at (Sample* buf, return 0; /* read nothing */ } - std::shared_ptr pl (playlist()); - if (!pl){ - return 0; - } - /* COMPUTE DETAILS OF ANY FADES INVOLVED IN THIS READ */ + /* 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(); @@ -639,15 +665,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 @@ -655,18 +679,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; } } @@ -760,6 +785,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. */ @@ -838,9 +906,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; } } @@ -879,11 +951,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; } } diff --git a/libs/ardour/luabindings.cc b/libs/ardour/luabindings.cc index c7768296c6..c0e558c763 100644 --- a/libs/ardour/luabindings.cc +++ b/libs/ardour/luabindings.cc @@ -1692,7 +1692,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) From c2169d6d5111e517b4fe99497cb72f69e149002d Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Tue, 13 Aug 2024 19:43:36 +0200 Subject: [PATCH 019/111] Add GUI to toggle pre/post region fade FX --- gtk2_ardour/audio_region_editor.cc | 20 ++++++++++++++++++++ gtk2_ardour/audio_region_editor.h | 5 +++++ gtk2_ardour/region_editor.cc | 11 ++++++++--- gtk2_ardour/region_editor.h | 2 +- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/gtk2_ardour/audio_region_editor.cc b/gtk2_ardour/audio_region_editor.cc index 613a395149..aff876dc3b 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,6 +95,7 @@ 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; _region_line_label.set_name ("AudioRegionEditorLabel"); @@ -105,10 +107,12 @@ AudioRegionEditor::AudioRegionEditor (Session* s, AudioRegionView* arv) ++_table_row; 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))); @@ -141,6 +145,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 (); @@ -178,6 +186,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 () { 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/region_editor.cc b/gtk2_ardour/region_editor.cc index bb7a2b5973..6fc4f04d38 100644 --- a/gtk2_ardour/region_editor.cc +++ b/gtk2_ardour/region_editor.cc @@ -598,7 +598,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 ()); } @@ -936,7 +937,7 @@ 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) { @@ -947,7 +948,11 @@ RegionEditor::RegionFxEntry::RegionFxEntry (std::shared_ptr rfx) _fx_btn.set_fallthrough_to_parent (true); _fx_btn.set_text (name ()); _fx_btn.set_active (true); - _fx_btn.set_name ("processor postfader"); + if (pre) { + _fx_btn.set_name ("processor prefader"); + } else { + _fx_btn.set_name ("processor postfader"); + } 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 ())); diff --git a/gtk2_ardour/region_editor.h b/gtk2_ardour/region_editor.h index ecc5f9f6b1..9d7d8676ae 100644 --- a/gtk2_ardour/region_editor.h +++ b/gtk2_ardour/region_editor.h @@ -75,7 +75,7 @@ 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; } From c16e31012b11eb5117fe6f0ff7c918a85b53528c Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Wed, 14 Aug 2024 04:13:25 +0200 Subject: [PATCH 020/111] RegionFX: include plugin tail with pre-fade Fx --- libs/ardour/ardour/audioregion.h | 4 ++ libs/ardour/ardour/playlist.h | 2 +- libs/ardour/ardour/region.h | 8 ++- libs/ardour/audio_playlist.cc | 19 +++-- libs/ardour/audioregion.cc | 119 +++++++++++++++++++++++-------- libs/ardour/playlist.cc | 6 +- libs/ardour/region.cc | 19 +++++ 7 files changed, 136 insertions(+), 41 deletions(-) diff --git a/libs/ardour/ardour/audioregion.h b/libs/ardour/ardour/audioregion.h index c2de836097..07a6e99dec 100644 --- a/libs/ardour/ardour/audioregion.h +++ b/libs/ardour/ardour/audioregion.h @@ -171,6 +171,8 @@ class LIBARDOUR_API AudioRegion : public Region, public AudioReadable bool remove_plugin (std::shared_ptr); void reorder_plugins (RegionFxList const&); + timecnt_t tail () const; + /* automation */ std::shared_ptr @@ -264,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; @@ -274,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/playlist.h b/libs/ardour/ardour/playlist.h index 8405fd197c..4533a8336c 100644 --- a/libs/ardour/ardour/playlist.h +++ b/libs/ardour/ardour/playlist.h @@ -402,7 +402,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/region.h b/libs/ardour/ardour/region.h index a66dc036f0..ea842a3f40 100644 --- a/libs/ardour/ardour/region.h +++ b/libs/ardour/ardour/region.h @@ -145,6 +145,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 +303,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 +566,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 +587,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/audio_playlist.cc b/libs/ardour/audio_playlist.cc index d751152d4c..67b391c384 100644 --- a/libs/ardour/audio_playlist.cc +++ b/libs/ardour/audio_playlist.cc @@ -176,7 +176,7 @@ 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)); samplecnt_t const scnt (cnt.samples ()); @@ -204,7 +204,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 +236,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 +256,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 +285,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/audioregion.cc b/libs/ardour/audioregion.cc index ab7bf64766..4ef78b659e 100644 --- a/libs/ardour/audioregion.cc +++ b/libs/ardour/audioregion.cc @@ -254,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; } @@ -347,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; @@ -375,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; @@ -401,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; @@ -521,6 +525,16 @@ AudioRegion::set_fade_before_fx (bool yn) } } +timecnt_t +AudioRegion::tail () const +{ + if (_fade_before_fx && has_region_fx ()) { + return timecnt_t (_session.sample_rate ()); // TODO use plugin API + } 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. @@ -614,24 +628,33 @@ AudioRegion::read_at (Sample* buf, /* WORK OUT WHERE TO GET DATA FROM */ - samplecnt_t to_read; const samplepos_t psamples = position().samples(); const samplecnt_t lsamples = _length.val().samples(); + const samplecnt_t tsamples = tail ().samples (); assert (pos >= psamples); - sampleoffset_t const internal_offset = pos - psamples; + sampleoffset_t internal_offset = pos - psamples; + sampleoffset_t suffix = 0; - if (internal_offset >= lsamples) { + 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 ((to_read = min (cnt, 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 * @@ -699,16 +722,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); @@ -716,24 +740,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) { @@ -742,13 +763,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); @@ -756,6 +784,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. @@ -838,26 +867,27 @@ 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); + const_cast(this)->apply_region_fx (_readcache, offset + suffix, offset + suffix + n_proc, n_proc); } /* for mono regions without plugins, mixdown_buffer is valid as-is */ if (n_chn > 1 || have_fx) { /* copy data for current channel */ if (chan_n < n_channels()) { - copy_vector (mixdown_buffer, _readcache.get_audio (chan_n).data (), to_read); + 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); + copy_vector (mixdown_buffer, _readcache.get_audio (chan_n).data (), to_read + n_tail); } else { - memset (mixdown_buffer, 0, sizeof (Sample) * to_read); + 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 (); } @@ -974,8 +1004,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 @@ -2445,6 +2483,7 @@ AudioRegion::_add_plugin (std::shared_ptr rfx, std::shared_ptrLatencyChanged.connect_same_thread (*this, boost::bind (&AudioRegion::fx_latency_changed, this, false)); + rfx->plugin()->TailChanged.connect_same_thread (*this, boost::bind (&AudioRegion::fx_tail_changed, this, false)); rfx->set_block_size (_session.get_block_size ()); if (from_set_state) { @@ -2463,6 +2502,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 } @@ -2484,6 +2525,7 @@ 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 @@ -2523,6 +2565,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->plugin()->effective_tail ()); + } + 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) { 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/region.cc b/libs/ardour/region.cc index 6393eac02e..ec982c1529 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) @@ -1589,6 +1594,7 @@ Region::_set_state (const XMLNode& node, int version, PropertyChange& what_chang } if (changed) { fx_latency_changed (true); + fx_tail_changed (true); send_change (PropertyChange (Properties::region_fx)); // trigger DiskReader overwrite RegionFxChanged (); /* EMIT SIGNAL */ } @@ -2449,3 +2455,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->plugin()->effective_tail ()); + } + if (t == _fx_tail) { + return; + } + _fx_tail = t; +} From 961cf955d278e432b7c3788217b89872dac862c4 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Mon, 19 Aug 2024 00:22:38 +0200 Subject: [PATCH 021/111] Hide warnings caused by glibmm/helperlist.h declutter build log --- wscript | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wscript b/wscript index a10fceeb11..f5aa95d68f 100644 --- a/wscript +++ b/wscript @@ -71,7 +71,7 @@ compiler_flags_dictionaries= { # Any additional flags for warnings that are specific to C (not C++) 'extra-c-warnings' : [ '-Wstrict-prototypes', '-Wmissing-prototypes' ], # Any additional flags for warnings that are specific to C++ (not C) - 'extra-cxx-warnings' : [ '-Woverloaded-virtual', '-Wno-unused-local-typedefs' ], + 'extra-cxx-warnings' : [ '-Woverloaded-virtual', '-Wno-unused-local-typedefs', '-Wno-deprecated-copy' ], # Flags used for "strict" compilation, C and C++ (i.e. compiler will warn about language issues) 'strict' : ['-Wall', '-Wcast-align', '-Wextra', '-Wwrite-strings', '-Wunsafe-loop-optimizations', '-Wlogical-op' ], # Flags used for "strict" compilation, C only (i.e. compiler will warn about language issues) From 516f8a9d45cb92e9b9027f21a477d27fb91fdc9d Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Mon, 19 Aug 2024 01:00:31 +0200 Subject: [PATCH 022/111] Add some tooltips to audio region properties dialog --- gtk2_ardour/audio_region_editor.cc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gtk2_ardour/audio_region_editor.cc b/gtk2_ardour/audio_region_editor.cc index aff876dc3b..0991626eb5 100644 --- a/gtk2_ardour/audio_region_editor.cc +++ b/gtk2_ardour/audio_region_editor.cc @@ -60,7 +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")) + , _pre_fade_fx_toggle (_("Pre-Fade Fx")) , _show_on_touch (_("Show on Touch")) , _peak_channel (false) { @@ -106,6 +106,10 @@ AudioRegionEditor::AudioRegionEditor (Session* s, AudioRegionView* arv) _table.attach (_show_on_touch, 2, 3, _table_row, _table_row + 1, Gtk::FILL, Gtk::FILL); ++_table_row; + 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 (); @@ -127,6 +131,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 () From 409a5ee2ea5b8bfb2e8a0ccc64532f30809d3bf7 Mon Sep 17 00:00:00 2001 From: Alexandre Prokoudine Date: Mon, 19 Aug 2024 01:44:27 +0200 Subject: [PATCH 023/111] Update Russian translation --- gtk2_ardour/po/ru.po | 1723 ++++++++++++++++++++++-------------------- 1 file changed, 900 insertions(+), 823 deletions(-) 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-области" From fdf5b0f8a13fc4bbffca1f69275c3303daba9d8d Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Mon, 19 Aug 2024 02:57:03 +0200 Subject: [PATCH 024/111] Fix export with RegionFX Effect processing requires session event pool, and thread local disk reader working buffers. --- libs/ardour/export_handler.cc | 4 ++++ libs/ardour/session_export.cc | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/libs/ardour/export_handler.cc b/libs/ardour/export_handler.cc index d90c9cfb46..028c56685a 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; } 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 From 9311a767cc0e58747e91876157ea538f3846acef Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Mon, 19 Aug 2024 03:42:19 +0200 Subject: [PATCH 025/111] Export Report needs to heed RESPONSE_CANCEL for close-all-dialogs This still does not work on macOS. closing a session (using the menu) while the dialog is visible still causes a crash: `unload_session()` completes and destroys the session before the dialog's run() method returns and destroys the dialog. --- gtk2_ardour/export_report.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From 50044bd059699fc72d9db53569fac6d1d39c20cd Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Mon, 19 Aug 2024 06:35:35 +0200 Subject: [PATCH 026/111] RegionFX: clamp automation line to region extent --- gtk2_ardour/region_fx_line.cc | 2 ++ gtk2_ardour/region_gain_line.cc | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gtk2_ardour/region_fx_line.cc b/gtk2_ardour/region_fx_line.cc index 44945d930a..84a8c09e14 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 (); } diff --git a/gtk2_ardour/region_gain_line.cc b/gtk2_ardour/region_gain_line.cc index bfabb2604c..2340c6f794 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 From fb1ca67e39a203551ca2cc5bc1e2349cdb627775 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Mon, 19 Aug 2024 15:22:59 +0200 Subject: [PATCH 027/111] RegionFX: add clear-automation action --- gtk2_ardour/region_editor.cc | 44 +++++++++++++++++++++++++++++++++++- gtk2_ardour/region_editor.h | 1 + 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/gtk2_ardour/region_editor.cc b/gtk2_ardour/region_editor.cc index 6fc4f04d38..081261e1d2 100644 --- a/gtk2_ardour/region_editor.cc +++ b/gtk2_ardour/region_editor.cc @@ -671,7 +671,8 @@ RegionEditor::RegionFxBox::fxe_button_press_event (GdkEventButton* ev, RegionFxE if (!ac_items.empty ()) { items.push_back (SeparatorElem ()); - items.push_back (MenuElem ("Automation Enable", *automation_menu)); + 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; } @@ -744,6 +745,47 @@ RegionEditor::RegionFxBox::on_key_press (GdkEventKey* ev) return true; } +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 () { diff --git a/gtk2_ardour/region_editor.h b/gtk2_ardour/region_editor.h index 9d7d8676ae..35ea310db9 100644 --- a/gtk2_ardour/region_editor.h +++ b/gtk2_ardour/region_editor.h @@ -108,6 +108,7 @@ 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); /* PluginInterestedObject */ bool use_plugins (SelectedPlugins const&); From 8e063110dbb1d6fa8f49786d785022c6b2922bd5 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Mon, 19 Aug 2024 22:15:58 +0200 Subject: [PATCH 028/111] Fix assert() when aborting automation line drag --- gtk2_ardour/editor_mouse.cc | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gtk2_ardour/editor_mouse.cc b/gtk2_ardour/editor_mouse.cc index e397ef02f1..96b4c45278 100644 --- a/gtk2_ardour/editor_mouse.cc +++ b/gtk2_ardour/editor_mouse.cc @@ -1843,8 +1843,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); From 7dac8994f69ea51d4873e28388b5f0e51ad9795d Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Mon, 5 Aug 2024 22:35:09 +0200 Subject: [PATCH 029/111] LV2/Generic UI: Remove direct calls to plugin API Use Ardour Controllable indirection. --- gtk2_ardour/generic_pluginui.cc | 4 ++-- gtk2_ardour/lv2_plugin_ui.cc | 15 +++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) 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/lv2_plugin_ui.cc b/gtk2_ardour/lv2_plugin_ui.cc index 70466d478d..fc09902b5b 100644 --- a/gtk2_ardour/lv2_plugin_ui.cc +++ b/gtk2_ardour/lv2_plugin_ui.cc @@ -70,9 +70,9 @@ 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 +208,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. @@ -241,7 +241,7 @@ LV2PluginUI::queue_port_update() for (uint32_t i = 0; i < num_ports; ++i) { bool ok; uint32_t port = _lv2->nth_parameter(i, ok); - if (ok) { + if (ok && _lv2->parameter_is_input (i)) { _updates.insert (port); } } @@ -277,7 +277,7 @@ LV2PluginUI::output_update() 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); + float val = _pib->control_output (index)->get_parameter (); if (val != _values_last_sent_to_ui[index]) { /* Send to GUI */ @@ -292,7 +292,7 @@ LV2PluginUI::output_update() */ for (Updates::iterator i = _updates.begin(); i != _updates.end(); ++i) { - float val = _lv2->get_parameter (*i); + 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; @@ -483,11 +483,10 @@ LV2PluginUI::lv2ui_instantiate(const std::string& title) 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()); + /* queue for first update ("push") to GUI */ + _updates.insert (port); } } - - /* queue for first update ("push") to GUI */ - _updates.insert (port); } } From 4ff1de4c75fd93d80cd3ce08c8a12ef81c85798c Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Mon, 5 Aug 2024 22:37:35 +0200 Subject: [PATCH 030/111] RegionFX: save and replay Plugin Parameter Outputs This is somewhat experimental, and only works for plugins that have DSP/UI separation and use [float] control ports to pass information to the UI. --- libs/ardour/ardour/readonly_control.h | 4 +- libs/ardour/region_fx_plugin.cc | 82 ++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 3 deletions(-) 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/region_fx_plugin.cc b/libs/ardour/region_fx_plugin.cc index 9f5cffbf6f..7107c9d105 100644 --- a/libs/ardour/region_fx_plugin.cc +++ b/libs/ardour/region_fx_plugin.cc @@ -37,6 +37,76 @@ 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; +}; + RegionFxPlugin::RegionFxPlugin (Session& s, Temporal::TimeDomain const td, std::shared_ptr plug) : SessionObject (s, (plug ? plug->name () : string ("toBeRenamed"))) , TimeDomainProvider (td) @@ -320,7 +390,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; } @@ -549,6 +619,11 @@ void RegionFxPlugin::flush () { _flush.store (1); + + for (auto const& i : _control_outputs) { + shared_ptr toc = std::dynamic_pointer_cast(i.second); + toc->flush (); + } } bool @@ -1167,6 +1242,11 @@ 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); + } + const samplecnt_t l = effective_latency (); if (_plugin_signal_latency != l) { _plugin_signal_latency= l; From 79ff99ba1508ca88c66b4a2bd525430feb076164 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Tue, 6 Aug 2024 01:39:35 +0200 Subject: [PATCH 031/111] RegionFX: replay control automation (1/2) This riffs off the previous commit, a simple way to replay ctrl events without re-evaluating plugin automation line. There may be a better way to handle this (e.g. replicate the plugin for display only, evaluate on the fly in the UI thread) Short of redesigning our disk-reader/playlist/region infrastructure this is likely a sucks-least compromise for the time being. --- libs/ardour/ardour/plug_insert_base.h | 15 +-- libs/ardour/ardour/region_fx_plugin.h | 6 ++ libs/ardour/region_fx_plugin.cc | 146 +++++++++++++++++++++++++- 3 files changed, 159 insertions(+), 8 deletions(-) diff --git a/libs/ardour/ardour/plug_insert_base.h b/libs/ardour/ardour/plug_insert_base.h index 2f5d32748d..fcc4ff254e 100644 --- a/libs/ardour/ardour/plug_insert_base.h +++ b/libs/ardour/ardour/plug_insert_base.h @@ -73,17 +73,18 @@ public: 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); @@ -98,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/region_fx_plugin.h b/libs/ardour/ardour/region_fx_plugin.h index e39bd276e8..10809b5547 100644 --- a/libs/ardour/ardour/region_fx_plugin.h +++ b/libs/ardour/ardour/region_fx_plugin.h @@ -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 @@ -189,11 +191,15 @@ 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; + + mutable Glib::Threads::Mutex _process_lock; }; } // namespace ARDOUR diff --git a/libs/ardour/region_fx_plugin.cc b/libs/ardour/region_fx_plugin.cc index 7107c9d105..a61aee9290 100644 --- a/libs/ardour/region_fx_plugin.cc +++ b/libs/ardour/region_fx_plugin.cc @@ -107,12 +107,111 @@ private: 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) { _flush.store (0); @@ -381,6 +480,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; @@ -398,7 +509,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); } @@ -624,6 +735,10 @@ RegionFxPlugin::flush () 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 @@ -1069,6 +1184,8 @@ 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) { + Glib::Threads::Mutex::Lock lp (_process_lock); + int canderef (1); if (_flush.compare_exchange_strong (canderef, 0)) { for (auto const& i : _plugins) { @@ -1247,6 +1364,13 @@ RegionFxPlugin::connect_and_run (BufferSet& bufs, samplepos_t start, samplepos_t 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; @@ -1254,3 +1378,23 @@ RegionFxPlugin::connect_and_run (BufferSet& bufs, samplepos_t start, samplepos_t } 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 (); + } + } +} From 002eabc01f5741e3e1292444c7b294b49d0fbebc Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Mon, 19 Aug 2024 15:43:12 +0200 Subject: [PATCH 032/111] RegionFX: replay control automation (2/2) Trigger GUI updates when region-fx are automated --- gtk2_ardour/region_editor.cc | 16 ++++++++++++++++ gtk2_ardour/region_editor.h | 3 +++ 2 files changed, 19 insertions(+) diff --git a/gtk2_ardour/region_editor.cc b/gtk2_ardour/region_editor.cc index 081261e1d2..73572c8c0b 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" @@ -552,6 +553,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 (); @@ -745,6 +748,19 @@ 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) { diff --git a/gtk2_ardour/region_editor.h b/gtk2_ardour/region_editor.h index 35ea310db9..5bd365456e 100644 --- a/gtk2_ardour/region_editor.h +++ b/gtk2_ardour/region_editor.h @@ -109,6 +109,7 @@ private: 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&); @@ -130,6 +131,8 @@ private: Gtk::EventBox _base; bool _no_redisplay; int _placement; + + sigc::connection screen_update_connection; }; std::shared_ptr _region; From 221fd82d595269fec64bbec8fe0b0a09c83796c0 Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Mon, 19 Aug 2024 11:19:26 -0600 Subject: [PATCH 033/111] initial infrastructure for handling modal dialogs on macOS --- libs/tk/ydk/gdkwindow.c | 6 ++++++ libs/tk/ydk/quartz/gdkwindow-quartz.c | 3 +++ libs/tk/ydk/ydk/gdk/gdkinternals.h | 2 ++ libs/tk/ydk/ydk/gdk/gdkwindow.h | 5 ++++- 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/libs/tk/ydk/gdkwindow.c b/libs/tk/ydk/gdkwindow.c index 4fd12edaa0..9c9bd9e396 100644 --- a/libs/tk/ydk/gdkwindow.c +++ b/libs/tk/ydk/gdkwindow.c @@ -159,6 +159,7 @@ typedef struct { int dx, dy; /* The amount that the source was moved to reach dest_region */ } GdkWindowRegionMove; +void (*_gdk_modal_motify)(GdkWindowModalNotify) = 0; /* Global info */ @@ -11408,6 +11409,11 @@ gdk_window_get_height (GdkWindow *window) return height; } +void +gdk_window_set_modal_notify (void (*modal_notify)(GdkWindow*,gboolean)) +{ + _gdk_modal_motify = 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..5dac19923a 100644 --- a/libs/tk/ydk/quartz/gdkwindow-quartz.c +++ b/libs/tk/ydk/quartz/gdkwindow-quartz.c @@ -2389,6 +2389,9 @@ gdk_window_set_modal_hint (GdkWindow *window, !WINDOW_IS_TOPLEVEL (window)) return; + if (_gdk_modal_notify) { + _gdk_modal_notify (window, modal); + } /* FIXME: Implement */ } diff --git a/libs/tk/ydk/ydk/gdk/gdkinternals.h b/libs/tk/ydk/ydk/gdk/gdkinternals.h index 289878b1d4..39c6e25ec4 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_motify)(GdkWindowModalNotify); + 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, From 4ad332ae50164b767d29217bf08bb34699e35fba Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Mon, 19 Aug 2024 11:46:34 -0600 Subject: [PATCH 034/111] fix initial commit --- libs/tk/ydk/quartz/gdkwindow-quartz.c | 1 + libs/tk/ydk/ydk/gdk/gdkinternals.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/tk/ydk/quartz/gdkwindow-quartz.c b/libs/tk/ydk/quartz/gdkwindow-quartz.c index 5dac19923a..67d1077696 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" diff --git a/libs/tk/ydk/ydk/gdk/gdkinternals.h b/libs/tk/ydk/ydk/gdk/gdkinternals.h index 39c6e25ec4..895b2587ce 100644 --- a/libs/tk/ydk/ydk/gdk/gdkinternals.h +++ b/libs/tk/ydk/ydk/gdk/gdkinternals.h @@ -709,7 +709,7 @@ void _gdk_offscreen_window_new (GdkWindow *window, void _gdk_image_exit (void); void _gdk_windowing_exit (void); -extern void (*_gdk_modal_motify)(GdkWindowModalNotify); +extern void (*_gdk_modal_notify)(GdkWindow*, gboolean); G_END_DECLS From 0901b239c1b24ad00c3df0f067f0c8e8af3bde82 Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Mon, 19 Aug 2024 12:32:17 -0600 Subject: [PATCH 035/111] fix spelling error --- libs/tk/ydk/gdkwindow.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libs/tk/ydk/gdkwindow.c b/libs/tk/ydk/gdkwindow.c index 9c9bd9e396..ce1370aa40 100644 --- a/libs/tk/ydk/gdkwindow.c +++ b/libs/tk/ydk/gdkwindow.c @@ -159,8 +159,6 @@ typedef struct { int dx, dy; /* The amount that the source was moved to reach dest_region */ } GdkWindowRegionMove; -void (*_gdk_modal_motify)(GdkWindowModalNotify) = 0; - /* Global info */ static GdkGC *gdk_window_create_gc (GdkDrawable *drawable, @@ -11412,7 +11410,7 @@ gdk_window_get_height (GdkWindow *window) void gdk_window_set_modal_notify (void (*modal_notify)(GdkWindow*,gboolean)) { - _gdk_modal_motify = modal_notify; + _gdk_modal_notify = modal_notify; } #define __GDK_WINDOW_C__ From b544ac832c56c8b4f9cf5af8011488563af857f8 Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Mon, 19 Aug 2024 12:32:40 -0600 Subject: [PATCH 036/111] move gdk global for modal notification to be with other gdk globals --- libs/tk/ydk/gdkglobals.c | 1 + 1 file changed, 1 insertion(+) 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; From ce6a2d73d8bda52c9c1f4318809997b50b326a6e Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Mon, 19 Aug 2024 12:33:05 -0600 Subject: [PATCH 037/111] use new GDK modal notification to (try to) desensitize global app menu items --- libs/gtkmm2ext/gtkapplication_quartz.mm | 35 ++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/libs/gtkmm2ext/gtkapplication_quartz.mm b/libs/gtkmm2ext/gtkapplication_quartz.mm index 0b0083efdd..a921154432 100644 --- a/libs/gtkmm2ext/gtkapplication_quartz.mm +++ b/libs/gtkmm2ext/gtkapplication_quartz.mm @@ -41,10 +41,13 @@ #import #import +#include +#include + #define UNUSED_PARAMETER(a) (void) (a) -// #define DEBUG(format, ...) g_printerr ("%s: " format, G_STRFUNC, ## __VA_ARGS__) -#define DEBUG(format, ...) +#define DEBUG(format, ...) g_printerr ("%s: " format, G_STRFUNC, ## __VA_ARGS__) +//#define DEBUG(format, ...) /* TODO * @@ -56,6 +59,7 @@ */ static gint _exiting = 0; +static std::vector global_menu_items; static guint gdk_quartz_keyval_to_ns_keyval (guint keyval) @@ -1082,7 +1086,7 @@ add_menu_item (NSMenu* cocoa_menu, GtkWidget* menu_item, int index) [cocoa_item setHidden:YES]; #endif - if (GTK_IS_CHECK_MENU_ITEM (menu_item)) + 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)) @@ -1092,6 +1096,11 @@ add_menu_item (NSMenu* cocoa_menu, GtkWidget* menu_item, int index) 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 @@ -1448,12 +1457,32 @@ namespace Gtk { } @end +static void +gdk_quartz_modal_notify (GdkWindow*, gboolean modal) +{ + for (auto & mitem : global_menu_items) { + + GNSMenuItem *cocoa_item; + cocoa_item = cocoa_menu_item_get (GTK_WIDGET(mitem)); + + if (cocoa_item) { + [cocoa_item setEnabled:!modal]; + } + + GtkAction* act = gtk_activatable_get_related_action (GTK_ACTIVATABLE(mitem)); + if (act) { + gtk_action_set_sensitive (act, !modal); + } + } +} /* 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) From 51d95c189fa57e02f79cc4ddeb286d1f49fe9477 Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Mon, 19 Aug 2024 14:04:09 -0600 Subject: [PATCH 038/111] use NSMenuValidation informal protocol to desensitize app menu items --- libs/gtkmm2ext/gtkapplication_quartz.mm | 48 ++++++++++++++++--------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/libs/gtkmm2ext/gtkapplication_quartz.mm b/libs/gtkmm2ext/gtkapplication_quartz.mm index a921154432..cb1a90f709 100644 --- a/libs/gtkmm2ext/gtkapplication_quartz.mm +++ b/libs/gtkmm2ext/gtkapplication_quartz.mm @@ -46,8 +46,8 @@ #define UNUSED_PARAMETER(a) (void) (a) -#define DEBUG(format, ...) g_printerr ("%s: " format, G_STRFUNC, ## __VA_ARGS__) -//#define DEBUG(format, ...) +//#define DEBUG(format, ...) g_printerr ("%s: " format, G_STRFUNC, ## __VA_ARGS__) +#define DEBUG(format, ...) /* TODO * @@ -60,6 +60,7 @@ static gint _exiting = 0; static std::vector global_menu_items; +static bool _modal_state = false; static guint gdk_quartz_keyval_to_ns_keyval (guint keyval) @@ -548,14 +549,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 @@ -581,6 +584,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) { + 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, @@ -712,10 +729,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 @@ -766,7 +784,7 @@ cocoa_menu_item_update_submenu (NSMenuItem *cocoa_item, 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 @@ -1312,7 +1330,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", @@ -1438,7 +1456,6 @@ namespace Gtk { @interface GtkApplicationDelegate : NSObject -(BOOL) application:(NSApplication*) app openFile:(NSString*) file; - (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *) app; -- (void) startApp; @end @implementation GtkApplicationDelegate @@ -1460,15 +1477,12 @@ namespace Gtk { static void gdk_quartz_modal_notify (GdkWindow*, gboolean modal) { + /* this global will control sensitivity of our app menu items, via validateMenuItem */ + _modal_state = modal; + + /* Need to notify GTK that actions are insensitive where necessary */ + for (auto & mitem : global_menu_items) { - - GNSMenuItem *cocoa_item; - cocoa_item = cocoa_menu_item_get (GTK_WIDGET(mitem)); - - if (cocoa_item) { - [cocoa_item setEnabled:!modal]; - } - GtkAction* act = gtk_activatable_get_related_action (GTK_ACTIVATABLE(mitem)); if (act) { gtk_action_set_sensitive (act, !modal); From 9e9164f0d0fdeff04212c465bbd0cdb38fc17938 Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Mon, 19 Aug 2024 14:05:51 -0600 Subject: [PATCH 039/111] remove unnecessary header include --- libs/gtkmm2ext/gtkapplication_quartz.mm | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/gtkmm2ext/gtkapplication_quartz.mm b/libs/gtkmm2ext/gtkapplication_quartz.mm index cb1a90f709..b0bf4e26f6 100644 --- a/libs/gtkmm2ext/gtkapplication_quartz.mm +++ b/libs/gtkmm2ext/gtkapplication_quartz.mm @@ -41,7 +41,6 @@ #import #import -#include #include #define UNUSED_PARAMETER(a) (void) (a) From d455f06f51676c36361f9d8f181a93b0f639dd0c Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Mon, 19 Aug 2024 14:34:31 -0600 Subject: [PATCH 040/111] fix trailing whitespace --- libs/gtkmm2ext/gtkapplication_quartz.mm | 108 ++++++++++++------------ 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/libs/gtkmm2ext/gtkapplication_quartz.mm b/libs/gtkmm2ext/gtkapplication_quartz.mm index b0bf4e26f6..8baafadc7d 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 @@ -313,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) { @@ -525,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 @@ -535,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; @@ -688,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); @@ -751,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]; @@ -762,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); @@ -778,7 +778,7 @@ 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:@""]; @@ -811,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:@""]; } @@ -827,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 */ @@ -858,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]]; @@ -881,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; } @@ -925,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); @@ -982,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)); @@ -1037,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), @@ -1056,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)) { @@ -1077,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]; @@ -1085,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]; @@ -1102,14 +1102,14 @@ 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)) + + 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]; @@ -1119,7 +1119,7 @@ add_menu_item (NSMenu* cocoa_menu, GtkWidget* menu_item, int index) global_menu_items.push_back (mitem); } } - + static void push_menu_shell_to_nsmenu (GtkMenuShell *menu_shell, NSMenu* cocoa_menu, @@ -1143,7 +1143,7 @@ push_menu_shell_to_nsmenu (GtkMenuShell *menu_shell, add_menu_item (cocoa_menu, menu_item, -1); } - + g_list_free (children); } @@ -1290,7 +1290,7 @@ add_to_window_menu (NSMenu *menu) static int create_window_menu () -{ +{ _window_menu = [[NSMenu alloc] initWithTitle: @"Window"]; [_window_menu addItemWithTitle:@"Minimize" @@ -1303,7 +1303,7 @@ create_window_menu () add_to_menubar(_window_menu); return 0; -} +} #endif /* @@ -1381,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]; @@ -1415,7 +1415,7 @@ namespace Gtk { } @interface GtkApplicationNotificationObject : NSObject {} -- (GtkApplicationNotificationObject*) init; +- (GtkApplicationNotificationObject*) init; @end @implementation GtkApplicationNotificationObject @@ -1431,7 +1431,7 @@ namespace Gtk { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidBecomeInactive:) - name:NSApplicationWillResignActiveNotification + name:NSApplicationWillResignActiveNotification object:[NSApplication sharedApplication]]; } @@ -1498,7 +1498,7 @@ gtk_application_init () _main_menubar = [[NSMenu alloc] initWithTitle: @""]; - if (!_main_menubar) + if (!_main_menubar) return -1; [NSApp setMainMenu: _main_menubar]; From 6de2d8f5c413ed83640a086912f16d3753834af7 Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Mon, 19 Aug 2024 15:00:13 -0600 Subject: [PATCH 041/111] macOS: prevent the Quit main menu item from interferring with modal rules --- libs/gtkmm2ext/gtkapplication_quartz.mm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/gtkmm2ext/gtkapplication_quartz.mm b/libs/gtkmm2ext/gtkapplication_quartz.mm index 8baafadc7d..38db6a2d42 100644 --- a/libs/gtkmm2ext/gtkapplication_quartz.mm +++ b/libs/gtkmm2ext/gtkapplication_quartz.mm @@ -1468,6 +1468,9 @@ namespace Gtk { - (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *) app { UNUSED_PARAMETER(app); + if (_modal_state) { + return NSTerminateCancel; + } Gtkmm2ext::Application::instance()->ShouldQuit (); return NSTerminateCancel; } From 09a3c325ec7a3962b1060a8d752886062c3ad3a8 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Tue, 20 Aug 2024 01:45:11 +0200 Subject: [PATCH 042/111] RegionFx: don't crash if plugin is missing on session load --- libs/ardour/region.cc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/ardour/region.cc b/libs/ardour/region.cc index ec982c1529..6f411e3500 100644 --- a/libs/ardour/region.cc +++ b/libs/ardour/region.cc @@ -1584,7 +1584,11 @@ Region::_set_state (const XMLNode& node, int version, PropertyChange& what_chang 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; } From 1b07ad731e51cbb6638c5f39e1de84238bbeee60 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Tue, 20 Aug 2024 18:58:45 +0200 Subject: [PATCH 043/111] RegionFX: use actual plugin tail --- libs/ardour/audioregion.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/ardour/audioregion.cc b/libs/ardour/audioregion.cc index 4ef78b659e..55cdccd52b 100644 --- a/libs/ardour/audioregion.cc +++ b/libs/ardour/audioregion.cc @@ -529,7 +529,7 @@ timecnt_t AudioRegion::tail () const { if (_fade_before_fx && has_region_fx ()) { - return timecnt_t (_session.sample_rate ()); // TODO use plugin API + return timecnt_t ((samplecnt_t)_fx_tail); } else { return timecnt_t (0); } From 4d1d9382636beb1d3824998b6742d0fbb2080710 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Wed, 21 Aug 2024 03:43:56 +0200 Subject: [PATCH 044/111] I/O Plugin: delete plugin after removing it Previously this kept a shared pointer reference to the plugin around, and ports remained registered. See also 44610c7877fc25 (RCU update) --- libs/ardour/session.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/ardour/session.cc b/libs/ardour/session.cc index 76a1c7df33..3897542d8d 100644 --- a/libs/ardour/session.cc +++ b/libs/ardour/session.cc @@ -5505,6 +5505,7 @@ Session::unload_io_plugin (std::shared_ptr ioplugin) } IOPluginsChanged (); /* EMIT SIGNAL */ set_dirty(); + _io_plugins.flush (); return true; } From 908a402a75f5922ac708d57168954790510758c0 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Wed, 21 Aug 2024 03:47:40 +0200 Subject: [PATCH 045/111] Drop some shared pointer references after RCU writes When removing instances from some RCU managed list or vector, they can still live on as shared_ptr references on the RCU's dead wood stack. In many cases this is not an issue, in these cases it's prudent. see also 44610c7877fc25fe --- libs/ardour/monitor_port.cc | 5 +++++ libs/ardour/port_manager.cc | 3 +++ libs/ardour/session_bundles.cc | 1 + 3 files changed, 9 insertions(+) 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/port_manager.cc b/libs/ardour/port_manager.cc index 7da3445f8f..db611aec91 100644 --- a/libs/ardour/port_manager.cc +++ b/libs/ardour/port_manager.cc @@ -1158,6 +1158,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/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(); From 9465ff16cb9e9684fe36d2ee0b71ae891ce740ee Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Wed, 21 Aug 2024 18:37:23 +0200 Subject: [PATCH 046/111] Fix Route Fader (and mute) latency offset #9780 _output latency was not used for those. Processor automation was not affected. This also fixes the visual offset of automation vs buttons/slider when the transport is not running. --- libs/ardour/route.cc | 59 +++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/libs/ardour/route.cc b/libs/ardour/route.cc index ee038563a5..f5c1375362 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" From 9df6d7c5fad9a90a8ede4424bdf91b6f7c170a08 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Wed, 21 Aug 2024 18:53:55 +0200 Subject: [PATCH 047/111] Fix Lua Audio to MIDI script MIDI channel (off by one) --- share/scripts/vamp_audio_to_midi.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/scripts/vamp_audio_to_midi.lua b/share/scripts/vamp_audio_to_midi.lua index ed3f779efc..efe123790a 100644 --- a/share/scripts/vamp_audio_to_midi.lua +++ b/share/scripts/vamp_audio_to_midi.lua @@ -88,7 +88,7 @@ function factory () return function () local pos = bs - b_off local len = be - bs - local note = ARDOUR.LuaAPI.new_noteptr (1, pos, len, fn + 1, 0x7f) + local note = ARDOUR.LuaAPI.new_noteptr (0, pos, len, fn + 1, 0x7f) midi_command:add (note) end mm:apply_command (Session, midi_command) From a1ba561cc5a6797f6b0fae840d15b4c93192b7f4 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Thu, 22 Aug 2024 18:32:38 +0200 Subject: [PATCH 048/111] Improve reliability of MIDI event rec-box Previously there was a race condition. DiskWriter::run() cleared the gui_feed_buffer before writing new events. If the GUI thread had not yet picked up the events by then they were not displayed. Furthermore due to the try-lock, some events may have been written to the buffer in the first place. This fixes missing events (notably stuck notes) in the red record box while recording MIDI. --- libs/ardour/ardour/disk_writer.h | 5 ++-- libs/ardour/disk_writer.cc | 46 ++++++++++++++++++-------------- 2 files changed, 29 insertions(+), 22 deletions(-) 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/disk_writer.cc b/libs/ardour/disk_writer.cc index 0c8d197480..3ea33de71c 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 (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. */ From fc07a92d36e2d021abd576d5d6108da826d2fd05 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Tue, 20 Aug 2024 20:01:06 +0200 Subject: [PATCH 049/111] Prepare GUI for missing (stub) RegionFx --- gtk2_ardour/audio_region_editor.cc | 4 ++ gtk2_ardour/region_editor.cc | 111 ++++++++++++++++------------- gtk2_ardour/region_editor.h | 3 +- 3 files changed, 69 insertions(+), 49 deletions(-) diff --git a/gtk2_ardour/audio_region_editor.cc b/gtk2_ardour/audio_region_editor.cc index 0991626eb5..c1aea81769 100644 --- a/gtk2_ardour/audio_region_editor.cc +++ b/gtk2_ardour/audio_region_editor.cc @@ -302,6 +302,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/region_editor.cc b/gtk2_ardour/region_editor.cc index 73572c8c0b..44a832dcd2 100644 --- a/gtk2_ardour/region_editor.cc +++ b/gtk2_ardour/region_editor.cc @@ -636,51 +636,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)); - 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 (_("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 (); }); @@ -962,7 +964,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; } @@ -1001,18 +1003,29 @@ RegionEditor::RegionFxEntry::RegionFxEntry (std::shared_ptr 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); - if (pre) { + + 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 ()->has_editor ()) { + 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 ())); @@ -1043,7 +1056,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 @@ -1065,7 +1078,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 5bd365456e..86952592f3 100644 --- a/gtk2_ardour/region_editor.h +++ b/gtk2_ardour/region_editor.h @@ -80,7 +80,7 @@ private: 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 From b6e187193da56a9d3a0ebca61087ad18ddf5061a Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Fri, 23 Aug 2024 15:26:50 +0200 Subject: [PATCH 050/111] RegionFx: abstract Plugin tail API --- libs/ardour/ardour/region_fx_plugin.h | 4 ++++ libs/ardour/audioregion.cc | 4 ++-- libs/ardour/region_fx_plugin.cc | 7 +++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/libs/ardour/ardour/region_fx_plugin.h b/libs/ardour/ardour/region_fx_plugin.h index 10809b5547..74f28cf43f 100644 --- a/libs/ardour/ardour/region_fx_plugin.h +++ b/libs/ardour/ardour/region_fx_plugin.h @@ -155,6 +155,10 @@ public: return _required_buffers; } + /* wrapped Plugin API */ + PBD::Signal0 TailChanged; + samplecnt_t effective_tail () const; + private: /* disallow copy construction */ RegionFxPlugin (RegionFxPlugin const&); diff --git a/libs/ardour/audioregion.cc b/libs/ardour/audioregion.cc index 55cdccd52b..ffec0b9b54 100644 --- a/libs/ardour/audioregion.cc +++ b/libs/ardour/audioregion.cc @@ -2483,7 +2483,7 @@ AudioRegion::_add_plugin (std::shared_ptr rfx, std::shared_ptrLatencyChanged.connect_same_thread (*this, boost::bind (&AudioRegion::fx_latency_changed, this, false)); - rfx->plugin()->TailChanged.connect_same_thread (*this, boost::bind (&AudioRegion::fx_tail_changed, this, false)); + rfx->TailChanged.connect_same_thread (*this, boost::bind (&AudioRegion::fx_tail_changed, this, false)); rfx->set_block_size (_session.get_block_size ()); if (from_set_state) { @@ -2570,7 +2570,7 @@ AudioRegion::fx_tail_changed (bool no_emit) { uint32_t t = 0; for (auto const& rfx : _plugins) { - t = max (t, rfx->plugin()->effective_tail ()); + t = max (t, rfx->effective_tail ()); } if (t == _fx_tail) { return; diff --git a/libs/ardour/region_fx_plugin.cc b/libs/ardour/region_fx_plugin.cc index a61aee9290..b069cdf259 100644 --- a/libs/ardour/region_fx_plugin.cc +++ b/libs/ardour/region_fx_plugin.cc @@ -390,6 +390,7 @@ RegionFxPlugin::add_plugin (std::shared_ptr plugin) plugin->ParameterChangedExternally.connect_same_thread (*this, boost::bind (&RegionFxPlugin::parameter_changed_externally, this, _1, _2)); plugin->StartTouch.connect_same_thread (*this, boost::bind (&RegionFxPlugin::start_touch, this, _1)); plugin->EndTouch.connect_same_thread (*this, boost::bind (&RegionFxPlugin::end_touch, this, _1)); + plugin->TailChanged.connect_same_thread (*this, [this](){ TailChanged (); }); } plugin->set_insert (this, _plugins.size ()); @@ -466,6 +467,12 @@ RegionFxPlugin::signal_latency () const return _plugins.front ()->signal_latency (); } +ARDOUR::samplecnt_t +RegionFxPlugin::effective_tail () const +{ + return _plugins.front ()->effective_tail (); +} + PlugInsertBase::UIElements RegionFxPlugin::ui_elements () const { From e419b314a2232d4b64fbb17d9c69ac39c4abad3b Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Fri, 23 Aug 2024 15:42:33 +0200 Subject: [PATCH 051/111] RegionFx: handle missing plugin, retain state Since RegionFx are significantly simpler compared to processors, missing plugin state can directly be handled in the implantation without creating a subclass similar to UnknownProcessor. --- libs/ardour/ardour/region_fx_plugin.h | 10 ++--- libs/ardour/region_fx_plugin.cc | 59 ++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/libs/ardour/ardour/region_fx_plugin.h b/libs/ardour/ardour/region_fx_plugin.h index 74f28cf43f..72b35bd6df 100644 --- a/libs/ardour/ardour/region_fx_plugin.h +++ b/libs/ardour/ardour/region_fx_plugin.h @@ -67,16 +67,14 @@ public: { 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(); } } @@ -203,6 +201,8 @@ private: Gtkmm2ext::WindowProxy* _window_proxy; std::atomic _flush; + XMLNode* _state; + mutable Glib::Threads::Mutex _process_lock; }; diff --git a/libs/ardour/region_fx_plugin.cc b/libs/ardour/region_fx_plugin.cc index b069cdf259..1b10eaef2c 100644 --- a/libs/ardour/region_fx_plugin.cc +++ b/libs/ardour/region_fx_plugin.cc @@ -213,6 +213,7 @@ RegionFxPlugin::RegionFxPlugin (Session& s, Temporal::TimeDomain const td, std:: , _no_inplace (false) , _last_emit (0) , _window_proxy (0) + , _state (0) { _flush.store (0); @@ -234,11 +235,18 @@ 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); @@ -294,7 +302,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); @@ -370,6 +386,22 @@ RegionFxPlugin::set_state (const XMLNode& node, int 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) { @@ -464,12 +496,18 @@ RegionFxPlugin::drop_references () ARDOUR::samplecnt_t RegionFxPlugin::signal_latency () const { + if (_plugins.empty ()) { + return 0; + } return _plugins.front ()->signal_latency (); } ARDOUR::samplecnt_t RegionFxPlugin::effective_tail () const { + if (_plugins.empty ()) { + return 0; + } return _plugins.front ()->effective_tail (); } @@ -639,6 +677,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) { @@ -671,6 +710,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 (); @@ -700,6 +743,8 @@ RegionFxPlugin::can_reset_all_parameters () bool RegionFxPlugin::reset_parameters_to_default () { + assert (!_plugins.empty ()); + bool all = true; std::shared_ptr plugin = _plugins.front (); @@ -751,6 +796,10 @@ RegionFxPlugin::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; } @@ -904,6 +953,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; @@ -1191,6 +1244,10 @@ 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); From 66b3ad79c12e08b90d382d0e98c92e0b231bfa88 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Sat, 24 Aug 2024 00:23:03 +0200 Subject: [PATCH 052/111] RegionFx: mark session dirty when adding/removing plugins --- libs/ardour/audioregion.cc | 1 + libs/ardour/region.cc | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/ardour/audioregion.cc b/libs/ardour/audioregion.cc index ffec0b9b54..38ce0c36a7 100644 --- a/libs/ardour/audioregion.cc +++ b/libs/ardour/audioregion.cc @@ -2531,6 +2531,7 @@ AudioRegion::remove_plugin (std::shared_ptr fx) send_change (PropertyChange (Properties::region_fx)); // trigger DiskReader overwrite } RegionFxChanged (); /* EMIT SIGNAL */ + _session.set_dirty (); return true; } diff --git a/libs/ardour/region.cc b/libs/ardour/region.cc index 6f411e3500..9c65b38c18 100644 --- a/libs/ardour/region.cc +++ b/libs/ardour/region.cc @@ -2418,7 +2418,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 @@ -2445,6 +2449,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 From 09bddcad10be3c7a92bcf0014db3c0ab30c4f1e2 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Sat, 24 Aug 2024 00:24:28 +0200 Subject: [PATCH 053/111] NO-OP: fix function name spelling --- gtk2_ardour/audio_region_view.cc | 2 +- gtk2_ardour/region_fx_line.cc | 6 +++--- gtk2_ardour/region_fx_line.h | 2 +- gtk2_ardour/region_gain_line.cc | 4 ++-- gtk2_ardour/region_gain_line.h | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gtk2_ardour/audio_region_view.cc b/gtk2_ardour/audio_region_view.cc index 7c62f2990d..77c969dc20 100644 --- a/gtk2_ardour/audio_region_view.cc +++ b/gtk2_ardour/audio_region_view.cc @@ -1562,7 +1562,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/region_fx_line.cc b/gtk2_ardour/region_fx_line.cc index 84a8c09e14..8b86d67fb6 100644 --- a/gtk2_ardour/region_fx_line.cc +++ b/gtk2_ardour/region_fx_line.cc @@ -58,7 +58,7 @@ RegionFxLine::get_origin() const } void -RegionFxLine::enable_autoation () +RegionFxLine::enable_automation () { std::shared_ptr ac = _ac.lock (); if (ac) { @@ -69,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 2340c6f794..3a0d18776b 100644 --- a/gtk2_ardour/region_gain_line.cc +++ b/gtk2_ardour/region_gain_line.cc @@ -117,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: From c92c8c8fa28e91b6efc3a3f4ac1666ca65fa62e5 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Sat, 24 Aug 2024 11:26:49 +0200 Subject: [PATCH 054/111] VST3: fix deadlock when recalling program changes latency The GUI thread may call set_program() which can triggers the plugin directly calling restartComponent from the same thread. --- libs/ardour/ardour/vst3_plugin.h | 2 ++ libs/ardour/vst3_plugin.cc | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/libs/ardour/ardour/vst3_plugin.h b/libs/ardour/ardour/vst3_plugin.h index aaa94f7b9c..feadc6b52b 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, diff --git a/libs/ardour/vst3_plugin.cc b/libs/ardour/vst3_plugin.cc index a7823a7d14..66eb37f5d2 100644 --- a/libs/ardour/vst3_plugin.cc +++ b/libs/ardour/vst3_plugin.cc @@ -1025,9 +1025,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)) { @@ -1044,14 +1045,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); } From eac3283b4964f0753cd8840437f3522398774218 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Sun, 25 Aug 2024 00:04:17 +0200 Subject: [PATCH 055/111] Use a sensible size for DSP to GUI MIDI messages See also a1ba561cc5a. JACK2 and pipewire unconditionally report 32kB, Ardour internal backends report 8kB, also independent of the buffersize. While jack1 by default announces the audio buffersize. A sensible value assumes that the GUI reads the FIFO at a least 25fps, while also allowing MIDI ports to merge data (hence 2 * raw_buffer_size). Yet limit to 64k per track. --- libs/ardour/disk_writer.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/ardour/disk_writer.cc b/libs/ardour/disk_writer.cc index 3ea33de71c..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_fifo (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); From 190cd657b98e48b61c5bc07467b0448dbc1c5169 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Sun, 25 Aug 2024 15:27:04 +0200 Subject: [PATCH 056/111] LV2: fix port/nth-parameter confusion various LV2 callbacks from plugin DSP/GUI use raw port_index, not nth_parameter. This lead to incorrectly queued updates (_values_last_sent_to_ui) and since 7dac8994f69ea51 to potential crashes (invalid _controllables[idx]). --- gtk2_ardour/lv2_plugin_ui.cc | 66 +++++++++++++++--------------------- gtk2_ardour/lv2_plugin_ui.h | 2 +- 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/gtk2_ardour/lv2_plugin_ui.cc b/gtk2_ardour/lv2_plugin_ui.cc index fc09902b5b..45e1a464cf 100644 --- a/gtk2_ardour/lv2_plugin_ui.cc +++ b/gtk2_ardour/lv2_plugin_ui.cc @@ -70,7 +70,6 @@ LV2PluginUI::write_from_ui(void* controller, std::shared_ptr ac = me->_controllables[port_index]; - if (ac) { me->_updates.insert (port_index); ac->set_value(*(const float*)buffer, Controllable::NoGroup); @@ -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 && _lv2->parameter_is_input (i)) { - _updates.insert (port); + if (_lv2->parameter_is_control (i) && _lv2->parameter_is_input(i)) { + _updates.insert (i); } } } @@ -274,9 +271,7 @@ 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]; + for (auto const& index: _output_ports) { float val = _pib->control_output (index)->get_parameter (); if (val != _values_last_sent_to_ui[index]) { @@ -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 = _controllables[*i]->get_value (); + 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,28 +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()); - /* queue for first update ("push") to GUI */ - _updates.insert (port); - } - } + _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 (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; From 3ced8cc6bcae213f8e063c6222ae9e9f3512f90a Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Tue, 27 Aug 2024 01:09:06 +0200 Subject: [PATCH 057/111] RegionFX: fix undo/redo of region state Previously this caused deadlocks (read lock while holding write lock), and also not dropped references of plugins. Ideally undo/redo of FX unrelated region state will not re-instantiate plugins; we can optimize this later. --- libs/ardour/region.cc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/ardour/region.cc b/libs/ardour/region.cc index 9c65b38c18..a90f0bc8cc 100644 --- a/libs/ardour/region.cc +++ b/libs/ardour/region.cc @@ -1579,6 +1579,10 @@ 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 ()) { @@ -1596,6 +1600,7 @@ 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); From 83207e04e78f342aaa881eec7464f008cd778cf4 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Tue, 27 Aug 2024 02:04:21 +0200 Subject: [PATCH 058/111] VST3: move static runloop from plugin to Host This fixes a crash at exit for some Linux VST3s when Ardour calls ::release_factory() before unloading the module. The plugin will then also call unref the hostContext. Previously the lookup interface for the host context was provided by the plugin; which at that point in time was already deleted. --- libs/ardour/vst3_host.cc | 149 +++++++++++++++++++++++++++++++++ libs/ardour/vst3_plugin.cc | 165 ++----------------------------------- 2 files changed, 157 insertions(+), 157 deletions(-) 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 66eb37f5d2..2360efb9b0 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) @@ -1221,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); @@ -1425,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)); @@ -1659,13 +1517,6 @@ VST3PI::queryInterface (const TUID _iid, void** obj) QUERY_INTERFACE (_iid, obj, IPlugFrame::iid, IPlugFrame) -#if SMTG_OS_LINUX - if (FUnknownPrivate::iidEqual (_iid, Linux::IRunLoop::iid)) { - *obj = &static_runloop; - return kResultOk; - } -#endif - if (DEBUG_ENABLED (DEBUG::VST3Config)) { char fuid[33]; FUID::fromTUID (_iid).toString (fuid); From 981dfd67e78b7e22e74148d0decdfd61849249da Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Tue, 27 Aug 2024 09:42:31 -0600 Subject: [PATCH 059/111] call our "transport stopped" code path when JACK transport stops this fixes issues with MIDI region recording, which otherwise never goes through the code path required to "fix" the nascent data into sources and regions. --- libs/ardour/session_process.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libs/ardour/session_process.cc b/libs/ardour/session_process.cc index 33b188161c..2c3b1fe982 100644 --- a/libs/ardour/session_process.cc +++ b/libs/ardour/session_process.cc @@ -1258,6 +1258,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) { From 8fd081679d4751caaa04a9c1f1ce01a109e7779c Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Tue, 27 Aug 2024 17:50:10 +0200 Subject: [PATCH 060/111] Add a debug option to investigate RegionFX related underruns --- libs/ardour/ardour/debug.h | 1 + libs/ardour/audio_playlist.cc | 7 +++++++ libs/ardour/audioregion.cc | 5 +++++ libs/ardour/debug.cc | 1 + libs/ardour/disk_reader.cc | 4 ++++ 5 files changed, 18 insertions(+) diff --git a/libs/ardour/ardour/debug.h b/libs/ardour/ardour/debug.h index 18aaa74ae7..a8e127a187 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; diff --git a/libs/ardour/audio_playlist.cc b/libs/ardour/audio_playlist.cc index 67b391c384..b3a2e3bfc2 100644 --- a/libs/ardour/audio_playlist.cc +++ b/libs/ardour/audio_playlist.cc @@ -178,6 +178,13 @@ AudioPlaylist::read (Sample *buf, Sample *mixdown_buffer, float *gain_buffer, ti DEBUG_TRACE (DEBUG::AudioPlayback, string_compose ("Playlist %1 read @ %2 for %3, channel %4, regions %5 mixdown @ %6 gain @ %7\n", 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 ()); /* optimizing this memset() away involves a lot of conditionals diff --git a/libs/ardour/audioregion.cc b/libs/ardour/audioregion.cc index 38ce0c36a7..89d79c6579 100644 --- a/libs/ardour/audioregion.cc +++ b/libs/ardour/audioregion.cc @@ -610,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. */ diff --git a/libs/ardour/debug.cc b/libs/ardour/debug.cc index 99a8dbce20..2cb4becc65 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"); 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; } From c77d2d42b9f3e7e3db6842b8312def760b2bca70 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Tue, 27 Aug 2024 21:56:17 +0200 Subject: [PATCH 061/111] VST3: fix runloop query (amend 83207e04e78) The plugin itself needs to be able to return a runloop, for the UI, even if the factory has one. --- libs/ardour/vst3_plugin.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libs/ardour/vst3_plugin.cc b/libs/ardour/vst3_plugin.cc index 2360efb9b0..6ff5509362 100644 --- a/libs/ardour/vst3_plugin.cc +++ b/libs/ardour/vst3_plugin.cc @@ -1517,6 +1517,12 @@ VST3PI::queryInterface (const TUID _iid, void** obj) QUERY_INTERFACE (_iid, obj, IPlugFrame::iid, IPlugFrame) +#if SMTG_OS_LINUX + if (FUnknownPrivate::iidEqual (_iid, Linux::IRunLoop::iid)) { + return HostApplication::getHostContext()->queryInterface (_iid, obj); + } +#endif + if (DEBUG_ENABLED (DEBUG::VST3Config)) { char fuid[33]; FUID::fromTUID (_iid).toString (fuid); From 79e2ffff2a7ca65f0346822e1d2fbcfb9d2cb36a Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Wed, 28 Aug 2024 04:10:41 +0200 Subject: [PATCH 062/111] LXVST: fix crash at exit if Linux VSTs are disabled or X11 connection fails --- gtk2_ardour/linux_vst_gui_support.cc | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/gtk2_ardour/linux_vst_gui_support.cc b/gtk2_ardour/linux_vst_gui_support.cc index 80bc465b0f..c321071c57 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*/ From 84a3d0c559e5accc3e76374db2f0b202148a3ba3 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Wed, 28 Aug 2024 04:28:26 +0200 Subject: [PATCH 063/111] Fix copy/paste, windows (not Linux) VSTs.. --- libs/ardour/windows_vst_plugin.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; } From 24fe6adb02f90930d8db36eafc555c94e604f4ae Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Wed, 28 Aug 2024 05:14:41 +0200 Subject: [PATCH 064/111] Amend previous commit --- gtk2_ardour/linux_vst_gui_support.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtk2_ardour/linux_vst_gui_support.cc b/gtk2_ardour/linux_vst_gui_support.cc index c321071c57..f2d287c2ea 100644 --- a/gtk2_ardour/linux_vst_gui_support.cc +++ b/gtk2_ardour/linux_vst_gui_support.cc @@ -783,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; From ed105fdf668fde449f76cb7fe9981f1a5ca4fb69 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Thu, 29 Aug 2024 02:28:30 +0200 Subject: [PATCH 065/111] Allow to reactivate disable tracks in the editor Disabled tracks cannot be selected, so "apply to selection" does not work to reactivate track... unless it has just been deactivated. The Selection is not cleared when changing active state. --- gtk2_ardour/route_time_axis.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtk2_ardour/route_time_axis.cc b/gtk2_ardour/route_time_axis.cc index ec11e11db9..41485e8d4c 100644 --- a/gtk2_ardour/route_time_axis.cc +++ b/gtk2_ardour/route_time_axis.cc @@ -883,7 +883,7 @@ RouteTimeAxisView::build_display_menu () 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))); From f4c978cf88a2c8c98bfa81328a9dcb76370965c5 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Thu, 29 Aug 2024 04:26:33 +0200 Subject: [PATCH 066/111] Fix MIDI patch/CC state restore #9770 MidiTrack::restore_controls is triggered by SessionLoaded event. Yet MidiControl::actually_set_value checks if _session.loading() is true and postpones sending actual events to the synth (which may not be fully loaded). Before 5d02970de866e, Session::post_engine_init unset "Loading" flag early on, so "loading" was already unset by the time `SessionLoaded` was emitted. --- libs/ardour/session_state.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/ardour/session_state.cc b/libs/ardour/session_state.cc index 0ca1d62f3a..c09ff047f3 100644 --- a/libs/ardour/session_state.cc +++ b/libs/ardour/session_state.cc @@ -419,10 +419,10 @@ Session::post_engine_init () void Session::session_loaded () { - SessionLoaded(); - set_clean (); + SessionLoaded(); + if (_is_new) { save_state (""); } From c37d523b2edc3145097a9864c32b646b8c87641f Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Thu, 29 Aug 2024 16:05:56 +0200 Subject: [PATCH 067/111] Fix heap-use-after-free when deleting tempo-markers --- gtk2_ardour/editor_tempodisplay.cc | 3 +++ 1 file changed, 3 insertions(+) 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; } From a18c1c5750a0ef8c64ec9d786fb227178913bc67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Mon, 19 Aug 2024 04:05:00 +0200 Subject: [PATCH 068/111] Remove #include --- gtk2_ardour/io_plugin_window.cc | 2 -- libs/ardour/ardour/export_smf_writer.h | 2 -- libs/ardour/ardour/graph_edges.h | 2 -- libs/ardour/ardour/mixer_scene.h | 2 -- 4 files changed, 8 deletions(-) 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/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/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" From eff42b22fd4f168d4672dd2a0d658f6af263ca58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Mon, 19 Aug 2024 04:16:19 +0200 Subject: [PATCH 069/111] Remove #include --- gtk2_ardour/ardour_ui.h | 2 -- libs/ardour/ardour/midi_cursor.h | 2 -- libs/ardour/ardour/midi_playlist.h | 2 -- libs/ardour/ardour/playlist.h | 1 - libs/ardour/ardour/port_set.h | 2 -- libs/ardour/ardour/region.h | 2 -- libs/ardour/ardour/session.h | 2 -- libs/ardour/ardour/source.h | 2 -- libs/ardour/ardour/stripable.h | 2 -- libs/ardour/ardour/uri_map.h | 2 -- libs/ardour/lv2_plugin.cc | 2 -- libs/panners/vbap/vbap_speakers.h | 2 -- 12 files changed, 23 deletions(-) diff --git a/gtk2_ardour/ardour_ui.h b/gtk2_ardour/ardour_ui.h index 9b8529b726..b91b174f2a 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/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_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/playlist.h b/libs/ardour/ardour/playlist.h index 4533a8336c..882c886845 100644 --- a/libs/ardour/ardour/playlist.h +++ b/libs/ardour/ardour/playlist.h @@ -37,7 +37,6 @@ #include #include -#include #include 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/region.h b/libs/ardour/ardour/region.h index ea842a3f40..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" diff --git a/libs/ardour/ardour/session.h b/libs/ardour/ardour/session.h index 91440d837d..cb16cb3c5e 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/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/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/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/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" From 635446fd3b9a97e276c3c1471a1e42f871eb7832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Mon, 19 Aug 2024 06:56:33 +0200 Subject: [PATCH 070/111] Remove #include --- libs/audiographer/audiographer/process_context.h | 1 - 1 file changed, 1 deletion(-) 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" From 114eefed113e92fbf3bf6134a63eeb286ec3ad79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Mon, 19 Aug 2024 07:31:45 +0200 Subject: [PATCH 071/111] Remove #include --- gtk2_ardour/beatbox_gui.cc | 2 -- 1 file changed, 2 deletions(-) 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" From dba8f4ec405c2f60c4632ddb0525c4d982379f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Mon, 19 Aug 2024 22:58:38 +0200 Subject: [PATCH 072/111] Remove #include --- libs/ardour/ardour/step_sequencer.h | 1 - 1 file changed, 1 deletion(-) 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 From 7c39a374fa86a922a84f551c317dc4d11dd0e23d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Mon, 19 Aug 2024 23:03:15 +0200 Subject: [PATCH 073/111] Narrow scope of boost::hash_combine include --- libs/surfaces/websockets/state.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From 7586c2d50a563dbd61b5aafa998f8fc5c079a210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Mon, 19 Aug 2024 23:07:14 +0200 Subject: [PATCH 074/111] Remove #include --- libs/ardour/session.cc | 2 -- libs/ardour/session_process.cc | 2 -- libs/ardour/session_transport.cc | 2 -- 3 files changed, 6 deletions(-) diff --git a/libs/ardour/session.cc b/libs/ardour/session.cc index 3897542d8d..538eedb750 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" diff --git a/libs/ardour/session_process.cc b/libs/ardour/session_process.cc index 2c3b1fe982..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" diff --git a/libs/ardour/session_transport.cc b/libs/ardour/session_transport.cc index 354f38e7a4..1937dbdca5 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" From da2935c2854af9767bff00ed7e6ef40dec58db11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Tue, 20 Aug 2024 00:18:03 +0200 Subject: [PATCH 075/111] Remove #include --- libs/surfaces/console1/c1_plugin_operations.cc | 2 -- 1 file changed, 2 deletions(-) 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" From 843e776ca84f9fffa3bcd66d984ddeff1e85f57e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Tue, 20 Aug 2024 00:35:05 +0200 Subject: [PATCH 076/111] Remove #include --- libs/ardour/ardour/monitor_control.h | 2 -- libs/ardour/ardour/record_enable_control.h | 2 -- 2 files changed, 4 deletions(-) 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/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" From 1752f30a66aed4ebef7cdc64859deb70b2a3a82f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Sun, 25 Aug 2024 18:01:28 +0200 Subject: [PATCH 077/111] Remove #include --- libs/ardour/ardour/audioanalyser.h | 2 +- libs/ardour/ardour/buffer.h | 2 +- libs/ardour/ardour/midi_model.h | 1 - libs/ardour/ardour/plugin_manager.h | 1 - libs/ardour/ardour/port.h | 2 +- libs/evoral/evoral/ControlSet.h | 2 +- libs/evoral/evoral/Curve.h | 2 +- 7 files changed, 5 insertions(+), 7 deletions(-) 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/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/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/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/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" From 3abf0a905864761c5c7b8c7cbc1f5ec39de2f182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Sun, 25 Aug 2024 20:18:01 +0200 Subject: [PATCH 078/111] Remove unneeded #include --- libs/vamp-pyin/LocalCandidatePYIN.cpp | 2 -- libs/vamp-pyin/MonoNoteHMM.cpp | 2 -- libs/vamp-pyin/MonoPitchHMM.cpp | 2 -- libs/vamp-pyin/YinUtil.cpp | 2 -- 4 files changed, 8 deletions(-) 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) { From c0062fff86079f3e38c2def489551f8708e99ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Mon, 26 Aug 2024 05:53:54 +0200 Subject: [PATCH 079/111] Remove #include --- gtk2_ardour/automation_line.cc | 2 -- gtk2_ardour/midi_channel_selector.h | 1 - libs/ardour/ardour/clip_library.h | 2 -- libs/ardour/ardour/utils.h | 2 -- libs/audiographer/wscript | 1 - libs/pbd/boost-debug/shared_ptr.hpp | 6 +----- libs/pbd/boost_debug.cc | 1 - 7 files changed, 1 insertion(+), 14 deletions(-) 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/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/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/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/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/pbd/boost-debug/shared_ptr.hpp b/libs/pbd/boost-debug/shared_ptr.hpp index f64b582d99..39aa0111f0 100644 --- a/libs/pbd/boost-debug/shared_ptr.hpp +++ b/libs/pbd/boost-debug/shared_ptr.hpp @@ -1,9 +1,5 @@ #define DEBUG_SHARED_PTR -#ifndef DEBUG_SHARED_PTR - -#include - -#else +#ifdef DEBUG_SHARED_PTR #include "pbd/stacktrace.h" 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" From 17275239db6c721852aa629bef724715847b3cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Mon, 26 Aug 2024 05:56:56 +0200 Subject: [PATCH 080/111] Remove boost-debug --- libs/pbd/boost-debug/shared_ptr.hpp | 487 ---------------------------- 1 file changed, 487 deletions(-) delete mode 100644 libs/pbd/boost-debug/shared_ptr.hpp diff --git a/libs/pbd/boost-debug/shared_ptr.hpp b/libs/pbd/boost-debug/shared_ptr.hpp deleted file mode 100644 index 39aa0111f0..0000000000 --- a/libs/pbd/boost-debug/shared_ptr.hpp +++ /dev/null @@ -1,487 +0,0 @@ -#define DEBUG_SHARED_PTR -#ifdef DEBUG_SHARED_PTR - -#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 From eb72aae3899584c8b378734dfec1b7357984deaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Mon, 26 Aug 2024 06:10:03 +0200 Subject: [PATCH 081/111] Remove #include --- libs/ctrl-interface/control_protocol/control_protocol/types.h | 1 - libs/surfaces/mackie/controls.h | 2 -- libs/surfaces/mackie/mackie_control_protocol.h | 2 -- libs/surfaces/mackie/subview.h | 2 -- libs/surfaces/us2400/controls.h | 2 -- libs/surfaces/us2400/us2400_control_protocol.h | 2 -- 6 files changed, 11 deletions(-) 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/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/mackie_control_protocol.h b/libs/surfaces/mackie/mackie_control_protocol.h index d91bfd175f..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" 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/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" From f16ad8053ef540eb2caaca96f71e88cb094bff95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Wed, 28 Aug 2024 22:23:13 +0200 Subject: [PATCH 082/111] Remove #include --- libs/ardour/ardour/export_channel_configuration.h | 2 -- libs/ardour/ardour/lua_api.h | 1 - libs/ardour/ardour/lv2_plugin.h | 1 - 3 files changed, 4 deletions(-) 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/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" From 2d076cccb1286b0ea662009d9147dc06592ba643 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Fri, 30 Aug 2024 02:18:49 +0200 Subject: [PATCH 083/111] Only enable RegionFX in debug builds for the time being --- gtk2_ardour/audio_region_editor.cc | 2 ++ gtk2_ardour/region_editor.cc | 4 ++++ libs/ardour/luabindings.cc | 2 ++ 3 files changed, 8 insertions(+) diff --git a/gtk2_ardour/audio_region_editor.cc b/gtk2_ardour/audio_region_editor.cc index c1aea81769..e76c086fdf 100644 --- a/gtk2_ardour/audio_region_editor.cc +++ b/gtk2_ardour/audio_region_editor.cc @@ -98,6 +98,7 @@ AudioRegionEditor::AudioRegionEditor (Session* s, AudioRegionView* arv) _table.attach (_pre_fade_fx_toggle, 2, 3, _table_row, _table_row + 1, Gtk::FILL, Gtk::FILL); ++_table_row; +#ifndef NDEBUG // disable region Fx for now _region_line_label.set_name ("AudioRegionEditorLabel"); _region_line_label.set_text (_("Region Line:")); _region_line_label.set_alignment (1, 0.5); @@ -105,6 +106,7 @@ AudioRegionEditor::AudioRegionEditor (Session* s, AudioRegionView* arv) _table.attach (_region_line, 1, 2, _table_row, _table_row + 1, Gtk::FILL, Gtk::FILL); _table.attach (_show_on_touch, 2, 3, _table_row, _table_row + 1, Gtk::FILL, Gtk::FILL); ++_table_row; +#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)")); diff --git a/gtk2_ardour/region_editor.cc b/gtk2_ardour/region_editor.cc index 44a832dcd2..650dd1c73b 100644 --- a/gtk2_ardour/region_editor.cc +++ b/gtk2_ardour/region_editor.cc @@ -187,8 +187,10 @@ 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 NDEBUG // disable region FX for now _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 get_vbox()->pack_start (_table, true, true); @@ -230,11 +232,13 @@ RegionEditor::RegionEditor (Session* s, RegionView* rv) spin_arrow_grab = false; +#ifndef NDEBUG // disable region FX for now /* for now only audio region effects are supported */ if (std::dynamic_pointer_cast (_region)) { region_fx_label.show (); _region_fx_box.show (); } +#endif connect_editor_events (); } diff --git a/libs/ardour/luabindings.cc b/libs/ardour/luabindings.cc index c0e558c763..9e96eb46aa 100644 --- a/libs/ardour/luabindings.cc +++ b/libs/ardour/luabindings.cc @@ -1638,10 +1638,12 @@ LuaBindings::common (lua_State* L) .addFunction ("has_transients", &Region::has_transients) .addFunction ("transients", (AnalysisFeatureList (Region::*)())&Region::transients) +#ifndef NDEBUG // disable region FX for now .addFunction ("load_plugin", &Region::load_plugin) .addFunction ("add_plugin", &Region::add_plugin) .addFunction ("remove_plugin", &Region::add_plugin) .addFunction ("nth_plugin", &Region::nth_plugin) +#endif /* editing operations */ .addFunction ("set_length", &Region::set_length) From 22411416cab59f7926026823f1c9c30023e7a365 Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Thu, 29 Aug 2024 18:28:04 -0600 Subject: [PATCH 084/111] add new member to a MIDI surface to allow receipt of data before surface is running (for device inquiries) --- libs/ctrl-interface/midi_surface/midi_surface.cc | 5 +++-- libs/ctrl-interface/midi_surface/midi_surface/midi_surface.h | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) 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; From cac849fe6d5fa3139fe0b5abcb9f87ccdaa85982 Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Thu, 29 Aug 2024 18:31:00 -0600 Subject: [PATCH 085/111] add new novation LK4 surface support --- gtk2_ardour/ardev_common.sh.in | 2 +- libs/ardour/ardour/debug.h | 1 + libs/ardour/debug.cc | 1 + libs/surfaces/launchkey_4/gui.cc | 283 ++ libs/surfaces/launchkey_4/gui.h | 101 + libs/surfaces/launchkey_4/launchkey_4.cc | 2715 +++++++++++++++++ libs/surfaces/launchkey_4/launchkey_4.h | 408 +++ .../launchkey_4/launchkey_4_interface.cc | 88 + libs/surfaces/launchkey_4/wscript | 52 + libs/surfaces/wscript | 4 +- 10 files changed, 3653 insertions(+), 2 deletions(-) create mode 100644 libs/surfaces/launchkey_4/gui.cc create mode 100644 libs/surfaces/launchkey_4/gui.h create mode 100644 libs/surfaces/launchkey_4/launchkey_4.cc create mode 100644 libs/surfaces/launchkey_4/launchkey_4.h create mode 100644 libs/surfaces/launchkey_4/launchkey_4_interface.cc create mode 100644 libs/surfaces/launchkey_4/wscript 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/libs/ardour/ardour/debug.h b/libs/ardour/ardour/debug.h index a8e127a187..3e6f2661fa 100644 --- a/libs/ardour/ardour/debug.h +++ b/libs/ardour/ardour/debug.h @@ -69,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/debug.cc b/libs/ardour/debug.cc index 2cb4becc65..bc715bcacc 100644 --- a/libs/ardour/debug.cc +++ b/libs/ardour/debug.cc @@ -64,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/surfaces/launchkey_4/gui.cc b/libs/surfaces/launchkey_4/gui.cc new file mode 100644 index 0000000000..ac71bb0dcc --- /dev/null +++ b/libs/surfaces/launchkey_4/gui.cc @@ -0,0 +1,283 @@ +/* + * 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; +#ifdef LAUNCHPAD_MINI + std::string name = "launchpad-mini.png"; +#else + std::string name = "launchpad-x.png"; +#endif + 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/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') From 1c9d159241ed771c60e5d5b299ec2e09a9b3a829 Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Thu, 29 Aug 2024 18:49:54 -0600 Subject: [PATCH 086/111] new image files for launchkey mk4 --- gtk2_ardour/icons/lkmk4.png | Bin 0 -> 27900 bytes gtk2_ardour/icons/lkmk4mini.png | Bin 0 -> 116597 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 gtk2_ardour/icons/lkmk4.png create mode 100644 gtk2_ardour/icons/lkmk4mini.png diff --git a/gtk2_ardour/icons/lkmk4.png b/gtk2_ardour/icons/lkmk4.png new file mode 100644 index 0000000000000000000000000000000000000000..8d546a9b719f1fbb18642724c8b9e22112f7ca6d GIT binary patch literal 27900 zcmeFZWl&trx;8wxyZZox+c3B@=n#Ut+hD=nJ!o)(1Pvj95FCOA2rj{$V8IE&gZoFG zXYYOXsq_?t_tkx0eRcQh9*I>~Rm8!h!~_5UI7%=%O#lD^_H^%! zj`VcS8@aK2y7c*L>w9Wi_yAqqU2Nz<0q(eUYFWh<$rWOoJeW zXhS{u-e>vkhF<8DvO7sMz972Qg47Y2gy_ozN49*2QH<&rI+QOr6p z)S1Ziy`Mbx2zD50$r974rrjaGZ9%gR?+mYQM_6emO;e;DEwsB~*381#3wOHTW`?e@ z_VO!_MM~2$*6#V5k1E~IuOr^97j=v9V+{9z6%IK z0LU2#K_UlkpInTvmz7mll9m0ZM^Ap_2Bk>Ax}~Xm&5Sv!xcc*pZRAmFuv| zM5=Rq{$=jIOU7>x(=?_=nG27gd!>U$W@|!Yi~>DI>iOQOG$H@&&P>yeV58GbYGpZK zsSW9Fgk5VSANlQzj1(oNNaJ7~^mc}q1b~g0XT#Gwa=bWWCti4TW=HGdL$^x>MP_VS z*vA+>7{yeiC#WaG#UDIYSQ4vi-{Th$sgZj^*A**=xWBq_i|p^bAz;jT!;*-P^?^I% zOIA8gx^K2{@kh$7TMurI+(zND8BvEHX8gto#ujo!M9 z98|O=O`!pmO+L7UM#!3%%YEOYJ;L?6yf}K}9U@i9?cPrOI#>k&z>~Cp5}3Zqb5Sc7 zCvFS4i={QUuaoPO@&EvFDPLC$D+g;&pry5~y|V0|C9AUegDh&$x1~reHR z)E zyuzaV{GxokTwp;_@W0u+!0m1P{%_WQIuB6%?4Jxa^k?eEavK}Y+) zx)KohSEmrQu=?8t4-0Q=_+RyW!umVO%Fe>s*7|Am_=jBo8E^l8kb(t-PsEZB4CWHC z5P4FQ5FE~B0e=z&#D-Uh*GAYHF2MV@n*N3E;bP2rL^{<9v z`_Fapv9tcu3QvS_frXz$@)u#eqJM&a5ytcX-bG7@H9uU$QhOZ&3|3EALo9zGLqBzf=k^i5%73cXc%KqEn-=>PE*8e;1X$pFp(Rltf zrTs$|Pfhaw^7W4%_5ZR6An<<+`ET+2zjXaCUH>fx{#(ZXtFHg0>%Yaof6MrP)%8C{ z7v_H~imjcWo@ITWmb@(ohuHuC5 zA))-&NK6duzaxo=$O!OB=x7;esDTvZG^nW9kB^TJcXzi}S63Gom*-~}r>7e?kBbkF z=f8i?JzkxioSdB;O+7rWTs{6iK0Y}*`h9pX`MAAs@pycA__*6?%wXt!;QyZKYuR%di=Gs({s0cH~+Y|`uKBuYxMB0 z{rs-?>|keeV|M3pdvkMpWBqdGZfkvQXW_B!Vs>No$N0w0`pWX^@?zK4<=XW9%HqP# zi^|Um!HPp0})b-R=G*q9ft0=2If2OLer1A{*TuD(GrtnNrUP(a?CNHa~Af+iM zqwsWy$*IUfgQAsfo2|jTNQDF%Y zVKHGrabZDGApucAeo+BF5dl77em((yh!7vIAmr)Un&;`@e_CMja6z~^c{w@29PB(C zY~1W@+-$5|Y%CnCESxON92_7v7AAIPCUzze8;FsWk%5H)$V^Yi1f&Jg(J<1|FwjuZ zQ&G}UkkdXL2F|_ar}{Z z2tEBXbP)CShr#2P?zP?%R}6BiM4HAS&(Nqf>!^3wS;|%(B_C6?XJHv<3?O>t+V>Sh z3haA{_$Up{BwqQp6Q-_M+F$!L)q`-Xb)@``$StHuYH} z+vi>zEp94RbDiTa>M6-zP_x%R`=HHEg4tBTuB)e~r>m!{n~|BBIiPQB478%{r>3X_ z=Uew_iQ43kx>`|wEnY+2*sY^zl^9u)>Xq_U5OgJSIX0{t+uqLa-0ONZmPXY|)5_Go zhuA8$-!sXu|84R&;Xdj9(l5v%^FH@?r}=>NXCHzS6_NVi&vZ!T>6QrnT(i;Vv-5)g zJa_2xB4@4}bga>L68u7gA(E$>IdtG1ARdzxN$?103^WBQK^LZ|h44Xd`70@~{e7{x zD5zoZ!9uzCvKRsp!qiv7qprD3)O^5#U?R}F+-+1zJSpI1iO@?-;X3H(s6+(Whz*8| z&{wdOqPaUK8HVz_*nGE6E8Bo{^0Vm(1$q50X>Wf-JpJ{~^q$1Hm|^73!|u=Ie8B^+Mn+1m5yV$BGBy~3jSThm z<$^b(kVup^(Vxamce!|1u%SSVhHO(OltGUCEi7)Lx&hVwGeCZ|DP<%j^vpj986Fx6 zj|GXOmH;(glM_J|)Ta42(hZ^N6oadfa+(8Z}vPhkYjBU(wqUt!1e(!vp&4q@X(q$;fJ z!fCo}t7m01t^v45$B0Qgk+f!Xtz^=k%_h!$iNH1bnIaJ+Las$9Ni6j;#Sktsq6Hu} z^^EUG)zw(Z<1CLcNeKTv7i~h6tlMv!pXqHo&tLGP*BcUuJj$TVR%o(jT#39@dcFNk zQx%&S@slN%JvOb! zwMf%Vk}wJ8@Ekcj%$%AN{|udaZlXG+=neVBE$w8t9}Ey12TFzGUi{c@4f0q8_8nB$ zDYA9qKE}Ls4&Wq=jlzIIT3xtaj4VYeMH*7z|J44pJBSf!DUymDM?^#f4AVz%oxZy2 znM`F^%Qx~B6qIZRSFIrpl-AUgHh({V@9*tw>F9a$eR$|V0GDygV&x(7K|7tm30?;fhQKVglK2V~ig}L>N6#-bnZ45|W>sbH2-m@90^C5A^#2_yS!rNB; zR;he!bWGe2f&?u3)!dAw)@PePGT#Mox1<(YhJF%JCu_2P)%{VuDoHB1Ji*zCu0YJk z9h+1wwXlRDw1hksctV%XSOP;#F*knaZcL9a5^s#!)BJ4x`79wq84{E-tffg z+;tFdhXF(J^FA>23=Lgr41tIA6%$@A_ou;Cs;lP9`KS~s=_tk&Dym1oUMexl&oY;7YbahomNc2xg82RW8SlBb{_2|ennIx6&fC(cA78#U5%t%Z^ zx12bWjy{m`AlHu$tv>H;9c@mi<2OXIg;UJd$tS5M7*ZI|^GPggqvzg$Blk87UOtRAG*ifxxesLXAAb`sN}%4qMc|x#0%8E}I#t-2(0MN@Rr1 zZyW7wHt2nCp9xJrdIoEd33Y4WVu!B1vC6*wzPl0@*<5llFu7D0o4?lly@Wf%CR@6@ z%QAgBZgMvKVNGn7Yp1|+Ri3=r74xEDi6eTc>k! zZN87S7<~!3{n&c6biGkj=3)G7j7bKwY}sPMN>Z|nPmEeupnbjEk48{WBk|O^Zpxl1 zb`#UV2VxQ-Sy!QyF<~&f!oY<19t_QMJS*2_sW? zd2J45wZ6W50{Kz;qU%VJJ=4+7iO${+A5`v(C%^m@6_u1M2HH;biSLECefHa);EQs^ zjUEXju;Iqc@#Y)sXA~TNi7;YF<1JJBPI1RB0H-G zHqtubNuOWTmz|!P69>0Li%fG(j^)Br>{z>oN5!orR#mB$HqNH3k4`>#uw-8FC&PKT zcHd=0_I}4otD_!xGdQ>#&eHW$0o@IOA|_?VhpUV{pz~I z_5gg{kaPLgs7jcu&${4y;wzrPZ~i^u-PwD<``OQ^1T0y|^6lVCzRaYm@ayI?)PwD` ziHRDGNDV8nu$IV9B69q8B zkd+!{Eb0s=-j&;2$*8&1wU9y#a8Wa?7mfNz@XCM`W?n}S=XNs``zChux86(7K33L? z=P$BUm+I-hcKD*9rfxpPO8mk>b)E>7u1eG10fqqBk}k9eIWcV9dfvxsA7DhbdYJVZM{{F~2(6T;Q=ub%c8qnmH|OWDpC&5=QIl#S(yFb~C|mn4NDn0HOt#dxn}qSFsR zp_nWyi$xHzY%kQBPeIOGEN+KAvHLI)d2PT9gJcG~HclNH<|>yg zui}sqZ%?0`gy}URp6$CNCRKqAKf?KkA7(ZrgnX?d!z7ygIbC0nlzloM*edG}jMl|S zCqMf6^Tkr3UfKC*-bPtw5iU}b!ck6{9-b9$T_oA~?j7%dPW+w$3eLd9Cy0?!hqtDL zKiKBB+5{>bZeJ9JHgCSknjeF@j;8Ng#HZ3szcXp-Z(V0F5UM9Y58F5Q^>%C_0})K( zCnpbxc9oW8HhlL=P@og-S5tZ?q=3`Ujoq zDe{S9>QkDNG~3ESW7f&Y-uX5?Zw4y zFcIzj_Gvand}*gSIY0kAvlAPwq-Oa!M~Z9YHdCRaqPj&?{hPUsC1%*MPc4yg1}k zXO|aKP0pY5d=|>7=GBdaDyA+rv)!CVc&Z)FmHGHyB(Nt#jHktO{A|1-x0%a=_rqto zx8G*c(#lHfOO>u7>vP{~){FD<*44gCLVyj7WWE{1BIADUBpC!L{4NpVemI#DbTdd4 zsbu9NErLLR)RXD<>)yLx;GgQ=z`QK^QAKzF!xrncUwS^-8m zqd1GYU_uNgAS8=3(U%{0mL$3hlrq1tp!6ZtXz*j&Ykqy2;xfuEUTJ9=KZ-b-HoweM z=K4LiH0r_3IDhiKoDGo5+jEkNPICmMbua@=cs&-QeuVOwFlPoE`pn*5IL999NKema z#ukr8g#EU_R!DVTx0QxTqhgqK?;OZR#x9emWOuS*A+gJo~sUA zWt^+NMeeN`NmxV5BiwLzji3ikSr4VkSa~-eC2O#DSI9-)9Fa5yUxUNy04$0xt*PGrV zj@w>E-*3K+G#OVp>f+(XIP0eMQuPwNk|#1^O9sUdz8;J|R=Z8+KVRc~+R>nDd>8#J z-EzjGBYDjlMQo7E^elDAIr>WJj_V+c@ma_>i*X6MJae1=zW#m|vyr6dGTyX{VOK%| zchA2%2ear;*%x3-XtA1s#FMjalv7y|*lZOd*h1b6n&dhxK{2kXQi8jOBm#{$fb7a*uBJzz5 zt0hv&2r44*D#EqojFkAa*oxS+m{2T&v!!KfJQmqciHnVraf^-D6Llqgr~Erz{^O1( z@H;rrsP+%OZKf3C2Nswtn?oEjZZU4oM81&NmsfVde7!-P@Zea|wtVp&K|Dk0>uE*L$0~K+5N#gOBfI^PMiq zdbe8`!|E!b#s@=QLCHcy`EYxl#6$^_tL}-9A+{Edj;2K?vegk{BoX+}k2$~d6D0X~68$4jQe2ye13BH4Qw)9CcSC@{`rhW>(nNI-a+UdhW&{xaPkC1^26v!Uz zv}dI*viNY*JKwb(m_GQFC+-fvG>J$0y}6*UCU;iM09BF z=W=y6D>f1kF2@y?D$LeoTL&iKx{T<7;9*pSTJkPE2fjFdo4c-^x5%7`|joPg1vOfzK)aFdYqIj%Z+AW4~0F z=lm!Lnle#=0~-;d&JThisj2!Zq+_=7A9XnMvS(&~7WMMQ$`uDqp+m5N^z<*sX--(R zy8oiV{< zld8uXI(+zRBzs#cw=KQ4qKJ)BId;*znd4)9&fB7|Bj_`p>( zOP1%&#>H(KnwXf_38T>HOWwN!B@<4JiGc3y<3nniJh2u+<7QW=i)qsK{A>bW8qq3M zH^#?y`flkd+|uc9CoE}X0yiHOOARRtviKJ76xY1&yRPQ>K6Eu$YqOU0DUj)A=3$)w zV%HYL?+_{~gA)=G8j_(;)Hh(`Be4$JQ+>iGJlq+Zf#&j_Q+|m7hHV|qJs!XM`p7~a z^7xAT@+eqN`XM>=PO|1eEBS{M%5_`8o!*X*Nb;jUDqI^6xLidoDE2D-IpT(3u@reL z(V$%1Nr6f)f*H9IB8)q!l@djm=R`G9ckvWU?XjcwvhDa3)0cM;)t1ia42DRfgO zd0NSIA@?Om_m2_rC*3zGc8INz(C5D!e2tVSox!uKFl(YThT z?rb{M|IKisd3)_|ZRi{h+iNVfDWZ|MW`kN<2NFoPHI0dIc-y8xZ9Qr)a8%1a+&g;2 zk93nK28i%JQ)@E`R_$k&LidOwUuytNn`kx%t$8EB#I{9(=`OAjb6zg&&C&Ob$!AZ+ z-~hWdwBcC|JQrhTZT{z10O-vcNf*oU@lWJ0t<(ct#)F7TOmIzFR*hx>-96??WhFhJ zeRH9RNlV|db%mz}$KY?O7T1_z9uAQVxC+L$inFNkl25#__YDLZIO;NtoWqPsSbi&H z=1V_+hp%s==!7mf3Sf(=LhA3_;_jdM3oTSH&G~onifA_zW*YOdp&Lghv0Gjsfld zev7T-oPu9iQV49;;eOH54vj49eEkOMJ9%PUu0{U!=KQNs@ckZL$jxqv)Wf;dTHKa>KY-w;BNB=;^W1TnGE6?-q-iQBJ@dXpK6S6c#C$S;K6Qp z;Vp>`pb}u{>{4W30RRkWrO8D>{iTCakzD;ut}?7#^X{|i=`h+|~d-w%%}{f~}j zM^^<)SK-&)N5Pjo;HUSa^YuR8(oN==PS z_e$tEg#aIjw=#Q({?@;NcNEyQIm;dXw26ta$~vI$xQBE_%j^vKEY3S9LqCeBz*9y^ z6C^G!_<||bQ&=O`fLw_TNyAV$bx*;WGW_{GVZP{SpTL_Gl+-U{18K0=TCBt)fKk5l zbqDlWXqC-f?_DHD>1s%`tHSTg9ty zcnnu(!A&!)i{?%+oSN#_?ZOChn{s!=eNo#ol_`=z5BcvaV0@0hSksC5!H}wRp!GdN zn@Gb-uaj3`o2yWrNcMx@+^edlgi0w$$o_htUR5JBxuybppyHyzPQ3Gn60=X=*L-3S z5h70jWp%SLwTNc2eJa#Bj!KIOeYvMKro_EvG!j$jPp#OYJukaAmf?4(XOu1Yqv9yTd5>A#1YG>|{okT`%}-?s9R0gPG>A)OD0da&P$UvEedVn3*m4t&*S zh*E_*yC9{@5lS!Qb%HKhVKsRS^f~<-ry40G;hTs!cS_-pP+`iEWYD|!eF^+Z$_JcE zU%C|uy+^0ULpxlXoCZevvWy_ zxL*q8&~S2hpeEC1oTHXg!v+|I>}WA|@pM3J4Q#MV`;*=N62HyMc7vxXox&W|qzQ5R zw_H&h!?R?a5$k;cX#;bG#|<50#aNV;f^tJqh%t`{&IRGv@bVn)i4qVm#u~M?Fm4H3 z+)h|4dekQ{#@$kAW_06l>n!sj)~h#qvp^G8cbNgDwNY#d5sL~#+i}0QwI|7o4qv11 zhWD%!;Oz8{A-{WQKIb!UQtiIH6Ef-aw3dYi`)4PIJl?)}d1rNZnf55DGEfH&8lN%oh6M8?8MLy80?YNPM&`n@)ayU4!A0$Xs3BbO3r|uji#F# zlc@BZG)~mWFvo?U{uq1?dc%R|w@aEYqy~;Dm8h(+ImRTe?U?as+5P=q$bbwZo7R`% z-PBJ4ka%Y`Ky0%yK)-Qj$5x-n)XE(F*0lAw$ew6uRCDu(t@OiB?UVv{6|B2Vo$rA{ zRPZa}O>g5WZSkaVEsthdCBU`W334bh-QcN7jMIr$c)2Oc4n2kLk8> z-rkyHW@TkQ>p9A|1cVI7$uNs23kIPjdwF+03d4*d3IMRcV!D<%5#PMf3x zoJfU%pr(I*7vOux{5?-QIrcRpGz8zwTzoYN^=^fBKKJ7G-9t@bO;~5)$w&;X^e=?> zdZ*S$#bb28@q*5`##i;s63~X91)~WIiPtYOSD9o zI*44ob6X$yG%=i3i%=X#M?7?Z97lYvZf~@++OP_Gzou`GW;bPLcrW(2j$6*)FYirgVbGA z;v)($W7|{SSJqBp}_xHgJa$U3(`^8BgM#%*F7^G!Vx_Z{av={d#8DNaw zrs6A82(V@T3U@}+q|z@5RF{J7b|pm{%04@EO(%32XR~yDUq>B-@9B0cMjua0&bG<+ z>5i5I0mBB2ck<2|M~d_dktIomqjW+H~GN@Qd`&L-?EBA<kSJOAAZunPJ~Y=92B^X8{)W-a4h)d!Ny<-M^(@ztQnM-}$^& z=vNr#NcY^RWVT)10ee(`IN0snQoOJ7*3vDa^IOk`sZ|y)xzj>s%f}%?J_;K4yg^Ez z?}X*caDkS0ZkexkymL(3KliP3XB-`8=MF0eFOevT2%5D-s6yjA-O2ALG7#g31_cVp zogoBQsFM%8xk`bx(tSlQH|>8GGvRWP-XGadZkbY0<&gVWQNixvc5q|saO4TMwUhDV zNDSl57f58n;sW;l$U%XwuDdeY;^{gZ&W^ zsC9l;ts#M)cIJ*W-b%cJ!gs!-)d);ITJzL&W{psGAqhcoh`?@7SyHirV&Aq+t8*Nd zO80?MRod^8MdCL)B=lXplp+F-mJ6$Bmc|_-OccY(J*22@9f`%S6ZKTJnFl|+ji7PY z)f#-pdbaT`T)`@`cJX=k4tzO#@I_6w>p2IEGQqesxDwoJzo6zjap_R$>Cz4g}B8t$@o5d&>xbAX*nOO zmE45Vr=pRsD{L$}V59Dyz(wWbw6r9%{Y%CeR^=}a2J?c)t8()F9w|q5ODTq0es_K6 z6)C0obwrb&7Z_qZz8AmeuvaWl6suoLK!SEPiy@bmV*J~I-ylEFufSHVU}Tlyo}yO0 zz^FFq_5;lx%@N&C0UMLn&oG`y)Ah>p_hb(Gm#%XTHo`Vx*w$xRYgwpD z8{nt}d@?(wz}1#5$qZc)+N2!gSNjZnqqvpP7DZ0jm83o1Q3)BVd!F!YN?4bpp->u5 z8eSK0><4ZH4Fv(ad142=2i(4HSt;7`J)2uYG7JOaq-uf4FISWx z2&Fp@gIhz2nLH+mOumbyvc9gq*DsS!Uk!^1QY*h#O~l2+OHzA_t5TepWO4fwM3Urk zf~Si6^x`>@E>H~_9zrdOJnhF6(N#W27h~M)=~^A)8eYQr zuubiFXeQd>$>8OV9^!M2K4UrN=G*FM_ef|u>spJ}Y{VHH7)-bFt0$?dXJ00d0+w$a zRg#$0F~^e1(hpZ$jafI^8C1<{?L+p(n`AQsLN6YFvcHKL6p5Z<+qnd*dACR0@Z(E{ zxcmB2B$ChlG?f&QA^sx%vGH<@raR&^z%Iuz{EIg?zOe8L19h6cJToC674Nn)Bmjej zX3Q+gWSa>O*EQuAlca%I4pNO&YU^ukMHv^h_2kIY#7pU4cSOF-8NyBbKVII$9Q7Rq z)yB6hkS0#WU2(*AER$6gkazTtYhCa9Tkm7HSu++`dOWXbN7}0KQiT zY2f#?UlhGt0mlg=2vuHNkX+bU4Iq`P?Dle4ca2ItZH+xnCXOr5_7o z3~7z>ggTf|Dg@m#d`GC}pm8XaGO)&D@t8KCf1WXBGgnTgaPIcYyiKrGc#n%5;J=4MPMM+LTE%t!xI4Rjfc zyIh@;8LBoG+++PxiB@FC&QB&9l5@qCStF#kZseI0&a@k#>j&DaZvs0^LQNArYrog; z)(J!0taUypA5>9{Y!5;6wj)=3L4d|R(TTO*gJ1K%KRw%P?aAWWJBjd$uvm7JM8!zD zoJTdC@)ki;E!&uXi6!i781qYXrx{&+gD%^(z;CDK`*lW%j#Nk`v7On^x$^#7^V&jX z+T<@DkL0i7i&$I6^N1vSGJOJb(W+}fI7lvzK^}guMt5_=pC5CNjXhV9;xp}C84boJ zvsMl80Itg=awjIni_<=R4As}i!t)hX3`QHDl#rN!MF`;W4PHMP*wC0WQs?pj^8u{x z23J>C$2TOryDbxQ<8g3gO#N0YcyoyIA0FBt+GE!q!aAWv9oTOs*Tf$V&k9K{T*q)0 zBdqlD@J$)Q5w2!}SpspevscVLzZcNFl%iVLfO5L!$DehRPhR3-V@ry7SLDP`T%os4 z%|mMTX{2xFkph-}!34!cwCCQrzF&-7VXm z(x24dcX=6T8gl$=xF;<(c7DwP9UT1v#>+jQNQ4i+FmCeqGKA`un`X6wNhIHf52I2v zhwoval~h{lFVtO}U0hsnTmmuTsN<#lua4J{?u!(YT5c~Q0FZ`PW1eVMzMV5$hiI2{ z;`&RSWi*$lhh4tYL5*d5EBDoC(zThi-pg%^GXW&fC|yypqow94k0Vvvo#j|$)}afV zRc^=2?N$G+ow+k$bNA7YyywtW!S>yLvo@-rHXFmnf~9`F7NIby zV9(W;`ZTpG5kmgQ6@c(fiPpIpuOI!g$u4xAv`cx-KTyPGg1Tgc14ksALLMI?YJyOu zF^g%tE+lZMd%6|FiEr8?!ko!xg$e;w^K?f@#C*}bs|@-F(FdI;EA5iv2}nqNxx#BO zY4<~Stz-01acaqMOqMRQrbVyDy@yB>^y|Xhhs4E3m&UQB!=`1o5FWrNCwbfQ%It~- z9ujQqA#XNgw&4V1i1Y}xva*u3WmyucZ}&71;>_Q;=!@#hxlE)^M)G5Jo}QVV^{k(s zS^h|c&H#JpUTA5#;YMIi>YP|uSh#6uY|>tF`czR`S`n;?dwCw==4%tQ6CC8^WKt0k z#y{mh87JjJTV7e}wD0SHmb`DNb)V4JFlRb&x18UQlOU9HPHDs%>Wa*TxA@&n_3xo!x@B5ML^SQkB7}5rvU7*=cC)lJ)AFRC)jaAQ{ zaj~&wq{o|P!a%j{=$kRyo6DM|+RU?y^i=h^+RDn$;9n0oQonsOM8*x>$@|30kv<1; zp7(BV)zDzBGR82UBzGx&J!Fj}#6C2{k*)f+!OQpcjDXIJ03I?eEH>>v!BzEJf0@hl zjL;QK$h)d;Yqr8DO`{y+hq%mW+ZSPq4!SlZ{X@wYUxTP}sPlaC_vQQsFNt>(m+V~} z_I`@UB(Bx;6&sB>F|zHKme9_wi6!XH%`Vv~qtMU$8Vl&8>(rCES!eBWCA0I;$?^F_ ziN+~|hPReyTlq`%;Oj42gr_wg-^le@`;i;!&F=SwvhZ-t;MqJg}4 zbo9YE9o%G_(z_4Q|RSO}Ga zAGvaGPo6(}g{91HqIw#2onE79sLg3~Y*9<$#L2}?dZ9N6Vw$AVBLLNDv-6~7{@CDi z1+=dGiu;W@__4LN_S0nKeV}($TTMfQ|D8aFUjUw=v_g{iyEQv_!27ljrOg{qqJif6 zii$R;*K{H%7?;SKHUSPE%3>%KzQ&8=u6159p_@OdD$C20CAh1~%k7ocHQ|>-ahQJc zGf#gz!t+(nh&??WSB`$y3w5zqKJs0$O;hGG9sP4*JPyn?eWR*YM91PzaSJHaIY1{k z417t@ei|vk$^AXfE9UeRvyu(g z>t6pE$fNeeM~<1g{#ad?8JI|(!NTHN*|!KMx~5fMTVH&q_j{vVn>Od@QCTuEDao9% zmql+i5aClTcRm>|a6-$#(2$)vC7gB>*OBekXYtpkPwWZ?`yUUNzj|#bEO!&@jt>^j z8YMI?90~~v2nYx@er~`%Wn40>pGGs^kV~l?x!5i)Za8G*{PKN`65oFapN?aA^B$9Y zGUfNTxKB37yB=+vnVH`>cu)&Jx$n=ym1@|CO-tYjFG@)bv> z27-d|5p!$DQZK3oM!1?T(2eB1FT9;iO@kfCXY3EYX5?_Bp1Pq#dtDbT?fG@B;Z^Ny zZ(m+P-7|hVJ#9RF>FKaUCrPF!}U7)eIe0g!e-jz*kJEVXV5Vj#T=V4@WK-@?DmI*bQl122RJI=uGT# z9MkCT$B%8|LPB`bh9l^Nuq`*>EdY*yVsFsyofrPnMZy%b7SJGNY|tGEi>?*-1pupd z;v{$0P+-F^KLYWf&hzEC5l=tO;0#odjtVitwgCX;W!$!O+&>(Le|-P3X_Gq~4uC7g zZr&urn7Pso5I{qi`0(B8%y7q#6{V=j!ffE_Y1a*fy*5$w;+WsFK&f1W*tW9@F0hjs zhw%LDp{%2Mb%YKwNayLFS@cc$jdfL}!|laX+=YX-$O@6ͬ?Yo)^Z8UUK0;m7+U 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 literal 0 HcmV?d00001 diff --git a/gtk2_ardour/icons/lkmk4mini.png b/gtk2_ardour/icons/lkmk4mini.png new file mode 100644 index 0000000000000000000000000000000000000000..a843d5243a78e2029948b42176996c7d62638d3d GIT binary patch literal 116597 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

g2aY76wb<7N=n{j*JgM%&Aix zSr+dD%ZUVJ0G85%saeoB)_R3;B4`n?+`N@Gk!8Q6VL*!<$i)m;OwHn?7HXIF*~o&Z zB5`F`oQ7q#MivVaJxqabz>-coh%5&ZIT%6f6bBr467ED=h4WOnNerrAIXj${A{wNh|`uIvQKBrA%O z87xin1-TSicx(zyS;k78hms=J%!V^oX_G2`QMBPzIB0XKLK2;BiU=>%EGd+kS}ALj z1*UnM+ibl;k%hZqRJ4pniWLTq0ZVsn<7T};-7w})4j{@p=wY*^*YD$=0ZX-7bW5Z= zQJqHwsCNJM+dsNBZBT6K%r+J?i-r3l%l%-v!I9KTEJ>IgNivNjd-?}R>oqjmSM8y=+Qx+w5PBokkJUDB|Q zNS}GGm3Cr&HLoXl;EIVPfdl>=)p@)+iKBp_3j&MS8>4bz5eri$E}_653QR`Y7z3@6 z?nS0%F_C4n#~Wb-O!)_d5n{h!2h5V_RUnk2f!&11Spk>yJT%UFd7-v&)7=_bq{P>c5m~Y{XL;n?BFn)&oQ82^v0$-mmjeQuCyqTqHA@j-p?E*= z?p*oN>HzJqxlAIlyMoO>P+S-;EO&%SWX)M-6W`<+*0X+*7A-yo7oM$I0(F!KL??_2 z77XrN_HR}gF193ZVaiC?WQRxkV3L1Db{-ijrPicj)K|u?OSctvk$$yRKuO9U>V^?2 z>#|76n5|Pc%tMq3xn=zT;nFV*KE*vLcL+N1o8`D0k+BZf?O zUab|KBI!FAuoxh9R!_gTGQ9Sqwn@pPW+AZL$8_0Vvs@>zoO;RD_HiDDei+v*i>Zf3 zmM2l*-!fp?hmoWvUi;&c1!GA9%dM0Y_;zm3NYQQ>BFjxv;P2bL4`A5^ti*4-2sWIC zF<6piN)j*zEc@PXFQUNLW*DVkXd7#2dEyB!@N;=nv*6d#UAy)6Z%kyFP-IboYnXYl zQ+JIta@lUQ1`E6zR0-h^&x|lHsnH6o1Y)hqu_cf-dshFJ!9BC`xe;cED|OwAGvCRFHN>w znWeNd@0v)n`tI5Fz+XFedZmlVBHgf)59x+c8g_F*(l8xHDpL4kPQz|&uUTR;WnADF z6)cB%ydrN2upB@1&XjZEUO1 zkOfhNXZYYfnQJQWDM6z)SzO~cnXBAkMI@wQGi7TkVa$B~F0qcW*%Gd;HpvUNAv8Aj zO*WC`UKfl6Pus^#g2OC!nVp+l7Yz_LZ*iC^MnZhp%%{$Ag$)P_y8ciUYhzQ~XB(Az zF}kz1{*YsJy`J{V>2H4l{`Klw%FDaOOfH}1IZMAVXhl(TQs|m=jcS%`y70HbQd*4X zESiW(28PJOL&+Pd#ch%0#J0dfY1l1Ovm8x!!?;wj^*y(aEm-zSWZAK6msa}}S!gKf zEZ$1Aj&};nMeaSUz)$iiISuQk)9#iuOwl5@+&V5%Hw^wP{nd+ahqG`*J7AN^rt~)1 z*S*ZEs`z!&Oj6Av#UxVc#Wkyr@3P>e?o9=3o6VhL7UvSdBa|%lR=sZ3@hx+Uwe&c; zbT|uZ3U>tqMr_=Nh%N{!oPSYh;ci%mH;kD$h9Q018Xq;m7^69hm8oPwKYSIYzz1iI zx?wF}H1@$`LwIDmi^~67A2Szg#@y%N^|vaqfl%kO4l=h#Xt`8pR_Hgv7fMVzd8S`A z__;B?v+8B9xYx`-S;#)%1?&7Ox?Q_*vq+M#nT$!pLZT#(pfukdQ{Sw!B5Byk zhYnh(8+Icxl>C08dknb9oaM&i0}A})RtwiGTZfYSrFk6lU8ozjXls^T@t9?o9p@9O zc3-HaZc=2~XJ?XjG^t!lu2~+?4LcZ9kB?9q2C$%Yyp0$paFL;8z6fsEXgrvud(()t zEvUf1%VjV*;fx@@X+rY*1y51-dJ0 z=>-Ew8&SBz&;>4vCtb?JxLwTqD^l4rn*-~!-~A4)g3;zAr()C-=(cRzSm!JhS-5j- zw`b}h%~@KsBtT%PD|by7pzFjs4tdwuXNm&5|M3q1-Wsg(48ECD;pRF|a(NBD0(M=u@b=6R}Fu=gUgW18@9i7Ze3-9zXuiwjJ5ITdqUv|@>ek_%kmf7?*f zrePk_T@rlbm{7^wFe&hl#x=_h8A(cckJhofc*_yGOUyP_&>*lJCa`2A4YO0pJ=@7) zNW(nTEb&m1>lW^d=};2gu;F+(84p%Q=!%j0%XE(YVG##oQ6EyIsBjky%aJS&#%EJk zr6<+d*VhX-kpf?D0#OL7$L=d?i>mG`4b@qti>R;C=1Pm~B-$oQ@I6YfuuCkgFrG)! zpF-G2H;k&84zrOvusGtGW=Bh)PGgEWi}aG~f=+;i9CiSf4vCaE*zt&oFLihAn|tlp zUPebPz6w4GTg>6k2ioZrkZYWVu@gAdB)ECPD*LoZDSqk2eAT4B%N4KUmOOHgD7q!L zOz%#4xgt^&RjMQl)q+#36_)enz2NX|ak*G4Rm9}8R3`PF0%^QDSgIV*9AcDmJ&Z_c!E#EA`e(RlOkh#gF%KmlMx@j^i$s>K41LbR3|MwkWT83B(fwOE$I5uY zn8idE=hh;?!oA~Nb{?s5hW5W~8us9J*g>JYbg5=}j`Oes+Bz1*@ctJODJ!``5oy?D zLfv~ffKhDOgi9nbTcA2wuGQGyz9Q=@5NIU@W8ERWP9V8Mb71YSkm%%ow&4bwquf>>>v8y+_YnLbtnGaFMW z-oh5^b7Z$5h@o&P6=S-7p15I+QoYnD(sjL9FZktpDJZ!Wx9V1wtJP)mn{bLw(JT4} zB>s|LaC3g%lqUtRNOJqtQjML)tHp{z6zOBT1Qzqt8SoAzDOR*+8@DnHmXi-<9hVkw zC`JENq7Nphv0$NY*xzCumu}oNU;#xxF^$|W<4I8T_r3F{6i1f*NhYNwAEOyYbeF}& ztrUIBrWB4oSjRao@RLl+gUMw{u#UU5R?J|zk9CZ|0!3vR5L|dyQ7f{J(w%X6b3ILG2L7?yIe{`j6Y{8&zyM)Y3#%Kn54wnODvr( z=Lsx%q+vaXTSU5aOs(TIrD3nUa`HhOeUYqq<99{shGF?j2rqUtiIIyee=VdGEGNE& zMoG(GRNymMY$j$qVf(TCMcuG;%2M>Du8;FE&enFDG%SV4!c@wg+rc6f{cI+s%U=ni z%Rx({#PXM^S-h*y30k)Cj)kFbz*44tmg#W9K!Lyu#TInKSe2rRuunV}Rt4knisvvN z->pf{SQ#5+W8Xu+Pgj zCJm!Gi_dEZKI;oeI_Am12`dXAz|agIn&Coy0sEyw25Fpwi7BTDvq7M(;Fi5|eSOXj zk;!Ymw@K!aYn5!RlpAku;v@6E$#*y9E^ar%Or=mUV9BLxAX3tHGFW5ulOnL>GKFtj zD^|%01C|tng%Yus;%OxB6~9F5EVNd9gTO*N_$OYtZ>tz<#n_ZY&7y0?`x6xl_KHd5 zXrD>L(y4!ci$|Bcb|s2@jw}03U^%|;f_L+ltQG63WMZut1by0+%uvljV7Y(&E8f9J z8YV0F4_YhcP04b;R5UelUvpz zt!=k8A?A*;+S_Bz%QE9(h|1G@k!(#Wvgo*_BTxYw3~?pWxIztMnXg14Pq2FiEL}W@ z(uM%jZnwdzY|C;BL6RcN?M570Sb?uAX;_0IOPf~*;F=7y_yK_>bd`(jB_fN*?o>L< zcX_4E;h&qeq;RBWAu~n^-zAY2D`J7jBwayqrSD3a#O*}+M7mW|oet7&p|V!xa#1uh zoZ2$CUBIa@x${~9U|Gsj8b%q|fZe@&RJoWmY;lRwuvgxAMMT5y-;<=ZVx(cQy346} zC~4JQu!Emcbr(`Vx(_U`A+jtkd?D&CTY`&S!N*#0DlO_Rc%B9;GQ{7@dy)W)n@YKt zo{mGyPTMn9tdP1(R={#(t6qiXU<{Tvb;BT7!C>JT3-1(*U{QE>AT} zjg~5j2h-<*h35OSjnqogj1*5r={#8i;WUi3DXbnv17<~aY8l%ZOT)^n+7{Jq z-H&8zFGzqBeWk-VVWenyQPnK^i$H;ou5zbMRlW%;7|OIkDWeD_M9MaEGwsH4y%F8} z>pJqnI=W$X-Ak(n99<}~bUb10*8?w}xcIL603zJtJW``pvbjQFuEJ1{656s%C#%Ae zU?;T#yj%+PieK7X8OAghOLno87vhFBi_LjY36`ptBwqKp*YD2VO0ildzXfL=4q2&k zpn80vi7dHvQNaQg%A_hKg_T!G3aOB~36VXGEux6Tw*-|Rl5W^SDxFlMWI;+G1+_h4 z<;_njRI?bc04*#XrU!H;pMU<<;}luaU%s{P`QxI&Ct8%dbZ!Z-NMs?jFkE)WB76H{ zMap~{V1Xi~;AM|h*kegbq+tXW7DGA~>ro=C98*Qgiuv`-r=uy?D&xu2T&Cu-St->- zO0mMyah0lD0sCBAq7G|j$VVE*qsd6Q#v;E18H-WRfMq$`GGH0WIv7b?(DY=WG-PvQz>+JF?RG9*-He9ZGVZg2 zE4pDfmojhg}h5r6Yy!F*rpC+(m02Wc=iwFIzEk-XiktKB#U=g&ihU-37qhzc+9Y+>H z3zS=rST#zPZ#|-sg<^|Fmt%S`R0u5Pe4#L(4XJYs0oM_Cj;C|kio_5yHowDcg=^TF zrpiJm^jy0X=?Y$SzQTstEcmUAeBOtQrHO<-sf&>)=Q*l~JZlrH_9Ua%VSbCr!*p;T zMzSnRh~bXUcfZrYGhGHux5+@m{NJv&a5qdHDL`#DHxbQQFnNjAR+{V+1NprM_BzoG z3n4%21+u6Upx9TxBlSAtYQM%!G5W!oA16ylv7QAs^h;$Xz6fRrGP$7AsB99Y6>@H4 z6Er5HK5#-SEw@|{5Rxy@x@nOjE`63(SI~xS^U$lf2arNW1?~Vx<2~LuWqI@3>Lx*A%+Bw z5mFc^0+y72;Z+>Z^KqQY{o{O}pLpjhM3&>PvLy0$L1f0%^OsCy@c|ab3m*g%O2F{w zt52KAV!^_|;5Wqy9WNh#KTK!*t1rv(9FAkpQ436f^723Y14Wi%p-7Qs$^k`=*?7JT z1wHUISfJh1^d6Uu;uh(~QVnrT!QkEsPO?iR9u@o+F`EZjQQn#B^1tO(qaS8m?>0C}Iam{GHc4W@8;M2{op?2GnG zjZY-bGPT&TJy~Tx3U(bRmujq9NnUCtsPI92{dTowBFkv3>Qw#JU;O>muP4!9q7V}T-I4iOkk%`0>xXm4E4t-EM)I}>(~GDp~tU(n@Oh^Fa4Z9`pN(N$(F-BfQR3|l}%@^zyFhm9zXwW zI+smf`uW3O^?o{+$u7O0bYuC+Pk;JjT*t>%k_ovgJ}hu-;rCzATkOwb@_>T7x&4QD zckULLSzawyWcXZ-CG?Pl$>`1HwwSC!$qF&$VRav9VRecih!w$5Fbii<;UX(vc0W@5 z?ch`-)jcy@uoa0U%qC!AJZ9_5l{tSJu#k0CN2-598Rc(nP+wT`h371-1hM%x0NXQZ z7?}Hh4|JEh5Z4;w4Td?3S7!kWQ9|}qfscD)tY4d;0!jK>WoNW1XSYrTVg@B%&9AWL z6zPAOAH1nqhM0X3V0yD({Qckm;_B+imMn-Ym0WAyXX?JW?o8kPCptcuhJ7t!!nv0% zuw!^3<@Nic0iHr;QUJ04r5-Z2|!RHwCB(fN+bOkHWfC&*++RX27GFaLy zRH;4d1`Q?aw3r|8oQ3=-h@BHOTSy`?XYm5drS@a*W!JNf4!$^c*V+|L52in0Rw>dS ztigvFFgM2Xty-&kXJrWg4@$cL76X?*j;thgw6>9M7bUWwxF0V66Ca}CC)MC+6DR&^_vjA|+oLx60W}E+6zKQ+{qb zqnw>zIGfETxakKZPgZG->0$Gvd3r+skNg|FY&H#@C3B)*?$7u!b3K{byED0rn#D{y zTdYr3@LBNObUL2e7tCfWE3=+QV4<&lWwyfq-_(2?EA;d#KEHWp5>-qFTC5M*5RG0b zxAFDL%D720VXrfTVcLq=r?I7)y}L39Oa@SPXuE=x@vUr2#c^2_4Dxx1EMQMElY=K- zp5w-bUQ)8CBn97yNfFQT>b~@hIk4!iRV!wMfod$BVii7uNxvE0Z<*LYvRhhA0);Lq3Gz^RigN&~kEPsEM z72YHbOP8q|CTZDlFe(XHT77OJlUAjPEK4LcPENdP$EU7*BpAz2h))8dTA={?a|w%_ zd2)-*!0b2AUe)YtRj}&>(A&T)hb)xMCzA(@ELy`19{4xW`x4oi;#H-5F^|{4t^szx z#Ox{Mismd&>uOn&=`0m<%$UnDgRacF+Z2QIOXSE+ZnR~HL=d@7rn+TF-r>B1PN4ap z#O(~NN6dsPTr~2MQ4zjtWp3?P)m^OyK`Y&wcgSxC&0$lLR9JAFE>>elb+=b@u2_AN7q#dw8r0zsVWk~!+(*ox zGzjV1&=Rpf2@mf$vhWLm7~rJK#1{fhn`;&bR?2cehZmZ&Y=BEpH)&Xx)d9qlg8Iiz zZ|$#lyYziFgqaZ7*}8;Rt?zx65p$e4%+T+`g#o(Bamkevp`~JX?Mmf*yVkCm0-wyK zP|XhjmS6mFIv6Di{8Y!(EPXW#G2b&|QR#AZ+58pJ{pE@XUR4MMnVfmSa%q`ZoK<4n6JitH>kRl?jOJl;B;@r3JZfX$W8r3W^HzOEn>-z&4SLPFCrG zsVZgF73hLtv|>dmSipphh^^?6ugmtFf}yzQRLr0TG#8{{tlmNqL_i}3MWWpzQuop_ zW+S3*vkQeV5eVyQVUN;1%|gI3w| zZ#AQDXeilauylmB*07rdh%7A`yVRx14|LNFL6&c1&xcc02&XSQj+8Ccs>`}B=4V?f z*Vv#FxfpARSEyzIS&v%By_H}5{ZFq>M?LM1A+ls7ve3sQ9yY!SuoQ-98xwp?8s=xs zX`AfCvH|7m8zx`h;H2H$t`y+_|M};?`}}wDg^$ne`{zrPwk`ePp~q*Rr40aYefb$5 zf1vk2e8x}x0UzQZtL)U$XMf{Jk?z0T|E;;u!Pn+ik6Q*%{3`q0mV^G~XJ5kF{)^u~ z^!Vc!xqPnh#UKCpu-E+Yj|lv??@&E+#}e_()w&Os3s$?V(oxMyF0ybb+j%7-3y&h< zK`fT`sx_%5MVRl&;)?KFn$`}Yl~8GIu`V9!6`rfKJHj*Wbd`|LVCeNiowAU+$pFq+ z3>LgVl=T@g+#f*4z=SOVi`s+7faw{q1Rxgl)>c~({^Rn#$*XEUEr0pkOV;&IUzfpr zUCtMxoXdtse2bFBW!EQa$`fJCVKLlVRh+ejk_Chwl(RUftyzU)%es$1G=W?0P|MA6#9{>0| zlROncq)bdCe~_3;g6^`E%A`Jf2Q6ap$(e{?@CPd;hxK51Y2{DoX5o4@qz(IZEW96fq4aiPQ9&}FvDJc?#F*5qU-vw-i6{kso0^E zZp_xwES( zLORB27?tyE^JIR+b^`>N4x4rUMt+qAatp z7}qNlVW?)opoPZxlhJIY*B?rGk2xiE!%EriyeDZGC2PIWY&;uAg8>UBp};qh1zO{| z%6c<`@g^~U+M&s5qvmuyozLVTg+%j}$L~8ZZF%K)rSwfJviE?41uOn^lel3|K7gUW zOURTbNRcv~Dqq-r{3r;=v1kj^B=>{&^~gJ4f=KzjSnHTvkIiAOgnmqll$CV4bb;n8 zJ0IPtvzQ%^e2?!w3f9Z+w+Jj>|9B@`bqZWy71Nt6f4wD_@u zNjoE2D>fC==2+aMprXT#$c+kHSk-DZyR$+8Muf3=I_+7DBK1^__gDPbPKyO6!3*Q{ zl@^m+>Z}@(@E}%Few!<92u^l{+0unXYeYkno|G%GEf2FifR!F|55M~zeFlh;I-l_t zcDoc?x@`hWJGOmc!OCXu9wJMN!NSu@_SNGJHGqYZO6!OjsD`sxW(FcfAho{rqQ#Y0 ztI1DR4Y}0?9E--JhukrdWpkD7aM3*;nUrc|y^1xmbY;$BO%B1P9K_0%+zqQ&3}7cx_zRB)3Ql>;Ey30wONs7z2xlI_b7HNX7HhHH`ow6&%SZ1N{LPW}&M0a`N zHQi`AasN8YYj1v1N-chIAtoK~=K}(+`;Q({16EA%t-4r3kaDlJ$=i3qOQ#*7=t(ZBL)>ASq&R@r#h>4Vsi3O{*$)ly#yR(TM;G<YX=>SX)}0s15l>0~*w5r}*c*t(p<@TQlR_Khum+fAKI>J$>)Ds#H0 zzv~wIza7gVMnnli9j`<}q~$G*J~DPh78Syd2l+zn@A7U7r(x`PBnqzvEDZt+Al&E^ zSY$(Aq-V@scb!M3b; zC|Tk~(ZXPHFR`(A(j|`;iW8oa`K642<%p%|TbsNi*k{@Iew5C61eWI#{;SN&&cL3WSiXz7H%IHMTGJh`sw7{uYwoB~r2XsgV?zlrTQr^bX*H5*leE&_2u#u3 zQtssKY%apl#;8Vsrdl*WNDGejmaetc#@|I&1&0V+bS8}Fi z@c@>-xE;ekEHq%rXNtW1b@D-;k`h@;>BR(Cw)nGvr7vJfWnu%?6Wf-*ELhShPr$;0 z$Flyle{XW1C7sFH$g*q8`q!>q;^MOJg3n-y3Hk{YBt{DnDGL@X+m^rfBs?X^$ z{lY(%fbk-8TXQU%4}AP2tE`4hk(;MjRh~^^C!*|>-SXxNhkE=v=xWBa@Cnny`)eEN zTCO|qvl`D>S^=nY@Bz>jZTf{G8lxGHWRZyi)932n+;nFf>3p`xY1o4|C3*R)oL*3{ zh^M3|?LDKOk{nsG5?Su|$+HHmRhx!wjVx;KjUD{2(&?=Al#ESUL`$DxC6OhuDVg-h z&FS&nR9{yLag!GBhXlS= zVx+Ij_Ogp!_dT6L*5M)%hQgAy0G*GH2UAr1fPQ50Fi}4 zV@vyYrc|MtrK_9yDs+wnKDiE;5qOH3uS~#+N zl}hIlZpCW8D*mfLivdfJPJ6KdD@BzZYLus|lKTiOS+Vwhpr@p!VObBJlE=2~;BNs- ztw>-Q^G|#l|90kXwWgx14x7NaD*Me42|QccyB>O9)`XbF!PHS|JzZhx$vPp+{l<}{hbFV7h#?hgv-BR)urBwG z5zc^_21OPOfl#x^47COO7MP9)re^8LnoFHSOFV3mvMunvcq2|_Z#a>KRV%5jVf~jY zLK;pwV%TZ|jJxcYmNxI*y?YPu<~n(YZ&y==YUO6v+~Wt#aqq5v#E;y&H?O8D`3f0_ zX5G1rDhDI_`B0W-Y8De&N?QdhsJd6fVil|~xl_WeIJT3-<&+9mEM_tf5v&jc)@4e= zh?E~MK_(?DOd^KJl3}n&$+9D9!OBI;(cLBu%XqBrvU~S~3|JW~oQ7$6za1_pS`Hhq zWK^(XyQ$;>cF7~b3UfxInk7DcN#viC=bKkmC$DOalQGYe5sRNt(Gyh)5y=;aNS@g@ zWYofPkD81%c{tfjNVBr>9g1xeSGs6}wRm@hTgKf^BL7m33#l(KR#@k^FS^(~XQ8ko z0~ot|9Ev^?CY%J87J2N^vNH5A&^k6?*&yCchYS3eYrvLzO#_yB1j)*}$RKNP17XHm z(QwoiT>>7qSOXW=VJCDd^=bM-QN__#a;aHuI62=0JQ6hTGzr(CDhzgEQ`2e|a`~XT zly6WK*dk-^pd~|J?47c@l)I4iN?tiv$eWra8t`17JD(=Z^b{-!x8lbW*4`E@D1ugq_#|8fO4SZ{o$5Kd9CTrDQ9{U!wm^92}$OM)|Z4aj0TnKKViyyF)Zy;{nVkLntSiNa??d$Ovycz35C@WvRRe9Fa0z0_H5_zb|)n2t5YSGTD zNiOVpHy!hpfqX=17z;Dlg>-EkbEAOvT1?H-29u`4Caq0e+nS|e(y%T!9}>E>2nnSA+U;Km;a>D;xxiAi&iFm2bVILROdG%_3wdppTMzvn8J3*!VeKySi z03ZNKL_t(u4Qh4sm{(KFB-%}_)XHAfFPpcbS25QWujE(!ic4=$Ofd1vE!IlaqABoA z8itXK(2jwYXyp#qEL*{HziwDLoo3xIsaT$|>Mojy0W3?DhQ07@kp+3!r?oVLC4rSK zEwMvKpC_;^Ws@R%)>+cl_$0DtB8#hO*dqz2yjZ;zktL&%<tiU1a_@Mvk`N$ZD<143}l12sml1twaAvB zg{uW@5rb+>9b=OywYwc_Cc$X2GyAX?FWpB{urONqkHMs2oSLbELl|F}2Vla>Y;D?N zX)*J8n(*B-&v0Zx^|H3wl}(%55Nv7$=F6otETkd~P+@lfoFQ1M*3sMVdi!9cxOFV{;XuNRbvs$ z3xlI(`CE#V99fPht-tmrP4Xb$YSOS6EVjJg5i3$c>zE7tefxIqPPpUkOokS&Su)$* zito4f?q#!nY`_|uv~W-C39M9X^`vVd|u_tbKQ=^EnHt?#~i`N-OEY*)l1 zIy-{I+6s0e*?Yw*3#-X}D^_x8x573k#wO(mb+FRKTtugektj~~M9_zGFt%41aO0R8 zr~($=pk&rd&o-SQlGKiP&Vmp|1}<#7*XlO<_r8hKuuxo;RMd=wDrwGAXQe=lT%}Bt z9&#WwWLa}@nJnhZ6yMXID5P&m1=kUKFqrGrz~tlNa}Yy$zX z*>duEuewwvQ5x(M;2o+`9hHnWKlyydE4d^Nx8zC90^cxhGE*8>uwZ$J)-k~H1MP(+ zJX*wYLLy5hhcxWl3|Ijc$K*~HEcYj3tfK!_h6;SK_O^Z#Dz&#u+=_h$OT13WL&yLQ2GzyE;F!68SP_vBKYg3fLVwWI*ap z=UNQx8-cQ0#3qu(!QwwVTe9#(QvOyU``vDn`%2gRL96t)2#~C&2ARI6 zV3GA|D-30AS{+BF#T<<;=p)lC3;gg(wc^*jYX8ndkByBW?^Y@+_dl`aMbo@V!)UcB z3$P4^;Q)=W-jMe6sb*v`87NLY~@m}FPEWo~fLv|q< zDlFe3T(E}SAWq1GL5P?NZz8Cn0Y;Z@H`J|2wT$c~GO%8s4dVKu0mR}aLjsG`EHb&| zhO-d*!O5h^(l-BN2b;#QNNRUFGH0PLjB1uRW7K{(gQdw*0?0YJufYvKKd?U&*XpDD z@mWR?!#`81K5DLPPeP8bQmH!Srqijc_Z%K$sa2)x+EbSSTxq5O$NR;)aezzeJBqh@(B2Fv~a zs{|GsS@zm#bOrk-{U*fUQS77xL<9r%(((;%24DA+b7$iL;Bi_IF*Y(>S0ZS;xM@$hznIF&u z9~5NSaN&I&*#@ionu-M$FfliqMGJIF2@*10q)T6eOBCr!yXDO_?M=?6b_}Kil*x3s zzMgUOojIM|CSw^zj-mfX1AZAy_U1vcSfQ;iOz=nj>G@!6etg4o!#--3w5?gTyW~A( zEBs^{CdEBGB~K+nOI)*TH8V;&e{mX?%G58Qacpgj_G3`bU^xUL^mf^G6h^34?Z zqqU2t&-MD_{-0LcZA!!DepP!oTGxASLKn+Vt`PfN;Wq4!SA$pWsRBk4CNp|ltZ)w2 z*&;h>{pv|?vJG`GF_h?qDwNM07p~m!Bl_sW6VJ3_i zu(UcN9!{a9OYG}r|C?{t>lQ4x6M7oW+muN96j?aE6ovCfUDiVDvB{Zb0l8uf=39FK ztM1CG1~s+SLsyKf3ffM$wz)D^O9RFL59%kv4ePCKq`T$Lyf;zS@fMy-Om_*Y^$IOX z`c&ZeRz5zx5(XPTJG&YVMqIOSWO;y{WFigA2w1Ft5r{5YzdWO0$!`J63AO$@k??0R zX_&8W#n#Sf|K3AetiAt~$>bB(-j8Lof)_W5(^_s*QTc;?)S2`zJ9&wV2&SaSuvH^?46QNU1Cqmdq*yyis zEICWzyf+?=!~)#<8ml_e$biMCC5(K&nDvQ9HVW^&cY1a0S7$cD{y+-+%GSv8K(I(; zIhFKYJ@vpe?8E~a$M=DSYL@+CQ@kGvu7_+I2InuNVY`m*RWI*ddt zzG)s7gC)@oi>EDn5@}dIOJHGQSi;x)fTUt5C$; zbXrMb4&DJ4%vX_J(hc{HWsiqD9e(Uc0nr!Vnz(qD4^d-91Ga^NZg_?0`<2Lzh@wwP z81-HzP@_sd%4dt6T9ivGW(YVJBXDA&x6s$j1VfRK~k-R118_98+MiMlIY8OJ@pS zzO@6^-n*0&mY^R?WXYH`3EW5YXEC&TT*k{>e(y(4;Yt52Ci<(ZL z8-_G&ymIT-9TQZp{pL4k&#jJU(|N#VMLbkNvR+4!D&Z0|#7cs-!Ndso zwm8qnK1Izqw1_kqWkeKRFkkO~d_)pk5%YnyYq!4g;DCt$b>fM#eWVABtt*ougrYmc!6VxmiM5^GV zE;G3@xA(1}alq+1lZyjZXVqnO>4vd65zkjpEh`mnB;ihg7w%aB~Cs7RZxu8qRbe6Oh4B)s(I9?k#rg+k6_u$Xho zWHKP;%Lr*$@u72;r#P}yiAZ@$2l#3>5f3Ld4a*ByPCU82Y)M9zfWUHe{~=vt0bZ<0 z3nEK)DX*>Lt?e-R2oro0SpxBt+?jximip8>ra4P4mrTQWwz9?Bn<7gt=Rcrzd|;~q zt5>a+4OphL(P%n?s@o7`R|6I`@uG8~%%Hv4Atcc+wk!AbhBSuQ)E>CN{z^lKDz>p? zeI&FOhSDB8!zj0{QzTnSgjVv%_KhX~>g#@n&ZW6%p#r}zZtxiKL*g~+M`Hessn=PaD4Fi^rb!=rTN&}XAkucmEtVGLu)BH0Cn7z;AS>Xct zHZ0rad@fl$-G9Ke(jku{laBe0U(qydVz6@M%)9-Swdst$NrW2nps88hf{82>knso8 z;+eDM+3IxK>rI9O)wSr&gL1J_%omG!2Fr-h(i7=@)GWEm-vGnFl z*&G-6-#)>A^HX;z<0m4Eu6+qs$oq>X_zae#dw~|ab}UAD<{pTY^-M;&VKQbByhs7V zo{~jk9q&v|@b@IzVM=#d$_H1U6Mnu8Ew*@hf(v}NQe)OJFMr8Wivdeu*%|U{*4~$H z+y^T8%@g|u3jxDNqQR8I27@Jbv*)x+C5Ob2z6BjUa^FUY-(sGz$QetbplE!hQ z;N+{1@e1^P`L6+0E*1C#TtpzrYKuvrA}FqGtf1KMiWnbL_xl=In&UegkyQjB6H2Q3 z5m_SozSIZ+F(zO&K0b5Qg)tT@y9Tjny2p)p(CqBy1L}N7Htj;E?HI7|uVggKpZ?-k z?_NH4=G^Mq$^c!l$;1@+=}x&jA56?QYZ9^H=_}{YTz~iM*~#qutyy2d63soYT(6O< z9#iyf+l+ox(dsYv9?2@+yDbIXghmslUh9x{Dx7}c&MY>_fw-G5d z4->k}zh^Qz3zi*Hx9qazK0GB8VA&lT8A-Jw1N_4Zmfa6x=qKqeOS#}aPe~oNh((Ku zET+H*SjLuhJQJ`$$_IE6A2^LEv01m4ETW_r2)7tTOB1&8O*OaIiM=vhI&3DvLaLaq z@X(%a;?h18fqX=ls3(8*VueRoustxPa10^T@V_Jtvxl0FqAeza{HU{XXQk8QtxLuz zU0R%mHJhCVz|xMlY`8Ol$f6sc{QZg+he+7eZMle@dSNTV03a2lwzjt|cNb8d$0?U$LjOGJ0$A;4{PH(Qh{r0W1XV&{Z%)9^-e00NJ`BnqglQyz2MPIE~)l>2r*_Hf()GS*m z`rAqt6In397f;D}EU6BxRI*THK{rf1C6y3n{k_4X1nZd6u*2~>i&pkJl4K$!3jAmH zB=WF>iE+!3WMpB6{(;95{4i}C6IgsxQ_g3JO%~D8G-;Tl25feDSVD_HMHG*8aYamj z8mfELl(~H@kl#$ML+Z%hBwkDi!@vW>^e&g&Ju&s_Sud*|EUx-IY{cZAS;oSnmAJxv zAp@4-z>>y#?3QJ_Vm($2U%Ru~#3Y9n^eGu@wc8=pEO&@V*||M2U}=Jj+-z8;ehXlc zYOJZ_7FOpF@pBX{)*?_z`^tS#7jRd!qlHxDIeOHZPNTNDiWonhOh@B0XV08FbN=kP z|F~9G!zlxn%#caT7*eDTP zP_yJKcKK^NQfy?Yq|?cL7SVK(@&#C-P036?r(ij;J+g@T7wxm8|77=Bwk%4@It!M+ zvWb0`EqNGPVc07^B$34~f7wyXBRi8|Ddw|a9Us|Zz?ujw3>K57x zFjL@u25)mx^!*!(*FRR@;W!ha3{Tli8Y)~v^V_6?P1MSS5b3?5rCURZLn&6cRB zQowMLCE_lx%xecsqX}m*Pv-ru{zUVB(9KPtrwq8lp3VT zn00?*1MrwFM%TBU+cl|KCgd+QJb!C#=}J94UWJtoIU4rY*E2!ML>AT-Geyhg%QOFC z{Z`Z;j|L-At~3R{Q}Qa5hDl^m>#zYwmP|2MdBCQmfJN7etC@@%u-dXkqf5M2oTi#Z zcJS}x=d-gnB1>S`iuZ4&Qd+@^uQHkBrew@Fwgi3J!RG>B^RPGz6O!0IHefBv4*s4a z+r7Q5Tk)Rv`vntO5^Kda2U8&=fW;eSVTuml&3BY|dLeOtMs5jNCSSy7WG z)!z}(U7(31U1dtyD6sS+*>36Mzh2)Q=SiMZ{q1mquQdfMo#yn;rpaPJ1&00#sj!r5 zg|+{r$kJ+R$FJFtm|AD^$%a+3pdqbqV@v>;1io!8vWkUYQ!NmodDj|ly*ghTi(dP9 zI9i`Yvz5`xXf);>e2OgV**euMW3{DNSqVoggBb@{2Fut)mU6`{Q-SY8=~V`pJo7h^ z#ndd-gaPY)ov^1MSW(Slt@wvl-6baJi(tj;081V_`1i@X(B>Ddvk+L^l!+|QzZx&$ z3t%7_cIbE#EQgL-16DCA22%DWz~WyLbC%>1zAUqdx(m`U-N8=;7x9&(wc;c1_p-T$ z1iVWk!PSARauL>P3v7Iq|mM+xe155 zx*~j($krEJu`m}*XDg7vl0(aTNz19g$Fpda#brLz$|51Zm^AF-hvz>&PZv7)-p7|W z{%fNin%uB4x!e3ufy4(v=Y#ZL@Ac|2*G&?!dW(GkImSro64NV1-PQG!hpIm4+-r=z z)I6sam;!%p(lDV=PW2LBO{sP4QR|rJlq0L|!t;D6jg7|9yk0C$Za(SS1Z(1J>Anb&FXsc}k|K8%F+k)~@(OLca9{c;$WSW;1mdu#$wUMN1MZ zuO1e#?Ay{a6Ui{am5D#5X2}NDt#~V35@0FjvH})?ivZ;ytA;UHdier@2|s0;s-K^g#B<+G{WLPqGnQS66QoQTxs%FWG56w zdyz4Q01H%!xu!R8>GwrLk4?Kp%(&0PH)(}!6U)MBl_N{v2A6>>zVzabLDb#oefaVD zi-ePl=MzWtUw0a`vss_ry=K4^A0`SJxO{MN{!P?SV`XGva*hbtn?ht?;vbU=+N!{s z<%p@ms}S)}@tdoj8zHhxrna5W@kB|-DDan}O4on|gO`yoEH$J^cri6gz1lEnDF9_a zn!o0Q`2biMOR{QS7a6dWP_w-83LCIKz^#}JSgT6{mLG1jUyU7D2`ozhOYFajnac}S z>XXb-v$=;$lKypO#S`&Y{ES-$-|I_d7!!2->)_7(Z(?h@ajr7u1mjI zs`};@PYH!gNBdx1c9iIl=rD^?puz{EG15)1NUe&5WY12-eciI}2^6HP=l6jcUynwb zhRII8%rK)tYyfT`vw7n~LdnI87wH{01eWwScS!D~(^y^qScA^T=M^soF5|z3stl+J zXHy3**vhYqVlDDipZx-ZSc=e+w&U&?Yw%G^3>vv!ZF7xJVKeRKPecX@mzk+q@-7wl zQwZ&k)vJ}A^_Zs56#a75)GVc(q+!Cg97*fgfTc_ZtOS=APr{=ZhQ(}F{31aGHH$`; zZEnS{C*6vXhKXBo!ms!>l?8_Z>vG0KmRGG?F=_3ICV0ZF_^WJ2z`|z6tnO;viaA-k zP&Yy9lGv~AW4~gKG1jd3>8E9aFRsKu3tz=hp~%wD7aDB9`W!J^2r%+);|fIpQU^woctm9FbNuox#>muew9q5{wT*1AVrrIHB}|0FM8y)qd*K}uR~pMy;8!+#^m9Qs01l!$ zl&p%iw+SsT!rS|#VCBVEkc4q$36qiKb>M`cvK1^vM3y&S+wSfCng*6PKk+l^Hd%YW z%2+WKJsgOS9v2s4faU9~!QETCEm%Yq`H<-F!P|S^B@$$;llqSiHcO*|Yi->DZoq z@Wd!GXlPVXtD*)>R~nXOEcPpQ$ij*$Z6>!s%1efVGG)=VMrF4!R(k!u3WUk3m$)ko zhjCYw68Hbbk!5J}FyLh{NN~kScfa^y99Pccjj_>NzfIEN&B5i5&&OavfI(=vxE2YE z+A0c|8iwMltiIPJzR1jlEd>H!`PvE;UkiJ%key|heJE?@nc`r+vWi0t+vs|2b%j4% zU7fD?bG1tK&dNiM)#=(=wpJuhta2&6vN6+tj4OJ2m?;&YNcjffLWq%fKJfTIeqPLG z{e-o5@~{v7`U{#Qz9S~yuWdcl+54?Ol(PBiC2Q?{JU*UB3P#7%Klvh?&wu&jefs<% z$<+=?zYKxp)k{t`Xa1|hv9&kC3vLgKx%c6BzAP5A&7U4tWAEhgtb*m)_gkf+_xpcH zID1ogdG0w4D>#1qN8feI>+AIMTU*sDbA4x8s{6daq7z81P;KK_mBAXqbv!#i%S$Wb6M^=HF?vg6sz5ew|Az!}syT5T6s(zV~pN0;bwNpC*6M30;I z-(JZV^Vy4RTx1?JPhR5JnOk~*pGjBp<!;OnAy+P~-m`GNXHN<8(Y<@~R@FeN+Onvh z^CyNtr~S;UuK9%J%Iuzeu6y>Yk{9Fa9|hH7sn*!khwdi6@Voj6naQQZ001BWNkl^ReV{Ey|+SpiVi`n)%5A$n9P)4p9-a+5hbiqwLL`uO!A(J}Vp?G;_~QmiD3 zl}4)>>!QS(lT_71uAV$1p^}g6OANcCcnql;#mwho*$KNstT2iF6$mX{)ed>0jn=UW z%WJ96yh>Tj7r|uuB%^7+Z)%iLH%wTzcCiQ+bq1|wx6z9lp{Y|E5iuwmR>g@2NG$LK z86u{`@xHO&V0wcO8o#pjEeD36!d;YjTU%@jLbvBde&qGsz7sj-z7u+p*YP?*yH1X< ze%o*PZKvyWoY0A?UGo-I+tskvskWVl+jQ&DX7}n|qt>XlYK1acmsX0+mRmCS3!cGF zkT1b@wNUa~BxJyJ?CDA`9QB92VSi|9I_^(4t9cVNf;J{y<~I@cX9H%pc0*wgg<(4& z{qgE;`sEFJBk1dz_m$ZwWH+f%-&{4llKHQ^CjFEC>U7w~;$3eNj{E%ytar?R*sfR2 zmsP2UJG_h!s0c9YH=%Uf7BmF=Ei&lk}s_Kg;IB1os zJ_TL@B*tov53Tl!Q>{U6(t)9Lt;9Bzm1>F1!O5|rD9*YD!%G1ozFMu z3u@f<+4r4ZR4kGKa*4jWOgZf|oVwF;f{!m=^Q*VbpHXp(8LjpCP`@zLlEohRrr{C%NwbC`Oim!{JVxe52 zyPosG<)#Z-cr%D1Ke+aBo!!*ordF!)0z~uU^Ylv!ySGiA8?_p#^Kzmx-%O?IV6V{) z(;qCg8eym3>YMXp&z$8J%!#fJmtFBFa(GfNLwjCUvMj*3nIm+3=!4aLYkXI?%~Q_f z?KBbw^X=HCNTj*CY$eeV-?}b96GDj>hLzT4PflDN-le|Yp)zRp^UwwG_2;|&hwM*N4}R}OVCjn1_QtE^}T^R ztPPxj+cO{aywEpq^=8nxZ4#}P+j2t+J5E#!s{^NB?Ny>GJra5i-y~(e-|$Rc<~D0> zCx;+Xs=W8#Ke~KAedWDhUCTM|zFRF22#Wla`I|C-d4>P+?!PVnKbL>?-p@Wb?-j|h zkBqqsxqQA@CghQ+cE0(mpZ)CGwRA3Xe{>KuPt3Jp7-DV z*#|%SsP*xU>sK!SbEaY-kSA}Y0-?0}FTeWmpD#Ke@Zzy0cibn42LcR#*-G5zuRYuyk3-$tQ~FQ&*}yZNAQUX;pQGcUSwG4t{H z>y3~8`J--;BsnW&4ppU7Nu|_z?^hq($X~zm;RlN!SI=Mk{|S4$mp0b@eRwl}&t7E} zJaADFf+yxg6#NPv+JYbmLQC1DJt!V19#jy)13^?QSSdg|G+gfd;)J!UqJBgaACq*!@$_gblkw1q%?ySzpm!^z4)e}r*07zjRIvh4bAYY2V@aO|`YLy|=ZswYRnLXR9Qw_RB~th1Sm6#>VEm_3h1#Pa8Yy z>l^Pjwl~(-zj*~+4aV&%b4s-oih>qf;YCJp$Pnc zai_JFmF4%bg%#VP7Som28QsY^!}-NB7mhkIW>w{IHYcNp1EmsN zm9buBvB7BY9aXV#jxlze)a~=6->RyZmvE%7;wzF7M$!A4avJMi4Kf`I3^4{vT@4~7 z*bqW|$`E6%@339#R%+WP`|r2kzI*@IUaRa>q$VFTJsZ34wszlcy?wj6y}q@*w)GB^ z!`|M}TcMQzQ_!iFCCwDT<4B&)0_Jv4ekKnjkDuyL9Y+1aJ_qc9*Ss!=>e$w?! z4Xz=xIGL(l$(Hk_yba>Tt{}6dJm3^B?t@-&6YdWe%$iCXd4*~=3s%uAr%SN92tPkQ zY`oolv%0pqx}N&HvX2WrChIOncsY(OEpM#7TiaaUUM(L8j1GvD06pWj z+G%~Ab}BEu`9_c;<1>ci)RM87{cd@EV`+Vv_S$*383+C;A5-&Ouw=0>XfpXDm0Vlh zTm^i?D_Y;)dADP~U4>IuCN1+MWTdX<71PXw>Q*etnPkzD)dPwo3o39H=frp~gcN=- z+7wx(fCgBpT;v|c>Ab~^3FD^9v(Cu>L5axPqw zBbf?CYR*wCs3Bo&qS`9lB9VMH43 z$A#n(Jm?XMe>is|4zyWB+;IHu-PhRPAK!n24Hd`{g9v$rE)P=gzkT_X{BZp3$b7$d zyr16vc9_`x6gyAl2JV(%b!m`uR#>+emZ6?h0l3U&@SI>Qd`IPoA7>|+4~mzYyVs9+t;t@Lo7)6_#NMQ0^d?1PDW-TacqD5 zWPXZ$O@BN1WFOfj0Nok{Oyz8&wwq5KPo2GovIkm3m|)R-re4h9da zn2IHuJ}U}eO1$J3Qw#D+5VcpW!HPlaBN6)OF9z96^}`i1bZDant-9Z&YO$*NeR&j2 z42@q=?OA;)$?!wbNbKCe^Gcf(lw?W6u%bvl`;akDhNc>a+E}m~x$acMQ;(_?C&e5C zzu-DWk7{n)beFO*M7*)LgJ20U9*psjj*(kbhQ51Pfd zYF0f{M}9F;wcwXySE&73wrp22j_H_g+BJbsQeM(ec_|kaOuA~8tRg1rfpp0ft*Tiz zaZn{qcSR!&Z1`jR0|638Na59E3SGkLOCQA!5GMp9G{VqWi?b(ytTOmc*zu#oghg5f z834%zF@XOkM3`_kKT5%$2k8_5BMtUcI)VQ?K4c)^eGX0FhC}ocr?#AW6{; z#WN~czmQKXB?||NW5ed&qm? zwyz&WB++5)!$C5Bkop`uO28{j9;CkPC&eJcyBr?=Ew7TWxi9!dHw7AifbydQRz-|H z7GrUO7QW*|TvKQQ1s$9+5=rtT(nNJse+eG9o+1uUr0L+(gabVMVt`quE*)mYq6>#r zkry|LrxPS6)=KD2{9r{jbWyR23PP5sF&G!zmxxnvogz>y? zpKURg>vfhYO5HPJIK`F{38}{~gQXT!PWn7|wC1aXWqEYT5!0huT~`fp5zfPIFW5u_ zM}Jo^`LjgB{InV?>fB=vxa75PLC6%OFK15YRg!Yasy0%YaBiigoslWu_H0rtX3eaD zW^sXM9E`l!xtAJndXEjYwA!a$| zE3r6RY4CG&oRQR}45xkC5)hJEW~f#5av(RJZOXJ=)pIcu8d$APy26&Wn(IWv6lrm( zc&ucx^I0N>v}W!uE`Q#)_g1{+w|k3)z3lhZ&j(2|xQ;a9WBh9ExopB zMe>zgFvu{U<3J*OAf*Dg9?IqEG*-lzGp_rQ4~7 zHq|6w&4ti0d)DMK*Q@5~L@+5J=JHs+Mo5t+bWJ7jYXU8Tm6~HLibpDr1)22asVOcB zz3@6KWi6>NL}qt_+&Xdcm*L#wIFCS<_zRvq2`R_qAQFFBEWRTpJ96=V+qQ#! zu)0Uzx~(qdHvKQU#9+(H$wv%h@udKh>Sa}0rIYOEKpD*&iZ@3?2@X3{3ycYbX^uFEy%ZAy2oDR50S8?Ot zyB}oaAq_&%6Wp13vr-c)8DQ)P;{g)f7>v7pXM=o6ZN=j)Wu8w|5%5rCr9iT|qQ(52bKcb=43qD~*?EHJ;O>Syas?Jy>7kH1=ChDo7uU6Ls|@lut7k>9{k zgN~`nOuH9EM=@Nat858gJ5KQ{OQAtQ%(0U4iRIMf85uuA23RI1MWlQbvmA3#6OL)f zv!#s@m5gko#R4J|E>jrc&!m;djAkLx*f43qZ>Df8CC?8_r6|y!M8%q*5I^3CVSZ9F zF$M>3v17zd4Ao39JC}?Jw3yi5vI0v9yJ}`6;Xw6sLZvG>=adAUP)&7EoD#xu2`+id zsB=O_%~|A71w1wwVMoV_Z=ce;?+&+iQy;#4`TSwae!KI1n=Hk6%(O%{(*xV8YJ|N` zma-pt9In(?NzWm!A+g!t0v1_5pDkuVW*74q@-N_o2MnH^>O%$m7$ zA;8CJnVSDag`%+8{-Z5b3jjbzz+d`Q<@q`?nBd#XU!vg#i_!vZ6$1U%*EO9 zMh@)bqXU2sT{)tAX;&UQ$dn%(s0uyDlI??UI3|`T4)x&XBbYYC196gkcA?}M?$K`Y zWGJ3(FT+(uqL`+dQN?Mo;lkkzwe`VP6e?_=RnY`RBwB=G|F@Fk9Z&>>B!%AeZz`Bo7tS80`K89$OyHcd(kX{wNj5r}%yC{_WO0ZuU z|HktGI*u{Qm=uRG2BQ)=O zqOw~gVDI@l6Q7bJL&QQYPwLbwyQUhPd!pK(mbc^zLPvBB*Vd{T!Gs@7r5?6x~FHwub5RsYcs0RE0KB{ zU0oFmIR$=@Jb*~b=;F#~e!}p4!OWY*blEDU^66r_lrH?prwb{>7@1TAEz3^lFqj$V z6Dr$aL@h~*UL?_zOvYJ^mK;eL3Tfh1`c9IIImXc#1sTfBJYvAa;)DQ72#|$;B)~zU ziX93!(21Ut$rL5fLlI1vA&&T;N9?VO>n8-Ok@jSzMQ(8pz{cVnP=PH;(OF~&{qAC{|i|U2NstvvqHLRxN-MivtAe zY$y(6Dx)p%DB@yJJj{KwixP;CmXpLUMY84?|P=4^lcuBU@DzD=Qge@SV57Rtf#URyLC}0VtH7M~svs z^`1>-%{)#4|B*{)Q#21e1zdwtA}zx!t+bu8u%-pwWQpT+A}+=Vh3X^>L+HRz_c6u0 zah=j%d=&%#Y|R`=n1(1J!ONS@?ng(wcw>|!8RJYdHjgOY9*+s}#EBRX8$fU;!G*6zsf>+(L>3PGhE>E`TH{ zrT|GvfD5opHf@_oCn_N}jX5)!G$qOqT*wqjQeGjsmBn~x=dvUnlB+q+C?Z3RX@*0X zaYYM9$IKST%s-^++X5?!=yyKE>&jnqR2q5vjo- zX2@A-=~R44ShBU7A;X=HB-*3%~ktJMTd zHfyfaXm}03>elOS#c!Xqst){xvll%FUVh!dC-Hm$e$I1C7^dVwn5ELa=}fw$T3QTd zB?X3Og{eXy3a`--&+0IsSYa73yPQlVQ-w>%tbnvHlOa~LiwG8=s7#(RS&Alr1ujcw z*({p5R3V*Dg9!#8!e%mPf+2DMK?sl}Tk>%cEpir)(Mm(>B_aM~4)G^qlCn!$2G88I zv=Kf>va)HNiCah2}9vH<$nvu3sX~IZKlSx`PL=eT2 zF;xeijHS+Hs)mpI!KnsuIpup}jjH>}1=Z&{-R@iGhLt&>!qErNfRNU1_gk zD_&6Q#c_Mr_g$S}?AJw=OUMMXR|03T1nw1Hq{T_A)i|d(i<%s#k)qrL) z9QXX(VYcQTC!K?0vl5Fr?ey;Zznb;q<8mc+P&CVhru$d^>!;5j4-cwdwib^&#F!Ft zEo+a)y>S6K1(}?V-hC$|>%mT+`?u-nluw&k?dks4yzs2G;LI3khx-3nPRF&Fq99*h6x2(xYbd$T&VDHRSJm< zQdFtE>&PexlGZYC#pBNSo=%%oUNB#$w*)AuF}09h!3m5k@R?aS0VZG;#92+{i8*IG zvm0lwbFwo(zx#7*_H8FNzw~i#u~e9U)7qY&o10xaoL_J|E3+S0|GYR{+nWb1Q?V?+B&|JMehH3W0v{wuTuWLRi5#$kl6>Aj=5OW?k}X_Bqjm>4B$yGnR1@pE z658a7DJ0OnR6&@q?uj|>&ts@7Cxnx0W8V)i+excZv1StYbPrk?fJcvLbXa+2_+2V7w%`kgqrW^hzkjR z?%YW+Ga4&sak;9kldt%cW!XG_kdmqJ!=P(KC76)K{l%#coI53BiY&R3wc``do)$=z zBn^HcQ=QO)u)wQu>Ls~L))}opPR5K&fS-&q=G|g0001BWNklRCSLcMZ`UKp=#FOtGnV7zcsF*+kjwxTVy+Zof(`Zj!V0a(Vj9ZYz!t2y|h zESr2aTeTg#V!4)QV)e0?c7bIayv&qsP&C;Rrr3bW$yDvK3Fp`}Cn*cXq}cot4TGm* zVin9=j;S*^5sf&SS@w&K6qB1U3E)_4B<*fi5fD@{THuIQ$6JpDjhqS1_=rp&s6s6kZ1-`>Ho)nbCM><7rNn6s^hU)h@_DJOM!<4)T zQ7Wo+h|Zccb!tUMC6(H`-;tfw1Bj{r6*W8Jv`LE87bRyhT)PtV`z}airGmXC%Y{<0 zN)d21cZHp?oQtofNI0rB1$uC$-z128)$8b98{=AFiPfb)MN?^d>D7p>0{{JQ)GTD6 z)f!>M;FSh?3OwIEdq1~POP#FEFTR=I+MeCKJe*(pJh!$pJHNa$yIehcxBC9g!p`o} z+~?KxpX*DP+Z*S#MyuHVu++&TQV1;Syb#2gqykP(E7CS*>tfP-HsoClCn;gxm(l32`8BTa9Dochk(4 zpx_R%hVV<0oOQ^8$+)=h#N0wLLQzMu#mF8@aupX53q>DIng*Q|O@h(dG=)@xK~ge0 zT4X*|Nyro(>B-6$GJ;wvBRS_8TT$4IlT1$3eYT*x9jS>D8XVEd$pY}s-p$R=&uz>vY;MfY?|}XFW_EM`_0sv~>_T;Qc42XLes6xxzgT|xVdbEH zvb3@D=H1rT+hTKXxm~Chakz1rrva2I1&+UB;Z7kGc9D~bn!F*o$CNy-&qAC4V+YNW zY};&P>$#d;x9e8jZrJqn3Jfx~6MYvej&f zd@n#RWfV1bl6|I>G{$ZrOiM;vEXAkb6feM}sD3)H6N<<$n?#F?NLOzD zg>mdKBsBb5AW~`~+?!HG+|aFld4TM{8!YfS873$^R?RC(3bhZnalpj(Zs% z`o{Z)wY^(w?!2qj-fygLm+IT^ir(&9b1zl(D%;<-Ha9olR=xPHUqoLrcCD;b)Bsss zl)wgQK}RXMn;NBw@%$rJ`kdZp^Y|m3{=$|RWl`!*R3>-YG0l7J#Vq>J$?u6Z;wh7B zka2C8)UhN_P(w0tevGgP>^<_zODv>Kv;zb=sXli># zm(^r~Y)xBcix#MdTu?cys_c~;OksuA4bAl`k@uLgTd0JXtS`K%fK-*N zRja8|FHcYPlBVCvnBwkWGllsZ&*~v+@=r$`n(~l;VRm3SqnUy8X zMGkSU>XM{N<3#7{_Alg~&+5`THj1-WBMaP;t=l!b3A|Fz*0Qy{pRJYLtXse{PQl3o z&18X9u>RPrS#=Y;TGVXcanMlxC90fWHL%ptXqg-@B~dY{md;1 zW#c5-6TtBd3290$Carzig`vPPn{c{fEFLHfN(bQXGpa)Xb|zv>66*LlD8mzzRsbUD zq=*xlw?!A!6sbEpF6~txStT%WhRwJZZ7%WX@pjt!CEm1Wt z9Bh%P#h#D828@=vUImL60dMApN5-xRkTLmgO#xM(>5{i|3LuSJx zj#QIZoy_2 zl-rTu@HKn9#1Nx_e;mq|e5Hm)Sm{obKD#VmwB-%a63Lm2#l14_MM-dy`8iqB= z@v##06uIXxN`mXn&=VHtrYlF%OoC)gQo{7iS+V0Jl>qbeBq-v-n2Za@Fnp*B6vvL} zheO8Rdn;DiG<<%Y%3tNs}n`>KO(el#7$3rhU1^q0V_scog6!6fc@3y!TVK4ES z!0)`6MtP|%rd%yLKT(2_nu1E}nWvuS)$r4Nfm&MURoZx-M+H+mh7!{Ol`m3#6UlEL zlTwyKMu!DQkkqA{1nUA|Nv{&;R*(u=Q)5&(0M8P{3kItwrl{3Orvn$XlumR`a#mlvS>gY+?&=toxIH-Kj0hB^*@j&NtfZG&Qqy;hLh#kU`i9z zutzJKN}Sh=)-1W%tu)zF^lE-k<1Zu*cwBnVa2xz5BTYXuc0P}vPd*Q5pr)#(3Y=tA zj~x^19sx3WJa55wK2H^{N=({8YCe#PDYvPv#<_x>$6-UNP~Isa_A2<)6;Xp~o)}T}yecIl z*BU1hv~OaJrP&hMf{ZxfPJD7T$LMz%~`-!^cN>5 zP0uOjRho{K;qLLkkCeS5L*WYeH1H&67nk}ut}ZX&UBCwkuPX;I&B4i~0v~7V1ox6B z;k`g8x>Y#a!1BfhLa>+LI5|DXmiavD0bX$Dq*ZgvMFP7tHh|UYIz_-7@+O(Qd02if zyxWu0i%Wf+uC9K<2V3XKiDfF;g$>qK^YpaCJ^9rV=$y40KCNkC3|y#W^|&%}5mzmm zBJeKg&cWyLv(tf9)+YOfx%`nsz{Wsq^ycyvaLwoXW-T$^%_x=XA z+&iFM+2`)=u6uV2m;3{_-QCJ|bHm^Gs>3hj3ZLTc=H}}9-cXpUt6MlI;U|H=yTQzu zOFcf~&d^t{{#X9}zwm-?!pjHle5ddHzv`Lb*4yyu=SJ;yDPu5bKf3joW1{!M9cj1) z+e|?G@frQMdpbzucBYJ%Yp935%IcRYB;ERqZ9$GxQc!2S@tf8Cx zch+kn_`rCt8(KeVFSQhn&`RWwM=!?PUlv){P|NApMRC{tW!)M-Yb;fmT||E{Tqrk; zL`{>2#?@v1H7AwT-3~n3b+azBDnTP5=-Ve}=eNdH^4r(XfBp3lwm<*;&@qh5I_!sy zGReS%^M32}^rvB*eEIZ&x4jR0U#^Y2mQ$k06lSO}Pv^|GabBRhYA|(Cy8v;t+LwmW z+S`-SNbvpP+PH0d4wd;M|GM~c+815pbZ=MpY1rAlG`gprO99Yq0f`s5sC{;8T<(RZ z$F_`ar&KQ3s9vepHCQ2byknfCY-vpdwn#2p#CLMuH5xq5j5mCW6R^y|nBY0oIZ#q= zy>(^Wev$FB)Klf?&bTb)GhiT5%3=Zom+_R9_}Q&cFC?&wP|SekRjoB|jJvPY`emE@ z!Tb$(_VWJ+j?@1G3Nh7ZE4=JU2iVT}KiN7JndLm`=xndSx zNe!09^&a^*%IqvyK%W}j?m3+LRf#)nu%h9kHOlbsKVkd$@zd9@1$e_CKUOA-u}Rr=I8qkd!*{IRWk&cgXUMXVMBs{oCgYd{o$g~WX;oE4@fl>Qr-g_A2A zl5yFnDX7#M{uyBT;$R8#w`03N9zrMl~#Z%^q zad)1x$YDdS)WW54o!I~S<b9b9RKHA^^ z^5rWWOML$a$LY$QapR`hNr8E_<<4E#izg2M{u|!a_wV}$-~T?0n>L(7PtB|v6;^c5 zu8ou9gYRGXG|t<{UDJqv_(*4LK5^6Qp%3}9>SY1I7_K<(-K9d%ljMp3(aAFqt-6KvB@MPKD&xpQM=)htQ1C#;Tx?(Kz7 zX$tHNbT5qS_ix|6#jqu8_-hNc&0XX8E#8b5+i&sA3QvXEdZz(&RXl~H`l&#}It zE?{fkzU3$0+@R-zd%t_vF|J(fT5&7`Dd5f)ywxo{NO;LFrEXl;xxFNA@tAvMG~aD+ zsn-Jg!3%kJ45zaA5qEJ#M|0uaxOhhgJ$SF18(TLgc*CZ8$9QKrq^HJpjVn5-WxiSa zX?X9j)4MvP^g`Z!xb0pP(b>#leJgu%d%L%dFJ)677ktbe!`j^9H^3hUer(^3?xmCF zx(g)c>e=nh2V6ee_-L2B5Sak;*-vJBf2 zZS-q-am(0UrdyZs*UHKYJi^+paos87^hp|!lQEr+alW=9yV0(&+v?lyZKq&!;SriP z`LpizIy^i)@*+Jr?YDkuoLAAIOHCM3NfStS3-(xi6WSK%KN;(97IEXtMR*i=LEFa7 ziIvs~VKEz@VhQiPB)h>g!L$4T7KbIqRb8>;I9MB>7u9ntEG)nUKFdPkb_E}wpOc?v zWuDr&vCsr-*THiew_;T^J_3*~Q>>;5*(iuG=Y{s-H>~ z!3J5OcPsCg-rw?#_eMYZB7YpqE30L&x^QMuGRevKwR7VLkNT3{aIck(n{KC^GId&h zp?!U`0sE=LvcwO$xNiKl2uE=__%Ps8+Xkb-LBF2l96M(>H=DS4@SiL4`LAu=z`M;! z<_jRfKfCU}TU%GFY>jqW-#9a_PIBmbpfF3Ves*bm2=-cCq08l6V|zv4BG=Y%naZ-> z*swwS0JAj1oAM)&Y+-L03mRq>H6)%B{6XC%7v;0Yk9&a*_0r0jadC=HWlmeGR8Fpp zo%w}@`GtA>#tU28^;sHcK2#wmiL37*J{5%jgFJxL}JS7a?gyN z*YxYZk)3AujEfpYHz}B(OnM#T z%j_@P-0KbFvX#aHFUl!MrK@eDK6me};`7d3o3Hho2o6@vIInXy5V3{@pV{8Mw9le)iLNM+A67pN(1q*K=@KySYELqM1;OphfSFb0QjP>c)cpLn{|8R%d zSL?KtTT2dgXC?CjBV_)WRF3%Y6fY>GWN=nJz2*t>lm_ROQH-Tk<^T_ zX6%$giG)x{$kunhKYadx=ks;r$&zVNQ7+6+B4{4S?{z|#OU@%%Sq<^z3 zwqvHtK%t>okWi>JeOq~@yeno~OlxMdH)BQFt*cajvs~wKXhd&n!v180+KD?37ZoKE zi(6g!cy6}u_#ou9!SFv~$-p=|;v$3_+FE5SAG!)2Jd+^5fZ4^R7pGDMltL&!rFVXYmQ`L5sgmAxr z1^@Vewzf;Z>2$q&+uzE@x26AO?r^;p@Yo;BsN1z#_ zhcQ2YJm0Kw@{5&q^t*Xu^lb!7{|PLX7N5v4!TB?pFHH{O2lv6pV{Y(x>^e&Ke#>fw z>rJHvj%3Z_EFZV_jq>S`Qu9>kwatx|PJUhLw(dg((Xy~g`ckU?TepjyDw-xawI{fn zw}gk3P5gh0SHY;!_@HQqI@dESf|en^erlN|uUnllDbuy~GXLRec>n@W6K=XQ!t58Y z{N8il(u!__Ia_*t)U%4dL%P57X@s?>%v)z5X-dB8vQBk}^SsYeP{r3#pY)$AjJT)0XzN6baPp*^CeQB!hE? zu@HX&r3wo38{M^X?E-(xNR`3YAy_((jcw+GZcT(H0->v`sk^=KO!Z8$@HyJ7d4Hcp ze}DgZ*~b>2g^O%luD$~JSl!xRTs~OZwr>}`pGD1{e9)E^rZ{MJuyi28FuykiQNbk^rv0;(;h#LiFc%(g*)@S`Y1|I7t$tx^zftyFY9!l8Hi@|UgOQSS4o zn4R?Jw6ZN^1p!tH0CHA7F}HNpT%k`!Tv5w}>dWn{^sZXx{O0 zHb~=eUC4J-^HAvPMPF@6+z9B~b(K;x_!HqSW^nBg-&NPY@=Iyf4Edt2WuoxM@ZSb| z*KLxdIX+U+R?nYiq44bTm`mw1*U*(iixe!A1sf~t0$c2Z(~u$JCM=EHjE#-0n%UlU zo<>_b0b_b+eH~?5`mLrrGrUct?BCq>)H6ulP+E_3)y_zdDOpqp%VOQ zyMOlg_sv!%5xb*ZLto#-vw!BEy#)HD|Nb}Cr`^&r*R=BMi_Ga1kArT>c4fWsv9W_+ zF6KivtI{K(pPs>fiD%QKAlzeasr}96W=nG~BL-NfZaJRbqf6Pg|tRZWo7Tb zkOWMPlp#WQ+|WP30CPE;Nn(40xvnB|sqJf`r~O^G*rOeHXavvK-H1HRM{^&+)n*u~ zsmYl1BlDl1>91>+dkYK_dn(jzU08y(J%r!58LkVjkGe4ljBEv)u6Bvt(E6N72^=lo z;c>e+Qg^p+bp6al^L_kMPP!!!nc4lwMyMb_^J+jI`Q|{p>pm~sk@D=ySJ?K$@`dGf z^f@9+^;R6z8opHmAmn;adb(H-qVIq2RZpmm|9GMB0;@sr8~=kl6BThRJhO~gskwcW z&^)K6VPL!Ra(w$ARZ3T_6@m$xN?HmS303HOP|0zq{;g3@cP}D>o8s`|s$WLobHc5W z3NB^{MwG$=KmtbQo^?JxUmd3MTk1NHvaJCi8ArracbA@pG(_qxVNQaE*>Ca!!rbOFW@P*)+ul1 zis<=pDK6=Zfy&O2Xtm>zos515F2CGEIx2%&z32MBazm`$o7 zC@N?lceo}PBv^Xod{D7|bZ|gvD~f#Z{Cb0y?pY76|1|V_3G&XJSJ>WXCdjiLb!>>(9Prg~Ub$?<)Er?Q$e>O`TT=6aBzq75xwRDnS1YCwi9v3y?^Z^;_ zOC~2jH2`pvD|Op85*no9-GD=z{qrSQqQ-S2(v%o2aY@DC!h6aVZ~4={*a|k1btY*r z-kd3i>f{ukyfXcXa9uOj^t?GfB_Yt$-RI|C%hkR%DA6PpBR>~bZl3bD=1Rj^gSvmk zKA&I2>=5`~)U@l9hu&}toYnNz)(0lDg4Jdb5c9{DM&-Lb0*ZvywRH&_n~2beY5l6cSRy+ zW>N)v)n1o5F#PM{Gk74nHuWJLsd*=Y*^6C zEl(VRWQi%$KWhjgIXx{6OVjCLC#w|qBf7p>Fm_S!^ndM1?B^dfl#m+O!IA7(@4w&4Fh?ZR zDYZU0plcuu*fBcwxT_h`3Y*Z|7blrb65W?2+^V+zs^4aU!{+xcR2Z@`Ch3p-BBfUW z2-n~8BwZ7Hwv+-Ol-LZ5T3h)v33iroFge}+BIOnUf{wEN$UU`83l*a5xD@10OqzVT zqq;RZUy&=0zrs_+_VPb>=nsv|Jka#zVU$;mYTuS6{{C!SAzesO8tnig{P#J7u}fF) zJups&wlqqrQCu$QOyNm6u-P&B!;dmJ4%sTwx)_J?ZC_O`So>3qza4OD0F`;X`D?{P z>lLumGO3K`|SbJ@T-y9o2c|MO``m5QQm8-)JUkcC| z;|LMFHYgld`_WVlveKZN%|j3F+yV{%iHm!T>)X!?1Cu57t-k#hEmQLd{PB0H#w`;w zJ$l^iM>i~)Fc+^F=0nf{Y7m0I*#&H4>ul*0Io+Gb_=y&T@aFRyw->1(IQqwb<*g2uYRiH@b(gyM?pZkBKcPXOVy=AE zi#-KPBK{;X7_|LA?I=kAA+|fq9C`b`AZ0PixA6vUFhO(f-R;C~%l0!H7MN{z5=s=( zmbVT-Rnc!e@a(nVgWEz+D=yowyJKd5j?<{IGu1D(B3`=zk+&%iE-UxIvr9BH?Vd=X z1||gFB}-k@7Ax*(SLl3dEd+V$wa7f*C0GFA{Aisq z2xNXwIo{0D>);%f^<#2O>Lb7M*BY4(QR@Z`>NLv}G3xnW#2OhbQ4y6a*?6zxNK^*E zxm*<`;wp~U8DS?B4nVN6z%MMuxZv-Dz=YlqZK^oc0U^elLdz8T&zot;N7#^7o&SK4 zIZ}V!DcK#JPeE{^j2r=htRS$;&}dt}<8M8+$IQ5yP|uv*q?lG_6WY@7Y>AXctlUdHWSKcaDWrdZcrUVESDB{QYpv_p@i5BO~^|e&K~rrIfo% zY=;(7x-Vl}7g#YJ5xY5eCQZ&n{GzgT;@2P7_`)&6ARL;>`Nvh#V%;Uu9U~F00;4*% zgk?TjcDN`=ZKJWI)>+H?$f}#;+s{YdIlR-=5}e^3Gl|zNQt$6x7OuN}K;_OUs_k4- zBkKW3AiTKPq+CMn;$y|Z?$PgJjDRi2Ud^PtAy;v@y`YM+c=h5$M;4U9<99)?;cvYeYV7`QucQ zbJjPUSwN!qPc+N(yM_} zd)E>cNw@y~-1?6!Xee(2rXa>jdXsBV8SJK#}E%%g~(v*Z=WtIZp^7%HtF`u>W?NnNes+U6bG3%#i$8FmKkX>oO zw+>at1+P@VhkM(iCzTKa?jzP)m=Mj_>r-yKjSv&?f5t}LL7{KATw+n`0Vtnu1HiN5 zt{BVZf;DN*0f{f1C7q?ezO}?eYMsW&e*2cd7Mo5_z!-%K?}3}%|5yP+t1d@eJcz={ z7Nf_yT&WVV&+QSM)l1#*aU&30sh>HJ#6An)G0bL1Ica5Bb9r6Z(4Z7>8wTKSlwzLC zo-sy)2)-bsp(EICOhN^D()P1>u8D2Fs#lDN<%An6bwLQHZ!Xy+QQRHobtBU*BzKax zKgT{ZJ4jN(sMb==5LiF8jq9Y+eZ#4jU!@+cwfaZ6=?-UQV9Fk5*HI=Ws|OS~G4(p~ zQ8Dn(ExhpCUdks`Z08=~n$uW1QeL{ep7kc}t}4%r7#{!5wG;jP%RUwDxyb2^<^l~} zmj3n!&}s^U%OgKv`NJU0ibU#PbEU zGW&jUdSwPO$hS3_=qvCv`qFHHiA-lN=C?_r?<5eDpZ-Zh%dJJp=qf5?V}FCGL;Vr^y!mf-HKuHGFDiD9jeZ_TwGkRmm@zEG6sq60Y8MLQXOoD zB+wF20repl_4iRGk0fzH!BeRTqFK60JM zKv@V(tQlA8&RvM3cMQNEc>#nLC8Ud@Qqf7y3%|{A+kON;foo6SyVRh~0dRX3LJ$bq zRani@AZYH(j{>mp2MbqUen`ZM<@Y3*F%5ZWhNL?8)v>^>Bz@NYMdYQKQ5-spxS=Gg zhg*ryJ+69uaeA?LxA4>$1G{ISh?^_MezL^%Eu0Q_%JJZ7|YS6nF?UTE^yItZnJkbOvTR?yyWdBfY^G@QSd~K;1 z!$FkQp@MX{@AO$1(Tq+~af|UCYKJLQ-mCeG6!_|J&Y3x>i_ECQ!MOGDSWYi|Z6W144)ss}yr#;&x({^J zsmv8PEJ^QPk?)1yTQsjG`G&ThBliALoX)O7ZO3C;yVmdSQMqBMQ5Bd1olySCqQ@Pz`w+$=-WV7z`e z#-7^XGXTlhsq7XQTJ`k&sD1Hw!JAPbHHobUQ47x#zwu;~C~P<#!=RRAu@{*Vq`6Yo zlG>LQ#b)tldHIun=UCJ-P%5o4f~crJX@pC0bRb4GGLNV9gy}(D2^x4^C_ed6Th^zw zbeSJ@81<9Jcx=_UnaPhJsFh{*{jvfnsSDQw*C-Lqm@ffzfS|SLJ6aQl6!M5n3*9RA+!wnZMtg6i2Cm z)>z}I#@JX^upePL-gh|;L$NmS!fi)likX3L?Lnl>2id9J{V{y)Vysfp+xx14CC!xU zm|S}pGFx-}m?*7{3`ADT2Ii~e+#DdoWW$7S zCw_vg^S#~$O3#QAwC-lgF3-GlCJ%@R;p(GM8N|o3_k4%s!KhP5auU;&0t1g!<5~>c zZ%uNkWyB>HvoL3&N)rEW8>k^~lD_vsLcsFH6D&oyY<;22eUFizP#YI4NqbH<^HU;P zQJrRfc*M-6=0_OV{d=Rg7W+;+R3CseJtF45XC9VBrD1Tqh>RugKU+eqLp|E&^=54V za^}DF0u5ww=W65=^ygGcf7Z2=-LdnGYWO~t==uA23RMwhP)`>F_B)NXsQ&B!*3Pde zDJX7u&9Z%Z3*R$Z%(Dk(Ko4RX+2M81gO!1I zr^rvxRZlEfwP9YV#kHFYZ#!68;$*DQV&A@J@}fRz#SVWDzFu(9o4|@(2o$ly%}9AU zf2|XjQER;P@uYxU8rBld@Z<-I@xA?F&IoHgM@1%7)!Wshn;?T{u6y`+H%&*qM@nTQ zPG=RjG!pqxG2qMKo8UI`{)JDXebIZ(jN@a8o0u?}*iElO_pm!+jBXS2Y>s24YS4N@ zII4t|07WuV>qm~MB;zkHJyz6wh-IJ>VQlDAASsjye&XCR2IHm#gXvY?7o1M=!RZXA zh0uUR;pnh;bO-92yH{0cwi>8Dx!*!rC-NXvfI>%5#F} zj%i;#JF5H6+DWfyUkOWMp3oO*lC=ENhkauVpO^%SQc^>Rw_H5e^v=lgMEp?P-j1Io zR;6-ih#6INfRkyV@kF-mk6x~jv`4fY9-$VzO`9<{5xfU3JmyE;Rkel)!D8L>Bs&@~ z;Py9l-ER1&UTdQZKxEAog_!qnt(Ui?6n<}J)LK2e$L9lG))kNDW*{(mLW67J{jkqw zq)cBmNPc&U0T3l-u`Kpw5)S8FV57EdRg|!hCwfKqf@1`ZY+6H?gs<%<6cBds$&l8^ zKU%K*?PrH3Qu(v3BvA}nPFvY7}u5`iI(j-dMOP_ABr~bg9+leP{Rgm8*I?GNO z+L-MwA=DGJtfcJHD<;%IhDOG(kZ?6|@(o7}N3sPBjxIh<#Tco<*{wJ)F~QHpp`%QE zI1>CQ33p~!^XDDk#{7pvj{_^5Fx-@u2cJoDFzkt- zH_U?6>S`rk-6kyC_-#+R#P1{lil9CIq?PrmcZs{%w@C&0mfLFa5GFx_Bq6NtoTO@5 z+eB+kTAmLNPp9)YdbV4wDb?zh=hbi47q$&Rj&27LY`>VCE$BJ&xFt-)8pum-dlLmgshi531hYI;dQo}HeG4*;|xosFYKq!J_z5?F;jz6F7o?& zf~bp6unH6rG;oM$d;c*Q!?@pdV^B+ec%W~qN+qL5x|iP(hvN07f||!IC~!Bax{LQO zYN*g~=LdcZT%AIwj^KzTjp-$|llKxkupY!aI(=6~)Y8$fMG|YrIsBp9sWxA{3EL9> z)*abt=vyV&reu>DF7U4|NJuS zoWNC z-BT!4z=H^-ho+>l^T?khC!O%v2k5O89vtJ`eV=N26T6GQOGKlT>*h8#EhV;OZhcNk zE2wpL4md{_G$I}MeJ15hv(STWF&r_Djp%pcS&=b z=Za-ESaaq`WIv;QX;!cN=^%nQOOXK(X<8<%?aibybTN}m+`w71sy7ftp=oo-a9Fx^ z#XR($;|n7E+f#Oe+}|#ua}OlivZS`cQQ@n4%I8rgPXg;OS`iz5?051_@*7w62FZR5 zJ^-J>S%QL(_^O!88DTvy)85Rm*bz6DQ04-HrIMCYZp&Yo?#s zFwKd`s8>fi~!%;tY=@yW+IC&WT6@iL1Kszr`fn-K8Q zcMyv~#p07WP9h7%p=ox5+XPpi)P=oA=UV0JjJBljTrS1l6M?h9tP)CCZuGn3|LpqS z&BVxmXkWXqH>w$^NsIcYCYMI%PueQff=I`Oa!~Ltx8yI^IJH~`#X9pOV@M>}`!jk_ z1X4+c*vlUKbMHu>bm!V9`C@EWyqYzOjK0gL%izo^7c*&h{hbKBgbAn9r3gShv3$ct zU(_5?k{{u}qbqww1trNZB9&}dLpA;_cxUfU^)sRw)yp$9MvRgGd~2*9-)uR9PFaF` zhYL|5s{Wb`@sgzv|9ykt{mW`!}hHcx`bZ^G3EUR2aa=XJ0;Ql`j3(7QPH zr=}RSC{;f}rhB)(k#riXPSg8v&8D0R@xRK*_;yN4y8j>$F6)19U)Gk$5C3k_VSoe} zJhSf2O)9^A923dZZsCKMxTjl$d(f*P zw(4o~#jk`nS+WVqUD!%1geSpJHj!?XyPv<$NIb(`2zS1{Pg3vQhr9oA1%We;A1jx5 z>AGrEQzrc0N2<4654=E1+L1FNv!h~uIIZa}8%ybaA9epwEAQt`E@r8}8s&plFxN&jzX zzGW^)J1Nq^CD*|MGqd!9pF0?pt&l&L-#4$U>oR*1_!DbiHd%O+(_oK%dL>C>jBPV` z^&vK^-%hu7+<-&BAW)bn)~iL4pP`M9_EKgibhg}{IQDs3bDfT$yzZXjXHwh9cI|(c zs1t|NilROJQ(8iEc&=%y#17xk6gO{t1beS}eC@d$bLTkZVy6p;gwa*=aIJ(rlwiBB^N!9w=Y!B8@iiu`BAi*Mb~TBl#|OfTI0 zz5bz;sp=z3@$w#v3i9T{#h-QTv1h36^`*Ll;8v$iflCE@ufji7%p(3&-he#49*VqEzGo^;Ur?yR+m(F&sxZI zu<(c{EPs7s1>ygo8n>K8?WVgkUh748Kq}|;j#9Ju-dd}B03@EeBMkB}yqlKu=-X=O z`qPiR3Xq~09R-{)JlF70n*9Oh@0n;?=*e}%Xc`f%eXU4V@ASWHz^5agad@E!Ee$W{ z%h0wNt>DZ4=tG`TqmFtY3elEt&g*me5!fnY7RwEZ=u&5VqXyhWZ8&N$qTxorL-=5c z%qSO|3g_+jEiY5_$73xoUJG*C!3XIRwbgp=X42iarA1UoB|>loal1Alek6+Q>7&G_ zg8a>BT5f7M9E!lB60bx^i^Q>zfKyC3;dnElR?M@Fo`Xq7R+#>phaI(03)tqszp+3s zR!3B%{Zu8BlpFCSwVej?LD1x^!Oyiv^c~``ie4QV7a5P~;INVsdeQom+A{(loF?#tNb)nfJtN8m z^}=UeNyPawqpZwiwnSk|A)~^J;9*kik1m9&%Kcd#nfM2Zrap68M67*d!c|pEPeSo~ zudeb!qdq{Ufsc6QGRB)E$h&ys10b{_BKfEZ^@4%fX0m_;CAAC$ z?Z@ATiQ;?tYb*)HMW`oT3a*lP{ECh6 z1URHWj7Y%E`0anAHxv|l4r&A}A(elct^J)c@`(TvmmxR$FN{?cvur2TY3Xrs^Dv`I z?mjeM{j7GNr@;I_EdVXanNj~09y}ksqWo`X_H~3;SZ<*$6RKqCf#>F* zUA$|5?<<+5rmwHX-3gzMfd2}H(i4+sl`-S-rfQB-q}_V4!EystTFg1=GvS7bXB?F+ z(LqmuDO3lijvNa7PqzjGo`}3;LPg2^9Ic0Y;-_E3xw3jXJi}}`V&sqW2Bs-fgEwBp z3yikRkVWYvSt2$rF*cBdO)HNxD?Q0QHG0m%=oviuAT1lqL{?{qKj^0z)McIl3<;|t zMM+MNi8a^3M3xgQFbM|C7zI|y-un;r*rX(R5l(^Mj*|UVM9%51Z~#_NB`b?EDhzT0#Os_h-azE6FzPue5evcZet%Epib_S9t;B-Ifcx1_hl=oFVohn>%DpBHQyU&}=L8)>0Jby0MPKG}T4Qx?VMq*4r;{z80-p<(k9^_j0qt-5LhEWOH@Z zQ9Uqx7+u;Q8j#RaZD&so)p{(WWvx26doBY+II<^18x6*x1sz&PQGx8x!bARm#WT!m zbO~JX;^T^AE;=GS_!-FHM>t!tS5RuPm$lpYQvC{mz|G*PUtaO;Prf#inw3Q1Kl@E2 z1~5cx=0!1_vH?I4d5|wnQV(46P)=52^P!1bER846Ix-5`Crl{1Ie}u_f;R@{QlG@h zNYQ2k6mE>9!py;^e9F-*P-mTYcQeutl~#4mx50(6#$-2GJ@BRR z{Ea9Rqd_-LtMa`+px&*G;G=slk!wnv3IvrQDXLT#K`yTLhxZdeo(0>7of%Hcm6csT zoY8IJi{Ilc);rb{4XaM_#2%Xt(PT9mBc|$3zMb4-roFu_diTS3-V7Fcu742>j`g0o z6nf|I+jFOWY)l6{ODqfqXJ&qCt|9q+$;Gzjn*`D{vGxO z9QWufba>J3NN?oW0b?YH0o@NRJsSFt9I*;`qyqLOWEG#8*tToTjE<5@D%N%!?LcUk!XzeeZ|HT8AE6LcZ~DtK_u z>g#V~Q8QfqlB-1!!h4Q{SESqtMt+sMsb9L!&5UtSfH)%bpx70ujvT{eaT~<3 ze10~JiLX{>AjR*>ueyoo)4SD_1RVNYIafbBe9oXL)h63jg_eWvGp}JRAjs~o(Bw+# z6!vhMZPH+eUy*N3)ww!Baz8^&=#t}8V3hiw0EA2AvHe}CUie!SR?MfkIP!3ZBcXOa zfQ_So$*L8GfA?k{a(y)Lz#V<2sIvk#REbE%#I{u!i?cDvd=1?~H2Dzp5~XDZ^(8^%*$7jW`j50TWbERH+2a2l z*$r^gwBNFVS5VBToevfds_xx;^*(DaW=1mp%LmaP`yU?A7caKST>TWL^x;C-7XvX- zh={$N--SN(rOItjZ#_pQ^0Bm9=9DBqGKsi+6RVbaCFl`a5Vb%y`~HNPOh36FdT-WW z;P1av#QiHS&I4vvv`fT?bQ@xP40DHd(j^JrCj%C{#;@)6+|Lk_yLdf?ScL4I>MMIG|35LF5DF{yQK1skOFF`q-^*Kl*e1QO1i`>)*ya%OlHI?&`5&(tcmksW9o+ zZ=jN82V3r}67|6e$s=TIBg(|1s~<}XrZDy^9wpQ~N4sKCl#!orS+N;Qx1j0wGa@0Y zD6>J3JhMjdTPaq<^R)$$%jiBynyw(90E{qNaw-o-Y+NT`1FHz;_d3CJ{*-;wXy`s1 z<+Q4jhJj_Kq9ZzywJuDgnyJP?qG((#%cV+Tz#U|#Itwn1quDgF9h^`*SsN@Irr7BP z0&{%?;}~u|wvduoAZK;#8cy(8Rlz2tu^aHpzKGX>E= zl>`5v*&fc*+EG?Res$@E9Q{muAEdg`@W7T?X?`Z_sUBj8pbaJclB31yyewa?R77yr zF9@JSh+dU1ZrsBWUJt_8N+SrZ#W9wXbbW4T4)X2_*fn9+4)P{la(&J#C!7yA-qY>2 z3QS~qnY8qMA*^%Udo<7T;U5quBYiuBx&FHLjEAAf0%RaD94xFAuV6%UN&EH8uMTU3 zXWk8#KrmwnwKKliyHcmAcKDX>Cx(@Qc<%%AAj#!JfJwg$TDPlXTrLAbmMXdIv`WN{ z!!R7!)%FoHuTX>(dCS!5h7s!u8d%JtH^h4T@8>x=t(k>~&+;PF2T2-*=I2{;AhUjI z!W`_~@UI=5KIgb%AC~BA&2%b^w&465XQXK*vz%8R3bO)W(3LY{p3cT+ff+a`w2T6` z0fC3rDPr$1VEqiytBHBO@Y;-V6#)1a^Vc`G-JxGrKB*)Noo@?{tupS{oS_q|DGqhE zg=e?m7dt(-n#zVA$E4%!lMRMQ=Jy+v^3hE2XaHd&>YCN8d}yf|4i@K4rKqyf4)vGO zEv$&(f8WuFkD+@I6stM#W?~Njsk}O%<=4a7!zh|(>%pxb63Or$cu_HWrWN2UlE|t$ z_<1`^x*$%;c%4hc~Z= ze=@Hpt5rkZ*gtU3_x3%L9Ie7Pp)`6KpWM-;%@E{VMUau9AWuAhB2 zdb`0wjLU>^XO@Y*S|0>XiM1Y`5MKBc8ju*!<^qH^!#B_^t7z8tcV$xK`BPZpZ3ObC z^$W1-Tl<#1M{;S)83kVy<*n)y83Nafb}XKpefx}(P|GXxb9J4K@-WR0 z2SV1y&gV&`kdcyT8#t|UK;=fZ-t~hC57Jy;jVA?u&<-IOYyuhFGCMr>zlPa#)!;_T zL~Z;4YOAH1%CBWTgeGB_H3vxA3AH!AR4z2fEe~EWqnVywfNA;-d}E%%mJR!Aoh<^p zCJp7jmX52PjR)6Sn|}5wNYDcd!ImtU5gSQVeh&X31$s-T{8{n1gloB8C{*x4!A{EL z#?Qw0gnVHhMGy5%Y85Hr{wMGQ{9gKV_pf2r80c%ekQ7N8fZDBMr=jY(75>Sr^!ZEq z^5ut2(2{N|L_j*drT#AJ3?z2yMPiEwaMkKp{SN$$2Q)BCi%G#&9aDX9ffkCr@LX8t+PLGrr*>reIIp4%e6BB(V%5adIZT2l;i** zD{U+XZ*bNooL~Eu<^2|;4pGg?*+d3x&2rgVg!%^#AZ_Yt%BcWH)R7U&9R0m1#N4UdU?J5ltyc8{uQd~<_pA~yajFX%@Tovx7 zJ9iOfp47ryJ)Bl8g)=|?*;pS?Hm9Ek_X!+O`4Gg!%!wH4L8Kx3GcH9?&o2> z0LZELhg?*5h8$$ls!96ZsW6wnlZ*&X&}LEaB<19){+p|U*Gj3~r{z_*Km=`}=k>v2 zz@adpfKiHYi(bq#>n)nE7km41ULxb2+P>ambv~f~)c!{+(RFxgL&sIEo zN9V>*WBShI+i&R5HJ z!WcgQf9gxZoCps$0uuiXM@g}M;dQqJ4E-r|qG-$v=R9z9;LjY%p6ZOF-ftmPF^hdH zS|U?#UyXEgs0GnmRDHdk#Ac2sNbw&_C)Bzaak+Je(s4_j5CgT0JnW}*c5-W5WF5=& zcAhsiq?@P&{f{*D$}=z`6%q+;K3geK&wnFA9mTb7|MV#>(`yF5%L{-!-9S6d;)t9N zZ=+v?r8QqpXu47U)Q4!=!WRJ`9PZ(hUS$jzp8pj%noX~@A0&emnA^*GvADn2x*sc9 z?_86Yx1@KA`c^qvL+BF^oo0JW;1x}Ikk0Ra^<0a5lB@O<{{4q*?hp`TyQzwj1rdI5 z>k`ZzDaHu6(zpr`EF81p>APSZ$jS&Ig3B_K@emr+w7yypMyPmboBQ&9ORT{7*yi!SG*I-lOIjYdi%0+0Lb5PyFzC zd7FymJL$Fk60{QEcP_dudlV*;tFTyD^e&a)JEbr{s#a*oc2@5>&wx*V3w<6f>JTp= ztf%*H-%?Mn>uNnaZ7K5HR(5nE*A?=Zd8qS3;yFZ^ZH_mZi;b2ch>~^D4>dC@QSC&t zlIAclD5qKI-;av^4ZU`Pr~i>mLuI7*js9t$)d+uMWx<*Lf-}y{xIdS&fK-R`bIbmx ziYgHb_)(SQQTQ7!e?1!de93&&VZ936$cf59AA=Yp_sqW;FZY<@Ir`ZcY#-BjT-i+! zVSD^Vh)!w3Fx6x1D`qPfEhm$v(;rrsf`IpzCqrde=@Y(oC@9xyTybrVhJw5hYDAqp z7DZR1@8~zExGt|LNa68)4}6205C8MbIZkdy>%+;uk5kH>4AP>H1-O z1P#hTQPB2+t9EH$4@fvZDnEIcMe!5a6!;(hLOIp=FrAA>6|7OhW`CXYEN#sZPS6~U zfNpNV&T^{^Im_|Wr5!N!f6^0};#^>m(=3^AC4*;N1{NK5Yj+_yj=ocWSl_z1)~n_G zef!aJFuoEJjSuX_8zu_q#UmQ8euO*n)v_W>Set`VGd``&1@O436xs#zu&4};zlfN) z+U!wo_Z9#CN)K6EuTw}ld%J;cvMlzlML0wN)uQt$IQgaRWdcCitTMp@PD%difI_ghan2;*@`so=j3ZziZL9kOXnfHL<%WD&V_xQh1 zgh-GfGO`nYb*X9`LJ1Ul(H6u>|ou!u`~(0ze3c#9iiw^HvO7xKrm=pi#b zDyxhyt*lAdM6JHYrA*%VK=KJOGz_13))>1qf zyYVcvqS(?>{AGIOk~!J=n~vZNNau;ouVe?Ofb(gQR#v#9Nvk5!}aWFp#p5Z+8?Dsvpj5h#{rU9JNx9r zH*-XQhD_m!s(1hsrV{doNVpIM&B3-78SA2(*Dgi|Ma*o{R-B(Eykpg|rSpe5U9mD{ zz4iza0ueQ-yRup*&(NQ*r~)6O1xcW*o{wy17RWyC+cfz;Vf%_4Q%8bETzhqZ9Ik$R(BU=tJ6)|0NM0l$gs( z;&o=ySf3XVJV-LgzWdP__C8>jh`wV}?ujOW`6&~tQYFrM5^oK~M`DM|c%0iEy5Uk7 z0=LXo5TPhe##@tbWu;g~3iF?kdFQ#&n@;o{i4cP|mDiVeQK{${A(_2_g$!>#_FQlIb~-$c5cvF}cmW@L z$^({PY$+b^HTT`gx|UP7fq|$cP-4&LmVzWfgj>mY$|L`e0fAMJkj>O$f)l-}R_Kq; zU#$8;r=DW@@#ThyPqvjwS`T})Mc%Je55!%+gr7$RXPq_fk=1-h+H$rN>%&CqJ>#E9 zQoB%gdjl=3K43f@TD8a4GAHsX2@?le9%?5g2SKlS4YSyFGylr z{M!m_lbZ%v$9-uG$%AqZ7IdcKmBa_pRdmkv2pc|C@DFG`HdgEm=hwDS(AO!?}^q(Pv2 z@E^-eIAsL6HFAwlZgf8OW7H;Yg5zJN_uphMBIMN{B09DoVgMb)JPZqL;0%v_P1xxW zD(=iXfe(e?&HeHEihC^J(9B+U!kgxcD=QWguaPg5rz-;&K4YZ$( zDUz$npwQ@U*aPGKD7S3dxXM_T*9-SrXa7G&_`mJbM2l;bqeQ3fauza8^ES(Q2FLla z8=c?S&0uOfUNcSs35eR^gmYyk-vz*FYfT+@KLX0HjjQa!=Vj0Q+3V2U7>UFXG6&(( zv=;BBK6f;Pv^xq-lD`%IcAp0VgJ#CsFDt`oO3(t2!4($x5x?%^7k%u|z^z0*yHkF| z(C0YKB#q|pwN#9y%s}hv4k1aNrWd%y2Y)tyXnU`Sm7zd010Z#8ML>C|-B32d4+0SG-o&W#DL`TlInuMcLx1*RNkc%asB8Kbp=no~{3VT617L{#H_uyniVxlt)ObuE=7o0o7zf^8l`6Ky*K~-9{iu?bsn9Qb3XTd zU+?R>S2)EW0~+gwRy3ShL2cl&>N4mj_d?0Om4Gq(>>KHbp9J84h7+E0Kt2)Zxdfl# z&-tFQadLdcm%;jR5G?1MRgl#h=z#vRoNHG6_oACj77UH;8F_@*WGmtP@*a7CHx*0i zx2+QoSFJ0=f&p2Jl!xGYZ>-uEc_fek{&Yubdmq%_^<`OIk*d=^&2}V<)P)}UK}6g{ zqTz|oae)FQbxY(Rv{>ISg+#fNmLA+EiG4rG%I&Z+U&pMuXp*%Gz~eV5Fa|1~BCqDvqwb6ha@$-~V!Nz{sA*?+q5uh? zDeg5PJgPl$X-JO4sy43|oD%f)#~$`mp{0Wu##$0 zu>0c*dK~yZ?onf(|95q3|KP4kXl$e^KNd?(Y_p-Oe(T;`48y1?0&wlwrN4aeN#3lC z;|9KZTv#<0aLI0(e9yE{+PB1FNG$qf3MWxTG`m{Wq(>O@lP2Jwp*BA&+6$%2GtKtl zogr)#B?AH-H3B~yCR-Cu#;^qLgZ+#`A`vQw_Iu~VAMv&`q04|@C}yb^T)A}21OCSd zeRo?+RC1dEbekuok%Zq-Vth9%f9efn7%Zo%bc(aLN%+hikH?MJlWaG9dY8l%(blW} zzy3qhSGpCnC%mVO$t# zSzci9Sqy=e)GA26&qn}&0-i_xUU+9~;|97NV4Bf07=Sdp5ala8z8-tfMwG0zR4U1w zHCEWa;D9B-VX2oXq5FD&1&MLKt76h|Xz9%gB3xR`$5&{*Mo{J1zOMDNb9`etVu81< z42JdY;A7ddIMN+dzU1d)P$3wzjek!4ONB#xN*pU_M#G_4KOy% zl?$YCcUUFp+g{z56&XbAJ>LgN|2JXR^ zA_O_3nM$qi26JICFUT>;oSwKxh*xMYx8n0%T40lIKn4NkSwtKMOB6V(ppQd1*-j3B z(|#P^ve#4Hnr||M6bz^KQ$Yr7;Gv-PuQZW=BO3WxZK{7=mi_=Menlu@A5em8%N69J z{&5@Zeu*wp3YN+gI4OxnN5ne$JnBSym?9jQ9{|IJccQv1QeO51Da4!64?|16RKt|O ze>YQXsWFdC@xGMtt!ILX=>^6zN?4NHck(^WqR`lwfO}wL#1ZM<{gwg>j9vl?KMg8O zc4TYQ##{rholh=*fS2~I@iliGAy`ddQ-_$PRs;F9^x}NMFs@>S9!#m>iBL_8U@EQc z!a)$@pB|s$r^d}fbToM<{1lk5Q*Z~EHVnl1u47f7QRh8Vn?>fWagd_gt?RRAXLr6Cq$dz8crgyy!XKM1Gtw^YIROR_%jPO zj}T3CS;ZUS$2F*^=ciyjXEkz4e0WKF{H75L3P=Q338eXa^xNOsTVA70C%jE=TbgyV zIi-Z?1N8iF?kC4u!*3`u*;nEv^k;QtC2F2aq+ZPoeUKOLwO#A}V9n;i{X-N4aQe2T zEr^H@y}&TTb$u<~5Iieq)XtT?-;zfSz)!#ncDV438rg{9A~Sc8jG(KpfdBuq0LNgn z=$_gE_o(9Kvei(-rQ!$<<)5psFDQWQigIxg`lWGk6>Dxs(Zb?H@W1xdX$Ehm@6XCGmus(W+9lGEpo}ps0iuC@M{mVh>6g~UR%u!^_jS3#U8E|h zv5=Yte;}ySf7{4N{umtfa;P|OkP5d=2XTu!eYig=$c2D-Zuhj3$FM3h^V-m3x5KDP zEw!u2s*fd-scQ9}+OW{fym-DnUbHXrS;i|7RU7z*)(<0%K~-l}neYqPM5Q^^@UsF( zI83LuQXhrSw+RhG8P8p6M;2b-CuWsSRHW4XSPQMLpCvDzo3h!cy;j63$kPv17gGRP zuU$BH_TU+MW4Lvj`^l(GXgEP@O2Ep7^f6fKu^j3R5x`5+(!Nr6H_>*)3#p2($!5WK zg47Pr>jt(BiLm!5>Pus+5pn5?wagRVf4DofeNz&f9oJ2l9Q+Q!uLQJG*x{78?eV&= zw=E-^@yfiDDeXOz+XTp z_5JX>yc2;ikmdN=5fx)e{^Z~&Cv|v8<&W5!doS3e^gTXW3vu>h=Ox33J-f*6s0JYA zOKWeYM8ZHBlnm!o-~~e|STx-L>pgH<7!=k<6g+U>v=zOOMz2$5S z;aA-g5aEQFJGCrpWq-3?+_ufKDmsD1ixJT~p2H_tBTkHcbN?q4%xW^or{0hJn)*IL zl>)CjD*cK2bZeGY09`0y?ts?}A)^#@LuGD=!evcF#SGn@ABvuHoJu|8%jqA1)fYKzeo4|a&o&?72xv`yJ88JI!Uq!{(eOYAj||Z#r8DEDRRcJ@k27!-1^e_o z<1g9pXpTsC^_T+^5@RN>hTJ%n2-V1YOb=!v1%EPRCxrh8{%kGMZ~D&g(xcG&(YKd% z1mK&7fBB9hI!KYK#7CXwAF1;afXP2xXhv|{j0oMLe+5V4Be+LduKd;xb)m#Yzi`|P zi6QxWeO=}>!%HX*j$SNxXaxV2o6QjLmH*ZVe~4mjy=Dr1ctOq2#h`~>yn{<5ewchB z057OC#lm5Ef1QdJCei>RLmHY_zP_Ha+PAf&<}i3~*p}=WbAKPKHS=m=X35ZBkEQM% zTTm@lVLnMMrz9_#x=`W$f*zQgVgA3i)1cZaKX-~^b+C+R9iE;l{rDM|kS07wh3VFhq@0P%EW^a)F?hZ<5Oolbh-^&uE9v)XvnQLbGbwJng3V9ohhR}b1;3E@jV z{61eQAt#wTfA9l-Wz;G7u=c70T=t1Jk_%Bw3bxOA9}-cwmMr3@xtl|69J3)!<@peA zee0=LONw6m`%nlDsVY02yqNT?;c(y&!atu^;Se zqH987W98c|JgZCn9>Q1uQJT*^#Em3Yp~DF6wC9W=V$m0~O3NZ?8AY)GNuIqz|0Tu8 zTeOLo1`!Q2tc;Z9JBqOCudbNJG7-TaE23U07k5=Imr{X+Vr&!eCZs>7LX1Jr*?!(q zSf%*_Q5fz}YbIr_YQEt44G2Q0m&{M{03Z}*{VP|S>3he0{G5&&km*ATPW(P><(Z`z z5$B=w3{1fqPa>=!#7@I@JCMfx>7&A+duTd{_mH;jaCx!s_Tdgder(itv)zqz)a(TD}W#Q;w2^bkxftW=}=a>#oHWsJ-r+e^CdzAU6DcBLzlb-1Fg^ecQY;9wszn zrRM{yskM4r-HwqwAs=%JL_W1T2-Ln{z+c5wL^cR*Z2eT|C;3@+QHyK(^c%lCXt92$ z%-hA+T`*xa$|-t0bIShqSg04W{X3~|A}f9nI#UP(Tm}#gAlecsU~nE~C^eX7*MejX zfUI&m+)VZ3Nh@S_kK_FmD9?+yP0Rr5)fYYO|%Y^|#3hpYr`W(&F zZIb54n2!8RbIp4zo*+(XRnQ~vC)Mj{$buO{BDk`Zv1;h6a2a|)Xn{^mmSz^|q-sgo z+C%2+jT%>|+<6qS1@=EStEv3P$@YY$5a6~wAv4Sh+h+VQZ|J?k(A|Y6WOH1uz)1vm z9G&D&MjLHmNJix(0ZI8jJHK_>dY~J_T8}yHZl7Z`)@YmnM7P0t#9D9 zFij^7<2x;}PKQuekMn6TgXKOC5Dr7XdfOKys{S+}>(YUoYdxX8r-=Q0?CoP01u>vK z>yO;S_p>Y7_spi>R!NB4{ECz41t*b(L=boQ+OY&?%$na1AO3k#odYqK?t;Y(PLxeZ zh0oDL04IS1)>MXX%Umh4NR5q8x;2wg%VYk*k=<`~0^SD0V53m*;uw#GL}v_gUj*`Q z1Pk=V93b7p$#6V+XZ~cc0lclUHDcvL+TLWAQ*GC*rO6o@6B5DNvFaDmqxzA$(aqg6 zSLalItTc9=={NeVhJe2(2r|&Ox0XK(5J*K@J;C9GVo+ zc3+nB44s+jl$o-PiNA|Px@|z;`$lyoeHGYoi>HyPd9Nqc)hyx<^q{ODi*Y||vVT^2 zI;NF3L(pFwMn~A%G%d(MfM6G8d^6(=^<2xJQnn4JCLt=;7tVBF_jNAEYnXKq*NsaDp?Kj&vZaRs^TYeZG2M=MDs z+nQZxZyW#aaS-5qwfD*{cP9?XeSp_#F?|7rK;eYr3TpMux9U~TrWfewBv^SWEx$<( z{ks!)8u|ESGxP^vcX$#TX_f^zPlFPS(<+)@dc*9b#qxQsIq>Vp;dBG%v!1WL5Tj=a zT=pZ&%~&Z3pJl8MOuI~g*jl$m^gPio-IDpzm@Xak5hW?udD@2Cf5z;2{@Ma#)vg=G zLNwqRi~NGR+L>@J#e2T}fY~LTp0WUc3`D6DHf_~O7A(f>gPAALBua!W=f zEf$*^&<`3l=zP|NP~}{4Pvfrf!=k=L11YvEt9_>f0e)6*BSh3Ez`TfO)9G0Cyb=q# z=H`*TYV!xnk0{khl?2LOY$MLSwPP9c@o_CHWB-D3wKb*6x^HX^%0EF{As5wUMw-|D z48PZVOW!8i@EPJi^iRBN^FI5Z4AqK!Sd7e8Xo8ZHXzdJiLg^xRR z^`Y`3Sd`1J+;*X~*^Q{4%T{L=ioq^L`N7}^*I zjScVfv{tF$4bCP)yg78pqm`m=Km6GC_*H1Q2&*?0Sup%NzW!8G?FF;H@Gjnak@9_X ze($tkf*~@tuJs{1KD`G`k@vD+M_sl5E-%IjdOemu)B(V?3Gpd>33F$Ie?t1BfO0i|BK)+sd*2%IC8zIz1S{d7$GD=MdaJn7$m)TVs zuh?kJY8dQbAj1qbPo#dRjbK$;)lVd=Q%bcrQ{v{k-r{Jf=}dNIGl&7Nua(yHTpUwM zOSp5zNWxnS*H)(NH9UiSw08ix_A@7$LKEl&xTae8g)wO{KG!8ZSmcCf=2iWz5Kh&6n5XC+S!9c;uXRX=9p`PIC zAmct;=mWsO{ypwbR9SRw?p2XU$t^u4aOXZC-I^IPF#Yna1t#VqOpUClUXB3o$otXO z8n1>e&d(63BV|hC&Z!vd?JC}h@SzO|JP(G z^LU@JB_Gd6kI+cIGdBrIlcR%o^Ce8S;tfsk6>fQIOmK&sXd(0Uy)e>2iPOW!p<2(> ztTnm22laaX8by)kq!2XRJmv-ce#V-BN}5ymAhsjT@ZoO*_izT97iob?u(QFH>&&NW zq1Hyk_!Uhdbr0lwJ{fY4#SB&jXz%D*EElmAno&=$pl`I-7{m0;_)pF4o{dgEsp1Ym3Q1Lt`uL8=0j*`+IN z1Fv~BsC9otP1;fbb04VA*@{k^{3p1zCcHulrY09<`x?RQ2h=n0n^+gXx~@wwy^9-J zz}XUJ_I|M>feV!?hbK|LxVuV*o>vpI?~iU)Qp4T15d}u?3_U|)tr5{y%q{6K-M}J) z>IaDN2Wi}WShs+H##+z`ek4bQj`KQautse5uM?z42qcx(xZ{!3vC!l0O~?TDI@+zW zrVsZB=gG=wZzQzH@@HCt5Gzp_76ZZMP|I^DtxA1Ft-fER2(#96%Kw)%X@e!O&FRdE z9n!DMPfGOXAq!hECO>&J+$LafmDaAIaB@$S6mzSd#7qlV#Fi|c%Xh?o{q+eyr##CN zCi`M(oU%TjIg#P@yU4V3hdRyBN>EOB!s7{oU;2+yeqv(h@>bsiKVZ;&g2j?U!+jC} zt)iwK&Mg8<RN!!ZCIg)7 zGsj+J-zQ|0d7X0zwY*WEyI+u|av$$=$UqD9oG3Xo=UwXu3b2QiW6;#?hNHWvpo+># zTL(Cx>L}u6kFj#mn|&OMX=5SYhB>;%0&%@>qoQR&dx8!vyZ!pXJ65#b;a}f z3?o$ta35{8L|ZEr%&qtFF0dYn41u9fZ&w-r$jAC&GW?5DtC@j;Hoy`udKR%}AC~kk z=0fs8iyL6S2RC(HmDOrE3fxJnj{Xh}_Ed`cpd=M8d+qb>5h6&|M{b3i*k;<+1{FE) zB#K*CSEm6YffBLrBUH`&pKrOrA{__7OMEt!|~MLjfveboM#+xp2jz2Bq%X3B(g zsh7TC4m!IN-nw&_V)uF4qmj)j7W}0>zQH}TGoK)A1v``a<_%4IRb~7j!}F>{lU;3u z$i%7UdRp749oAY7q`&)C`p;`gsm=bP;tY#n?F7*l-C1YHLGExD$5&nX`Tq>V{tAg$ zVchGYj|j!!83gq^>MT}iwrtXe{R=KS-^yM-7?;O#voEJ8FoR#v$Pn4i8eA4E$8&d% zlR#0JJ0aF*tRNQr5;gnh{2gN8MO!o)3J=MlBCYzBOl!^uB@c`s8#=8y{tzSp;=M8!^FH1$l_53qL+i*Kt-A1G-GC<}*yp&*ZIkd_GqUqO) zfGo!&TrWEMAp}m726`_3gn;7kkdy@8su3Xi-pHRZ0X4hQIoqMq=Nhe=;1(~^WXLCS z-i*m9XffVP#kYrw+ev;_7`Hmw62FX_S3Z{7+`|^7`S@?lBFZHPjMr_OZv;l_>7@!^ zs*=EUmm@cGX8-*ezW+$0e24tLX8AD@OW|s6n;+(}O?!%J@sE&)3rj_=F9;*T-+E#U&kPPI1t7qkq30sl! z1-c&c5GPQ>e|&|g)i8?UjyGSVS#h!OAp2a^DQ%y_ZEIYVBz1b@S54YC_La1 z&AADim^Id_g&6Q(1TQ-qrAi#T1OER!IrHWR=u(bma9UTU4?C9e_3pv=_oXI<&LAlX zZWf+&%*30mw8X^1_F?zWqkWs~z zEg=|0$8EY5;`I=WWWX&Gz;`kjO(6>Zsv&>fulLRTn|O@9x>;J}=3*U1xOMqU1_|P} zs*x4B*AFyioS8nTt2F;6Y8##F=2#7xe(fviO4v};p6g?EEwM3STt6N4OIyyyZ;#hR zu=D%?6CljAM#Fkt%}n4DP2Jgca7qgSgn@v1V+9mo8DEUH#z8+Y1pg#7{D1|)T0}F3 z;f4~Sd-$xWbpuvi?b#xRo0mqmhJh%Ie(Uc?gz5PQdkgDKf?m7M6_sZzC9p}$zhA11bhMC76*)`^b5 zHGk*OV&_7^M@f=Ia37yY*nl@j+hK|{07un0@{Bp~BMJsh|G^q!&53Vhe*Sq%7~}L= zCZvW00w?`p)QE4CYrHP0?J58+4K8SQOJjqJfj~kux!#G+xZQAQ$`j7D0{L>Z7i{A zvX1W(@!{oYu1*TDTvo&=m;z&RldmvX6>&sgtBK|??}5Q*6U4L72X6{RMi z#8QyY4GJF~%ze(Rf<5R>MoHa4H7&sF!CWlfyV_jY;z#0CV3SNWsAEiOlJ1gEj@b^Iql9dX5<)i*}gf%os4PIRcco_EkdrHT|$rt_3y#_A|m+8m=3^aSuWH_VABATD*IwP z?NnQAK)QWA62D{!EB8?;5M8?^&mtw)TKF2i(JkT!9Ksa`&3fGlLE)tWjU0?P6G*=) zlm(C38=9Sg+${N)w2_h$EI&`MZ$8KVnrDML?l)7g9lt^sOfRgB`!bOXaD`YGlNlfX z>2zVB0i5Ejs;I!@T2aJj?Rk2=3q}8^I{X-|Ff8GUik%A`VaSd6D<|hlM+*+|h&btH zdRTdy^{k5~%Kfda16!NBQ7&~$HKX!k5-B$JtdY2h!acWj6o1-r#2i}qgc}|TpkpV= zTF314L5ksmq7*<58o+?MwhhpcxSgBy6*rc~YJcbVE3D*WycA_S8xh{XOb5s~ppPDzk z_-il5&#?MuZkppGb<(Qv>686kUiTM8D-UZ&CX1IV36YXt-lQeRAH;S_gp(o}077o- zhCDSgHl$A%r|iy@NcI;BlG%wyZqu;c~ zlij_P`G_*Kz+~WMmJi^g!Nj{AtL(Oy`^>K%Y6r7@{pI#6F}QmP*S_}C++PlWXVL(} z+K)kG`00ed>T4gBqH5}ld$?avk^kToJ^$mZdPan8fFv;+hLB-*2O{K#b{8~`+woYu zjGRAwj|53*!}9B?^`Y2Ictw%bzUff(i0VhH!+wf3ZI|sOvY1Qt=FyqinNC;?$MX)< z4=@*RNVK|YSbLGW+5~%eh4M-AGIPX1ty80&4N%WVr%a6Xb29Zr9@^If#B%-IBe#k; zYsC{MfT7|fX8+Sf4FKRAy*I!2R({XRL$x1fQX9j&4hgQWHh(R>TyN{}q1y+@eV1J`82)PzYVRi52_eAntr#A| z`q>RQI3MnO=(&F6x0<-3WN++ZpREGI8pyry&q{-dui7_>bBQ571@u z2RWM!rP$;KDx z9ag@UIg^RGJOMsLo3dsSDs^h2CBvSw7jhnLd$fVG@W6Pd22Xh`Z;hGqX-_9PjC(jy z161%b`&XiL9jeCK3z97#(8*%EA5Qak!SEiKp({7%cW5u9pz~Kg1VLr^#NAQt%p&}5 zX5;oCEy~)pa4yYi%GL8+Ub9~Iy!^1hM+*1OP!|N^1;vL}#VvY!dIow12Ks7BrJJp} zr;fAZ5S9`l&OOSJk&)qvCsTbY5~iPG@MD$E@3pLsPK&?XmN3t!dIC5&IF{bhDVu+b zM6DRU9}@|`i>aye@$_|l-_v&1+1y&@L=qE+?C!;}XL`ob((ZpGZsNFo>j@(V+F-86 znxN~JeX{x7Ud-3A{@n-k=lI^j)PPzRpEdD^HG}h2X z<>5~P_ckbas+&A(rBaWh^L`t=^J5# z5zbCb+(^{pTyA|h5=FK){p)3TMR!=y14Orz*{f@IU^G0sz{>+gdfy0b$bOyeE4cPzivVS>Ai%uZ4Zn0Lc6a(kQOUp+d z_!1#p>cW{dM02{>F#hjnde+Ub-qq;_@hr{m30pxdp^$oReax98^@%cu+G-#m*zwmWTC=3EH4U9^?xxjB#pS+JSMq?8@}QwEP{XlC3Wo*KXh8O$(#a#H6g z`TaMvfVyQ0qgK8^gs8P==Bdyt#^p?7AL0%0RuaKLio{XXmwfll)GKwqQ4n+LZy8~49U z-yZ#2cRy}~;j4Mp5iuUBB078w_(9(?@9_z9Ys--TEFa5pkYeTo=zznyk3^Vryjksk z=&yE4_DZC^Sl+95l{Lsoc|7gs*y?{QQ7=7@2O$QdkldY8!N0Vwx#P3+A`D~k^uWqO z_K{c}0yPy&FM5Qk2uPcrEdxFxTO*+ExL2wnnsZAO0sNlI>Htg-? zgL#j0|4VOJb5?K8Qk547)C;k~yo2-gF+11_ZFyysy77eq311{I5^n-$HJcm(!U zdJb2>v~J&KkS}ev?Ce}qs7o;2JHrsfj~c5KLSC_OjtC zlLYhq3K7JwSqGXzARq>REcY!2UYboahV;wkHFFMrzIAQU{H6ij{tSt!l4la)LVW7_ z$sjEDZpK{E3e%Kz@8c3zz{Px7bN}Q|;S6*9-b1$kP8K`MPu{0RbfT){AbiM#+_&kk zSnxNh{=nYkpGKN)Kk z`^8McS*^Yo+tzI4`cq1B!1GJJeK2|HA~G`ZT^y(JiELJj?=AZ-WnJnpp&bckzl0D| zV61-@Z#$gTp4a*=FH81DI26RHCY$Gew>5Daxh8x-ba>4I8$U|T=20A``~I5*AC26P zL%!t98x-jQ;p`T%2*YGsLVkSUocds*sG%YaxFbmlkAw<$ zuD$8>ZH`1zL-+_OZA*hJfo$a$XEfoQt7)#WZHI)r z_$P{6yL(H>eY%bNtx~T(PVs{Z;cX_}>A^hYS1%nffj;^h!|Ji`2N=Xh+e)kpMKR!> znZf){ulXK@?q20O^u?q?JAcRe_LOHF1HO3XfDLzk-TYjB}JMwOj9>k zH;utUe#MZ?BTD79puEU=s&16b^1Y&U&dlw8Rq@NyjR8I=TtA+)rx>62``W?ex~Y;< zooMm!Vr&L|@>I131uGsal35ymEE2QE1~3}k+7)!TCanL><-ttm<3hxQ_55UM9B!;;)a`v7G;hUkefxbh$s3vKWMm6!X7_*& zs!4Odlr;?4Gkb(;3MA$28c5Y>B|vaHe3^y59;x%r8Y9gtzs$Lin)m=3X6I=qyE~Y( zm$1-(^_cphapN#O$@?0ESCmgXk4JT%CmdpaKiQPSgg?o~QO69W82h(@$z3$liDcv+^&(p@FX0+Zy|*K=9-jxBX!t(LEINe0QjAxi8BBS^rjiQ zvp*l&bWL$YxBWV(pumXUn0=0YU?FG2@*x&)tSElV5^dX_&wW1zGEe}Yq|+!p-h6+K z|M>0S9v>U=3ny3>VjOpu8ZrnjTNiR#?=H8N%{k}Z63iSt{RD?Uq7ZWJ&=t<-GV=(YgWf+AGK(8WMV_`% zFG^jRcWmusyUA16d@q@&^l83nR)AXE-+J#KQZ^w(-e^2*&AbuLavhZTZz>s8dk7YL zDorquWUKMM#jI`SZ&XIz&qULb$!EwHB6556N4k@RR$DWHQ zz)ey)uC|_(Ue2C7-uV*y$SCU^OH*?2Cb68kIGlk(4g^g|=Uan`Yf0MN)epgaB&iSA zCY_A~f&M zCOMbck6mtGsz0+p$L*{P1>inHU$X5tDN3BFo&XuU&fK8Gv@pe|g8B+JT#|0Xe1g09-Wr#I)k>@j(rZr*6kQH{2gu=-(exjXgUM~jM26h;K&MRI-1 zIU3$FN`)B$sX2%dJj?AS;|PfeXUjxXUWl%e{1dQP{vXH|Ss%9PpX)s^ld#E5#%DEK zT)K5TQ((sHqMpgbi`bzu?dE}t;}3m(R^5?pa;{TfPaw&b!4>Q>O@eJ;gPr7ll`6Zw ziW`|1pB6H0uDK^1E&dv-bcH*0)$_RQ^lVUa6l9TPEhTOBk?CLb!uGD$|9RSP%s7gr z$KBmmoadsQFMOGN#cOy7_P+PXy^ZMe!vT$UpRBAQ`w|zmpP2*=wZ67wF41mN|F|XQ zS=bzifF9og6`v9Q5-&uue|*Xs4ARMHO%71TwrOor?n|oMoMC8SOUn%clj<70-T1?c zhcj=^j);@eBFur|MSB)V1E(?FsX)Eak1O_sGAN>1d1b z68xn7BkVW9`H=GcNM@R(8=a;7<&B%Ypg+?!+)fRL<75*GQpc?XdzUmPlYU1`bC9%T z^9IJx?>4=KUoRWupwkjapaPW}T&Tht!KE>RPatUd76G4-o_( zfIf*(TKIM{zxOv_|D&^pdRaKRkMn#(x=xo6pI)6#uStW)+T0Cq5iVSr?jfu`cu$Dk z=kIG?2K)EqL!sUI++m<7Mu<#v>c9PE1Lx)D4ez}^E3uFl=Eg_sJD8CMXBhqPUC`Cg z-+;3{>5L)t*#V#P-&;QJGSx)j`-ZtcEk7K zC}h?Q?$s?w?#_jq5A)zwIRc{@^GMCL&j$=#=D&NaU3gBsBVOd&oo(^j8jYrxv`Kn` z9B~NW;QYMeP`9qKyL8l)IV9%1m?;@rRWjseyF$g`wYM9&;duYHE!Ex0@C*Rw2@-TW znke(ZP0#ym*ml+3oW^ra);{sj>A)BV3-yORelk+!u4hKkRP=lLnB$tcuHt}Am3t66 z=q)e1s9_xbWL>ml(p_|A@eP&nWLKLl<7*BJyca4t<)4?5L+fmz28}b_?wM^;s?Y9q z9PTKvzu8Z_z~#K?*o!pa57ji|PxXB3;gLplG0LugqPSq30hKiaLg0Yevxpj$gdq1WXrZ`ZB}MaTHR`77)i4&jP}S;b zwMR&10bIc5o{TLSy|K2oj*5#`I7q@5O^0CKi=2othox?^nSQ$9E8Ggj^`-Kq9TsIsbzde-g&By^KNoOqTSuG95hS149TIZ1%)57%eZ#IV{rz2CzhG(i3M;-xbgur8b;Pp9 zEta5V9l5eqcwisMBAy2Lt?igRQ;X&^l}+gHkQO$91#kF@*JKUn2P$`m!D+K$QK@Px z9;q798}L*i(tvLHtb6oariU3oEvGD!< z_JXlwt1s?M>S`TqFK0r8OzN^$7q1%vH##C-8qHFfc=+v*v`l$SmMO25cd+=8x*(V_ z2AGH#<3l3Mx3KrL5u`OJe1p+#Gv!MQ#&<V%Cl`M< zQMim*pZRR1dTeVo~VblIY102Vv>PCv1BOT?U9R5n(7B8VX`9Ujk{>NLx1Pj3BD$qP22-wbQm=7M*BIE6@;!9@Y7_bU{` z7Exj~bLY2rAe}EiiDg;GCCHFpIQX4sHKu+_8(D6VJbB)U5}jR82)OZul2)*oMZ=A^ z*T`1!t1DfGSp!v>4>U4Vt+g*YA9tIY5QR(y7)KeKIBu131|tTN=Gx0$8L5_Q&eD=d z!h`=C%ngUy_X6J9yGmus`gmPh3C7#Xo)K&JZ4f^?G$REILP|AvB!CTTa>wcsbN8f4^GNUhiB(T(rx_q|N(tG&lACBNEQ7si^u!Mp|IZ+=l-1++r*Kd|~T02gy+YXJM9Y9hs-Ycirt#a4Q* z4WKJbeVb*tIx3^}T_2rp=y)IO!D#8pI`&X;7%l{ZHDe2(pcWhSdPHj;|AL!(yaSLUMPB*hso>cFK)W-pHYcr{;@ar`4Oiy{tsAB;OBg~9?$qv z^`*2P^_)SfcH(iC;{9Imfp2Ufmx^B+Kc>Ji;j2KBa=pz{J|=j=BIPcjyh!$UsQ_l9 z9*=l~Pi>O#49cw@XC|uzEWFx{5c+mMUUgk8LxtQ9NCq$d_%ppPfRRQ3f(S}yKzRBM zxWdN&V~Ov1Af?ZqS0$p|6(6$k1Yb6ocQECAOw{q^!-`P-%^~$u5`g!cSYrChE3TbQ zH5l_Pq|$#?Cw^?Me%z_eZe?(k09(q|jaFT)ilGK(HY?Zqa2V>mkF){q{VbPI`To5j zP9#>D;X*{b!S(JqHv`$`mQ?Sw#T~;9im&3b&2si#elGH$I5L8q_d-wEXBW@8WQLad zX88f)>2yjxBU1W9uy#g{_P3k&5giE^m8ahCm@HoGE;S~E?>_ogS3t(6++h{y=Ya)! z%}pl{;$5;5$2sZ_y@pN|8+!`)0eX=BHk-`S;ANCHibs z#Qo0QDc9k^&CwZE#-4S=8){I<3KvVDKOg#Dr#GUCy$_xgIgqTb85hz1l89vZi^*aC zu}s9<-ybbWsDi#axXZYCd5e2yanl*2zsX1qkjeH9Ceqn|IFiK8MOaNyw?Szcz@L@b z1%6zMa$~7$C^F)?lBll)0fXATUsufSTT5pr7!gprC&9vF;>F-wlJ>%oj2_8ykF9_@zBK`%+%+LMp|q{zdaYZr{SR-U=k+(@iW% z$V!tu9sTnC_e1J`d0yd-113*_m1_Y`hEep#*Ae7B)2_N_YoDO8Px)EReLd6ikGh!Y z=*>iV8VGR5Vf$(AuxnUxH*o%g{4*&NE)a#+0fCFpM)1wYGRh~8+!BUpLInSe!0e2} z@NfM{MKh(H6@He$F|2~G9cdovdxvZ1K94+mIJdr$()c))y_{E5I1^mO?puxkvlF>I zrQ=w*F9oLST_WLkO_fdS%7Cf6;f>A$&zCgTIRgbn!?dv_3Z`)Scp0(E+R<(JsD$G=@vKpkn;8;!$R5q47uid8@V zG~N(vZ_uZ4WYib)Fa4I!*C7qAw*GvdH1ofxUL2R)>p36*Z`Jmc{D_KLb4DE%9dciPbnls(fQJlB_)D&+?`!3+LWed^8ql5=**g(pdXmNv#BoYq^R-9vxQ zcM_Q#*Zw8va~lzWUEc-ldy2Gg!hV`haVZsoQt{g_`HIn~ZNmh;QWmjuGDNHn!ao?G z%jW%ce_%t+3h@!8#Y){ukBimIoHG0t;vku+9{j?`&Qv^jQPfpi*@ALQfEw7WELrdx z9_{es5H4Qk(DF>3H<9Xb)H^YJ!#h?QH}TE>H}?rzDv?ZlTV%wxgv{eH~>3UL8f_N@R?a$nG7B**YT)xC6&mZFj%->Bd%g$geB?%^Dj;AEupI+XOC_L z{1pCyh@p&L;>z?6l}YjxV^!uNqs8Ea%_CSyhDpj$B4Nr;U^MLK)2TN2XU#pLWk{Yy zO?nxf>8Mo9a;cyq2RS29%}=?$a>M%3pI_jo`GtPx5G+Bvx@_9_6B#MJ5!f&0MN9j6=r)ma{=)hRR4A{Ge`s zZoVsl6w&_76uHx3SbuHNHjizG=55$C7x5pOlKrTieVAZ;?2obR}8-E1@EP5 zGxn0qEHYk?V_{szDs2)9EFjPvTh%5RyA1^_xwW_>3Ou4O{&YTJ_5hgW5Wo^OF0u1b zRXF0oQ6g*%OEPu|tNOA>2TnL@{F|YqgjrCAY0A;r5tE%>^;RP0wuxZ3o$$uu$JTPh zo>yRDLuJ`EfMw~7pbXDdfdN?FEjhOF!v3AL`L@Of2z~CSJ@J~>)e|8o!f@Mlz2~-$Y z*XV&Vg=GLsqL4XDDJj7{Fv}u?CB;Ks46rC22FxL_Fg`r@w#4Kie zZjj;#i)TTw*y>6yUc=aShXy~Sm?dns;w!W`23U+CdqzQvv-qL860r>s&3^@8Ih$Sy z!r=gx_e)OXY%omC9>8nL%HZvytHQ=Su7Vtm!^2ygc+}Yg&%%3)IFLdGdE{AW*#~$Q z0E@3-5=6uJ(*?2RW=|e2rqH00itu$xQU-`604%2n7P;7>WkJ(|=0!~sBCwn(O;l}? zh?aaIb#)SW7NdkRU&x6JQ(zee?tX|E8S`y47O;#B6&6IrB+mjeY`t*C;O98I1F+45 z3vKc<9SMkk+lV*d-3G%5}rPcl%%pN5=Kg;Gyau{Fw5B+ zZ}`dsvrJSpEFXl#3W``1Sj4jktI)5p`s+W^{53MVTy&d0F$;qwAIx{jDx5G_h*{$K zY&tvVSyF0Kc`BVH8Ah4cL_jSGm2XjCaZ|pUVWHnj{Z|bZS4SWD8s)^q@+TJVoSct% zn7j|xe>&zLAj6I_tfu(_Ny19ia8Owb%P@{AN9JW1Cm4`f5}~NBCex><)=&hNkZlLx zm`_PkiGD9kXYhRRI6zBm;bm_nJXe7^H_;ox^5^by#LV6sVvJ(|#4K}&w}X~8`q7um zZlK{nKTj7u<`rTT>50H<7xec47A3=2dnpMP zbS?q25Ln`>M^bntA*mK{`2NKcU>43pCb5Px!L?z*jJp3XSZH$*7XK=+1j+Sacz3`o zlf5B36qI8s!!jPTX~Bt`4k=7m3kdz~E05Sb`)zisqPf;Q6@2-uBS4IJzLi zbfXS>mh76Y8sp2TPD$cfz|0bhs!rqZcbEaN9Qt8n8nQMWh78+tjABeP1)>&+lMD+R z$^Wp~9FIr50I(QUni^;-kK!!8Z7fIT@;qTvkD(0PKpFNg!7wE}87$GjxCJ4NFc&RE zL!DRTS!^k^Q;tQCTz8@)0*lnQ5Q}pJ*>QP2F)+&`3;`%}?;3Dn85YkSqQUn(`SOKX zbany3a(5{!U?Ix_mtE9k7&uO`3_JZ!hN<%WEP+MKFjcuFNfzE^w0{1=<-*S=z%0X& z4i+o8Fkry5D1)y@i4Bd`7m{IF>&wuk4%&(aHnC85XX<0McD^HZ@NDlq{I8JMW##dv6i!wbGK zbbOSKsIamGLD{#;e~AqHt^y>uG9y^P;0NJWgFjS#*`vK^*x0dUR#B>Q7h)Ej{GY4U zCnykDY_rlOAR#7>#rK+70$K_9jhLmp6giWP6!>#M1HkfONev;;+$mR%2mvD0eZ3e9 zrWpvYsJ3AnJ^&lQqBa&p9F+>DR0}CCkYVFTi+)^B8PP~_Wua)oMX)4geu?LEHd@t& zv1cI}mZY->lab`{NxLLjh%s35-}@Erz&B)A-q2tGZ8upLb+PDJktu?Z}zu2k;Zx_W~%ZFt6b;#mSe zWAq$LUGjq?Viwyx3Bz+sToEi$Q=RE$t6|M7tIRA00!*|Jvqa2A#d8hK8dg(Ifi z)6jMn#k1J|Uusz{k_?**htZE7yWbt-ulew(?Wg#ySiiXRhLqMno| znQ>#Or38_6?-JE6Y*qkXIC04(b{?Iv{1vEffrt~zk+&aZ#7B3z|AW443=mG}F0fT>xGA!;$J8@L!S~vw@qrp$Fsx*tOh9D^w z2IPWZ$%JryuTv%h&RfZJ!Zh0d}x@tn_w?_C%^`mx#*JbB!9vu-+?W@EzGGsj4VBD;Zrw zu*9fx5`Np*?`J>^`u`Y;-8#;^!^@3+Pcko~_?Q`~?mM02cM15fO*fB9nf?0)1 zD7p7b^-U!&1G9YBC6eK8mK=wUPn5wo>b1zUv-;`krDa!DZ zYXKSd%egaE$2=bcOK`}u{5HLwEac84gX9NEvS*oG^RqfB=^J2Sf)TK&85X9+H^CCf zFnZe+On7hzgC#C&MC{;dsAugFSR$_4<}Q*01qX*t#o&jH(6N}_Ah#VmYw&g9d_)I^ zAB{23qBAhzxg339@Pjdx5i@{1C&LU%2@DKJ!xUKN`jgH6PGXkGzhzlOdO9G(mg$%X831B3sMu0bB2N#=tLJ0@l+1bxMU4r9|oU_sB4N&ibYOk~(8F-wvw73dG;?Fldd zmJ@C8Wnu9=Sis<)fWhbgEhR2aJeV2c60;N{6ey!sJwu79WuO3-RK$&{7^kjuvgMMB z27jNyGFKL6KqHP|2}S2K<7Pe)9z$f9t}zM3ky>VC7E&6cBsF%l!4K*+!`i@zXL-{I z@+@WpfS!mic^3O$b4FO*VlQvLS&E4QHG<`I0ikVp(NGTzessR?G`L|Yca&t9V{0oD z!(1gsS-9mMsD2#eS>o=Qz%ho7z4QSad_N(9uplKVU^#zuZ<$6^@O3tskZ~1iwPYr7 z`S^R!!YWM4T>@rNtAv#lQ!;G*r(Cj`XW?pJ^O1M69A(^BJRH7ODNQ?e|zxK@j;TuYVhOM+nGmaefzi$(@o zmXk%1=u7xfD_>x!R29I2nqqbXM91V~>o9c{YR*D6lEm@Ruz)M3PM(3qSr9}sW!s-8 zR--J7;Vfq}IAC zqtpv50SpDi$S;F60$@pt70>7o%T{`utMrEm7GrM8GZ|k;5&WKSz%r3yryM_WmU4#k z*xImi#tpL!ivCTWpF=lnI%3P@sQtF#OyOO^Pt+R(mc^F>IL~Otn5b@%-op zfd%A~af08qM7uEwfaTiRXjom`Fa*oU0!vGaT@WiRal=mfmSyRQE5>&SEIoG2tXw{^ z7X=n&Sz3$prZYI2mxe|RfMtwA%8cwdO%m;rDb8|ok-&n1j9OfavzSQ>UyqTqNFy;b zL66$*MFO>LtIh`_gPi5YC@F{HpP9Mtygq!4hMk+C zK_q26grrju8Z2ka!{!WFyctGW#?!Ja3q`{wOZct+W!kdLSYz#srUnVVo6!TCrhd`OprWE<~|g z2$oX2Y5QM{7643#!7}b0YKkJkW6kqFd{6|xC%zeHV-62ZE`Nkz>9*Olm=-T$UfS;E zV*pFDZW+PEE(B{7z8a%84B}xm+ZB^L%Ca1v4lrt_)S#y?N zxpX#ho&`&B!uHr*iWF?M+$Bql41ZT(`Q4Fbnlop0UjV0P>yFlhnqU_*c`Q6$Rc;uh z4|N#KijAOwaNj*-&LWaxlfm~8EZ)&df%;8WsGW;n5mmT^ypC=c><>;P$CQ(dt2G6d zn}wumX*zWBKGtBl?$l8aMqi06FHs%WmWJ2F4Xcy#7}WZ@u`z&Uc(v4NO1DTB9P6!1 zU^zyxXgQd;gMCce=zv-PqHj+J^Cf%^EH%Z86jtBG_tfsdt?pgt7Eh* zRM3Lw22w5v-%ko(j}*bjwlDmiiR2;V5v{o^$4h=~O1e_Q_m*X$Bn%6dDkNKi$rHI7 z!vxDTIHymt^0MwQ<}1rGfZ(!88<2a(d5JQ-23eM}XPvWc^%!s#OiG?Da-EKWyXj**t zz~U@AMTJ$871ZFfWkC_Rp5Dz9Kfwivg2)0(U{d!~JPdltus*oyB(Y@dIp{;&9-1(u zY1|zsuwZuzJ*GHGt>kwzSsGj=uwZ5dr`>a3V{Q(XUg%$$?TFGvf!X^l<0cxeb=X;LUzYM(9GG!>k%_{;o{H8P|S z{ew(w3Sfy+xEotFl*WIJ2rSTFs2YXg6v!yfa&6T1BU7SZ;m8>1V$eTOmSw0oi-ap)jLYgXG2I;Ihk@_YVN9e&!Og!W3nskxsmKix7M#I#3ouxETgIVUO&pZQn$`R{hzvJPHZ^n8x!xsPD+=1=6I|YK0t}SMC;0&F{K0OLAiop;bkwJd`^!@g+O@$+%VQE_!&hh*DmFg4=Q|bJq4C(Mr{$9vl;08{OaMYLd@w#YIY^3 zK>Ub5t3_%523QtwG2r7z?w&|{c~~#;=Y0T6Vs$$;*%UxWD_QCRSeg@COKMlzo%F)t z6_09H+S;ykx`33<{5*o?iMW&3|2mT&|t~5v(S2d1dFmP^Bvd(E{sVioN2bp z|MNzHrKK&7$;A+K(nj&02N&a3J?o?;x|&jL?xn*EkvtKnQfVD0T>nx{3YYs!K}{Ns zNi$0>)`rg~&VN1ZcvZ^(hml=5X5gg25>Yal!v0h|1_D`(3j_*I=gI-j-GOK`RAmsL zq+~2m)=9ZxzU7g4viLAY1T8K4K}lv`2`7@7eXzQN-11%#Y>#C7%APKMCd373S^P4~ zd=06V?)1t!J$dPt%`YyqG)rn2mHSzE_MGzbh-+;0tBEE+R#JFLu8e z|2aJBS!89xurR^zWptKSY0JUjkCQLki}Sd3JJ`ZZoV^yhWgX{h7?5YR4maEe;gYtT zE2kIYR>QEGdYT1JGk^8ud^OGX=c8gvaMvJSmb-qk7^PJ!kXAJ#)1h=f&fPffRA|_3 zBp+I2Yi42y40}=5lU_@px zg7i_0qv&X*ARHvFe#S(#iaZ_8ifOezxKl8nyR`@eIB|D$+i9f2+GQ|gLz|LzvUGgY zHF_-ODP%&(j-=;LZWKGHY-xAdoB&1^s8{3yfd!*sJsktf+7=@C8c2wdp5ZPBhr>Vr zP?iOur!d%2h6_v=!4soyUFYhj7t=NsoVAREOlk%cSfD{(t!X)EsBUW*%rtU0{s6Gp zIZf(uLsc>A9nIZ3T?|snNo5i>(yb5L-?anX6qY)LP(+PdO)Ie6IR>yy`SckW)!g!^ z1^O~w|61V|y%p5dfbCg6uFX{-=|+A2fc{oIbmC!-H7xX%epK?yymHA0(pCe`|CRjR zgECpCa@mBr%bHv(H|*g-X-+Dt_=S*ipvQ<{`EkePG?I_qYyxtx${3Xbicxs}y8|tU z!wT3<#nn7tckQBcgS+{>Q1NwhPsy;GOnXq=``NLYheWnaB?@r9+Tr);hDDJ%bH%!D z3uYlky+?(bu=N^d7L%H3#8^w?bEA6&K}RyJBqiw_uQ*NW0LID@nsvvs?3=B0@pRc~ zYca7-pJ-mvNzec1w%dh9s(_O&o0A2yE6|8|sjJ+u@!(LZBYS5^3rEAu(p0d7);4%@C=mqaCel%X0nne6^m@ zVM^Bv?x~c!ak3a-vnUuh0C-98?HANUN$%EhH~37ZiOC6$N+j@Oj&rvMb>G&~OcXFz zs<}IZ=B!bZTW#OD-y$%QM1h58#Hd>!K@}Qo1?3&!WF3Vh`+%!}yU3zyo%|&Qr@eBC zq=|Hm0t0SI1K2*T668 zATMfn(3W)EtET`KWH2qhvigWC_Eoqx2C!IZf21(iQ``YARE78pbfi3}`_&*c!hi|TRCBiv$PF`LY2E1xPr6a= z)_&s28Q7ep(UXCoH0Y9dq8W)x2P7->VI_knhk5V(@)me-YMuIX)UUNd#f4m`rl7F0 zQ3hCq(#_5N|p!nNE*LttGuP(s_Wfnp3@~(ra#h9@Y>liSDCEi(as0sxlTnT4MtY%rY#=)|a zH7Qx-G-~bM9gs;vJ$>B{2utCkvMj^f#Ww3AHA=KuQJ3b0(XC>aLSOR&6EZ7^mc^yc z7e}L$fewfD&IH4HA1KcvI%Ls~abx6zqtRf@ZWz687biA!I_!ukq`FuCxOh%ee)Pp! zW4pH2I(0RJLlz4$CIsG8CSOA%r-12_4q)+9Q5YPA_p7=qH3r^O`TQDg=yOA=4Xgp}; zdr%V{jNDhS*KJ0>MY1+%VU+ zqGvY-i}VMjDMJy>1EIXJ_xnSKu@akL7yHKW0Y&lNT_>qRWK-b5`bW{%*Xl=io%%Gs zk~T2M*i71e=(EoP;- z4)4iXu-K>pI+(i&mWA^&XkW07tR_OU2rWx!y2e=SpS$xbu=E-M@%}KXTCrtG4S;27 zltVl4oT;D*pfPx_<^`qz&9v~Pc|8c0aL%{XI2AEa6>8k5z_J@aL=9j7)EiS6)OH?< z482Mv|IO>!ImCq@iZT(ND_3ZordKIE+APjdXD)5#3}k^nxj!nfR4RH;XeCJKX!qGq z&V*trdQBw*?iLL{iQe#gYH&p<@Z^t2!zy#TZfi?ydvvR%-?xX(q22*FUUG zFWhQSi^*mU!P2N_hH^_6eDqm7kDq~X|tff64%eR6BJQ{1`7%3YLcb_^=XPd48XDsm1VJ`WU)O11{(D#%MuXb z52>*(V}%3E5Dn|M%F*;tDXy>x!4lfejCF&80RT&x2U%yx0-o}L=qoo&(Q#`0TcHj$ z6d)km1aP_sV8K)%#dUoMYH7})jsUQfbsjy)v`59itm6E*R`P?u4uoqPL;sWli#yGr z7#Fp)@x3cCcrBin^dj{Sef_`@7mN%0giBJAtXfr;1)Jgtqpwk*Q(KZ+sw~Tt;(72o zP!m#T%VNcfZ4C)U!$vT{MRM~NUVvP~)=m4S?Wh>t%Rq1%9yLhBbUsTtuOrsupW1R+C@uH@#_M8d+cD zhMBa49cbb-rvgjt8Ji>LbDXY>aosSdz~V8(pgK>VNBsc#0Ea98&8t$G`k(rN?on%8 ztUy_o7sWa44rbd3({EN_DVD|K3hZ0i+~0n7t;B6K%?yn~cEd8Cc>{}4Fgvf%B5qh?q65AZ(YM+R7Lx)boN)RP zGK2&c_QLe3AF%>ZQCk-~+)AXq1@l z+nn)~l#`|0u$c;JcmtC(#h0OB8;anUaOgug;fD!KBj?A}QpL9>(@ivsPz3+ypWU-c z2g&Keaz!ti000wjNklUAI2L?y>Tg&ZcQIZTD8lvRK_$O;L*SV$LC0nif|l?6`DoWEaoCrQxmd?_mMTq7pg1pdecjv_oOS^=Xrm-w7F)acX z8FifX#;^=VS(fqn8v9`NDSC$*g2m+crFRTs1qDTMs=(3?G5f-D%qqr|g}Q(nc1IC> z@U*j18P3C00Se251`1+6W?d9mJ|td-4_h(-42*#r=IL0PUd4ip3bpLV3u`H~v$7|6SBDqDd=s_n< z(*YkMNzl$HT^IsbOu)FM@iLE-bLZRVkMGRFnw{6l$i;;WSmysRfM{6nquYlbaTZcyJ1}ud}xY!k%48}vT)^1Q#!+NCWBS7Ds}YkigRP!Y~sr3nNQW3-L0x zEQZTyy%eVWm1T+iFvz$@X5JOJT)DgZ*eb{dfxHM&oBTIBIN(%U1WTGlnnrNL%9hzO zU4-TLIX|ow{fyr=*@ejc~U-`P5FiOfg->qia~7Vg@O zl0NFa0@o2*mN+q5z;r?bmeh~Faq4z;BgVi4EFj2jDX<8;H#liYlRUI6%f*b4SZ+Xa z0F983hK08QxnVjIX0>)D%QC{y@dCp!v!NU2P7r*P58`Bg zyKo)BVh`FgmC5x~EK5Vn8>ZxPfwRP!(cyL`CV_!Sw-GD}C--%YnUEH6!+MQSg3FO< zBchvN3THtz=9vf)gQZ_%RufFEt%(#^LNBzOA4_6}k1F}Qdz2qB&zqnwIcneTl;;dN zx6Bh>lyg>i__9>axL!I_$}i|VT`&6KG%^wxu(;TPg-;IL#U7{NQk$L9)>9XA^~CPu zOlcP0&h9*Mv`!p>l321~m|azU2X2_HGoJ_%*V4mp$PL4A53v=ypEiM|NqtxaCH5J) z-EJ%nmtjnvNkc*)3^Rhtw16{YfkiK77P{D$Z3`~LH(}@?>XH>$IuU2WYF1FD zMiEWqEWNZ6OKP7p1svqTMN}1n&!Ubnh%(`!fGtZjCF3oAmU@kYuw;uzf-vJLF<9&n zmStmT;BWrk4m9NoEF@W(c&K=Hlx6Yl$v^@=p-GCftQT4G7)*(G0l`;b(Owv9g{xGP z71G#X0!tO=6-s8FOiGO&MBb~%uGIf>LkWNj(_|JFS*pdMqpMh+&y;0aySxdKIfRy{Mt0!NNG0 zjv3a5S;-cy=+A&ZA6O;#nVb3oz731{#p$}CxyOkk-s zQVxM+8uF9B1(xOWSl;JaCdm)pF%kTBNR2&NZb`A38YKYG4O5n-YR-y+l$g(>;n~n} z6q-6Fal>lf)C68Y9hjrs3RxCQCKFm24&M(ISOPDbN7OD99hATKX106=N1+~b=7Sqn z^8CP3u1)?q&W{_#>BLL^LBm;y(g)~-p<ZPrJX|R~IqmG4v8}>$lh5AM&F^j*X zqA`JLi)#4p5xHSggiMo`rA}bs0xk}m8q1>3wj2a&X>>FehTw*^bbJX;<84L@greB8 zK;&4~F{C2Tg5_^4i|vMKVKQ5m{&=XsqFze9u(l{&ozU-gTOIfM>BUwjTcs&1i<*=d zZiw7P(-x~)A6k~11Qsd^O|xjhE^4`3$Mdm83bl2?Xk-oKF3Jt7a;u!07AMzq)&;?m zhPHw!Qy9ZwX?S8*s-hT-bfcL7g_Pd(wMVn72`nZQ7V54|x6^jEUc6p_FGY>WobZNWOZ0UH|$^JhG|g*e+em! zyx$+W?12$csikaI$Ujc1@wJ%*?$ zu~%Ag=N5)5Ho=GFN|#h%9STV)uq=ZcrZX&4I7|Qdboduw(V^qsgd~PLSo8oa5Dhb? z#kxkz;wZ2*AfKPX3rlle%+p(X*eLH9IF~8Nw)j}2EQO@ z33O$@?!|@*WzAXAP&?f=ShyECxM2z`qJU!EAo2YmB1Qt1CHiP|Il$0y=w&s$x`6{_ zV!#daSu0~P8jUZINxt^x0xNN1frVa9xnU(8S3XNf04&O~`1W&Ac9J9 zeY#cjgYe@S)`&TaXe1+UK}9J@d|294V6nQ|Yjvu&&)&{=6gr0Y9Y2g3vH67h#3i4F z`;Q&mJ+)i~!E$)d;mJd?2I>thZ2?EUslbBM?wVEtf?z|j*9L$^au+%x*2Q8-TCVLS zuq^G4Q5r8!u`DN}qoeNh(?V8mf+O_Z3)(X;v+Y&zY7es)1z*v{WO` zCR;dk+&|}8g5)UEM>a5E34*EdFdV9+qhZ-&0@kfmg52GAvva|(9e;21r|q+}fIE6eg(EcyE+>x|)tYY09umpW;}s6|Vi!P11zMxr?6 z9E(W9TIV{06{u&iR3t5FM$sX)i2;i!!`KfK>e9W~AODRTW<|qvmA)R~qTI02=nJZ6 zAxhsx4P}Q_`pR&0Al7wG2#Qp-wDiPQsFYM-N!xa7KGwN#lsIYTZjKjgY5k*i8d8p< zZp@{|SQmjK1rz*KuthQm@LOSAH4VpOs|YM4M)DyExf-m>b+H-40brRZJPyKarh>py z2~AOvRA2{D_6+Pq0oh@4dH_XZQ>7KNf?#559mIGC#c~PWg zFUt1({=K$6ncj(7;u4G{v9Xkh6RNP4+qyzq`$9 z777fXqXDh0Olu9H?+BKp*=X37qn>7*ECrUho>*aF8tcS>04M9#s9jau# ziCG9NQcGOb^}}KuFzo9`;q>W-m`qn-foc`2@{8;5v8@THeE}?GR0{ht4US8H11bv+ zf#A>5;NDCZZQTK;0$`c*b!!RT>q1(Vmz5j#rZnes(-@zM{H<0p(7WZ)D*#KmtY>O) z9WzvDma$UuUoVPfY+FQiFcrFMD#0qwvsDLl$sv~9SVl|$%2^?OI-;qV`wMF>`t zIq9&7Q_80Kcjv4&y1w_0u1g~|2&=uUCIt#dC- zXu2J&GDgT;I2xZ0`h$T2!GJe?2lpO*`pNMJnBk4z2Xec=uP*xszbiNFGQukb!cDeBFx_G5F$IadT<9wkg zkMM$Td)&;KhiA8Z?%Q!Er*8EQxa9M) zL*2{W9XE6G0591sm(SfF)^dd$+=U;vwgT+RsqM)iP{$RlAEKXBPxAer`d&_+;Ooqz zTyFKKD6f#4_!|ne^W}k?lRuOG5c#X|KT=n_J5H{UH~;RL_rux$-;Y0wN4STZ69>=- z;P5BmfbxqS1{PyhwB3b!@=Qy zfAY!cuzz^KLs@W`q+mx~j)zC5t6zWp6>XQz?d8kr`Z8Q!IsD__U&GzY_;AX0<>-$e zr#@kh_;C98FP^@1>C%3wU5?8oQm`s(2H8{Bn?zVVu#r|rwDgOim@){ZmVmsf`; zcQ0M~n9NG`{`UN8C6*zV+8f+&n(M`PWC> z8jt?^h)cJ}qZ?o1dwtn@=Py~G!JU)Q^-Gt`Cy5_Q=F?q-9+Jh`-?v_5e}A7IA8xJg9uNTaYs0he@v{Hs{rkPmXFL15?`eDc z=KY&DZ`Ea2UGKhEPpdoc*M8Z44|{gsyxHB^-F>s8zSMnnwY{^myYu?Z&a)r3c3y99 z@4ni8z5VjV_M4a6+uN_yx7xR}z4h|t>sSB&Vg1GC_V(uXi_I7C?AGSy*7l2yjm-^p zXLECNo3-~wqB~oTkC3vdSY{ZV{2mr9;nOO=8un_ZNcsh+_$~~chp_HQrp_b z#^Z;-ZqcKS)y<9dRdrccKe)EGvG#mpQ$1LJeE<38>W134uAW<6-B7#e@aLX5Y!b>Ig4Qni0${nv+&)K6^SkExg0P)~2JuRn)9>Oghm=K7<@>Jj|u z>i<#y0{)xu>^kgHf7#Z?gGXyyoACd`6}(mSror2}xuLFKtUh|Oz6o#fmU{WEEqt$E zyx7`;=eAzFRNtFVp1ynupU_M7vAo=V37?)ia_hwwd`R1`UT$yx`1AJe&W?JhKKk9= zo$X!qsjK_&P<`~UJmeixY_ix|6 zeGlvE{oB1g^;qpt>+tQ~?r$6W@AvlKs|RWqu1&g9>va#V4|kvaCLox){nsaI`}Dt$ zk4GbQ;cckzshw)~a5xysHc)p*=An8#puGb+irhQeuf4wEXqa^Y`?e>j9`)%5Gp=G^ zKaV%^()Q2yGe4n@upCZ&S)ySB;bX%=U+?7Wfgx;gI~Z#Ec6P(3hC}-mrXDuz|IKC{ zG3Zab+qeFV!I?jS?hQWvH=cQokNmZG#NZ>pV=$QfUj~`mLFPTxZ#aD@bQf=69rQCF zSKnp=1HDovKKJYq!!x@kUSv4TtOdHtY`h4E+4YGJhP0QLzP0{sApAdNSpGlr_T)Iy Sx-*^t0000 Date: Thu, 29 Aug 2024 18:54:11 -0600 Subject: [PATCH 087/111] use new launckkey image file for surface GUI dialog --- libs/surfaces/launchkey_4/gui.cc | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/libs/surfaces/launchkey_4/gui.cc b/libs/surfaces/launchkey_4/gui.cc index ac71bb0dcc..bd87dd6800 100644 --- a/libs/surfaces/launchkey_4/gui.cc +++ b/libs/surfaces/launchkey_4/gui.cc @@ -97,11 +97,8 @@ LK4_GUI::LK4_GUI (LaunchKey4& p) _table.set_homogeneous (false); std::string data_file_path; -#ifdef LAUNCHPAD_MINI - std::string name = "launchpad-mini.png"; -#else - std::string name = "launchpad-x.png"; -#endif + 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); From 520bbfe515265d13314c845fb8ff3bca217e9105 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Fri, 30 Aug 2024 21:21:42 +0200 Subject: [PATCH 088/111] Add proper API to expose plugin tailtime This is in preparation to display current tail time, and allow a user to override it. This is similar to existing processor latency API. --- libs/ardour/ardour/plugin.h | 13 ++-- libs/ardour/ardour/region_fx_plugin.h | 11 ++-- libs/ardour/ardour/tailtime.h | 69 +++++++++++++++++++++ libs/ardour/ardour/vst3_plugin.h | 4 +- libs/ardour/audioregion.cc | 4 +- libs/ardour/plugin.cc | 9 +-- libs/ardour/region.cc | 2 +- libs/ardour/region_fx_plugin.cc | 15 ++++- libs/ardour/tailtime.cc | 87 +++++++++++++++++++++++++++ libs/ardour/vst3_plugin.cc | 6 +- libs/ardour/wscript | 1 + 11 files changed, 191 insertions(+), 30 deletions(-) create mode 100644 libs/ardour/ardour/tailtime.h create mode 100644 libs/ardour/tailtime.cc diff --git a/libs/ardour/ardour/plugin.h b/libs/ardour/ardour/plugin.h index b8510bbc35..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&); @@ -172,12 +173,14 @@ 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; } - samplecnt_t effective_tail() const; - PBD::Signal0 TailChanged; - virtual int set_block_size (pframes_t nframes) = 0; virtual bool requires_fixed_sized_buffers () const { return false; } virtual bool inplace_broken () const { return false; } @@ -429,7 +432,7 @@ private: /** tail duration in samples. e.g. for reverb or delay plugins. * * The default when unknown is 2 sec */ - virtual samplecnt_t plugin_tail () const; + virtual samplecnt_t plugin_tailtime () const; /** Fill _presets with our presets */ diff --git a/libs/ardour/ardour/region_fx_plugin.h b/libs/ardour/ardour/region_fx_plugin.h index 72b35bd6df..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,6 +61,8 @@ public: /* Latent */ samplecnt_t signal_latency () const; + /* TailTime */ + samplecnt_t signal_tailtime () const; /* PlugInsertBase */ uint32_t get_count () const @@ -153,10 +155,6 @@ public: return _required_buffers; } - /* wrapped Plugin API */ - PBD::Signal0 TailChanged; - samplecnt_t effective_tail () const; - private: /* disallow copy construction */ RegionFxPlugin (RegionFxPlugin const&); @@ -178,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; 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/vst3_plugin.h b/libs/ardour/ardour/vst3_plugin.h index feadc6b52b..3ac40037b6 100644 --- a/libs/ardour/ardour/vst3_plugin.h +++ b/libs/ardour/ardour/vst3_plugin.h @@ -182,7 +182,7 @@ public: /* API for Ardour -- Setup/Processing */ uint32_t plugin_latency (); - uint32_t plugin_tail (); + uint32_t plugin_tailtime (); bool set_block_size (int32_t); bool activate (); bool deactivate (); @@ -444,7 +444,7 @@ public: private: samplecnt_t plugin_latency () const; - samplecnt_t plugin_tail () const; + samplecnt_t plugin_tailtime () const; void init (); void find_presets (); void forward_resize_view (int w, int h); diff --git a/libs/ardour/audioregion.cc b/libs/ardour/audioregion.cc index 89d79c6579..51260a0959 100644 --- a/libs/ardour/audioregion.cc +++ b/libs/ardour/audioregion.cc @@ -2488,7 +2488,7 @@ AudioRegion::_add_plugin (std::shared_ptr rfx, std::shared_ptrLatencyChanged.connect_same_thread (*this, boost::bind (&AudioRegion::fx_latency_changed, this, false)); - rfx->TailChanged.connect_same_thread (*this, boost::bind (&AudioRegion::fx_tail_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) { @@ -2576,7 +2576,7 @@ AudioRegion::fx_tail_changed (bool no_emit) { uint32_t t = 0; for (auto const& rfx : _plugins) { - t = max (t, rfx->effective_tail ()); + t = max (t, rfx->effective_tailtime ()); } if (t == _fx_tail) { return; diff --git a/libs/ardour/plugin.cc b/libs/ardour/plugin.cc index 6c4d0fe378..06e442d91a 100644 --- a/libs/ardour/plugin.cc +++ b/libs/ardour/plugin.cc @@ -318,14 +318,7 @@ Plugin::input_streams () const } samplecnt_t -Plugin::effective_tail () const -{ - /* consider adding a user-override per plugin; compare to HasLatency, Latent */ - return max (0, min (plugin_tail (), Config->get_max_tail_samples ())); -} - -samplecnt_t -Plugin::plugin_tail () const +Plugin::plugin_tailtime () const { return _session.sample_rate () * Config->get_tail_duration_sec (); } diff --git a/libs/ardour/region.cc b/libs/ardour/region.cc index a90f0bc8cc..eced85fec7 100644 --- a/libs/ardour/region.cc +++ b/libs/ardour/region.cc @@ -2475,7 +2475,7 @@ Region::fx_tail_changed (bool) { uint32_t t = 0; for (auto const& rfx : _plugins) { - t = max (t, rfx->plugin()->effective_tail ()); + t = max (t, rfx->effective_tailtime ()); } if (t == _fx_tail) { return; diff --git a/libs/ardour/region_fx_plugin.cc b/libs/ardour/region_fx_plugin.cc index 1b10eaef2c..b0547e94d5 100644 --- a/libs/ardour/region_fx_plugin.cc +++ b/libs/ardour/region_fx_plugin.cc @@ -250,6 +250,7 @@ RegionFxPlugin::get_state () const 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 ()); @@ -383,6 +384,10 @@ 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; } @@ -422,7 +427,6 @@ RegionFxPlugin::add_plugin (std::shared_ptr plugin) plugin->ParameterChangedExternally.connect_same_thread (*this, boost::bind (&RegionFxPlugin::parameter_changed_externally, this, _1, _2)); plugin->StartTouch.connect_same_thread (*this, boost::bind (&RegionFxPlugin::start_touch, this, _1)); plugin->EndTouch.connect_same_thread (*this, boost::bind (&RegionFxPlugin::end_touch, this, _1)); - plugin->TailChanged.connect_same_thread (*this, [this](){ TailChanged (); }); } plugin->set_insert (this, _plugins.size ()); @@ -503,12 +507,12 @@ RegionFxPlugin::signal_latency () const } ARDOUR::samplecnt_t -RegionFxPlugin::effective_tail () const +RegionFxPlugin::signal_tailtime () const { if (_plugins.empty ()) { return 0; } - return _plugins.front ()->effective_tail (); + return _plugins.front ()->signal_tailtime (); } PlugInsertBase::UIElements @@ -1440,6 +1444,11 @@ RegionFxPlugin::connect_and_run (BufferSet& bufs, samplepos_t start, samplepos_t _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; } 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_plugin.cc b/libs/ardour/vst3_plugin.cc index 6ff5509362..27c5f691a5 100644 --- a/libs/ardour/vst3_plugin.cc +++ b/libs/ardour/vst3_plugin.cc @@ -678,9 +678,9 @@ VST3Plugin::set_block_size (pframes_t n_samples) } samplecnt_t -VST3Plugin::plugin_tail () const +VST3Plugin::plugin_tailtime () const { - return _plug->plugin_tail (); + return _plug->plugin_tailtime (); } samplecnt_t @@ -1816,7 +1816,7 @@ VST3PI::plugin_latency () } uint32_t -VST3PI::plugin_tail () +VST3PI::plugin_tailtime () { if (!_plugin_tail) { // XXX this is currently never reset _plugin_tail = _processor->getTailSamples (); diff --git a/libs/ardour/wscript b/libs/ardour/wscript index 1ca9653a6a..6da16f8c45 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', From 6d477586710b56efe4fb6ec42dae167f805045e1 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Sat, 31 Aug 2024 02:29:55 +0200 Subject: [PATCH 089/111] Add TailTime Icon --- libs/widgets/ardour_icon.cc | 42 ++++++++++++++++++++++++++++++ libs/widgets/widgets/ardour_icon.h | 1 + 2 files changed, 43 insertions(+) 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 }; From aa5dbdd7700ac5f7b999e670562696123b360261 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Sat, 31 Aug 2024 01:12:28 +0200 Subject: [PATCH 090/111] GUI combined Latency/TailTime widget --- gtk2_ardour/plugin_ui.cc | 59 ++++++++++- gtk2_ardour/plugin_ui.h | 11 ++- gtk2_ardour/port_insert_ui.cc | 4 +- gtk2_ardour/port_insert_ui.h | 4 +- .../{latency_gui.cc => timectl_gui.cc} | 98 +++++++++++++------ gtk2_ardour/{latency_gui.h => timectl_gui.h} | 49 +++++----- gtk2_ardour/wscript | 2 +- 7 files changed, 167 insertions(+), 60 deletions(-) rename gtk2_ardour/{latency_gui.cc => timectl_gui.cc} (59%) rename gtk2_ardour/{latency_gui.h => timectl_gui.h} (69%) 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/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/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/wscript b/gtk2_ardour/wscript index d1e0f94274..e16141a3f1 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', @@ -301,6 +300,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', From b13f04c61ea27bdb12330732779563c02b6781cc Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Sat, 31 Aug 2024 02:52:54 +0200 Subject: [PATCH 091/111] RegionFX: fix hiding RFx GUI when removing regions --- gtk2_ardour/region_editor.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtk2_ardour/region_editor.cc b/gtk2_ardour/region_editor.cc index 650dd1c73b..a12e2cdd69 100644 --- a/gtk2_ardour/region_editor.cc +++ b/gtk2_ardour/region_editor.cc @@ -987,7 +987,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); From 2263b0280bf67a96013fe3f6eb00c642be2a250f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Sat, 31 Aug 2024 02:28:34 +0200 Subject: [PATCH 092/111] Change to C++11-style std::map initialization in ExportHandler::finish_timespan() --- libs/ardour/export_handler.cc | 59 +++++++++++++++-------------------- 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/libs/ardour/export_handler.cc b/libs/ardour/export_handler.cc index 028c56685a..46fbc9564e 100644 --- a/libs/ardour/export_handler.cc +++ b/libs/ardour/export_handler.cc @@ -451,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 (); @@ -470,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; From 0cf73d459b35232aa1f082c2476b73f2fb31ae92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Sat, 31 Aug 2024 02:52:58 +0200 Subject: [PATCH 093/111] Deprecate ARDOUR::PinMappings C++11's std::map::at emulation --- libs/ardour/ardour/plugin_insert.h | 13 ++++--------- libs/ardour/plugin_insert.cc | 16 ++++++++-------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/libs/ardour/ardour/plugin_insert.h b/libs/ardour/ardour/plugin_insert.h index 61e871a446..0db97ce823 100644 --- a/libs/ardour/ardour/plugin_insert.h +++ b/libs/ardour/ardour/plugin_insert.h @@ -324,6 +324,8 @@ private: /* ordered map [plugin instance ID] => ARDOUR::ChanMapping * TODO: consider replacing with boost::flat_map<> or std::vector<>. + * TODO: Remove class and turn it into a type alias when .p(i) method + * becomes completely unused. */ #if defined(_MSC_VER) /* && (_MSC_VER < 1900) * Regarding the note (below) it was initially @@ -341,16 +343,9 @@ private: #endif { public: - /* this emulates C++11's std::map::at() - * return mapping for given plugin instance */ + [[deprecated]] inline ARDOUR::ChanMapping const& p (const uint32_t i) const { -#ifndef NDEBUG - const_iterator x = find (i); - assert (x != end ()); - return x->second; -#else - return find(i)->second; -#endif + return at(i); } }; diff --git a/libs/ardour/plugin_insert.cc b/libs/ardour/plugin_insert.cc index e69f67ea06..fa51bf1bf8 100644 --- a/libs/ardour/plugin_insert.cc +++ b/libs/ardour/plugin_insert.cc @@ -916,11 +916,11 @@ PluginInsert::connect_and_run (BufferSet& bufs, samplepos_t start, samplepos_t e continue; } bool valid; - uint32_t first_idx = in_map.p(0).get (*t, 0, &valid); + uint32_t first_idx = in_map.at(0).get (*t, 0, &valid); assert (valid && first_idx == 0); // check_inplace ensures this /* copy the first stream's buffer contents to the others */ for (uint32_t i = 1; i < natural_input_streams ().get (*t); ++i) { - uint32_t idx = in_map.p(0).get (*t, i, &valid); + uint32_t idx = in_map.at(0).get (*t, i, &valid); if (valid) { x_assert (idx, idx == 0); bufs.get_available (*t, i).read_from (bufs.get_available (*t, first_idx), nframes, offset, offset); @@ -1034,7 +1034,7 @@ PluginInsert::connect_and_run (BufferSet& bufs, samplepos_t start, samplepos_t e for (DataType::iterator t = DataType::begin(); t != DataType::end(); ++t) { for (uint32_t out = 0; out < natural_output_streams().get (*t); ++out) { bool valid; - uint32_t out_idx = out_map.p(pc).get (*t, out, &valid); + uint32_t out_idx = out_map.at(pc).get (*t, out, &valid); if (valid) { used_outputs.set (*t, out_idx, 1); // mark as used } @@ -1066,14 +1066,14 @@ PluginInsert::connect_and_run (BufferSet& bufs, samplepos_t start, samplepos_t e for (Plugins::iterator i = _plugins.begin(); i != _plugins.end(); ++i, ++pc) { ARDOUR::ChanMapping i_in_map (natural_input_streams()); - ARDOUR::ChanMapping i_out_map (out_map.p(pc)); + ARDOUR::ChanMapping i_out_map (out_map.at(pc)); ARDOUR::ChanCount mapped; /* map inputs sequentially */ for (DataType::iterator t = DataType::begin(); t != DataType::end(); ++t) { for (uint32_t in = 0; in < natural_input_streams().get (*t); ++in) { bool valid; - uint32_t in_idx = in_map.p(pc).get (*t, in, &valid); + uint32_t in_idx = in_map.at(pc).get (*t, in, &valid); uint32_t m = mapped.get (*t); if (valid) { inplace_bufs.get_available (*t, m).read_from (bufs.get_available (*t, in_idx), nframes, offset, offset); @@ -1120,7 +1120,7 @@ PluginInsert::connect_and_run (BufferSet& bufs, samplepos_t start, samplepos_t e /* in-place processing */ uint32_t pc = 0; for (Plugins::iterator i = _plugins.begin(); i != _plugins.end(); ++i, ++pc) { - if ((*i)->connect_and_run(bufs, start, end, speed, in_map.p(pc), out_map.p(pc), nframes, offset)) { + if ((*i)->connect_and_run(bufs, start, end, speed, in_map.at(pc), out_map.at(pc), nframes, offset)) { deactivate (); } } @@ -2608,9 +2608,9 @@ PluginInsert::state () const for (uint32_t pc = 0; pc < get_count(); ++pc) { char tmp[128]; snprintf (tmp, sizeof(tmp), "InputMap-%d", pc); - node.add_child_nocopy (* _in_map.p (pc).state (tmp)); + node.add_child_nocopy (* _in_map.at (pc).state (tmp)); snprintf (tmp, sizeof(tmp), "OutputMap-%d", pc); - node.add_child_nocopy (* _out_map.p (pc).state (tmp)); + node.add_child_nocopy (* _out_map.at (pc).state (tmp)); } node.add_child_nocopy (* _thru_map.state ("ThruMap")); From f41d7514b084189c45f38e9e3c0a7b8eac1540d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Sat, 31 Aug 2024 02:54:41 +0200 Subject: [PATCH 094/111] Use C++11-style initialization of Editor.last_event_time --- gtk2_ardour/editor.cc | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/gtk2_ardour/editor.cc b/gtk2_ardour/editor.cc index e358292e8a..6a55099cf8 100644 --- a/gtk2_ardour/editor.cc +++ b/gtk2_ardour/editor.cc @@ -374,7 +374,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) @@ -472,9 +472,6 @@ Editor::Editor () _have_idled = false; - last_event_time.tv_sec = 0; - last_event_time.tv_usec = 0; - selection_op_history.clear(); before.clear(); From 98c906b733dbea5230237d75018630563bce6e95 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Sat, 31 Aug 2024 05:54:39 +0200 Subject: [PATCH 095/111] Remove unused function --- libs/ardour/ardour/plugin_insert.h | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/libs/ardour/ardour/plugin_insert.h b/libs/ardour/ardour/plugin_insert.h index 0db97ce823..b0d096a296 100644 --- a/libs/ardour/ardour/plugin_insert.h +++ b/libs/ardour/ardour/plugin_insert.h @@ -322,11 +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<>. - * TODO: Remove class and turn it into a type alias when .p(i) method - * becomes completely unused. - */ + /* 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 @@ -341,13 +337,7 @@ private: #else class PinMappings : public std::map , PBD::StackAllocator, 4> > #endif - { - public: - [[deprecated]] - inline ARDOUR::ChanMapping const& p (const uint32_t i) const { - return at(i); - } - }; + {}; PinMappings _in_map; PinMappings _out_map; From 960d72012dca728f55d9936f22a57e78b6296886 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Sat, 31 Aug 2024 13:51:21 +0200 Subject: [PATCH 096/111] Reduce locate overhead for optimized builds --- libs/ardour/session_transport.cc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/ardour/session_transport.cc b/libs/ardour/session_transport.cc index 1937dbdca5..7167182bf9 100644 --- a/libs/ardour/session_transport.cc +++ b/libs/ardour/session_transport.cc @@ -1276,7 +1276,6 @@ Session::non_realtime_locate () /* no more looping .. should have been noticed elsewhere */ } - microseconds_t start; uint32_t nt = 0; samplepos_t tf; @@ -1288,7 +1287,9 @@ Session::non_realtime_locate () restart: sc = _seek_counter.load (); tf = _transport_sample; - start = get_microseconds (); +#ifndef NDEBUG + microseconds_t start = get_microseconds (); +#endif std::shared_ptr tl = io_tasklist (); for (auto const& i : *rl) { @@ -1300,9 +1301,9 @@ Session::non_realtime_locate () goto restart; } +#ifndef NDEBUG microseconds_t end = get_microseconds (); int usecs_per_track = lrintf ((end - start) / std::max (1.0, nt)); -#ifndef NDEBUG std::cerr << "locate to " << tf << " took " << (end - start) << " usecs for " << nt << " tracks = " << usecs_per_track << " per track\n"; #endif if (usecs_per_track > _current_usecs_per_track.load ()) { From 3391c69ec02b03a48c5408d6993dfb274c6f7173 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Sat, 31 Aug 2024 15:19:35 +0200 Subject: [PATCH 097/111] Profile RegionFx processing --- libs/ardour/audioregion.cc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/libs/ardour/audioregion.cc b/libs/ardour/audioregion.cc index 51260a0959..90a68f0e0f 100644 --- a/libs/ardour/audioregion.cc +++ b/libs/ardour/audioregion.cc @@ -872,7 +872,19 @@ AudioRegion::read_at (Sample* buf, /* apply region FX to all channels */ if (have_fx) { +#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 */ From fb0b4d254e21ed7a811833d4c48a307f065346e7 Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Sat, 31 Aug 2024 09:03:00 -0600 Subject: [PATCH 098/111] automation time axis should respond to base fill color changes --- gtk2_ardour/automation_time_axis.cc | 7 +++++++ 1 file changed, 7 insertions(+) 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 From 1b343a1fec0c90a670dc90bb7059e71955f8d3e1 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Sat, 31 Aug 2024 23:42:33 +0200 Subject: [PATCH 099/111] Revert "Reduce locate overhead for optimized builds" This reverts commit 960d72012dca728f55d9936f22a57e78b6296886. --- libs/ardour/session_transport.cc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/libs/ardour/session_transport.cc b/libs/ardour/session_transport.cc index 7167182bf9..1937dbdca5 100644 --- a/libs/ardour/session_transport.cc +++ b/libs/ardour/session_transport.cc @@ -1276,6 +1276,7 @@ Session::non_realtime_locate () /* no more looping .. should have been noticed elsewhere */ } + microseconds_t start; uint32_t nt = 0; samplepos_t tf; @@ -1287,9 +1288,7 @@ Session::non_realtime_locate () restart: sc = _seek_counter.load (); tf = _transport_sample; -#ifndef NDEBUG - microseconds_t start = get_microseconds (); -#endif + start = get_microseconds (); std::shared_ptr tl = io_tasklist (); for (auto const& i : *rl) { @@ -1301,9 +1300,9 @@ Session::non_realtime_locate () goto restart; } -#ifndef NDEBUG microseconds_t end = get_microseconds (); int usecs_per_track = lrintf ((end - start) / std::max (1.0, nt)); +#ifndef NDEBUG std::cerr << "locate to " << tf << " took " << (end - start) << " usecs for " << nt << " tracks = " << usecs_per_track << " per track\n"; #endif if (usecs_per_track > _current_usecs_per_track.load ()) { From da0e6c7d6029e343154b3f69b7d2448cd18f2cba Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Sun, 1 Sep 2024 18:40:08 +0200 Subject: [PATCH 100/111] Add explicit PBD namespace prefix to Signal This will simplify search/replace when using a variadic template for PBD::Signal. --- libs/ardour/element_importer.cc | 4 ++-- .../control_protocol/control_protocol.cc | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) 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/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; From e48d97ed69cd86b049924980be2f05d328f59498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Sat, 31 Aug 2024 17:30:55 +0200 Subject: [PATCH 101/111] Turn PinMappings class into a type alias --- libs/ardour/ardour/plugin_insert.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libs/ardour/ardour/plugin_insert.h b/libs/ardour/ardour/plugin_insert.h index b0d096a296..1b3517123e 100644 --- a/libs/ardour/ardour/plugin_insert.h +++ b/libs/ardour/ardour/plugin_insert.h @@ -333,11 +333,10 @@ private: * which is known to be troublesome with Visual C++ :- * https://www.boost.org/doc/libs/1_65_0/libs/type_traits/doc/html/boost_typetraits/reference/aligned_storage.html */ - class PinMappings : public std::map + typedef std::map PinMappings; #else - class PinMappings : public std::map , PBD::StackAllocator, 4> > + typedef std::map , PBD::StackAllocator, 4> > PinMappings; #endif - {}; PinMappings _in_map; PinMappings _out_map; From 867eaa0b132ccaf7358eb860b36a76c508259f6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Mon, 19 Aug 2024 16:47:24 +0200 Subject: [PATCH 102/111] Remove unused libs/pbd/pbd/stl_functors.h --- libs/pbd/pbd/stl_functors.h | 93 ------------------------------------- 1 file changed, 93 deletions(-) delete mode 100644 libs/pbd/pbd/stl_functors.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__ From 142fa9f55db4c3488edf417feeb94bdc8e7d5809 Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Sun, 30 Jun 2024 13:29:09 +0200 Subject: [PATCH 103/111] osc: Let OSCSelectObserver know about feedback config changes Before this commit, OSCSelectObserver would read the feedback value when it was created, but then never update it anymore. In practice, the OSCSelectObserver is created on startup, and when the surface connects and configures feeback, this value is not applied. For example, when sending `/set_surface` with a feedback value of 4 (Send SSID as path extension), `/strip/*` would get their ssid put into the path, but `/select/plugin/*` messages would not have their parameter id in the path. When setting the corresponding checkbox in the default feedback preferences, it is applied as expected. This commit passes the new feedback value to the OSCSelectObserver instance whenever it changes, which ensures the value is applied as expected. --- libs/surfaces/osc/osc.cc | 6 ++++++ libs/surfaces/osc/osc_select_observer.cc | 12 ++++++++++-- libs/surfaces/osc/osc_select_observer.h | 1 + 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/libs/surfaces/osc/osc.cc b/libs/surfaces/osc/osc.cc index 11e7c5fae9..a81a7bef4a 100644 --- a/libs/surfaces/osc/osc.cc +++ b/libs/surfaces/osc/osc.cc @@ -1873,6 +1873,9 @@ 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; @@ -1953,6 +1956,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; From a9a5787399327921ff364d6e2d455ea0f94278c7 Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Sun, 30 Jun 2024 18:04:41 +0200 Subject: [PATCH 104/111] osc: Fix send and plugin page size in /set_surface When handling the `/set_surface` command, the code would set plug_page_size to the new value first, and call `sel_plug_pagesize()` later. The latter then sees the page size is already the same, so it leaves it unchanged and also does not send the page size to the OSCSelectObserver object. In practice, this means that only the default plugin page size from the preferences or set with `/set_surface/plugin_page_size` take effect and values set with `/set_surface` are ignored. Exactly the same thing happens for the send page size. This code has been like this since it was first introduced in comit 9c0f6ea948 (OSC: Allow set_surface to set send and plugin page sizes., 2017-06-13) This commit fixes this by omitting the first assignment. --- libs/surfaces/osc/osc.cc | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/surfaces/osc/osc.cc b/libs/surfaces/osc/osc.cc index a81a7bef4a..f1b6a8f9d6 100644 --- a/libs/surfaces/osc/osc.cc +++ b/libs/surfaces/osc/osc.cc @@ -1882,8 +1882,6 @@ OSC::set_surface (uint32_t b_size, uint32_t strips, uint32_t fb, uint32_t gm, ui } 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; } From e4d9344d2a29367a193c71be48d2ce7efc1693ca Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Tue, 3 Sep 2024 13:20:48 +0200 Subject: [PATCH 105/111] Revert PinMapping Changes This reverts * e48d97ed69cd86b049924980be2f05d328f59498 * 98c906b733dbea5230237d75018630563bce6e95 * 0cf73d459b35232aa1f082c2476b73f2fb31ae92 because the C++ API std::map:at can throw and exception was not implemented (and also deemed excessive for the case at hand). Also an explicit API for *p*plugin_pin mapping is preferable and facilitates debugging. --- libs/ardour/ardour/plugin_insert.h | 18 ++++++++++++++++-- libs/ardour/plugin_insert.cc | 16 ++++++++-------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/libs/ardour/ardour/plugin_insert.h b/libs/ardour/ardour/plugin_insert.h index 1b3517123e..5ad4192080 100644 --- a/libs/ardour/ardour/plugin_insert.h +++ b/libs/ardour/ardour/plugin_insert.h @@ -333,10 +333,24 @@ private: * which is known to be troublesome with Visual C++ :- * https://www.boost.org/doc/libs/1_65_0/libs/type_traits/doc/html/boost_typetraits/reference/aligned_storage.html */ - typedef std::map PinMappings; + class PinMappings : public std::map #else - typedef std::map , PBD::StackAllocator, 4> > PinMappings; + class PinMappings : public std::map , PBD::StackAllocator, 4> > #endif + { + public: + /* this emulates C++11's std::map::at() + * return mapping for given plugin instance */ + inline ARDOUR::ChanMapping const& p (const uint32_t i) const { +#ifndef NDEBUG + const_iterator x = find (i); + assert (x != end ()); + return x->second; +#else + return find(i)->second; +#endif + } + }; PinMappings _in_map; PinMappings _out_map; diff --git a/libs/ardour/plugin_insert.cc b/libs/ardour/plugin_insert.cc index fa51bf1bf8..e69f67ea06 100644 --- a/libs/ardour/plugin_insert.cc +++ b/libs/ardour/plugin_insert.cc @@ -916,11 +916,11 @@ PluginInsert::connect_and_run (BufferSet& bufs, samplepos_t start, samplepos_t e continue; } bool valid; - uint32_t first_idx = in_map.at(0).get (*t, 0, &valid); + uint32_t first_idx = in_map.p(0).get (*t, 0, &valid); assert (valid && first_idx == 0); // check_inplace ensures this /* copy the first stream's buffer contents to the others */ for (uint32_t i = 1; i < natural_input_streams ().get (*t); ++i) { - uint32_t idx = in_map.at(0).get (*t, i, &valid); + uint32_t idx = in_map.p(0).get (*t, i, &valid); if (valid) { x_assert (idx, idx == 0); bufs.get_available (*t, i).read_from (bufs.get_available (*t, first_idx), nframes, offset, offset); @@ -1034,7 +1034,7 @@ PluginInsert::connect_and_run (BufferSet& bufs, samplepos_t start, samplepos_t e for (DataType::iterator t = DataType::begin(); t != DataType::end(); ++t) { for (uint32_t out = 0; out < natural_output_streams().get (*t); ++out) { bool valid; - uint32_t out_idx = out_map.at(pc).get (*t, out, &valid); + uint32_t out_idx = out_map.p(pc).get (*t, out, &valid); if (valid) { used_outputs.set (*t, out_idx, 1); // mark as used } @@ -1066,14 +1066,14 @@ PluginInsert::connect_and_run (BufferSet& bufs, samplepos_t start, samplepos_t e for (Plugins::iterator i = _plugins.begin(); i != _plugins.end(); ++i, ++pc) { ARDOUR::ChanMapping i_in_map (natural_input_streams()); - ARDOUR::ChanMapping i_out_map (out_map.at(pc)); + ARDOUR::ChanMapping i_out_map (out_map.p(pc)); ARDOUR::ChanCount mapped; /* map inputs sequentially */ for (DataType::iterator t = DataType::begin(); t != DataType::end(); ++t) { for (uint32_t in = 0; in < natural_input_streams().get (*t); ++in) { bool valid; - uint32_t in_idx = in_map.at(pc).get (*t, in, &valid); + uint32_t in_idx = in_map.p(pc).get (*t, in, &valid); uint32_t m = mapped.get (*t); if (valid) { inplace_bufs.get_available (*t, m).read_from (bufs.get_available (*t, in_idx), nframes, offset, offset); @@ -1120,7 +1120,7 @@ PluginInsert::connect_and_run (BufferSet& bufs, samplepos_t start, samplepos_t e /* in-place processing */ uint32_t pc = 0; for (Plugins::iterator i = _plugins.begin(); i != _plugins.end(); ++i, ++pc) { - if ((*i)->connect_and_run(bufs, start, end, speed, in_map.at(pc), out_map.at(pc), nframes, offset)) { + if ((*i)->connect_and_run(bufs, start, end, speed, in_map.p(pc), out_map.p(pc), nframes, offset)) { deactivate (); } } @@ -2608,9 +2608,9 @@ PluginInsert::state () const for (uint32_t pc = 0; pc < get_count(); ++pc) { char tmp[128]; snprintf (tmp, sizeof(tmp), "InputMap-%d", pc); - node.add_child_nocopy (* _in_map.at (pc).state (tmp)); + node.add_child_nocopy (* _in_map.p (pc).state (tmp)); snprintf (tmp, sizeof(tmp), "OutputMap-%d", pc); - node.add_child_nocopy (* _out_map.at (pc).state (tmp)); + node.add_child_nocopy (* _out_map.p (pc).state (tmp)); } node.add_child_nocopy (* _thru_map.state ("ThruMap")); From 8fea1ea42ec9c0f37e002f301f59041a7b5bd255 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Tue, 3 Sep 2024 19:34:44 +0200 Subject: [PATCH 106/111] Update hidapi --- libs/hidapi/README | 3 +- libs/hidapi/hidapi/hidapi.h | 381 +++- libs/hidapi/linux/hid.c | 1305 +++++++++---- libs/hidapi/mac/hid.c | 1111 +++++++---- libs/hidapi/mac/hidapi_darwin.h | 98 + libs/hidapi/windows/hid.c | 1633 +++++++++++------ libs/hidapi/windows/hidapi_cfgmgr32.h | 75 + .../windows/hidapi_descriptor_reconstruct.c | 987 ++++++++++ .../windows/hidapi_descriptor_reconstruct.h | 247 +++ libs/hidapi/windows/hidapi_hidclass.h | 38 + libs/hidapi/windows/hidapi_hidpi.h | 72 + libs/hidapi/windows/hidapi_hidsdi.h | 59 + libs/hidapi/windows/hidapi_winapi.h | 74 + libs/hidapi/wscript | 1 + 14 files changed, 4812 insertions(+), 1272 deletions(-) create mode 100644 libs/hidapi/mac/hidapi_darwin.h create mode 100644 libs/hidapi/windows/hidapi_cfgmgr32.h create mode 100644 libs/hidapi/windows/hidapi_descriptor_reconstruct.c create mode 100644 libs/hidapi/windows/hidapi_descriptor_reconstruct.h create mode 100644 libs/hidapi/windows/hidapi_hidclass.h create mode 100644 libs/hidapi/windows/hidapi_hidpi.h create mode 100644 libs/hidapi/windows/hidapi_hidsdi.h create mode 100644 libs/hidapi/windows/hidapi_winapi.h 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' From 26217e9d859d07c9567539d1961006e975117d9b Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Wed, 4 Sep 2024 01:22:22 +0200 Subject: [PATCH 107/111] Doxygen: disable collaboration diagrams They are convoluted and not readable for the vast majority of Ardour classes. --- doc/Doxyfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 3acc8c76ca4306e01f65aa7c4f8cbb61f470a884 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Wed, 4 Sep 2024 22:29:24 +0200 Subject: [PATCH 108/111] Handle Filechooser Location entry In order for the Filechooser Location widget to work two things need to be setup, which only the FileChooserDialog does: * subscribe to Widget's "response-requested" signal * call should_respond () hook from top-level window's default handler. The Location Entry emits "activates-default". In case of the Dialog, that calls the dialogs response callback, which then calls ` _gtk_file_chooser_embed_should_respond`. That handles changes made by the user to the location entry. -=- Gtk::FileChooserWidget does not handle this, "response-requested" signal is not exposed, nor is _gtk_file_chooser_embed_should_respond available outside Gtk. This change at least selects the file in the treeview, which allows further handling, without interfering with FileChooserDialog's behavior. --- libs/tk/ytk/gtkfilechooserdefault.c | 74 ++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) 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) From 267cddfb0508a08423544b7c32a3da1d04cfc6b2 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Thu, 5 Sep 2024 18:27:44 +0200 Subject: [PATCH 109/111] Fix stuck insensitive macOS main menu Popup Dialog Windows never unset the modal flag. e.g. Session > Save Snapshot & switch. Furthermore a 2nd dialog was able to get the menu stuck forever (e.g. Snapshot & Switch .. -> Replace existing? --- libs/gtkmm2ext/gtkapplication_quartz.mm | 14 +++++++++----- libs/tk/ydk/quartz/gdkwindow-quartz.c | 19 ++++++++++++++++--- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/libs/gtkmm2ext/gtkapplication_quartz.mm b/libs/gtkmm2ext/gtkapplication_quartz.mm index 38db6a2d42..f83180f4c9 100644 --- a/libs/gtkmm2ext/gtkapplication_quartz.mm +++ b/libs/gtkmm2ext/gtkapplication_quartz.mm @@ -59,7 +59,7 @@ static gint _exiting = 0; static std::vector global_menu_items; -static bool _modal_state = false; +static gint _modal_state = 0; static guint gdk_quartz_keyval_to_ns_keyval (guint keyval) @@ -585,7 +585,7 @@ idle_call_activate (gpointer data) } - (BOOL) validateMenuItem:(NSMenuItem*) menuItem { - if (_modal_state) { + if (_modal_state > 0) { return false; } @@ -1468,7 +1468,7 @@ namespace Gtk { - (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *) app { UNUSED_PARAMETER(app); - if (_modal_state) { + if (_modal_state > 0) { return NSTerminateCancel; } Gtkmm2ext::Application::instance()->ShouldQuit (); @@ -1480,14 +1480,18 @@ static void gdk_quartz_modal_notify (GdkWindow*, gboolean modal) { /* this global will control sensitivity of our app menu items, via validateMenuItem */ - _modal_state = modal; + 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, !modal); + gtk_action_set_sensitive (act, 0 == _modal_state); } } } diff --git a/libs/tk/ydk/quartz/gdkwindow-quartz.c b/libs/tk/ydk/quartz/gdkwindow-quartz.c index 67d1077696..facb9b1e4b 100644 --- a/libs/tk/ydk/quartz/gdkwindow-quartz.c +++ b/libs/tk/ydk/quartz/gdkwindow-quartz.c @@ -197,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); @@ -2386,14 +2394,19 @@ void gdk_window_set_modal_hint (GdkWindow *window, gboolean modal) { + GdkWindowObject *private; + if (GDK_WINDOW_DESTROYED (window) || !WINDOW_IS_TOPLEVEL (window)) return; - if (_gdk_modal_notify) { + private = (GdkWindowObject*) window; + + if (_gdk_modal_notify && private->modal_hint != modal) { _gdk_modal_notify (window, modal); } - /* FIXME: Implement */ + + private->modal_hint = modal; } void From 4692227168d094691c210db8394ab8bc48861d19 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Thu, 5 Sep 2024 20:16:55 +0200 Subject: [PATCH 110/111] Fix hiding of incactive tracks See ed105fdf668fde449f76cb7fe9981f1a5ca4fb69 for detailed explanation. --- gtk2_ardour/route_time_axis.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtk2_ardour/route_time_axis.cc b/gtk2_ardour/route_time_axis.cc index 41485e8d4c..8f3e88ea18 100644 --- a/gtk2_ardour/route_time_axis.cc +++ b/gtk2_ardour/route_time_axis.cc @@ -886,7 +886,7 @@ RouteTimeAxisView::build_display_menu () 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 ()) { items.push_back (SeparatorElem()); From 00f646860682909a8fffa4a2ab5e228238081540 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Thu, 5 Sep 2024 20:35:24 +0200 Subject: [PATCH 111/111] Consolidate Route context menus (Mixer, Editor, Cue Page) Consistently hide elements that are not accessible on inactive Routes; fix duplicate separators, and group items --- gtk2_ardour/mixer_strip.cc | 47 +++++++++++-------- gtk2_ardour/route_time_axis.cc | 83 ++++++++++++++++++---------------- gtk2_ardour/trigger_strip.cc | 24 ++++++---- 3 files changed, 89 insertions(+), 65 deletions(-) diff --git a/gtk2_ardour/mixer_strip.cc b/gtk2_ardour/mixer_strip.cc index a5298afe4c..076b6da25a 100644 --- a/gtk2_ardour/mixer_strip.cc +++ b/gtk2_ardour/mixer_strip.cc @@ -1090,8 +1090,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) @@ -1100,11 +1098,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()); } @@ -1113,18 +1118,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; @@ -1133,7 +1126,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))); } @@ -1142,13 +1156,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/route_time_axis.cc b/gtk2_ardour/route_time_axis.cc index 8f3e88ea18..0cc2043d78 100644 --- a/gtk2_ardour/route_time_axis.cc +++ b/gtk2_ardour/route_time_axis.cc @@ -326,6 +326,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); @@ -406,6 +407,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(), @@ -630,28 +632,31 @@ RouteTimeAxisView::build_display_menu () TimeAxisView::build_display_menu (); - /* now fill it with our stuff */ + bool active = _route->active (); MenuList& items = display_menu->items(); - 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(); @@ -795,32 +800,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) { @@ -833,9 +840,9 @@ RouteTimeAxisView::build_display_menu () always_active |= r->route()->mixbus() != 0; #endif if (r->route()->active()) { - ++active; + ++n_active; } else { - ++inactive; + ++n_inactive; } } @@ -876,10 +883,10 @@ 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); @@ -888,7 +895,7 @@ RouteTimeAxisView::build_display_menu () items.push_back (SeparatorElem()); 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/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.