One of the issues with most commercially available control surfaces is the fact that they use the Mackie HUI or MCU MIDI protocol for communication. These protocols were designed to work with the 31kb/s midi bandwidth and as a result are limited when it comes to (text) feedback. On a Mackie control I often find myself guessing as to what it is I'm looking at. Open Sound Control (OSC) has none of these limitations but unfortunately OSC support is still rare. Reaper is one of the few DAWs offering comprehensive OSC support. To explore the available OSC interface I build a basic OSC controller.

Click image for larger version. 

Name:	Reaper_OSC_test.jpg 
Views:	19 
Size:	141.3 KB 
ID:	14737

A Teensy 3.6 with a WIZ820_SD_ADAPTOR and WIZ850io module for wired ethernet, the connections are as in the example for the WIZ adapter. Two rotary encoders, one (with the small knob, indented) for selecting a parameter and one (big knob, not indented) for changing the value. A 4D Systems display showing the track number and name, plugin name, parameter name and parameter value as string and virtual knob display. The display also has vu meter showing the track volume. Not shown are five button for navigating tracks and plugins.
The code only uses libraries that come with the Teensyduino installation. The 4D Systems VisiGenie library is not used, relevant parts are used directly in the sketch and asynchronous communication with a kind of display queue is implemented.
During setup the controller sends the settings to Reaper and a 'refresh all control surfaces' action is triggered' which will cause Reaper to send all track data.
Code:
  // send bundle with controller definition & state
  OSCBundle bndl;
  bndl.add("/device/track/count").add(NUM_OF_TRACKS);
  bndl.add("/device/send/count").add(NUM_OF_SENDS);
  bndl.add("/device/fx/count").add(NUM_OF_INSERTS);
  bndl.add("/device/fxparam/count").add(NUM_OF_PARAMETERS);
  bndl.add("/device/track/select").add(selectedTrack.number);
  bndl.add("/device/fx/select").add(selectedInsert.number);
  bndl.add("/device/fxparam/bank/select").add(selectedParameterBank);

  Udp.beginPacket(outIp, destPort);
  bndl.send(Udp);
  Udp.endPacket(); // mark the end of the OSC Packet
  bndl.empty(); // empty the bundle to free room for a new one

  // send 'reset all controllers' message so Reaper will send all track data
  sendOscMessage("/action",41743);
The received data is parsed and stored in structs modelling the track structure (tracks, sends, inserts, parameters):

Code:
// struct containing OSC parameter data & values
struct oscParameter{
  char  name[NAME_LENGTH];                            // parameter long name
  int   number;                                       // parameter number
  char  valueStr[VALUE_LENGTH];                       // formatted string value e.g. '350Hz'
  float value;                                        // normalised value
};
typedef struct oscParameter OscParameter;

// struct for fx plugin
struct trackInsert{
  char  name[NAME_LENGTH];                            // plugin long name
  int   number;                                       // plugin number
  bool  bypass;                                       // bypass status
  //OscParameter parameter[NUM_OF_PARAMETERS];        // plugin parameters
};
typedef struct trackInsert TrackInsert;

// track send parameters
struct trackSend{
  char  name[NAME_LENGTH];                            // send long name
  int   number;                                       // send number
  char  volumeStr[VALUE_LENGTH];                      // volume string    
  float volume;                                       // normalised volume
  char  panStr[VALUE_LENGTH];                         // panning string    
  float pan;                                          // normalised panning
};
typedef struct trackSend TrackSend;

// struct containing Reaper track info & data
struct track{
  char        name[NAME_LENGTH];                      // track name
  int         number;                                 // track number
  char        volumeStr[VALUE_LENGTH];                // volume string    
  float       volume;                                 // normalised volume
  char        panStr[VALUE_LENGTH];                   // panning string    
  float       pan;                                    // normalised panning
  bool        rec;                                    // record arm
  bool        mute;                                   // track mute
  bool        solo;                                   // track solo
  bool        monitor;                                // track monitor
  TrackSend   send[NUM_OF_SENDS];                     // track sends
  TrackInsert insert[NUM_OF_INSERTS];                 // track effects/instrument plugins
};
typedef struct track Track;
Using these data the controller maintains the state of the tracks (for the number of tracks as defined in settings). This is a major improvement over HUI or MCU controllers which are essentially stateless and all functionality is implemented on the DAW. Maintaining state allows for context switching on the controller (e.g. select sends or pan for editing) without having to rely on the DAW to provide this function.
The state of the plugin parameters is only only maintained for the selected plugin, this will be expanded to the selected track. Maintaining state for all tracks requires lots of memory at higher track counts so I didn't implement this.
A print function is available to print the content of the structs to the serial monitor:

Code:
SELECTED TRACK PARAMETERS
	Name		EMT Plate
	Number		11
	Volume		0.00dB
	Pan		C
	Record		0
	Mute		0
	Solo		0
	Monitor		0

	Send 1		Send 1		-inf dB		center
	Send 2		Send 2		-inf dB		center
	Send 3		Send 3		-inf dB		center
	Send 4		Send 4		-inf dB		center
	Send 5		                                		                		                
	Send 6		                                		                		                
	Send 7		                                		                		                
	Send 8		                                		                		                

	Insert 1	1	EMT 140
	Insert 2	0	
	Insert 3	0	
	Insert 4	0	
	Insert 5	0	
	Insert 6	0	
	Insert 7	0	
	Insert 8	0	

SELECTED INSERT PARAMETERS
	Plate		B
	Damping A	2.2 s
	Damping B	2.4 s
	Damping C	1.2 s
	EQ		Out
	LFreq		0.0000
	LGain		-8.5 dB
	HFreq		1.0000
	HGain		0.0 dB
	PreDly		0.0000
	ModRate		0.0000
	ModDepth	0.0000
	Width		1.0000
	WetSolo		Solo
	Mix		0.2474
	LowCut		M 90Hz


TRACK OVERVIEW
	1	-8.42dB		C	SEM		Oberheim SEM V		Lexicon
	2	0.00dB		C	Marshall	Big Muff		Lexicon
	3	0.00dB		C	Clean Guitar	ReaTuner		EMT Plate
	4	0.00dB		C	Piano		ReaInsert (Cockos)	Lexicon
	5	-9.72dB		C	Moog		ReaInsert (Cockos)	Lexicon
	6	0.00dB		C	ARP		ReaInsert (Cockos)	Lexicon
	7	-0.29dB		C	OB-6		ReaInsert (Cockos)	Send 1
	8	0.00dB		C	Prophet		ReaInsert (Cockos)	Send 1
	9	+1.13dB		C	Oppo					Lexicon
	10	0.00dB		C	Lexicon		Lexicon 224		Send 1
	11	0.00dB		C	EMT Plate	EMT 140			Send 1
	12	0.00dB		C	Time Cube	Cooper Time Cube	Send 1
	13	-inf dB		C	Track 13				Send 1
	14	-inf dB		C	Track 14				Send 1
	15	-inf dB		C	Track 15				Send 1
	16	-inf dB		C	Track 16				Send 1
Next up will be some more testing and figuring out how to best use the available functionality.

Here's the complete sketch: OSC_Controller_20180902.zip

Hopefully this helps a little to get people started with building OSC controllers.


Suggestions for improving the code are welcome. I'm still learning C(++), just starting to get the hang of structs and pointers. Next up: classes