-
Notifications
You must be signed in to change notification settings - Fork 32
Expand file tree
/
Copy pathfunctions.ts
More file actions
914 lines (828 loc) · 26.2 KB
/
functions.ts
File metadata and controls
914 lines (828 loc) · 26.2 KB
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
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
import context from 'js-slang/context';
import {
pair,
head,
tail,
list,
length,
is_null,
is_pair,
accumulate,
type List
} from 'js-slang/dist/stdlib/list';
import { RIFFWAVE } from './riffwave';
import type {
Wave,
Sound,
SoundProducer,
SoundTransformer,
AudioPlayed,
} from './types';
// Global Constants and Variables
const FS: number = 44100; // Output sample rate
const fourier_expansion_level: number = 5; // fourier expansion level
const audioPlayed: AudioPlayed[] = [];
context.moduleContexts.sound.state = {
audioPlayed
};
// Singular audio context for all playback functions
let audioplayer: AudioContext;
// Track if a sound is currently playing
let isPlaying: boolean;
// Instantiates new audio context
function init_audioCtx(): void {
audioplayer = new window.AudioContext();
// audioplayer = new (window.AudioContext || window.webkitAudioContext)();
}
// linear decay from 1 to 0 over decay_period
function linear_decay(decay_period: number): (t: number) => number {
return (t) => {
if (t > decay_period || t < 0) {
return 0;
}
return 1 - t / decay_period;
};
}
// // ---------------------------------------------
// // Microphone Functionality
// // ---------------------------------------------
// permission initially undefined
// set to true by granting microphone permission
// set to false by denying microphone permission
let permission: boolean | undefined;
let recorded_sound: Sound | undefined;
// check_permission is called whenever we try
// to record a sound
function check_permission() {
if (permission === undefined) {
throw new Error(
'Call init_record(); to obtain permission to use microphone'
);
} else if (permission === false) {
throw new Error(`Permission has been denied.\n
Re-start browser and call init_record();\n
to obtain permission to use microphone.`);
} // (permission === true): do nothing
}
let globalStream: any;
function rememberStream(stream: any) {
permission = true;
globalStream = stream;
}
function setPermissionToFalse() {
permission = false;
}
function start_recording(mediaRecorder: MediaRecorder) {
const data: any[] = [];
mediaRecorder.ondataavailable = (e) => e.data.size && data.push(e.data);
mediaRecorder.start();
mediaRecorder.onstop = () => process(data);
}
// duration of recording signal in milliseconds
const recording_signal_ms = 100;
// duration of pause after "run" before recording signal is played
const pre_recording_signal_pause_ms = 200;
function play_recording_signal() {
play(sine_sound(1200, recording_signal_ms / 1000));
}
function process(data) {
const audioContext = new AudioContext();
const blob = new Blob(data);
convertToArrayBuffer(blob)
.then((arrayBuffer) => audioContext.decodeAudioData(arrayBuffer))
.then(save);
}
// Converts input microphone sound (blob) into array format.
function convertToArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
const url = URL.createObjectURL(blob);
return fetch(url)
.then((response) => response.arrayBuffer());
}
function save(audioBuffer: AudioBuffer) {
const array = audioBuffer.getChannelData(0);
const duration = array.length / FS;
recorded_sound = make_sound((t) => {
const index = t * FS;
const lowerIndex = Math.floor(index);
const upperIndex = lowerIndex + 1;
const ratio = index - lowerIndex;
const upper = array[upperIndex] ? array[upperIndex] : 0;
const lower = array[lowerIndex] ? array[lowerIndex] : 0;
return lower * (1 - ratio) + upper * ratio;
}, duration);
}
/**
* Initialize recording by obtaining permission
* to use the default device microphone
*
* @returns string "obtaining recording permission"
*/
export function init_record(): string {
navigator.mediaDevices
.getUserMedia({ audio: true })
.then(rememberStream, setPermissionToFalse);
return 'obtaining recording permission';
}
/**
* Records a sound until the returned stop function is called.
* Takes a <CODE>buffer</CODE> duration (in seconds) as argument, and
* returns a nullary stop function <CODE>stop</CODE>. A call
* <CODE>stop()</CODE> returns a Sound promise: a nullary function
* that returns a Sound. Example: <PRE><CODE>init_record();
* const stop = record(0.5);
* // record after 0.5 seconds. Then in next query:
* const promise = stop();
* // In next query, you can play the promised sound, by
* // applying the promise:
* play(promise());</CODE></PRE>
* @param buffer - pause before recording, in seconds
* @returns nullary <CODE>stop</CODE> function;
* <CODE>stop()</CODE> stops the recording and
* returns a Sound promise: a nullary function that returns the recorded Sound
*/
export function record(buffer: number): () => () => Sound {
check_permission();
const mediaRecorder = new MediaRecorder(globalStream);
setTimeout(() => {
play_recording_signal();
start_recording(mediaRecorder);
}, recording_signal_ms + buffer * 1000);
return () => {
mediaRecorder.stop();
play_recording_signal();
return () => {
if (recorded_sound === undefined) {
throw new Error('recording still being processed');
} else {
return recorded_sound;
}
};
};
}
/**
* Records a sound of given <CODE>duration</CODE> in seconds, after
* a <CODE>buffer</CODE> also in seconds, and
* returns a Sound promise: a nullary function
* that returns a Sound. Example: <PRE><CODE>init_record();
* const promise = record_for(2, 0.5);
* // In next query, you can play the promised Sound, by
* // applying the promise:
* play(promise());</CODE></PRE>
* @param duration duration in seconds
* @param buffer pause before recording, in seconds
* @return <CODE>promise</CODE>: nullary function which returns recorded Sound
*/
export function record_for(duration: number, buffer: number): () => Sound {
recorded_sound = undefined;
const recording_ms = duration * 1000;
const pre_recording_pause_ms = buffer * 1000;
check_permission();
const mediaRecorder = new MediaRecorder(globalStream);
// order of events for record_for:
// pre-recording-signal pause | recording signal |
// pre-recording pause | recording | recording signal
setTimeout(() => {
play_recording_signal();
setTimeout(() => {
start_recording(mediaRecorder);
setTimeout(() => {
mediaRecorder.stop();
play_recording_signal();
}, recording_ms);
}, recording_signal_ms + pre_recording_pause_ms);
}, pre_recording_signal_pause_ms);
return () => {
if (recorded_sound === undefined) {
throw new Error('recording still being processed');
} else {
return recorded_sound;
}
};
}
// =============================================================================
// Module's Exposed Functions
//
// This file only includes the implementation and documentation of exposed
// functions of the module. For private functions dealing with the browser's
// graphics library context, see './webGL_curves.ts'.
// =============================================================================
// Core functions
/**
* Makes a Sound with given wave function and duration.
* The wave function is a function: number -> number
* that takes in a non-negative input time and returns an amplitude
* between -1 and 1.
*
* @param wave wave function of the Sound
* @param duration duration of the Sound
* @return with wave as wave function and duration as duration
* @example const s = make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5);
*/
export function make_sound(wave: Wave, duration: number): Sound {
if (duration < 0) {
throw new Error('Sound duration must be greater than or equal to 0');
}
return pair((t: number) => (t >= duration ? 0 : wave(t)), duration);
}
/**
* Accesses the wave function of a given Sound.
*
* @param sound given Sound
* @return the wave function of the Sound
* @example get_wave(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns t => Math_sin(2 * Math_PI * 440 * t)
*/
export function get_wave(sound: Sound): Wave {
return head(sound);
}
/**
* Accesses the duration of a given Sound.
*
* @param sound given Sound
* @return the duration of the Sound
* @example get_duration(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns 5
*/
export function get_duration(sound: Sound): number {
return tail(sound);
}
/**
* Checks if the argument is a Sound
*
* @param x input to be checked
* @return true if x is a Sound, false otherwise
* @example is_sound(make_sound(t => 0, 2)); // Returns true
*/
export function is_sound(x: any): x is Sound {
return (
is_pair(x)
&& typeof get_wave(x) === 'function'
&& typeof get_duration(x) === 'number'
);
}
/**
* Plays the given Wave using the computer’s sound device, for the duration
* given in seconds.
*
* @param wave the wave function to play, starting at 0
* @return the resulting Sound
* @example play_wave(t => math_sin(t * 3000), 5);
*/
export function play_wave(wave: Wave, duration: number): Sound {
return play(make_sound(wave, duration));
}
/**
* Plays the given Sound using the computer’s sound device.
* The sound is added to a list of sounds to be played one-at-a-time
* in a Source Academy tab.
*
* @param sound the Sound to play
* @return the given Sound
* @example play_in_tab(sine_sound(440, 5));
*/
export function play_in_tab(sound: Sound): Sound {
// Type-check sound
if (!is_sound(sound)) {
throw new Error(`${play_in_tab.name} is expecting sound, but encountered ${sound}`);
// If a sound is already playing, terminate execution.
} else if (isPlaying) {
throw new Error(`${play_in_tab.name}: audio system still playing previous sound`);
} else if (get_duration(sound) < 0) {
throw new Error(`${play_in_tab.name}: duration of sound is negative`);
} else if (get_duration(sound) === 0) {
return sound;
} else {
// Instantiate audio context if it has not been instantiated.
if (!audioplayer) {
init_audioCtx();
}
// Create mono buffer
const channel: number[] = [];
const len = Math.ceil(FS * get_duration(sound));
let temp: number;
let prev_value = 0;
const wave = get_wave(sound);
for (let i = 0; i < len; i += 1) {
temp = wave(i / FS);
// clip amplitude
// channel[i] = temp > 1 ? 1 : temp < -1 ? -1 : temp;
if (temp > 1) {
channel[i] = 1;
} else if (temp < -1) {
channel[i] = -1;
} else {
channel[i] = temp;
}
// smoothen out sudden cut-outs
if (channel[i] === 0 && Math.abs(channel[i] - prev_value) > 0.01) {
channel[i] = prev_value * 0.999;
}
prev_value = channel[i];
}
// quantize
for (let i = 0; i < channel.length; i += 1) {
channel[i] = Math.floor(channel[i] * 32767.999);
}
const riffwave = new RIFFWAVE([]);
riffwave.header.sampleRate = FS;
riffwave.header.numChannels = 1;
riffwave.header.bitsPerSample = 16;
riffwave.Make(channel);
const soundToPlay = {
toReplString: () => '<AudioPlayed>',
dataUri: riffwave.dataURI
};
audioPlayed.push(soundToPlay);
return sound;
}
}
/**
* Plays the given Sound using the computer’s sound device
* on top of any Sounds that are currently playing.
*
* @param sound the Sound to play
* @return the given Sound
* @example play(sine_sound(440, 5));
*/
export function play(sound: Sound): Sound {
// Type-check sound
if (!is_sound(sound)) {
throw new Error(
`${play.name} is expecting sound, but encountered ${sound}`
);
} else if (get_duration(sound) < 0) {
throw new Error(`${play.name}: duration of sound is negative`);
} else if (get_duration(sound) === 0) {
return sound;
} else {
// Instantiate audio context if it has not been instantiated.
if (!audioplayer) {
init_audioCtx();
}
// Create mono buffer
const theBuffer = audioplayer.createBuffer(
1,
Math.ceil(FS * get_duration(sound)),
FS
);
const channel = theBuffer.getChannelData(0);
let temp: number;
let prev_value = 0;
const wave = get_wave(sound);
for (let i = 0; i < channel.length; i += 1) {
temp = wave(i / FS);
// clip amplitude
if (temp > 1) {
channel[i] = 1;
} else if (temp < -1) {
channel[i] = -1;
} else {
channel[i] = temp;
}
// smoothen out sudden cut-outs
if (channel[i] === 0 && Math.abs(channel[i] - prev_value) > 0.01) {
channel[i] = prev_value * 0.999;
}
prev_value = channel[i];
}
// Connect data to output destination
const source = audioplayer.createBufferSource();
source.buffer = theBuffer;
source.connect(audioplayer.destination);
isPlaying = true;
source.start();
source.onended = () => {
source.disconnect(audioplayer.destination);
isPlaying = false;
};
return sound;
}
}
/**
* Stops all currently playing sounds.
*/
export function stop(): void {
audioplayer.close();
isPlaying = false;
}
// Primitive sounds
/**
* Makes a noise Sound with given duration
*
* @param duration the duration of the noise sound
* @return resulting noise Sound
* @example noise_sound(5);
*/
export function noise_sound(duration: number): Sound {
return make_sound((_t) => Math.random() * 2 - 1, duration);
}
/**
* Makes a silence Sound with given duration
*
* @param duration the duration of the silence Sound
* @return resulting silence Sound
* @example silence_sound(5);
*/
export function silence_sound(duration: number): Sound {
return make_sound((_t) => 0, duration);
}
/**
* Makes a sine wave Sound with given frequency and duration
*
* @param freq the frequency of the sine wave Sound
* @param duration the duration of the sine wave Sound
* @return resulting sine wave Sound
* @example sine_sound(440, 5);
*/
export function sine_sound(freq: number, duration: number): Sound {
return make_sound((t) => Math.sin(2 * Math.PI * t * freq), duration);
}
/**
* Makes a square wave Sound with given frequency and duration
*
* @param freq the frequency of the square wave Sound
* @param duration the duration of the square wave Sound
* @return resulting square wave Sound
* @example square_sound(440, 5);
*/
export function square_sound(f: number, duration: number): Sound {
function fourier_expansion_square(t: number) {
let answer = 0;
for (let i = 1; i <= fourier_expansion_level; i += 1) {
answer += Math.sin(2 * Math.PI * (2 * i - 1) * f * t) / (2 * i - 1);
}
return answer;
}
return make_sound(
(t) => (4 / Math.PI) * fourier_expansion_square(t),
duration
);
}
/**
* Makes a triangle wave Sound with given frequency and duration
*
* @param freq the frequency of the triangle wave Sound
* @param duration the duration of the triangle wave Sound
* @return resulting triangle wave Sound
* @example triangle_sound(440, 5);
*/
export function triangle_sound(freq: number, duration: number): Sound {
function fourier_expansion_triangle(t: number) {
let answer = 0;
for (let i = 0; i < fourier_expansion_level; i += 1) {
answer
+= ((-1) ** i * Math.sin((2 * i + 1) * t * freq * Math.PI * 2))
/ (2 * i + 1) ** 2;
}
return answer;
}
return make_sound(
(t) => (8 / Math.PI / Math.PI) * fourier_expansion_triangle(t),
duration
);
}
/**
* Makes a sawtooth wave Sound with given frequency and duration
*
* @param freq the frequency of the sawtooth wave Sound
* @param duration the duration of the sawtooth wave Sound
* @return resulting sawtooth wave Sound
* @example sawtooth_sound(440, 5);
*/
export function sawtooth_sound(freq: number, duration: number): Sound {
function fourier_expansion_sawtooth(t: number) {
let answer = 0;
for (let i = 1; i <= fourier_expansion_level; i += 1) {
answer += Math.sin(2 * Math.PI * i * freq * t) / i;
}
return answer;
}
return make_sound(
(t) => 1 / 2 - (1 / Math.PI) * fourier_expansion_sawtooth(t),
duration
);
}
// Composition Operators
/**
* Makes a new Sound by combining the sounds in a given list
* where the second Sound is appended to the end of the first Sound,
* the third Sound is appended to the end of the second Sound, and
* so on. The effect is that the Sounds in the list are joined end-to-end
*
* @param list_of_sounds given list of Sounds
* @return the combined Sound
* @example consecutively(list(sine_sound(200, 2), sine_sound(400, 3)));
*/
export function consecutively(list_of_sounds: List): Sound {
function consec_two(ss1: Sound, ss2: Sound) {
const wave1 = get_wave(ss1);
const wave2 = get_wave(ss2);
const dur1 = get_duration(ss1);
const dur2 = get_duration(ss2);
const new_wave = (t: number) => (t < dur1 ? wave1(t) : wave2(t - dur1));
return make_sound(new_wave, dur1 + dur2);
}
return accumulate(consec_two, silence_sound(0), list_of_sounds);
}
/**
* Makes a new Sound by combining the Sounds in a given list.
* In the result sound, the component sounds overlap such that
* they start at the beginning of the result sound. To achieve
* this, the amplitudes of the component sounds are added together
* and then divided by the length of the list.
*
* @param list_of_sounds given list of Sounds
* @return the combined Sound
* @example simultaneously(list(sine_sound(200, 2), sine_sound(400, 3)))
*/
export function simultaneously(list_of_sounds: List): Sound {
function simul_two(ss1: Sound, ss2: Sound) {
const wave1 = get_wave(ss1);
const wave2 = get_wave(ss2);
const dur1 = get_duration(ss1);
const dur2 = get_duration(ss2);
// new_wave assumes sound discipline (ie, wave(t) = 0 after t > dur)
const new_wave = (t: number) => wave1(t) + wave2(t);
// new_dur is higher of the two dur
const new_dur = dur1 < dur2 ? dur2 : dur1;
return make_sound(new_wave, new_dur);
}
const mushed_sounds = accumulate(simul_two, silence_sound(0), list_of_sounds);
const len = length(list_of_sounds);
const normalised_wave = (t: number) => head(mushed_sounds)(t) / len;
const highest_duration = tail(mushed_sounds);
return make_sound(normalised_wave, highest_duration);
}
/**
* Returns an envelope: a function from Sound to Sound.
* When the adsr envelope is applied to a Sound, it returns
* a new Sound with its amplitude modified according to parameters
* The relative amplitude increases from 0 to 1 linearly over the
* attack proportion, then decreases from 1 to sustain level over the
* decay proportion, and remains at that level until the release
* proportion when it decays back to 0.
* @param attack_ratio proportion of Sound in attack phase
* @param decay_ratio proportion of Sound decay phase
* @param sustain_level sustain level between 0 and 1
* @param release_ratio proportion of Sound in release phase
* @return Envelope a function from Sound to Sound
* @example adsr(0.2, 0.3, 0.3, 0.1)(sound);
*/
export function adsr(
attack_ratio: number,
decay_ratio: number,
sustain_level: number,
release_ratio: number
): SoundTransformer {
return (sound) => {
const wave = get_wave(sound);
const duration = get_duration(sound);
const attack_time = duration * attack_ratio;
const decay_time = duration * decay_ratio;
const release_time = duration * release_ratio;
return make_sound((x) => {
if (x < attack_time) {
return wave(x) * (x / attack_time);
}
if (x < attack_time + decay_time) {
return (
((1 - sustain_level) * linear_decay(decay_time)(x - attack_time)
+ sustain_level)
* wave(x)
);
}
if (x < duration - release_time) {
return wave(x) * sustain_level;
}
return (
wave(x)
* sustain_level
* linear_decay(release_time)(x - (duration - release_time))
);
}, duration);
};
}
/**
* Returns a Sound that results from applying a list of envelopes
* to a given wave form. The wave form is a Sound generator that
* takes a frequency and a duration as arguments and produces a
* Sound with the given frequency and duration. Each envelope is
* applied to a harmonic: the first harmonic has the given frequency,
* the second has twice the frequency, the third three times the
* frequency etc. The harmonics are then layered simultaneously to
* produce the resulting Sound.
* @param waveform function from pair(frequency, duration) to Sound
* @param base_frequency frequency of the first harmonic
* @param duration duration of the produced Sound, in seconds
* @param envelopes – list of envelopes, which are functions from Sound to Sound
* @return Sound resulting Sound
* @example stacking_adsr(sine_sound, 300, 5, list(adsr(0.1, 0.3, 0.2, 0.5), adsr(0.2, 0.5, 0.6, 0.1), adsr(0.3, 0.1, 0.7, 0.3)));
*/
export function stacking_adsr(
waveform: SoundProducer,
base_frequency: number,
duration: number,
envelopes: List
): Sound {
function zip(lst: List, n: number) {
if (is_null(lst)) {
return lst;
}
return pair(pair(n, head(lst)), zip(tail(lst), n + 1));
}
return simultaneously(
accumulate(
(x: any, y: any) => pair(tail(x)(waveform(base_frequency * head(x), duration)), y),
null,
zip(envelopes, 1)
)
);
}
/**
* Returns a Sound transformer which uses its argument
* to modulate the phase of a (carrier) sine wave
* of given frequency and duration with a given Sound.
* Modulating with a low frequency Sound results in a vibrato effect.
* Modulating with a Sound with frequencies comparable to
* the sine wave frequency results in more complex wave forms.
*
* @param freq the frequency of the sine wave to be modulated
* @param duration the duration of the output Sound
* @param amount the amount of modulation to apply to the carrier sine wave
* @return function which takes in a Sound and returns a Sound
* @example phase_mod(440, 5, 1)(sine_sound(220, 5));
*/
export function phase_mod(
freq: number,
duration: number,
amount: number
): SoundTransformer {
return (modulator: Sound) => make_sound(
(t) => Math.sin(2 * Math.PI * t * freq + amount * get_wave(modulator)(t)),
duration
);
}
// MIDI conversion functions
/**
* Converts a letter name to its corresponding MIDI note.
* The letter name is represented in standard pitch notation.
* Examples are "A5", "Db3", "C#7".
* Refer to <a href="https://i.imgur.com/qGQgmYr.png">this mapping from
* letter name to midi notes.
*
* @param letter_name given letter name
* @return the corresponding midi note
* @example letter_name_to_midi_note("C4"); // Returns 60
*/
export function letter_name_to_midi_note(note: string): number {
let res = 12; // C0 is midi note 12
const n = note[0].toUpperCase();
switch (n) {
case 'D':
res += 2;
break;
case 'E':
res += 4;
break;
case 'F':
res += 5;
break;
case 'G':
res += 7;
break;
case 'A':
res += 9;
break;
case 'B':
res += 11;
break;
default:
break;
}
if (note.length === 2) {
res += parseInt(note[1]) * 12;
} else if (note.length === 3) {
switch (note[1]) {
case '#':
res += 1;
break;
case 'b':
res -= 1;
break;
default:
break;
}
res += parseInt(note[2]) * 12;
}
return res;
}
/**
* Converts a MIDI note to its corresponding frequency.
*
* @param note given MIDI note
* @return the frequency of the MIDI note
* @example midi_note_to_frequency(69); // Returns 440
*/
export function midi_note_to_frequency(note: number): number {
// A4 = 440Hz = midi note 69
return 440 * 2 ** ((note - 69) / 12);
}
/**
* Converts a letter name to its corresponding frequency.
*
* @param letter_name given letter name
* @return the corresponding frequency
* @example letter_name_to_frequency("A4"); // Returns 440
*/
export function letter_name_to_frequency(note: string): number {
return midi_note_to_frequency(letter_name_to_midi_note(note));
}
// Instruments
/**
* returns a Sound reminiscent of a bell, playing
* a given note for a given duration
* @param note MIDI note
* @param duration duration in seconds
* @return Sound resulting bell Sound with given pitch and duration
* @example bell(40, 1);
*/
export function bell(note: number, duration: number): Sound {
return stacking_adsr(
square_sound,
midi_note_to_frequency(note),
duration,
list(
adsr(0, 0.6, 0, 0.05),
adsr(0, 0.6618, 0, 0.05),
adsr(0, 0.7618, 0, 0.05),
adsr(0, 0.9071, 0, 0.05)
)
);
}
/**
* returns a Sound reminiscent of a cello, playing
* a given note for a given duration
* @param note MIDI note
* @param duration duration in seconds
* @return Sound resulting cello Sound with given pitch and duration
* @example cello(36, 5);
*/
export function cello(note: number, duration: number): Sound {
return stacking_adsr(
square_sound,
midi_note_to_frequency(note),
duration,
list(adsr(0.05, 0, 1, 0.1), adsr(0.05, 0, 1, 0.15), adsr(0, 0, 0.2, 0.15))
);
}
/**
* returns a Sound reminiscent of a piano, playing
* a given note for a given duration
* @param note MIDI note
* @param duration duration in seconds
* @return Sound resulting piano Sound with given pitch and duration
* @example piano(48, 5);
*/
export function piano(note: number, duration: number): Sound {
return stacking_adsr(
triangle_sound,
midi_note_to_frequency(note),
duration,
list(adsr(0, 0.515, 0, 0.05), adsr(0, 0.32, 0, 0.05), adsr(0, 0.2, 0, 0.05))
);
}
/**
* returns a Sound reminiscent of a trombone, playing
* a given note for a given duration
* @param note MIDI note
* @param duration duration in seconds
* @return Sound resulting trombone Sound with given pitch and duration
* @example trombone(60, 2);
*/
export function trombone(note: number, duration: number): Sound {
return stacking_adsr(
square_sound,
midi_note_to_frequency(note),
duration,
list(adsr(0.2, 0, 1, 0.1), adsr(0.3236, 0.6, 0, 0.1))
);
}
/**
* returns a Sound reminiscent of a violin, playing
* a given note for a given duration
* @param note MIDI note
* @param duration duration in seconds
* @return Sound resulting violin Sound with given pitch and duration
* @example violin(53, 4);
*/
export function violin(note: number, duration: number): Sound {
return stacking_adsr(
sawtooth_sound,
midi_note_to_frequency(note),
duration,
list(
adsr(0.35, 0, 1, 0.15),
adsr(0.35, 0, 1, 0.15),
adsr(0.45, 0, 1, 0.15),
adsr(0.45, 0, 1, 0.15)
)
);
}