-
Notifications
You must be signed in to change notification settings - Fork 37
Lesson 4: sequencing 101
In this lesson we will learn about more sequencing with ofxPDSP. Copy again the init app code again. in 'ofApp.h' we need:
// pdsp modules
ofxPDSPEngine engine;
pdsp::Sequence sequence;
pdsp::ADSR adsr;
pdsp::Amp amp;
pdsp::FMOperator sine;
now in the ofApp.cpp
code:
void ofApp::setup(){
ofSetWindowShape(640, 360);
//--------PATCHING-------
engine.score.setTempo(108.0);
engine.score.sections.resize(1); // by default we have 0 sections, we need 1
engine.score.sections[0].setCell(0, &sequence, pdsp::Behavior::Loop);
// arguments are: index, pointer to pdsp::Sequence, pointer to pdsp::SeqChange
engine.score.sections[0].launchCell(0); // cells are stopped by default, start the first cell of sections[0]
// we patch our section to our synth
engine.score.sections[0].out_trig() >> adsr;
// our synth is a simple sine wave
adsr.set(0.0f, 50.0f, 1.0f, 50.0f) >> amp.in_mod();
sine >> amp * 0.5f >> engine.audio_out(0);
amp * 0.5f >> engine.audio_out(1);
// SEQUENCE CODING
sequence.set( { 1.0f, 0.0f, 0.5f, 0.0f, 0.3f, 0.0f, 0.2f, 0.0f }, 16.0, 1.0);
// arguments are: an inline array of values, the time division (16.0 = 1/16t), the sequence length
// FINISHED SEQUENCE CODING
//------------SETUPS AND START AUDIO-------------
engine.listDevices();
engine.setDeviceID(0); // REMEMBER TO SET THIS AT THE RIGHT INDEX!!!!
engine.setup( 44100, 512, 3);
}
Now i will explain what it's happening here. Inside our engine
we have a score
member. score
is the object we use for updating the global playhead and for sequencing. Inside score there is a vector member called sections
. Think of those sections as tracks of your arrangment, or different sections of an orchestral score. Each section has one ore more outputs you can patch to your modules.
Also each sections has a table with pointers to our pdsp::Sequence
object with each index.
pdsp::Sequence
is a class that rapresent a block of messages we are scoring. We are setting sequence
giving it a list of values that will be sent to the connected envelope sequentially at the given clock division (in our case 8.0 = 1/16th). After the sequence length is expired another Sequence is triggered (in our case the sequence retrigger itself as we have set pdsp::Behavior::Loop for that section index). The positive values are opening the envelope gate, the 0.0f value are closing it, so even if we have set our time to 16.0 we will hear an 8th division.
Compile and run, you should hear our sequentially triggered beeps.
Now, if you give negative number in the sequence those numbers will be ignored, they won't generate any output. Change the part after // SEQUENCE CODING
:
// SEQUENCE CODING
float o = -1.0f;
sequence.set( { 1.0f, o , o , o , 0.0f, o , 0.3f, 0.0f, 0.3f, 0.0f }, 16.0, 1.0);
// FINISHED SEQUENCE CODING
Compile and run. You should ear a longer beep and two shorter ones.
Well, we are just controlling the gate, but each sections have multiple output. We can patch them to more synth paramenters:
void ofApp::setup(){
ofSetWindowShape(640, 360);
//--------PATCHING-------
engine.score.setTempo(108.0);
engine.score.sections.resize(1);
engine.score.sections[0].setCell(0, &sequence, pdsp::Behavior::Loop);
engine.score.sections[0].launchCell(0);
// we patch our section to our synth
// in out_trig() or out_value() you pass the output number (or 0 if you don't give an argument)
// you can't use both out_trig() and out_value() for the same output number
engine.score.sections[0].out_trig(0) >> adsr; // first output is patched to envelope
engine.score.sections[0].out_value(1) >> sine.in_pitch(); // second output is patched to pitch
// our synth is a simple sine wave
adsr.set(0.0f, 50.0f, 1.0f, 50.0f) >> amp.in_mod();
sine >> amp * 0.5f >> engine.audio_out(0);
amp * 0.5f >> engine.audio_out(1);
// SEQUENCE CODING
float o = -1.0f;
// we use a multidimensional array when we need more than one output
sequence.set( { {1.0f, 0.0f, 1.0f, 0.0f, 1.0f, o, o, 0.0f }, // out 0 = gate
{72.0f, o, o, o, 83.0f, o, 84.0f, o }}, // out 1 = pitch
16.0, 1.0 ); // 1/16th division, 1 bar length
// FINISHED SEQUENCE CODING
//------------SETUPS AND START AUDIO-------------
engine.listDevices();
engine.setDeviceID(0); // REMEMBER TO SET THIS AT THE RIGHT INDEX!!!!
engine.setup( 44100, 512, 3);
}
Compile and run. Our beep just become (a little!) less boring...
Now, there is also another way of setting a sequence, using begin()
, message()
and end()
. Modify again after // SEQUENCE CODING
// SEQUENCE CODING
sequence.setDivision(16.0);
sequence.setLength(1.0);
sequence.begin();
float pitch = 72.0f;
for( double step=0.0; step<=5.1; step+=1.0 ){
sequence.message(step, 1.0f, 0); // gate on
sequence.message(step, pitch, 1); // pitch
sequence.message(step+0.8, 0.0f, 0); // gate off
pitch+=1.0f;
}
sequence.end();
// FINISHED SEQUENCE CODING
Compile and run. This will generate a chromatically rising sequence.
Ok up to now things haven't been very interesting, but imagine programming sequence that can change every time they are started, calculating their patterns having access to their own variables... that's what the assignable 'code' member of pdsp::Sequence is for:
// SEQUENCE CODING
sequence.code = [&]() noexcept { // [&] defines the scope, noexcept disables exceptions
sequence.begin( 16.0, 1.0 );
for( double step=0.0; step<4.!; step+=1.0 ){
sequence.message(step, 1.0f, 0); // gate on
sequence.message(step, 72.0f, 1); // pitch
sequence.message(step+0.8, 0.0f, 0); // gate off
}
for( double step=4.0; step<15.1; step+=1.0 ){
if( pdspChance(0.5f) ){
float pitch = 72.0f + pdspURan()*24.0f;
sequence.message(step, 1.0f, 0); // gate on
sequence.message(step, pitch, 1); // pitch
sequence.message(step+0.8, 0.0f, 0); // gate off
}
}
sequence.end();
};
// FINISHED SEQUENCE CODING
Compile and run. Now you can ear that the first four steps will be always the same, but all the other step will be totally random. The sequence code
function is called in the audio thread, so make it as fast as possible, avoid allocating and freeing memory or using slow resources (if you need some heavy lifting do it elsewere, like in the setup() or in another running thread). Also take attention to the random functions used. As this code won't run in the oF main thread you shouldn't use ofRandom()
, as ofRandom()
isn't thread safe. There are some random functions in pdsp you can use instead, there also aren't thread-safe but as long you are using ofRandom()
in the main thread and those in the sequences code you are fine.
Also remember that the code
function is executed just before the sequence starts, so it can (and probably will) erase all the changes you have made calling set()
.
You can also extend the pdps::Sequence
class when you need, the easiest way is something like this:
struct MySequence : public pdsp::Sequence{
int myVar1;
float myVar2;
MySequence(){
myVar1 = 0;
myVar2 = 42.0f;
code = [&] () noexcept {
// do all your calculations
// and then set the sequence here
}
}
}
Also: you can assign an already existing function to code
using the std::bind
function
// for example in our ofApp::setup() function we can write
sequence.code = bind(ofApp::yourFunction, this);
Now: you can use as many sequence and sections you want and also code your functions for making them automatically switch at the right time. You can learn it by yourself by checking the scoring1 and scoring2 examples.
As usual you can find the code of this tutorial in the lessons repo.