-
Notifications
You must be signed in to change notification settings - Fork 2
/
scriptprocessor_player.js
1680 lines (1459 loc) · 59.8 KB
/
scriptprocessor_player.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
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
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* Generic ScriptProcessor based WebAudio player.
*
* This infrastructure consists of two parts:
*
* <p>SamplePlayer: The generic player which must be parameterized with a specific AudioBackendAdapterBase
* subclass (which is not contained in this lib)
*
* <p>AudioBackendAdapterBase: an abstract base class for specific backend (i.e. 'sample data producer') integration.
*
* version 1.1.2 (with WASM support, cached filename translation & track switch bugfix, "internal filename"
* mapping, getVolume, setPanning, AudioContext get/resume, AbstractTicker revisited, bugfix for
* duplicate events, improved "play after user gesture" support + doubled sample buffer size),
* support for use of "alias" names for same file (see modland), added EmsHEAPF32BackendAdapter,
* added silence detection, extended copyTickerData signature, added JCH's "choppy ticker" fix,
* added setSilenceTimeout()
*
* Copyright (C) 2019 Juergen Wothke
*
* Terms of Use: This software is licensed under a CC BY-NC-SA
* (http://creativecommons.org/licenses/by-nc-sa/4.0/).
*/
var fetchSamples= function (e) {
// it seems that it is necessary to keep this explicit reference to the event-handler
// in order to pervent the dumbshit Chrome GC from detroying it eventually
var f= window.player['genSamples'].bind(window.player); // need to re-bind the instance.. after all this
// joke language has no real concept of OO
f(e);
};
var calcTick= function (e) {
var f= window.player['tick'].bind(window.player);
f(e);
};
var setGlobalWebAudioCtx= function() {
if (typeof window._gPlayerAudioCtx == 'undefined') { // cannot be instantiated 2x (so make it global)
var errText= 'Web Audio API is not supported in this browser';
try {
if('AudioContext' in window) {
window._gPlayerAudioCtx = new AudioContext();
} else if('webkitAudioContext' in window) {
window._gPlayerAudioCtx = new webkitAudioContext(); // legacy stuff
} else {
alert(errText + e);
}
} catch(e) {
alert(errText + e);
}
}
try {
if (window._gPlayerAudioCtx.state === 'suspended' && 'ontouchstart' in window) { //iOS shit
window._gPlayerAudioCtx.resume();
}
} catch(ignore) {}
}
/*
Poor man's JavaScript inheritance: 'extend' must be used to subclass AudioBackendAdapterBase to create backend specific adapters.
usage:
SomeBackendAdapter = (function(){ var $this = function () { $this.base.call(this, channels, bytesPerSample);};
extend(AudioBackendAdapterBase, $this, {
getAudioBuffer: function() {
...
},
getAudioBufferLength: function() {
...
},
...
}); return $this; })();
*/
function surrogateCtor() {}
function extend(base, sub, methods) {
surrogateCtor.prototype = base.prototype;
sub.prototype = new surrogateCtor();
sub.prototype.constructor = sub;
sub.base = base;
for (var name in methods) {
sub.prototype[name] = methods[name];
}
return sub;
}
/*
* Subclass this class in order to sync/associate stuff with the audio playback.
*
* The basic problem: WebAudio will request additional audio data whenever *it feels like* requesting it. The provider of that data has
* no way of knowing when exactly the delivered data will actually be used. WebAudio usually requests it *before* its current supply runs
* out. Supposing WebAudio requests chunks of 8192 samples at a time (which is the default used here). Depending on the user's screen refresh
* rate (e.g. 50Hz) and the browser's playback rate (e.g. 44100Hz) a different number of samples will correspond to one typical animation frame,
* i.e. screen redraw (e.g. 882 samples). The sample "supply" delivered in one batch may then last for roughly 1/5 of a second (obviously much less
* when higher playback speeds are used).
* The size of the sample data batches delivered by the underlying emulator may then not directly match the chunks requested by WebAudio, i.e.
* there may be more or also less data than what is needed for one WebAudio request. And as a further complication the sample rate used by the
* backend may differ from the one used by WebAudio, i.e. the raw data relivered by the emulator backend may be subject to a resampling.
* With regards to the actual audio playback this isn't a problem. But the problems start if there is additional data accociated with the
* audio data (maybe some raw data that was used to create the respective audio data) and the GUI needs to handle that add-on data *IN SYNC*
* with the actual playback, e.g. visualize the audio that is played back.
*
* It is the purpose of this AbstractTicker API to deal with that problem and provide the GUI with some API that allows to access
* add-on data in-sync with the playback.
*
* If a respective subclass is specified upon instanciation of the ScriptNodePlayer, then the player will track
* playback progress as 'ticks' (one 'tick' typically measuring 256 audio samples). "Ticks" are measured within the
* context of the current playback buffer and whenever a new buffer is played the counting restarts from 0.
*
* During playback (e.g. from some "animation frame" handler) the current playback position can be queried using
* ScriptNodePlayer.getInstance().getCurrentTick().
*
* The idea is for the AbstractTicker to provide additional "tick resolution" data that can be queried using the
* "current tick". During playback the original audio buffers are fed to the AbstractTicker before they are played
* (see 'calcTickData'). This allows the AbstractTicker to build/update its "tick resolution" data.
*/
AbstractTicker = function() {}
AbstractTicker.prototype = {
/*
* Constructor that allows the AbstractTicker to setup its own data structures (the
* number of 'tick' events associated with each sample buffer is: samplesPerBuffer/tickerStepWidth).
* @samplesPerBuffer number of audio samples in the original playback buffers - that the AbstractTicker can use to
* derive its additional data streams from
* @tickerStepWidth number of audio samples that are played between "tick events"
*/
init: function(samplesPerBuffer, tickerStepWidth) {},
/*
* Gets called at the start of each audio buffer generation.
*/
start: function() {},
/*
* Gets called each time the computeAudioSamples() has been invoked.
* @deprecated Legacy API used in early VU meter experiments
*/
computeAudioSamplesNotify: function() {},
/*
* Hook allows to resample the add-on data in-sync with the underlying audio data.
*/
resampleData: function(sampleRate, inputSampleRate, origLen, backendAdapter) {},
/*
* Copies data from the resampled input buffers to the "WebAudio audio buffer" sized output.
*/
copyTickerData: function(outBufferIdx, inBufferIdx, backendAdapter) {},
/*
* Invoked after audio buffer content has been generated.
* @deprecated Legacy API used in early VU meter experiments
*/
calcTickData: function(output1, output2) {}
};
var SAMPLES_PER_BUFFER = 16384; // allowed: buffer sizes: 256, 512, 1024, 2048, 4096, 8192, 16384
/*
* Abstract 'audio backend adapter'.
*
* Not for "end users"! Base infrastructure for the integration of new backends:
*
* Must be subclassed for the integration of a specific backend: It adapts the APIs provided by a
* specific backend to the ones required by the player (e.g. access to raw sample data.) It
* provides hooks that can be used to pass loaded files to the backend. The adapter also has
* built-in resampling logic so that exactly the sampleRate required by the player is provided).
*
* Most backends are pretty straight forward: A music file is input and the backend plays it. Things are
* more complicated if the backend code relies on additional files - maybe depending on the input -
* that must be loaded in order to play the music. The problem arises because in the traditional runtime
* environment files are handled synchronously: the code waits until the file is loaded and then uses it.
*
* "Unfortunately" there is no blocking file-load available to JavaScript on a web page. So unless some
* virtual filesystem is built-up beforehand (containing every file that the backend could possibly ever
* try to load) the backend code is stuck with an asynchronous file loading scheme, and the original
* backend code must be changed to a model that deals with browser's "file is not yet ready" response.
*
* The player offers a trial & error approach to deal with asynchronous file-loading. The backend code
* is expected (i.e. it must be adapted accordingly) to attempt a file-load call (which is handled by
* an async web request linked to some sort of result cache). If the requested data isn't cached yet,
* then the backend code is expected to fail but return a corresponding error status back to the
* player (i.e. the player then knows that the code failed because some file wasn't available yet - and
* as soon as the file-load is completed it retries the whole initialization sequence).
* (see "fileRequestCallback()" for more info)
*/
AudioBackendAdapterBase = function (channels, bytesPerSample) {
this._resampleBuffer= new Float32Array();
this._channels= channels;
this._bytesPerSample= bytesPerSample;
this._sampleRate= 44100;
this._inputSampleRate= 44100;
this._observer;
this._manualSetupComplete= true; // override if necessary
};
AudioBackendAdapterBase.prototype = {
// ************* core functions that must be defined by a subclass
/**
* Fills the audio buffer with the next batch of samples
* Return 0: OK, -1: temp issue - waiting for file, 1: end, 2: error
*/
computeAudioSamples: function() {this.error("computeAudioSamples");},
/**
* Load the song's binary data into the backend as a first step towards playback.
* The subclass can either use the 'data' directly or us the 'filename' to retrieve it indirectly
* (e.g. when regular file I/O APIs are used).
*/
loadMusicData: function(sampleRate, path, filename, data, options) {this.error("loadMusicData");},
/**
* Second step towards playback: Select specific sub-song from the loaded song file.
* Allows to select a specific sub-song and/or apply additional song setting..
*/
evalTrackOptions: function(options) {this.error("evalTrackOptions");},
/**
* Get info about currently selected music file and track. Respective info very much depends on
* the specific backend - use getSongInfoMeta() to check for available attributes.
*/
updateSongInfo: function(filename, result) {this.error("updateSongInfo");},
/**
* Advertises the song attributes that can be provided by this backend.
*/
getSongInfoMeta: function() {this.error("getSongInfoMeta");},
// ************* sample buffer and format related
/**
* Return: pointer to memory buffer that contains the sample data
*/
getAudioBuffer: function() {this.error("getAudioBuffer");},
/**
* Return: length of the audio buffer in 'ticks' (e.g. mono buffer with 1 8-bit
* sample= 1; stereo buffer with 1 32-bit * sample for each channel also= 1)
*/
getAudioBufferLength: function() {this.error("getAudioBufferLength");},
/**
* Reads one audio sample from the specified position.
* Return sample value in range: -1..1
*/
readFloatSample: function(buffer, idx) {this.error("readFloatSample");},
/**
* @param pan 0..2 (1 creates mono)
*/
applyPanning: function(buffer, len, pan) {this.error("applyPanning");},
/**
* Return size one sample in bytes
*/
getBytesPerSample: function() {
return this._bytesPerSample;
},
/**
* Number of channels, i.e. 1= mono, 2= stereo
*/
getChannels: function() {
return this._channels;
},
// ************* optional: setup related
/*
* Implement if subclass needs additional setup logic.
*/
isAdapterReady: function() {
return true;
},
/*
* Creates the URL used to retrieve the song file.
*/
mapInternalFilename: function(overridePath, defaultPath, uri) {
return ((overridePath)?overridePath:defaultPath) + uri; // this._basePath ever needed?
},
/*
* Allows to map the filenames used in the emulation to external URLs.
*/
mapUrl: function(filename) {
return filename;
},
/*
* Allows to perform some file input based manual setup sequence (e.g. setting some BIOS).
* return 0: step successful & init completed, -1: error, 1: step successful
*/
uploadFile: function(filename, options) {
return 0;
},
/*
* Check if this AudioBackendAdapterBase still needs manually performed
* setup steps (see uploadFile())
*/
isManualSetupComplete: function() {
return this._manualSetupComplete;
},
/**
* Cleanup backend before playing next music file
*/
teardown: function() {this.error("teardown");},
// ************* optional: song "position seek" functionality (only available in backend)
/**
* Return: default 0 = seeking not supported
*/
getMaxPlaybackPosition: function() { return 0;},
/**
* Return: default 0
*/
getPlaybackPosition: function() { return 0;},
/**
* Move playback to 'pos': must be between 0 and getMaxPlaybackPosition()
* Return: 0 if successful
*/
seekPlaybackPosition: function(pos) { return -1;},
// ************* optional: async file-loading related (only if needed)
/**
* Transform input filename into path/filename expected by the backend
* Return array with 2 elements: 0: basePath (backend specific - most don't need one),
* 1: filename (incl. the remainder of the path)
*/
getPathAndFilename: function(filename) {this.error("getPathAndFilename");},
/**
* Let backend store a loaded file in such a way that it can later deal with it.
* Return a filehandle meaningful to the used backend
*/
registerFileData: function(pathFilenameArray, data) {this.error("registerFileData");},
// if filename/path used by backend does not match the one used by the browser
mapBackendFilename: function(name) { return name;},
// introduced for backward-compatibility..
mapCacheFileName: function(name) { return name;},
/*
* Backend may "push" update of song attributes (like author, copyright, etc)
*/
handleBackendSongAttributes: function(backendAttr, target) {this.error("handleBackendSongAttributes");},
// ************* built-in utility functions
mapUri2Fs: function(uri) { // use extended ASCII that most likely isn't used in filenames
// replace chars that cannot be used in file/foldernames
var out= uri.replace(/\/\//, "ýý");
out = out.replace(/\?/, "ÿ");
out = out.replace(/:/, "þ");
out = out.replace(/\*/, "ü");
out = out.replace(/"/, "û");
out = out.replace(/</, "ù");
out = out.replace(/>/, "ø");
out = out.replace(/\|/, "÷");
return out;
},
mapFs2Uri: function(fs) {
var out= fs.replace(/ýý/, "//");
out = out.replace(/ÿ/, "?");
out = out.replace(/þ/, ":");
out = out.replace(/ü/, "*");
out = out.replace(/û/, "\"");
out = out.replace(/ù/, "<");
out = out.replace(/ø/, ">");
out = out.replace(/÷/, "|");
return out;
},
// used for interaction with player
setObserver: function(o) {
this._observer= o;
},
notifyAdapterReady: function() {
if (typeof this._observer !== "undefined" ) this._observer.notify();
},
error: function(name) {
alert("fatal error: abstract method '"+name+"' must be defined");
},
resetSampleRate: function(sampleRate, inputSampleRate) {
if (sampleRate > 0) { this._sampleRate= sampleRate; }
if (inputSampleRate > 0) { this._inputSampleRate= inputSampleRate; }
var s= Math.round(SAMPLES_PER_BUFFER *this._sampleRate/this._inputSampleRate) *this.getChannels();
if (s > this._resampleBuffer.length) {
this._resampleBuffer= this.allocResampleBuffer(s);
}
},
allocResampleBuffer: function(s) {
return new Float32Array(s);
},
getCopiedAudio: function(input, len, funcReadFloat, resampleOutput) {
var i;
// just copy the rescaled values so there is no need for special handling in playback loop
for(i= 0; i<len*this._channels; i++){
resampleOutput[i]= funcReadFloat(input, i);
}
return len;
},
resampleTickerData: function(externalTicker, origLen) {
externalTicker.resampleData(this._sampleRate, this._inputSampleRate, origLen, this);
},
getResampledAudio: function(input, len) {
return this.getResampledFloats(input, len, this._sampleRate, this._inputSampleRate);
},
getResampledFloats: function(input, len, sampleRate, inputSampleRate) {
var resampleLen;
if (sampleRate == inputSampleRate) {
resampleLen= this.getCopiedAudio(input, len, this.readFloatSample.bind(this), this._resampleBuffer);
} else {
resampleLen= Math.round(len * sampleRate / inputSampleRate);
var bufSize= resampleLen * this._channels; // for each of the x channels
if (bufSize > this._resampleBuffer.length) { this._resampleBuffer= this.allocResampleBuffer(bufSize); }
// only mono and interleaved stereo data is currently implemented..
this.resampleToFloat(this._channels, 0, input, len, this.readFloatSample.bind(this), this._resampleBuffer, resampleLen);
if (this._channels == 2) {
this.resampleToFloat(this._channels, 1, input, len, this.readFloatSample.bind(this), this._resampleBuffer, resampleLen);
}
}
return resampleLen;
},
// utility
resampleToFloat: function(channels, channelId, inputPtr, len, funcReadFloat, resampleOutput, resampleLen) {
// Bresenham (line drawing) algorithm based resampling
var x0= 0;
var y0= 0;
var x1= resampleLen - 0;
var y1= len - 0;
var dx = Math.abs(x1-x0), sx = x0<x1 ? 1 : -1;
var dy = -Math.abs(y1-y0), sy = y0<y1 ? 1 : -1;
var err = dx+dy, e2;
var i;
for(;;){
i= (x0*channels) + channelId;
resampleOutput[i]= funcReadFloat(inputPtr, (y0*channels) + channelId);
if (x0>=x1 && y0>=y1) { break; }
e2 = 2*err;
if (e2 > dy) { err += dy; x0 += sx; }
if (e2 < dx) { err += dx; y0 += sy; }
}
},
getResampleBuffer: function() {
return this._resampleBuffer;
}
};
/*
* Emscripten based backends that produce 16-bit sample data.
*
* NOTE: This impl adds handling for asynchronously initialized 'backends', i.e.
* the 'backend' that is passed in, may not yet be usable (see WebAssebly based impls:
* here a respective "onRuntimeInitialized" event will eventually originate from the 'backend').
* The 'backend' allows to register a "adapterCallback" hook to propagate the event - which is
* used here. The player typically observes the backend-adapter and when the adapter state changes, a
* "notifyAdapterReady" is triggered so that the player is notified of the change.
*/
EmsHEAP16BackendAdapter = (function(){ var $this = function (backend, channels) {
$this.base.call(this, channels, 2);
this.Module= backend;
// required if WASM (asynchronously loaded) is used in the backend impl
this.Module["adapterCallback"] = function() { // when Module is ready
this.doOnAdapterReady(); // hook allows to perform additional initialization
this.notifyAdapterReady(); // propagate to change to player
}.bind(this);
if (!window.Math.fround) { window.Math.fround = window.Math.round; } // < Chrome 38 hack
};
extend(AudioBackendAdapterBase, $this, {
doOnAdapterReady: function() { }, // noop, to be overridden in subclasses
/* async emscripten init means that adapter may not immediately be ready - see async WASM compilation */
isAdapterReady: function() {
if (typeof this.Module.notReady === "undefined") return true; // default for backward compatibility
return !this.Module.notReady;
},
registerEmscriptenFileData: function(pathFilenameArray, data) {
// create a virtual emscripten FS for all the songs that are touched.. so the compiled code will
// always find what it is looking for.. some players will look to additional resource files in the same folder..
// Unfortunately the FS.findObject() API is not exported.. so it's exception catching time
try {
this.Module.FS_createPath("/", pathFilenameArray[0], true, true);
} catch(e) {
}
var f;
try {
if (typeof this.Module.FS_createDataFile == 'undefined') {
f= true; // backend without FS (ignore for drag&drop files)
} else {
f= this.Module.FS_createDataFile(pathFilenameArray[0], pathFilenameArray[1], data, true, true);
var p= ScriptNodePlayer.getInstance().trace("registerEmscriptenFileData: [" +
pathFilenameArray[0]+ "][" +pathFilenameArray[1]+ "] size: "+ data.length);
}
} catch(err) {
// file may already exist, e.g. drag/dropped again.. just keep entry
}
return f;
},
readFloatSample: function(buffer, idx) {
return (this.Module.HEAP16[buffer+idx])/0x8000;
},
// certain songs use an unfavorable L/R separation - e.g. bass on one channel - that is
// not nice to listen to. This "panning" impl allows to "mono"-ify those songs.. (this._pan=1
// creates mono)
applyPanning: function(buffer, len, pan) {
pan= pan * 256.0 / 2.0;
var i, l, r, m;
for (i = 0; i < len*2; i+=2) {
l = this.Module.HEAP16[buffer+i];
r = this.Module.HEAP16[buffer+i+1];
m = (r - l) * pan;
var nl= ((l << 8) + m) >> 8;
var nr= ((r << 8) - m) >> 8;
this.Module.HEAP16[buffer+i] = nl;
this.Module.HEAP16[buffer+i+1] = nr;
/*
if ((this.Module.HEAP16[buffer+i] != nl) || (this.Module.HEAP16[buffer+i+1] == nr)) {
console.log("X");
}*/
}
}
}); return $this; })();
/*
* Emscripten based backends that produce 32-bit float sample data.
*
* NOTE: This impl adds handling for asynchronously initialized 'backends', i.e.
* the 'backend' that is passed in, may not yet be usable (see WebAssebly based impls:
* here a respective "onRuntimeInitialized" event will eventually originate from the 'backend').
* The 'backend' allows to register a "adapterCallback" hook to propagate the event - which is
* used here. The player typically observes the backend-adapter and when the adapter state changes, a
* "notifyAdapterReady" is triggered so that the player is notified of the change.
*/
EmsHEAPF32BackendAdapter = (function(){ var $this = function (backend, channels) {
$this.base.call(this, backend, channels);
this._bytesPerSample= 4;
};
extend(EmsHEAP16BackendAdapter, $this, {
readFloatSample: function(buffer, idx) {
return (this.Module.HEAPF32[buffer+idx]);
},
// certain songs use an unfavorable L/R separation - e.g. bass on one channel - that is
// not nice to listen to. This "panning" impl allows to "mono"-ify those songs.. (this._pan=1
// creates mono)
applyPanning: function(buffer, len, pan) {
pan= pan * 256.0 / 2.0;
var i, l, r, m;
for (i = 0; i < len*2; i+=2) {
l = this.Module.HEAPF32[buffer+i];
r = this.Module.HEAPF32[buffer+i+1];
m = (r - l) * pan;
var nl= ((l *256) + m) /256;
var nr= ((r *256) - m) /256;
this.Module.HEAPF32[buffer+i] = nl;
this.Module.HEAPF32[buffer+i+1] = nr;
}
}
}); return $this; })();
// cache all loaded files in global cache.
FileCache = function() {
this._binaryFileMap= {}; // cache for loaded "file" binaries
this._pendingFileMap= {};
this._isWaitingForFile= false; // signals that some file loading is still in progress
};
FileCache.prototype = {
getFileMap: function () {
return this._binaryFileMap;
},
getPendingMap: function () {
return this._pendingFileMap;
},
setWaitingForFile: function (val) {
this._isWaitingForFile= val;
},
isWaitingForFile: function () {
return this._isWaitingForFile;
},
getFile: function (filename) {
var data;
if (filename in this._binaryFileMap) {
data= this._binaryFileMap[filename];
}
return data;
},
// FIXME the unlimited caching of files should probably be restricted:
// currently all loaded song data stays in memory as long as the page is opened
// maybe just add some manual "reset"?
setFile: function(filename, data) {
this._binaryFileMap[filename]= data;
this._isWaitingForFile= false;
}
};
/**
* Generic ScriptProcessor based WebAudio music player (end user API).
*
* <p>Deals with the WebAudio node pipeline, feeds the sample data chunks delivered by
* the backend into the WebAudio input buffers, provides basic file input facilities.
*
* This player is used as a singleton (i.e. instanciation of a player destroys the previous one).
*
* GUI can use the player via:
* ScriptNodePlayer.createInstance(...); and
* ScriptNodePlayer.getInstance();
*/
var ScriptNodePlayer = (function () {
/*
* @param externalTicker must be a subclass of AbstractTicker
*/
PlayerImpl = function(backendAdapter, basePath, requiredFiles, spectrumEnabled, onPlayerReady, onTrackReadyToPlay, onTrackEnd, onUpdate, externalTicker, bufferSize) {
if(typeof backendAdapter === 'undefined') { alert("fatal error: backendAdapter not specified"); }
if(typeof onPlayerReady === 'undefined') { alert("fatal error: onPlayerReady not specified"); }
if(typeof onTrackReadyToPlay === 'undefined') { alert("fatal error: onTrackReadyToPlay not specified"); }
if(typeof onTrackEnd === 'undefined') { alert("fatal error: onTrackEnd not specified"); }
if(typeof bufferSize !== 'undefined') { window.SAMPLES_PER_BUFFER= bufferSize; }
if (backendAdapter.getChannels() >2) { alert("fatal error: only 1 or 2 output channels supported"); }
this._backendAdapter= backendAdapter;
this._backendAdapter.setObserver(this);
this._basePath= basePath;
this._traceSwitch= false;
this._spectrumEnabled= spectrumEnabled;
// container for song infos like: name, author, etc
this._songInfo = {};
// hooks that allow to react to specific events
this._onTrackReadyToPlay= onTrackReadyToPlay;
this._onTrackEnd= onTrackEnd;
this._onPlayerReady= onPlayerReady;
this._onUpdate= onUpdate; // optional
// "external ticker" allows to sync separately maintained data with the actual audio playback
this._tickerStepWidth= 256; // shortest available (i.e. tick every 256 samples)
if(typeof externalTicker !== 'undefined') {
externalTicker.init(SAMPLES_PER_BUFFER, this._tickerStepWidth);
}
this._externalTicker = externalTicker;
this._currentTick= 0;
this._silenceStarttime= -1;
this._silenceTimeout= 5; // by default 5 secs of silence will end a song
// audio buffer handling
this._sourceBuffer;
this._sourceBufferLen;
this._numberOfSamplesRendered= 0;
this._numberOfSamplesToRender= 0;
this._sourceBufferIdx=0;
// // additional timeout based "song end" handling
this._currentPlaytime= 0;
this._currentTimeout= -1;
if (!this.isAutoPlayCripple()) {
// original impl
setGlobalWebAudioCtx();
this._sampleRate = window._gPlayerAudioCtx.sampleRate;
this._correctSampleRate= this._sampleRate;
this._backendAdapter.resetSampleRate(this._sampleRate, -1);
}
// general WebAudio stuff
this._bufferSource;
this._gainNode;
this._analyzerNode;
this._scriptNode;
this._freqByteData = 0;
this._pan= null; // default: inactive
// the below entry points are published globally they can be
// easily referenced from the outside..
window.fileRequestCallback= this.fileRequestCallback.bind(this);
window.fileSizeRequestCallback= this.fileSizeRequestCallback.bind(this);
window.songUpdateCallback= this.songUpdateCallback.bind(this);
// --------------- player status stuff ----------
this._isPaused= false; // 'end' of a song also triggers this state
// setup asyc completion of initialization
this._isPlayerReady= false; // this state means that the player is initialized and can be used now
this._isSongReady= false; // initialized (including file-loads that might have been necessary)
this._initInProgress= false;
this._preLoadReady= false;
window.player= this;
var f= window.player['preloadFiles'].bind(window.player);
f(requiredFiles, function() {
this._preLoadReady= true;
if (this._preLoadReady && this._backendAdapter.isAdapterReady() && this._backendAdapter.isManualSetupComplete()) {
this._isPlayerReady= true;
this._onPlayerReady();
}
}.bind(this));
};
PlayerImpl.prototype = {
// ******* general
notify: function() { // used to handle asynchronously initialized backend impls
if ((typeof this.deferredPreload !== "undefined") && this._backendAdapter.isAdapterReady()) {
// now that the runtime is ready the "preload" can be started
var files= this.deferredPreload[0];
var onCompletionHandler= this.deferredPreload[1];
delete this.deferredPreload;
this.preload(files, files.length, onCompletionHandler);
}
if (!this._isPlayerReady && this._preLoadReady && this._backendAdapter.isAdapterReady() && this._backendAdapter.isManualSetupComplete()) {
this._isPlayerReady= true;
this._onPlayerReady();
}
},
handleBackendEvent: function() { this.notify(); }, // deprecated, use notify()!
/**
* Is the player ready for use? (i.e. initialization completed)
*/
isReady: function() {
return this._isPlayerReady;
},
/**
* Change the default 5sec timeout (0 means no timeout).
*/
setSilenceTimeout: function(silenceTimeout) {
// usecase: user may temporarrily turn off output (see DeepSID) and player should not end song
this._silenceTimeout= silenceTimeout;
},
/**
* Turn on debug output to JavaScript console.
*/
setTraceMode: function (on) {
this._traceSwitch= on;
},
// ******* basic playback features
/*
* start audio playback
*/
play: function() {
this._isPaused= false;
// this function isn't invoked directly from some "user gesture" (but
// indirectly from "onload" handler) so it might not work on braindead iOS shit
try { this._bufferSource.start(0); } catch(ignore) {}
},
/*
* pause audio playback
*/
pause: function() {
if ((!this.isWaitingForFile()) && (!this._initInProgress) && this._isSongReady) {
this._isPaused= true;
}
},
isPaused: function() {
return this._isPaused;
},
/*
* resume audio playback
*/
resume: function() {
if ((!this.isWaitingForFile()) && (!this._initInProgress) && this._isSongReady) {
this.play();
}
},
/*
* gets the index of the 'tick' that is currently playing.
* allows to sync separately stored data with the audio playback.
*/
getCurrentTick: function() {
var idx= Math.ceil(SAMPLES_PER_BUFFER/this._tickerStepWidth)-1;
idx= Math.min(idx, this._currentTick)
return idx;
},
/*
* set the playback volume (input between 0 and 1)
*/
setVolume: function(value) {
if (typeof this._gainNode != 'undefined') {
this._gainNode.gain.value= value;
}
},
getVolume: function() {
if (typeof this._gainNode != 'undefined') {
return this._gainNode.gain.value;
}
return -1;
},
/**
* @value null=inactive; or range; -1 to 1 (-1 is original stereo, 0 creates "mono", 1 is inverted stereo)
*/
setPanning: function(value) {
this._pan= value;
},
/*
* is playback in stereo?
*/
isStereo: function() {
return this._backendAdapter.getChannels() == 2;
},
/**
* Get backend specific song infos like 'author', 'name', etc.
*/
getSongInfo: function () {
return this._songInfo;
},
/**
* Get meta info about backend specific song infos, e.g. what attributes are available and what type are they.
*/
getSongInfoMeta: function() {
return this._backendAdapter.getSongInfoMeta();
},
/*
* Manually defined playback time to use until 'end' of a track (only affects the
* currently selected track).
* @param t time in millis
*/
setPlaybackTimeout: function(t) {
this._currentPlaytime= 0;
if (t<0) {
this._currentTimeout= -1;
} else {
this._currentTimeout= t/1000*this._correctSampleRate;
}
},
/*
* Timeout in seconds.
*/
getPlaybackTimeout: function() {
if (this._currentTimeout < 0) {
return -1;
} else {
return Math.round(this._currentTimeout/this._correctSampleRate);
}
},
getCurrentPlaytime: function() {
// return Math.round(this._currentPlaytime/this._correctSampleRate);
return this._currentPlaytime/this._correctSampleRate; // let user do the rounding in needed
},
// ******* access to frequency spectrum data (if enabled upon construction)
getFreqByteData: function () {
if (this._analyzerNode) {
if (this._freqByteData === 0) {
this._freqByteData = new Uint8Array(this._analyzerNode.frequencyBinCount);
}
this._analyzerNode.getByteFrequencyData(this._freqByteData);
}
return this._freqByteData;
},
// ******* song "position seek" related (if available with used backend)
/**
* Return: default 0 seeking not supported
*/
getMaxPlaybackPosition: function() { return this._backendAdapter.getMaxPlaybackPosition();},
/**
* Return: default 0
*/
getPlaybackPosition: function() { return this._backendAdapter.getPlaybackPosition();},
/**
* Move playback to 'pos': must be between 0 and getMaxSeekPosition()
* Return: 0 if successful
*/
seekPlaybackPosition: function(pos) { return this._backendAdapter.seekPlaybackPosition(pos);},
// ******* (music) file input related
/**
* Loads from a JavaScript File object - e.g. used for 'drag & drop'.
*/
loadMusicFromTmpFile: function (file, options, onCompletion, onFail, onProgress) {
this.initByUserGesture(); // cannot be done from the callbacks below.. see iOS shit
var filename= file.name; // format detection may depend on prefixes and postfixes..
this._fileReadyNotify= "";
var fullFilename= ((options.basePath)?options.basePath:this._basePath) + filename; // this._basePath ever needed?
if (this.loadMusicDataFromCache(fullFilename, options, onFail)) { return; }
var reader = new FileReader();
reader.onload = function() {
var pfn= this._backendAdapter.getPathAndFilename(filename);
var data= new Uint8Array(reader.result);
var fileHandle= this._backendAdapter.registerFileData(pfn, data);
if (typeof fileHandle === 'undefined' ) {
onFail();
return;
} else {
var cacheFilename= this._backendAdapter.mapCacheFileName(fullFilename);
this.getCache().setFile(cacheFilename, data);
}
this.prepareTrackForPlayback(fullFilename, reader.result, options);
onCompletion(filename);
}.bind(this);
reader.onprogress = function (oEvent) {
if (onProgress) {
onProgress(oEvent.total, oEvent.loaded);
}
}.bind(this);
reader.readAsArrayBuffer(file);
},
isAppleShit: function() {
return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform);
},
isAutoPlayCripple: function() {
return window.chrome || this.isAppleShit();
},
initByUserGesture: function() {
// try to setup as much as possible while it is "directly triggered"
// by "user gesture" (i.e. here).. seems POS iOS does not correctly
// recognize any async-indirections started from here.. bloody Apple idiots
if (typeof this._sampleRate == 'undefined') {
setGlobalWebAudioCtx();
this._sampleRate = window._gPlayerAudioCtx.sampleRate;
this._correctSampleRate= this._sampleRate;
this._backendAdapter.resetSampleRate(this._sampleRate, -1);
} else {
// just in case: handle Chrome's new bullshit "autoplay policy"
if (window._gPlayerAudioCtx.state == "suspended") {
try {window._gPlayerAudioCtx.resume();} catch(e) {}
}
}
if (typeof this._bufferSource != 'undefined') {
try {
this._bufferSource.stop(0);
} catch(err) {} // ignore for the benefit of Safari(OS X)
} else {
var ctx= window._gPlayerAudioCtx;
if (this.isAppleShit()) this.iOSHack(ctx);
this._analyzerNode = ctx.createAnalyser();
this._scriptNode= this.createScriptProcessor(ctx);
this._gainNode = ctx.createGain();
this._scriptNode.connect(this._gainNode);
// optional add-on
if (typeof this._externalTicker !== 'undefined') {
var tickerScriptNode= this.createTickerScriptProcessor(ctx);
tickerScriptNode.connect(this._gainNode);