Path: blob/main/misc/emulator/gba/user_scripts/XAudioJS/XAudioServer.js
28798 views
//2010-2013 Grant Galitz - XAudioJS realtime audio output compatibility library:1var XAudioJSscriptsHandle = document.getElementsByTagName("script");2var XAudioJSsourceHandle = XAudioJSscriptsHandle[XAudioJSscriptsHandle.length-1].src;3function XAudioServer(channels, sampleRate, minBufferSize, maxBufferSize, underRunCallback, volume, failureCallback) {4XAudioJSChannelsAllocated = Math.max(channels, 1);5this.XAudioJSSampleRate = Math.abs(sampleRate);6XAudioJSMinBufferSize = (minBufferSize >= (XAudioJSSamplesPerCallback * XAudioJSChannelsAllocated) && minBufferSize < maxBufferSize) ? (minBufferSize & (-XAudioJSChannelsAllocated)) : (XAudioJSSamplesPerCallback * XAudioJSChannelsAllocated);7XAudioJSMaxBufferSize = (Math.floor(maxBufferSize) > XAudioJSMinBufferSize + XAudioJSChannelsAllocated) ? (maxBufferSize & (-XAudioJSChannelsAllocated)) : (XAudioJSMinBufferSize * XAudioJSChannelsAllocated);8this.underRunCallback = (typeof underRunCallback == "function") ? underRunCallback : function () {};9XAudioJSVolume = (volume >= 0 && volume <= 1) ? volume : 1;10this.failureCallback = (typeof failureCallback == "function") ? failureCallback : function () { throw(new Error("XAudioJS has encountered a fatal error.")); };11this.initializeAudio();12}13XAudioServer.prototype.MOZWriteAudioNoCallback = function (buffer) {14//Resample before passing to the moz audio api:15var bufferLength = buffer.length;16for (var bufferIndex = 0; bufferIndex < bufferLength;) {17var sliceLength = Math.min(bufferLength - bufferIndex, XAudioJSMaxBufferSize);18for (var sliceIndex = 0; sliceIndex < sliceLength; ++sliceIndex) {19XAudioJSAudioContextSampleBuffer[sliceIndex] = buffer[bufferIndex++];20}21var resampleLength = XAudioJSResampleControl.resampler(XAudioJSGetArraySlice(XAudioJSAudioContextSampleBuffer, sliceIndex));22if (resampleLength > 0) {23var resampledResult = XAudioJSResampleControl.outputBuffer;24var resampledBuffer = XAudioJSGetArraySlice(resampledResult, resampleLength);25this.samplesAlreadyWritten += this.audioHandleMoz.mozWriteAudio(resampledBuffer);26}27}28}29XAudioServer.prototype.callbackBasedWriteAudioNoCallback = function (buffer) {30//Callback-centered audio APIs:31var length = buffer.length;32for (var bufferCounter = 0; bufferCounter < length && XAudioJSAudioBufferSize < XAudioJSMaxBufferSize;) {33XAudioJSAudioContextSampleBuffer[XAudioJSAudioBufferSize++] = buffer[bufferCounter++];34}35}36/*Pass your samples into here!37Pack your samples as a one-dimenional array38With the channel samples packed uniformly.39examples:40mono - [left, left, left, left]41stereo - [left, right, left, right, left, right, left, right]42*/43XAudioServer.prototype.writeAudio = function (buffer) {44switch (this.audioType) {45case 0:46this.MOZWriteAudioNoCallback(buffer);47this.MOZExecuteCallback();48break;49case 2:50this.checkFlashInit();51case 1:52case 3:53this.callbackBasedWriteAudioNoCallback(buffer);54this.callbackBasedExecuteCallback();55break;56default:57this.failureCallback();58}59}60/*Pass your samples into here if you don't want automatic callback calling:61Pack your samples as a one-dimenional array62With the channel samples packed uniformly.63examples:64mono - [left, left, left, left]65stereo - [left, right, left, right, left, right, left, right]66Useful in preventing infinite recursion issues with calling writeAudio inside your callback.67*/68XAudioServer.prototype.writeAudioNoCallback = function (buffer) {69switch (this.audioType) {70case 0:71this.MOZWriteAudioNoCallback(buffer);72break;73case 2:74this.checkFlashInit();75case 1:76case 3:77this.callbackBasedWriteAudioNoCallback(buffer);78break;79default:80this.failureCallback();81}82}83//Developer can use this to see how many samples to write (example: minimum buffer allotment minus remaining samples left returned from this function to make sure maximum buffering is done...)84//If null is returned, then that means metric could not be done.85XAudioServer.prototype.remainingBuffer = function () {86switch (this.audioType) {87case 0:88return Math.floor((this.samplesAlreadyWritten - this.audioHandleMoz.mozCurrentSampleOffset()) * XAudioJSResampleControl.ratioWeight / XAudioJSChannelsAllocated) * XAudioJSChannelsAllocated;89case 2:90this.checkFlashInit();91case 1:92case 3:93return (Math.floor((XAudioJSResampledSamplesLeft() * XAudioJSResampleControl.ratioWeight) / XAudioJSChannelsAllocated) * XAudioJSChannelsAllocated) + XAudioJSAudioBufferSize;94default:95this.failureCallback();96return null;97}98}99XAudioServer.prototype.MOZExecuteCallback = function () {100//mozAudio:101var samplesRequested = XAudioJSMinBufferSize - this.remainingBuffer();102if (samplesRequested > 0) {103this.MOZWriteAudioNoCallback(this.underRunCallback(samplesRequested));104}105}106XAudioServer.prototype.callbackBasedExecuteCallback = function () {107//WebKit /Flash Audio:108var samplesRequested = XAudioJSMinBufferSize - this.remainingBuffer();109if (samplesRequested > 0) {110this.callbackBasedWriteAudioNoCallback(this.underRunCallback(samplesRequested));111}112}113//If you just want your callback called for any possible refill (Execution of callback is still conditional):114XAudioServer.prototype.executeCallback = function () {115switch (this.audioType) {116case 0:117this.MOZExecuteCallback();118break;119case 2:120this.checkFlashInit();121case 1:122case 3:123this.callbackBasedExecuteCallback();124break;125default:126this.failureCallback();127}128}129//DO NOT CALL THIS, the lib calls this internally!130XAudioServer.prototype.initializeAudio = function () {131try {132this.initializeMozAudio();133}134catch (error) {135try {136this.initializeWebAudio();137}138catch (error) {139try {140this.initializeMediaStream();141}142catch (error) {143try {144this.initializeFlashAudio();145}146catch (error) {147this.audioType = -1;148this.failureCallback();149}150}151}152}153}154XAudioServer.prototype.initializeMediaStream = function () {155this.audioHandleMediaStream = new Audio();156this.resetCallbackAPIAudioBuffer(XAudioJSMediaStreamSampleRate);157if (XAudioJSMediaStreamWorker) {158//WebWorker is not GC'd, so manually collect it:159XAudioJSMediaStreamWorker.terminate();160}161XAudioJSMediaStreamWorker = new Worker(XAudioJSsourceHandle.substring(0, XAudioJSsourceHandle.length - 3) + "MediaStreamWorker.js");162this.audioHandleMediaStreamProcessing = new ProcessedMediaStream(XAudioJSMediaStreamWorker, XAudioJSMediaStreamSampleRate, XAudioJSChannelsAllocated);163this.audioHandleMediaStream.src = this.audioHandleMediaStreamProcessing;164this.audioHandleMediaStream.volume = XAudioJSVolume;165XAudioJSMediaStreamWorker.onmessage = XAudioJSMediaStreamPushAudio;166XAudioJSMediaStreamWorker.postMessage([1, XAudioJSResampleBufferSize, XAudioJSChannelsAllocated]);167this.audioHandleMediaStream.play();168this.audioType = 3;169}170XAudioServer.prototype.initializeMozAudio = function () {171this.audioHandleMoz = new Audio();172this.audioHandleMoz.mozSetup(XAudioJSChannelsAllocated, XAudioJSMozAudioSampleRate);173this.audioHandleMoz.volume = XAudioJSVolume;174this.samplesAlreadyWritten = 0;175this.audioType = 0;176//if (navigator.platform != "MacIntel" && navigator.platform != "MacPPC") {177//Add some additional buffering space to workaround a moz audio api issue:178var bufferAmount = (this.XAudioJSSampleRate * XAudioJSChannelsAllocated / 10) | 0;179bufferAmount -= bufferAmount % XAudioJSChannelsAllocated;180this.samplesAlreadyWritten -= bufferAmount;181//}182this.initializeResampler(XAudioJSMozAudioSampleRate);183}184XAudioServer.prototype.initializeWebAudio = function () {185if (!XAudioJSWebAudioLaunchedContext) {186try {187XAudioJSWebAudioContextHandle = new AudioContext(); //Create a system audio context.188}189catch (error) {190XAudioJSWebAudioContextHandle = new webkitAudioContext(); //Create a system audio context.191}192XAudioJSWebAudioLaunchedContext = true;193}194if (XAudioJSWebAudioAudioNode) {195XAudioJSWebAudioAudioNode.disconnect();196XAudioJSWebAudioAudioNode.onaudioprocess = null;197XAudioJSWebAudioAudioNode = null;198}199try {200XAudioJSWebAudioAudioNode = XAudioJSWebAudioContextHandle.createScriptProcessor(XAudioJSSamplesPerCallback, 0, XAudioJSChannelsAllocated); //Create the js event node.201}202catch (error) {203XAudioJSWebAudioAudioNode = XAudioJSWebAudioContextHandle.createJavaScriptNode(XAudioJSSamplesPerCallback, 0, XAudioJSChannelsAllocated); //Create the js event node.204}205XAudioJSWebAudioAudioNode.onaudioprocess = XAudioJSWebAudioEvent; //Connect the audio processing event to a handling function so we can manipulate output206XAudioJSWebAudioAudioNode.connect(XAudioJSWebAudioContextHandle.destination); //Send and chain the output of the audio manipulation to the system audio output.207this.resetCallbackAPIAudioBuffer(XAudioJSWebAudioContextHandle.sampleRate);208this.audioType = 1;209/*210Firefox has a bug in its web audio implementation...211The node may randomly stop playing on Mac OS X for no212good reason. Keep a watchdog timer to restart the failed213node if it glitches. Google Chrome never had this issue.214*/215XAudioJSWebAudioWatchDogLast = (new Date()).getTime();216if (navigator.userAgent.indexOf('Gecko/') > -1) {217if (XAudioJSWebAudioWatchDogTimer) {218clearInterval(XAudioJSWebAudioWatchDogTimer);219}220var parentObj = this;221XAudioJSWebAudioWatchDogTimer = setInterval(function () {222var timeDiff = (new Date()).getTime() - XAudioJSWebAudioWatchDogLast;223if (timeDiff > 500) {224parentObj.initializeWebAudio();225}226}, 500);227}228}229XAudioServer.prototype.initializeFlashAudio = function () {230var existingFlashload = document.getElementById("XAudioJS");231this.flashInitialized = false;232this.resetCallbackAPIAudioBuffer(44100);233switch (XAudioJSChannelsAllocated) {234case 1:235XAudioJSFlashTransportEncoder = XAudioJSGenerateFlashMonoString;236break;237case 2:238XAudioJSFlashTransportEncoder = XAudioJSGenerateFlashStereoString;239break;240default:241XAudioJSFlashTransportEncoder = XAudioJSGenerateFlashSurroundString;242}243if (existingFlashload == null) {244this.audioHandleFlash = null;245var thisObj = this;246var mainContainerNode = document.createElement("div");247mainContainerNode.setAttribute("style", "position: fixed; bottom: 0px; right: 0px; margin: 0px; padding: 0px; border: none; width: 8px; height: 8px; overflow: hidden; z-index: -1000; ");248var containerNode = document.createElement("div");249containerNode.setAttribute("style", "position: static; border: none; width: 0px; height: 0px; visibility: hidden; margin: 8px; padding: 0px;");250containerNode.setAttribute("id", "XAudioJS");251mainContainerNode.appendChild(containerNode);252document.getElementsByTagName("body")[0].appendChild(mainContainerNode);253swfobject.embedSWF(254XAudioJSsourceHandle.substring(0, XAudioJSsourceHandle.length - 9) + "JS.swf",255"XAudioJS",256"8",257"8",258"9.0.0",259"",260{},261{"allowscriptaccess":"always"},262{"style":"position: static; visibility: hidden; margin: 8px; padding: 0px; border: none"},263function (event) {264if (event.success) {265thisObj.audioHandleFlash = event.ref;266thisObj.checkFlashInit();267}268else {269thisObj.failureCallback();270thisObj.audioType = -1;271}272}273);274}275else {276this.audioHandleFlash = existingFlashload;277this.checkFlashInit();278}279this.audioType = 2;280}281XAudioServer.prototype.changeVolume = function (newVolume) {282if (newVolume >= 0 && newVolume <= 1) {283XAudioJSVolume = newVolume;284switch (this.audioType) {285case 0:286this.audioHandleMoz.volume = XAudioJSVolume;287case 1:288break;289case 2:290if (this.flashInitialized) {291this.audioHandleFlash.changeVolume(XAudioJSVolume);292}293else {294this.checkFlashInit();295}296break;297case 3:298this.audioHandleMediaStream.volume = XAudioJSVolume;299break;300default:301this.failureCallback();302}303}304}305//Checks to see if the NPAPI Adobe Flash bridge is ready yet:306XAudioServer.prototype.checkFlashInit = function () {307if (!this.flashInitialized) {308try {309if (this.audioHandleFlash && this.audioHandleFlash.initialize) {310this.flashInitialized = true;311this.audioHandleFlash.initialize(XAudioJSChannelsAllocated, XAudioJSVolume);312}313}314catch (error) {315this.flashInitialized = false;316}317}318}319//Set up the resampling:320XAudioServer.prototype.resetCallbackAPIAudioBuffer = function (APISampleRate) {321XAudioJSAudioBufferSize = XAudioJSResampleBufferEnd = XAudioJSResampleBufferStart = 0;322this.initializeResampler(APISampleRate);323XAudioJSResampledBuffer = this.getFloat32(XAudioJSResampleBufferSize);324}325XAudioServer.prototype.initializeResampler = function (sampleRate) {326XAudioJSAudioContextSampleBuffer = this.getFloat32(XAudioJSMaxBufferSize);327XAudioJSResampleBufferSize = Math.max(XAudioJSMaxBufferSize * Math.ceil(sampleRate / this.XAudioJSSampleRate) + XAudioJSChannelsAllocated, XAudioJSSamplesPerCallback * XAudioJSChannelsAllocated);328XAudioJSResampleControl = new Resampler(this.XAudioJSSampleRate, sampleRate, XAudioJSChannelsAllocated, XAudioJSResampleBufferSize, true);329}330XAudioServer.prototype.getFloat32 = function (size) {331try {332return new Float32Array(size);333}334catch (error) {335return [];336}337}338function XAudioJSFlashAudioEvent() { //The callback that flash calls...339XAudioJSResampleRefill();340return XAudioJSFlashTransportEncoder();341}342function XAudioJSGenerateFlashSurroundString() { //Convert the arrays to one long string for speed.343var XAudioJSTotalSamples = XAudioJSSamplesPerCallback << 1;344if (XAudioJSBinaryString.length > XAudioJSTotalSamples) {345XAudioJSBinaryString = [];346}347XAudioJSTotalSamples = 0;348for (var index = 0; index < XAudioJSSamplesPerCallback && XAudioJSResampleBufferStart != XAudioJSResampleBufferEnd; ++index) {349//Sanitize the buffer:350XAudioJSBinaryString[XAudioJSTotalSamples++] = String.fromCharCode(((Math.min(Math.max(XAudioJSResampledBuffer[XAudioJSResampleBufferStart++] + 1, 0), 2) * 0x3FFF) | 0) + 0x3000);351XAudioJSBinaryString[XAudioJSTotalSamples++] = String.fromCharCode(((Math.min(Math.max(XAudioJSResampledBuffer[XAudioJSResampleBufferStart++] + 1, 0), 2) * 0x3FFF) | 0) + 0x3000);352XAudioJSResampleBufferStart += XAudioJSChannelsAllocated - 2;353if (XAudioJSResampleBufferStart == XAudioJSResampleBufferSize) {354XAudioJSResampleBufferStart = 0;355}356}357return XAudioJSBinaryString.join("");358}359function XAudioJSGenerateFlashStereoString() { //Convert the arrays to one long string for speed.360var XAudioJSTotalSamples = XAudioJSSamplesPerCallback << 1;361if (XAudioJSBinaryString.length > XAudioJSTotalSamples) {362XAudioJSBinaryString = [];363}364for (var index = 0; index < XAudioJSTotalSamples && XAudioJSResampleBufferStart != XAudioJSResampleBufferEnd;) {365//Sanitize the buffer:366XAudioJSBinaryString[index++] = String.fromCharCode(((Math.min(Math.max(XAudioJSResampledBuffer[XAudioJSResampleBufferStart++] + 1, 0), 2) * 0x3FFF) | 0) + 0x3000);367XAudioJSBinaryString[index++] = String.fromCharCode(((Math.min(Math.max(XAudioJSResampledBuffer[XAudioJSResampleBufferStart++] + 1, 0), 2) * 0x3FFF) | 0) + 0x3000);368if (XAudioJSResampleBufferStart == XAudioJSResampleBufferSize) {369XAudioJSResampleBufferStart = 0;370}371}372return XAudioJSBinaryString.join("");373}374function XAudioJSGenerateFlashMonoString() { //Convert the array to one long string for speed.375if (XAudioJSBinaryString.length > XAudioJSSamplesPerCallback) {376XAudioJSBinaryString = [];377}378for (var index = 0; index < XAudioJSSamplesPerCallback && XAudioJSResampleBufferStart != XAudioJSResampleBufferEnd;) {379//Sanitize the buffer:380XAudioJSBinaryString[index++] = String.fromCharCode(((Math.min(Math.max(XAudioJSResampledBuffer[XAudioJSResampleBufferStart++] + 1, 0), 2) * 0x3FFF) | 0) + 0x3000);381if (XAudioJSResampleBufferStart == XAudioJSResampleBufferSize) {382XAudioJSResampleBufferStart = 0;383}384}385return XAudioJSBinaryString.join("");386}387//Some Required Globals:388var XAudioJSWebAudioContextHandle = null;389var XAudioJSWebAudioAudioNode = null;390var XAudioJSWebAudioWatchDogTimer = null;391var XAudioJSWebAudioWatchDogLast = false;392var XAudioJSWebAudioLaunchedContext = false;393var XAudioJSAudioContextSampleBuffer = [];394var XAudioJSResampledBuffer = [];395var XAudioJSMinBufferSize = 15000;396var XAudioJSMaxBufferSize = 25000;397var XAudioJSChannelsAllocated = 1;398var XAudioJSVolume = 1;399var XAudioJSResampleControl = null;400var XAudioJSAudioBufferSize = 0;401var XAudioJSResampleBufferStart = 0;402var XAudioJSResampleBufferEnd = 0;403var XAudioJSResampleBufferSize = 0;404var XAudioJSMediaStreamWorker = null;405var XAudioJSMediaStreamBuffer = [];406var XAudioJSMediaStreamSampleRate = 44100;407var XAudioJSMozAudioSampleRate = 44100;408var XAudioJSSamplesPerCallback = 2048; //Has to be between 2048 and 4096 (If over, then samples are ignored, if under then silence is added).409var XAudioJSFlashTransportEncoder = null;410var XAudioJSMediaStreamLengthAliasCounter = 0;411var XAudioJSBinaryString = [];412function XAudioJSWebAudioEvent(event) { //Web Audio API callback...413if (XAudioJSWebAudioWatchDogTimer) {414XAudioJSWebAudioWatchDogLast = (new Date()).getTime();415}416//Find all output channels:417for (var bufferCount = 0, buffers = []; bufferCount < XAudioJSChannelsAllocated; ++bufferCount) {418buffers[bufferCount] = event.outputBuffer.getChannelData(bufferCount);419}420//Make sure we have resampled samples ready:421XAudioJSResampleRefill();422//Copy samples from XAudioJS to the Web Audio API:423for (var index = 0; index < XAudioJSSamplesPerCallback && XAudioJSResampleBufferStart != XAudioJSResampleBufferEnd; ++index) {424for (bufferCount = 0; bufferCount < XAudioJSChannelsAllocated; ++bufferCount) {425buffers[bufferCount][index] = XAudioJSResampledBuffer[XAudioJSResampleBufferStart++] * XAudioJSVolume;426}427if (XAudioJSResampleBufferStart == XAudioJSResampleBufferSize) {428XAudioJSResampleBufferStart = 0;429}430}431//Pad with silence if we're underrunning:432while (index < XAudioJSSamplesPerCallback) {433for (bufferCount = 0; bufferCount < XAudioJSChannelsAllocated; ++bufferCount) {434buffers[bufferCount][index] = 0;435}436++index;437}438}439//MediaStream API buffer push440function XAudioJSMediaStreamPushAudio(event) {441var index = 0;442var audioLengthRequested = event.data;443var samplesPerCallbackAll = XAudioJSSamplesPerCallback * XAudioJSChannelsAllocated;444var XAudioJSMediaStreamLengthAlias = audioLengthRequested % XAudioJSSamplesPerCallback;445audioLengthRequested = audioLengthRequested - (XAudioJSMediaStreamLengthAliasCounter - (XAudioJSMediaStreamLengthAliasCounter % XAudioJSSamplesPerCallback)) - XAudioJSMediaStreamLengthAlias + XAudioJSSamplesPerCallback;446XAudioJSMediaStreamLengthAliasCounter -= XAudioJSMediaStreamLengthAliasCounter - (XAudioJSMediaStreamLengthAliasCounter % XAudioJSSamplesPerCallback);447XAudioJSMediaStreamLengthAliasCounter += XAudioJSSamplesPerCallback - XAudioJSMediaStreamLengthAlias;448if (XAudioJSMediaStreamBuffer.length != samplesPerCallbackAll) {449XAudioJSMediaStreamBuffer = new Float32Array(samplesPerCallbackAll);450}451XAudioJSResampleRefill();452while (index < audioLengthRequested) {453var index2 = 0;454while (index2 < samplesPerCallbackAll && XAudioJSResampleBufferStart != XAudioJSResampleBufferEnd) {455XAudioJSMediaStreamBuffer[index2++] = XAudioJSResampledBuffer[XAudioJSResampleBufferStart++];456if (XAudioJSResampleBufferStart == XAudioJSResampleBufferSize) {457XAudioJSResampleBufferStart = 0;458}459}460XAudioJSMediaStreamWorker.postMessage([0, XAudioJSMediaStreamBuffer]);461index += XAudioJSSamplesPerCallback;462}463}464function XAudioJSResampleRefill() {465if (XAudioJSAudioBufferSize > 0) {466//Resample a chunk of audio:467var resampleLength = XAudioJSResampleControl.resampler(XAudioJSGetBufferSamples());468var resampledResult = XAudioJSResampleControl.outputBuffer;469for (var index2 = 0; index2 < resampleLength;) {470XAudioJSResampledBuffer[XAudioJSResampleBufferEnd++] = resampledResult[index2++];471if (XAudioJSResampleBufferEnd == XAudioJSResampleBufferSize) {472XAudioJSResampleBufferEnd = 0;473}474if (XAudioJSResampleBufferStart == XAudioJSResampleBufferEnd) {475XAudioJSResampleBufferStart += XAudioJSChannelsAllocated;476if (XAudioJSResampleBufferStart == XAudioJSResampleBufferSize) {477XAudioJSResampleBufferStart = 0;478}479}480}481XAudioJSAudioBufferSize = 0;482}483}484function XAudioJSResampledSamplesLeft() {485return ((XAudioJSResampleBufferStart <= XAudioJSResampleBufferEnd) ? 0 : XAudioJSResampleBufferSize) + XAudioJSResampleBufferEnd - XAudioJSResampleBufferStart;486}487function XAudioJSGetBufferSamples() {488return XAudioJSGetArraySlice(XAudioJSAudioContextSampleBuffer, XAudioJSAudioBufferSize);489}490function XAudioJSGetArraySlice(buffer, lengthOf) {491//Typed array and normal array buffer section referencing:492try {493return buffer.subarray(0, lengthOf);494}495catch (error) {496try {497//Regular array pass:498buffer.length = lengthOf;499return buffer;500}501catch (error) {502//Nightly Firefox 4 used to have the subarray function named as slice:503return buffer.slice(0, lengthOf);504}505}506}507508