Initial commit: simple synth

This commit is contained in:
Paul Mathieu 2025-04-10 10:39:25 -07:00
commit 8fde361246
4 changed files with 144 additions and 0 deletions

30
index.css Normal file
View File

@ -0,0 +1,30 @@
.whiteKey {
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;
}
.blackKey {
color: black;
background-color: black;
position: absolute;
z-index: 1;
height: 40pt;
width: 15pt;
/*
max-height: 40pt;
max-width: 15pt;
*/
display: inline-block;
}

18
index.html Normal file
View File

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<link rel="stylesheet" href="pico.min.css">
<link rel="stylesheet" href="index.css">
<script src="index.js"></script>
<title>Tooner 0</title>
</head>
<body>
<main class="container">
<div id="keyboard">
</div>
</main>
</body>
</html>

92
index.js Normal file
View File

@ -0,0 +1,92 @@
const kNumKeys = 49;
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();
function playNote(frequency, duration) {
let currentGain = 1.0;
const bassBoost = f => {
const t = f / 20000;
const a = 4.0;
const b = 0.2;
return b * t + a * (1 - t);
};
while (frequency < 20000) {
const oscillator = new OscillatorNode(audioCtx);
const gain = new GainNode(audioCtx);
oscillator.type = 'sine';
oscillator.frequency.value = frequency; // value in hertz
oscillator.connect(gain);
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);
oscillator.start();
setTimeout(() => {oscillator.stop();}, duration);
frequency *= 2;
currentGain *= 0.3;
}
}
function noteFrequency(key) {
let [note, octave, ] = key.split(/(\d)/);
octave = parseInt(octave);
const interval = kNotes.indexOf(note) - kNotes.indexOf('A') + octave * 12;
let freq = kA0Frequency * Math.pow(kSemiToneMultiplier, interval);
return freq;
}
function onKeyClick(event) {
const note = event.srcElement.id;
const freq = noteFrequency(note);
playNote(freq, 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);
return key;
}
function setupKeyboard() {
const keyboard = document.getElementById('keyboard');
keyboard.innerHTML = '';
let curIndex = kNotes.indexOf(kFirstKey.charAt(0));
let octave = parseInt(kFirstKey.charAt(1));
for (let i = 0; i < kNumKeys; 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;
}
}
}
window.addEventListener('load', e => {
setupKeyboard();
});

4
pico.min.css vendored Normal file

File diff suppressed because one or more lines are too long