UNPKG

@sebastbake/music-tempo

Version:

Finding out tempo of the music

497 lines (447 loc) 16.8 kB
document.addEventListener("DOMContentLoaded", function () { "use strict"; var srVal = document.getElementById("sampleRateInput").value || 44100; var context = new AudioContext({ sampleRate: srVal }), trackGainNode = context.createGain(); trackGainNode.gain.value = 0.01; //beats var clickSound = document.getElementById("clickSound"), beatTimes, beatInterval, nextBeatTime = 0; clickSound.volume = 0.25; //visualisation var spectrogram, waveform, beatLines, scaleLines, spectrogramContainer, waveformContainer, audioSource, audioBuffer, minFQ = 77, maxFQ = 12000, BPO = 40, idAnimFrame, specHeight = getSpecHeight(minFQ, maxFQ, BPO); var secPerPx; //time var animStart, playbackTime, startedAt, pausedAt, duration, isPaused = true; //controls var playbackBar = document.getElementById("playbackBar"), btnTogglePlay, fileInput, iconTogglePlay, trackGainBar, clickGainBar, beatIndicator = document.getElementById("beatIndicator"), playbackTimeValue, isPlaybackBarMouseDown = false; var processAudioData = function (buffer) { audioBuffer = buffer; playbackBar.max = buffer.duration; var audioData = []; if (buffer.numberOfChannels > 1) { var channel1Data = buffer.getChannelData(0); var channel2Data = buffer.getChannelData(1); var length = channel1Data.length; for (var i = 0; i < length; i++) { audioData[i] = (channel1Data[i] + channel2Data[i]) / 2; } } else { audioData = buffer.getChannelData(0); } asyncCalcChain(audioData); } function getSpecHeight(minimumFrequency, maximumFreqency, binsPerOctave) { return (Math.ceil(binsPerOctave * Math.log(maximumFreqency / minimumFrequency) / Math.log(2)) | 0);; } function asyncCalcChain(audioData) { document.getElementById("bpmSpinner").classList.add("is-active"); document.getElementById("spectrogramSpinner").classList.add("is-active"); document.getElementById("waveformSpinner").classList.add("is-active"); asyncCalcTempo(audioData); } function asyncCalcTempo(audioData) { setTimeout(function() { console.time("MusicTempo"); var mt = new MusicTempo(audioData); console.timeEnd("MusicTempo"); document.getElementById("bpmSpinner").classList.remove("is-active"); document.getElementById("bpmValue").textContent = Math.round(mt.tempo) + " BPM"; document.getElementById("bpmValue").style.display = "inline-block"; beatIndicator.style.display = "inline-block"; beatTimes = mt.beats; beatInterval = mt.beatInterval; beatIndicator.style.animationDuration = beatInterval + "s"; asyncCalcAndDrawSpectrogram(audioData, beatTimes); }, 100); } function asyncCalcAndDrawSpectrogram(audioData, beatTimes) { setTimeout(function() { console.time("calcSpectrogram"); var res = getSpectrogramCQ(audioData); console.timeEnd("calcSpectrogram"); console.time("drawSpectrogram"); drawSpectrogram(res.spectrum, res.specMaxVal); drawBeatLines(res.spectrum.length, res.spectrum[0].length, beatTimes); drawScale(minFQ, maxFQ, res.spectrum[0].length); console.timeEnd("drawSpectrogram"); document.getElementById("spectrogramSpinner").classList.remove("is-active"); asyncCalcAndDrawWaveform(audioData, res.spectrum.length); }, 100); } function asyncCalcAndDrawWaveform(audioData, length) { setTimeout(function() { console.time("drawWaveform"); drawWaveform(audioData, length); console.timeEnd("drawWaveform"); resetTime(); //disabled playbackBar.disabled = false; btnTogglePlay.disabled = false; document.getElementById("waveformSpinner").classList.remove("is-active"); }, 100); } function initControls() { playbackTimeValue = document.getElementById("playbackTimeValue"); spectrogramContainer = document.getElementById("spectrogramContainer"); spectrogramContainer.style.height = (52 + specHeight) + "px"; waveformContainer = document.getElementById("waveformContainer"); document.getElementById("spectrogramTracker").style.height = (52 + specHeight) + "px"; document.getElementById("beatLinesSwitch").onchange = function () { var isVisible = document.getElementById("beatLinesSwitch").checked; if (isVisible) { beatLines.style.display = "block"; } else { beatLines.style.display = "none"; } }; //document.getElementById("beatIndicator").style.top = specHeight + "px"; spectrogramContainer, waveformContainer, btnTogglePlay = document.getElementById("btnTogglePlay"); btnTogglePlay.onclick = togglePlay; iconTogglePlay = document.getElementById("iconTogglePlay"); fileInput = document.getElementById("fileInput"); fileInput.onchange = function () { var files = document.getElementById("fileInput").files; if (files.length == 0) return; var reader = new FileReader(); reader.onload = function(fileEvent) { context.decodeAudioData(fileEvent.target.result, processAudioData); } reset(); playbackBar.disabled = true; btnTogglePlay.disabled = true; reader.readAsArrayBuffer(files[0]); } // playbackBar.addEventListener("change", function () { // seek(); // }); playbackBar.addEventListener("click", function () { seek(); }); playbackBar.addEventListener("mousedown", function () { isPlaybackBarMouseDown = true; }); document.addEventListener("mouseup", function () { isPlaybackBarMouseDown = false; }); window.addEventListener("blur", function () { if (!isPaused) { togglePlay(); } }); trackGainBar = document.getElementById("trackGainBar"); trackGainBar.oninput = function() { var val = parseFloat(trackGainBar.value, 10); trackGainNode.gain.value = val * val; } clickGainBar = document.getElementById("clickGainBar"); clickGainBar.oninput = function() { var val = parseFloat(clickGainBar.value, 10); clickSound.volume = val * val; } beatIndicator = document.getElementById("beatIndicator"); } function initAudioSource(buffer) { audioSource = context.createBufferSource(); audioSource.buffer = buffer; audioSource.connect(trackGainNode); trackGainNode.connect(context.destination); } function resetTime() { playbackTimeValue.textContent = "0c"; playbackTime = 0; pausedAt = 0; startedAt = 0; isPaused = true; nextBeatTime = 0; } function reset() { if (!isPaused) togglePlay(); startedAt = false; //if (idAnimFrame) cancelAnimationFrame(idAnimFrame); playbackBar.MaterialSlider.change(0); playbackTimeValue.textContent = "0c"; var shift = Math.round(spectrogramContainer.clientWidth / 2) + "px"; if (spectrogram) { spectrogram.style.left = shift; waveform.style.left = shift; beatLines.style.left = shift; } } function seek() { if (!startedAt) { resetTime(); idAnimFrame = requestAnimationFrame(animCycle); } var newPlaybackTime = parseFloat(playbackBar.value, 10); startedAt += playbackTime - newPlaybackTime; playbackTime = newPlaybackTime; for (nextBeatTime = 0; nextBeatTime < beatTimes.length; nextBeatTime++) { if (playbackTime <= beatTimes[nextBeatTime]) break; } if (!isPaused) { audioSource.stop(); initAudioSource(audioBuffer); audioSource.start(0, playbackTime); } } function togglePlay() { if (!startedAt) { resetTime(); idAnimFrame = requestAnimationFrame(animCycle); } if (isPaused) { initAudioSource(audioBuffer); startedAt += context.currentTime - pausedAt; audioSource.start(0, playbackTime); iconTogglePlay.textContent = "pause"; } else { audioSource.stop(); pausedAt = context.currentTime; iconTogglePlay.textContent = "play_arrow"; } isPaused = !isPaused; } function animCycle(timestamp) { if (!isPaused) { playbackTime = context.currentTime - startedAt; if (!isPlaybackBarMouseDown) { playbackBar.MaterialSlider.change(playbackTime); if (playbackTime >= playbackBar.max) { reset(); return; } } if (beatTimes[nextBeatTime] <= playbackTime || beatTimes[nextBeatTime] - playbackTime < 0.0166) { nextBeatTime++; clickSound.play(); beatIndicator.classList.remove("anim"); void beatIndicator.offsetWidth; beatIndicator.classList.add("anim"); } } playbackTimeValue.textContent = playbackTime.toFixed(2) + "s"; var shift = Math.round(spectrogramContainer.clientWidth / 2 + playbackTime / secPerPx * -1) + "px"; spectrogram.style.left = shift; waveform.style.left = shift; beatLines.style.left = shift; requestAnimationFrame(animCycle); } function getSpectrogramCQ(inputBuffer) { //var constQ = new ConstantQ(44100, 112, 14700, 29); var constQ = new ConstantQ(44100, minFQ, maxFQ, BPO); var len = inputBuffer.length; var bufferSize = constQ.fftLength; var hopSize = 882; secPerPx = 1 / (44100 / hopSize); var hammWindow = FFT.getHammingWindow(bufferSize); var length = inputBuffer.length; // var zerosStart = new Array(bufferSize - hopSize); // zerosStart.fill(0); // inputBuffer = zerosStart.concat(inputBuffer); inputBuffer = Array.from(inputBuffer); var zerosEnd = new Array(bufferSize - (inputBuffer.length % hopSize)); zerosEnd.fill(0); inputBuffer = inputBuffer.concat(zerosEnd); var spectrum = []; var specMaxVal = 0; for (var wndStart = 0; wndStart < length; wndStart += hopSize) { var wndEnd = wndStart + bufferSize; var re = []; var k = 0; for (var i = wndStart; i < wndEnd; i++) { re[k * 2] = /*hammWindow[k] */ inputBuffer[i]; re[k * 2 + 1] = 0; //re[k] = inputBuffer[i]; k++; } constQ.calculateMagintudes(re); for (var i = 0; i < constQ.magnitudes.length; i++) { if (specMaxVal < constQ.magnitudes[i]) specMaxVal = constQ.magnitudes[i]; } spectrum.push(constQ.magnitudes.slice()); } return {spectrum: spectrum, specMaxVal: specMaxVal}; } function drawWaveform(waveData, width) { if (!waveform) { var container = document.getElementById("waveformContainer"); waveform = document.createElement("canvas"); container.appendChild(waveform); } var ctx = waveform.getContext("2d"); var height = 100; var halfHeight = height / 2; length = waveData.length; var step = Math.round(length / width); waveform.width = width; waveform.height = height; waveform.style.width = width + "px"; waveform.style.height = height + "px"; waveform.style.left = Math.round(waveformContainer.clientWidth / 2) + "px"; var x = 0, sumPositive = 0, sumNegative = 0, maxPositive = 0, maxNegative = 0, kNegative = 0, kPositive = 0, drawIdx = step; for (var i = 0; i < length; i++) { if (i == drawIdx) { var p1 = maxNegative * halfHeight + halfHeight; ctx.strokeStyle = '#558b2f'; ctx.strokeRect(x, p1, 1, (maxPositive * halfHeight + halfHeight) - p1); var p2 = sumNegative / kNegative * halfHeight + halfHeight; ctx.strokeStyle = '#8bc34a'; ctx.strokeRect(x, p2, 1, (sumPositive / kPositive * halfHeight + halfHeight) - p2); x++; drawIdx += step; sumPositive = 0; sumNegative = 0; maxPositive = 0; maxNegative = 0; kNegative = 0; kPositive = 0; } else { if (waveData[i] < 0) { sumNegative += waveData[i]; kNegative++; if (maxNegative > waveData[i]) maxNegative = waveData[i]; } else { sumPositive += waveData[i]; kPositive++; if (maxPositive < waveData[i]) maxPositive = waveData[i]; } } } } function drawSpectrogram(spectrum, maxVal) { var scale = 1 / Math.log(maxVal + 1); //var gradient = chroma.scale(["black", "blue", "red", "yellow"]).domain([0, 0.03, 0.3, 0.7]); var gradient = chroma.scale(["white", "#448aff", "#f44336", "#ffee58"]).domain([0, 0.1, 0.3, 0.7]); //var gradient = chroma.scale(["#448aff", "white"]).domain([0, 0.3]); if (!spectrogram) { var container = document.getElementById("spectrogramContainer"); spectrogram = document.createElement("canvas"); container.appendChild(spectrogram); } var ctx = spectrogram.getContext("2d"); var width = spectrum.length; var height = spectrum[0].length; var shift = 6; var shiftWidth = width - shift; var shiftStart = Math.floor(height / 2); var shiftFactor = shiftStart / shift; spectrogram.width = width; spectrogram.height = height; spectrogram.style.width = (1 * width) + "px"; spectrogram.style.height = (1 * height) + "px"; spectrogram.style.left = Math.round(spectrogramContainer.clientWidth / 2) + "px"; spectrogram.style.transform = "scaleY(-1)"; var canvasData = ctx.getImageData(0, 0, width, height); for (var i = 0; i < shiftWidth; i++) { for (var j = 0; j < shiftStart; j++) { var index = (i + (shift - Math.round(j / shiftFactor)) + j * width) * 4; var val = gradient(Math.log(spectrum[i][j] + 1) * scale).rgb(); canvasData.data[index + 0] = val[0]; canvasData.data[index + 1] = val[1]; canvasData.data[index + 2] = val[2]; canvasData.data[index + 3] = 255; } for (; j < height; j++) { index = (i + j * width) * 4; //var val = 225 - (spectrum[i][j] * scale); //var val = gradient(Math.log(spectrum[i][j] + 1) * scale); val = gradient(Math.log(spectrum[i][j] + 1) * scale).rgb(); canvasData.data[index + 0] = val[0]; canvasData.data[index + 1] = val[1]; canvasData.data[index + 2] = val[2]; canvasData.data[index + 3] = 255; } } ctx.putImageData(canvasData, 0, 0); } function drawBeatLines(width, height, beats) { beats = beats.map(function(time) {return Math.round(time / secPerPx);}); if (!beatLines) { var container = document.getElementById("spectrogramContainer"); beatLines = document.createElement("canvas"); container.appendChild(beatLines); } var ctx = beatLines.getContext("2d"); beatLines.width = width; beatLines.height = height; beatLines.style.width = (1 * width) + "px"; beatLines.style.height = (1 * height) + "px"; beatLines.style.left = Math.round(spectrogramContainer.clientWidth / 2) + "px"; ctx.strokeStyle = "#616161"; for (var i = 0; i < beats.length; i++) { ctx.strokeRect(beats[i], 0, 0.5, height); } } function drawScale(startFQ, end, height) { if (!scaleLines) { var container = document.getElementById("spectrogramContainer"); scaleLines = document.createElement("canvas"); container.appendChild(scaleLines); } var ctx = scaleLines.getContext("2d"); var width = 200; scaleLines.width = width; scaleLines.height = height; scaleLines.style.left = (Math.round(spectrogramContainer.clientWidth / 2) - width) + "px"; var canvasData = ctx.getImageData(0, 0, width, height); for (var i = 0; i < width; i++) { for (var j = 0; j < height; j++) { var index = (i + j * width) * 4; var alpha = 255 / width * i; canvasData.data[index + 0] = 255; canvasData.data[index + 1] = 255; canvasData.data[index + 2] = 255; canvasData.data[index + 3] = alpha; } } ctx.putImageData(canvasData, 0, 0); var step = height / 8; ctx.font="10px Courier New"; ctx.fillStyle = "#9e9e9e"; ctx.strokeStyle = "#9e9e9e"; for (i = 0; i < height; i+=step) { // var idx = Math.round(i * factor); var hz = Math.round(Math.pow(Math.pow(2, 1 / BPO), i) * (minFQ * 2)); ctx.fillText(hz + " Hz", 150, (height - i) - 4); ctx.strokeRect(150, (height - i), 50, 0.5); } } initControls(); });