Preys and predators

can have a lot of timbre!

Log in to post a comment.

input.x0 = 7; // min=1.01, max=20, step=0.001

// The equations of Lotka-Volterra are a simple model of
// prey-predator population evolution.
// x represents the amount of preys
// y represents the amount of predators
// Their evolution equation is
// x' = x * (1 - y)
// y' = y * (x - 1)
// When there are few preys (x<1), the number of predators decreases.
// Once there are few predators (y<1), the prey population is able to increase exponentially...
// except this allows the number of predators to increase again, and eat them, leading to fewer preys,
// and the cycle repeats...
//
// This specific model has stable cycles: from any starting position (x0,y0) with positive values,
// the trajectory will return to (x0,y0) after one cycle, thus
// the behavior will be periodic (but the period depends on the starting position).
// In particular there is no "damping" toward the fixed point (x=1, y=1)
// 
// If we start from the point (x0, 1), larger values of x0 mean that the cycle will have stronger variations,
// but also a longer period.

// In terms of sound, I think it's rather cool,
// but the fact that it detunes depending on x0 is inconvenient for using it in synth code.


// Retune the synth !
function calc_period_lk(x0, a0) {
    // How many samples does it take to run one period?
    var x = x0;
    var y = 1 + 0.5 * (x-1) * a0;
    var s = 0;
    while(y >= 1) {
        x += (x * (1-y)) * a0;
        y += (y * (x-1)) * a0;
        s++;
    }
    var dy = 0;
    while(y < 1) {
        x += (x * (1-y)) * a0;
        dy = (y * (x-1)) * a0;
        y += dy;
        s++;
    }
    // Sub-sample correction
    var overshoot = (y-1) / dy;
    return s - overshoot; 
}

const lk = synth.def(class {
    constructor(options) {
        this.x0 = input.x0;
        this.calc_a0(options);
        this.x = this.x0;
        this.y = 1 + 0.5 * (this.x-1) * this.a0;
    }
    calc_a0(options) {
        // Change the coefficient a0 so that the frequency is correct despite the trajectory being different
        var freq = midi_to_hz(options.note);
        var a0 = 2*Math.PI * freq * ditty.dt;
        var spl = calc_period_lk(this.x0, a0); // current period duration (in samples)
        debug.log("spl", spl);
        var spl_target = 1 / (freq * ditty.dt); // desired period duration
        debug.log("spl_target", spl_target);
        var factor = (spl / spl_target);
        this.a0 = a0 * factor; // Just multiply the speed by this factor and hope for the best
        debug.log("factor", factor);
        var spl2 = calc_period_lk(this.x0, this.a0); // Now the period is a bit closer, but still not perfect
        debug.log("spl2", spl2);
        var factor2 = (spl2 / spl_target);
        this.a0 *= factor2; // So we correct the factor again
        var spl3 = calc_period_lk(this.x0, this.a0); // Almost perfect now
        debug.log("spl3", spl3);
        var factor3 = (spl3 / spl_target);
        this.a0 *= factor3; // One last time
        
        // It can be nice to use adaptive oversampling for large a0,
        // however this implementation is not compatible with the retuning.
        this.os = 1;
        /*
        while(this.a0 > 0.01) {
            this.os *= 2;
            this.a0 /= 2;
        }
        debug.log("os",this.os);
        */
    }
    process(note,env,tick,options) {
        if(this.x0 != input.x0) { // Reset on input changd
            this.x0 = input.x0;
            this.x = this.x0;
            this.calc_a0(options);
            this.y = 1 + 0.5 * (this.x-1) * this.a0;
        }
        for(let i=0; i<this.os; i++) {
            // Störmer-Verlet integrator 
            // (symplectic numerical integrator, so that it doesn't explode nor dampen)
            this.x += (this.x * (1-this.y)) * this.a0;
            this.y += (this.y * (this.x-1)) * this.a0;
        }
        return this.y - 1;
    }
}, {env:one, amp:0.03});

lk.play(c3);