Public
Edited
May 9, 2023
Paused
2 forks
26 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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more