DJ Pi 4: doing C++ properly
October 11, 2017
Having updated the Arduino code to output more complex messages, we now need to update the Pi app to be able to understand these messages. Remember that we’ll have a set of audio effect components running, each linked to a physical control made up of a potentiometer and button. The task that now befalls our Pi app is to parse the binary messages coming across on the serial and then dispatch the message to the correct effect component.
You can find the code for this post here.
For this task I’ll present a triumvirate of classes: our trusty MainComponent
, a SerialConnection
and finally the actual Control
. You might remember that the previous iteration of the Pi app used a funky sub-classing approach. Let’s take a look at an alternative.
Design
My aim is to have loosely-coupled components that have minimal knowledge of each other. This is so that the application can easily be extended and modified as I think of new things to do with it.
The SerialConnection is responsible for interacting with the serial connection (obviously) and notifying a Control when a message is directed its way. This class also looks after running the separate thread. It has no idea what the Controls are doing.
Each Control stores a representation of its physical counterpart’s state and broadcasts any change to the rest of the application. These are the simplest components, acting mostly as an interface between the (future) effects components and the serial connection.
This leaves the MainComponent with not much more to do than instantiating everything and hookng together the broadcasters and listeners. In time, however, this component will be responsible for routing the audio through all of the effects components.
Code walkthrough
MainComponent
Rather than creating a threaded subclass of MainComponent
, we hold a reference to the threaded SerialConnection
(initialised on line 25). We create the required number of Control
s, registering MainComponent
as a listener for each of them, and move them into the SerialConnection for safe keeping. Finally we start off the SerialConnection’s thread so that it can listen to the connection.
Note that the MainComponent
now inherits from the ChangeListener
class and implements the changeListenerCallback
method. This is a JUCE class for components that register themselves to be notified of particular changes. I have only registered the MainComponent has a listener for convenience, as it allows us to easily handle in one place the messages that the Controls are broadcasting. Once I have actual effects components the MainComponent will register each effect with its corresponding control, rather than itself.
In the callback we do some cool dynamic casting. In JUCE, when a message is broadcasted the argument to the callback is the broadcasting object itself, which will inherit from the ChangeBroadcaster
class (see the Control section below). You could have all kinds of objects broadcasting lots of different messages, so we need to make sure that we’re only acting on relevant messages. Here, we only want to hear from Controls, so we attempt to cast the argument to that type. The dynamic cast returns a (falsey) null pointer if the casting fails (in contrast to std::static_cast
, which would raise an exception). This check therefore ensures that we’re only dealing with broadcasters of the expected type.
Back up to the constructor, I am using std::make_unique
to create a unique pointer to the control. This is because only the SerialConnection will hold a reference to the controls. I’m initialising them in the main class so that the listeners can be registered. Note that the pointer is moved around using std::move
. This casts the pointer to an rvalue so that it’s passed by value into SerialConnection::addControl
, thereby transferring ownership of the pointer.
Finally, you can see that I’m defining the number of controls using the macro NUM_CTRLS. This is something I’d like to make configurable down the line, perhaps setting the value in both the Pi and Arduino code.
SerialConnection
Let’s jump over to the SerialConnection. You can see that this is like a more highly evolved version of ThreadedMainComponent
from the last C++ post. We start up a thead to get serial values as usual, only this time we parse the message and look up the correct destination in the list of Control
s that the class maintains.
Regrettably, I am still hardcoding the file descriptor path and baud rate. These are also something that should be configurable, at some point. I have added a TODO comment as a reproach to my future self.
Of note is the custom destructor. The implicit one created by the compiler won’t suffice in this case because the thread and file descriptor need to be closed. When creating a user-defined destructor it is good C++ practice to also define a copy constructor, copy-assignment operator (rule of three), move constructor and move-assignment operator (rule of five). This is because whatever necessitates a custom destructor will probably necessitate a custom implementation of these members.
I’m not implementing them because they’re not yet needed. However, if you look in the header file you can see that I’ve marked them with the delete
operator:
This tells the compiler not to create implicit definitions of these methods. Doing so helped me pick up an interesting bug from an earlier version of this code. In the MainComponent
initialisation list I’d initialised the connection as follows:
conn(SerialConnection()) // so very wrong
conn() // correct
Looking at it now it’s pretty obvious, but sometimes when you become familiar with code you see what you think it’s doing rather than what it’s actually doing. So rather than just initialising conn
(which is of type SerialConnection
) I was actually initialisation another connection and then trying to initialise my conn
by moving the first one in. This compiled and ran fine. I only noticed the problem when I marked the move constructor as deleted and my compiler complained that it couldn’t use the deleted member. I have no idea what was happening to the connection’s thread and file descriptor. Perhaps they were moving successfully, or perhaps they were being duplicated?
While in the header file you can see the macros that pull out the correct bits of the binary message. They’re kind of the opposite to their counterparts on the Arduino side so there’s not much to say. It’s a bit sub-optimal to have this information about the message structure in two different parts of the codebase. The ideal (and very much further down the line) would be to have a specification for the message structure and then something that generated the code to correctly serialise and deserialise it, like gRPC.
Back in the run
method, my apologies for the ugly error checking around reading the serial values. At this stage of the project they’re more to alert me early to some error, rather than let me waste time trying to work out why the output looks so weird. I put them in because I had mistakenly defined lowerByte
and upperByte
to be unsigned integers, thinking that that was what I’d used on the Arduino end. But of course the serialGetchar
function returns a signed integer with -1 indicating an error. RTFM.
Control
This class is kind of the Robbie Williams of the group. It doesn’t really do much but store state and broadcast changes to the rest of the application.
It does this by inheriting from the ChangeBroadcaster
class, giving it the sendChangeMessage
method to call when it wants to broadcast a message. Previous versions of JUCE seemed to allow a void*
argument for this method, but it’s been deprecated in favour of passing the broadcasting object itself.
Next steps
Four posts later and we’ve finally covered the basic project structure and communication between its various parts. All that’s left is to start the fun bit - the actual audio components!