strumming-metronome
Version:
A simple JavaScript metronome for strumming practice.
113 lines (92 loc) • 4.76 kB
JavaScript
import { beatIds, upQuarterBeatIds, downQuarterBeatIds, subdivisionConfigs } from './constants.js';
import { playBuffer, getDownStrumBuffer, getUpStrumBuffer } from './audio.js';
import {
resetVisualStates,
setPlayingState,
setStoppedState,
setControlsEnabled,
setupButtonToggles,
injectTable
} from './ui.js';
export function createMetronomeEngine(timeSelection) {
const oldButton = document.getElementById("metronome-btn");
const button = oldButton.cloneNode(true);
oldButton.parentNode.replaceChild(button, oldButton);
const metronomeTable = injectTable(timeSelection);
setupButtonToggles(metronomeTable);
const container = document.querySelector("#metronome-container");
const bpmInput = container.querySelector("#bpm-input");
const icon = container.querySelector("#metronome-icon");
const text = container.querySelector("#metronome-text");
const timeSignature = container.querySelector("#metronome-time-signature");
const subdivision = container.querySelector("#metronome-subdivision");
const beats = beatIds[timeSelection].map(id => document.getElementById(id));
const upQuarterBeatButtons = upQuarterBeatIds[timeSelection].map(id => document.getElementById(id));
const downQuarterBeatButtons = downQuarterBeatIds[timeSelection].map(id => document.getElementById(id));
let currentBeatIndex = 0;
let interval;
function playMetronome() {
if (interval) {
clearInterval(interval);
interval = null;
setStoppedState(icon, text, button);
resetVisualStates(beats, downQuarterBeatButtons, upQuarterBeatButtons);
currentBeatIndex = 0;
setControlsEnabled(true, timeSignature, bpmInput, upQuarterBeatButtons, downQuarterBeatButtons, subdivision);
} else {
const bpm = parseInt(bpmInput.value, 10);
let subBeat = 0;
let activeDownButton = document.getElementById(downQuarterBeatIds[timeSelection][0]);
let activeUpButton = document.getElementById(upQuarterBeatIds[timeSelection][0]);
if (activeDownButton.classList.contains("metronome-selected-button")) {
activeDownButton.firstElementChild.className = "metronome-arrow-down-white-icon";
activeDownButton.classList.add("metronome-active-btn");
playBuffer(getDownStrumBuffer());
}
if (activeUpButton.classList.contains("metronome-selected-button")) {
activeUpButton.firstElementChild.className = "metronome-arrow-up-white-icon";
activeUpButton.classList.add("metronome-active-btn");
playBuffer(getUpStrumBuffer());
}
beats[currentBeatIndex].style.color = "red";
const baseTimeSignature = timeSelection.replace("-triplet", "");
const subdivisionType = timeSelection.includes("-triplet") ? "triplet" : "sixteenth";
const config = subdivisionConfigs[baseTimeSignature][subdivisionType];
let beatDivide = config.beatDivide;
let subBeatMultiply = config.subBeatMultiply;
let effectiveBpm = bpm * (config.bpmMultiplier || 1);
interval = setInterval(() => {
resetVisualStates(beats, downQuarterBeatButtons, upQuarterBeatButtons);
beats[currentBeatIndex].style.color = "black";
subBeat = (subBeat + 1) % beatDivide;
if (subBeat === 0) {
currentBeatIndex = (currentBeatIndex + 1) % beats.length;
}
beats[currentBeatIndex].style.color = "red";
let subBeatValue;
if (subdivisionType === "triplet") {
const tripletMap = [0, 33, 67];
subBeatValue = tripletMap[subBeat];
} else {
subBeatValue = subBeat * subBeatMultiply;
}
const subBeatId = `${currentBeatIndex + 1}.${subBeatValue}`;
activeDownButton = document.getElementById(`down-${subBeatId}`);
activeUpButton = document.getElementById(`up-${subBeatId}`);
if (activeDownButton && activeDownButton.classList.contains("metronome-selected-button")) {
activeDownButton.firstElementChild.className = "metronome-arrow-down-white-icon";
activeDownButton.classList.add("metronome-active-btn");
playBuffer(getDownStrumBuffer());
}
if (activeUpButton && activeUpButton.classList.contains("metronome-selected-button")) {
activeUpButton.firstElementChild.className = "metronome-arrow-up-white-icon";
activeUpButton.classList.add("metronome-active-btn");
playBuffer(getUpStrumBuffer());
}
}, (60 / effectiveBpm) * 1000 / beatDivide);
setPlayingState(icon, text, button);
setControlsEnabled(false, timeSignature, bpmInput, upQuarterBeatButtons, downQuarterBeatButtons, subdivision);
}
}
button.addEventListener("click", playMetronome);
}