Public
Edited
May 9, 2023
Paused
2 forks
27 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
pitchesSharp = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
Insert cell
pitchesFlat = ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"]
Insert cell
function toneAdd(pitch, n, tendency = "#") {
while (pitch.at(-1) == "#") {
tendency = "#";
pitch = pitch.slice(0, -1);
n++;
}
while (pitch.at(-1) == "b") {
tendency = "b";
pitch = pitch.slice(0, -1);
n--;
}
const pitches = tendency == "b" ? pitchesFlat : pitchesSharp;
let k = pitchClass(pitch);
return pitches[(k + n + 12) % 12];
}
Insert cell
function pitchClass(pitchName) {
let i = pitchesFlat.indexOf(pitchName);
if (i < 0) i = pitchesSharp.indexOf(pitchName);
return i;
}
Insert cell
Insert cell
class TCoord {
constructor(u = 0, v = 0) {
Object.assign(this, { u, v });
}
equals(other) {
return this.u == other.u && this.v == other.v;
}
get right() {
return new TCoord(this.u + 1, this.v);
}
get rightDown() {
return new TCoord(this.u, this.v + 1);
}
get leftDown() {
return new TCoord(this.u - 1, this.v + 1);
}
get left() {
return new TCoord(this.u - 1, this.v);
}
get leftUp() {
return new TCoord(this.u, this.v - 1);
}
get rightUp() {
return new TCoord(this.u + 1, this.v - 1);
}
neighbor(i) {
switch (i) {
case 0:
return this.right;
case 1:
return this.rightUp;
case 2:
return this.leftUp;
case 3:
return this.left;
case 4:
return this.leftDown;
case 5:
return this.rightDown;
}
}
get key() {
return [this.u, this.v];
}
}
Insert cell
class TGrid {
static keyToTCoord = (key) => new TCoord(...key.split(",").map((d) => +d));
constructor() {
this.grid = {};
}
*cells() {
for (let key of Object.keys(this.grid)) {
yield TGrid.keyToTCoord(key);
}
}
limits() {
let umin = Infinity,
vmin = Infinity;
let umax = -Infinity,
vmax = -Infinity;
for (let { u, v } of this.cells()) {
umin = Math.min(umin, u);
vmin = Math.min(vmin, v);
umax = Math.max(umax, u);
vmax = Math.max(vmax, v);
}
return { umin, umax, vmin, vmax };
}
getCell(tcoord) {
return this.grid[tcoord.key];
}
setCell(tcoord, value) {
this.grid[tcoord.key] = value;
}
cartesian(tcoord) {
let { u, v } = tcoord;
return [u + v / 2, (v * Math.sqrt(3)) / 2];
}
}
Insert cell
function gridDiagram(grid, options = {}) {
const { xmargin = 80, ymargin = 80, cellSize = 40, edgeSize = 60 } = options;
const { umin, umax, vmin, vmax } = grid.limits();
const svgCoords = (tcoords) => {
const [x, y] = grid.cartesian(tcoords);
return { x: x * edgeSize + xmargin, y: y * edgeSize + ymargin };
};
const p = svgCoords(new TCoord(umin, vmin));
const q = svgCoords(new TCoord(umax, vmax));
const width = Math.abs(q.x - p.x) + xmargin * 2;
const height = Math.abs(q.y - p.y) + ymargin * 2;

const svg = htl.svg`<svg width=${width} height=${height} viewbox="${
p.x - xmargin
} ${p.y - ymargin} ${width} ${height}" style="font-family:sans-serif;">`;
const sel = d3.select(svg);

for (let u = umin; u <= umax; u++) {
for (let v = vmin; v <= vmax; v++) {
const src = new TCoord(u, v);
if (!grid.getCell(src)) continue;
const { x: x1, y: y1 } = svgCoords(src);
let edges = new Set();
for (let dir of [0, 1, 2]) {
const dst = src.neighbor(dir);
if (!grid.getCell(dst)) continue;
edges.add(dir);
const { x: x2, y: y2 } = svgCoords(dst);
sel
.append("line")
.datum([src, dst])
.attr("class", "edge")
.attr("x1", x1)
.attr("x2", x2)
.attr("y1", y1)
.attr("y2", y2)
.attr("stroke", "black");
}
if (edges.has(0) && edges.has(1)) {
const dst0 = src.neighbor(0);
const dst1 = src.neighbor(1);
const [{ x: x0, y: y0 }, { x: x1, y: y1 }, { x: x2, y: y2 }] = [
svgCoords(src),
svgCoords(dst0),
svgCoords(dst1)
];
sel
.append("polygon")
.datum([src, dst0, dst1])
.attr("class", "minor")
.attr("points", `${x0},${y0} ${x1},${y1} ${x2},${y2}`)
.attr("fill", "rgba(0,0,255,0.3)");
}
if (edges.has(1) && edges.has(2)) {
const dst0 = src.neighbor(1);
const dst1 = src.neighbor(2);
const [{ x: x0, y: y0 }, { x: x1, y: y1 }, { x: x2, y: y2 }] = [
svgCoords(src),
svgCoords(dst0),
svgCoords(dst1)
];
sel
.append("polygon")
.datum([src, dst0, dst1])
.attr("class", "major")
.attr("points", `${x0},${y0} ${x1},${y1} ${x2},${y2}`)
.attr("fill", "rgba(255,0,0,0.3)");
}
}
}

const data = [...grid.cells()];
const nodes = sel
.selectAll("g.node")
.data(data)
.join("g")
.style("user-select", "none")
.attr("class", "node")
.each(function (d) {
const group = d3.select(this);
const { x, y } = svgCoords(d);
group
.attr("transform", `translate(${x},${y})`)
.append("circle")
.attr("r", cellSize / 2)
.attr("fill", "white")
.attr("stroke", "black");
group
.append("text")
.attr("alignment-baseline", "middle")
.attr("text-anchor", "middle")
.text(grid.getCell(d));
const degreeSize = cellSize * 0.5,
dr = degreeSize / 2,
dx = ((cellSize / 2) * Math.sqrt(2)) / 2;
const degreeGroup = group
.append("g")
.attr("class", "degree")
.attr("transform", `translate(${dx},${-dx})`)
.style("visibility", "hidden");
degreeGroup.append("circle").attr("r", dr).attr("fill", "black");
degreeGroup
.append("text")
.attr("alignment-baseline", "middle")
.attr("text-anchor", "middle")
.text("R")
.style("fill", "white")
.style("font-size", `60%`);
});
return svg;
}
Insert cell
grid = {
let grid = new TGrid();
let cell = new TCoord();
grid.setCell(cell, "A");
for (let i = 0; i < 5; i++) {
cell = cell.neighbor(i);
grid.setCell(cell, `${i}`);
}
return grid;
}
Insert cell
tonnetz = {
const grid = new TGrid();
const base = new TCoord();
const fillRow = (base, pitch, n, tendency = "#") => {
while (n--) {
grid.setCell(base, pitch);
pitch = toneAdd(pitch, 7, tendency);
base = base.right;
}
};
fillRow(base, "C", 12);
fillRow(base.rightUp, toneAdd("C", 3, "b"), 11, "b");
fillRow(base.rightUp.rightUp, toneAdd("C", 6, "b"), 10, "b");
fillRow(base.rightUp.rightUp.rightUp, toneAdd("C", 9, "b"), 9, "b");
fillRow(base.rightDown, toneAdd("C", 4), 11);
fillRow(base.rightDown.rightDown, toneAdd("C", 8), 10);
fillRow(base.rightDown.rightDown.rightDown, toneAdd("C", 12), 9);
return grid;
}
Insert cell
applications = ({
"5th interval": (coord) => [coord.right],
"Major 3rd interval": (coord) => [coord.rightDown],
"Minor 3rd interval": (coord) => [coord.rightUp],
"Major Triad": (coord) => [coord.rightDown, coord.right],
"Minor Triad": (coord) => [coord.rightUp, coord.right],
"Sus 4": (coord) => [coord.left, coord.right],
"Sus 2": (coord) => [coord.right, coord.right.right],
"Maj 7th": (coord) => [coord.rightDown, coord.right, coord.right.rightDown],
"Maj 9th": (coord) => [
coord.rightDown,
coord.right,
coord.right.rightDown,
coord.right.right
],
"7th": (coord) => [coord.rightDown, coord.right, coord.right.rightUp],
"9th": (coord) => [
coord.rightDown,
coord.right,
coord.right.rightUp,
coord.right.right
],
"13th": (coord) => [
coord.rightDown,
coord.right,
coord.right.rightUp,
coord.right.right,
coord.right.right.rightUp,
coord.right.right.right
],
m7: (coord) => [coord.rightUp, coord.right, coord.right.rightUp],
m9: (coord) => [
coord.rightUp,
coord.right,
coord.right.rightUp,
coord.right.right
],
"Augmented Triad": (coord) => [coord.rightDown, coord.rightDown.rightDown],
"Diminished Triad": (coord) => [coord.rightUp, coord.rightUp.rightUp],
"Fully Diminished": (coord) => [
coord.rightUp,
coord.rightUp.rightUp,
coord.rightUp.rightUp.rightUp
],
"Half Diminished": (coord) => [
coord.rightUp,
coord.rightUp.rightUp.rightDown,
coord.rightUp.rightUp
],
"Major Scale": (coord) => [
coord.right.right,
coord.rightDown,
coord.left,
coord.right,
coord.leftDown,
coord.right.rightDown
],
"Major Pentatonic Scale": (coord) => [
coord.right.right,
coord.rightDown,
coord.right,
coord.leftDown
],
"Natural Minor Scale": (coord) => [
coord.right.right,
coord.rightUp,
coord.right.right.rightUp,
coord.right,
coord.leftUp,
coord.right.rightUp
],
"Minor Pentatonic Scale": (coord) => [
coord.rightUp,
coord.right.right.rightUp,
coord.right,
coord.right.rightUp
],
"Ionian Mode (Major Scale)": (coord) => [
coord.right.right,
coord.rightDown,
coord.left,
coord.right,
coord.leftDown,
coord.right.rightDown
],
"Dorian Mode": (coord) => [
coord.left.leftDown,
coord.left.left.left,
coord.left,
coord.left.left.leftDown,
coord.leftDown,
coord.left.left
],
"Phrygian Mode": (coord) => [
coord.left.leftUp,
coord.rightUp,
coord.left,
coord.right,
coord.leftUp,
coord.right.rightUp
],
"Lydian Mode": (coord) => [
coord.right.right,
coord.rightDown,
coord.rightDown.right.right,
coord.right,
coord.right.right.right,
coord.right.rightDown
],
"Aeolian Mode": (coord) => [
coord.right.right,
coord.rightUp,
coord.right.right.rightUp,
coord.right,
coord.leftUp,
coord.right.rightUp
],
"Locrian Mode": (coord) => [
coord.left.leftUp,
coord.rightUp,
coord.left,
coord.left.left.leftUp,
coord.leftUp,
coord.left.left
],
"Harmonic Minor Scale": (coord) => [
coord.right.right,
coord.rightUp,
coord.left,
coord.right,
coord.leftUp,
coord.right.rightDown
],
"Melodic Minor Scale": (coord) => [
coord.right.right,
coord.rightUp,
coord.right.right.rightUp,
coord.right,
coord.right.right.right,
coord.right.rightDown
]
})
Insert cell
degrees = (root, application) => {
const degree = [
"1",
"2b",
"2",
"3b",
"3",
"4",
"5b",
"5",
"6b",
"6",
"7b",
"7",
"8",
"9b",
"9",
"10b",
"10",
"11",
"12b",
"12",
"13b",
"13",
"14b",
"14"
];
const pitches = [root, ...application(root)].map((coord) =>
tonnetz.getCell(coord)
);
let semiTones = 0;
let prevClass = pitchClass(pitches[0]);
const result = ["R"];
for (let i = 1; i < pitches.length; i++) {
const pitch = pitches[i];
const nextClass = pitchClass(pitch);
semiTones += nextClass - prevClass;
if (nextClass < prevClass) {
semiTones += 12;
}
result.push(degree[semiTones]);
prevClass = nextClass;
}
return result;
}
Insert cell
function play(root, application) {
const pitches = [root, ...application(root)].map((coord) =>
tonnetz.getCell(coord)
);
let baseOctave = 3;
let prevClass = pitchClass(pitches[0]) - 1;
for (let i = 0; i < pitches.length; i++) {
const pitch = pitches[i];
const nextClass = pitchClass(pitch);
if (nextClass < prevClass) {
baseOctave++;
}
prevClass = nextClass;
const note = pitches[i] + baseOctave;
const [duration, time] =
playmode == "arpeggio" ? [1.5, "+" + i * 0.5] : [2, undefined];
instrument.triggerAttackRelease(note, duration, time, velocity);
}
}
Insert cell
function diagramInteraction(diagram, nodeSelection, callback) {
const sel = d3.select(diagram);
sel.selectAll("g.node").each(function (baseCoord) {
const derivedCoords = nodeSelection(baseCoord);
const deg = degrees(baseCoord, nodeSelection);

const group = d3.select(this);
const groupKey = baseCoord.key + "";
const fill = group.select("circle").attr("fill");
const nodeMap = new Map();
derivedCoords.forEach((coord, i) => {
const key = "" + coord.key;
nodeMap.set(key, { degree: deg[i + 1] });
});
let count = 0;
sel.selectAll("g.node").each(function (coord) {
const key = "" + coord.key;
if (nodeMap.has(key)) {
const obj = nodeMap.get(key);
const sel = d3.select(this);
const fill = sel.select("circle").attr("fill");
Object.assign(obj, { sel, fill });
count++;
}
});
if (count != nodeMap.size) return;
const triangles = [];
sel.selectAll("polygon.minor, polygon.major").each(function (d) {
for (let coord of d) {
const key = "" + coord.key;
if (!nodeMap.has(key) && key != groupKey) return;
}
const sel = d3.select(this);
const fill = sel.attr("fill");
triangles.push({ sel, fill });
});

group
.on("mouseenter", () => {
group.select("circle").attr("fill", "orange");
for (let { sel, fill } of triangles)
sel.attr("fill", "rgba(255,255,0,0.5)");
for (let [key, obj] of nodeMap.entries()) {
obj.sel.select("circle").attr("fill", "yellow");
obj.sel
.select("g.degree")
.style("visibility", "visible")
.select("text")
.text(obj.degree);
}
})
.on("mouseleave", () => {
group.select("circle").attr("fill", fill);
for (let { sel, fill } of triangles) sel.attr("fill", fill);
for (let [key, obj] of nodeMap.entries()) {
obj.sel.select("circle").attr("fill", obj.fill);
obj.sel.select("g.degree").style("visibility", "hidden");
}
})
.on("click", function (e, d) {
callback(d);
});
});
return function () {
sel.selectAll("g.node circle").on("mouseenter mouseleave click", null);
};
}
Insert cell
Insert cell
import {
viewof instrumentName,
viewof velocity,
instrument,
test
} from "@esperanc/tone-js-instruments"
Insert cell
Tone = require("tone")
Insert cell
Piano = import("https://cdn.skypack.dev/@tonejs/piano@0.2.1?min").then(
(obj) => obj.Piano
)
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more