Composing with dyads (two notes) is a way to explore leading melodies and counter melodies in the simplest form. And, even in the simplest form, the results can be quite complex as the dyad is usually not enough information to frame a chord as minor or major in a way one might typically expect.

Listen to it now in the website visualizer: dyads.infinitedigits.co

Or on Bandcamp:

Sheet music #

I worked from sheet music for the entire album. I wrote the score by hand but transcribed most of the pieces into Lilypond to archive as PDF.

Album art #

Code for the album art

https://editor.p5js.org/schollz/sketches/ZcdByv91a

let points = [];
let t = 0;
let gravityStrength = -0.3; // controls pull intensity
let center;

function setup() {
    createCanvas(1800, 1800, P2D);
    pixelDensity(displayDensity());
    noFill();
    smooth();

    center = createVector(width / 2, height / 2);

    let gridSize = 3;
    let padL = 120;
    let padR = 200;
    let padT = 220;
    let padB = 150;
    let availableW = width - padL - padR;
    let availableH = height - padT - padB;
    let cellW = availableW / gridSize;
    let cellH = availableH / gridSize;

    for (let row = 0; row < gridSize; row++) {
        for (let col = 0; col < gridSize; col++) {
            let cx = padL + cellW * (col + 0.5);
            let cy = padT + cellH * (row + 0.5);
            let w = cellW * random(0.6, 0.7);
            let h = cellH * random(0.6, 0.7);
            let depth = random(0.25, 0.4);
            let seed = int(random(1000));
            let boxPts = createBox(cx, cy, w, h, depth, seed);
            boxPts = smoothPoints(boxPts);
            points = points.concat(boxPts);
        }
    }
}

function draw() {
    background("#ffefbc");
    stroke("#222222");
    strokeWeight(14);

    // apply gravitational pull
    for (let p of points) {
        let dir = p5.Vector.sub(center, p);
        let dist = dir.mag();
        dir.normalize();
        // gravity falls off with distance
        let pull = gravityStrength / (dist * 0.01 + 1);
        p.add(dir.mult(pull));
    }

    beginShape();
    if (points.length > 0) curveVertex(points[0].x, points[0].y);
    for (let i = 1; i < t && i < points.length; i++) {
        let p = points[i];
        curveVertex(p.x, p.y);
    }
    if (t > 1 && t < points.length) {
        let last = points[int(t)];
        curveVertex(last.x, last.y);
    }
    endShape();

    if (t < points.length) {
        t += 1.5;
    } else {
        noLoop();
    }
}

function createBox(cx, cy, w, h, depthFactor, seed) {
    randomSeed(seed);
    let d = w * depthFactor;
    let angle = random(-PI / 6, PI / 6);
    let cosA = cos(angle);
    let sinA = sin(angle);
    function rot(x, y) {
        let dx = x - cx;
        let dy = y - cy;
        return createVector(
            cx + dx * cosA - dy * sinA,
            cy + dx * sinA + dy * cosA,
        );
    }
    let fTL = rot(cx - w / 2, cy - h / 2);
    let fTR = rot(cx + w / 2, cy - h / 2);
    let fBR = rot(cx + w / 2, cy + h / 2);
    let fBL = rot(cx - w / 2, cy + h / 2);
    let bTL = rot(cx - w / 2 + d, cy - h / 2 - d);
    let bTR = rot(cx + w / 2 + d, cy - h / 2 - d);
    let bBR = rot(cx + w / 2 + d, cy + h / 2 - d);
    let bBL = rot(cx - w / 2 + d, cy + h / 2 - d);
    let ordered = [
        fTL,
        fTR,
        fBR,
        fBL,
        fTL,
        bTL,
        bTR,
        bBR,
        bBL,
        bTL,
        bBL,
        fBL,
        fBR,
        bBR,
        bTR,
        fTR,
    ];
    let start = seed % ordered.length;
    let rotated = [];
    for (let i = 0; i < ordered.length; i++) {
        rotated.push(ordered[(i + start) % ordered.length]);
    }
    return rotated;
}

function smoothPoints(arr) {
    let smoothed = [];
    for (let i = 0; i < arr.length - 1; i++) {
        let p1 = arr[i];
        let p2 = arr[i + 1];
        smoothed.push(p1);
        let mid = p5.Vector.lerp(p1, p2, 0.5);
        smoothed.push(mid);
    }
    smoothed.push(arr[arr.length - 1]);
    return smoothed;
}