-
Notifications
You must be signed in to change notification settings - Fork 1
/
minimal-webaudio-synth-improved.html
181 lines (161 loc) · 7.18 KB
/
minimal-webaudio-synth-improved.html
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Improved Minimal WebAudio synth</title>
<style>
body { background-color: darkgray; box-sizing: border-box; }
.box { max-width: 60em; margin: auto; background-color: white; padding: 1em; }
.control input { width: 80%; }
.keyboard {
padding: 0;
overflow: auto;
border-left: 1px solid darkgray;
margin: 1em 0;
display: inline-block;
}
.keyboard .key {
float: left;
display: inline-block;
width: 2em;
height: 6em;
border: 1px solid darkgray;
border-left: none;
margin: 0;
padding: 0;
}
.keyboard .key.black {
background-color: black;
}
</style>
</head>
<body>
<div class="box">
<form id="form">
<h1>Improved Minimal WebAudio synth</h1>
<p><small>This is example implementation of minimal usable WebAudio synth without any external dependencies.</small></p>
<button id="power">POWER ON</button>
<div class="control">
<label>volume</label> <input type="range" id="volume" min="0" max="1" value="0.5" step="0.001">
</div>
<div class="control">
<label>attack</label> <input type="range" id="attack" min="0.001" max="1" value="0.001" step="0.001">
</div>
<div class="control">
<label>decay</label> <input type="range" id="decay" min="0.001" max="1" value="0.5" step="0.001">
</div>
<div class="control">
<label>cutoff</label> <input type="range" id="cutoff" min="0" max="20000" value="20000" step="1">
</div>
<div class="control">
<label>waveform</label>
<select id="waveform">
<option>sawtooth</option>
<option>sine</option>
<option>square</option>
<option>triangle</option>
</select>
</div>
</form>
<div class="keyboard" id="keyboard">
<div class="key" data-midi="60"></div>
<div class="key black" data-midi="61"></div>
<div class="key" data-midi="62"></div>
<div class="key black" data-midi="63"></div>
<div class="key" data-midi="64"></div>
<div class="key" data-midi="65"></div>
<div class="key black" data-midi="66"></div>
<div class="key" data-midi="67"></div>
<div class="key black" data-midi="68"></div>
<div class="key" data-midi="69"></div>
<div class="key black" data-midi="70"></div>
<div class="key" data-midi="71"></div>
<div class="key" data-midi="72"></div>
<div class="key black" data-midi="73"></div>
<div class="key" data-midi="74"></div>
<div class="key black" data-midi="75"></div>
<div class="key" data-midi="76"></div>
<div class="key" data-midi="77"></div>
<div class="key black" data-midi="78"></div>
<div class="key" data-midi="79"></div>
<div class="key black" data-midi="80"></div>
<div class="key" data-midi="81"></div>
<div class="key black" data-midi="82"></div>
<div class="key" data-midi="83"></div>
</div>
<div>
<small>This keyboard only work with mouse for now.</small>
</div>
</div>
<script>
document.getElementById('form').reset(); // to reset form to initial state on each opening of page
// determines freq of current MIDI note
function midi2cps(midi) {
const A4 = 440;
return A4 * Math.pow(2, (midi - 69) / 12);
}
// only complex piece of machinery - slowly moving AudioParams
function moveAudioParamTo(ac, audioParam, target, time)
{
time = Number(time);
var curr = audioParam.value; // first getting current value of it
var now = ac.currentTime; // then current time
audioParam.cancelScheduledValues(now); // you need to stop any scheduled values first
audioParam.setValueAtTime(curr, now); // stop ENV at current point
audioParam.linearRampToValueAtTime(target, now + time); // move to target at now + time
}
document.getElementById('power').addEventListener('click', function (event){ // you need to click on something to be able to construct AudioContext
event.preventDefault(); // to not submit form
document.getElementById('power').setAttribute('disabled', 'disabled'); // to prevent clicking it again
let ac = new AudioContext(); // create audio context to work on
let osc = ac.createOscillator(); // oscillator
osc.frequency.value = 440;
osc.type = 'sawtooth';
osc.start(ac.currentTime); // start oscillator so it makes some sound
let filter = ac.createBiquadFilter(); // filter
filter.type = 'lowpass';
filter.frequency.value = 20000;
let vca = ac.createGain(); // VCA
vca.gain.value = 0;
let amp = ac.createGain(); // final volume knob of synth
amp.gain.value = 0.5;
osc.connect(vca).connect(amp).connect(filter).connect(ac.destination);
// keyboard playing:
document.getElementById('keyboard').addEventListener('mousedown', function (event){ // user pressed an key (mouse up)
moveAudioParamTo(ac, osc.frequency, midi2cps(event.target.getAttribute('data-midi')), 0.001); // change freq of OSC
moveAudioParamTo(ac, vca.gain, 1, document.getElementById('attack').value); // open VCA up
});
document.getElementById('keyboard').addEventListener('mouseover', function (event){ // user slide to another
if (event.buttons == 1 || event.buttons == 3){ // but only if they have pressed button before
if (!event.target.hasAttribute('data-midi')) return; // to prevent triggering from 1px left border with no data-midi
moveAudioParamTo(ac, osc.frequency, midi2cps(event.target.getAttribute('data-midi')), 0.001); // change OSC freq
}
});
document.getElementById('keyboard').addEventListener('mouseup', function (event){ // user depressed an key (mouse up)
moveAudioParamTo(ac, vca.gain, 0, document.getElementById('decay').value); // close VCA
});
document.getElementById('keyboard').addEventListener('mouseleave', function (event){ // user moved mouse out of the keyboard
moveAudioParamTo(ac, vca.gain, 0, document.getElementById('decay').value); // close VCA
});
document.getElementById('keyboard').addEventListener('mouseenter', function (event){ // user moved mouse into the keyboard
if(event.buttons == 1 || event.buttons == 3) { // so if they have pressed button before
moveAudioParamTo(ac, vca.gain, 1, document.getElementById('attack').value); // open VCA
}
});
// volume handling
document.getElementById('volume').addEventListener('input', function (event){
moveAudioParamTo(ac, amp.gain, event.target.value, 0.001);
});
// moving filter
document.getElementById('cutoff').addEventListener('input', function (event){
// filter.frequency.setValueAtTime(event.target.value, ac.currentTime); // <- This should work in Chrome 109 but doesn't. Why? In Firefox 115 this is OK.
moveAudioParamTo(ac, filter.frequency, event.target.value, 0.001); // This only works in Chrome 109 when time > 1
});
// changing waveform
document.getElementById('waveform').addEventListener('change', function (event) {
osc.type = event.target.value;
});
});
</script>
</body>
</html>