Microphone pitch detection
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
body { | |
font-family: sans-serif; | |
} | |
.piano rect.key { | |
stroke: #111111; | |
} | |
.piano rect.key.white { | |
fill: #ffffff; | |
} | |
.piano rect.key.black { | |
fill: #000000; | |
} | |
</style> | |
<body> | |
<div id="target"> | |
<p>Access to microphone audio is required.</p> | |
</div> | |
<script src="https://d3js.org/d3.v5.min.js"></script> | |
<script> | |
function main() { | |
var target = d3.select("#target") | |
var AudioContext = window.AudioContext || window.webkitAudioContext | |
navigator.mediaDevices.getUserMedia({ audio: true }) | |
.then(function(stream) { init(stream); }) | |
.catch(function(reason) { | |
console.error(reason) | |
target.append("p").text("Failed to capture microphone audio.") | |
target.append("p").append("code").text(reason) | |
}) | |
function init(stream) { | |
// Set up audio source and analyser | |
console.log("Start capturing microphone audio") | |
var ctx = new AudioContext(); | |
var source = ctx.createMediaStreamSource(stream) | |
var analyser = ctx.createAnalyser() | |
var fftSize = 1024 * 8 | |
analyser.fftSize = fftSize | |
analyser.smoothingTimeConstant = 0.7 | |
source.connect(analyser) | |
function binFromFreq(f) { | |
return Math.round(fftSize * f / ctx.sampleRate); | |
} | |
function FreqFromBin(b) { | |
return b * ctx.sampleRate / fftSize; | |
} | |
function PitchFromFreq(f) { | |
return (Math.round(12 * (Math.log2(f) - Math.log2(tuning))) % 12 + 12) % 12 | |
} | |
var bins = analyser.frequencyBinCount | |
var tuning = 440 | |
var minFrequency = tuning / 4 | |
var maxFrequency = tuning * 6 | |
var minBin = binFromFreq(minFrequency) | |
var maxBin = binFromFreq(maxFrequency) | |
var frequencyData = new Float32Array(maxBin); | |
console.log(minFrequency, maxFrequency, minBin, maxBin) | |
function getPitchIntensities() { | |
// Get spectrum data from analyser | |
analyser.getFloatFrequencyData(frequencyData) | |
// Average energy for each pitch | |
var intensities = new Float64Array(12) | |
var counts = new Uint32Array(12) | |
for (var i=minBin; i <= maxBin; i++) { | |
var p = PitchFromFreq(FreqFromBin(i)) | |
var db = frequencyData[i] | |
if (!isNaN(db)) { | |
intensities[p] += Math.pow(10, frequencyData[i] / 10) | |
counts[p] += 1 | |
} | |
} | |
for (var p=0; p<12; p++) { | |
intensities[p] = 10 * Math.log10(intensities[p] / counts[p]) | |
} | |
return intensities; | |
} | |
// Set up D3 elements | |
var width = 960 | |
var height = 400 | |
var svg = target.html("").append("svg").attr("width", width).attr("height", height) | |
var padding = 30 | |
var round = 8 | |
var whiteHeight = height - 2 * padding | |
var blackHeight = 0.8 * whiteHeight | |
var plot = svg.append("g") | |
.attr("class", "plot") | |
.attr("transform", "translate(" + padding + "," + (padding) + ")") | |
var xScale = d3.scaleLinear().domain([-0.5, 11.5]).range([0, width - 2 * padding]) | |
var yScale = d3.scaleLinear().domain([-60, -40]).range([0, blackHeight]).clamp(true) | |
var wScale = yScale.copy().range([0, xScale(1) - xScale(0)]).clamp(true) | |
var colorScale = d3.scaleLinear().domain([-60, -45, -30]).range(["#bb0000", "#ffee00", "#66bb00"]).clamp(true) | |
var bandWidth = xScale(1) - xScale(0) | |
var pianoBack = plot.append("g").attr("class", "piano") | |
var white = pianoBack.append("g").attr("class", "white") | |
var whiteDims = [[-0.5, 1.5], [1, 2], [3, 1.5], [4.5, 1.5], [6, 2], [8, 2], [10, 1.5]] | |
whiteDims.forEach(function(d) { | |
white.append("rect") | |
.attr("class", "key white") | |
.attr("x", xScale(d[0])).attr("y", -round) | |
.attr("width", d[1] * bandWidth).attr("height", whiteHeight + round) | |
.attr("rx", round).attr("ry", round) | |
}) | |
var black = pianoBack.append("g").attr("class", "black") | |
var blackDims = [0.5, 2.5, 5.5, 7.5, 9.5] | |
blackDims.forEach(function (d) { | |
black.append("rect") | |
.attr("class", "key black") | |
.attr("x", xScale(d)).attr("y", -round) | |
.attr("width", bandWidth).attr("height", blackHeight + round) | |
.attr("rx", round).attr("ry", round) | |
}) | |
var barPlot = plot.append("g") | |
.attr("class", "bars") | |
var pianoFront = plot.append("g").attr("class", "piano") | |
pianoFront.append("rect") | |
.attr("x", xScale(-1)).attr("y", -padding) | |
.attr("width", xScale(12) - xScale(-1)).attr("height", padding) | |
.attr("fill", "#333333") | |
// Draw function | |
function draw(data) { | |
var bars = barPlot.selectAll("rect") | |
.data(data) | |
var enter = bars.enter() | |
.append("rect") | |
.attr("y", 0) | |
.attr("stroke", "#888888") | |
bars = bars.merge(enter) | |
bars | |
.attr("x", function (d, i) { return xScale((i + 9) % 12) - 0.5 * wScale(d); }) | |
.attr("width", wScale) | |
.attr("height", yScale) | |
.attr("fill", colorScale) | |
} | |
// Set up animation toggling | |
var timeout = 50 | |
function animate() { | |
data = getPitchIntensities() | |
draw(data) | |
setTimeout(animate, timeout) | |
} | |
animate() | |
} | |
} | |
document.addEventListener('DOMContentLoaded', main) | |
</script> | |
</body> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment