Public
Edited
Dec 2, 2023
Paused
1 fork
Importers
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function makeModes(nWholesteps, nPitchesPerOctave = 12) {
const nHalfsteps = nPitchesPerOctave - 2 * nWholesteps;
const nPitches = nWholesteps + nHalfsteps;
if (nHalfsteps < 0) {
return [];
} else if (nHalfsteps === 0) {
return [new Array(nPitches).fill(2)];
}
const range = new Array(nPitches).fill().map((_, i) => i);
return _.combinations(range, nHalfsteps).map((indexes) => {
let result = new Array(nPitches).fill(2);
indexes.forEach((i) => (result[i] = 1));
return result;
});
}
Insert cell
Insert cell
makeModes(0)
Insert cell
Insert cell
makeModes(4)
Insert cell
Insert cell
Insert cell
nTotal = nModes(0) + // chromatic (12 notes)
nModes(1) +
nModes(2) +
nModes(3) +
nModes(4) +
nModes(5) + // ionian mode, dorian mode, etc in here (5 whole steps, 2 half steps)
nModes(6) // whole tone scale (6 notes)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// abbreviating frequencies to "freqs" because I keep spelling frequencies wrong
function equalTemperedFreqs(
nPitchesPerOctave = 12,
startFrequency = 261.6255653, // start at C4, where A4=440Hz
nOctaves = 5
) {
const n = nPitchesPerOctave;
const logs = _.range(nOctaves * n + 1).map(
(i) => Math.log2(startFrequency) + i / n
);
return logs.map((d) => 2 ** d);
}
Insert cell
function modeToFreqs(
intervals,
nPitchesPerOctave = 12,
startFrequency = 261.6255653 // start at C4, where A4=440Hz
) {
const notes = equalTemperedFreqs(nPitchesPerOctave, startFrequency);

const result = [];
let index = 0;
result.push(notes[index]);
intervals.forEach((interval) => {
index += interval;
result.push(notes[index]);
});
return result;
}
Insert cell
Insert cell
wholeToneFrequencies = modeToFreqs(makeModes(6)[0])
Insert cell
Insert cell
function playFreqs(
freqs,
duration = 0.25,
playDescending = false,
arpeggiate = true
) {
const synth = new Tone.PolySynth(Tone.Synth).toDestination();
const now = Tone.now();
const n = freqs.length;
if (arpeggiate) {
freqs.forEach((f, i) => {
const dDown = (2 * duration) / 3;
synth.triggerAttackRelease(f, dDown, now + i * duration);
if (playDescending && i !== n - 1) {
let j = 2 * (n - 1) - i;
synth.triggerAttackRelease(f, dDown, now + j * duration);
}
});
} else {
freqs.forEach((f, i) => {
if (i !== freqs.length - 1) {
synth.triggerAttackRelease(f, duration, now);
}
});
}
}
Insert cell
function playButton(label, f, ...params) {
const button = Inputs.button(label);
button.oninput = () => {
f(...params);
};
return button;
}
Insert cell
playButton(
"⏵ Ionian Mode (Major Scale)",
playFreqs,
modeToFreqs([2, 2, 1, 2, 2, 2, 1])
)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
constellationPlot([2, 2, 1, 2, 2, 2, 1])
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function* allRotations(array) {
for (let i = 0; i < array.length; i++) {
yield [...array];
//array.unshift(array.pop()); // take the last element and move to front
array.push(array.shift()); // take the first element and move to end (will do this way because it’s how it was in the sketch above
}
}
Insert cell
Insert cell
[...allRotations([1, 2, 3])]
Insert cell
Insert cell
function groupRotations(scales) {
// this currently looks at every rotation of every scale that
// is passed in, which I’m pretty sure is not necessary. It could
// be made more efficient, but it will do for now.

// keys of incomplete are string
let incomplete = new Map(scales.map((d) => [d.toString(), d]));

// choose any key as the "focus" to start
let focus = incomplete.keys().next().value;
let group = [];
let result = [];
while (true) {
// group together all the scales that are rotations of the focus (and remove from incomplete)
for (let rotation of allRotations(incomplete.get(focus))) {
const found = incomplete.delete(rotation.toString());
if (found) {
group.push(rotation);
}
}
result.push(group);

// choose any key as the next focus, until incomplete is empty
focus = incomplete.keys().next().value;
group = [];
if (focus === undefined) {
break;
}
}
return result;
}
Insert cell
Insert cell
groupRotations([
[1, 1, 2],
[2, 1, 1], // same as first, rotated
[1, 1, 1] // different
])
Insert cell
Insert cell
sevenPitchRotationGroups = groupRotations(makeModes(5, 12))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
eightPitchRotationGroups = groupRotations(makeModes(4, 12)).map((d) => d[0])
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
nTotalTNE = _.sum(_.range(0, 7).map((nWhole) => nTNE(nWhole)))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function makeAllScales(n) {
const allSequences = new Set();

function* divide(...seq) {
yield seq;
allSequences.add(seq.toString());
for (let [i, n] of seq.entries()) {
for (let m = 1; m < n; m++) {
const sub = seq
.slice(0, i)
.concat([m, n - m])
.concat(seq.slice(i + 1));
if (!allSequences.has(sub.toString())) {
yield* divide(...sub);
}
}
}
}

return [...divide(n)];
}
Insert cell
allScales = makeAllScales(12)
Insert cell
Insert cell
function* makeAllScalesBinary(n) {
for (let i = 0; i < 2 ** (n - 1); i++) {
yield i
.toString(2) // convert to binary (I’m sure there’s a better way of doing this than casting to string, but 🤷🏻)
.padStart(n - 1, "0") // pad to the correct length
.split("") // convert back to array
.map((d) => +d) // convert back to numbers
.map((d, i) => (d ? i + 1 : null)) // convert to pitch class indexes
.filter((d) => d !== null) // remove empties
.concat([n]) // add next octave index
.map((d, i, a) => d - (i - 1 >= 0 ? a[i - 1] : 0)); // subtract consecutive indexes to get steps
}
}
Insert cell
allScales2 = [...makeAllScalesBinary(12)]
Insert cell
allScales.filter((scale) => !scale.some((step) => step > 2))
Insert cell
allScales2.filter((scale) => !scale.some((step) => step > 2))
Insert cell
Insert cell
eightNoteScales = allScales.filter((d) => d.length === 8)
Insert cell
transpositionallyNonEquivalentEightNoteScales = groupRotations(eightNoteScales)
Insert cell
Insert cell
totalShapes = groupRotations(allScales)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
sixNoteScales = allScales
.filter((scale) => !scale.some((step) => step > 3))
.filter((scale) => scale.length < 6)
//.filter((scale) => intervalsToIndexes(scale).includes(7)) // only include those that have a major fifth
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
_ = require("lodash", "lodash.combinations")
Insert cell
Tone = require("tone")
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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