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,60 +15,61 @@
<div id="keyboard" class="overflow-auto"></div>
<label>
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>
</article>
<section id="options">
<form>
<article>
<h3 id="current-note">-</h3>
<article>
<h3 id="current-note">-</h3>
<label>
<input type="checkbox" role="switch" id="hold">
Hold
</label>
</article>
<article>
<label>
<small>Frequency</small>
<input type="number" id="current-frequency" step="0.01">
</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>
<fieldset>
<legend><small>Tuning</small></legend>
<label>
<input type="checkbox" role="switch" id="hold">
Hold
<input type="radio" name="tuning" value="equal">
Equal temperament
</label>
</article>
<article>
<label>
<small>Frequency</small>
<input type="number" id="current-frequency" step="0.01">
<input type="radio" name="tuning" value="5th">
Perfect 5th
</label>
</article>
<article>
<fieldset>
<legend><small>Tuning</small></legend>
<label>
<input type="radio" name="tuning">
Equal temperament
</label>
<label>
<input type="radio" name="tuning">
Perfect 5th
</label>
<label>
<input type="radio" name="tuning">
Perfect 4th
</label>
<label>
<input type="radio" name="tuning">
Perfect major 3rd
</label>
<label>
<input type="radio" name="tuning">
Perfect minor 3rd
</label>
<div id="tune-from" style="display: none">
<label>
From:
<select name="tuning-from">
<option selected disabled value></option>
<option>C2</option>
</select>
</label>
</div>
<button>Tune</button>
</fieldset>
</article>
</form>
<label>
<input type="radio" name="tuning" value="4th">
Perfect 4th
</label>
<label>
<input type="radio" name="tuning" value="maj3rd">
Perfect major 3rd
</label>
<label>
<input type="radio" name="tuning" value="min3rd">
Perfect minor 3rd
</label>
<label>
From:
<select id="tuning-from"></select>
</label>
<button id="tune">Tune</button>
</fieldset>
</article>
<article>
<button class="secondary" id="reset">Reset all</button>
<button class="secondary" id="zero">Zero all</button>
</article>
</section>
</main>
</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) {
if (frequency < kA0Frequency) {
return undefined;
}
const voice = new Voice(frequency);
voice.play(duration);
return voice;
@ -116,10 +128,16 @@ function noteFrequency(key) {
}
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;
let frequency = startFrequency;
for (let i = 0; i < kMaxKeys; i++) {
@ -132,7 +150,8 @@ 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 freq = noteFrequency(key);
frequencySpan.value = freq.toFixed(2);
const index = keyIndex(key);
const hold = document.getElementById("hold");
if (holds[index] !== undefined) {
@ -140,6 +159,8 @@ function updateOptions(key) {
} else {
hold.checked = false;
}
document.getElementById("adjust").value = 1.0;
document.getElementById("adjust-value").innerHTML = freq.toFixed(2) + ' Hz';
}
function onKeyClick(event) {
@ -148,6 +169,10 @@ function onKeyClick(event) {
if (holds[keyIndex(key)] === undefined) {
playNote(noteFrequency(key), 0.5);
}
const currentKey = document.getElementById("current-note").innerHTML;
if (currentKey !== '-') {
document.getElementById('tuning-from').value = currentKey;
}
updateOptions(key);
keySpan.classList.add('playing');
@ -174,6 +199,7 @@ function makeKey(note) {
function setupKeyboard(firstKey=kFirstKey, numKeys=kNumKeys) {
const keyboard = document.getElementById('keyboard');
const tuningFrom = document.getElementById('tuning-from');
keyboard.innerHTML = '';
let curIndex = kNotes.indexOf(firstKey.charAt(0));
@ -183,6 +209,9 @@ function setupKeyboard(firstKey=kFirstKey, numKeys=kNumKeys) {
const note = kNotes[curIndex] + octave;
const key = makeKey(note)
keyboard.appendChild(key);
const opt = document.createElement('option');
opt.innerHTML = note;
tuningFrom.appendChild(opt);
curIndex += 1;
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() {
const freq = document.getElementById("current-frequency");
freq.onchange = e => {
const adjust = document.getElementById("adjust");
freq.oninput = e => {
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();
document.getElementById("adjust-value").innerHTML = newFreq.toFixed(2) + ' Hz';
};
freq.oninput = freq.onchange;
adjust.oninput = freq.oninput;
const hold = document.getElementById("hold");
hold.onchange = e => {
@ -232,16 +280,50 @@ function setupListeners() {
removeHold(key);
}
}
const range = document.getElementById("range");
range.oninput = e => {
const val = parseFloat(range.value);
const volume = document.getElementById("volume");
volume.oninput = e => {
const val = parseFloat(volume.value);
globalVolume.gain.value = val;
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 => {
setupKeyboard('C2', 49);
tuneEqual();
tuneAllEqual();
setupListeners();
});