Implement tunings

This commit is contained in:
Paul Mathieu 2025-04-11 17:19:04 -07:00
parent ebba9f43a2
commit 865823b00d
2 changed files with 140 additions and 57 deletions

View File

@ -15,11 +15,10 @@
<div id="keyboard" class="overflow-auto"></div> <div id="keyboard" class="overflow-auto"></div>
<label> <label>
Volume Volume
<input type="range" min="0.0" max="2.0" value="1.0" step="0.01" id="range"> <input type="range" min="0.0" max="2.0" value="1.0" step="0.01" id="volume">
</label> </label>
</article> </article>
<section id="options"> <section id="options">
<form>
<article> <article>
<h3 id="current-note">-</h3> <h3 id="current-note">-</h3>
<label> <label>
@ -32,43 +31,45 @@
<small>Frequency</small> <small>Frequency</small>
<input type="number" id="current-frequency" step="0.01"> <input type="number" id="current-frequency" step="0.01">
</label> </label>
<label>
<small id="adjust-value">- Hz</small>
<input type="range" min="0.944" max="1.059" value="1.0" step="0.001" id="adjust">
</label>
</article> </article>
<article> <article>
<fieldset> <fieldset>
<legend><small>Tuning</small></legend> <legend><small>Tuning</small></legend>
<label> <label>
<input type="radio" name="tuning"> <input type="radio" name="tuning" value="equal">
Equal temperament Equal temperament
</label> </label>
<label> <label>
<input type="radio" name="tuning"> <input type="radio" name="tuning" value="5th">
Perfect 5th Perfect 5th
</label> </label>
<label> <label>
<input type="radio" name="tuning"> <input type="radio" name="tuning" value="4th">
Perfect 4th Perfect 4th
</label> </label>
<label> <label>
<input type="radio" name="tuning"> <input type="radio" name="tuning" value="maj3rd">
Perfect major 3rd Perfect major 3rd
</label> </label>
<label> <label>
<input type="radio" name="tuning"> <input type="radio" name="tuning" value="min3rd">
Perfect minor 3rd Perfect minor 3rd
</label> </label>
<div id="tune-from" style="display: none">
<label> <label>
From: From:
<select name="tuning-from"> <select id="tuning-from"></select>
<option selected disabled value></option>
<option>C2</option>
</select>
</label> </label>
</div> <button id="tune">Tune</button>
<button>Tune</button>
</fieldset> </fieldset>
</article> </article>
</form> <article>
<button class="secondary" id="reset">Reset all</button>
<button class="secondary" id="zero">Zero all</button>
</article>
</section> </section>
</main> </main>
</body> </body>

102
index.js
View File

