Lesson 4: sequencing 101 - npisanti/ofxPDSP GitHub Wiki

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.