-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
159 lines (135 loc) · 5.16 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import makeNoteDisplay from "./note-display.js";
const TAU = 2*Math.PI;
const ctx = new AudioContext();
document.getElementById("resumeAudio").addEventListener("click", () => ctx.resume());
// Prevent the output from overshooting in Chrome:
const almostDestination = new GainNode(ctx, {gain: 0.25});
almostDestination.connect(ctx.destination);
class StringNode extends AudioWorkletNode {
constructor(ctx, {basePitch, amplitude = 1, onSilent} = {}) {
super(ctx, "string");
this.baseFreq = 440 * 2**((basePitch+3-60)/12);
this.amplitude = amplitude;
this.basePitch = basePitch;
this.currentFret = -1;
this.port.onmessage = ({data}) => {
if (data.type = "silent" && onSilent) {
onSilent();
}
};
}
pick(fret = 0) {
if (fret === this.currentFret) {
// Upon the following events
// - press key #1
// - press key #2
// - release key #1 (still holding key #2 down)
// Chrome creates (after a while) another keydown event for key #2 that
// is not marked as "repeat". (Could we recognize such keydown events
// in some easy way?)
// So this situation is detected here (in some hacky way) and the event
// is ignored.
return;
}
const fretFreq = this.baseFreq * 2**(fret/12);
const nHarmonics = Math.min(15, Math.floor(4000/fretFreq));
const stiffnesses = new Float32Array(nHarmonics);
const decayRates = new Float32Array(nHarmonics);
const vals = new Float32Array(nHarmonics);
const {sampleRate} = this.context;
for (let i = 0; i < nHarmonics; i++) {
const harmonic = i+1;
const freq = fretFreq * harmonic;
// stiffnesses are computed according to the physical model
stiffnesses[i] = 2 - 2*Math.cos(TAU * freq / sampleRate);
// absFriction and relVal are manually "designed"
const absFriction = 4 * (fretFreq/1000)*harmonic**1.5;
decayRates[i] = Math.exp(-(absFriction / sampleRate));
const relVal = (2000/fretFreq)**1.5 * (harmonic%2 ? 1 : 2) / harmonic**2;
vals[i] = this.amplitude * relVal;
}
this.port.postMessage({type: "pick", stiffnesses, decayRates, vals});
this.currentFret = fret;
}
damp() {
this.port.postMessage({type: "damp"});
this.currentFret = -1;
}
stop() {
this.port.postMessage({type: "stop"});
this.currentFret = -1;
}
}
async function setupStrings() {
const pitchDisplays = [..."ebgd"].map(c => {
const box = document.getElementById(`${c}-string`);
const textBox = document.createElement("div");
textBox.setAttribute("class", "note-text")
box.append(textBox);
const svgBox = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svgBox.setAttribute("class", "note-svg");
box.append(svgBox);
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
g.setAttribute("class", "note-display");
g.setAttribute("transform", "translate(25, 35) scale(10,10)");
svgBox.append(g);
const noteDisplay = makeNoteDisplay(g);
function set(pitch) {
textBox.innerHTML = pitch2note(pitch);
// Guitar notes are written an octave higher than they sound.
noteDisplay.display(pitch + 12);
}
function clear() {
textBox.innerHTML = "";
noteDisplay.clear();
}
return {set, clear};
});
// e4 b3 g3 d3
const stringPitches = [64, 59, 55, 50];
await ctx.resume();
await ctx.audioWorklet.addModule("StringWorklet.js")
const stringNodes = stringPitches.map((basePitch, i) => {
const node = new StringNode(ctx, {
basePitch,
onSilent: () => { pitchDisplays[i].clear(); },
});
node.connect(almostDestination);
return node;
});
const keyMap = {};
`
Backquote, Digit1, Digit2, Digit3, Digit4, Digit5, Digit6, Digit7, Digit8, Digit9, Digit0, Minus, Equal, Backspace
Tab, KeyQ, KeyW, KeyE, KeyR, KeyT, KeyY, KeyU, KeyI, KeyO, KeyP, BracketLeft, BracketRight, Backslash
CapsLock, KeyA, KeyS, KeyD, KeyF, KeyG, KeyH, KeyJ, KeyK, KeyL, Semicolon, Quote, Enter
ShiftLeft, KeyZ, KeyX, KeyC, KeyV, KeyB, KeyN, KeyM, Comma, Period, Slash, ShiftRight
`.trim().split(/\n\r?|\r/).map((line, row) =>
// ".splice(1)" removes the special keys Backquote/Tab/CapsLock/ShiftLeft
line.split(",").splice(1).forEach((field, col) => {
keyMap[field.trim()] = {row, col};
})
);
// TODO make enharmonic name choices (e.g. d# vs. eb) configurable
const noteNames = "c c♯ d d♯ e f f♯ g g♯ a b♭ b".split(" ");
const pitch2note = pitch =>
`${noteNames[pitch % 12]}<sub>${Math.floor(pitch / 12) - 1}</sub>`;
window.addEventListener("keydown", (ev) => {
if (ev.repeat || ev.altKey || ev.ctrlKey || ev.metaKey) return;
const keyPos = keyMap[ev.code];
if (!keyPos) return;
ev.preventDefault();
const node = stringNodes[keyPos.row];
const fret = keyPos.col;
node.pick(fret);
pitchDisplays[keyPos.row].set(node.basePitch + fret);
});
window.addEventListener("keyup", ev => {
const keyPos = keyMap[ev.code];
if (!keyPos) return;
const {row, col} = keyPos;
const node = stringNodes[row];
if (node.currentFret !== col) return;
node.damp();
});
}
setupStrings();