@ -98,7 +98,19 @@ class Voice {
} }
} }
function radioValue(name) {
try {
return document.getElementsByName(name).values().filter(e => e.checked).next().value.value;
} catch (e) {
// TypeError
}
return undefined;
}
function playNote(frequency, duration=86400) { function playNote(frequency, duration=86400) {
if (frequency < kA0Frequency) {
return undefined;
}
const voice = new Voice(frequency); const voice = new Voice(frequency);
voice.play(duration); voice.play(duration);
return voice; return voice;
@ -116,10 +128,16 @@ function noteFrequency(key) {
} }
function setFrequency(key, frequency) { function setFrequency(key, frequency) {
frequencies[keyIndex(key)] = frequency; const index = keyIndex(key);
for (let f = frequency, i = index; i >= 0; f *= 0.5, i -= 12) {
frequencies[i] = f;
}
for (let f = frequency * 2, i = index + 12; i < 88; f *= 2, i += 12) {
frequencies[i] = f;
}
} }
function tuneEqual() { function tuneAllEqual() {
const startFrequency = kA0Frequency; const startFrequency = kA0Frequency;
let frequency = startFrequency; let frequency = startFrequency;
for (let i = 0; i < kMaxKeys; i++) { for (let i = 0; i < kMaxKeys; i++) {
@ -132,7 +150,8 @@ function updateOptions(key) {
const noteSpan = document.getElementById("current-note"); const noteSpan = document.getElementById("current-note");
const frequencySpan = document.getElementById("current-frequency"); const frequencySpan = document.getElementById("current-frequency");
noteSpan.innerHTML = key; noteSpan.innerHTML = key;
frequencySpan.value = noteFrequency(key).toFixed(2); const freq = noteFrequency(key);
frequencySpan.value = freq.toFixed(2);
const index = keyIndex(key); const index = keyIndex(key);
const hold = document.getElementById("hold"); const hold = document.getElementById("hold");
if (holds[index] !== undefined) { if (holds[index] !== undefined) {
@ -140,6 +159,8 @@ function updateOptions(key) {
} else { } else {
hold.checked = false; hold.checked = false;
} }
document.getElementById("adjust").value = 1.0;
document.getElementById("adjust-value").innerHTML = freq.toFixed(2) + ' Hz';
} }
function onKeyClick(event) { function onKeyClick(event) {
@ -148,6 +169,10 @@ function onKeyClick(event) {
if (holds[keyIndex(key)] === undefined) { if (holds[keyIndex(key)] === undefined) {
playNote(noteFrequency(key), 0.5); playNote(noteFrequency(key), 0.5);
} }
const currentKey = document.getElementById("current-note").innerHTML;
if (currentKey !== '-') {
document.getElementById('tuning-from').value = currentKey;
}
updateOptions(key); updateOptions(key);
keySpan.classList.add('playing'); keySpan.classList.add('playing');
@ -174,6 +199,7 @@ function makeKey(note) {
function setupKeyboard(firstKey=kFirstKey, numKeys=kNumKeys) { function setupKeyboard(firstKey=kFirstKey, numKeys=kNumKeys) {
const keyboard = document.getElementById('keyboard'); const keyboard = document.getElementById('keyboard');
const tuningFrom = document.getElementById('tuning-from');
keyboard.innerHTML = ''; keyboard.innerHTML = '';
let curIndex = kNotes.indexOf(firstKey.charAt(0)); let curIndex = kNotes.indexOf(firstKey.charAt(0));
@ -183,6 +209,9 @@ function setupKeyboard(firstKey=kFirstKey, numKeys=kNumKeys) {
const note = kNotes[curIndex] + octave; const note = kNotes[curIndex] + octave;
const key = makeKey(note) const key = makeKey(note)
keyboard.appendChild(key); keyboard.appendChild(key);
const opt = document.createElement('option');
opt.innerHTML = note;
tuningFrom.appendChild(opt);
curIndex += 1; curIndex += 1;
if (curIndex >= kNotes.length) { if (curIndex >= kNotes.length) {
@ -214,14 +243,33 @@ function refreshHolds() {
} }
} }
function tuneFraction(key, fromKey, fraction) {
const fromIndex = keyIndex(fromKey);
const freq = frequencies[fromIndex] * fraction;
setFrequency(key, freq);
}
function tuneEqual(key, fromKey) {
const index = keyIndex(key);
const fromIndex = keyIndex(fromKey);
const diff = index - fromIndex;
const freq = frequencies[fromIndex] * Math.pow(kSemiToneMultiplier, diff);
setFrequency(key, freq);
}
function setupListeners() { function setupListeners() {
const freq = document.getElementById("current-frequency"); const freq = document.getElementById("current-frequency");
freq.onchange = e => { const adjust = document.getElementById("adjust");
freq.oninput = e => {
const key = document.getElementById("current-note").innerHTML; const key = document.getElementById("current-note").innerHTML;
setFrequency(key, parseFloat(freq.value)); const adj = parseFloat(adjust.value);
const fre = parseFloat(freq.value);
const newFreq = adj * fre;
setFrequency(key, newFreq);
refreshHolds(); refreshHolds();
document.getElementById("adjust-value").innerHTML = newFreq.toFixed(2) + ' Hz';
}; };
freq.oninput = freq.onchange; adjust.oninput = freq.oninput;
const hold = document.getElementById("hold"); const hold = document.getElementById("hold");
hold.onchange = e => { hold.onchange = e => {
@ -232,16 +280,50 @@ function setupListeners() {
removeHold(key); removeHold(key);
} }
} }
const range = document.getElementById("range"); const volume = document.getElementById("volume");
range.oninput = e => { volume.oninput = e => {
const val = parseFloat(range.value); const val = parseFloat(volume.value);
globalVolume.gain.value = val; globalVolume.gain.value = val;
refreshHolds(); refreshHolds();
} }
document.getElementById("reset").onclick = () => {
tuneAllEqual();
refreshHolds();
const currentKey = document.getElementById("current-note").innerHTML;
if (currentKey !== '-') {
updateOptions(currentKey);
}
}
document.getElementById("zero").onclick = () => {
frequencies.fill(0.0);
refreshHolds();
const currentKey = document.getElementById("current-note").innerHTML;
if (currentKey !== '-') {
updateOptions(currentKey);
}
}
document.getElementById('tune').onclick = () => {
const key = document.getElementById("current-note").innerHTML;
const fromkey = document.getElementById('tuning-from').value;
const method = radioValue('tuning');
if (method === 'equal') {
tuneEqual(key, fromkey);
} else if (method === '5th') {
tuneFraction(key, fromkey, 3/2);
} else if (method === '4th') {
tuneFraction(key, fromkey, 4/3);
} else if (method === 'maj3rd') {
tuneFraction(key, fromkey, 5/4);
} else if (method === 'min3rd') {
tuneFraction(key, fromkey, 6/5);
}
refreshHolds();
updateOptions(key);
}
} }
window.addEventListener('load', e => { window.addEventListener('load', e => {
setupKeyboard('C2', 49); setupKeyboard('C2', 49);
tuneEqual(); tuneAllEqual();
setupListeners(); setupListeners();
}); });