StepperMusic

Some scores initially arranged for a pair of Arduino-controlled stepper motors.

Log in to post a comment.

// To play another score, change this number and recompile: 
const SCORE_NUMBER = 2; // <----
input.portamento = 0.25;

/**
 * The scores here are encoded in the following format (one char per data field, the shift is just to make characters printable) :
 * . tempo
 * . number of subdivisions of the beat+31
 * . number of instruments+31
 * . For each instrument part
 *    . For each note
 *       . note+64, or 'Space' if silence
 *       . duration+63
 *    . `!` at the end of the current part
 */

 // These score come from an old project of mine, in which I was using an Arduino to control stepper motors
 // and make them play music by varying their rotation speed.
 // I have a script to convert MusicXML files to scores, so it's fairly easy to create such a score using notation software.

const scores = [
    "d#!9A@A@A @;@BC<ACACA @;@BC9A@A@A @;@BC<ACACA @;@BC9@<@EC9@<@ECGBH@G@H@G@C@@C@A9A<@>@@C A@A9A<@>@;C A9@<@EC9@<@ECGBH@G@H@G@C@@C@A9A<@>@@C@A!-A0A0A/A2C0A4A4A/A2C-A0A0A/A2C0A4A4A/A2C-A4A4A/A6C0A7A7A/A6C0A5A5A0A7C0A5A5A/A4C-A4A4A @/@6C0A7A7A @/@6C-A5A5A4A;C!",
    "P#!H@C@>@G@C@>@E@C@>@G@C@>@E@C@>@C@H@C@>@G@C@>@E@C@>@G@C@>@E@C@>@C@H@C@>@G@C@>@E@C@>@G@C@>@E@C@>@C@G@C@>@E@C@>@E@C@>@E@C@>@E@C@>@C@!+O-O0O+O!",
    "x#!@CHAGAEADAEC@CAC8A;A>AAA@A>A<C;A<A9C<CAA@AEACAHCGAEACAAA@AA@C@<C;C<G<C@CHAGAEADAEC@CAC8A;A>AAA@A>A<C;A<A9C<CAA@AEACAHCGAEACAAA@AA@C@<C;C<G<CLC<ALAJAHAG@H@JACGHC9AHAGAEAD@E@GA@G@ABADAEAGAHAJAGADAMALAJAH@G@EAGCDCEGECLC<ALAJAHAG@H@JACGHC9AHAGAEAD@E@GA@G@ABADAEAGAHAJAGADAMALAJAH@G@EAGCDCEGEC!-G/C0G2C4G(C-C(C-C-C+C)C(G/C0C(C+C$C0A/A-A,A-G/C0G2C4G(C-C(C-C-C+C)C(G/C0C(C+C0C+C$C0C4C5C7C7A2A/A+A-C+C)C(A*A,A-A/A0A2C/C,C(C4C8C9C2C4C-C-A(A-A/A0C4C5C7C7A2A/A+A-C+C)C(A*A,A-A/A0A2C/C,C(C4C8C9C2C4C-C(C-C!",
    "B/! AEAGAIA@AIAGAEALAEAGAIA@AIAGAEAJAGAIAJABAEADABADAGAIAJA@AJAIAGAIAEAGAIA@ADABA@ABAEAGAIABALAKAIAKAGAIAKABAEA@ABA@ADAEAGA=AGAEADAEABADAEA=A@A?A=A?ABADAEA;AEADABADA@ABADA;A>A=A;A=A@ABADA=AGAFADAFABADAFA=A@A>A=A;A>A@ABA8AEADABAAA;A=A>A8A>A=A;A9ABADAEA=A@A?A=A<A?A@ABA<AEADABAKAHAIAKADANALAKALAIAKALADAGAFADAFAIAJALABALAJAIAJAGAIAJABAEACABACAFAGAIA@ACABA@A>AGAIAJABAEADABADAGAIAJA@AJAIAGAIAEAGAIA@ACABA@ABA>A@ABA8ABA@A>A=A9A;A=A4A7A6A4A6A9A;A=A6A@A?A=A?A;A=A?A6A9A7A6A4A7A9A;A4A>A=A;A:A4A6A7A1A7A6A4A2A;A=A>A6A9A8A6A5A8A9A;A5A>A=A;ADAAABADA=AGAEADAEABADAEA=A@A?A=A?ABACAEA;AEACABACA@ABACA;A>A=A;A=A@ABACA9ADABA@ABA>A@ABA9ABA@A>AEA>A@ABA9ABA@A>ACA@ABACA;A>A=A;A=A@ABACA9ACABA@ABA>A@ABA9A=A;A9A;A>A@ABA;AEADA@ADA@ABADA;A>A=A;A9A=A>A@A6A@A>A=A>A;A=A>A6A9A8A6A8A;A=A>A4A>A=A;A=A9A;A=A4A=A;A9A@A=A>A@A9ACABA@ABA>A@ABA9ABA@A>AEABADAEA?AHAGAEADA@ABADA;A>A=A;A@AEAGAIABALAJAIAJADAEAGA@AJAIAGAIABADAEA?AHAGAEADA>A@AAA;A>A<A;A<AEAGAHADAGAEADAEAMALAJAHAGAEADAEAHAGAEANAKALANAHAEAGAHABA?A@ABA;A=A?A@ABADAEAGAHADAEA@AAA>AGA>A@A<AEA<A>A;ADA;A9G<G?GBGEGE@D@B@@@?@=@;@=@?@@@B@D@E@G@H@G@E@D@B@@@?@=@;@9@8C;C>CACDCGCJCMC C9C<C?CBCECHCKCNC C;G@GEG;G@GDG=G@GEG G O!-C C9C C1C C9C C/C C9C C4C C8C C-C C9C C-C C9C C-C C3C C,C C4C C*C C4C C/C C3C C(C C1C C(C C4C C(C C.C C&C C/C C%C C1C C%C C1C C%C C1C C%C C1C C%C C1C C*C C.C C#C C/C C#C C.C C#C C/C C(C C,C C-C C-C C-C C-C C-C C-C C-C C-C C-C C3C C+C C1C C*C C.C C/C C2C C*C C/C C2C C5C C*C C6C C/C C3C C(C C4C C-C C1C C&C C2C C*C C2C C(C C2C C-C C1C C&C C2C C&C C2C C&C C,C C%C C-C C#C C-C C(C C,C C-C C-C C%C C4C C&C C2C C*C C9C C G4C C1C C-C C(_/C C,C C-C C*C C(_ A/A0A2A,A/A-A,A-A0A2A4A/A2A0A/A(_0A-A/A0A(A+A)A(A'O(G'_ C4C2C,C0C-C/C2C(_(G-G0G3G6G G O O(C/C4C C(C2C4C C-C1C4C C G O!",
    "s#!FEHGAAHEJEM@K@J@F@FEHEAC<E:AF@F@ AF@F@ AFEHGAAHEJEM@K@J@F@FEHEACAC GA@C@F@C@JA @JA @HC AA@C@F@C@HA @HA @FBE@C@ @A@C@F@C@FCHAEBC@A@ @ AAAHCFB @F@F@ AA@C@F@C@JA @JA @HC AA@C@F@C@MCE@ @FBE@C@ @A@C@F@C@FCHAEBC@A@ @ AAAHCFB @ G!<@ @<@<@ @9@7@ @5@ @5@ @ A<@A@>@ @>@9@ @9@7@ B7@7@ @9@7A<@ @<@9@ @9@7@ @5@ @5@ @ A<@<@>@ @>@>@ @9@7@ @A@A@5@ @A@A@>@:@<@ @<@<@ @9@7@ @5@ @5@ @ A<@A@>@ @>@9@ @9@7@ B7@7@ @9@7A<@ @<@9@ @9@7@ @5@ @5@ @ A<@<@>@ @>@>@ @9@7@ @ A5@ @:@7@ @:@<@ @<@<@ @9@7@ @5@ @5A A<@A@>@ @>@9@ @9@7@ B7@7@ @9@7A<@ @<@9@ @9@7@ @5@ @5AA@ @<@<@5E7AA@A@5@ @:@:@ @:@<@ @<@<@ @9@7@ @5@ @5A A<@A@>@ @>@9@ @9@7@ B7@7@ @9@7A<@ @<@9@ @9@7@ @5@ @5AA@ @<@<@5@ @5@5@ A7A G!",
    "x#! @3@6@9A=A@A<@?@B@DC?@=@=@=@ @=@ @=@=@=@=@=@ @=@ @=@=@=@=@=@ @=@ @=@=@=@=@=@ @=@ @=@;@;@;@;@ @;@ @;@;@;@;@;@ @;@ @;@<@<@<@<@ @<@ @<@<@<@<@<@ @<@ @<@ @8@=@?@@B8@=@?@@E @9@=@?@@B9@=@?@@E @6@;@=@?B6@;@=@?E @=@?A<K @8@=@?@@B8@=@?@@E @9@=@?@@B9@=@?@@E @6@;@=@?B6@;@=@?E @@@B@@@?K A8B@B?B=B8A;C;@=@;@9@9G6B?B=B;B9A A8C9@;@9@8@8G A8B@B?B=B8A;C;@=@;@9F4A6E=A;E9A8O A@B?B@B?B8A;C;@9@8@9@9G A?B=B?B;B9A8C9@;@9@8@8G A1@3@4@8B1@3@4@8@8C A1@3@4@9B1@3@4@9B4A6E1A/E-A,O O O O O@@?@@G=@?@@@B@@@?@@@?@@MB@@@BG?@@@B@D@E@G@D@E@DM1A4@3B4A1A4@3B4A-A4@3B4A-A4@3B4A/A6@5B6A/A6@5B6A9@;@9@8@8K! @'F'@,F1G1C @,@1@/@-G-C @-@,@-@/O,E$A&A(A*A-A,G/A(E(G+A%E#G*A/E @%@'A$K%A A(A,A1A,A(A%A-A A(A-A-A A(A-A%A A(A,A1A,A(A%A,A A'A,A,A*A(A'A1A A1A1A1A A1A1A-A A-A-A-A A-A-A/A A/A/A/A A/A/A,A A0A3A8A6A4A3A1A A1A1A1A A1A1A-A A-A-A-A A-A-A*E*A*E'A'O%A A(A,A1A,A(A%A-A A(A-A-A A(A-A%A A(A,A1A,A(A%A,A A'A,A,A*A(A'A%A A(A,A1A,A(A%A-A A(A-A-A A(A-A*E*A*E'A'O4@3@4G1@3@4@6@4@3@4@3@4M3@1@3G/@1@3@4@3@1@3@1@0M O O O O%G%G-G-G#G#G*A(A'K!",
];
const score = scores[SCORE_NUMBER];
const srand = () => Math.random()*2-1;

