The Synthesis ToolKit in C++ (STK)

Home Information Classes Download Usage Mail List Requirements Links FAQ Tutorial

Voice Management

The previous tutorial chapters were concerned only with monophonic ToolKit instrument playback and control. At this point, it should be relatively clear that one can instantiate multiple instruments and perhaps sum together their outputs or even direct their outputs to separate channels. It is less clear how one might go about controlling a group of instruments. The stk::Voicer class is designed to serve just this purpose.

The stk::Voicer class is a relatively simple voice manager. The user can dynamically add and delete instruments to/from its "control", with the option of controlling specific instruments via unique note tags and/or grouping sets of instruments via a "group" number. All sounding instrument outputs are summed and returned via the `tick()` function. The stk::Voicer class responds to noteOn, noteOff, setFrequency, pitchBend, and controlChange messages, automatically assigning incoming messages to the voices in its control. When all voices are sounding and a new noteOn is encountered, the stk::Voicer interrupts the oldest sounding voice. The user is responsible for creating and deleting all instrument instances.

In the following example, we modify the `controlbee.cpp` program to make use of three stk::BeeThree instruments, all controlled using a stk::Voicer.

// threebees.cpp STK tutorial program
#include "BeeThree.h"
#include "RtAudio.h"
#include "Messager.h"
#include "Voicer.h"
#include "SKINImsg.h"
#include <algorithm>
using std::min;
using namespace stk;
// The TickData structure holds all the class instances and data that
// are shared by the various processing functions.
struct TickData {
Voicer voicer;
Messager messager;
Skini::Message message;
int counter;
bool haveMessage;
bool done;
// Default constructor.
TickData()
: counter(0), haveMessage(false), done( false ) {}
};
#define DELTA_CONTROL_TICKS 64 // default sample frames between control input checks
// The processMessage() function encapsulates the handling of control
// messages. It can be easily relocated within a program structure
// depending on the desired scheduling scheme.
void processMessage( TickData* data )
{
StkFloat value1 = data->message.floatValues[0];
StkFloat value2 = data->message.floatValues[1];
switch( data->message.type ) {
case __SK_Exit_:
data->done = true;
return;
case __SK_NoteOn_:
if ( value2 == 0.0 ) // velocity is zero ... really a NoteOff
data->voicer.noteOff( value1, 64.0 );
else { // a NoteOn
data->voicer.noteOn( value1, value2 );
}
break;
case __SK_NoteOff_:
data->voicer.noteOff( value1, value2 );
break;
case __SK_ControlChange_:
data->voicer.controlChange( (int) value1, value2 );
break;
case __SK_AfterTouch_:
data->voicer.controlChange( 128, value1 );
case __SK_PitchChange_:
data->voicer.setFrequency( value1 );
break;
case __SK_PitchBend_:
data->voicer.pitchBend( value1 );
} // end of switch
data->haveMessage = false;
return;
}
// This tick() function handles sample computation and scheduling of
// control updates. It will be called automatically when the system
// needs a new buffer of audio samples.
int tick( void *outputBuffer, void *inputBuffer, unsigned int nBufferFrames,
double streamTime, RtAudioStreamStatus status, void *dataPointer )
{
TickData *data = (TickData *) dataPointer;
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->voicer.tick();
nTicks--;
}
if ( nTicks == 0 ) break;
// Process control messages.
if ( data->haveMessage ) processMessage( data );
}
return 0;
}
int main()
{
// Set the global sample rate and rawwave path before creating class instances.
Stk::setSampleRate( 44100.0 );
Stk::setRawwavePath( "../../rawwaves/" );
int i;
TickData data;
RtAudio dac;
Instrmnt *instrument[3];
for ( i=0; i<3; i++ ) instrument[i] = 0;
// Figure out how many bytes in an StkFloat and setup the RtAudio stream.
RtAudio::StreamParameters parameters;
parameters.deviceId = dac.getDefaultOutputDevice();
parameters.nChannels = 1;
RtAudioFormat format = ( sizeof(StkFloat) == 8 ) ? RTAUDIO_FLOAT64 : RTAUDIO_FLOAT32;
unsigned int bufferFrames = RT_BUFFER_SIZE;
if ( dac.openStream( &parameters, NULL, format, (unsigned int)Stk::sampleRate(), &bufferFrames, &tick, (void *)&data ) ) {
std::cout << dac.getErrorText() << std::endl;
goto cleanup;
}
try {
// Define and load the BeeThree instruments
for ( i=0; i<3; i++ )
instrument[i] = new BeeThree();
}
catch ( StkError & ) {
goto cleanup;
}
// "Add" the instruments to the voicer.
for ( i=0; i<3; i++ )
data.voicer.addInstrument( instrument[i] );
if ( data.messager.startStdInput() == false )
goto cleanup;
if ( dac.startStream() ) {
std::cout << dac.getErrorText() << std::endl;
goto cleanup;
}
// Block waiting until callback signals done.
while ( !data.done )
Stk::sleep( 100 );
// Shut down the callback and output stream.
dac.closeStream();
cleanup:
for ( i=0; i<3; i++ ) delete instrument[i];
return 0;
}
RtAudio.h
RtAudioStreamStatus
unsigned int RtAudioStreamStatus
RtAudio stream status (over- or underflow) flags.
Definition RtAudio.h:178
RtAudioFormat
unsigned long RtAudioFormat
RtAudio data format type.
Definition RtAudio.h:105
RtAudio
Realtime audio i/o C++ classes.
Definition RtAudio.h:268
RtAudio::getDefaultOutputDevice
unsigned int getDefaultOutputDevice(void)
A function that returns the ID of the default output device.
Definition RtAudio.h:915
RtAudio::getErrorText
const std::string getErrorText(void)
Retrieve the error message corresponding to the last error or warning condition.
Definition RtAudio.h:920
RtAudio::closeStream
void closeStream(void)
A function that closes a stream and frees any associated stream memory.
Definition RtAudio.h:916
RtAudio::openStream
RtAudioErrorType openStream(RtAudio::StreamParameters *outputParameters, RtAudio::StreamParameters *inputParameters, RtAudioFormat format, unsigned int sampleRate, unsigned int *bufferFrames, RtAudioCallback callback, void *userData=NULL, RtAudio::StreamOptions *options=NULL)
A public function for opening a stream with the specified parameters.
RtAudio::startStream
RtAudioErrorType startStream(void)
A function that starts a stream.
Definition RtAudio.h:917
stk::BeeThree
STK Hammond-oid organ FM synthesis instrument.
Definition BeeThree.h:43
stk::Instrmnt
STK instrument abstract base class.
Definition Instrmnt.h:20
stk::Messager
STK input control message parser.
Definition Messager.h:56
stk::StkError
STK error handling class.
Definition Stk.h:87
stk::Voicer
STK voice manager class.
Definition Voicer.h:35
stk
The STK namespace.
Definition ADSR.h:6
RtAudio::StreamParameters
The structure for specifying input or output stream parameters.
Definition RtAudio.h:302
RtAudio::StreamParameters::nChannels
unsigned int nChannels
Definition RtAudio.h:305
RtAudio::StreamParameters::deviceId
unsigned int deviceId
Definition RtAudio.h:304
stk::Skini::Message
A message structure to store and pass parsed SKINI messages.
Definition Skini.h:43

We have written this program to accept control messages from `STDIN`. Assuming the program is compiled as `threebees`, the three-voice SKINI scorefile `bachfugue.ski` (located in the `scores` directory with the examples) can be redirected to the program as:

threebees < scores/bachfugue.ski

For more fun, surf to Kern Scores for a huge assortment of other scorefiles that can be downloaded in the SKINI format.

Another easy extension would be to add the `stk::Messager::startMidiInput()` function to the program and then play the instruments via a MIDI keyboard.

[Main tutorial page]

The Synthesis ToolKit in C++ (STK)
©1995--2023 Perry R. Cook and Gary P. Scavone. All Rights Reserved.