The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

412 lines
16KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 7 End-User License
  8. Agreement and JUCE Privacy Policy.
  9. End User License Agreement: www.juce.com/juce-7-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. namespace juce::midi_ci
  19. {
  20. class PropertyHost::Visitor : public detail::MessageTypeUtils::MessageVisitor
  21. {
  22. public:
  23. Visitor (PropertyHost* h, ResponderOutput* o, bool* b)
  24. : host (h), output (o), handled (b) {}
  25. void visit (const Message::PropertyExchangeCapabilities& body) const override { visitImpl (body); }
  26. void visit (const Message::PropertyGetData& body) const override { visitImpl (body); }
  27. void visit (const Message::PropertySetData& body) const override { visitImpl (body); }
  28. void visit (const Message::PropertySubscribe& body) const override { visitImpl (body); }
  29. void visit (const Message::PropertyNotify& body) const override { visitImpl (body); }
  30. using MessageVisitor::visit;
  31. private:
  32. template <typename Body>
  33. void visitImpl (const Body& body) const { *handled = messageReceived (body); }
  34. bool messageReceived (const Message::PropertyExchangeCapabilities&) const
  35. {
  36. detail::MessageTypeUtils::send (*output, Message::PropertyExchangeCapabilitiesResponse { std::byte { host->delegate.getNumSimultaneousRequestsSupported() },
  37. {},
  38. {} });
  39. return true;
  40. }
  41. bool messageReceived (const Message::PropertyGetData& data) const
  42. {
  43. // This should always be a single message, so no need to accumulate chunks
  44. const auto reply = host->delegate.propertyGetDataRequested (output->getIncomingHeader().source,
  45. PropertyRequestHeader::parseCondensed (Encodings::jsonFrom7BitText (data.header)));
  46. const auto encoded = Encodings::tryEncode (reply.body, reply.header.mutualEncoding);
  47. if (! encoded.has_value())
  48. {
  49. // If this is hit, the data that was supplied isn't valid for the encoding that was specified
  50. jassertfalse;
  51. return false;
  52. }
  53. detail::PropertyHostUtils::send (*output,
  54. output->getIncomingGroup(),
  55. detail::MessageMeta::Meta<Message::PropertyGetDataResponse>::subID2,
  56. output->getIncomingHeader().source,
  57. data.requestID,
  58. Encodings::jsonTo7BitText (reply.header.toVarCondensed()),
  59. *encoded,
  60. host->cacheProvider.getMaxSysexSizeForMuid (output->getIncomingHeader().source));
  61. return true;
  62. }
  63. bool messageReceived (const Message::PropertySetData& data) const
  64. {
  65. auto* caches = host->cacheProvider.getCacheForMuidAsResponder (output->getIncomingHeader().source);
  66. if (caches == nullptr)
  67. return false;
  68. const auto source = output->getIncomingHeader().source;
  69. const auto dest = output->getIncomingHeader().destination;
  70. const auto group = output->getIncomingGroup();
  71. const auto request = data.requestID;
  72. caches->primeCache (host->delegate.getNumSimultaneousRequestsSupported(), [this, source, dest, group, request] (const PropertyExchangeResult& result)
  73. {
  74. const auto send = [&] (const PropertyReplyHeader& header)
  75. {
  76. detail::MessageTypeUtils::send (host->output,
  77. group,
  78. Message::Header { ChannelInGroup::wholeBlock,
  79. detail::MessageMeta::Meta<Message::PropertySetDataResponse>::subID2,
  80. detail::MessageMeta::implementationVersion,
  81. dest,
  82. source },
  83. Message::PropertySetDataResponse { { request, Encodings::jsonTo7BitText (header.toVarCondensed()) } });
  84. };
  85. if (result.getError() == PropertyExchangeResult::Error::tooManyTransactions)
  86. {
  87. PropertyReplyHeader header;
  88. header.status = 343;
  89. header.message = TRANS ("The device has initiated too many simultaneous requests");
  90. send (header);
  91. return;
  92. }
  93. if (result.getError().has_value())
  94. {
  95. PropertyReplyHeader header;
  96. header.status = 400;
  97. header.message = TRANS ("Request was incomplete");
  98. send (header);
  99. return;
  100. }
  101. send (host->delegate.propertySetDataRequested (source, { result.getHeaderAsRequestHeader(), result.getBody() }));
  102. }, request);
  103. caches->addChunk (data.requestID, data);
  104. return true;
  105. }
  106. bool messageReceived (const Message::PropertySubscribe& data) const
  107. {
  108. auto* caches = host->cacheProvider.getCacheForMuidAsResponder (output->getIncomingHeader().source);
  109. if (caches == nullptr)
  110. return false;
  111. if (data.header.empty() || data.thisChunkNum != 1 || data.totalNumChunks != 1)
  112. return false;
  113. const auto subHeader = PropertySubscriptionHeader::parseCondensed (Encodings::jsonFrom7BitText (data.header));
  114. const auto tryNotifyInitiator = subHeader.command == PropertySubscriptionCommand::start
  115. || subHeader.command == PropertySubscriptionCommand::end;
  116. if (! tryNotifyInitiator)
  117. return false;
  118. const auto source = output->getIncomingHeader().source;
  119. const auto sendResponse = [&] (const PropertyReplyHeader& header)
  120. {
  121. detail::PropertyHostUtils::send (*output,
  122. output->getIncomingGroup(),
  123. detail::MessageMeta::Meta<Message::PropertySubscribeResponse>::subID2,
  124. source,
  125. data.requestID,
  126. Encodings::jsonTo7BitText (header.toVarCondensed()),
  127. {},
  128. host->cacheProvider.getMaxSysexSizeForMuid (source));
  129. };
  130. if (subHeader.command == PropertySubscriptionCommand::start)
  131. {
  132. if (host->delegate.subscriptionStartRequested (source, subHeader))
  133. {
  134. auto& currentSubscribeIds = host->registry[source];
  135. const auto newToken = findUnusedSubscribeId (currentSubscribeIds);
  136. [[maybe_unused]] const auto pair = currentSubscribeIds.emplace (newToken, subHeader.resource);
  137. jassert (pair.second);
  138. const auto subscribeId = subscribeIdFromUid (newToken);
  139. host->delegate.subscriptionDidStart (source, subscribeId, subHeader);
  140. PropertyReplyHeader header;
  141. header.extended["subscribeId"] = subscribeId;
  142. sendResponse (header);
  143. }
  144. else
  145. {
  146. PropertyReplyHeader header;
  147. header.status = 405;
  148. sendResponse (header);
  149. }
  150. return true;
  151. }
  152. if (subHeader.command == PropertySubscriptionCommand::end)
  153. {
  154. const auto token = uidFromSubscribeId (subHeader.subscribeId);
  155. auto& currentSubscribeIds = host->registry[source];
  156. const auto iter = currentSubscribeIds.find (token);
  157. if (iter != currentSubscribeIds.end())
  158. {
  159. host->delegate.subscriptionWillEnd (source, { subHeader.subscribeId, iter->second });
  160. currentSubscribeIds.erase (iter);
  161. sendResponse ({});
  162. return true;
  163. }
  164. return false;
  165. }
  166. return false;
  167. }
  168. bool messageReceived (const Message::PropertyNotify& n) const
  169. {
  170. const auto m = output->getIncomingHeader().source;
  171. if (auto* it = host->cacheProvider.getCacheForMuidAsResponder (m))
  172. it->notify (n.requestID, n.header);
  173. if (auto* it = host->cacheProvider.getCacheForMuidAsInitiator (m))
  174. it->notify (n.requestID, n.header);
  175. return true;
  176. }
  177. PropertyHost* host = nullptr;
  178. ResponderOutput* output = nullptr;
  179. bool* handled = nullptr;
  180. };
  181. //==============================================================================
  182. std::set<Subscription> PropertyHost::findSubscriptionsForDevice (MUID device) const
  183. {
  184. const auto iter = registry.find (device);
  185. if (iter == registry.end())
  186. return {};
  187. std::set<Subscription> result;
  188. for (const auto& [subId, resource] : iter->second)
  189. {
  190. [[maybe_unused]] const auto pair = result.insert ({ subscribeIdFromUid (subId), resource });
  191. jassert (pair.second);
  192. }
  193. return result;
  194. }
  195. int PropertyHost::countOngoingTransactions() const
  196. {
  197. const auto muids = cacheProvider.getDiscoveredMuids();
  198. return std::accumulate (muids.begin(), muids.end(), 0, [&] (auto acc, const auto& m)
  199. {
  200. if (auto* cache = cacheProvider.getCacheForMuidAsResponder (m))
  201. return acc + cache->countOngoingTransactions();
  202. return acc;
  203. });
  204. }
  205. bool PropertyHost::tryRespond (ResponderOutput& responderOutput, const Message::Parsed& message)
  206. {
  207. bool result = false;
  208. detail::MessageTypeUtils::visit (message, Visitor { this, &responderOutput, &result });
  209. return result;
  210. }
  211. ErasedScopeGuard PropertyHost::sendSubscriptionUpdate (MUID device,
  212. const PropertySubscriptionHeader& header,
  213. Span<const std::byte> body,
  214. std::function<void (const PropertyExchangeResult&)> cb)
  215. {
  216. const auto deviceIter = registry.find (device);
  217. if (deviceIter == registry.end())
  218. {
  219. // That device doesn't have any active subscriptions
  220. jassertfalse;
  221. return {};
  222. }
  223. const auto uid = uidFromSubscribeId (header.subscribeId);
  224. const auto subIter = deviceIter->second.find (uid);
  225. if (subIter == deviceIter->second.end())
  226. {
  227. // That subscribeId isn't currently in use by that device
  228. jassertfalse;
  229. return {};
  230. }
  231. const auto resource = subIter->second;
  232. if (header.resource != resource)
  233. {
  234. // That subscribeId corresponds to a different resource
  235. jassertfalse;
  236. return {};
  237. }
  238. if (header.command == PropertySubscriptionCommand::start)
  239. {
  240. // This function is intended to update ongoing subscriptions. To start a new subscription,
  241. // use CIDevice.
  242. jassertfalse;
  243. return {};
  244. }
  245. auto* caches = cacheProvider.getCacheForMuidAsInitiator (device);
  246. if (caches == nullptr)
  247. return {};
  248. const auto terminator = detail::PropertyHostUtils::getTerminator (output, functionBlock, device);
  249. auto wrappedCallback = [&]() -> std::function<void (const PropertyExchangeResult&)>
  250. {
  251. if (header.command != PropertySubscriptionCommand::end)
  252. return cb;
  253. return [this, device, uid, resource, cb] (const PropertyExchangeResult& result)
  254. {
  255. if (! result.getError().has_value())
  256. {
  257. delegate.subscriptionWillEnd (device, { subscribeIdFromUid (uid), resource });
  258. registry[device].erase (uid);
  259. }
  260. NullCheckedInvocation::invoke (cb, result);
  261. };
  262. }();
  263. const auto encoded = Encodings::tryEncode (body, header.mutualEncoding);
  264. if (! encoded.has_value())
  265. {
  266. NullCheckedInvocation::invoke (wrappedCallback, PropertyExchangeResult { PropertyExchangeResult::Error::invalidPayload });
  267. return {};
  268. }
  269. auto primed = caches->primeCache (delegate.getNumSimultaneousRequestsSupported(),
  270. std::move (wrappedCallback),
  271. std::move (terminator));
  272. if (! primed.isValid())
  273. return {};
  274. detail::PropertyHostUtils::send (output,
  275. functionBlock.firstGroup,
  276. detail::MessageMeta::Meta<Message::PropertySubscribe>::subID2,
  277. device,
  278. primed.id,
  279. Encodings::jsonTo7BitText (header.toVarCondensed()),
  280. *encoded,
  281. cacheProvider.getMaxSysexSizeForMuid (device));
  282. return std::move (primed.token);
  283. }
  284. void PropertyHost::terminateSubscription (MUID device, const String& subscribeId)
  285. {
  286. const auto deviceIter = registry.find (device);
  287. if (deviceIter == registry.end())
  288. {
  289. // That device doesn't have any active subscriptions
  290. jassertfalse;
  291. return;
  292. }
  293. const auto uid = uidFromSubscribeId (subscribeId);
  294. const auto subIter = deviceIter->second.find (uid);
  295. if (subIter == deviceIter->second.end())
  296. {
  297. // That subscribeId isn't currently in use by that device
  298. jassertfalse;
  299. return;
  300. }
  301. PropertySubscriptionHeader header;
  302. header.command = PropertySubscriptionCommand::end;
  303. header.subscribeId = subscribeId;
  304. header.resource = subIter->second;
  305. sendSubscriptionUpdate (device, header, {}, nullptr).release();
  306. }
  307. PropertyHost::SubscriptionToken PropertyHost::uidFromSubscribeId (String id)
  308. {
  309. try
  310. {
  311. // from_chars would be better once we no longer need to support older macOS
  312. return { (size_t) std::stoull (id.toStdString(), {}, 36) };
  313. }
  314. catch (...) {}
  315. jassertfalse;
  316. return {};
  317. }
  318. String PropertyHost::subscribeIdFromUid (SubscriptionToken uid)
  319. {
  320. const auto str = std::to_string (uid.uid);
  321. jassert (str.size() <= 8);
  322. return str;
  323. }
  324. PropertyHost::SubscriptionToken PropertyHost::findUnusedSubscribeId (const std::map<SubscriptionToken, String>& used)
  325. {
  326. return ! used.empty() ? SubscriptionToken { std::prev (used.end())->first.uid + 1 } : SubscriptionToken { 0 };
  327. }
  328. } // namespace juce::midi_ci