const kNumKeys = 49; const kMaxKeys = 88; const kFirstKey = 'C2'; const kNotes = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'G#', 'A', 'Bb', 'B']; const kA0Frequency = 27.5; const kSemiToneMultiplier = Math.pow(2, 1/12); const audioCtx = new AudioContext(); const frequencies = []; const holds = []; let globalVolume = 1.0; /* * TODO: * - smooth frequency adjustments * - auto octave update * - implement perfect 5/4/3M/3m * - keyboard playing (qwerty) */ function playNote(frequency, duration=86400) { let currentGain = globalVolume; const bassBoost = f => { const t = Math.pow(f / 20000, 1/10); const a = 1.0; const b = 0.05; return b * t + a * (1 - t); }; const oscillators = []; while (frequency < 20000) { const oscillator = new OscillatorNode(audioCtx, {frequency}); const gain = new GainNode(audioCtx); const g = bassBoost(frequency) * currentGain; gain.gain.setValueAtTime(0, audioCtx.currentTime); gain.gain.linearRampToValueAtTime(g, audioCtx.currentTime + 0.010); gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + duration); oscillator.connect(gain).connect(audioCtx.destination); oscillator.start(); oscillators.push(oscillator); // break; // only one frequency for now frequency *= 2; currentGain *= 0.3; } const stop = () => { for (let oscillator of oscillators) { oscillator.stop(); } }; setTimeout(stop, duration*1000); return stop; } function keyIndex(key) { let [note, octave, ] = key.split(/(\d)/); octave = parseInt(octave); return kNotes.indexOf(note) - kNotes.indexOf('A') + octave * 12; } function noteFrequency(key) { return frequencies[keyIndex(key)]; } function setFrequency(key, frequency) { frequencies[keyIndex(key)] = frequency; } function tuneEqual() { const startFrequency = kA0Frequency; let frequency = startFrequency; for (let i = 0; i < kMaxKeys; i++) { frequencies[i] = frequency; frequency *= kSemiToneMultiplier; } } function updateOptions(key) { const noteSpan = document.getElementById("current-note"); const frequencySpan = document.getElementById("current-frequency"); noteSpan.innerHTML = key; frequencySpan.value = noteFrequency(key).toFixed(2); const index = keyIndex(key); const hold = document.getElementById("hold"); if (holds[index] !== undefined) { hold.checked = true; } else { hold.checked = false; } } function onKeyClick(event) { const keySpan = event.srcElement; const key = keySpan.id; playNote(noteFrequency(key), 0.5); updateOptions(key); keySpan.classList.add('playing'); setTimeout(() => { keySpan.classList.remove('playing'); }, 500) } function makeKey(note) { const key = document.createElement('span'); key.id = note; key.addEventListener('click', onKeyClick); if (note.includes('#') || note.includes('b')) { key.classList.add('blackKey'); const container = document.createElement('span'); container.classList.add('blackKeyOuter'); container.appendChild(key); return container; } key.classList.add('whiteKey'); return key; } function setupKeyboard(firstKey=kFirstKey, numKeys=kNumKeys) { const keyboard = document.getElementById('keyboard'); keyboard.innerHTML = ''; let curIndex = kNotes.indexOf(firstKey.charAt(0)); let octave = parseInt(firstKey.charAt(1)); for (let i = 0; i < numKeys; i++) { const note = kNotes[curIndex] + octave; const key = makeKey(note) keyboard.appendChild(key); curIndex += 1; if (curIndex >= kNotes.length) { octave += 1; curIndex = curIndex % kNotes.length; } } } function addHold(key) { holds[keyIndex(key)] = playNote(noteFrequency(key)); document.getElementById(key).classList.add('held'); } function removeHold(key) { const index = keyIndex(key); if (holds[index] !== undefined) { holds[index](); } delete holds[index]; document.getElementById(key).classList.remove('held'); } function refreshHolds() { for (let i = 0; i < holds.length; i++) { if (holds[i] !== undefined) { holds[i](); holds[i] = playNote(frequencies[i]); } } } function setupListeners() { const freq = document.getElementById("current-frequency"); freq.onchange = e => { const key = document.getElementById("current-note").innerHTML; setFrequency(key, parseFloat(freq.value)); refreshHolds(); }; freq.oninput = freq.onchange; const hold = document.getElementById("hold"); hold.onchange = e => { const key = document.getElementById("current-note").innerHTML; if (hold.checked) { addHold(key); } else { removeHold(key); } } const range = document.getElementById("range"); range.onchange = e => { const val = parseFloat(range.value); globalVolume = val; refreshHolds(); } } window.addEventListener('load', e => { setupKeyboard('C2', 49); tuneEqual(); setupListeners(); });