littlejsengine 1.18.28 → 1.19.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FAQ.md +5 -0
- package/README.md +207 -199
- package/dist/littlejs.d.ts +2099 -48
- package/dist/littlejs.esm.js +16251 -10315
- package/dist/littlejs.esm.min.js +13 -1
- package/dist/littlejs.js +15747 -9888
- package/dist/littlejs.min.js +13 -1
- package/dist/littlejs.release.js +15719 -9867
- package/package.json +1 -1
- package/plugins/audioEffects.js +371 -0
- package/plugins/lightSystem.js +5 -3
- package/plugins/math3d.js +870 -0
- package/plugins/medalSystem.js +280 -280
- package/plugins/pluginExport.js +181 -115
- package/plugins/postProcess.js +241 -166
- package/plugins/render3d.js +4006 -0
- package/plugins/textureSheet.js +458 -458
- package/plugins/threejs.js +6 -1
- package/plugins/tweenSystem.js +528 -526
- package/plugins/zzfxm.js +159 -166
- package/src/engine.js +172 -137
- package/src/engineAudio.js +918 -775
- package/src/engineBuild.mjs +3 -0
- package/src/engineDebug.js +15 -8
- package/src/engineDraw.js +1672 -1510
- package/src/engineExport.js +407 -396
- package/src/engineInput.js +1 -1
- package/src/engineLogo.js +4 -3
- package/src/engineMath.js +69 -0
- package/src/engineObject.js +560 -551
- package/src/engineSettings.js +759 -725
- package/src/engineTileLayer.js +3 -3
- package/src/engineUtilities.js +13 -0
- package/src/engineWebGL.js +1005 -941
package/src/engineAudio.js
CHANGED
|
@@ -1,776 +1,919 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LittleJS Audio System
|
|
3
|
-
* - Play audio files (mp3, ogg, wave) and generate sounds with ZzFX
|
|
4
|
-
* - ZzFX sound generator integration: <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX</a>
|
|
5
|
-
* - Sound caching for fast playback and memory efficiency
|
|
6
|
-
* - Volume control with attenuation and stereo panning
|
|
7
|
-
* - 2D spatial audio based on camera position with distance-based falloff
|
|
8
|
-
* - Sound instance management (pause, resume, stop)
|
|
9
|
-
* - Speech synthesis for text-to-speech
|
|
10
|
-
* - Music playback with ZzFXM support
|
|
11
|
-
* - Web Audio API integration with master gain control
|
|
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
|
-
if (
|
|
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
|
-
this.
|
|
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
|
-
this.
|
|
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
|
-
this.
|
|
425
|
-
|
|
426
|
-
this.
|
|
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
|
-
* @param {number}
|
|
649
|
-
* @param {number}
|
|
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
|
-
|
|
1
|
+
/**
|
|
2
|
+
* LittleJS Audio System
|
|
3
|
+
* - Play audio files (mp3, ogg, wave) and generate sounds with ZzFX
|
|
4
|
+
* - ZzFX sound generator integration: <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX</a>
|
|
5
|
+
* - Sound caching for fast playback and memory efficiency
|
|
6
|
+
* - Volume control with attenuation and stereo panning
|
|
7
|
+
* - 2D spatial audio based on camera position with distance-based falloff
|
|
8
|
+
* - Sound instance management (pause, resume, stop)
|
|
9
|
+
* - Speech synthesis for text-to-speech
|
|
10
|
+
* - Music playback with ZzFXM support
|
|
11
|
+
* - Web Audio API integration with master gain control
|
|
12
|
+
* - Sounds and the master bus can route through effects, see the audio effects plugin
|
|
13
|
+
* @namespace Audio
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
/** Audio context used by the engine
|
|
19
|
+
* @type {AudioContext}
|
|
20
|
+
* @memberof Audio */
|
|
21
|
+
let audioContext = new AudioContext;
|
|
22
|
+
|
|
23
|
+
/** Master gain node for all audio to pass through, made at load so effects can connect to it any time
|
|
24
|
+
* @type {GainNode}
|
|
25
|
+
* @memberof Audio */
|
|
26
|
+
let audioMasterGain = audioContext.createGain();
|
|
27
|
+
audioMasterGain.connect(audioContext.destination);
|
|
28
|
+
audioMasterGain.gain.value = soundVolume; // set starting value
|
|
29
|
+
|
|
30
|
+
// the current master effect, kept so setAudioMasterEffect can undo the route it made,
|
|
31
|
+
// and whether its output came from an effect, which gets its default route back
|
|
32
|
+
let audioMasterEffectInput, audioMasterEffectOutput, audioMasterEffectOutputIsEffect;
|
|
33
|
+
|
|
34
|
+
/** Default sample rate used for sounds
|
|
35
|
+
* @default 44100
|
|
36
|
+
* @memberof Audio */
|
|
37
|
+
const audioDefaultSampleRate = 44100;
|
|
38
|
+
|
|
39
|
+
/** Check if the audio context is running and available for playback
|
|
40
|
+
* @return {boolean} - True if the audio context is running
|
|
41
|
+
* @memberof Audio */
|
|
42
|
+
function audioIsRunning()
|
|
43
|
+
{ return audioContext.state === 'running'; }
|
|
44
|
+
|
|
45
|
+
function audioInit()
|
|
46
|
+
{
|
|
47
|
+
if (!soundEnable || headlessMode) return;
|
|
48
|
+
|
|
49
|
+
document.addEventListener('visibilitychange', audioVisibilityChange);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// a hidden page stops the game, so its sound stops too, and the audio clock with it so every sound picks up
|
|
53
|
+
// exactly where it was; only a suspend made here is undone, not one the browser holds until the first input
|
|
54
|
+
let audioSuspendedWhenHidden = false;
|
|
55
|
+
function audioVisibilityChange()
|
|
56
|
+
{
|
|
57
|
+
if (document.hidden)
|
|
58
|
+
{
|
|
59
|
+
if (!soundPauseWhenHidden || audioContext.state != 'running') return;
|
|
60
|
+
audioSuspendedWhenHidden = true;
|
|
61
|
+
audioContext.suspend();
|
|
62
|
+
}
|
|
63
|
+
else if (audioSuspendedWhenHidden)
|
|
64
|
+
{
|
|
65
|
+
audioSuspendedWhenHidden = false;
|
|
66
|
+
audioContext.resume();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Anything with input and output audio nodes, like an effect from the audio effects plugin
|
|
71
|
+
* @typedef {{input: AudioNode, output: AudioNode}} AudioEffectNodes
|
|
72
|
+
* @memberof Audio */
|
|
73
|
+
|
|
74
|
+
/** Route all sound through an effect between the master gain and the speakers
|
|
75
|
+
* - Pass a node or an effect, or the first and last of a chain, each a node or an effect
|
|
76
|
+
* - With one argument a node is both ends, and an effect uses its own input and output
|
|
77
|
+
* - The output node is disconnected from everything else first, so it only feeds the speakers
|
|
78
|
+
* - The two ends of a chain must already be connected to each other, like effectA.connect(effectB)
|
|
79
|
+
* - Call with no arguments to remove the effect, an effect that was the master goes back to feeding the master gain
|
|
80
|
+
* - Debug video capture records the end of the master chain, but loses its tap if the effect changes mid-capture
|
|
81
|
+
* @param {AudioNode|AudioEffectNodes} [input] - Node or effect the master gain connects to
|
|
82
|
+
* @param {AudioNode|AudioEffectNodes} [output] - Node or effect that connects to the audio destination, defaults to the input's output
|
|
83
|
+
* @memberof Audio */
|
|
84
|
+
function setAudioMasterEffect(input, output)
|
|
85
|
+
{
|
|
86
|
+
// an effect stands in for its nodes, and a node is both ends when no output is passed
|
|
87
|
+
// (the output resolves first since its default comes from the input effect)
|
|
88
|
+
const outputArg = output || input;
|
|
89
|
+
const outputIsEffect = !!outputArg && 'input' in outputArg;
|
|
90
|
+
output = audioEffectNode(output, 'output') || audioEffectNode(input, 'output');
|
|
91
|
+
input = audioEffectNode(input, 'input');
|
|
92
|
+
ASSERT(!input || typeof input.connect === 'function', 'input must be an AudioNode or an effect with input and output nodes');
|
|
93
|
+
ASSERT(!output || typeof output.connect === 'function', 'output must be an AudioNode or an effect with input and output nodes');
|
|
94
|
+
|
|
95
|
+
// undo the current route, the master gain selectively so other taps on it survive,
|
|
96
|
+
// but the output node from everything since it only ever fed the speakers;
|
|
97
|
+
// an effect's output then goes back to the master gain, its default, so it still works for sounds
|
|
98
|
+
audioMasterGain.disconnect(audioMasterEffectInput || audioContext.destination);
|
|
99
|
+
audioMasterEffectOutput?.disconnect();
|
|
100
|
+
if (audioMasterEffectOutputIsEffect)
|
|
101
|
+
audioMasterEffectOutput.connect(audioMasterGain);
|
|
102
|
+
audioMasterEffectInput = input;
|
|
103
|
+
audioMasterEffectOutput = output;
|
|
104
|
+
audioMasterEffectOutputIsEffect = outputIsEffect;
|
|
105
|
+
|
|
106
|
+
// connect the master gain to the speakers, through the effect if there is one
|
|
107
|
+
if (input)
|
|
108
|
+
{
|
|
109
|
+
audioMasterGain.connect(input);
|
|
110
|
+
output.disconnect();
|
|
111
|
+
output.connect(audioContext.destination);
|
|
112
|
+
}
|
|
113
|
+
else
|
|
114
|
+
audioMasterGain.connect(audioContext.destination);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// get one of an effect's nodes, or the thing itself when it is already a node
|
|
118
|
+
/** @param {AudioNode|AudioEffectNodes|undefined} effectOrNode
|
|
119
|
+
* @param {'input'|'output'} key
|
|
120
|
+
* @return {AudioNode} */
|
|
121
|
+
function audioEffectNode(effectOrNode, key)
|
|
122
|
+
{
|
|
123
|
+
if (effectOrNode && 'input' in effectOrNode)
|
|
124
|
+
return /** @type {AudioEffectNodes} */ (effectOrNode)[key];
|
|
125
|
+
return /** @type {AudioNode} */ (effectOrNode);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Sound Object - Stores a sound for later
|
|
132
|
+
* - this can be used to load and play wave, mp3, and ogg files
|
|
133
|
+
* - it can also create sounds using the ZzFX sound generator
|
|
134
|
+
* - can attenuate and apply stereo panning to sounds
|
|
135
|
+
* - sound instance control with pause/resume capability
|
|
136
|
+
*
|
|
137
|
+
* <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
|
|
138
|
+
* @memberof Audio
|
|
139
|
+
* @example
|
|
140
|
+
* // load an audio asset file
|
|
141
|
+
* const sound_example = new Sound('sound.mp3');
|
|
142
|
+
*
|
|
143
|
+
* // create a zzfx sound
|
|
144
|
+
* const sound_example = new Sound([.5,.5]);
|
|
145
|
+
*
|
|
146
|
+
* // play a sound
|
|
147
|
+
* sound_example.play();
|
|
148
|
+
*/
|
|
149
|
+
class Sound
|
|
150
|
+
{
|
|
151
|
+
/**
|
|
152
|
+
* @callback SoundLoadCallback - Function called when sound is loaded
|
|
153
|
+
* @param {Sound} sound
|
|
154
|
+
* @memberof Audio
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
/** Create a sound object and cache the audio for later use
|
|
158
|
+
* @param {string|Array} [asset] - Filename of audio file or zzfx array
|
|
159
|
+
* @param {number} [randomness] - How much to randomize frequency each time sound plays, for zzfx sounds the zzfx default is used if undefined
|
|
160
|
+
* @param {number} [range=soundDefaultRange] - World space max range of sound
|
|
161
|
+
* @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
|
|
162
|
+
* @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
|
|
163
|
+
*/
|
|
164
|
+
constructor(asset, randomness, range=soundDefaultRange, taper=soundDefaultTaper, onloadCallback)
|
|
165
|
+
{
|
|
166
|
+
if (!soundEnable || headlessMode) return;
|
|
167
|
+
|
|
168
|
+
ASSERT(!asset || isArray(asset) || isStringLike(asset), 'asset must be a file name or zzfx array');
|
|
169
|
+
ASSERT(randomness === undefined || isNumber(randomness), 'randomness must be a number');
|
|
170
|
+
ASSERT(randomness === undefined || randomness >= 0 && randomness <=1, 'randomness must be between 0 and 1');
|
|
171
|
+
ASSERT(isNumber(range), 'range must be a number');
|
|
172
|
+
ASSERT(isNumber(taper), 'taper must be a number');
|
|
173
|
+
|
|
174
|
+
/** @property {number} - World space max range of sound */
|
|
175
|
+
this.range = range;
|
|
176
|
+
/** @property {number} - At what percentage of range should it start tapering */
|
|
177
|
+
this.taper = taper;
|
|
178
|
+
/** @property {number} - How much to randomize frequency each time sound plays */
|
|
179
|
+
this.randomness = randomness ?? 0;
|
|
180
|
+
/** @property {number} - Sample rate for this sound */
|
|
181
|
+
this.sampleRate = audioDefaultSampleRate;
|
|
182
|
+
/** @property {number} - How many samples per channel this sound has */
|
|
183
|
+
this.sampleLength = 0;
|
|
184
|
+
/** @property {AudioBuffer} - Decoded audio shared by every play of this sound
|
|
185
|
+
* @type {AudioBuffer} */
|
|
186
|
+
this.sampleBuffer = undefined;
|
|
187
|
+
/** @private
|
|
188
|
+
* @type {Array<Array<number>|Float32Array>} */
|
|
189
|
+
this._sampleChannels = undefined;
|
|
190
|
+
/** @property {number} - Percentage of this sound currently loaded, sounds
|
|
191
|
+
* fetched from a url stay at 0 until decoding completes */
|
|
192
|
+
this.loadedPercent = 0;
|
|
193
|
+
/** @property {SoundLoadCallback} - function to call when sound is loaded */
|
|
194
|
+
this.onloadCallback = onloadCallback;
|
|
195
|
+
/** @property {AudioNode|AudioEffectNodes} - Node or effect to route every play of this sound through instead of the master gain
|
|
196
|
+
* - Where this sound's audio goes, unlike AudioEffect.output which is an effect's own node, effects chain with connect()
|
|
197
|
+
* @type {AudioNode|AudioEffectNodes} */
|
|
198
|
+
this.output = undefined;
|
|
199
|
+
|
|
200
|
+
if (isArray(asset))
|
|
201
|
+
{
|
|
202
|
+
// generate zzfx sound — copy so we don't mutate the caller's array
|
|
203
|
+
const zzfxSound = asset.slice();
|
|
204
|
+
|
|
205
|
+
// remove randomness so it can be applied on playback
|
|
206
|
+
const defaultRandomness = randomness ?? .05;
|
|
207
|
+
const randomnessIndex = 1;
|
|
208
|
+
this.randomness = zzfxSound[randomnessIndex] ?? defaultRandomness;
|
|
209
|
+
zzfxSound[randomnessIndex] = 0;
|
|
210
|
+
|
|
211
|
+
// generate the zzfx samples, then hand them to an audio buffer so
|
|
212
|
+
// the plain arrays can be released and every play shares the buffer
|
|
213
|
+
this.sampleChannels = [zzfxG(...zzfxSound)];
|
|
214
|
+
this.buildSampleBuffer();
|
|
215
|
+
this.loadedPercent = 1;
|
|
216
|
+
onloadCallback?.(this);
|
|
217
|
+
}
|
|
218
|
+
else if (typeof asset === 'string')
|
|
219
|
+
{
|
|
220
|
+
// load the audio file, report failures rather than leaving an
|
|
221
|
+
// unhandled rejection, the sound just stays unloaded and silent
|
|
222
|
+
const filename = asset;
|
|
223
|
+
this.loadSound(filename).catch(e=>
|
|
224
|
+
LOG('Sound load failed for', filename, '-', e.message));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Sample data for each channel
|
|
229
|
+
* Sounds keep their samples in an audio buffer, so reading this rebuilds
|
|
230
|
+
* the arrays from it and caches them. The copies are safe to hold onto,
|
|
231
|
+
* playing a sound detaches the buffer's own channel arrays.
|
|
232
|
+
* @type {Array<Array<number>|Float32Array>} */
|
|
233
|
+
get sampleChannels()
|
|
234
|
+
{
|
|
235
|
+
const buffer = this.sampleBuffer;
|
|
236
|
+
if (!this._sampleChannels && buffer)
|
|
237
|
+
{
|
|
238
|
+
const channels = [];
|
|
239
|
+
for (let i = 0; i < buffer.numberOfChannels; i++)
|
|
240
|
+
channels.push(buffer.getChannelData(i).slice());
|
|
241
|
+
this._sampleChannels = channels;
|
|
242
|
+
}
|
|
243
|
+
return this._sampleChannels;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** @param {Array<Array<number>|Float32Array>} sampleChannels */
|
|
247
|
+
set sampleChannels(sampleChannels)
|
|
248
|
+
{
|
|
249
|
+
// new samples invalidate the buffer built from the old ones
|
|
250
|
+
this._sampleChannels = sampleChannels;
|
|
251
|
+
this.sampleBuffer = undefined;
|
|
252
|
+
this.sampleLength = sampleChannels?.[0]?.length || 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Move this sound's samples into an audio buffer that every play can share
|
|
256
|
+
* Does nothing if there is already a buffer or no samples to build one from */
|
|
257
|
+
buildSampleBuffer()
|
|
258
|
+
{
|
|
259
|
+
if (this.sampleBuffer || !this._sampleChannels || headlessMode) return;
|
|
260
|
+
|
|
261
|
+
this.sampleBuffer = createAudioBuffer(this._sampleChannels, this.sampleRate);
|
|
262
|
+
|
|
263
|
+
// the buffer owns the samples now, release the arrays we built it from
|
|
264
|
+
this._sampleChannels = undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Play the sound
|
|
268
|
+
* Sounds may not play until a user interaction occurs
|
|
269
|
+
* @param {Vector2} [pos] - World space position to play the sound if any
|
|
270
|
+
* @param {number} [volume] - How much to scale volume by
|
|
271
|
+
* @param {number} [pitch] - How much to scale pitch by
|
|
272
|
+
* @param {number} [randomnessScale] - How much to scale pitch randomness
|
|
273
|
+
* @param {boolean} [loop] - Should the sound loop?
|
|
274
|
+
* @param {boolean} [paused] - Should the sound start paused
|
|
275
|
+
* @return {SoundInstance} - The sound instance, or undefined if sound is disabled, not loaded, or running in headless mode
|
|
276
|
+
*/
|
|
277
|
+
play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
|
|
278
|
+
{
|
|
279
|
+
ASSERT(!pos || isVector2(pos), 'pos must be a vec2');
|
|
280
|
+
ASSERT(isNumber(volume), 'volume must be a number');
|
|
281
|
+
ASSERT(isNumber(pitch), 'pitch must be a number');
|
|
282
|
+
ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
|
|
283
|
+
|
|
284
|
+
if (!soundEnable || headlessMode) return;
|
|
285
|
+
if (!this.sampleBuffer && !this._sampleChannels) return;
|
|
286
|
+
|
|
287
|
+
let pan;
|
|
288
|
+
if (pos)
|
|
289
|
+
{
|
|
290
|
+
const range = this.range;
|
|
291
|
+
if (range)
|
|
292
|
+
{
|
|
293
|
+
// apply range based fade
|
|
294
|
+
const lengthSquared = cameraPos.distanceSquared(pos);
|
|
295
|
+
if (lengthSquared > range*range)
|
|
296
|
+
return; // out of range
|
|
297
|
+
|
|
298
|
+
// attenuate volume by distance
|
|
299
|
+
volume *= percent(lengthSquared**.5, range, range*this.taper);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// get pan from screen space coords
|
|
303
|
+
pan = worldToScreen(pos).x * 2/mainCanvasSize.x - 1;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Create sound instance
|
|
307
|
+
const rate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
|
|
308
|
+
const instance = new SoundInstance(this, volume, rate, pan, loop, paused);
|
|
309
|
+
|
|
310
|
+
if (debug && debugSound && pos)
|
|
311
|
+
{
|
|
312
|
+
// visualize where positioned sounds play and their falloff range
|
|
313
|
+
debugCircle(pos, .5, '#0ff', .5, true);
|
|
314
|
+
if (this.range)
|
|
315
|
+
{
|
|
316
|
+
debugCircle(pos, 2*this.range, '#0ff', .5); // silent radius
|
|
317
|
+
debugCircle(pos, 2*this.range*this.taper, '#0ff', .5); // full volume radius
|
|
318
|
+
}
|
|
319
|
+
debugText('vol '+volume.toFixed(2)+' pitch '+rate.toFixed(2), pos, .5, '#0ff', .5);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return instance;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Play the sound on a loop, the same as play with loop on; stop or change it through the SoundInstance returned
|
|
326
|
+
* @param {Vector2} [pos] - World space position to play the sound if any
|
|
327
|
+
* @param {number} [volume] - How much to scale volume by
|
|
328
|
+
* @param {number} [pitch] - How much to scale pitch by
|
|
329
|
+
* @param {number} [randomnessScale] - How much to scale pitch randomness
|
|
330
|
+
* @param {boolean} [paused] - Should the sound start paused
|
|
331
|
+
* @return {SoundInstance} - The sound instance, or undefined if sound is disabled, not loaded, or running in headless mode */
|
|
332
|
+
playLoop(pos, volume=1, pitch=1, randomnessScale=1, paused=false)
|
|
333
|
+
{ return this.play(pos, volume, pitch, randomnessScale, true, paused); }
|
|
334
|
+
|
|
335
|
+
/** Play a music track that loops by default
|
|
336
|
+
* @param {number} [volume] - Volume to play the music at
|
|
337
|
+
* @param {boolean} [loop] - Should the music loop?
|
|
338
|
+
* @param {boolean} [paused] - Should the music start paused
|
|
339
|
+
* @return {SoundInstance} - The sound instance
|
|
340
|
+
*/
|
|
341
|
+
playMusic(volume=1, loop=true, paused=false)
|
|
342
|
+
{ return this.play(undefined, volume, 1, 0, loop, paused); }
|
|
343
|
+
|
|
344
|
+
/** Play the sound as a musical note with a semitone offset
|
|
345
|
+
* This can be used to play music with chromatic scales
|
|
346
|
+
* @param {number} [semitoneOffset] - How many semitones to offset pitch
|
|
347
|
+
* @param {Vector2} [pos] - World space position to play the sound if any
|
|
348
|
+
* @param {number} [volume=1] - How much to scale volume by
|
|
349
|
+
* @return {SoundInstance} - The sound instance
|
|
350
|
+
*/
|
|
351
|
+
playNote(semitoneOffset=0, pos, volume)
|
|
352
|
+
{
|
|
353
|
+
ASSERT(isNumber(semitoneOffset), 'semitoneOffset must be a number');
|
|
354
|
+
const pitch = getNoteFrequency(semitoneOffset, 1);
|
|
355
|
+
return this.play(pos, volume, pitch, 0);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Get how long this sound is in seconds
|
|
359
|
+
* @return {number} - How long the sound is in seconds (0 if loading)
|
|
360
|
+
*/
|
|
361
|
+
getDuration()
|
|
362
|
+
{ return this.sampleLength / this.sampleRate || 0; }
|
|
363
|
+
|
|
364
|
+
/** Check if sound is loaded, for sounds fetched from a url
|
|
365
|
+
* @return {boolean} - True if sound is loaded and ready to play
|
|
366
|
+
*/
|
|
367
|
+
isLoaded() { return this.loadedPercent === 1; }
|
|
368
|
+
|
|
369
|
+
/** Loads a sound from a URL and decodes it into sample data.
|
|
370
|
+
* @param {string} filename
|
|
371
|
+
* @return {Promise} */
|
|
372
|
+
async loadSound(filename)
|
|
373
|
+
{
|
|
374
|
+
const response = await fetch(filename);
|
|
375
|
+
if (!response.ok)
|
|
376
|
+
throw new Error(`Failed to load sound from ${filename}: ${response.status} ${response.statusText}`);
|
|
377
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
378
|
+
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
379
|
+
|
|
380
|
+
// keep the decoded buffer as is, it is exactly what playback needs and
|
|
381
|
+
// every play shares it, no channel data is read or copied
|
|
382
|
+
this.sampleRate = audioBuffer.sampleRate;
|
|
383
|
+
this.sampleLength = audioBuffer.length;
|
|
384
|
+
this.sampleBuffer = audioBuffer;
|
|
385
|
+
this.loadedPercent = 1;
|
|
386
|
+
this.onloadCallback?.(this);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Sound Instance - Wraps an AudioBufferSourceNode for individual sound control
|
|
394
|
+
* Represents a single playing instance of a sound with pause/resume capabilities
|
|
395
|
+
* @memberof Audio
|
|
396
|
+
* @example
|
|
397
|
+
* // Play a sound and get an instance for control
|
|
398
|
+
* const jumpSound = new Sound([.5,.5,220]);
|
|
399
|
+
* const instance = jumpSound.play();
|
|
400
|
+
*
|
|
401
|
+
* // Control the individual instance
|
|
402
|
+
* instance.setVolume(.5);
|
|
403
|
+
* instance.pause();
|
|
404
|
+
* instance.resume();
|
|
405
|
+
* instance.stop();
|
|
406
|
+
*/
|
|
407
|
+
class SoundInstance
|
|
408
|
+
{
|
|
409
|
+
/** Create a sound instance
|
|
410
|
+
* @param {Sound} sound - The sound object
|
|
411
|
+
* @param {number} [volume] - How much to scale volume by
|
|
412
|
+
* @param {number} [rate] - The playback rate to use
|
|
413
|
+
* @param {number} [pan] - How much to apply stereo panning
|
|
414
|
+
* @param {boolean} [loop] - Should the sound loop?
|
|
415
|
+
* @param {boolean} [paused] - Should the sound start paused? */
|
|
416
|
+
constructor(sound, volume=1, rate=1, pan=0, loop=false, paused=false)
|
|
417
|
+
{
|
|
418
|
+
ASSERT(sound instanceof Sound, 'SoundInstance requires a valid Sound object');
|
|
419
|
+
ASSERT(volume >= 0, 'Sound volume must be positive or zero');
|
|
420
|
+
ASSERT(rate >= 0, 'Sound rate must be positive or zero');
|
|
421
|
+
ASSERT(isNumber(pan), 'Sound pan must be a number');
|
|
422
|
+
|
|
423
|
+
/** @property {Sound} - The sound object */
|
|
424
|
+
this.sound = sound;
|
|
425
|
+
/** @property {number} - How much to scale volume by */
|
|
426
|
+
this.volume = volume;
|
|
427
|
+
/** @property {number} - The playback rate to use */
|
|
428
|
+
this.rate = rate;
|
|
429
|
+
/** @property {number} - How much to apply stereo panning */
|
|
430
|
+
this.pan = pan;
|
|
431
|
+
/** @property {boolean} - Should the sound loop */
|
|
432
|
+
this.loop = loop;
|
|
433
|
+
/** @property {number} - Timestamp for audio context when paused */
|
|
434
|
+
this.pausedTime = 0;
|
|
435
|
+
/** @property {number} - Timestamp for audio context when started */
|
|
436
|
+
this.startTime = undefined;
|
|
437
|
+
/** @property {GainNode} - Gain node for the sound */
|
|
438
|
+
this.gainNode = undefined;
|
|
439
|
+
/** @property {AudioBufferSourceNode} - Source node of the audio */
|
|
440
|
+
this.source = undefined;
|
|
441
|
+
/** @property {AudioNode|AudioEffectNodes} - Node or effect to route this instance through, copied from the sound
|
|
442
|
+
* @type {AudioNode|AudioEffectNodes} */
|
|
443
|
+
this.output = sound.output;
|
|
444
|
+
// setup end callback and start sound, a sound that ends is stopped, its time back at 0
|
|
445
|
+
this.onendedCallback = (source)=>
|
|
446
|
+
{
|
|
447
|
+
if (source === this.source)
|
|
448
|
+
{
|
|
449
|
+
this.source = undefined;
|
|
450
|
+
this.startTime = undefined;
|
|
451
|
+
this.pausedTime = 0;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
if (!paused)
|
|
455
|
+
this.start();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Start playing the sound instance from the offset time
|
|
459
|
+
* @param {number} [offset] - Offset in seconds to start playback from
|
|
460
|
+
*/
|
|
461
|
+
start(offset=0)
|
|
462
|
+
{
|
|
463
|
+
ASSERT(offset >= 0, 'Sound start offset must be positive or zero');
|
|
464
|
+
if (this.isPlaying())
|
|
465
|
+
this.stop();
|
|
466
|
+
this.gainNode = audioContext.createGain();
|
|
467
|
+
|
|
468
|
+
// build the shared buffer if it was not made at load time, then play it
|
|
469
|
+
this.sound.buildSampleBuffer();
|
|
470
|
+
this.source = this.sound.sampleBuffer ?
|
|
471
|
+
playAudioBuffer(this.sound.sampleBuffer, this.volume, this.rate, this.pan, this.loop, this.gainNode, offset, this.onendedCallback, this.output) :
|
|
472
|
+
playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback, this.output);
|
|
473
|
+
if (this.source)
|
|
474
|
+
{
|
|
475
|
+
this.startTime = audioContext.currentTime - offset;
|
|
476
|
+
this.pausedTime = undefined;
|
|
477
|
+
}
|
|
478
|
+
else
|
|
479
|
+
{
|
|
480
|
+
// the sound could not start, keep the place so a later resume picks it up
|
|
481
|
+
this.startTime = undefined;
|
|
482
|
+
this.pausedTime = offset;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Set the volume of this sound instance, with an optional fade to it
|
|
487
|
+
* - A fade ducks music under dialogue or cross fades two tracks without a click
|
|
488
|
+
* @param {number} volume
|
|
489
|
+
* @param {number} [fadeTime] - Seconds to fade to the new volume over */
|
|
490
|
+
setVolume(volume, fadeTime=0)
|
|
491
|
+
{
|
|
492
|
+
ASSERT(volume >= 0, 'Sound volume must be positive or zero');
|
|
493
|
+
ASSERT(fadeTime >= 0, 'Sound fade time must be positive or zero');
|
|
494
|
+
this.volume = volume;
|
|
495
|
+
if (!this.gainNode) return;
|
|
496
|
+
|
|
497
|
+
// drop any fade still scheduled so stacked calls don't fight,
|
|
498
|
+
// then ramp from wherever the gain is now or jump straight there
|
|
499
|
+
const gain = this.gainNode.gain;
|
|
500
|
+
const startFade = audioContext.currentTime;
|
|
501
|
+
gain.cancelScheduledValues(startFade);
|
|
502
|
+
if (fadeTime)
|
|
503
|
+
{
|
|
504
|
+
gain.setValueAtTime(gain.value, startFade);
|
|
505
|
+
gain.linearRampToValueAtTime(volume, startFade + fadeTime);
|
|
506
|
+
}
|
|
507
|
+
else
|
|
508
|
+
gain.value = volume;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Set the playback rate of this sound instance, its speed and pitch, while it plays
|
|
512
|
+
* - A looping sound can follow something smoothly this way, like an engine with the speed
|
|
513
|
+
* - A rate of 0 freezes the sound in place, its current time is not tracked until it moves again
|
|
514
|
+
* @param {number} rate - 1 is normal, 2 is twice as fast and an octave up */
|
|
515
|
+
setRate(rate)
|
|
516
|
+
{
|
|
517
|
+
ASSERT(rate >= 0, 'Sound rate must be positive or zero');
|
|
518
|
+
// keep the place in the sound, only the speed changes from here, so the current time stays true
|
|
519
|
+
if (this.isPlaying() && rate)
|
|
520
|
+
this.startTime = audioContext.currentTime - this.getCurrentTime() * this.rate / rate;
|
|
521
|
+
this.rate = rate;
|
|
522
|
+
if (this.source)
|
|
523
|
+
this.source.playbackRate.value = rate;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Stop this sound instance and reset position to the start
|
|
527
|
+
* @param {number} [fadeTime] - Seconds to fade out over before stopping */
|
|
528
|
+
stop(fadeTime=0)
|
|
529
|
+
{
|
|
530
|
+
ASSERT(fadeTime >= 0, 'Sound fade time must be positive or zero');
|
|
531
|
+
if (this.isPlaying())
|
|
532
|
+
{
|
|
533
|
+
if (fadeTime)
|
|
534
|
+
{
|
|
535
|
+
// ramp off gain from where it is now (not 1, or low-volume
|
|
536
|
+
// instances would jump back up before fading, and a volume
|
|
537
|
+
// fade in flight carries on down from its current point);
|
|
538
|
+
// cancel any prior scheduling so stacked stop calls don't
|
|
539
|
+
// re-anchor partway through a previous fade
|
|
540
|
+
const gain = this.gainNode.gain;
|
|
541
|
+
const startFade = audioContext.currentTime;
|
|
542
|
+
const endFade = startFade + fadeTime;
|
|
543
|
+
gain.cancelScheduledValues(startFade);
|
|
544
|
+
gain.setValueAtTime(gain.value, startFade);
|
|
545
|
+
gain.linearRampToValueAtTime(0, endFade);
|
|
546
|
+
this.source.stop(endFade);
|
|
547
|
+
}
|
|
548
|
+
else
|
|
549
|
+
this.source.stop();
|
|
550
|
+
}
|
|
551
|
+
this.pausedTime = 0;
|
|
552
|
+
this.source = undefined;
|
|
553
|
+
this.startTime = undefined;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Pause this sound instance */
|
|
557
|
+
pause()
|
|
558
|
+
{
|
|
559
|
+
if (this.isPaused()) return;
|
|
560
|
+
|
|
561
|
+
// save current time and stop sound
|
|
562
|
+
this.pausedTime = this.getCurrentTime();
|
|
563
|
+
this.source.stop();
|
|
564
|
+
this.source = undefined;
|
|
565
|
+
this.startTime = undefined;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Resume this sound instance */
|
|
569
|
+
resume()
|
|
570
|
+
{
|
|
571
|
+
if (!this.isPaused()) return;
|
|
572
|
+
|
|
573
|
+
// restart sound from paused time
|
|
574
|
+
this.start(this.pausedTime);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Check if this instance is currently playing
|
|
578
|
+
* @return {boolean} - True if playing
|
|
579
|
+
*/
|
|
580
|
+
isPlaying() { return !!this.source; }
|
|
581
|
+
|
|
582
|
+
/** Check if this instance is paused or stopped (not currently playing)
|
|
583
|
+
* @return {boolean} - True if not playing
|
|
584
|
+
*/
|
|
585
|
+
isPaused() { return !this.isPlaying(); }
|
|
586
|
+
|
|
587
|
+
/** Get the current playback time in seconds
|
|
588
|
+
* @return {number} - Current playback time
|
|
589
|
+
*/
|
|
590
|
+
getCurrentTime()
|
|
591
|
+
{
|
|
592
|
+
if (!this.isPlaying()) return this.pausedTime;
|
|
593
|
+
const duration = this.getDuration();
|
|
594
|
+
// guard mod against 0 duration (rate=0 or sound not loaded)
|
|
595
|
+
return duration ? mod(audioContext.currentTime - this.startTime, duration) : 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** Get the total duration of this sound
|
|
599
|
+
* @return {number} - Total duration in seconds (0 if loading)
|
|
600
|
+
*/
|
|
601
|
+
getDuration() { return this.rate ? this.sound.getDuration() / this.rate : 0; }
|
|
602
|
+
|
|
603
|
+
/** Get source of this sound instance
|
|
604
|
+
* @return {AudioBufferSourceNode}
|
|
605
|
+
*/
|
|
606
|
+
getSource() { return this.source; }
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
610
|
+
|
|
611
|
+
/** Speak text with passed in settings
|
|
612
|
+
* @param {string} text - The text to speak
|
|
613
|
+
* @param {number} [volume] - How much to scale volume by
|
|
614
|
+
* @param {number} [rate] - How quickly to speak
|
|
615
|
+
* @param {number} [pitch] - How much to change the pitch by
|
|
616
|
+
* @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
|
|
617
|
+
* @return {SpeechSynthesisUtterance|undefined} - The utterance that was spoken, or undefined if speech is unavailable
|
|
618
|
+
* @memberof Audio */
|
|
619
|
+
function speak(text, volume=1, rate=1, pitch=1, language='')
|
|
620
|
+
{
|
|
621
|
+
ASSERT(typeof volume !== 'string', 'speak() signature changed: language is now the last parameter, after pitch');
|
|
622
|
+
if (!soundEnable || headlessMode) return;
|
|
623
|
+
if (typeof speechSynthesis === 'undefined') return;
|
|
624
|
+
|
|
625
|
+
// common languages (not supported by all browsers)
|
|
626
|
+
// en - english, it - italian, fr - french, de - german, es - spanish
|
|
627
|
+
// ja - japanese, ru - russian, zh - chinese, hi - hindi, ko - korean
|
|
628
|
+
|
|
629
|
+
// build utterance and speak
|
|
630
|
+
const utterance = new SpeechSynthesisUtterance(text);
|
|
631
|
+
utterance.lang = language;
|
|
632
|
+
utterance.volume = clamp(volume*soundVolume);
|
|
633
|
+
utterance.rate = rate;
|
|
634
|
+
utterance.pitch = pitch;
|
|
635
|
+
speechSynthesis.speak(utterance);
|
|
636
|
+
return utterance;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** Stop all queued speech
|
|
640
|
+
* @memberof Audio */
|
|
641
|
+
function speakStop()
|
|
642
|
+
{
|
|
643
|
+
if (typeof speechSynthesis !== 'undefined')
|
|
644
|
+
speechSynthesis.cancel();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** Get frequency of a note on a musical scale
|
|
648
|
+
* @param {number} semitoneOffset - How many semitones away from the root note
|
|
649
|
+
* @param {number} [rootFrequency] - Frequency at semitone offset 0
|
|
650
|
+
* @return {number} - The frequency of the note
|
|
651
|
+
* @memberof Audio */
|
|
652
|
+
function getNoteFrequency(semitoneOffset, rootFrequency=220)
|
|
653
|
+
{ return rootFrequency * 2**(semitoneOffset/12); }
|
|
654
|
+
|
|
655
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* @callback AudioEndedCallback - Function called when a sound ends
|
|
659
|
+
* @param {AudioBufferSourceNode} source
|
|
660
|
+
* @memberof Audio
|
|
661
|
+
*/
|
|
662
|
+
|
|
663
|
+
/** Play cached audio samples with given settings
|
|
664
|
+
* @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
|
|
665
|
+
* @param {number} [volume] - How much to scale volume by
|
|
666
|
+
* @param {number} [rate] - The playback rate to use
|
|
667
|
+
* @param {number} [pan] - How much to apply stereo panning
|
|
668
|
+
* @param {boolean} [loop] - True if the sound should loop when it reaches the end
|
|
669
|
+
* @param {number} [sampleRate=44100] - Sample rate for the sound
|
|
670
|
+
* @param {GainNode} [gainNode] - Optional gain node for volume control while playing (disconnected when the sound ends)
|
|
671
|
+
* @param {number} [offset] - Offset in seconds to start playback from
|
|
672
|
+
* @param {AudioEndedCallback} [onended] - Callback for when the sound ends
|
|
673
|
+
* @param {AudioNode|AudioEffectNodes} [output] - Node or effect to connect the gain to instead of the master gain
|
|
674
|
+
* @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
|
|
675
|
+
* @memberof Audio */
|
|
676
|
+
function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended, output)
|
|
677
|
+
{
|
|
678
|
+
if (!soundEnable || headlessMode) return;
|
|
679
|
+
|
|
680
|
+
if (!audioIsRunning())
|
|
681
|
+
{
|
|
682
|
+
// fix stalled audio, don't build a buffer that can't be played
|
|
683
|
+
audioContext.resume();
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const buffer = createAudioBuffer(sampleChannels, sampleRate);
|
|
688
|
+
return playAudioBuffer(buffer, volume, rate, pan, loop, gainNode, offset, onended, output);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** Copy arrays of samples into a new audio buffer
|
|
692
|
+
* @param {Array} sampleChannels - Array of arrays of samples (for stereo playback)
|
|
693
|
+
* @param {number} [sampleRate=44100] - Sample rate for the sound
|
|
694
|
+
* @return {AudioBuffer} - The audio buffer holding the samples
|
|
695
|
+
* @memberof Audio */
|
|
696
|
+
function createAudioBuffer(sampleChannels, sampleRate=audioDefaultSampleRate)
|
|
697
|
+
{
|
|
698
|
+
const channelCount = sampleChannels.length;
|
|
699
|
+
const sampleLength = sampleChannels[0].length;
|
|
700
|
+
const buffer = audioContext.createBuffer(channelCount, sampleLength, sampleRate);
|
|
701
|
+
sampleChannels.forEach((c,i)=> buffer.getChannelData(i).set(c));
|
|
702
|
+
return buffer;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** Play an audio buffer with given settings
|
|
706
|
+
* The buffer can be shared by any number of sounds playing at once
|
|
707
|
+
* @param {AudioBuffer} buffer - The audio buffer to play
|
|
708
|
+
* @param {number} [volume] - How much to scale volume by
|
|
709
|
+
* @param {number} [rate] - The playback rate to use
|
|
710
|
+
* @param {number} [pan] - How much to apply stereo panning
|
|
711
|
+
* @param {boolean} [loop] - True if the sound should loop when it reaches the end
|
|
712
|
+
* @param {GainNode} [gainNode] - Optional gain node for volume control while playing (disconnected when the sound ends)
|
|
713
|
+
* @param {number} [offset] - Offset in seconds to start playback from
|
|
714
|
+
* @param {AudioEndedCallback} [onended] - Callback for when the sound ends
|
|
715
|
+
* @param {AudioNode|AudioEffectNodes} [output] - Node or effect to connect the gain to instead of the master gain
|
|
716
|
+
* @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
|
|
717
|
+
* @memberof Audio */
|
|
718
|
+
function playAudioBuffer(buffer, volume=1, rate=1, pan=0, loop=false, gainNode, offset=0, onended, output)
|
|
719
|
+
{
|
|
720
|
+
if (!soundEnable || headlessMode) return;
|
|
721
|
+
|
|
722
|
+
if (!audioIsRunning())
|
|
723
|
+
{
|
|
724
|
+
// fix stalled audio, this sound won't be able to play
|
|
725
|
+
audioContext.resume();
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// setup source, many sources can share one buffer
|
|
730
|
+
const source = audioContext.createBufferSource();
|
|
731
|
+
source.buffer = buffer;
|
|
732
|
+
source.playbackRate.value = rate;
|
|
733
|
+
source.loop = loop;
|
|
734
|
+
|
|
735
|
+
// create and connect gain node
|
|
736
|
+
gainNode = gainNode || audioContext.createGain();
|
|
737
|
+
gainNode.gain.value = volume;
|
|
738
|
+
const outputNode = audioEffectNode(output, 'input') || audioMasterGain;
|
|
739
|
+
ASSERT(typeof outputNode.connect === 'function', 'output must be an AudioNode or an effect with input and output nodes');
|
|
740
|
+
gainNode.connect(outputNode);
|
|
741
|
+
|
|
742
|
+
// connect source to stereo panner and gain
|
|
743
|
+
const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
|
|
744
|
+
source.connect(pannerNode).connect(gainNode);
|
|
745
|
+
|
|
746
|
+
// disconnect nodes when the sound ends so the audio graph doesn't grow
|
|
747
|
+
// unbounded across many play() calls (source.stop() also fires 'ended')
|
|
748
|
+
source.addEventListener('ended', ()=>
|
|
749
|
+
{
|
|
750
|
+
gainNode.disconnect();
|
|
751
|
+
pannerNode.disconnect();
|
|
752
|
+
if (onended) onended(source);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// play and return sound
|
|
756
|
+
const startOffset = offset * rate;
|
|
757
|
+
source.start(0, startOffset);
|
|
758
|
+
|
|
759
|
+
if (debug && debugSound)
|
|
760
|
+
LOG('sound', 'vol', volume.toFixed(2), 'rate', rate.toFixed(2), 'pan', pan.toFixed(2), loop ? 'loop' : '');
|
|
761
|
+
|
|
762
|
+
return source;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
766
|
+
// ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.2 by Frank Force
|
|
767
|
+
|
|
768
|
+
/** Generate and play a ZzFX sound
|
|
769
|
+
*
|
|
770
|
+
* <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
|
|
771
|
+
* @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
|
|
772
|
+
* @return {AudioBufferSourceNode} - The audio node of the sound played
|
|
773
|
+
* @memberof Audio */
|
|
774
|
+
function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
|
|
775
|
+
|
|
776
|
+
/** Generate samples for a ZzFX sound
|
|
777
|
+
* @param {number} [volume] - Volume scale (percent)
|
|
778
|
+
* @param {number} [randomness] - How much to randomize frequency (percent Hz)
|
|
779
|
+
* @param {number} [frequency] - Frequency of sound (Hz)
|
|
780
|
+
* @param {number} [attack] - Attack time, how fast sound starts (seconds)
|
|
781
|
+
* @param {number} [sustain] - Sustain time, how long sound holds (seconds)
|
|
782
|
+
* @param {number} [release] - Release time, how fast sound fades out (seconds)
|
|
783
|
+
* @param {number} [shape] - Shape of the sound wave
|
|
784
|
+
* @param {number} [shapeCurve] - Squareness of wave (0=square, 1=normal, 2=pointy)
|
|
785
|
+
* @param {number} [slide] - How much to slide frequency (kHz/s)
|
|
786
|
+
* @param {number} [deltaSlide] - How much to change slide (kHz/s/s)
|
|
787
|
+
* @param {number} [pitchJump] - Frequency of pitch jump (Hz)
|
|
788
|
+
* @param {number} [pitchJumpTime] - Time of pitch jump (seconds)
|
|
789
|
+
* @param {number} [repeatTime] - Resets some parameters periodically (seconds)
|
|
790
|
+
* @param {number} [noise] - How much random noise to add (percent)
|
|
791
|
+
* @param {number} [modulation] - Frequency of modulation wave, negative flips phase (Hz)
|
|
792
|
+
* @param {number} [bitCrush] - Resamples at a lower frequency in (samples*100)
|
|
793
|
+
* @param {number} [delay] - Overlap sound with itself for reverb and flanger effects (seconds)
|
|
794
|
+
* @param {number} [sustainVolume] - Volume level for sustain (percent)
|
|
795
|
+
* @param {number} [decay] - Decay time, how long to reach sustain after attack (seconds)
|
|
796
|
+
* @param {number} [tremolo] - Trembling effect, rate controlled by repeat time (percent)
|
|
797
|
+
* @param {number} [filter] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
|
|
798
|
+
* @return {Array} - Array of audio samples
|
|
799
|
+
* @memberof Audio */
|
|
800
|
+
function zzfxG
|
|
801
|
+
(
|
|
802
|
+
volume = 1,
|
|
803
|
+
randomness = .05,
|
|
804
|
+
frequency = 220,
|
|
805
|
+
attack = 0,
|
|
806
|
+
sustain = 0,
|
|
807
|
+
release = .1,
|
|
808
|
+
shape = 0,
|
|
809
|
+
shapeCurve = 1,
|
|
810
|
+
slide = 0,
|
|
811
|
+
deltaSlide = 0,
|
|
812
|
+
pitchJump = 0,
|
|
813
|
+
pitchJumpTime = 0,
|
|
814
|
+
repeatTime = 0,
|
|
815
|
+
noise = 0,
|
|
816
|
+
modulation = 0,
|
|
817
|
+
bitCrush = 0,
|
|
818
|
+
delay = 0,
|
|
819
|
+
sustainVolume = 1,
|
|
820
|
+
decay = 0,
|
|
821
|
+
tremolo = 0,
|
|
822
|
+
filter = 0
|
|
823
|
+
)
|
|
824
|
+
{
|
|
825
|
+
// init parameters
|
|
826
|
+
let sampleRate = audioDefaultSampleRate,
|
|
827
|
+
PI2 = PI*2,
|
|
828
|
+
startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
|
|
829
|
+
startFrequency = frequency *=
|
|
830
|
+
(1 + rand(randomness,-randomness)) * PI2 / sampleRate,
|
|
831
|
+
modOffset = 0, // modulation offset
|
|
832
|
+
repeat = 0, // repeat offset
|
|
833
|
+
crush = 0, // bit crush offset
|
|
834
|
+
jump = 1, // pitch jump timer
|
|
835
|
+
length, // sample length
|
|
836
|
+
b = [], // sample buffer
|
|
837
|
+
t = 0, // sample time
|
|
838
|
+
i = 0, // sample index
|
|
839
|
+
s = 0, // sample value
|
|
840
|
+
f, // wave frequency
|
|
841
|
+
|
|
842
|
+
// biquad LP/HP filter
|
|
843
|
+
quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
|
|
844
|
+
cosw = cos(w), alpha = sin(w) / 2 / quality,
|
|
845
|
+
a0 = 1 + alpha, a1 = -2*cosw / a0, a2 = (1 - alpha) / a0,
|
|
846
|
+
b0 = (1 + sign(filter) * cosw) / 2 / a0,
|
|
847
|
+
b1 = -(sign(filter) + cosw) / a0, b2 = b0,
|
|
848
|
+
x2 = 0, x1 = 0, y2 = 0, y1 = 0;
|
|
849
|
+
|
|
850
|
+
// scale by sample rate
|
|
851
|
+
const minAttack = 9; // prevent pop if attack is 0
|
|
852
|
+
attack = attack * sampleRate || minAttack;
|
|
853
|
+
decay *= sampleRate;
|
|
854
|
+
sustain *= sampleRate;
|
|
855
|
+
release *= sampleRate;
|
|
856
|
+
delay *= sampleRate;
|
|
857
|
+
deltaSlide *= 500 * PI2 / sampleRate**3;
|
|
858
|
+
modulation *= PI2 / sampleRate;
|
|
859
|
+
pitchJump *= PI2 / sampleRate;
|
|
860
|
+
pitchJumpTime *= sampleRate;
|
|
861
|
+
repeatTime = repeatTime * sampleRate | 0;
|
|
862
|
+
|
|
863
|
+
// generate waveform
|
|
864
|
+
for (length = attack + decay + sustain + release + delay | 0;
|
|
865
|
+
i < length; b[i++] = s * volume) // sample
|
|
866
|
+
{
|
|
867
|
+
if (!(++crush%(bitCrush*100|0))) // bit crush
|
|
868
|
+
{
|
|
869
|
+
s = shape? shape>1? shape>2? shape>3? shape>4? // wave shape
|
|
870
|
+
(t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
|
|
871
|
+
sin(t**3) : // 4 noise
|
|
872
|
+
max(min(tan(t),1),-1): // 3 tan
|
|
873
|
+
1-(2*t/PI2%2+2)%2: // 2 saw
|
|
874
|
+
1-4*abs(round(t/PI2)-t/PI2): // 1 triangle
|
|
875
|
+
sin(t); // 0 sin
|
|
876
|
+
|
|
877
|
+
s = (repeatTime ?
|
|
878
|
+
1 - tremolo + tremolo*sin(PI2*i/repeatTime) // tremolo
|
|
879
|
+
: 1) *
|
|
880
|
+
(shape>4?s:sign(s)*abs(s)**shapeCurve) * // shape curve
|
|
881
|
+
(i < attack ? i/attack : // attack
|
|
882
|
+
i < attack + decay ? // decay
|
|
883
|
+
1-((i-attack)/decay)*(1-sustainVolume) : // decay falloff
|
|
884
|
+
i < attack + decay + sustain ? // sustain
|
|
885
|
+
sustainVolume : // sustain volume
|
|
886
|
+
i < length - delay ? // release
|
|
887
|
+
(length - i - delay)/release * // release falloff
|
|
888
|
+
sustainVolume : // release volume
|
|
889
|
+
0); // post release
|
|
890
|
+
|
|
891
|
+
s = delay ? s/2 + (delay > i ? 0 : // delay
|
|
892
|
+
(i<length-delay? 1 : (length-i)/delay) * // release delay
|
|
893
|
+
b[i-delay|0]/2/volume) : s; // sample delay
|
|
894
|
+
|
|
895
|
+
if (filter) // apply filter
|
|
896
|
+
s = y1 = b2*x2 + b1*(x2=x1) + b0*(x1=s) - a2*y2 - a1*(y2=y1);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
f = (frequency += slide += deltaSlide) *// frequency
|
|
900
|
+
cos(modulation*modOffset++); // modulation
|
|
901
|
+
t += f + f*noise*sin(i**5); // noise
|
|
902
|
+
|
|
903
|
+
if (jump && ++jump > pitchJumpTime) // pitch jump
|
|
904
|
+
{
|
|
905
|
+
frequency += pitchJump; // apply pitch jump
|
|
906
|
+
startFrequency += pitchJump; // also apply to start
|
|
907
|
+
jump = 0; // stop pitch jump time
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (repeatTime && !(++repeat % repeatTime)) // repeat
|
|
911
|
+
{
|
|
912
|
+
frequency = startFrequency; // reset frequency
|
|
913
|
+
slide = startSlide; // reset slide
|
|
914
|
+
jump ||= 1; // reset pitch jump time
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return b; // return sample buffer
|
|
776
919
|
}
|