Public
Edited
Feb 19
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
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
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
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
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
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import {slider} from "@jashkenas/inputs"
Insert cell
Insert cell
Tone = require("tone")
Insert cell
import { reconcile, html } from '@tomlarkworthy/reconcile-nanomorph' //import { html } from "@observablehq/htl"
Insert cell
_ = require("lodash@4.17") /*= {
const [_, fp] = await Promise.all([
require("lodash@4"),
require("https://cdn.jsdelivr.net/gh/lodash/lodash@4/dist/lodash.fp.min.js")
]);
return fp(_);
}*/
Insert cell
simplify = require("simplify-js")
Insert cell
Insert cell
md`### Graphical`

Insert cell
highlightColor = "#c8008c"
Insert cell
period = () => tip('period')`${tex`\frac{seconds}{cycle}`}`
Insert cell
frequency = () => tip('frequency')`${tex`\frac{cycles}{second}`}`
Insert cell
cycle = () => tip('cycle')`One full repetitition of the wave`
Insert cell
standing_wave = () =>
md`${tip(
'standing wave'
)`A wave that alternates up-and-down (amplitude) instead traveling left or right.`}`
Insert cell
node = () => tip('node')`A point on a standing wave that doesn't move.`
Insert cell
harmonic = () =>
tip(
'harmonic'
)`Whole number multiples of a base frequency. If the base frequency is 440 Hz, the first harmonic is 880 Hz, the second harmonic is 1320 Hz, etc.`
Insert cell
html`<style>
.unimportant-aside {
color: #999;
}
.frequency-display {
padding-left: .5em;
}
.frequency-display-item {
background: #f8f8f8;
color: black;

border: none;
border-radius: 0.75em;

padding-left: .2em;
padding-right: .2em;

margin-right: .5em;
}
.play {
color: white;
background-color: black;
border-radius: 3em;
}
</style>
`
Insert cell
md`1:2:3 <span class="unimportant-aside">(440 Hz + 660 Hz + 880 Hz + ${tex`l`})</span>`
Insert cell
md`### Mechanical`
Insert cell
notes = _.range(starting_piano_key, starting_piano_key + 12).map(i => ({
frequency: calc_frequency(i),
index: i
}))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
base_x_stretch = 2 * Math.PI
Insert cell
who_for = ["gamers", "programmers", "alex", "speech pathologists"][3]
Insert cell
starting_piano_key = 49
Insert cell
speed_of_sound = 340.27 // meters/second
Insert cell
nomag = ({magFactor: 1, magFactorRepetition: 1})
Insert cell
soundmag = ({ magFactor: 440, magFactorRepetition: 220 })
Insert cell
soundmag_out = ({ magFactor: 440, magFactorRepetition: 440 })
Insert cell
Insert cell
mutable show_nodal_points = false
Insert cell
mutable show_wavelength = false
Insert cell
mutable show_nodal_points_2 = false
Insert cell
Insert cell
Insert cell
triangle = x =>
((a, p) =>
((4 * a) / p) * Math.abs(((((x - p / 4) % p) + p) % p) - p / 2) - a)(
1,
base_x_stretch
) // thank god for wikipedia lol
Insert cell
wave1 = wave({ waveType: sin, frequency: 1, speed: .2, phase: 0, amplitude: 1 })
Insert cell
wave1_reversed = wave({
waveType: sin,
frequency: 1,
speed: -.2,
phase: 0,
amplitude: 1
})
Insert cell
wave2 = wave({ waveType: sin, frequency: 2, speed: 1, phase: 0, amplitude: 1 })
Insert cell
md`## Pianos`
Insert cell
consonance = pianoSVG(notes)
Insert cell
dissonance = pianoSVG(
notes.map(({ frequency, index }) => ({
frequency: frequency + 50,
index
}))
)
Insert cell
Insert cell
speed = 1
Insert cell
Insert cell
Insert cell
{
return simpleNotePlayer([440], soundmag, "test1")(this);
}
Insert cell
{
return simpleNotePlayer([2], nomag, "test2")(this);
}
Insert cell
wavePlayer(
[{ frequency: 1, amplitude: 1, speed: 1, phase: 0, waveType: sin }],
repetitions,
nomag,
"test2"
)(this)
Insert cell
simpleNotePlayer = (notes, mag, description, options) => {
const { speed, amplitude_adjustment, show_frequencies } = Object.assign(
{ speed: 1, amplitude_adjustment: notes.length, show_frequencies: false },
options
);
const mywaves = notes.map(frequency => ({
waveType: sin,
frequency: frequency,
speed: speed,
phase: 0,
amplitude: 1 / amplitude_adjustment
}));

return wavePlayer(mywaves, repetitions, mag, description, options);
}
Insert cell
wavePlayer = (waves, repetitions, mag, description, options) => {
{
const { show_frequencies } = Object.assign(
{ show_frequencies: false },
options
);
const frequency_display = htl.html.fragment`<span ${{
class: "frequency-display"
}}>${waves.map(
({ frequency }) =>
htl.html`<span class="frequency-display-item">${frequency.toFixed(
0
)} Hz</span>`
)}</span>`;

const w = showWave(waves.map(wave), t, repetitions, mag, description);

const b = () => {
const params = { id: description + "-play-button" };
const elem = html`${playButton(params)}${
show_frequencies ? frequency_display : ""
}`;
const onclick = async () => {
let s = synth;
if (synth.unset) {
const synthToUse = new Tone.PolySynth(Tone.Synth).toDestination();
s = synthToUse;
mutable synth = Object.assign(synthToUse, { unset: false });
}
const sounds = _.uniqBy(waves, "frequency").filter(
({ frequency }) => frequency > 20 && frequency < 20000
);

const toSample = 200;
const volume =
_.sum(
_.range(0, toSample).map(() =>
Math.abs(w.waveFunc(Math.random())(Math.random()))
)
) / toSample;

sounds.forEach(({ frequency }) => {
s.triggerAttackRelease(frequency, 0.4, Tone.now(), volume);
});
};
elem.onclick = onclick;
return elem;
};

const bo = b();

const output = html`${bo}${w.node()}`;
return (that) => reconcile(that, output);
}
}
Insert cell
wave = ({ waveType, frequency, speed, phase, amplitude }) => ({
waveFunc: t => x =>
amplitude *
waveType(
frequency * (x - t * speed + (phase * base_x_stretch) / frequency)
),
amplitude,
frequency
})
Insert cell
heights = (waveFunc, repetitions, t) => {
const indices = _.range(0, fidelity + 1).map(i => i / fidelity);
const f = waveFunc(t * base_x_stretch);
return indices.map(i => {
const scale = repetitions * base_x_stretch;
return f(i * scale);
});
}
Insert cell
showWave = (
waves,
t,
repetitions,
{ magFactor, magFactorRepetition },
description
) => {
const waveFunc = t => x => _.sum(waves.map(({ waveFunc }) => waveFunc(t)(x)));

const waveHeights = heights(
waveFunc,
repetitions / magFactorRepetition,
t / magFactor
);

const totalHeight =
_.sum(waves.map(({ amplitude }) => amplitude)) * 2 * scale + margin * 2;

const fundamental_frequency = gcd(waves.map(({ frequency }) => frequency));

const createWave = (heights, stretch) => {
const l = heights.length;
const unsimple = heights.map((height, index) => ({
x: (index / l) * stretch,
y: height
}));
const simple = simplify(unsimple, .01, true);

return simple.map(({ x, y }) => ({
time: x,
pressure_delta: y
}));
};

const wavePoints = createWave(waveHeights, stretch);

const svg = d3.create("svg").attr("viewBox", [0, 0, width, totalHeight]);

svg
.attr("id", description + "-path")
.append("path")
.attr("d", line(totalHeight)(wavePoints))
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-width", "3");

if (false) {
svg.append("path").attr("d");
}

return Object.assign(svg, {
waveFunc,
totalHeight,
wavePoints,
waveHeights,
createWave
});

/*return Object.assign(svg.node(), {
update(domain) {
const t = svg.transition().duration(750);
zx.domain(domain);
gx.transition(t).call(xAxis, zx);
path.transition(t).attr("d", line(data));
}
});*/
}
Insert cell
getHeight = ({ amplitude }) => Math.ceil(amplitude * 2 * scale) + margin * 2
Insert cell
line = totalHeight => {
const offset = totalHeight / 2;
return d3
.line()
.x(d => d.time * scale)
.y(d => -d.pressure_delta * scale + offset);
}
Insert cell
Insert cell
Insert cell
pianoSVG = notes => {
const keyWidth = 30;
const keyHeight = keyWidth * 2;
const keyHeightExpand = keyHeight * 1.05;
const keyPad = 2;

const computeColor = index =>
d3.interpolateLab(
d3.lab(83.813, -10.891, -11.484),
d3.lab(86.385, 17.854, 7.357)
)((index - smallest_index) / notes.length);

const svg = d3
.create('svg')
.attr('width', (keyWidth + keyPad) * notes.length + margin * 2)
.attr('height', keyHeightExpand + margin * 2);

const smallest_index = _.min(notes.map(({ index }) => index));

svg
.selectAll('rect')
.data(notes)
.join('rect')
.attr("width", keyWidth)
.attr("height", keyHeight)
.attr(
"x",
({ frequency, index }) =>
(index - smallest_index) * (keyWidth + keyPad) + margin
)
.attr("y", ({ frequency, index }) => margin)
.style("fill", ({ frequency, index }) => computeColor(index))
.style("rx", 2)
.on('mouseover', async function(event) {
// The 'this' variable refers to the underlying SVG element.
// We can select it directly, then use D3 attribute setters.
// (Note that 'this' is set when using "function() {}" definitions,
// but *not* when using arrow function "() => {}" definitions.)
d3.select(this)
.transition()
.attr('height', keyHeightExpand)
.attr('stroke', '#333')
.attr('stroke-width', 2);
})
.on('mouseout', async function(event, { frequency, index }) {
//Setting the stroke color to null removes it entirely.
d3.select(this)
.transition()
.attr('height', keyHeight)
.attr('stroke', null)
.style("fill", computeColor(index));
})
.on('mousedown', async function(event, { frequency, index }) {
press(index);
});

const press = (index, length) => {
let selection = svg
.selectAll('rect')
.filter((d, i) => i == index - smallest_index);

const oldColor = computeColor(index);
const newColor = d3.color(oldColor).darker(1);

selection.style("fill", newColor).attr('height', keyHeightExpand);
selection
.transition()
.style("fill", oldColor)
.attr('height', keyHeight);

synth.triggerAttackRelease(notes[index - smallest_index].frequency, length);
};

return Object.assign(svg, {
press,
notes: notes,
length: notes.length
});
return svg;
}
Insert cell
mySVG = pianoSVG(notes)
Insert cell
nodes = mySVG.node()
Insert cell
{
let elem = html`<button>a</button>`;
elem.onmousedown = () => {
mySVG.press(starting_piano_key + 0, .1);
mySVG.press(starting_piano_key + 4, .1);
};
return elem;
}
Insert cell
playButton = params => html`<button class="play" ${params}>Play ▸</Button>`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
combine = (a, b) => a.map((x, i) => x + b[i])
Insert cell
Insert cell
Insert cell
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