1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE examples.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    The code included in this file is provided under the terms of the ISC license
8    http://www.isc.org/downloads/software-support-policy/isc-license. Permission
9    To use, copy, modify, and/or distribute this software for any purpose with or
10    without fee is hereby granted provided that the above copyright notice and
11    this permission notice appear in all copies.
12 
13    THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
14    WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
15    PURPOSE, ARE DISCLAIMED.
16 
17   ==============================================================================
18 */
19 
20 /*******************************************************************************
21  The block below describes the properties of this PIP. A PIP is a short snippet
22  of code that can be read by the Projucer and used to generate a JUCE project.
23 
24  BEGIN_JUCE_PIP_METADATA
25 
26  name:             OSCDemo
27  version:          1.0.0
28  vendor:           JUCE
29  website:          http://juce.com
30  description:      Application using the OSC protocol.
31 
32  dependencies:     juce_core, juce_data_structures, juce_events, juce_graphics,
33                    juce_gui_basics, juce_osc
34  exporters:        xcode_mac, vs2019, linux_make
35 
36  moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1
37 
38  type:             Component
39  mainClass:        OSCDemo
40 
41  useLocalCopy:     1
42 
43  END_JUCE_PIP_METADATA
44 
45 *******************************************************************************/
46 
47 #pragma once
48 
49 
50 //==============================================================================
51 class OSCLogListBox    : public ListBox,
52                          private ListBoxModel,
53                          private AsyncUpdater
54 {
55 public:
OSCLogListBox()56     OSCLogListBox()
57     {
58         setModel (this);
59     }
60 
61     ~OSCLogListBox() override = default;
62 
63     //==============================================================================
getNumRows()64     int getNumRows() override
65     {
66         return oscLogList.size();
67     }
68 
69     //==============================================================================
paintListBoxItem(int row,Graphics & g,int width,int height,bool rowIsSelected)70     void paintListBoxItem (int row, Graphics& g, int width, int height, bool rowIsSelected) override
71     {
72         ignoreUnused (rowIsSelected);
73 
74         if (isPositiveAndBelow (row, oscLogList.size()))
75         {
76             g.setColour (Colours::white);
77 
78             g.drawText (oscLogList[row],
79                         Rectangle<int> (width, height).reduced (4, 0),
80                         Justification::centredLeft, true);
81         }
82     }
83 
84     //==============================================================================
85     void addOSCMessage (const OSCMessage& message, int level = 0)
86     {
87         oscLogList.add (getIndentationString (level)
88                         + "- osc message, address = '"
89                         + message.getAddressPattern().toString()
90                         + "', "
91                         + String (message.size())
92                         + " argument(s)");
93 
94         if (! message.isEmpty())
95         {
96             for (auto& arg : message)
97                 addOSCMessageArgument (arg, level + 1);
98         }
99 
100         triggerAsyncUpdate();
101     }
102 
103     //==============================================================================
104     void addOSCBundle (const OSCBundle& bundle, int level = 0)
105     {
106         OSCTimeTag timeTag = bundle.getTimeTag();
107 
108         oscLogList.add (getIndentationString (level)
109                         + "- osc bundle, time tag = "
110                         + timeTag.toTime().toString (true, true, true, true));
111 
112         for (auto& element : bundle)
113         {
114             if (element.isMessage())
115                 addOSCMessage (element.getMessage(), level + 1);
116             else if (element.isBundle())
117                 addOSCBundle (element.getBundle(), level + 1);
118         }
119 
120         triggerAsyncUpdate();
121     }
122 
123     //==============================================================================
addOSCMessageArgument(const OSCArgument & arg,int level)124     void addOSCMessageArgument (const OSCArgument& arg, int level)
125     {
126         String typeAsString;
127         String valueAsString;
128 
129         if (arg.isFloat32())
130         {
131             typeAsString = "float32";
132             valueAsString = String (arg.getFloat32());
133         }
134         else if (arg.isInt32())
135         {
136             typeAsString = "int32";
137             valueAsString = String (arg.getInt32());
138         }
139         else if (arg.isString())
140         {
141             typeAsString = "string";
142             valueAsString = arg.getString();
143         }
144         else if (arg.isBlob())
145         {
146             typeAsString = "blob";
147             auto& blob = arg.getBlob();
148             valueAsString = String::fromUTF8 ((const char*) blob.getData(), (int) blob.getSize());
149         }
150         else
151         {
152             typeAsString = "(unknown)";
153         }
154 
155         oscLogList.add (getIndentationString (level + 1) + "- " + typeAsString.paddedRight(' ', 12) + valueAsString);
156     }
157 
158     //==============================================================================
addInvalidOSCPacket(const char *,int dataSize)159     void addInvalidOSCPacket (const char* /* data */, int dataSize)
160     {
161         oscLogList.add ("- (" + String(dataSize) + "bytes with invalid format)");
162     }
163 
164     //==============================================================================
clear()165     void clear()
166     {
167         oscLogList.clear();
168         triggerAsyncUpdate();
169     }
170 
171     //==============================================================================
handleAsyncUpdate()172     void handleAsyncUpdate() override
173     {
174         updateContent();
175         scrollToEnsureRowIsOnscreen (oscLogList.size() - 1);
176         repaint();
177     }
178 
179 private:
getIndentationString(int level)180     static String getIndentationString (int level)
181     {
182         return String().paddedRight (' ', 2 * level);
183     }
184 
185     //==============================================================================
186     StringArray oscLogList;
187 
188     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OSCLogListBox)
189 };
190 
191 //==============================================================================
192 class OSCSenderDemo   : public Component
193 {
194 public:
OSCSenderDemo()195     OSCSenderDemo()
196     {
197         addAndMakeVisible (senderLabel);
198         senderLabel.attachToComponent (&rotaryKnob, false);
199 
200         rotaryKnob.setRange (0.0, 1.0);
201         rotaryKnob.setSliderStyle (Slider::RotaryVerticalDrag);
202         rotaryKnob.setTextBoxStyle (Slider::TextBoxBelow, true, 150, 25);
203         rotaryKnob.setBounds (50, 50, 180, 180);
204         addAndMakeVisible (rotaryKnob);
205         rotaryKnob.onValueChange = [this]
206         {
207             // create and send an OSC message with an address and a float value:
208             if (! sender1.send ("/juce/rotaryknob", (float) rotaryKnob.getValue()))
209                 showConnectionErrorMessage ("Error: could not send OSC message.");
210             if (! sender2.send ("/juce/rotaryknob", (float) rotaryKnob.getValue()))
211                 showConnectionErrorMessage ("Error: could not send OSC message.");
212         };
213 
214         // specify here where to send OSC messages to: host URL and UDP port number
215         if (! sender1.connect ("127.0.0.1", 9001))
216             showConnectionErrorMessage ("Error: could not connect to UDP port 9001.");
217         if (! sender2.connect ("127.0.0.1", 9002))
218             showConnectionErrorMessage ("Error: could not connect to UDP port 9002.");
219     }
220 
221 private:
222     //==============================================================================
showConnectionErrorMessage(const String & messageText)223     void showConnectionErrorMessage (const String& messageText)
224     {
225         AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
226                                           "Connection error",
227                                           messageText,
228                                           "OK");
229     }
230 
231     //==============================================================================
232     Slider rotaryKnob;
233     OSCSender sender1, sender2;
234     Label senderLabel { {}, "Sender" };
235 
236     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OSCSenderDemo)
237 };
238 
239 //==============================================================================
240 class OSCReceiverDemo   : public Component,
241                           private OSCReceiver,
242                           private OSCReceiver::ListenerWithOSCAddress<OSCReceiver::MessageLoopCallback>
243 {
244 public:
245     //==============================================================================
OSCReceiverDemo()246     OSCReceiverDemo()
247     {
248         addAndMakeVisible (receiverLabel);
249         receiverLabel.attachToComponent (&rotaryKnob, false);
250 
251         rotaryKnob.setRange (0.0, 1.0);
252         rotaryKnob.setSliderStyle (Slider::RotaryVerticalDrag);
253         rotaryKnob.setTextBoxStyle (Slider::TextBoxBelow, true, 150, 25);
254         rotaryKnob.setBounds (50, 50, 180, 180);
255         rotaryKnob.setInterceptsMouseClicks (false, false);
256         addAndMakeVisible (rotaryKnob);
257 
258         // specify here on which UDP port number to receive incoming OSC messages
259         if (! connect (9001))
260             showConnectionErrorMessage ("Error: could not connect to UDP port 9001.");
261 
262         // tell the component to listen for OSC messages matching this address:
263         addListener (this, "/juce/rotaryknob");
264     }
265 
266 private:
267     //==============================================================================
oscMessageReceived(const OSCMessage & message)268     void oscMessageReceived (const OSCMessage& message) override
269     {
270         if (message.size() == 1 && message[0].isFloat32())
271             rotaryKnob.setValue (jlimit (0.0f, 10.0f, message[0].getFloat32()));
272     }
273 
showConnectionErrorMessage(const String & messageText)274     void showConnectionErrorMessage (const String& messageText)
275     {
276         AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
277                                           "Connection error",
278                                           messageText,
279                                           "OK");
280     }
281 
282     //==============================================================================
283     Slider rotaryKnob;
284     Label receiverLabel { {}, "Receiver" };
285 
286     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OSCReceiverDemo)
287 };
288 
289 //==============================================================================
290 class OSCMonitorDemo   : public Component,
291                          private OSCReceiver::Listener<OSCReceiver::MessageLoopCallback>
292 {
293 public:
294     //==============================================================================
OSCMonitorDemo()295     OSCMonitorDemo()
296     {
297         portNumberLabel.setBounds (10, 18, 130, 25);
298         addAndMakeVisible (portNumberLabel);
299 
300         portNumberField.setEditable (true, true, true);
301         portNumberField.setBounds (140, 18, 50, 25);
302         addAndMakeVisible (portNumberField);
303 
304         connectButton.setBounds (210, 18, 100, 25);
305         addAndMakeVisible (connectButton);
306         connectButton.onClick = [this] { connectButtonClicked(); };
307 
308         clearButton.setBounds (320, 18, 60, 25);
309         addAndMakeVisible (clearButton);
310         clearButton.onClick = [this] { clearButtonClicked(); };
311 
312         connectionStatusLabel.setBounds (450, 18, 240, 25);
313         updateConnectionStatusLabel();
314         addAndMakeVisible (connectionStatusLabel);
315 
316         oscLogListBox.setBounds (0, 60, 700, 340);
317         addAndMakeVisible (oscLogListBox);
318 
319         oscReceiver.addListener (this);
320         oscReceiver.registerFormatErrorHandler ([this] (const char* data, int dataSize)
321                                                 {
322                                                     oscLogListBox.addInvalidOSCPacket (data, dataSize);
323                                                 });
324     }
325 
326 private:
327     //==============================================================================
328     Label portNumberLabel    { {}, "UDP Port Number: " };
329     Label portNumberField    { {}, "9002" };
330     TextButton connectButton { "Connect" };
331     TextButton clearButton   { "Clear" };
332     Label connectionStatusLabel;
333 
334     OSCLogListBox oscLogListBox;
335     OSCReceiver oscReceiver;
336 
337     int currentPortNumber = -1;
338 
339     //==============================================================================
connectButtonClicked()340     void connectButtonClicked()
341     {
342         if (! isConnected())
343             connect();
344         else
345             disconnect();
346 
347         updateConnectionStatusLabel();
348     }
349 
350     //==============================================================================
clearButtonClicked()351     void clearButtonClicked()
352     {
353         oscLogListBox.clear();
354     }
355 
356     //==============================================================================
oscMessageReceived(const OSCMessage & message)357     void oscMessageReceived (const OSCMessage& message) override
358     {
359         oscLogListBox.addOSCMessage (message);
360     }
361 
oscBundleReceived(const OSCBundle & bundle)362     void oscBundleReceived (const OSCBundle& bundle) override
363     {
364         oscLogListBox.addOSCBundle (bundle);
365     }
366 
367     //==============================================================================
connect()368     void connect()
369     {
370         auto portToConnect = portNumberField.getText().getIntValue();
371 
372         if (! isValidOscPort (portToConnect))
373         {
374             handleInvalidPortNumberEntered();
375             return;
376         }
377 
378         if (oscReceiver.connect (portToConnect))
379         {
380             currentPortNumber = portToConnect;
381             connectButton.setButtonText ("Disconnect");
382         }
383         else
384         {
385             handleConnectError (portToConnect);
386         }
387     }
388 
389     //==============================================================================
disconnect()390     void disconnect()
391     {
392         if (oscReceiver.disconnect())
393         {
394             currentPortNumber = -1;
395             connectButton.setButtonText ("Connect");
396         }
397         else
398         {
399             handleDisconnectError();
400         }
401     }
402 
403     //==============================================================================
handleConnectError(int failedPort)404     void handleConnectError (int failedPort)
405     {
406         AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
407                                           "OSC Connection error",
408                                           "Error: could not connect to port " + String (failedPort),
409                                           "OK");
410     }
411 
412     //==============================================================================
handleDisconnectError()413     void handleDisconnectError()
414     {
415         AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
416                                           "Unknown error",
417                                           "An unknown error occurred while trying to disconnect from UDP port.",
418                                           "OK");
419     }
420 
421     //==============================================================================
handleInvalidPortNumberEntered()422     void handleInvalidPortNumberEntered()
423     {
424         AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
425                                           "Invalid port number",
426                                           "Error: you have entered an invalid UDP port number.",
427                                           "OK");
428     }
429 
430     //==============================================================================
isConnected()431     bool isConnected() const
432     {
433         return currentPortNumber != -1;
434     }
435 
436     //==============================================================================
isValidOscPort(int port)437     bool isValidOscPort (int port) const
438     {
439         return port > 0 && port < 65536;
440     }
441 
442     //==============================================================================
updateConnectionStatusLabel()443     void updateConnectionStatusLabel()
444     {
445         String text = "Status: ";
446 
447         if (isConnected())
448             text += "Connected to UDP port " + String (currentPortNumber);
449         else
450             text += "Disconnected";
451 
452         auto textColour = isConnected() ? Colours::green : Colours::red;
453 
454         connectionStatusLabel.setText (text, dontSendNotification);
455         connectionStatusLabel.setFont (Font (15.00f, Font::bold));
456         connectionStatusLabel.setColour (Label::textColourId, textColour);
457         connectionStatusLabel.setJustificationType (Justification::centredRight);
458     }
459 
460     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OSCMonitorDemo)
461 };
462 
463 //==============================================================================
464 class OSCDemo   : public Component
465 {
466 public:
OSCDemo()467     OSCDemo()
468     {
469         addAndMakeVisible (monitor);
470         addAndMakeVisible (receiver);
471         addAndMakeVisible (sender);
472 
473         setSize (700, 400);
474     }
475 
resized()476     void resized() override
477     {
478         auto bounds = getLocalBounds();
479 
480         auto lowerBounds = bounds.removeFromBottom (getHeight() / 2);
481         auto halfBounds  = bounds.removeFromRight  (getWidth()  / 2);
482 
483         sender  .setBounds (bounds);
484         receiver.setBounds (halfBounds);
485         monitor .setBounds (lowerBounds.removeFromTop (getHeight() / 2));
486     }
487 
488 private:
489     OSCMonitorDemo  monitor;
490     OSCReceiverDemo receiver;
491     OSCSenderDemo   sender;
492 
493     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OSCDemo)
494 };
495