function receiveScore(score) {
    let res = {tempo:0, ticksPerBeat:0, nInstr:0};
    let scoreParts = [];
    let partNotes = [];
    let curInstr = 0;
    for(let char of score) {
        let v = char.charCodeAt(0) & 0xff;
        if (res.tempo == 0) {
            res.tempo = v;
            debug.log("Tempo : ", res.tempo);
        } else if (res.ticksPerBeat == 0) {
            res.ticksPerBeat = v-31;
            debug.log("Ticks per beat : ", res.ticksPerBeat);
        } else if (res.nInstr == 0) {
            res.nInstr = v-31;
            debug.log("Number of voices : ", res.nInstr);
        } else if (v==33) { // End of score
            scoreParts.push(partNotes);
            partNotes = [];
            curInstr++;
            //if(curInstr>=res.nInstr) {break;}
        } else {
            partNotes.push(v);
        }
    }
    res.scoreParts = scoreParts;
    return res;
}



const res = receiveScore(score);
function softclip(x) {
    return x<-1?-1:x>1?1:1.5*(1-x*x/3)*x;
}
const varsquare = (p, formant) => {let x = 4*(p-~~p); return softclip(formant * (x < 2 ? 1-x : -3+x));}

const sqsyn = synth.def(class {
    constructor(options) {
        this.p = 0;
        this.freq = 0;
        this.amp = 0;
    }
    process(nn,env,tick,options) {
        this.a0 = clamp01(ditty.dt / (input.portamento * 0.03 + 0.00001));
        this.freq += this.a0 * (midi_to_hz(options.nn) - this.freq);
        this.amp += this.a0 * (options.gate - this.amp);
        this.p += this.freq * ditty.dt;
        let formant = options.cutoff / (2*this.freq);
        return varsquare(this.p, formant) * this.amp;
    }
}, {env: one, nn: c4, gate: 1, amp:0.1, cutoff: 8000});



