From eea0e3f879a4b7ea62c2d7159c8a4025a1592425 Mon Sep 17 00:00:00 2001 From: Paul Mathieu Date: Fri, 11 Apr 2025 09:38:16 -0700 Subject: [PATCH] Adjust frequency, hold note, better synth --- index.css | 35 +++++++---- index.html | 61 ++++++++++++++++++- index.js | 173 +++++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 224 insertions(+), 45 deletions(-) diff --git a/index.css b/index.css index a8b26b8..1af85c7 100644 --- a/index.css +++ b/index.css @@ -1,17 +1,19 @@ .whiteKey { color: white; + background-color: white; border: solid 1px; border-color: grey; - height: 67pt; -/* - max-height: 60pt; - max-width: 20pt; - */ position: relative; - left: 7.5pt; - width: 20pt; display: inline-block; + height: 67pt; + width: 20pt; +} + +.blackKeyOuter { + position: relative; + left: -7.5pt; + z-index: 1; } .blackKey { @@ -19,12 +21,19 @@ background-color: black; position: absolute; - z-index: 1; + display: inline-block; height: 40pt; width: 15pt; - /* - max-height: 40pt; - max-width: 15pt; - */ - display: inline-block; +} + +.playing { + background-color: lime; +} + +.held { + background-color: gold; +} + +#keyboard { + white-space: nowrap; } diff --git a/index.html b/index.html index 1ecd53c..c57a983 100644 --- a/index.html +++ b/index.html @@ -11,8 +11,65 @@
-
-
+
+
+ +
+
+
+
+

-

+ +
+
+ +
+
+
+ Tuning + + + + + + + +
+
+
+
diff --git a/index.js b/index.js index dce83d8..de83689 100644 --- a/index.js +++ b/index.js @@ -1,80 +1,142 @@ 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 = []; -function playNote(frequency, duration) { - let currentGain = 1.0; +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 = f / 20000; - const a = 4.0; - const b = 0.2; + 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); + const oscillator = new OscillatorNode(audioCtx, {frequency}); const gain = new GainNode(audioCtx); - oscillator.type = 'sine'; - oscillator.frequency.value = frequency; // value in hertz - oscillator.connect(gain); + const g = bassBoost(frequency) * currentGain; gain.gain.setValueAtTime(0, audioCtx.currentTime); - gain.gain.exponentialRampToValueAtTime(bassBoost(frequency) * currentGain, audioCtx.currentTime + 0.010); - gain.gain.linearRampToValueAtTime(1e-10, audioCtx.currentTime + 0.500); - gain.connect(audioCtx.destination); + gain.gain.linearRampToValueAtTime(g, audioCtx.currentTime + 0.010); + gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + duration); + + oscillator.connect(gain).connect(audioCtx.destination); oscillator.start(); - setTimeout(() => {oscillator.stop();}, duration); + 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 noteFrequency(key) { +function keyIndex(key) { let [note, octave, ] = key.split(/(\d)/); octave = parseInt(octave); - const interval = kNotes.indexOf(note) - kNotes.indexOf('A') + octave * 12; + return kNotes.indexOf(note) - kNotes.indexOf('A') + octave * 12; +} - let freq = kA0Frequency * Math.pow(kSemiToneMultiplier, interval); +function noteFrequency(key) { + return frequencies[keyIndex(key)]; +} - return freq; +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 note = event.srcElement.id; - const freq = noteFrequency(note); - playNote(freq, 500); + 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'); - if (note.includes('#') || note.includes('b')) { - key.classList.add('blackKey'); - } else { - key.classList.add('whiteKey'); - } 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() { +function setupKeyboard(firstKey=kFirstKey, numKeys=kNumKeys) { const keyboard = document.getElementById('keyboard'); keyboard.innerHTML = ''; - let curIndex = kNotes.indexOf(kFirstKey.charAt(0)); - let octave = parseInt(kFirstKey.charAt(1)); + let curIndex = kNotes.indexOf(firstKey.charAt(0)); + let octave = parseInt(firstKey.charAt(1)); - for (let i = 0; i < kNumKeys; i++) { + for (let i = 0; i < numKeys; i++) { const note = kNotes[curIndex] + octave; const key = makeKey(note) keyboard.appendChild(key); @@ -87,6 +149,57 @@ function setupKeyboard() { } } +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(); + setupKeyboard('C2', 49); + tuneEqual(); + setupListeners(); });