-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathSynthie.js
332 lines (282 loc) · 8.79 KB
/
Synthie.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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
var kSineWave = 0
var kTriangleWave = 1
var kSquareWave = 2
var kSawtoothWave = 3
var kNoiseWave = 4
var kSilence = 5
var kSynthesizerAction_PlayTone = 0
var kSynthesizerAction_ReleaseTone = 1
// Limit theta to 0.0 to 2xPI. This assumes theta >= 0.0.
function Mod2Pi(theta) {
while (theta >= 2.0*Math.PI) {
theta -= 2.0*Math.PI
}
return theta
}
function normalize_angle(theta) {
if (theta < 0) {
let multiples = theta / (2.0*Math.PI)
theta += (2.0*Math.PI) * (multiples + 1)
}
theta = Mod2Pi(theta)
return theta
}
function sawtooth(theta) {
theta = normalize_angle(theta)
if (theta < Math.PI ) {
return theta / Math.PI
} else {
return -1.0 + (theta - Math.PI) / Math.PI
}
}
//
// Triangle wave
//
function triwave(theta) {
theta = normalize_angle(theta)
if (theta < Math.PI / 2.0) {
return theta / (Math.PI / 2.0)
} else if (theta < 3.0 * Math.PI / 2.0) {
return 1.0 - 2.0 * (theta - (Math.PI / 2.0)) / Math.PI
} else {
return (theta - 3.0 * Math.PI / 2.0) / (Math.PI / 2.0)
}
}
// Duty cycle = 0.0 to 1.0
function squarewave(theta, duty_cycle) {
theta = normalize_angle(theta)
if (theta < 2.0*Math.PI*duty_cycle)
return 1.0
else
return -1.0
}
function noisewave(theta) {
theta = normalize_angle(theta)
let sample = Math.random()*2 - 1
return sample
}
// Oscillates from -1 to +1
function f_oscillator_sample(oscillator_type, freq, start_time, time) {
let normalized_time = time - start_time
let theta = 2.0 * Math.PI * freq * normalized_time
switch(oscillator_type) {
case kSineWave:
return Math.sin(theta)
case kTriangleWave:
return triwave(theta)
case kSquareWave:
return squarewave(theta, 0.70)
case kSawtoothWave:
return sawtooth(theta)
case kNoiseWave:
return noisewave(theta)
case kSilence:
return 0.0
default:
return 0.0
}
}
function f_envelope_gain(envelope, note_released, note_release_time, time) {
if (time < 0.0) {
return 0.0
}
if (time < envelope.attack_time) {
let gain = envelope.attack_gain * time / envelope.attack_time
return gain
}
// Normalize to start of decay time, which is the end of attack_time
let decay_time = time - envelope.attack_time
// See if we're in the decay phase
if (decay_time < envelope.decay_time) {
let gain_drop = (envelope.attack_gain - envelope.sustain_gain)
let gain = envelope.attack_gain - gain_drop * (decay_time / envelope.decay_time)
return gain
}
// If we got past decay phase, we must either be sustaining, releasing, or no longer playing
// TODO: We'll support non-sustainable later. Assume everything
// sustains for now.
// See if we're still in the sustain phase
if (!note_released || time < note_release_time) {
return envelope.sustain_gain
}
// If got here, we must be releasing or no longer playing (post-release)
// Normalize to release time
let release_time = time - note_release_time
// See if we're still releasing
if (release_time < envelope.release_time) {
let gain_drop = envelope.sustain_gain
let gain = envelope.sustain_gain - gain_drop * (release_time / envelope.release_time)
return gain
}
// We must be in post-release phase
return 0.0
}
function f_instrument_sample(instrument, freq, note_play_time, note_released, note_release_time, time) {
let oscillator_sample = f_oscillator_sample(instrument.oscillator, freq, note_play_time, time)
// Translate time into a time in the envelope's timeline (where time 0.0 is where the note starts playing).
let envelope_time = time - note_play_time
let envelope_note_release_time = note_release_time - note_play_time
let envelope_gain = f_envelope_gain(instrument.envelope, note_released, envelope_note_release_time, envelope_time)
let sample = oscillator_sample * envelope_gain
return sample;
}
//
// Get a sample for the given synthesizer state at a given time.
//
// - synthesizer_state: the synthesizer state, which specifies how many channels there are, what instrument they're playing,
// at what frequency, what envelope they're using, and when the tone started (and possibly ended) playing.
// - time: the time, in seconds, of the sample to get
//
function f_synthesizer_sample(synthesizer, time) {
// Our "mixing" of audio is super simple: just take the average of all of the channels
let avg_sample = 0.0
for (var i=0; i < synthesizer.channels.length; i++) {
let channel = synthesizer.channels[i];
let instrument_sample = f_instrument_sample(channel.instrument, channel.freq, channel.note_play_time,
channel.note_released, channel.note_release_time, time)
avg_sample += instrument_sample
}
if (synthesizer.num_channels > 0) {
avg_sample = avg_sample / synthesizer.num_channels
}
return avg_sample
}
// Given a set of synth commands, update the synth state. The only way to change a synth's state is through
// this function.
//
// - prev_state: the previous synthesizer state
// - num_commands: how many commands to execute on the synth
// - commands: an array of the commands that will change the synth state
//
function f_next_synthesizer_state(prev_state, commands) {
let next_state = {...prev_state}
for (var i=0; i < commands.length; i++) {
let command = commands[i]
switch (command.action_type) {
case kSynthesizerAction_PlayTone:
next_state.channels[command.channel].instrument = command.instrument
next_state.channels[command.channel].freq = command.freq
next_state.channels[command.channel].note_play_time = command.time
next_state.channels[command.channel].note_released = false
break
case kSynthesizerAction_ReleaseTone:
next_state.channels[command.channel].note_release_time = command.time
next_state.channels[command.channel].note_released = true
break
default:
console.log("Bad command")
}
}
return next_state
}
function f_create_dialtone_synthesizer() {
let envelope = {
attack_time: 0.5,
attack_gain: 1.0,
decay_time: 0.2,
sustain_gain: 0.8,
release_time: 1.0,
}
let boom_envelope = {
attack_time: 0.1,
attack_gain: 1.0,
decay_time: 0.04,
sustain_gain: 0.0,
release_time: 0.0,
}
let bass = {
oscillator: kSineWave,
envelope: envelope,
}
let boom = {
oscillator: kSquareWave,
envelope: boom_envelope,
}
let silence = {
oscillator: kSilence,
envelope: envelope,
}
let channels = [
{
instrument: bass,
freq: 440,
note_play_time: 0.0,
note_released: true,
note_release_time: 5.0,
},
{
instrument: bass,
freq: 350,
note_play_time: 0.0,
note_released: true,
note_release_time: 5.0,
},
{
instrument: boom,
freq: 60,
note_play_time: 2.0,
note_released: true,
note_release_time: 4.0,
},
{
instrument: boom,
freq: 120,
note_play_time: 2.0,
note_released: true,
note_release_time: 1.0,
},
]
let synth = {
num_channels: 4,
channels: channels
}
return synth
}
function f_create_empty_synthesizer() {
let envelope = {
attack_time: 0.5,
attack_gain: 1.0,
decay_time: 0.2,
sustain_gain: 0.8,
release_time: 1.0,
}
let silence = {
oscillator: kSilence,
envelope: envelope,
}
let channels = [
{
instrument: silence,
freq: 440,
note_play_time: 0.0,
note_released: true,
note_release_time: 5.0,
},
{
instrument: silence,
freq: 350,
note_play_time: 0.0,
note_released: true,
note_release_time: 5.0,
},
{
instrument: silence,
freq: 60,
note_play_time: 2.0,
note_released: true,
note_release_time: 4.0,
},
{
instrument: silence,
freq: 120,
note_play_time: 2.0,
note_released: true,
note_release_time: 1.0,
},
]
let synth = {
num_channels: 4,
channels: channels
}
return synth
}