function playScore(res) {
    ditty.bpm = res.tempo;
    let sleept = 1 / res.ticksPerBeat;
    for(let partNotes of res.scoreParts) {
        loop( () => {
            let totaldur = sleept * partNotes.reduce( (acc,cur,j) => acc + ((j%2) ? (cur-63) : 0), 0);
            debug.log("totaldur", totaldur);
            let mySynth = sqsyn.play(60, {duration: totaldur});
            for(let i=0; i<partNotes.length; i+=2) {
                let note = partNotes[i];
                let duration = partNotes[i+1]-63;
                mySynth.options.nn = note - 64 + 69;
                mySynth.options.gate = (note == 32) ? 0 : 1;
                sleep(sleept * duration);
            }
            mySynth.options.gate = 0; // Ideally I would like to free this synth now (without having to manually compute how long it has run for)
        }).connect(echo.create());
    }
}





//////////////////////////////////////////////////////////////////////////
/// echo by srtuss
// https://dittytoy.net/ditty/24373308b4
class Delayline {
    constructor(n) {
        this.n = ~~n;
        this.p = 0;
        this.lastOutput = 0;
        this.data = new Float32Array(n);
    }
    clock(input) {
        this.lastOutput = this.data[this.p];
        this.data[this.p] = input;
        if(++this.p >= this.n) {
            this.p = 0;
        }
    }
    tap(offset) {
        var x = this.p - offset - 1;
        x %= this.n;
        if(x < 0) {
            x += this.n;
        }
        return this.data[x];
    }
}
input.echo = .15;
const echo = filter.def(class {
    constructor(options) {
        this.lastOutput = [0, 0];
        var time = 60 / ditty.bpm;
        //time *= 3 / 4;
        time /= 2;
        var n = Math.floor(time / ditty.dt);
        this.delay = [new Delayline(n), new Delayline(n)];
        this.dside = new Delayline(500);
        this.kfbl = .5;
        this.kfbr = .7;
    }
    process(inv, options) {
        this.dside.clock(inv[0]);
        var new0 = (this.dside.lastOutput + this.delay[1].lastOutput) * this.kfbl;
        var new1 =              (inv[1] + this.delay[0].lastOutput) * this.kfbr;
        this.lastOutput[0] = inv[0] + this.delay[0].lastOutput * input.echo;
        this.lastOutput[1] = inv[1] + this.delay[1].lastOutput * input.echo;
        this.delay[0].clock(new0);
        this.delay[1].clock(new1);
        var m = (this.lastOutput[0] + this.lastOutput[1])*.5;
        var s = (this.lastOutput[0] - this.lastOutput[1])*.5;
        s *= 2;
        return [m+s, m-s];
    }
});
////////////////////////////////////////////////////////////////////////


playScore(res);