Compare commits

..

2 commits

Author SHA1 Message Date
3e04ee0dd7 mode selector
All checks were successful
/ build (push) Successful in 49s
2026-04-05 18:57:25 -07:00
7f9345b6f8 fix issue with keys being captured outside of text field 2026-04-05 18:42:33 -07:00

View file

@ -3,25 +3,48 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>piano</title> <title>piano</title>
<h1>piano</h1> <h1>piano</h1>
<div> <p>play music with your computer keyboard! currently only works on desktop and with a qwerty keyboard.</p>
<p>
<label for="select-note-layout">note layout:</label>
<select name="select-note-layout" id="select-note-layout">
<option value="isomorphic-2-1" selected>isomorphic 2,1</option>
<option value="isomorphic-5-2">isomorphic 5,2</option>
<option value="classic">classic piano</option>
</select>
</p>
<!-- <p>
<textarea id="input"></textarea> <textarea id="input"></textarea>
</div> </p> -->
<h2>notes held</h2> <h2>notes held</h2>
<div id="notes-held"></div> <div id="notes-held"></div>
<script> <script>
const inputArea = document.querySelector('#input'); // const inputArea = document.querySelector('#input');
const selectNoteLayout = document.querySelector('#select-note-layout');
const notesDiv = document.querySelector('#notes-held'); const notesDiv = document.querySelector('#notes-held');
const ctx = new AudioContext; const ctx = new AudioContext;
const playing = {}; const playing = {};
let tone = {}; let tone = {};
let setupCompleted = false; addEventListener('keydown', onKeydown);
inputArea.onfocus = start; addEventListener('keyup', onKeyup);
inputArea.onblur = pause; useLayout(selectNoteLayout.value);
selectNoteLayout.onchange = function(ev) {
ev.preventDefault();
useLayout(ev.target.value);
}
function useLayout(layout) {
if (layout == 'classic') {
useClassicPiano();
} else if (layout == 'isomorphic-2-1') {
useIsomorphic(2, 1);
} else if (layout == 'isomorphic-5-2') {
useIsomorphic(5, 2);
}
}
// arrange notes like on a piano. the second and fourth rows are each a major scale, but the first and third rows have gaps. // arrange notes like on a piano. the second and fourth rows are each a major scale, but the first and third rows have gaps.
function usePianoKeys() { function useClassicPiano() {
tone = {}; tone = {};
("zsxdcvgbhnjm,l.;/q2w3e4rt6y7ui9o0p-[]" ("zsxdcvgbhnjm,l.;/q2w3e4rt6y7ui9o0p-[]"
.split('') .split('')
@ -31,8 +54,16 @@
); );
} }
// arrange notes "isomorphically". moving sideways is 5 semitones, moving diagonally upwards is 2 semitones. // arrange notes "isomorphically", meaning that transposing chords in frequency space is done by transposing points in regular space.
function useEqualSteps() { //
// default: 5, 2. this creates a pleasing "diagonal" layout where octaves can be reached by moving two keys sideways and one key up (5+5+2=12).
// the downside of this is that some notes are not reachable unless you have five rows assigned (maybe I can capture the F1-F12 keys somehow?)
//
// alternative: 2, 1. this is "more like a piano" except that there are no empty spaces (no "gaps" where a black key should be).
// this is probably better for playing sheet music but it does suck that common chords are much farther apart. also on a four-row keyboard every note appears twice.
// a neat side effect of this is that an octave is exactly 6 column steps, the same as the distance between "a" and "j", which means when your hands are in home row
// position, they are exactly one octave apart.
function useIsomorphic(stepX = 5, stepY = 2) {
const rows = [ const rows = [
"1234567890-=", "1234567890-=",
"qwertyuiop[]", "qwertyuiop[]",
@ -42,28 +73,14 @@
tone = {}; tone = {};
rows.map((row, y) => { rows.map((row, y) => {
row.split('').map((key, x) => { row.split('').map((key, x) => {
const semitone = Math.floor(x * 5 + y * 2 - 28); const semitone = Math.floor(x * stepX + y * stepY - 28);
tone[key] = 440 * 2 ** (semitone / 12); tone[key] = 440 * 2 ** (semitone / 12);
}) })
}); });
} }
function start() {
if (setupCompleted) {
addEventListener('keydown', onKeydown);
return;
}
useEqualSteps();
addEventListener('keydown', onKeydown);
addEventListener('keyup', onKeyup);
setupCompleted = true;
}
function pause() {
removeEventListener('keydown', onKeydown);
}
function onKeydown(ev) { function onKeydown(ev) {
ev.preventDefault();
let key = ev.key; let key = ev.key;
if (key == ' ') key = 'Space'; if (key == ' ') key = 'Space';
if (!tone[key]) return; if (!tone[key]) return;
@ -80,6 +97,7 @@
} }
function onKeyup(ev) { function onKeyup(ev) {
ev.preventDefault();
let key = ev.key; let key = ev.key;
if (key == ' ') key = 'Space'; if (key == ' ') key = 'Space';
if (!playing[key]) return; if (!playing[key]) return;
@ -89,11 +107,11 @@
oscillator.disconnect(); oscillator.disconnect();
gain.disconnect(); gain.disconnect();
delete playing[key]; playing[key] = undefined;
showNotes(); showNotes();
} }
function showNotes() { function showNotes() {
notesDiv.innerText = Object.keys(playing).join(' '); notesDiv.innerText = Object.keys(playing).filter((key) => playing[key]).join(' ');
} }
</script> </script>