-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathinstruments.coffee
846 lines (710 loc) · 26.9 KB
/
instruments.coffee
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
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
###
Oscillator Class
--------------------------------------------------------------------------------
The Oscillator class provides a basic sound wave. You can play the resulting
sound directly, or, more likely, process the sound with another class in the
library, such as Filter, Envelope, or Mixer.
Constants
Oscillator.SINE Wave type. Smooth sine waveform.
Oscillator.SQUARE Wave type. Square waveform.
Oscillator.SAWTOOTH Wave type. Sawtooth waveform.
Oscillator.TRIANGLE Wave type. Triangle waveform.
Oscillator.NOISE Wave type. Random samples between -1 and 1.
Wave types: https://en.wikipedia.org/wiki/Waveform
Arguments
type: Optional. Default Oscillator.SINE. The type of sound wave to
generate. Accepted values: Oscillator.SINE,
Oscillator.TRIANGLE, Oscillator.SQUARE, Oscillator.NOISE,
Oscillator.SAWTOOTH.
frequency: Optional. Default 440. Number, or a function that takes a time
parameter and returns a frequency. The time argument specifies
how long the oscillator has been running.
amplitude: Optional. Default 1. Number, or a function that takes a time
parameter and returns an amplitude multiplier. *Not* in
dB's. the time argument specifies how long the oscillator has
been running.
phase: Optional. Default 0. Number, or a function that takes a time
parameter and returns a phase shift. the time argument
specifies how long the oscillator has been running. This is an
advanced parameter and can probably be ignored in most cases.
Returns
An instance-like closure that wraps the values passed at instantiation. This
follows the `(time) -> sample` convention for use in play() and buildSample().
Usage 1 (JS) - Basic wave (build sample version)
require('v1/instruments');
var oscillator = new Oscillator({frequency: Frequency.A_3});
var buildSample = function(time){
return oscillator(time);
}
Usage 2 (JS) - Basic triangle wave (build track version)
require('v1/instruments');
var oscillator = new Oscillator({frequency: Frequency.A_3, type: Oscillator.TRIANGLE});
var buildTrack = function(){
this.play(oscillator);
}
Usage 3 (JS) - Basic square wave, with amplitude
require('v1/instruments');
var oscillator = new Oscillator({frequency: Frequency.A_3, type: Oscillator.SQUARE, amplitude: 0.7});
var envelope = new Envelope();
var buildTrack = function(){
this.play(envelope.process(oscillator));
}
Usage 4 (JS) - Vibrato
require('v1/instruments');
var phaseModulation = function(time){ return 0.1 * Math.sin(TWO_PI * time * 5); }
var oscillator = new Oscillator({frequency: Frequency.A_3, phase: phaseModulation});
var buildTrack = function(){
this.play(oscillator);
}
Usage 5 (JS) - Hi Hat
require('v1/instruments');
var oscillator = new Oscillator({frequency: Frequency.A_3, type: Oscillator.NOISE});
var filter = new Filter({type: Filter.HIGH_PASS, frequency: 10000});
var envelope = new Envelope();
var buildTrack = function(){
this.play(envelope.process(filter.process(oscillator)));
}
###
class Oscillator
# Types
@SINE = 0
@SQUARE = 1
@SAWTOOTH = 2
@TRIANGLE = 3
@NOISE = 4
constructor: (options={}) ->
@frequency = options.frequency || 440
@phase = options.phase || 0
@amplitude = options.amplitude || 1
@numericPhase = @phase unless Function.isFunction(@phase)
@numericFrequency = @frequency unless Function.isFunction(@frequency)
@numericAmplitude = @amplitude unless Function.isFunction(@amplitude)
@oscillatorFunction = switch (options.type || Oscillator.SINE)
when Oscillator.SQUARE
@square
when Oscillator.SAWTOOTH
@sawtooth
when Oscillator.TRIANGLE
@triangle
when Oscillator.NOISE
@noise
else
@sine
# Represents start time
@startTime = -1
# The closure to be returned at the end of this call
generator = (time) =>
# Using localTime makes it easier to anticipate the interference of
# multiple oscillators
@startTime = time if @startTime == -1
@localTime = time - @startTime
_phase = if @numericPhase? then @numericPhase else @phase(@localTime)
_frequency = if @numericFrequency? then @numericFrequency else @frequency(@localTime)
_amplitude = if @numericAmplitude? then @numericAmplitude else @amplitude(@localTime)
_amplitude * @oscillatorFunction((_frequency * @localTime) + _phase)
generator.displayName = "Oscillator Sound Generator"
generator.getFrequency = =>
if @numericFrequency? then @numericFrequency else @frequency(@localTime)
generator.setFrequency = (frequency) =>
@frequency = frequency
if Function.isFunction(@frequency)
@numericFrequency = undefined
else
@numericFrequency = @frequency
frequency
generator.getPhase = =>
if @numericPhase? then @numericPhase else @phase(@localTime)
generator.setPhase = (phase) =>
@phase = phase
if Function.isFunction(@phase)
@numericPhase = undefined
else
@numericPhase = @phase
phase
generator.getAmplitude = =>
if @numericAmplitude? then @numericAmplitude else @amplitude(@localTime)
generator.setAmplitude = (amplitude) =>
@amplitude = amplitude
if Function.isFunction(@amplitude)
@numericAmplitude = undefined
else
@numericAmplitude = @amplitude
amplitude
# Explicit return necessary in constructor
return generator
sine: (value) ->
# Smooth wave intersecting (0, 0), (0.25, 1), (0.5, 0), (0.75, -1), (1, 0)
Math.sin(2 * Math.PI * value)
sawtooth: (value) ->
# Line from (-.5,-1) to (0.5, 1)
progress = (value + 0.5) % 1
2 * progress - 1
triangle: (value) ->
# Linear change from (0, -1) to (0.5, 1) to (1, -1)
progress = value % 1
if progress < 0.5
4 * progress - 1
else
-4 * progress + 3
square: (value) ->
# -1 for the first half of a cycle; 1 for the second half
progress = value % 1
if progress < 0.5
1
else
-1
noise: (value) ->
Math.random() * 2 - 1
###
Envelope Class
--------------------------------------------------------------------------------
Shapes the sound wave passed into process().
Constants
Envelope.AD Attack / Decay envelope. Only the attackTime and decayTime
parameters are used.
AD Envelope - Amplitude:
/\ 1
/ \
/ \ 0
|-| Attack phase
|-| Decay phase
Envelope.ADSR Attack Decay Sustain Release envelope. All parameters are
used.
ADSR Envelope - Amplitude:
/\ 1
/ \____ sustainLevel
/ \ 0
|-| Attack phase
|-| Decay phase
|---| Sustain phase
|-| Release phase
Arguments
type: Optional. Default Envelope.AD. Accepted values: Envelope.AD,
Envelope.ADSR.
attackTime: Optional. Default 0.03. Value in seconds.
decayTime: Optional. Default 1.0. Value in seconds.
sustainTime: Optional. Default 0. Value in seconds. Ignored unless envelope
type is Envelope.ADSR.
releaseTime: Optional. Default 0. Value in seconds. Ignored unless envelope
type is Envelope.ADSR.
sustainLevel: Optional. Default 0. Value in seconds. Ignored unless envelope
type is Envelope.ADSR.
Returns
An object with a process() method, ready to accept an oscillator or other sound
generator to be shaped.
Usage 1 (JS)
require('v1/instruments');
var o = new Oscillator;
var e = new Envelope;
var processor = e.process(o);
var buildSample = function(time) {
return processor(time);
}
Usage 2 (JS)
require('v1/instruments');
var o = new Oscillator;
var e = new Envelope;
var buildTrack = function() {
this.play(e.process(o));
}
Usage 3 (JS)
require('v1/instruments');
var o = new Oscillator;
var e = new Envelope({type: Envelope.ADSR, sustainTime: 1, releaseTime: 1, sustainLevel: 0.5});
var buildTrack = function() {
this.play(e.process(o));
}
###
class Envelope
# AD Envelope Type
#
# Amplitude:
# /\ 1
# / \
# / \ 0
# |-| Attack
# |-| Decay
@AD = 0
# ADSR Envelope Type
#
# Amplitude:
# /\ 1
# / \____ sustainLevel
# / \ 0
# |-| Attack
# |-| Decay
# |---| Sustain
# |-| Release
@ADSR = 1
constructor: (options={}) ->
options.sustainLevel ?= 0.3
options.type ?= Envelope.AD
if options.type == Envelope.AD
options.attackTime ?= 0.03
options.decayTime ?= 1
options.sustainTime = 0
options.releaseTime = 0
options.sustainLevel = 0
unless options.attackTime? && options.decayTime? && options.sustainTime? && options.releaseTime?
throw new Error "Options must specify 'attackTime', 'decayTime', 'sustainTime' and 'releaseTime' values for ADSR envelope type."
@type = options.type
@sustainLevel = options.sustainLevel
@attackTime = options.attackTime
@decayTime = options.decayTime
@sustainTime = options.sustainTime
@releaseTime = options.releaseTime
@totalTime = options.attackTime + options.decayTime + options.sustainTime + options.releaseTime
getMultiplier: (localTime) ->
if localTime <= @attackTime
# Attack
localTime / @attackTime
else if localTime <= @attackTime + @decayTime
# Plot a line between (attackTime, 1) and (attackTime + decayTime, sustainLevel)
# y = mx+b (remember m is slope, b is y intercept)
# m = (y2 - y1) / (x2 - x1)
m = (@sustainLevel - 1) / ((@attackTime + @decayTime) - @attackTime)
# plug in point (attackTime, 1) to find b:
# 1 = m(attackTime) + b
# 1 - m(attackTime) = b
b = 1 - m * @attackTime
# and solve, given x = localTime
m * localTime + b
else if localTime <= @attackTime + @decayTime + @sustainTime
# Sustain
@sustainLevel
else if localTime <= @totalTime
# Plot a line between (attackTime + decayTime + sustainTime, sustainLevel) and (totalTime, 0)
# y = mx+b (remember m is slope, b is y intercept)
# m = (y2 - y1) / (x2 - x1)
m = (0 - @sustainLevel) / (@totalTime - (@attackTime + @decayTime + @sustainTime))
# plug in point (totalTime, 0) to find b:
# 0 = m(totalTime) + b
# 0 - m(totalTime) = b
b = 0 - m * @totalTime
# and solve, given x = localTime
m * localTime + b
else
0
realProcess: (time, inputSample) ->
@startTime ?= time
localTime = time - @startTime
inputSample * @getMultiplier(localTime)
###
process()
---------
Arguments
A single instance of Oscillator, or the returned value from another process()
call.
Returns
An object that can be passed into play(), used in buildSample(), or passed
into another object's process() method. More precisely, process() returns a
closure in the format of `(time) -> sample`.
Usage 1
someOscillator = new Oscillator
envelope.process(someOscillator)
Usage 2
someOscillator1 = new Oscillator
someOscillator2 = new Oscillator
envelope.process(mixer.process(someOscillator1, someOscillator2))
###
process: (child) ->
unless arguments.length == 1
throw new Error "#{@constructor.name}.process() only accepts a single argument."
unless Function.isFunction(child)
throw new Error "#{@constructor.name}.process() requires a sound generator but did not receive any."
f = (time) => @realProcess(time, child(time))
f.duration = @attackTime + @decayTime + @sustainTime + @releaseTime
f
###
Mixer Class
--------------------------------------------------------------------------------
A mixer primarily does two things: adjust the volume of a signal, and add
multiple signals together into one.
Most process() methods allow only a single argument. If you'd like to process
multiple signals, you can combine them first using this class.
Constants
None
Arguments
gain: Gain amount in dB. Optional. Default -7.0. Float value.
Returns
An object with a process() method, ready to accept multiple oscillators, or any
results of calls to other process() methods.
Usage 1 (JS)
var oscillator1 = new Oscillator();
var oscillator2 = new Oscillator({frequency: Frequency.A_4});
var mixer = new Mixer({ gain: -5.0 });
var processor = mixer.process(oscillator1, oscillator2);
var buildSample = function(time){
return processor(time);
}
Usage 2 (JS)
var oscillator1 = new Oscillator();
var oscillator2 = new Oscillator({frequency: Frequency.A_4});
var envelope = new Envelope();
var mixer = new Mixer({ gain: -5.0 });
var processor = envelope.process(mixer.process(oscillator1, oscillator2));
var buildTrack = function(){
this.play(processor);
}
###
class Mixer
constructor: (options={}) ->
# Calculate amplitude multiplier given perceived dB gain.
# http://www.sengpielaudio.com/calculator-loudness.htm
@setGain(options.gain || -7.0)
getGain: -> @gain
setGain: (@gain=-7.0) ->
@multiplier = Math.pow(10, @gain / 20)
###
process()
---------
Arguments
Multiple instances of Oscillator, or the returned values from other process()
calls.
Returns
An object that can be passed into play(), used in buildSample(), or passed
into another object's process() method. More precisely, process() returns a
closure in the format of `(time) -> sample`.
Usage 1
someOscillator = new Oscillator
envelope.process(someOscillator)
Usage 2
someOscillator1 = new Oscillator
someOscillator2 = new Oscillator
envelope.process(mixer.process(someOscillator1, someOscillator2))
###
process: (nestedProcessors...) ->
f = (time, globalTime) =>
sample = 0
for processor in nestedProcessors when (!processor.duration? || time <= processor.duration)
sample += @multiplier * processor(time, globalTime)
sample
# Find longest child duration or leave empty if ANY child runs indefinitely
duration = -1
for processor in nestedProcessors
if processor.duration?
duration = Math.max(duration, processor.duration)
else
duration = -1
break
f.duration = duration if duration > 0
f
###
Filter Class
--------------------------------------------------------------------------------
Utility class to attenuate the different frequency components of a signal.
For example white noise (e.g.: (t) -> Math.random() * 2 - 1), contains a wide
range of frequencies. By filtering this noise you can shape the resulting sound.
This is best understood through experimentation.
This class implements a Biquad filter, a workhorse for general-purpose filter
use.
Filters are complex; it's not always intuitive how a parameter value will affect
the resulting frequency response. It may be helpful to use a frequency response
calculator, like this one, which is really nice:
http://www.earlevel.com/main/2013/10/13/biquad-calculator-v2/
Constants
Filter.LOW_PASS: Filter type. Let low frequencies through.
Filter.HIGH_PASS: Filter type. Let high frequencies through.
Filter.BAND_PASS_CONSTANT_SKIRT: Filter type. Let a range of frequencies
through. Optionally uses the band width
parameter (`filter.setBW(width)`).
Filter.BAND_PASS_CONSTANT_PEAK: Filter type. Let a range of frequencies
through. Optionally uses the band width
parameter (`filter.setBW(width)`).
Filter.NOTCH: Filter type. Remove a narrow range of
frequencies.
Filter.ALL_PASS: Filter type. Let all frequencies through.
Filter.PEAKING_EQ: Filter type. Boost frequencies around a
specific value. Optionally uses the
setDbGain value.
Filter.LOW_SHELF: Filter type. Boost low-frequency sounds.
Optionally uses the setDbGain value.
Filter.HIGH_SHELF: Filter type. Boost high-frequency sounds.
Optionally uses the setDbGain value.
Arguments
type: Optional. Default Filter.LOW_PASS. Accepts any filter type.
frequency: Optional. Default 300. A value in Hz specifying the midpoint or
cutoff frequency of the filter.
Returns
An object with a process() method, ready to accept multiple oscillators, or any
results of calls to other process() methods.
Usage 1 (JS):
require('v1/instruments');
var oscillator = new Oscillator({type: Oscillator.SQUARE, frequency: 55})
var filter = new Filter();
var processor = filter.process(oscillator);
var buildSample = function(time){
return processor(time);
}
Usage 2 (JS):
require('v1/instruments');
var oscillator = new Oscillator({frequency: Frequency.A_3, type: Oscillator.NOISE});
var filter = new Filter({type: Filter.HIGH_PASS, frequency: 10000});
var envelope = new Envelope();
var buildTrack = function(){
this.play(envelope.process(filter.process(oscillator)));
}
###
# Biquad filter
# Created by Ricard Marxer <email@ricardmarxer.com> on 2010-05-23.
# Copyright 2010 Ricard Marxer. All rights reserved.
# Translated to CoffeeScript by Ed McManus
#
# Implementation based on:
# http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt
class Filter
# Biquad filter types
@LOW_PASS = 0 # H(s) = 1 / (s^2 + s/Q + 1)
@HIGH_PASS = 1 # H(s) = s^2 / (s^2 + s/Q + 1)
@BAND_PASS_CONSTANT_SKIRT = 2 # H(s) = s / (s^2 + s/Q + 1) (constant skirt gain, peak gain = Q)
@BAND_PASS_CONSTANT_PEAK = 3 # H(s) = (s/Q) / (s^2 + s/Q + 1) (constant 0 dB peak gain)
@NOTCH = 4 # H(s) = (s^2 + 1) / (s^2 + s/Q + 1)
@ALL_PASS = 5 # H(s) = (s^2 - s/Q + 1) / (s^2 + s/Q + 1)
@PEAKING_EQ = 6 # H(s) = (s^2 + s*(A/Q) + 1) / (s^2 + s/(A*Q) + 1)
@LOW_SHELF = 7 # H(s) = A * (s^2 + (sqrt(A)/Q)*s + A)/(A*s^2 + (sqrt(A)/Q)*s + 1)
@HIGH_SHELF = 8 # H(s) = A * (A*s^2 + (sqrt(A)/Q)*s + 1)/(s^2 + (sqrt(A)/Q)*s + A)
# Biquad filter parameter types
@Q = 1
@BW = 2 # SHARED with BACKWARDS LOOP MODE
@S = 3
constructor: (options={}) ->
@Fs = SAMPLE_RATE
@type = options.type || Filter.LOW_PASS # type of the filter
@parameterType = Filter.Q # type of the parameter
@x_1_l = 0
@x_2_l = 0
@y_1_l = 0
@y_2_l = 0
@x_1_r = 0
@x_2_r = 0
@y_1_r = 0
@y_2_r = 0
@b0 = 1
@a0 = 1
@b1 = 0
@a1 = 0
@b2 = 0
@a2 = 0
@b0a0 = @b0 / @a0
@b1a0 = @b1 / @a0
@b2a0 = @b2 / @a0
@a1a0 = @a1 / @a0
@a2a0 = @a2 / @a0
@f0 = options.frequency || 300 # "wherever it's happenin', man." Center Frequency or
# Corner Frequency, or shelf midpoint frequency, depending
# on which filter type. The "significant frequency".
@dBgain = 12 # used only for peaking and shelving filters
@Q = 1 # the EE kind of definition, except for peakingEQ in which A*Q is
# the classic EE Q. That adjustment in definition was made so that
# a boost of N dB followed by a cut of N dB for identical Q and
# f0/Fs results in a precisely flat unity gain filter or "wire".
@BW = -3 # the bandwidth in octaves (between -3 dB frequencies for BPF
# and notch or between midpoint (dBgain/2) gain frequencies for
# peaking EQ
@S = 1 # a "shelf slope" parameter (for shelving EQ only). When S = 1,
# the shelf slope is as steep as it can be and remain monotonically
# increasing or decreasing gain with frequency. The shelf slope, in
# dB/octave, remains proportional to S for all other values for a
# fixed f0/Fs and dBgain.
# Since we now accept frequency as an option
@recalculateCoefficients()
###
setFrequency()
Alias for setF0(). Sometimes referred to as the center frequency, midpoint
frequency, or cutoff frequency, depending on filter type.
Arguments
Number value representing center frequency in Hz. Default 300.
###
setFrequency: (freq) ->
@setF0(freq)
freq
getFrequency: -> @f0
###
setQ()
"Q factor"
Arguments
Number value representing the filter's Q factor. Only used in some filter
types. To see the impact Q has on the filter's frequency response, use the
calculator at: http://www.earlevel.com/main/2013/10/13/biquad-calculator-v2/
###
getQ: -> @Q
setQ: (q) ->
@parameterType = Filter.Q
@Q = Math.max(Math.min(q, 115.0), 0.001)
@recalculateCoefficients()
q
#
# Advanced filter parameters
coefficients: ->
b = [@b0, @b1, @b2]
a = [@a0, @a1, @a2]
{b: b, a: a}
setFilterType: (type) ->
@type = type
@recalculateCoefficients()
###
setBW()
Set band width value used in "band" filter types.
Arguments
Number value representing the filter's band width. Ignored unless filter type
is set to band pass.
###
setBW: (bw) ->
@parameterType = Filter.BW
@BW = bw
@recalculateCoefficients()
bw
setS: (s) ->
@parameterType = Filter.S
@S = Math.max(Math.min(s, 5.0), 0.0001)
@recalculateCoefficients()
###
setF0()
Sometimes referred to as the center frequency, midpoint frequency, or cutoff
frequency, depending on filter type.
Arguments
Number value representing center frequency in Hz. Default 300.
###
setF0: (freq) ->
@f0 = freq
@recalculateCoefficients()
setDbGain: (g) ->
@dBgain = g
@recalculateCoefficients()
recalculateCoefficients: ->
if @type == Filter.PEAKING_EQ || @type == Filter.LOW_SHELF || @type == Filter.HIGH_SHELF
A = Math.pow(10, (@dBgain/40)) # for peaking and shelving EQ filters only
else
A = Math.sqrt( Math.pow(10, (@dBgain/20)) )
w0 = 2 * Math.PI * @f0 / @Fs
cosw0 = Math.cos(w0)
sinw0 = Math.sin(w0)
alpha = 0
switch @parameterType
when Filter.Q
alpha = sinw0/(2*@Q)
when Filter.BW
alpha = sinw0 * sinh( Math.LN2/2 * @BW * w0/sinw0 )
when Filter.S
alpha = sinw0/2 * Math.sqrt( (A + 1/A)*(1/@S - 1) + 2 )
#
# FYI: The relationship between bandwidth and Q is
# 1/Q = 2*sinh(ln(2)/2*BW*w0/sin(w0)) (digital filter w BLT)
# or 1/Q = 2*sinh(ln(2)/2*BW) (analog filter prototype)
#
# The relationship between shelf slope and Q is
# 1/Q = sqrt((A + 1/A)*(1/S - 1) + 2)
#
switch @type
when Filter.LOW_PASS # H(s) = 1 / (s^2 + s/Q + 1)
@b0 = (1 - cosw0)/2
@b1 = 1 - cosw0
@b2 = (1 - cosw0)/2
@a0 = 1 + alpha
@a1 = -2 * cosw0
@a2 = 1 - alpha
when Filter.HIGH_PASS # H(s) = s^2 / (s^2 + s/Q + 1)
@b0 = (1 + cosw0)/2
@b1 = -(1 + cosw0)
@b2 = (1 + cosw0)/2
@a0 = 1 + alpha
@a1 = -2 * cosw0
@a2 = 1 - alpha
when Filter.BAND_PASS_CONSTANT_SKIRT # H(s) = s / (s^2 + s/Q + 1) (constant skirt gain, peak gain = Q)
@b0 = sinw0/2
@b1 = 0
@b2 = -sinw0/2
@a0 = 1 + alpha
@a1 = -2*cosw0
@a2 = 1 - alpha
when Filter.BAND_PASS_CONSTANT_PEAK # H(s) = (s/Q) / (s^2 + s/Q + 1) (constant 0 dB peak gain)
@b0 = alpha
@b1 = 0
@b2 = -alpha
@a0 = 1 + alpha
@a1 = -2*cosw0
@a2 = 1 - alpha
when Filter.NOTCH # H(s) = (s^2 + 1) / (s^2 + s/Q + 1)
@b0 = 1
@b1 = -2*cosw0
@b2 = 1
@a0 = 1 + alpha
@a1 = -2*cosw0
@a2 = 1 - alpha
when Filter.ALL_PASS # H(s) = (s^2 - s/Q + 1) / (s^2 + s/Q + 1)
@b0 = 1 - alpha
@b1 = -2*cosw0
@b2 = 1 + alpha
@a0 = 1 + alpha
@a1 = -2*cosw0
@a2 = 1 - alpha
when Filter.PEAKING_EQ # H(s) = (s^2 + s*(A/Q) + 1) / (s^2 + s/(A*Q) + 1)
@b0 = 1 + alpha*A
@b1 = -2*cosw0
@b2 = 1 - alpha*A
@a0 = 1 + alpha/A
@a1 = -2*cosw0
@a2 = 1 - alpha/A
when Filter.LOW_SHELF # H(s) = A * (s^2 + (sqrt(A)/Q)*s + A)/(A*s^2 + (sqrt(A)/Q)*s + 1)
coeff = sinw0 * Math.sqrt( (A^2 + 1)*(1/@S - 1) + 2*A )
@b0 = A*((A+1) - (A-1)*cosw0 + coeff)
@b1 = 2*A*((A-1) - (A+1)*cosw0)
@b2 = A*((A+1) - (A-1)*cosw0 - coeff)
@a0 = (A+1) + (A-1)*cosw0 + coeff
@a1 = -2*((A-1) + (A+1)*cosw0)
@a2 = (A+1) + (A-1)*cosw0 - coeff
when Filter.HIGH_SHELF # H(s) = A * (A*s^2 + (sqrt(A)/Q)*s + 1)/(s^2 + (sqrt(A)/Q)*s + A)
coeff = sinw0 * Math.sqrt( (A^2 + 1)*(1/@S - 1) + 2*A )
@b0 = A*((A+1) + (A-1)*cosw0 + coeff)
@b1 = -2*A*((A-1) + (A+1)*cosw0)
@b2 = A*((A+1) + (A-1)*cosw0 - coeff)
@a0 = (A+1) - (A-1)*cosw0 + coeff
@a1 = 2*((A-1) - (A+1)*cosw0)
@a2 = (A+1) - (A-1)*cosw0 - coeff
@b0a0 = @b0/@a0
@b1a0 = @b1/@a0
@b2a0 = @b2/@a0
@a1a0 = @a1/@a0
@a2a0 = @a2/@a0
###
process()
---------
Arguments
A single instance of Oscillator or the returned value from another process()
call (such as Mixer).
Returns
An object that can be passed into play(), used in buildSample(), or passed
into another object's process() method. More precisely, process() returns a
closure in the format of `(time) -> sample`.
Usage 1
var someOscillator = new Oscillator({type: Oscillator.SAWTOOTH});
var filter = new Filter;
var processor = filter.process(someOscillator);
var buildSample = function(time) {
return processor(time);
}
Usage 2
var someOscillator1 = new Oscillator({type: Oscillator.SQUARE});
var someOscillator2 = new Oscillator;
var filter = new Fiter;
var processor = filter.process(envelope.process(mixer.process(someOscillator1, someOscillator2)));
var buildSample = function(time) {
return processor(time);
}
###
process: (child) ->
unless Function.isFunction(child)
throw new Error "#{@constructor.name}.process() requires a sound generator but did not receive any."
f = (time) => @realProcess(time, child(time))
f.duration = child.duration if child.duration
f
realProcess: (time, inputSample) ->
#y[n] = (b0/a0)*x[n] + (b1/a0)*x[n-1] + (b2/a0)*x[n-2]
# - (a1/a0)*y[n-1] - (a2/a0)*y[n-2]
sample = inputSample
output = @b0a0*sample + @b1a0*@x_1_l + @b2a0*@x_2_l - @a1a0*@y_1_l - @a2a0*@y_2_l
@y_2_l = @y_1_l
@y_1_l = output
@x_2_l = @x_1_l
@x_1_l = sample
output