Each Synthesis ToolKit instrument exposes its relevant control parameters via public functions such as setFrequency() and controlChange(). Programmers are free to implement the control scheme of their choice in exposing those parameters to the user.
A text-based control protocol called SKINI is provided with the Synthesis ToolKit. SKINI extends the MIDI protocol in incremental ways, providing a text-based messaging scheme in human-readable format and making use of floating-point numbers wherever possible. Each SKINI message consists of a message type (e.g., NoteOn, PitchBend), a time specification (absolute or delta), a channel number (scanned as a long integer), and a maximum of two subsequent message-specific field values. Knowing this, it should be relatively clear what the following SKINI "scorefile" specifies:
NoteOn 0.000082 2 55.0 82.3
NoteOff 1.000000 2 55.0 64.0
NoteOn 0.000082 2 69.0 82.8
StringDetune 0.100000 2 10.0
StringDetune 0.100000 2 30.0
StringDetune 0.100000 2 50.0
StringDetune 0.100000 2 40.0
StringDetune 0.100000 2 22.0
StringDetune 0.100000 2 12.0
NoteOff 1.000000 2 69.0 64.0
MIDI messages are easily represented within the SKINI protocol.
The class stk::Messager can be used to acquire and parse MIDI messages from a MIDI device and SKINI messages from STDIN and socket connections. Incoming messages are acquired asynchronously and saved to an internal message queue of stk::Skini::Message types (MIDI messages are converted to the stk::Skini:Message format). The user then uses the stk::Messager:popMessage() function to retrieve incoming control messages. This function does not block, instead returning a message type of zero when no more messages are in the queue. Many of the example programs included with the ToolKit distribution use a stk::Messager instance to accept control input from the accompanying Tcl/Tk graphical user interfaces, from external MIDI devices, or from SKINI scorefiles.
In the following example, we'll modify the bethree.cpp
program from the previous tutorial chapter and incorporate a stk::Messager class to allow control via SKINI messages read from a SKINI file.
#include "BeeThree.h"
#include "Messager.h"
#include "SKINImsg.h"
#include <math.h>
#include <algorithm>
using std::min;
void usage(void) {
std::cout << "\nuseage: controlbee file\n";
std::cout << " where file = a SKINI scorefile.\n\n";
exit(0);
}
struct TickData {
int counter;
bool haveMessage;
bool done;
TickData()
: instrument(0), counter(0), haveMessage(false), done( false ) {}
};
#define DELTA_CONTROL_TICKS 64
void processMessage( TickData* data )
{
register StkFloat value1 = data->message.floatValues[0];
register StkFloat value2 = data->message.floatValues[1];
switch( data->message.type ) {
case __SK_Exit_:
data->done = true;
return;
case __SK_NoteOn_:
if ( value2 == 0.0 )
data->instrument->noteOff( 0.5 );
else {
StkFloat frequency = 220.0 * pow( 2.0, (value1 - 57.0) / 12.0 );
data->instrument->noteOn( frequency, value2 * ONE_OVER_128 );
}
break;
case __SK_NoteOff_:
data->instrument->noteOff( value2 * ONE_OVER_128 );
break;
case __SK_ControlChange_:
data->instrument->controlChange( (int) value1, value2 );
break;
case __SK_AfterTouch_:
data->instrument->controlChange( 128, value1 );
}
data->haveMessage = false;
return;
}
int tick( void *outputBuffer, void *inputBuffer, unsigned int nBufferFrames,
{
TickData *data = (TickData *) dataPointer;
register StkFloat *samples = (StkFloat *) outputBuffer;
int counter, nTicks = (int) nBufferFrames;
while ( nTicks > 0 && !data->done ) {
if ( !data->haveMessage ) {
data->messager.popMessage( data->message );
if ( data->message.type > 0 ) {
data->counter = (long) (data->message.time * Stk::sampleRate());
data->haveMessage = true;
}
else
data->counter = DELTA_CONTROL_TICKS;
}
counter = min( nTicks, data->counter );
data->counter -= counter;
for ( int i=0; i<counter; i++ ) {
*samples++ = data->instrument->tick();
nTicks--;
}
if ( nTicks == 0 ) break;
if ( data->haveMessage ) processMessage( data );
}
return 0;
}
int main( int argc, char *argv[] )
{
if ( argc != 2 ) usage();
Stk::setSampleRate( 44100.0 );
Stk::setRawwavePath( "../../rawwaves/" );
TickData data;
RtAudioFormat format = (
sizeof(StkFloat) == 8 ) ? RTAUDIO_FLOAT64 : RTAUDIO_FLOAT32;
unsigned int bufferFrames = RT_BUFFER_SIZE;
try {
dac.
openStream( ¶meters, NULL, format, (
unsigned int)Stk::sampleRate(), &bufferFrames, &tick, (
void *)&data );
}
goto cleanup;
}
try {
}
goto cleanup;
}
if ( data.messager.setScoreFile( argv[1] ) == false )
goto cleanup;
try {
}
goto cleanup;
}
while ( !data.done )
Stk::sleep( 100 );
try {
}
}
cleanup:
delete data.instrument;
return 0;
}
unsigned long RtAudioFormat
RtAudio data format type.
Definition RtAudio.h:86
unsigned int RtAudioStreamStatus
RtAudio stream status (over- or underflow) flags.
Definition RtAudio.h:159
Exception handling class for RtAudio.
Definition RtAudio.h:220
virtual void printMessage(void) const
Prints thrown error message to stderr.
Definition RtAudio.h:243
Realtime audio i/o C++ classes.
Definition RtAudio.h:280
unsigned int getDefaultOutputDevice(void)
A function that returns the index of the default output device.
Definition RtAudio.h:860
void openStream(RtAudio::StreamParameters *outputParameters, RtAudio::StreamParameters *inputParameters, RtAudioFormat format, unsigned int sampleRate, unsigned int *bufferFrames, RtAudioCallback callback, void *userData=NULL, RtAudio::StreamOptions *options=NULL, RtAudioErrorCallback errorCallback=NULL)
A public function for opening a stream with the specified parameters.
void closeStream(void)
A function that closes a stream and frees any associated stream memory.
Definition RtAudio.h:861
void startStream(void)
A function that starts a stream.
Definition RtAudio.h:862
STK Hammond-oid organ FM synthesis instrument.
Definition BeeThree.h:43
STK instrument abstract base class.
Definition Instrmnt.h:20
STK input control message parser.
Definition Messager.h:56
STK error handling class.
Definition Stk.h:87
The STK namespace.
Definition ADSR.h:6
The structure for specifying input or output stream parameters.
Definition RtAudio.h:313
unsigned int nChannels
Definition RtAudio.h:315
unsigned int deviceId
Definition RtAudio.h:314
A message structure to store and pass parsed SKINI messages.
Definition Skini.h:43
A realtime control message will usually have a delta time of zero, in which case it is processed as soon as possible. Non-realtime messages, normally from a scorefile, will usually have non-zero delta times. The scheme used in this example is designed to work for both scorefile and realtime input types. When no message is available from the queue, the instrument is "ticked" for DELTA_CONTROL_TICKS and then the queue is checked again. The value of DELTA_CONTROL_TICKS roughly defines the program "control rate" in a realtime context, though multiple available messages in the queue are processed in immediate succession when their delta time values are zero.
The processMessage()
function centralizes the handling of control messages. Other control update schemes can be implemented, perhaps using a separate thread or in the main()
function, and this function should work in any context.
Assuming the program is compiled as controlbee
and the SKINI scorefile bookert.ski
is in the scores
directory, the program can be run as:
controlbee scores/bookert.ski
Only a few basic SKINI message type case statements are included in this example. It is easy to extend the program to support a much more elaborate set of instrument control parameters.
This example could also be easily extended to accept "realtime" control input messages via pipe, socket or MIDI connections. The stk::Messager class provides stk::Messager::startStdInput(), stk::Messager::startSocketInput(), and stk::Messager::startMidiInput() functions for this purpose.
[Main tutorial page] [Next tutorial]