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.

352 lines
13KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE examples.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. The code included in this file is provided under the terms of the ISC license
  6. http://www.isc.org/downloads/software-support-policy/isc-license. Permission
  7. To use, copy, modify, and/or distribute this software for any purpose with or
  8. without fee is hereby granted provided that the above copyright notice and
  9. this permission notice appear in all copies.
  10. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
  11. WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
  12. PURPOSE, ARE DISCLAIMED.
  13. ==============================================================================
  14. */
  15. /*******************************************************************************
  16. The block below describes the properties of this PIP. A PIP is a short snippet
  17. of code that can be read by the Projucer and used to generate a JUCE project.
  18. BEGIN_JUCE_PIP_METADATA
  19. name: MIDILogger
  20. version: 1.0.0
  21. vendor: JUCE
  22. website: http://juce.com
  23. description: Logs incoming MIDI messages.
  24. dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
  25. juce_audio_plugin_client, juce_audio_processors,
  26. juce_audio_utils, juce_core, juce_data_structures,
  27. juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
  28. exporters: xcode_mac, vs2022, linux_make
  29. moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
  30. type: AudioProcessor
  31. mainClass: MidiLoggerPluginDemoProcessor
  32. useLocalCopy: 1
  33. pluginCharacteristics: pluginWantsMidiIn, pluginProducesMidiOut, pluginIsMidiEffectPlugin
  34. END_JUCE_PIP_METADATA
  35. *******************************************************************************/
  36. #pragma once
  37. #include <iterator>
  38. class MidiQueue
  39. {
  40. public:
  41. void push (const MidiBuffer& buffer)
  42. {
  43. for (const auto metadata : buffer)
  44. fifo.write (1).forEach ([&] (int dest) { messages[(size_t) dest] = metadata.getMessage(); });
  45. }
  46. template <typename OutputIt>
  47. void pop (OutputIt out)
  48. {
  49. fifo.read (fifo.getNumReady()).forEach ([&] (int source) { *out++ = messages[(size_t) source]; });
  50. }
  51. private:
  52. static constexpr auto queueSize = 1 << 14;
  53. AbstractFifo fifo { queueSize };
  54. std::vector<MidiMessage> messages = std::vector<MidiMessage> (queueSize);
  55. };
  56. // Stores the last N messages. Safe to access from the message thread only.
  57. class MidiListModel
  58. {
  59. public:
  60. template <typename It>
  61. void addMessages (It begin, It end)
  62. {
  63. if (begin == end)
  64. return;
  65. const auto numNewMessages = (int) std::distance (begin, end);
  66. const auto numToAdd = juce::jmin (numToStore, numNewMessages);
  67. const auto numToRemove = jmax (0, (int) messages.size() + numToAdd - numToStore);
  68. messages.erase (messages.begin(), std::next (messages.begin(), numToRemove));
  69. messages.insert (messages.end(), std::prev (end, numToAdd), end);
  70. if (onChange != nullptr)
  71. onChange();
  72. }
  73. void clear()
  74. {
  75. messages.clear();
  76. if (onChange != nullptr)
  77. onChange();
  78. }
  79. const MidiMessage& operator[] (size_t ind) const { return messages[ind]; }
  80. size_t size() const { return messages.size(); }
  81. std::function<void()> onChange;
  82. private:
  83. static constexpr auto numToStore = 1000;
  84. std::vector<MidiMessage> messages;
  85. };
  86. //==============================================================================
  87. class MidiTable : public Component,
  88. private TableListBoxModel
  89. {
  90. public:
  91. MidiTable (MidiListModel& m)
  92. : messages (m)
  93. {
  94. addAndMakeVisible (table);
  95. table.setModel (this);
  96. table.setClickingTogglesRowSelection (false);
  97. table.setHeader ([&]
  98. {
  99. auto header = std::make_unique<TableHeaderComponent>();
  100. header->addColumn ("Message", messageColumn, 200, 30, -1, TableHeaderComponent::notSortable);
  101. header->addColumn ("Time", timeColumn, 100, 30, -1, TableHeaderComponent::notSortable);
  102. header->addColumn ("Channel", channelColumn, 100, 30, -1, TableHeaderComponent::notSortable);
  103. header->addColumn ("Data", dataColumn, 200, 30, -1, TableHeaderComponent::notSortable);
  104. return header;
  105. }());
  106. messages.onChange = [&] { table.updateContent(); };
  107. }
  108. ~MidiTable() override { messages.onChange = nullptr; }
  109. void resized() override { table.setBounds (getLocalBounds()); }
  110. private:
  111. enum
  112. {
  113. messageColumn = 1,
  114. timeColumn,
  115. channelColumn,
  116. dataColumn
  117. };
  118. int getNumRows() override { return (int) messages.size(); }
  119. void paintRowBackground (Graphics&, int, int, int, bool) override {}
  120. void paintCell (Graphics&, int, int, int, int, bool) override {}
  121. Component* refreshComponentForCell (int rowNumber,
  122. int columnId,
  123. bool,
  124. Component* existingComponentToUpdate) override
  125. {
  126. delete existingComponentToUpdate;
  127. const auto index = (int) messages.size() - 1 - rowNumber;
  128. const auto message = messages[(size_t) index];
  129. return new Label ({}, [&]
  130. {
  131. switch (columnId)
  132. {
  133. case messageColumn: return getEventString (message);
  134. case timeColumn: return String (message.getTimeStamp());
  135. case channelColumn: return String (message.getChannel());
  136. case dataColumn: return getDataString (message);
  137. default: break;
  138. }
  139. jassertfalse;
  140. return String();
  141. }());
  142. }
  143. static String getEventString (const MidiMessage& m)
  144. {
  145. if (m.isNoteOn()) return "Note on";
  146. if (m.isNoteOff()) return "Note off";
  147. if (m.isProgramChange()) return "Program change";
  148. if (m.isPitchWheel()) return "Pitch wheel";
  149. if (m.isAftertouch()) return "Aftertouch";
  150. if (m.isChannelPressure()) return "Channel pressure";
  151. if (m.isAllNotesOff()) return "All notes off";
  152. if (m.isAllSoundOff()) return "All sound off";
  153. if (m.isMetaEvent()) return "Meta event";
  154. if (m.isController())
  155. {
  156. const auto* name = MidiMessage::getControllerName (m.getControllerNumber());
  157. return "Controller " + (name == nullptr ? String (m.getControllerNumber()) : String (name));
  158. }
  159. return String::toHexString (m.getRawData(), m.getRawDataSize());
  160. }
  161. static String getDataString (const MidiMessage& m)
  162. {
  163. if (m.isNoteOn()) return MidiMessage::getMidiNoteName (m.getNoteNumber(), true, true, 3) + " Velocity " + String (m.getVelocity());
  164. if (m.isNoteOff()) return MidiMessage::getMidiNoteName (m.getNoteNumber(), true, true, 3) + " Velocity " + String (m.getVelocity());
  165. if (m.isProgramChange()) return String (m.getProgramChangeNumber());
  166. if (m.isPitchWheel()) return String (m.getPitchWheelValue());
  167. if (m.isAftertouch()) return MidiMessage::getMidiNoteName (m.getNoteNumber(), true, true, 3) + ": " + String (m.getAfterTouchValue());
  168. if (m.isChannelPressure()) return String (m.getChannelPressureValue());
  169. if (m.isController()) return String (m.getControllerValue());
  170. return {};
  171. }
  172. MidiListModel& messages;
  173. TableListBox table;
  174. };
  175. //==============================================================================
  176. class MidiLoggerPluginDemoProcessor : public AudioProcessor,
  177. private Timer
  178. {
  179. public:
  180. MidiLoggerPluginDemoProcessor()
  181. : AudioProcessor (getBusesLayout())
  182. {
  183. state.addChild ({ "uiState", { { "width", 600 }, { "height", 300 } }, {} }, -1, nullptr);
  184. startTimerHz (60);
  185. }
  186. ~MidiLoggerPluginDemoProcessor() override { stopTimer(); }
  187. void processBlock (AudioBuffer<float>& audio, MidiBuffer& midi) override { process (audio, midi); }
  188. void processBlock (AudioBuffer<double>& audio, MidiBuffer& midi) override { process (audio, midi); }
  189. bool isBusesLayoutSupported (const BusesLayout&) const override { return true; }
  190. bool isMidiEffect() const override { return true; }
  191. bool hasEditor() const override { return true; }
  192. AudioProcessorEditor* createEditor() override { return new Editor (*this); }
  193. const String getName() const override { return "MIDI Logger"; }
  194. bool acceptsMidi() const override { return true; }
  195. bool producesMidi() const override { return true; }
  196. double getTailLengthSeconds() const override { return 0.0; }
  197. int getNumPrograms() override { return 0; }
  198. int getCurrentProgram() override { return 0; }
  199. void setCurrentProgram (int) override {}
  200. const String getProgramName (int) override { return "None"; }
  201. void changeProgramName (int, const String&) override {}
  202. void prepareToPlay (double, int) override {}
  203. void releaseResources() override {}
  204. void getStateInformation (MemoryBlock& destData) override
  205. {
  206. if (auto xmlState = state.createXml())
  207. copyXmlToBinary (*xmlState, destData);
  208. }
  209. void setStateInformation (const void* data, int size) override
  210. {
  211. if (auto xmlState = getXmlFromBinary (data, size))
  212. state = ValueTree::fromXml (*xmlState);
  213. }
  214. private:
  215. class Editor : public AudioProcessorEditor,
  216. private Value::Listener
  217. {
  218. public:
  219. explicit Editor (MidiLoggerPluginDemoProcessor& ownerIn)
  220. : AudioProcessorEditor (ownerIn),
  221. owner (ownerIn),
  222. table (owner.model)
  223. {
  224. addAndMakeVisible (table);
  225. addAndMakeVisible (clearButton);
  226. setResizable (true, true);
  227. lastUIWidth .referTo (owner.state.getChildWithName ("uiState").getPropertyAsValue ("width", nullptr));
  228. lastUIHeight.referTo (owner.state.getChildWithName ("uiState").getPropertyAsValue ("height", nullptr));
  229. setSize (lastUIWidth.getValue(), lastUIHeight.getValue());
  230. lastUIWidth. addListener (this);
  231. lastUIHeight.addListener (this);
  232. clearButton.onClick = [&] { owner.model.clear(); };
  233. }
  234. void paint (Graphics& g) override
  235. {
  236. g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
  237. }
  238. void resized() override
  239. {
  240. auto bounds = getLocalBounds();
  241. clearButton.setBounds (bounds.removeFromBottom (30).withSizeKeepingCentre (50, 24));
  242. table.setBounds (bounds);
  243. lastUIWidth = getWidth();
  244. lastUIHeight = getHeight();
  245. }
  246. private:
  247. void valueChanged (Value&) override
  248. {
  249. setSize (lastUIWidth.getValue(), lastUIHeight.getValue());
  250. }
  251. MidiLoggerPluginDemoProcessor& owner;
  252. MidiTable table;
  253. TextButton clearButton { "Clear" };
  254. Value lastUIWidth, lastUIHeight;
  255. };
  256. void timerCallback() override
  257. {
  258. std::vector<MidiMessage> messages;
  259. queue.pop (std::back_inserter (messages));
  260. model.addMessages (messages.begin(), messages.end());
  261. }
  262. template <typename Element>
  263. void process (AudioBuffer<Element>& audio, MidiBuffer& midi)
  264. {
  265. audio.clear();
  266. queue.push (midi);
  267. }
  268. static BusesProperties getBusesLayout()
  269. {
  270. // Live doesn't like to load midi-only plugins, so we add an audio output there.
  271. return PluginHostType().isAbletonLive() ? BusesProperties().withOutput ("out", AudioChannelSet::stereo())
  272. : BusesProperties();
  273. }
  274. ValueTree state { "state" };
  275. MidiQueue queue;
  276. MidiListModel model; // The data to show in the UI. We keep it around in the processor so that
  277. // the view is persistent even when the plugin UI is closed and reopened.
  278. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiLoggerPluginDemoProcessor)
  279. };