Adjust frequency, hold note, better synth
This commit is contained in:
parent
8fde361246
commit
eea0e3f879
35
index.css
35
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;
|
||||
}
|
||||
|
61
index.html
61
index.html
@ -11,8 +11,65 @@
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<div id="keyboard">
|
||||
</div>
|
||||
<article>
|
||||
<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">
|
||||
</label>
|
||||
</article>
|
||||
<section id="options">
|
||||
<form>
|
||||
<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>
|
||||
</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>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
173
index.js
173
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();
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user