Public
Edited
May 10, 2023
2 stars
Insert cell
Insert cell
Plot.plot({
width: 1024,
height: 500,
x: {
grid: true,
label: "Frequency",
type: "log"
},
y: {
grid: true,
label: "dB"
},
marks: [
Plot.lineY(data, {
x: (x, i) => frequencies[i],
y: (y) => y
}),
Plot.dot(filtered, {
x: (x, i) => melToHertz(melMin + (i + 1) * melFilterWidth),
y: (y) => y,
stroke: "red"
}),
Plot.ruleY([-150, 0])
]
})
Insert cell
audioContext = new AudioContext()
Insert cell
analyser = {
const analyser = audioContext.createAnalyser();
analyser.fftSize = 1024;
// analyser.smoothingTimeConstant = 0.2;
analyser.channelCount = 1;
return analyser;
}
Insert cell
{
if (navigator.mediaDevices) {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: false,
googAutoGainControl: false,
googNoiseSuppression: false,
googHighpassFilter: false
},
video: false
});
const source = audioContext.createMediaStreamSource(stream);
source.connect(analyser);
}
}
Insert cell
mutable data = new Float32Array(analyser.frequencyBinCount)
Insert cell
mutable timeData = new Float32Array(analyser.fftSize)
Insert cell
Insert cell
{
let request = requestAnimationFrame(function draw(timestamp) {
// throttle to 60 fps
const fps = 60;
if (timestamp - prev < 1000 / fps) {
request = requestAnimationFrame(draw);
return;
}
mutable prev = timestamp;
analyser.getFloatFrequencyData(mutable data);
analyser.getFloatTimeDomainData(mutable timeData);
processFiltered(mutable data);
processData(mutable data, timestamp);
mutable data = mutable data;
mutable timeData = mutable timeData;

request = requestAnimationFrame(draw);
});

invalidation.then(() => cancelAnimationFrame(request));
}
Insert cell
frequencies = Array.from(
{ length: analyser.frequencyBinCount },
(x, i) => (audioContext.sampleRate / 2 / analyser.frequencyBinCount) * i
)
Insert cell
Insert cell
hertzToMel = (f) => {
return 2595 * Math.log10(1 + f / 700);
}
Insert cell
melToHertz = (m) => {
return 700 * (Math.pow(10, m / 2595) - 1);
}
Insert cell
viewof filterCount = Inputs.range([3, 100], {
label: "Filter Count",
step: 1,
value: 20
})
Insert cell
minFreq = 0
Insert cell
maxFreq = audioContext.sampleRate / 2
Insert cell
melMin = hertzToMel(minFreq)
Insert cell
melMax = hertzToMel(maxFreq)
Insert cell
melRange = melMax - melMin
Insert cell
melFilterWidth = (melMax - melMin) / (filterCount + 1)
Insert cell
Plot.plot({
width: width,
marks: [
...filterBank.map((f, fi) =>
Plot.line(f, {
x: (_, i) => frequencies[i],
y: (d) => d,
stroke: fi
})
),
Plot.ruleY([0])
]
})
Insert cell
filterBank = Array.from({ length: filterCount }, (_, i) => {
const melCenter = melMin + (i + 1) * melFilterWidth;
const hertzCenter = melToHertz(melCenter);
const hertzStart = melToHertz(melCenter - melFilterWidth);
const hertzEnd = melToHertz(melCenter + melFilterWidth);
return createFilter(
analyser.frequencyBinCount,
hertzStart,
hertzCenter,
hertzEnd
);
})
Insert cell
createFilter = (size, start, center, end) => {
const filter = new Float32Array(size);
const startBin = Math.floor(((size + 1) * start) / maxFreq);
const centerBin = Math.floor(((size + 1) * center) / maxFreq);
const endBin = Math.floor(((size + 1) * end) / maxFreq);
for (let i = startBin; i < centerBin; i++) {
filter[i] = (i - startBin) / (centerBin - startBin);
}
for (let i = centerBin; i < endBin; i++) {
filter[i] = (endBin - i) / (endBin - centerBin);
}
return filter;
}
Insert cell
mutable filtered = new Float32Array(filterCount)
Insert cell
function processFiltered(data) {
for (let i = 0; i < filterCount; i++) {
const filter = filterBank[i];
let sum = 0;
let coeff = 0;
for (let j = 0; j < analyser.frequencyBinCount; j++) {
sum += filter[j] * data[j];
coeff += filter[j];
}
mutable filtered[i] = sum / coeff;
}

mutable filtered = mutable filtered;
}
Insert cell
Insert cell
function processData(data) {
// Normalize the filtered data
const max = Math.min(...filtered);
const normalized = filtered.map((value) => value / max);

// Map the normalized data to colors for the LED strip
for (let i = 0; i < strip.length; i++) {
const index = Math.floor(i / (strip.length / filterCount));
const value = normalized[index];
const hue = (index / filterCount) * 270;
const color = getColor(value, hue);
strip[i] = color;
}
mutable strip = mutable strip;
}
Insert cell
function getColor(value, hue) {
const saturation = 1;
const brightness = value;
const rgb = d3.color(hsv(hue, saturation, brightness));
return [rgb.r, rgb.g, rgb.b];
}
Insert cell
mutable strip = Array.from({ length: 20 }, (_) => [0, 0, 0])
Insert cell
Plot.plot({
padding: 0,
width: width,
x: { tickSize: 0, tickFormat: (x) => "" },
marks: [
Plot.cellX(strip, {
fill: ([r, g, b]) => `rgb(${r},${g}, ${b})`
})
]
})
Insert cell
hsv = (await require("d3-hsv")).hsv
Insert cell
Insert cell
rms = Math.sqrt(
timeData.reduce((acc, curr) => curr ** 2 + acc, 0) / timeData.length
)
Insert cell
timeData.reduce(x)
Insert cell
10 * Math.log10(rms ** 2)
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