From 0a27c14041e2c62e323485dfba9d7b334067f3da Mon Sep 17 00:00:00 2001 From: Bruno Herbelin Date: Thu, 23 Dec 2021 22:17:05 +0100 Subject: [PATCH] Control manager and TouchOSC sync --- BaseToolkit.cpp | 15 +++ BaseToolkit.h | 3 + ControlManager.cpp | 228 +++++++++++++++++++++++-------------- ControlManager.h | 8 +- SourceCallback.cpp | 43 +++++++ SourceCallback.h | 25 +++- rsc/osc/vimix.mk1.touchosc | Bin 0 -> 2306 bytes rsc/osc/vimix.tosc | Bin 4573 -> 4586 bytes 8 files changed, 234 insertions(+), 88 deletions(-) create mode 100644 rsc/osc/vimix.mk1.touchosc diff --git a/BaseToolkit.cpp b/BaseToolkit.cpp index b03941a..c927d21 100644 --- a/BaseToolkit.cpp +++ b/BaseToolkit.cpp @@ -162,6 +162,21 @@ std::list BaseToolkit::splitted(const std::string& str, char delim) return strings; } +bool BaseToolkit::is_a_number(const std::string& str, int *val) +{ + bool isanumber = false; + + try { + *val = std::stoi(str); + isanumber = true; + } + catch (const std::invalid_argument&) { + // avoids crash + } + + return isanumber; +} + std::string BaseToolkit::common_prefix( const std::list & allStrings ) { if (allStrings.empty()) diff --git a/BaseToolkit.h b/BaseToolkit.h index 8dee544..84fbeaa 100644 --- a/BaseToolkit.h +++ b/BaseToolkit.h @@ -28,6 +28,9 @@ std::string truncated(const std::string& str, int N); // split a string into list of strings separated by delimitor (e.g. /home/me/toto.mpg -> {home, me, toto.mpg} ) std::list splitted(const std::string& str, char delim); +// returns true if the string +bool is_a_number(const std::string& str, int *val = nullptr); + // find common parts in a list of strings std::string common_prefix(const std::list &allStrings); std::string common_suffix(const std::list &allStrings); diff --git a/ControlManager.cpp b/ControlManager.cpp index 60dba24..a4b3d23 100644 --- a/ControlManager.cpp +++ b/ControlManager.cpp @@ -37,6 +37,7 @@ #define CONTROL_OSC_MSG "OSC: " + void Control::RequestListener::ProcessMessage( const osc::ReceivedMessage& m, const IpEndpointName& remoteEndpoint ) { @@ -55,19 +56,30 @@ void Control::RequestListener::ProcessMessage( const osc::ReceivedMessage& m, // A wellformed OSC address is in the form '/vimix/target/attribute {arguments}' // First test: should have 3 elements and start with APP_NAME ('vimix') // - if (address.size() == 3 && address.front().compare(OSC_PREFIX) == 0 ){ + if (address.size() > 2 && address.front().compare(OSC_PREFIX) == 0 ){ // done with the first part of the OSC address address.pop_front(); - // - // Execute next part of the OSC message according to target - // + // next part of the OSC message is the target std::string target = address.front(); - std::string attribute = address.back(); + // next part of the OSC message is the attribute + address.pop_front(); + std::string attribute = address.front(); // Log target: just print text in log window if ( target.compare(OSC_INFO) == 0 ) { - if ( attribute.compare(OSC_INFO_TEST) == 0) + if ( attribute.compare(OSC_INFO_SYNC) == 0) { + // send the global status Control::manager().sendStatus(remoteEndpoint); + // + // send the status of all sources + // + // (if an argument is given, it indicates the number of sources to update) + float N = 0.f; + if ( !m.ArgumentStream().Eos()) + m.ArgumentStream() >> N >> osc::EndMessage; + // send the status of all sources + Control::manager().sendSourcesStatus(remoteEndpoint, N); + } else if ( attribute.compare(OSC_INFO_LOG) == 0) Log::Info(CONTROL_OSC_MSG "received '%s' from %s", m.AddressPattern(), sender); } @@ -97,23 +109,36 @@ void Control::RequestListener::ProcessMessage( const osc::ReceivedMessage& m, // Current source target: apply attribute to the current sources else if ( target.compare(OSC_CURRENT) == 0 ) { - // attributes to change current - if ( attribute.compare(OSC_SET) == 0) { - int index = 0; - m.ArgumentStream() >> index >> osc::EndMessage; - Mixer::manager().setCurrentIndex(index); - // confirm by sending back the current source attributes - Control::manager().sendCurrentSourceAttibutes(remoteEndpoint); - } - else if ( attribute.compare(OSC_NEXT) == 0) { + int sourceid = -1; + if ( attribute.compare(OSC_NEXT) == 0) { + // set current to NEXT Mixer::manager().setCurrentNext(); // confirm by sending back the current source attributes Control::manager().sendCurrentSourceAttibutes(remoteEndpoint); + // send the updated status of all sources + Control::manager().sendSourcesStatus(remoteEndpoint); } else if ( attribute.compare(OSC_PREVIOUS) == 0) { + // set current to PREVIOUS Mixer::manager().setCurrentPrevious(); // confirm by sending back the current source attributes Control::manager().sendCurrentSourceAttibutes(remoteEndpoint); + // send the updated status of all sources + Control::manager().sendSourcesStatus(remoteEndpoint); + } + else if ( BaseToolkit::is_a_number( attribute.substr(1), &sourceid) ){ + // set current to given INDEX + Mixer::manager().setCurrentIndex(sourceid); + // confirm by sending back the current source attributes + Control::manager().sendCurrentSourceAttibutes(remoteEndpoint); + // + // send the status of all sources + // + // (if an argument is given, it indicates the number of sources to update) + float N = 0.f; + if ( !m.ArgumentStream().Eos()) + m.ArgumentStream() >> N >> osc::EndMessage; + Control::manager().sendSourcesStatus(remoteEndpoint, N); } // all other attributes operate on current source else @@ -126,16 +151,10 @@ void Control::RequestListener::ProcessMessage( const osc::ReceivedMessage& m, // try to find source by name Source *s = Mixer::manager().findSource(target); // if failed, try to find source by index - if (s == nullptr) { - int N = -1; - try { - N = std::stoi(target); - } catch (const std::invalid_argument&) { - N = -1; - } - if (N>=0) - s = Mixer::manager().sourceAtIndex(N); - } + int sourceid = -1; + if (s == nullptr && BaseToolkit::is_a_number(target, &sourceid) ) + s = Mixer::manager().sourceAtIndex(sourceid); + // if a source with the given target nameor index was found if (s) Control::manager().receiveSourceAttribute(s, attribute, m.ArgumentStream()); else @@ -212,19 +231,19 @@ void Control::receiveOutputAttribute(const std::string &attribute, try { /// e.g. '/vimix/output/enable' or '/vimix/output/enable T' or '/vimix/output/enable F' if ( attribute.compare(OSC_OUTPUT_ENABLE) == 0) { - bool on = true; + float on = 1.f; if ( !arguments.Eos()) { arguments >> on >> osc::EndMessage; } - Settings::application.render.disabled = !on; + Settings::application.render.disabled = on < 0.5f; } /// e.g. '/vimix/output/disable' or '/vimix/output/disable T' or '/vimix/output/disable F' else if ( attribute.compare(OSC_OUTPUT_DISABLE) == 0) { - bool on = true; + float on = 1.f; if ( !arguments.Eos()) { arguments >> on >> osc::EndMessage; } - Settings::application.render.disabled = on; + Settings::application.render.disabled = on > 0.5f; } /// e.g. '/vimix/output/fading f 0.2' else if ( attribute.compare(OSC_OUTPUT_FADING) == 0) { @@ -259,19 +278,19 @@ void Control::receiveSourceAttribute(Source *target, const std::string &attribut try { /// e.g. '/vimix/current/play' or '/vimix/current/play T' or '/vimix/current/play F' if ( attribute.compare(OSC_SOURCE_PLAY) == 0) { - bool on = true; + float on = 1.f; if ( !arguments.Eos()) { arguments >> on >> osc::EndMessage; } - target->call( new SetPlay(on) ); + target->call( new SetPlay(on > 0.5f) ); } /// e.g. '/vimix/current/pause' or '/vimix/current/pause T' or '/vimix/current/pause F' else if ( attribute.compare(OSC_SOURCE_PAUSE) == 0) { - bool on = true; + float on = 1.f; if ( !arguments.Eos()) { arguments >> on >> osc::EndMessage; } - target->call( new SetPlay(!on) ); + target->call( new SetPlay(on < 0.5f) ); } /// e.g. '/vimix/current/replay' else if ( attribute.compare(OSC_SOURCE_REPLAY) == 0) { @@ -307,6 +326,16 @@ void Control::receiveSourceAttribute(Source *target, const std::string &attribut arguments >> x >> y >> osc::EndMessage; target->call( new Resize( x, y), true ); } + /// e.g. '/vimix/current/turn f 1.0' + else if ( attribute.compare(OSC_SOURCE_TURN) == 0) { + float x = 0.f; + arguments >> x >> osc::EndMessage; + target->call( new Turn( x ), true ); + } + /// e.g. '/vimix/current/reset' + else if ( attribute.compare(OSC_SOURCE_RESET) == 0) { + target->call( new ResetGeometry(), true ); + } #ifdef CONTROL_DEBUG else { Log::Info(CONTROL_OSC_MSG "Ignoring attribute '%s' for target %s.", attribute.c_str(), target->name().c_str()); @@ -328,49 +357,87 @@ void Control::receiveSourceAttribute(Source *target, const std::string &attribut void Control::sendCurrentSourceAttibutes(const IpEndpointName &remoteEndpoint) { + // default values + char name[21] = {"\0"}; + float play = 0.f; + float depth = 0.f; + float alpha = 0.f; + + // fill values if the current source is valid Source *s = Mixer::manager().currentSource(); if (s!=nullptr) { - - // build socket to send message to indicated endpoint - UdpTransmitSocket socket( IpEndpointName( remoteEndpoint.address, Settings::application.control.osc_port_send ) ); - - // build messages packet - char buffer[IP_MTU_SIZE]; - osc::OutboundPacketStream p( buffer, IP_MTU_SIZE ); - - // create bundle - p.Clear(); - p << osc::BeginBundle(); - - /// - /// messages - /// - /// Play status - p << osc::BeginMessage( OSC_PREFIX OSC_CURRENT OSC_SOURCE_PLAY ); - p << s->playing(); - p << osc::EndMessage; - /// Depth - p << osc::BeginMessage( OSC_PREFIX OSC_CURRENT OSC_SOURCE_DEPTH ); - p << s->depth(); - p << osc::EndMessage; - /// Alpha - p << osc::BeginMessage( OSC_PREFIX OSC_CURRENT OSC_SOURCE_ALPHA ); - p << s->alpha(); - p << osc::EndMessage; - /// indexed alpha - std::string addresspattern = std::string(OSC_PREFIX) + OSC_SEPARATOR; - addresspattern += std::to_string(Mixer::manager().indexCurrentSource()) + OSC_SOURCE_ALPHA; - p << osc::BeginMessage(addresspattern.c_str()); - p << s->alpha(); - p << osc::EndMessage; - socket.Send( p.Data(), p.Size() ); - - // send bundle - p << osc::EndBundle; - socket.Send( p.Data(), p.Size() ); + strncpy(name, s->name().c_str(), 20); + play = s->playing() ? 1.f : 0.f; + depth = s->depth(); + alpha = s->alpha(); } + + // build socket to send message to indicated endpoint + UdpTransmitSocket socket( IpEndpointName( remoteEndpoint.address, Settings::application.control.osc_port_send ) ); + + // build messages packet + char buffer[IP_MTU_SIZE]; + osc::OutboundPacketStream p( buffer, IP_MTU_SIZE ); + + // create bundle + p.Clear(); + p << osc::BeginBundle(); + + /// name + p << osc::BeginMessage( OSC_PREFIX OSC_CURRENT OSC_SOURCE_NAME ) << name << osc::EndMessage; + /// Play status + p << osc::BeginMessage( OSC_PREFIX OSC_CURRENT OSC_SOURCE_PLAY ) << play << osc::EndMessage; + /// Depth + p << osc::BeginMessage( OSC_PREFIX OSC_CURRENT OSC_SOURCE_DEPTH ) << depth << osc::EndMessage; + /// Alpha + p << osc::BeginMessage( OSC_PREFIX OSC_CURRENT OSC_SOURCE_ALPHA ) << alpha << osc::EndMessage; + + // send bundle + p << osc::EndBundle; + socket.Send( p.Data(), p.Size() ); } + +void Control::sendSourcesStatus(const IpEndpointName &remoteEndpoint, float max_count) +{ + // build socket to send message to indicated endpoint + UdpTransmitSocket socket( IpEndpointName( remoteEndpoint.address, Settings::application.control.osc_port_send ) ); + + // build messages packet + char buffer[IP_MTU_SIZE]; + osc::OutboundPacketStream p( buffer, IP_MTU_SIZE ); + + p.Clear(); + p << osc::BeginBundle(); + + int i = 0; + char oscaddr[128]; + int index_current = Mixer::manager().indexCurrentSource(); + for (; i < Mixer::manager().count(); ++i) { + // send status of currently selected + sprintf(oscaddr, OSC_PREFIX OSC_CURRENT "/%d", i); + p << osc::BeginMessage( oscaddr ) << (index_current == i ? 1.f : 0.f) << osc::EndMessage; + + // send status of alpha + sprintf(oscaddr, OSC_PREFIX "/%d" OSC_SOURCE_ALPHA, i); + p << osc::BeginMessage( oscaddr ) << Mixer::manager().sourceAtIndex(i)->alpha() << osc::EndMessage; + } + + for (; i < max_count; ++i) { + // reset status of currently selected + sprintf(oscaddr, OSC_PREFIX OSC_CURRENT "/%d", i); + p << osc::BeginMessage( oscaddr ) << 0.f << osc::EndMessage; + + // reset status of alpha + sprintf(oscaddr, OSC_PREFIX "/%d" OSC_SOURCE_ALPHA, i); + p << osc::BeginMessage( oscaddr ) << 0.f << osc::EndMessage; + } + + p << osc::EndBundle; + socket.Send( p.Data(), p.Size() ); +} + + void Control::sendStatus(const IpEndpointName &remoteEndpoint) { // build socket to send message to indicated endpoint @@ -383,28 +450,21 @@ void Control::sendStatus(const IpEndpointName &remoteEndpoint) p.Clear(); p << osc::BeginBundle(); + /// + /// messages + /// /// Agree to test - p << osc::BeginMessage( OSC_PREFIX OSC_INFO OSC_INFO_TEST ); + p << osc::BeginMessage( OSC_PREFIX OSC_INFO OSC_INFO_SYNC ); p << true; p << osc::EndMessage; - - /// send output attributes + /// output attributes p << osc::BeginMessage( OSC_PREFIX OSC_OUTPUT OSC_OUTPUT_ENABLE ); - p << !Settings::application.render.disabled; + p << (Settings::application.render.disabled ? 0.f : 1.f); p << osc::EndMessage; p << osc::BeginMessage( OSC_PREFIX OSC_OUTPUT OSC_OUTPUT_FADING ); p << Mixer::manager().session()->fading(); p << osc::EndMessage; - /// send all sources alpha - for (int i = 0; i < Mixer::manager().count(); ++i) { - std::string addresspattern = std::string(OSC_PREFIX) + OSC_SEPARATOR; - addresspattern += std::to_string(i) + OSC_SOURCE_ALPHA; - p << osc::BeginMessage(addresspattern.c_str()); - p << Mixer::manager().sourceAtIndex(i)->alpha(); - p << osc::EndMessage; - } - p << osc::EndBundle; socket.Send( p.Data(), p.Size() ); } diff --git a/ControlManager.h b/ControlManager.h index d469e8f..d67f1e9 100644 --- a/ControlManager.h +++ b/ControlManager.h @@ -4,7 +4,7 @@ #include "NetworkToolkit.h" #define OSC_INFO "/info" -#define OSC_INFO_TEST "/test" +#define OSC_INFO_SYNC "/sync" #define OSC_INFO_LOG "/log" #define OSC_OUTPUT "/output" @@ -17,9 +17,8 @@ #define OSC_CURRENT "/current" #define OSC_NEXT "/next" #define OSC_PREVIOUS "/previous" -#define OSC_SET "/set" -//#define OSC_VERSION "version" +#define OSC_SOURCE_NAME "/name" #define OSC_SOURCE_PLAY "/play" #define OSC_SOURCE_PAUSE "/pause" #define OSC_SOURCE_REPLAY "/replay" @@ -28,6 +27,8 @@ #define OSC_SOURCE_DEPTH "/depth" #define OSC_SOURCE_GRAB "/grab" #define OSC_SOURCE_RESIZE "/resize" +#define OSC_SOURCE_TURN "/turn" +#define OSC_SOURCE_RESET "/reset" class Session; class Source; @@ -69,6 +70,7 @@ protected: osc::ReceivedMessageArgumentStream arguments); void sendCurrentSourceAttibutes(const IpEndpointName& remoteEndpoint); + void sendSourcesStatus(const IpEndpointName& remoteEndpoint, float max_count = 0.f); void sendStatus(const IpEndpointName& remoteEndpoint); private: diff --git a/SourceCallback.cpp b/SourceCallback.cpp index f8d6693..3dad7e7 100644 --- a/SourceCallback.cpp +++ b/SourceCallback.cpp @@ -27,6 +27,16 @@ SourceCallback::SourceCallback(): active_(true), finished_(false), initialized_( { } +void ResetGeometry::update(Source *s, float) +{ + s->group(View::GEOMETRY)->scale_ = glm::vec3(1.f); + s->group(View::GEOMETRY)->rotation_.z = 0; + s->group(View::GEOMETRY)->crop_ = glm::vec3(1.f); + s->group(View::GEOMETRY)->translation_ = glm::vec3(0.f); + s->touch(); + finished_ = true; +} + SetAlpha::SetAlpha(float alpha) : SourceCallback(), alpha_(CLAMP(alpha, 0.f, 1.f)) { step_ = glm::normalize(glm::vec2(1.f, 1.f)); // step in diagonal by default @@ -196,3 +206,36 @@ void Resize::update(Source *s, float dt) else finished_ = true; } + +Turn::Turn(float da, float duration) : SourceCallback(), speed_(da), + duration_(duration), progress_(0.f) +{ +} + +void Turn::update(Source *s, float dt) +{ + if (s && !s->locked()) { + // reset on first run or upon call of reset() + if (!initialized_){ + // start animation + progress_ = 0.f; + // initial position + start_ = s->group(View::GEOMETRY)->rotation_.z; + initialized_ = true; + } + + // calculate amplitude of movement + progress_ += dt; + + // perform movement + s->group(View::GEOMETRY)->rotation_.z = start_ + speed_ * ( dt * -0.001f / M_PI); + + // timeout + if ( progress_ > duration_ ) { + // done + finished_ = true; + } + } + else + finished_ = true; +} diff --git a/SourceCallback.h b/SourceCallback.h index 536de17..988261e 100644 --- a/SourceCallback.h +++ b/SourceCallback.h @@ -16,7 +16,8 @@ public: CALLBACK_PLAY, CALLBACK_REPLAY, CALLBACK_GRAB, - CALLBACK_RESIZE + CALLBACK_RESIZE, + CALLBACK_TURN } CallbackType; SourceCallback(); @@ -35,6 +36,14 @@ protected: bool initialized_; }; +class ResetGeometry : public SourceCallback +{ + +public: + ResetGeometry() : SourceCallback() {} + void update(Source *s, float) override; +}; + class SetAlpha : public SourceCallback { float alpha_; @@ -107,5 +116,19 @@ public: CallbackType type () override { return CALLBACK_RESIZE; } }; +class Turn : public SourceCallback +{ + float speed_; + float start_; + float target_; + float duration_; + float progress_; + +public: + Turn(float da, float duration = 0.f); + void update(Source *s, float) override; + CallbackType type () override { return CALLBACK_TURN; } +}; + #endif // SOURCECALLBACK_H diff --git a/rsc/osc/vimix.mk1.touchosc b/rsc/osc/vimix.mk1.touchosc new file mode 100644 index 0000000000000000000000000000000000000000..6719c9b6d4ec50fecd9a8c016e0e8011b04500f0 GIT binary patch literal 2306 zcmV+d3H|m^O9KQH00;;O0MD_PQvd(}000000000000{s90BLSyWq2-lZEV$B+j8PK z5dD=D@3Uh|HpW%MY$e$NlA4f(2{*r_e2Za`ZOYvF`jsuo7Xrj~EN1r!Oo4PxtJQtF zCBFT+bJ5Ml4wf_DYu3a+gxZbmnZ7mioMvtC()!Y<{rUYH()a#)mHM7iyK#NXZq{J! zX4|Ya1pK=x{v0&$8$X!Y-b!CZU!CHogO49k?fbWtZY*@izVUPy|JHKRN+08SZkDEw z?6+o32&6*SF`{?}=PbNk0{ATSwI{!UN*PLF@5 zr+=rXe)s*GiO~i=q9BcSYo?91@X2XnpgWAQcC*L4L!v3>Quu26$Pd_LtNjB19O;IQ zrufO;VK@)B;ULS|!6q;a-JCmtzxJ$VZ56EX^w`X3=GiGYUn!nrV4B;SYvac^tv%dY zZHIsZbplTjK?F|*3|@@RK;b+mEiQua$zPhUCbrX0s4a{Ul751&XnK#x@OqnMGD7c7 zx9kAZ3+&~>_m=n*-B|i)y~0z>JnSzkJEmr0r?7+Y`9p5R=({)baLaf#h}j`AyQD<4 zfSkkVPZx|%k>t0!)mA+X?9fw>x&t-1BoSaD+X+HGi+^Nrv%QNV$IdGX=nTtrc0Eq*a!IilP5f@2;9JutZ%;iA10FeWirz>;WM_iyH zaNzQAWiC6yMdT5ae_WZ%mS$=mG5PjdTr~JNoiM2fj*>f|Y@*kb$^DC1D7z=K5mN3N zt!BG@!r*lmV~#)CGg=qaBY6z$&FgNTQ%mY2*(0n$L|UXP2!uKz9lo+9oO$zvbcVBV znzJfeojcLGSKY~|zc5@g<{bDdJ=jl6uB7K<7RblnVqZDW(sYLheY`*TVjt1cJG+Z+oQmjl?OR=c&gZs`U+ z!zNW05H_dEfB5X;(;ncsEy?5#N82OQR6C< ze`+Hw;8Ut7EB^8bgjoC`!)?us_TW?PEYkRVAbLiDJj%fNwaSfT=g*szPkFuYTxEvA zyY!&{^+U?;c-r$OJ>>+GALyQQ&c^C#r$B4XZEQ@4){`if&TTwHPf(wSt;fG#?pJyq z5lIXlYRLSijytMhpoN)0q0&HctcFi+!+4yETgzkklr5#9d~PceUnpYrM5~jw_}Ojv zktR=}Wlf9*PqdeN;04gq_EBBsiPotKZMJd83GKEjw5(+cIHK)V=C|2qH7B%{`7Jvp zka?o5%x_`lfQA#=%KTQMM`JuA*mGrmn^iuX(0m5TB$ z?6OW0RX8w26?Ow9s&VQ^=q}j|i`$i!vfG8Te>F#g?_=vu;Bb%2ignx%BfCwa7M2?1 zC^}T~=j6Mi$*h-fcoWQod{~(ilqcdcuX|#n0&acKt0{=IW z7)?eyQDae90(q9>Y***E5Q$TXb4&uLC2~ohLoN~1F$s$NV$y>(8oual-q@{$7? zatc3k104%m6HGToyLYuhrh6popjbwr{JQ0!ot}?m9kYEgXhSa0jsr@phUO2W}M*{DCW9FjUf+XO*KcKJ15eJ#$VuDu6qAgU2mcREv7gu;nZa-$w zz<1rY6Kfmydn-S_9<*Dc3Fs9*@xNgywr(Q3ZgbXD?|Ql^Lor^|P+~QkR7h&+PoV41 zHCP!k6W*a%;zB9AgitNk7XyNDv^%TU<#uPjONU^I)=~=l+hZdEYXtr_C1bxxC;n{u zK?dv@u)DC@Xp=r(_zl zm{TDzavI=di=L86Cy!Rz7C93r-iH(7zKtRrC3yK9my@w=K78p9KXoE-RMmv_cw5_; zo;5XH8cBexW|M(jHW}ozVxIqj#0M(>D`!ZKU%ryR<{9000000000000000003!jWMz0R ccx`M@O9ci1000010096u0002B2mk;80Kx}f$N&HU literal 0 HcmV?d00001 diff --git a/rsc/osc/vimix.tosc b/rsc/osc/vimix.tosc index b583d5396c9bd57e85cd14149898d96a401a2b2a..f02fb437592e8bb656ad6bfeb117a220bd2b6f8d 100644 GIT binary patch delta 3463 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 w8tC=yz|pkUt<8