├── .gitignore
├── js
├── other
│ ├── XAudioJS.swf
│ ├── resize.js
│ ├── XAudioServerMediaStreamWorker.js
│ ├── mobile.js
│ ├── base64.js
│ ├── controls.js
│ ├── resampler.js
│ ├── swfobject.js
│ └── XAudioServer.js
└── GameBoyIO.js
├── rom
└── README.md
├── manifest.webapp
├── index.html
├── README.md
└── css
└── style.css
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gb
2 |
--------------------------------------------------------------------------------
/js/other/XAudioJS.swf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismaltby/GameBoy-Online/HEAD/js/other/XAudioJS.swf
--------------------------------------------------------------------------------
/rom/README.md:
--------------------------------------------------------------------------------
1 | Add your ROM here named as game.gb
2 | or edit the romPath in js/other/mobile.js to point to your ROM file.
3 |
--------------------------------------------------------------------------------
/manifest.webapp:
--------------------------------------------------------------------------------
1 | {
2 | "name": "GAMEBOY",
3 | "description": "GameBoy Online GAMEBOY Emulator",
4 | "launch_path": "/index.html",
5 | "fullscreen": true,
6 | "icons": {
7 | "128": "/images/128.png"
8 | },
9 | "orientation": ["portrait-primary"]
10 | }
11 |
--------------------------------------------------------------------------------
/js/other/resize.js:
--------------------------------------------------------------------------------
1 | function bindResize() {
2 | var canvas = document.getElementById("mainCanvas");
3 | var gameRatio = 160 / 144;
4 |
5 | function onResize() {
6 | var windowRatio = window.innerWidth / window.innerHeight;
7 | if (windowRatio < gameRatio) {
8 | canvas.style.width = window.innerWidth + "px";
9 | canvas.style.height = "auto";
10 | } else {
11 | canvas.style.height = window.innerHeight + "px";
12 | canvas.style.width = "auto";
13 | }
14 | }
15 |
16 | window.addEventListener("resize", onResize);
17 | onResize();
18 | }
19 |
20 | bindResize();
21 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
21 |
Select
22 |
Start
23 |
B
24 |
A
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # JavaScript GameBoy Color Emulator
2 |
3 | Fork of Grant Galitz's JavaScript GameBoy Color Emulator made for running a single ROM
4 | when uploading homebrew games to services like Itch.io.
5 |
6 | This version makes the following changes.
7 |
8 | - Canvas fills browser window on desktop/tablet while keeping aspect ratio
9 | - Touch controls displayed on mobile/tablet
10 | - Using css `image-rendering: pixelated` rather than bilinear filtering
11 | - Touch dpad controls for mobile using touch move with a deadzone
12 | - Keyboard fix for iPad keyboard case that doesn't report keyup event keycode
13 | - Wait for keyboard or touch input before starting AudioContext to fix issues in Chrome and iOS not playing Audio
14 | - No ability to switch ROM, or save/load states, this version is intended for deploying a single game
15 |
16 | ## Usage
17 |
18 | - Clone this repository
19 | - Add your ROM file as `rom/game.gb` (or edit romPath in js/other/mobile.js to point to your ROM file)
20 | - Upload to a webserver and visit index.html
21 |
22 | ## Keyboard Controls
23 |
24 | Up - Up Arrow / W
25 | Down - Down Arrow / S
26 | Left - Left Arrow / A
27 | Right - Right Arrow / D
28 | A - Alt / Z / J
29 | B - Ctrl / K / X
30 | Start - Enter
31 | Select - Shift
32 |
33 | Edit by changing `bindKeyboard` in `js/other/controls.js`.
34 |
35 | ## License
36 |
37 | **Copyright (C) 2010 - 2016 Grant Galitz**
38 |
39 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
40 |
41 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
42 |
43 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
44 |
--------------------------------------------------------------------------------
/js/other/XAudioServerMediaStreamWorker.js:
--------------------------------------------------------------------------------
1 | //This file is part of the XAudioJS library.
2 | var XAudioJSResampledBuffer = [];
3 | var XAudioJSOutputBuffer = [];
4 | var XAudioJSResampleBufferStart = 0;
5 | var XAudioJSResampleBufferEnd = 0;
6 | var XAudioJSResampleBufferSize = 0;
7 | var XAudioJSChannelsAllocated = 1;
8 | //Message Receiver:
9 | self.onmessage = function (event) {
10 | var data = event.data;
11 | switch (data[0]) {
12 | case 0:
13 | //Add new audio samples to our ring buffer:
14 | var resampledResult = data[1];
15 | var length = resampledResult.length;
16 | for (var i = 0; i < length; ++i) {
17 | XAudioJSResampledBuffer[XAudioJSResampleBufferEnd++] = resampledResult[i];
18 | if (XAudioJSResampleBufferEnd == XAudioJSResampleBufferSize) {
19 | XAudioJSResampleBufferEnd = 0;
20 | }
21 | if (XAudioJSResampleBufferStart == XAudioJSResampleBufferEnd) {
22 | XAudioJSResampleBufferStart += XAudioJSChannelsAllocated;
23 | if (XAudioJSResampleBufferStart == XAudioJSResampleBufferSize) {
24 | XAudioJSResampleBufferStart = 0;
25 | }
26 | }
27 | }
28 | break;
29 | case 1:
30 | //Initialize:
31 | XAudioJSResampleBufferSize = data[1];
32 | XAudioJSChannelsAllocated = data[2];
33 | XAudioJSResampledBuffer = new Float32Array(XAudioJSResampleBufferSize);
34 | }
35 | }
36 | //MediaStream Polyfill Event:
37 | self.onprocessmedia = function (event) {
38 | //Get some buffer length computations:
39 | var apiBufferLength = event.audioLength;
40 | var apiBufferLengthAll = apiBufferLength * event.audioChannels;
41 | if (apiBufferLengthAll > XAudioJSOutputBuffer.length) {
42 | XAudioJSOutputBuffer = new Float32Array(apiBufferLengthAll);
43 | }
44 | //De-interleave the buffered audio while looping through our ring buffer:
45 | var sampleFramesCount = Math.min(apiBufferLength, XAudioJSResampledSamplesLeft() / XAudioJSChannelsAllocated);
46 | for (var sampleFramePosition = 0, channelOffset = 0; sampleFramePosition < sampleFramesCount; ++sampleFramePosition) {
47 | for (channelOffset = sampleFramePosition; channelOffset < apiBufferLengthAll; channelOffset += apiBufferLength) {
48 | XAudioJSOutputBuffer[channelOffset] = XAudioJSResampledBuffer[XAudioJSResampleBufferStart++];
49 | if (XAudioJSResampleBufferStart == XAudioJSResampleBufferSize) {
50 | XAudioJSResampleBufferStart = 0;
51 | }
52 | }
53 | }
54 | //Add some zero fill if we underran the required buffer fill amount:
55 | while (sampleFramePosition < apiBufferLength) {
56 | for (channelOffset = sampleFramePosition++; channelOffset < apiBufferLengthAll; channelOffset += apiBufferLength) {
57 | XAudioJSOutputBuffer[channelOffset] = 0;
58 | }
59 | }
60 | //Write some buffered audio:
61 | event.writeAudio(XAudioJSOutputBuffer.subarray(0, apiBufferLengthAll));
62 | //Request a buffer from the main thread:
63 | self.postMessage(event.audioLength);
64 | }
65 | //Accessory function used to determine remaining samples in the ring buffer:
66 | function XAudioJSResampledSamplesLeft() {
67 | return ((XAudioJSResampleBufferStart <= XAudioJSResampleBufferEnd) ? 0 : XAudioJSResampleBufferSize) + XAudioJSResampleBufferEnd - XAudioJSResampleBufferStart;
68 | }
--------------------------------------------------------------------------------
/js/other/mobile.js:
--------------------------------------------------------------------------------
1 | var romPath = "rom/game.gb";
2 | var mainCanvas = null;
3 | var soundReady = false;
4 |
5 | var cout = console.log.bind(console);
6 | function startGame(blob) {
7 | var binaryHandle = new FileReader();
8 | binaryHandle.onload = function() {
9 | if (this.readyState === 2) {
10 | try {
11 | start(mainCanvas, this.result);
12 | } catch (e) {
13 | alert(e.message);
14 | }
15 | }
16 | };
17 | binaryHandle.readAsBinaryString(blob);
18 | }
19 |
20 | function loadViaXHR() {
21 | var xhr = new XMLHttpRequest();
22 | xhr.open("GET", romPath);
23 | xhr.responseType = "blob";
24 | xhr.onload = function() {
25 | startGame(new Blob([this.response], { type: "text/plain" }));
26 | };
27 | xhr.send();
28 | }
29 |
30 | function windowingInitialize() {
31 | cout("windowingInitialize() called.", 0);
32 | mainCanvas = document.getElementById("mainCanvas");
33 | window.onunload = autoSave;
34 | loadViaXHR();
35 | }
36 |
37 | //Wrapper for localStorage getItem, so that data can be retrieved in various types.
38 | function findValue(key) {
39 | try {
40 | if (window.localStorage.getItem(key) != null) {
41 | return JSON.parse(window.localStorage.getItem(key));
42 | }
43 | } catch (error) {
44 | //An older Gecko 1.8.1/1.9.0 method of storage (Deprecated due to the obvious security hole):
45 | if (window.globalStorage[location.hostname].getItem(key) != null) {
46 | return JSON.parse(window.globalStorage[location.hostname].getItem(key));
47 | }
48 | }
49 | return null;
50 | }
51 | //Wrapper for localStorage setItem, so that data can be set in various types.
52 | function setValue(key, value) {
53 | try {
54 | window.localStorage.setItem(key, JSON.stringify(value));
55 | } catch (error) {
56 | //An older Gecko 1.8.1/1.9.0 method of storage (Deprecated due to the obvious security hole):
57 | window.globalStorage[location.hostname].setItem(key, JSON.stringify(value));
58 | }
59 | }
60 | //Wrapper for localStorage removeItem, so that data can be set in various types.
61 | function deleteValue(key) {
62 | try {
63 | window.localStorage.removeItem(key);
64 | } catch (error) {
65 | //An older Gecko 1.8.1/1.9.0 method of storage (Deprecated due to the obvious security hole):
66 | window.globalStorage[location.hostname].removeItem(key);
67 | }
68 | }
69 |
70 | // Allow Audio context to be created in places which require
71 | // user input first. Hook this up to keyboard and touch inputs
72 | function initSound() {
73 | if (!soundReady && GameBoyEmulatorInitialized()) {
74 | window.AudioContext = window.AudioContext || window.webkitAudioContext;
75 | window.audioContext = new AudioContext();
76 | if (window.audioContext) {
77 | // Create empty buffer
78 | var buffer = window.audioContext.createBuffer(1, 1, 22050);
79 | var source = window.audioContext.createBufferSource();
80 | source.buffer = buffer;
81 | // Connect to output (speakers)
82 | source.connect(window.audioContext.destination);
83 | // Play sound
84 | if (source.start) {
85 | source.start(0);
86 | } else if (source.play) {
87 | source.play(0);
88 | } else if (source.noteOn) {
89 | source.noteOn(0);
90 | }
91 | }
92 | settings[0] = true;
93 | gameboy.initSound();
94 | soundReady = true;
95 | }
96 | }
97 |
98 | window.addEventListener("DOMContentLoaded", windowingInitialize);
99 |
--------------------------------------------------------------------------------
/js/other/base64.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var toBase64 = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
3 | "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
4 | "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+" , "/", "="];
5 | var fromBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
6 | function base64(data) {
7 | try {
8 | var base64 = window.btoa(data); //Use this native function when it's available, as it's a magnitude faster than the non-native code below.
9 | }
10 | catch (error) {
11 | //Defaulting to non-native base64 encoding...
12 | var base64 = "";
13 | var dataLength = data.length;
14 | if (dataLength > 0) {
15 | var bytes = [0, 0, 0];
16 | var index = 0;
17 | var remainder = dataLength % 3;
18 | while (data.length % 3 > 0) {
19 | //Make sure we don't do fuzzy math in the next loop...
20 | data[data.length] = " ";
21 | }
22 | while (index < dataLength) {
23 | //Keep this loop small for speed.
24 | bytes = [data.charCodeAt(index++) & 0xFF, data.charCodeAt(index++) & 0xFF, data.charCodeAt(index++) & 0xFF];
25 | base64 += toBase64[bytes[0] >> 2] + toBase64[((bytes[0] & 0x3) << 4) | (bytes[1] >> 4)] + toBase64[((bytes[1] & 0xF) << 2) | (bytes[2] >> 6)] + toBase64[bytes[2] & 0x3F];
26 | }
27 | if (remainder > 0) {
28 | //Fill in the padding and recalulate the trailing six-bit group...
29 | base64[base64.length - 1] = "=";
30 | if (remainder == 2) {
31 | base64[base64.length - 2] = "=";
32 | base64[base64.length - 3] = toBase64[(bytes[0] & 0x3) << 4];
33 | }
34 | else {
35 | base64[base64.length - 2] = toBase64[(bytes[1] & 0xF) << 2];
36 | }
37 | }
38 | }
39 | }
40 | return base64;
41 | }
42 | function base64_decode(data) {
43 | try {
44 | var decode64 = window.atob(data); //Use this native function when it's available, as it's a magnitude faster than the non-native code below.
45 | }
46 | catch (error) {
47 | //Defaulting to non-native base64 decoding...
48 | var decode64 = "";
49 | var dataLength = data.length;
50 | if (dataLength > 3 && dataLength % 4 == 0) {
51 | var sixbits = [0, 0, 0, 0]; //Declare this out of the loop, to speed up the ops.
52 | var index = 0;
53 | while (index < dataLength) {
54 | //Keep this loop small for speed.
55 | sixbits = [fromBase64.indexOf(data.charAt(index++)), fromBase64.indexOf(data.charAt(index++)), fromBase64.indexOf(data.charAt(index++)), fromBase64.indexOf(data.charAt(index++))];
56 | decode64 += String.fromCharCode((sixbits[0] << 2) | (sixbits[1] >> 4)) + String.fromCharCode(((sixbits[1] & 0x0F) << 4) | (sixbits[2] >> 2)) + String.fromCharCode(((sixbits[2] & 0x03) << 6) | sixbits[3]);
57 | }
58 | //Check for the '=' character after the loop, so we don't hose it up.
59 | if (sixbits[3] >= 0x40) {
60 | decode64.length -= 1;
61 | if (sixbits[2] >= 0x40) {
62 | decode64.length -= 1;
63 | }
64 | }
65 | }
66 | }
67 | return decode64;
68 | }
69 | function to_little_endian_dword(str) {
70 | return to_little_endian_word(str) + to_little_endian_word(str >> 16);
71 | }
72 | function to_little_endian_word(str) {
73 | return to_byte(str) + to_byte(str >> 8);
74 | }
75 | function to_byte(str) {
76 | return String.fromCharCode(str & 0xFF);
77 | }
78 | function arrayToBase64(arrayIn) {
79 | var binString = "";
80 | var length = arrayIn.length;
81 | for (var index = 0; index < length; ++index) {
82 | if (typeof arrayIn[index] == "number") {
83 | binString += String.fromCharCode(arrayIn[index]);
84 | }
85 | }
86 | return base64(binString);
87 | }
88 | function base64ToArray(b64String) {
89 | var binString = base64_decode(b64String);
90 | var outArray = [];
91 | var length = binString.length;
92 | for (var index = 0; index < length;) {
93 | outArray.push(binString.charCodeAt(index++) & 0xFF);
94 | }
95 | return outArray;
96 | }
--------------------------------------------------------------------------------
/js/other/controls.js:
--------------------------------------------------------------------------------
1 | const JS_KEY_UP = 38;
2 | const JS_KEY_LEFT = 37;
3 | const JS_KEY_RIGHT = 39;
4 | const JS_KEY_DOWN = 40;
5 | const JS_KEY_ENTER = 13;
6 | const JS_KEY_ALT = 18;
7 | const JS_KEY_CTRL = 17;
8 | const JS_KEY_SHIFT = 16;
9 |
10 | const JS_KEY_W = 87;
11 | const JS_KEY_A = 65;
12 | const JS_KEY_S = 83;
13 | const JS_KEY_D = 68;
14 | const JS_KEY_J = 74;
15 | const JS_KEY_K = 75;
16 |
17 | const JS_KEY_Z = 90;
18 | const JS_KEY_X = 88;
19 |
20 | const DEADZONE = 0.1;
21 |
22 | var isTouchEnabled = "ontouchstart" in document.documentElement;
23 | isTouchEnabled = true;
24 |
25 | var controller = document.getElementById("controller");
26 | var btnA = document.getElementById("controller_a");
27 | var btnB = document.getElementById("controller_b");
28 | var btnStart = document.getElementById("controller_start");
29 | var btnSelect = document.getElementById("controller_select");
30 | var dpad = document.getElementById("controller_dpad");
31 |
32 | function bindButton(el, code) {
33 | el.addEventListener("touchstart", function(e) {
34 | e.preventDefault();
35 | e.stopPropagation();
36 | e.currentTarget.className = e.currentTarget.className + " btnPressed";
37 | GameBoyKeyDown(code);
38 | });
39 |
40 | el.addEventListener("touchend", function(e) {
41 | e.preventDefault();
42 | e.stopPropagation();
43 | initSound();
44 | e.currentTarget.className = e.currentTarget.className.replace(
45 | / btnPressed/,
46 | ""
47 | );
48 | GameBoyKeyUp(code);
49 | });
50 | }
51 |
52 | function bindDpad(el) {
53 | el.addEventListener("touchstart", function(e) {
54 | e.preventDefault();
55 | e.stopPropagation();
56 | var rect = e.currentTarget.getBoundingClientRect();
57 | var x = (2 * (e.targetTouches[0].clientX - rect.left)) / rect.width - 1;
58 | var y = (2 * (e.targetTouches[0].clientY - rect.top)) / rect.height - 1;
59 | move(x, y);
60 | });
61 |
62 | el.addEventListener("touchmove", function(e) {
63 | e.preventDefault();
64 | e.stopPropagation();
65 | var rect = e.currentTarget.getBoundingClientRect();
66 | var x = (2 * (e.targetTouches[0].clientX - rect.left)) / rect.width - 1;
67 | var y = (2 * (e.targetTouches[0].clientY - rect.top)) / rect.height - 1;
68 | move(x, y);
69 | });
70 |
71 | function move(x, y) {
72 | if (x < -DEADZONE || x > DEADZONE) {
73 | if (y > x && y < -x) {
74 | GameBoyKeyUp("right");
75 | GameBoyKeyDown("left");
76 | } else if (y > -x && y < x) {
77 | GameBoyKeyUp("left");
78 | GameBoyKeyDown("right");
79 | }
80 |
81 | if (y > -DEADZONE && y < DEADZONE) {
82 | GameBoyKeyUp("up");
83 | GameBoyKeyUp("down");
84 | }
85 | }
86 |
87 | if (y < -DEADZONE || y > DEADZONE) {
88 | if (x > y && x < -y) {
89 | GameBoyKeyUp("down");
90 | GameBoyKeyDown("up");
91 | } else if (x > -y && x < y) {
92 | GameBoyKeyUp("up");
93 | GameBoyKeyDown("down");
94 | }
95 |
96 | if (x > -DEADZONE && x < DEADZONE) {
97 | GameBoyKeyUp("left");
98 | GameBoyKeyUp("right");
99 | }
100 | }
101 | }
102 |
103 | el.addEventListener("touchend", function(e) {
104 | e.preventDefault();
105 | e.stopPropagation();
106 | initSound();
107 | GameBoyKeyUp("left");
108 | GameBoyKeyUp("right");
109 | GameBoyKeyUp("up");
110 | GameBoyKeyUp("down");
111 | });
112 | }
113 |
114 | function bindKeyboard() {
115 | window.onkeydown = function(e) {
116 | initSound();
117 | if (isTouchEnabled) {
118 | controller.style.display = "none";
119 | isTouchEnabled = false;
120 | }
121 | if (
122 | e.keyCode !== JS_KEY_CTRL &&
123 | e.keyCode !== JS_KEY_ALT &&
124 | (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)
125 | ) {
126 | return;
127 | }
128 | if (e.keyCode === JS_KEY_LEFT || e.keyCode === JS_KEY_A) {
129 | GameBoyKeyDown("left");
130 | } else if (e.keyCode === JS_KEY_RIGHT || e.keyCode === JS_KEY_D) {
131 | GameBoyKeyDown("right");
132 | } else if (e.keyCode === JS_KEY_UP || e.keyCode === JS_KEY_W) {
133 | GameBoyKeyDown("up");
134 | } else if (e.keyCode === JS_KEY_DOWN || e.keyCode === JS_KEY_S) {
135 | GameBoyKeyDown("down");
136 | } else if (e.keyCode === JS_KEY_ENTER) {
137 | GameBoyKeyDown("start");
138 | } else if (
139 | e.keyCode === JS_KEY_ALT ||
140 | e.keyCode === JS_KEY_Z ||
141 | e.keyCode === JS_KEY_J
142 | ) {
143 | GameBoyKeyDown("a");
144 | } else if (
145 | e.keyCode === JS_KEY_CTRL ||
146 | e.keyCode === JS_KEY_K ||
147 | e.keyCode === JS_KEY_X
148 | ) {
149 | GameBoyKeyDown("b");
150 | } else if (e.keyCode === JS_KEY_SHIFT) {
151 | GameBoyKeyDown("select");
152 | }
153 | e.preventDefault();
154 | };
155 |
156 | window.onkeyup = function(e) {
157 | if (e.key === "Dead") {
158 | // Ipad keyboard fix :-/
159 | // Doesn't register which key was released, so release all of them
160 | ["right", "left", "up", "down", "a", "b", "select", "start"].forEach(
161 | key => {
162 | GameBoyKeyUp(key);
163 | }
164 | );
165 | }
166 | if (e.keyCode === JS_KEY_LEFT || e.keyCode === JS_KEY_A) {
167 | GameBoyKeyUp("left");
168 | } else if (e.keyCode === JS_KEY_RIGHT || e.keyCode === JS_KEY_D) {
169 | GameBoyKeyUp("right");
170 | } else if (e.keyCode === JS_KEY_UP || e.keyCode === JS_KEY_W) {
171 | GameBoyKeyUp("up");
172 | } else if (e.keyCode === JS_KEY_DOWN || e.keyCode === JS_KEY_S) {
173 | GameBoyKeyUp("down");
174 | } else if (e.keyCode === JS_KEY_ENTER) {
175 | GameBoyKeyUp("start");
176 | } else if (
177 | e.keyCode === JS_KEY_ALT ||
178 | e.keyCode === JS_KEY_Z ||
179 | e.keyCode === JS_KEY_J
180 | ) {
181 | GameBoyKeyUp("a");
182 | } else if (
183 | e.keyCode === JS_KEY_CTRL ||
184 | e.keyCode === JS_KEY_K ||
185 | e.keyCode === JS_KEY_X
186 | ) {
187 | GameBoyKeyUp("b");
188 | } else if (e.keyCode === JS_KEY_SHIFT) {
189 | GameBoyKeyUp("select");
190 | }
191 | e.preventDefault();
192 | };
193 | }
194 |
195 | if (isTouchEnabled) {
196 | bindButton(btnA, "a");
197 | bindButton(btnB, "b");
198 | bindButton(btnStart, "start");
199 | bindButton(btnSelect, "select");
200 | bindDpad(dpad);
201 | } else {
202 | controller.style.display = "none";
203 | }
204 | bindKeyboard();
205 |
--------------------------------------------------------------------------------
/css/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: #031921;
3 | color: #fff;
4 | font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue",
5 | Helvetica, Arial, "Lucida Grande", sans-serif;
6 | font-weight: 300;
7 | margin: 0px;
8 | padding: 0px;
9 | touch-action: none;
10 | text-align: center;
11 | -webkit-touch-callout: none;
12 | user-select: none;
13 | overflow: hidden;
14 | }
15 |
16 | #game {
17 | width: 100%;
18 | touch-action: none;
19 | text-align: center;
20 | }
21 |
22 | #game canvas {
23 | image-rendering: -moz-crisp-edges;
24 | image-rendering: -webkit-crisp-edges;
25 | image-rendering: pixelated;
26 | image-rendering: crisp-edges;
27 | }
28 |
29 | #controller {
30 | display: block;
31 | position: fixed;
32 | bottom: 0px;
33 | height: 210px;
34 | width: 100%;
35 | touch-action: none;
36 | opacity: 0.8;
37 | }
38 |
39 | #controller_dpad {
40 | position: absolute;
41 | bottom: 20px;
42 | left: 0px;
43 | width: 184px;
44 | height: 184px;
45 | }
46 |
47 | #controller_dpad:before {
48 | content: "";
49 | display: block;
50 | width: 48px;
51 | height: 48px;
52 | background: #5c5c5c;
53 | background: radial-gradient(
54 | ellipse at center,
55 | #5c5c5c 0%,
56 | #555 59%,
57 | #5c5c5c 60%
58 | );
59 | position: absolute;
60 | left: 68px;
61 | top: 68px;
62 | }
63 |
64 | #controller_left {
65 | position: absolute;
66 | left: 20px;
67 | top: 68px;
68 | width: 48px;
69 | height: 48px;
70 | background: #666;
71 | background: radial-gradient(ellipse at center, #666 0%, #5c5c5c 80%);
72 | border-top-left-radius: 4px;
73 | border-bottom-left-radius: 4px;
74 | }
75 |
76 | #controller_right {
77 | position: absolute;
78 | left: 116px;
79 | top: 68px;
80 | width: 48px;
81 | height: 48px;
82 | background: #666;
83 | background: radial-gradient(ellipse at center, #666 0%, #5c5c5c 80%);
84 | border-top-right-radius: 4px;
85 | border-bottom-right-radius: 4px;
86 | }
87 |
88 | #controller_up {
89 | position: absolute;
90 | left: 68px;
91 | top: 20px;
92 | width: 48px;
93 | height: 48px;
94 | background: #666;
95 | background: radial-gradient(ellipse at center, #666 0%, #5c5c5c 80%);
96 | border-top-left-radius: 4px;
97 | border-top-right-radius: 4px;
98 | }
99 |
100 | #controller_down {
101 | position: absolute;
102 | left: 68px;
103 | top: 116px;
104 | width: 48px;
105 | height: 48px;
106 | background: #666;
107 | background: radial-gradient(ellipse at center, #666 0%, #5c5c5c 80%);
108 | border-bottom-left-radius: 4px;
109 | border-bottom-right-radius: 4px;
110 | }
111 |
112 | #controller_a {
113 | position: absolute;
114 | bottom: 110px;
115 | right: 20px;
116 | }
117 |
118 | #controller_b {
119 | position: absolute;
120 | bottom: 80px;
121 | right: 100px;
122 | }
123 |
124 | .roundBtn {
125 | display: flex;
126 | justify-content: center;
127 | align-items: center;
128 | font-weight: bold;
129 | font-size: 32px;
130 | color: #440f1f;
131 | line-height: 64px;
132 | width: 64px;
133 | height: 64px;
134 | border-radius: 64px;
135 | background: #870a4c;
136 | background: radial-gradient(ellipse at center, #ab1465 0%, #8b1e57 100%);
137 | box-shadow: 0px 4px 5px rgba(0, 0, 0, 0.2);
138 | }
139 |
140 | .capsuleBtn {
141 | font-weight: bold;
142 | font-size: 10px;
143 | color: #111;
144 | display: flex;
145 | justify-content: center;
146 | align-items: center;
147 | line-height: 40px;
148 | text-transform: uppercase;
149 | width: 64px;
150 | height: 32px;
151 | border-radius: 40px;
152 | background: #222;
153 | background: radial-gradient(ellipse at center, #666 0%, #555 100%);
154 | box-shadow: 0px 4px 5px rgba(0, 0, 0, 0.2);
155 | }
156 |
157 | #controller_start {
158 | position: absolute;
159 | bottom: 20px;
160 | right: 15px;
161 | }
162 |
163 | #controller_select {
164 | position: absolute;
165 | bottom: 20px;
166 | right: 100px;
167 | }
168 |
169 | .btnPressed {
170 | opacity: 0.5;
171 | }
172 |
173 | .spinner {
174 | height: 50px;
175 | width: 50px;
176 | margin: 0px auto;
177 | -webkit-animation: rotation 0.8s linear infinite;
178 | -moz-animation: rotation 0.8s linear infinite;
179 | -o-animation: rotation 0.8s linear infinite;
180 | animation: rotation 0.8s linear infinite;
181 | border-left: 10px solid #306850;
182 | border-right: 10px solid #306850;
183 | border-bottom: 10px solid #306850;
184 | border-top: 10px solid #88c070;
185 | border-radius: 100%;
186 | background-color: #031921;
187 | }
188 | @-webkit-keyframes rotation {
189 | from {
190 | -webkit-transform: rotate(0deg);
191 | }
192 | to {
193 | -webkit-transform: rotate(360deg);
194 | }
195 | }
196 | @-moz-keyframes rotation {
197 | from {
198 | -moz-transform: rotate(0deg);
199 | }
200 | to {
201 | -moz-transform: rotate(360deg);
202 | }
203 | }
204 | @-o-keyframes rotation {
205 | from {
206 | -o-transform: rotate(0deg);
207 | }
208 | to {
209 | -o-transform: rotate(360deg);
210 | }
211 | }
212 | @keyframes rotation {
213 | from {
214 | transform: rotate(0deg);
215 | }
216 | to {
217 | transform: rotate(360deg);
218 | }
219 | }
220 |
221 | @media only screen and (max-width: 640px) {
222 | #game canvas {
223 | margin-top: 0px;
224 | width: 100%;
225 | max-width: 480px;
226 | border: 0px;
227 | border-radius: 0px;
228 | }
229 | }
230 |
231 | @media only screen and (max-device-width: 812px) and (orientation: portrait) {
232 | body {
233 | margin: 0;
234 | }
235 |
236 | #game {
237 | width: 100%;
238 | position: fixed;
239 | touch-action: none;
240 | }
241 |
242 | #game canvas {
243 | margin: 0;
244 | display: block;
245 | width: 100% !important;
246 | height: auto !important;
247 | }
248 | }
249 |
250 | @media only screen and (max-device-width: 320px) and (orientation: portrait) {
251 | #controller_dpad {
252 | left: 10px;
253 | }
254 |
255 | #controller_a {
256 | right: 5px;
257 | }
258 |
259 | #controller_b {
260 | right: 80px;
261 | }
262 |
263 | #controller_start {
264 | right: 5px;
265 | }
266 |
267 | #controller_select {
268 | right: 80px;
269 | }
270 | }
271 |
272 | @media only screen and (max-width: 500px) and (max-height: 500px) {
273 | #controller {
274 | display: none;
275 | }
276 | }
277 |
278 | @media only screen and (max-device-width: 812px) and (orientation: landscape) {
279 | html,
280 | body {
281 | height: 100%;
282 | }
283 | body {
284 | display: flex;
285 | justify-content: center;
286 | align-items: center;
287 | }
288 |
289 | #game:after {
290 | content: "PLEASE ROTATE ↻";
291 | font-size: 24px;
292 | font-weight: bold;
293 | color: #fff;
294 | }
295 |
296 | #game canvas {
297 | display: none;
298 | max-width: 480px;
299 | }
300 |
301 | #controller {
302 | display: none;
303 | }
304 | }
305 |
--------------------------------------------------------------------------------
/js/other/resampler.js:
--------------------------------------------------------------------------------
1 | //JavaScript Audio Resampler (c) 2011 - Grant Galitz
2 | function Resampler(fromSampleRate, toSampleRate, channels, outputBufferSize, noReturn) {
3 | this.fromSampleRate = fromSampleRate;
4 | this.toSampleRate = toSampleRate;
5 | this.channels = channels | 0;
6 | this.outputBufferSize = outputBufferSize;
7 | this.noReturn = !!noReturn;
8 | this.initialize();
9 | }
10 | Resampler.prototype.initialize = function () {
11 | //Perform some checks:
12 | if (this.fromSampleRate > 0 && this.toSampleRate > 0 && this.channels > 0) {
13 | if (this.fromSampleRate == this.toSampleRate) {
14 | //Setup a resampler bypass:
15 | this.resampler = this.bypassResampler; //Resampler just returns what was passed through.
16 | this.ratioWeight = 1;
17 | }
18 | else {
19 | this.ratioWeight = this.fromSampleRate / this.toSampleRate;
20 | if (this.fromSampleRate < this.toSampleRate) {
21 | /*
22 | Use generic linear interpolation if upsampling,
23 | as linear interpolation produces a gradient that we want
24 | and works fine with two input sample points per output in this case.
25 | */
26 | this.compileLinearInterpolationFunction();
27 | this.lastWeight = 1;
28 | }
29 | else {
30 | /*
31 | Custom resampler I wrote that doesn't skip samples
32 | like standard linear interpolation in high downsampling.
33 | This is more accurate than linear interpolation on downsampling.
34 | */
35 | this.compileMultiTapFunction();
36 | this.tailExists = false;
37 | this.lastWeight = 0;
38 | }
39 | this.initializeBuffers();
40 | }
41 | }
42 | else {
43 | throw(new Error("Invalid settings specified for the resampler."));
44 | }
45 | }
46 | Resampler.prototype.compileLinearInterpolationFunction = function () {
47 | var toCompile = "var bufferLength = buffer.length;\
48 | var outLength = this.outputBufferSize;\
49 | if ((bufferLength % " + this.channels + ") == 0) {\
50 | if (bufferLength > 0) {\
51 | var weight = this.lastWeight;\
52 | var firstWeight = 0;\
53 | var secondWeight = 0;\
54 | var sourceOffset = 0;\
55 | var outputOffset = 0;\
56 | var outputBuffer = this.outputBuffer;\
57 | for (; weight < 1; weight += " + this.ratioWeight + ") {\
58 | secondWeight = weight % 1;\
59 | firstWeight = 1 - secondWeight;";
60 | for (var channel = 0; channel < this.channels; ++channel) {
61 | toCompile += "outputBuffer[outputOffset++] = (this.lastOutput[" + channel + "] * firstWeight) + (buffer[" + channel + "] * secondWeight);";
62 | }
63 | toCompile += "}\
64 | weight -= 1;\
65 | for (bufferLength -= " + this.channels + ", sourceOffset = Math.floor(weight) * " + this.channels + "; outputOffset < outLength && sourceOffset < bufferLength;) {\
66 | secondWeight = weight % 1;\
67 | firstWeight = 1 - secondWeight;";
68 | for (var channel = 0; channel < this.channels; ++channel) {
69 | toCompile += "outputBuffer[outputOffset++] = (buffer[sourceOffset" + ((channel > 0) ? (" + " + channel) : "") + "] * firstWeight) + (buffer[sourceOffset + " + (this.channels + channel) + "] * secondWeight);";
70 | }
71 | toCompile += "weight += " + this.ratioWeight + ";\
72 | sourceOffset = Math.floor(weight) * " + this.channels + ";\
73 | }";
74 | for (var channel = 0; channel < this.channels; ++channel) {
75 | toCompile += "this.lastOutput[" + channel + "] = buffer[sourceOffset++];";
76 | }
77 | toCompile += "this.lastWeight = weight % 1;\
78 | return this.bufferSlice(outputOffset);\
79 | }\
80 | else {\
81 | return (this.noReturn) ? 0 : [];\
82 | }\
83 | }\
84 | else {\
85 | throw(new Error(\"Buffer was of incorrect sample length.\"));\
86 | }";
87 | this.resampler = Function("buffer", toCompile);
88 | }
89 | Resampler.prototype.compileMultiTapFunction = function () {
90 | var toCompile = "var bufferLength = buffer.length;\
91 | var outLength = this.outputBufferSize;\
92 | if ((bufferLength % " + this.channels + ") == 0) {\
93 | if (bufferLength > 0) {\
94 | var weight = 0;";
95 | for (var channel = 0; channel < this.channels; ++channel) {
96 | toCompile += "var output" + channel + " = 0;"
97 | }
98 | toCompile += "var actualPosition = 0;\
99 | var amountToNext = 0;\
100 | var alreadyProcessedTail = !this.tailExists;\
101 | this.tailExists = false;\
102 | var outputBuffer = this.outputBuffer;\
103 | var outputOffset = 0;\
104 | var currentPosition = 0;\
105 | do {\
106 | if (alreadyProcessedTail) {\
107 | weight = " + this.ratioWeight + ";";
108 | for (channel = 0; channel < this.channels; ++channel) {
109 | toCompile += "output" + channel + " = 0;"
110 | }
111 | toCompile += "}\
112 | else {\
113 | weight = this.lastWeight;";
114 | for (channel = 0; channel < this.channels; ++channel) {
115 | toCompile += "output" + channel + " = this.lastOutput[" + channel + "];"
116 | }
117 | toCompile += "alreadyProcessedTail = true;\
118 | }\
119 | while (weight > 0 && actualPosition < bufferLength) {\
120 | amountToNext = 1 + actualPosition - currentPosition;\
121 | if (weight >= amountToNext) {";
122 | for (channel = 0; channel < this.channels; ++channel) {
123 | toCompile += "output" + channel + " += buffer[actualPosition++] * amountToNext;"
124 | }
125 | toCompile += "currentPosition = actualPosition;\
126 | weight -= amountToNext;\
127 | }\
128 | else {";
129 | for (channel = 0; channel < this.channels; ++channel) {
130 | toCompile += "output" + channel + " += buffer[actualPosition" + ((channel > 0) ? (" + " + channel) : "") + "] * weight;"
131 | }
132 | toCompile += "currentPosition += weight;\
133 | weight = 0;\
134 | break;\
135 | }\
136 | }\
137 | if (weight <= 0) {";
138 | for (channel = 0; channel < this.channels; ++channel) {
139 | toCompile += "outputBuffer[outputOffset++] = output" + channel + " / " + this.ratioWeight + ";"
140 | }
141 | toCompile += "}\
142 | else {\
143 | this.lastWeight = weight;";
144 | for (channel = 0; channel < this.channels; ++channel) {
145 | toCompile += "this.lastOutput[" + channel + "] = output" + channel + ";"
146 | }
147 | toCompile += "this.tailExists = true;\
148 | break;\
149 | }\
150 | } while (actualPosition < bufferLength && outputOffset < outLength);\
151 | return this.bufferSlice(outputOffset);\
152 | }\
153 | else {\
154 | return (this.noReturn) ? 0 : [];\
155 | }\
156 | }\
157 | else {\
158 | throw(new Error(\"Buffer was of incorrect sample length.\"));\
159 | }";
160 | this.resampler = Function("buffer", toCompile);
161 | }
162 | Resampler.prototype.bypassResampler = function (buffer) {
163 | if (this.noReturn) {
164 | //Set the buffer passed as our own, as we don't need to resample it:
165 | this.outputBuffer = buffer;
166 | return buffer.length;
167 | }
168 | else {
169 | //Just return the buffer passsed:
170 | return buffer;
171 | }
172 | }
173 | Resampler.prototype.bufferSlice = function (sliceAmount) {
174 | if (this.noReturn) {
175 | //If we're going to access the properties directly from this object:
176 | return sliceAmount;
177 | }
178 | else {
179 | //Typed array and normal array buffer section referencing:
180 | try {
181 | return this.outputBuffer.subarray(0, sliceAmount);
182 | }
183 | catch (error) {
184 | try {
185 | //Regular array pass:
186 | this.outputBuffer.length = sliceAmount;
187 | return this.outputBuffer;
188 | }
189 | catch (error) {
190 | //Nightly Firefox 4 used to have the subarray function named as slice:
191 | return this.outputBuffer.slice(0, sliceAmount);
192 | }
193 | }
194 | }
195 | }
196 | Resampler.prototype.initializeBuffers = function () {
197 | //Initialize the internal buffer:
198 | try {
199 | this.outputBuffer = new Float32Array(this.outputBufferSize);
200 | this.lastOutput = new Float32Array(this.channels);
201 | }
202 | catch (error) {
203 | this.outputBuffer = [];
204 | this.lastOutput = [];
205 | }
206 | }
--------------------------------------------------------------------------------
/js/other/swfobject.js:
--------------------------------------------------------------------------------
1 | /* SWFObject v2.2
2 | is released under the MIT License
3 | */
4 | var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O.ActiveXObject!=D){try{var ad=new ActiveXObject(W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y0){for(var af=0;af0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad'}}aa.outerHTML='";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab 0) {
94 | cout("Saving the SRAM...", 0);
95 | if (findValue("SRAM_" + gameboy.name) != null) {
96 | //Remove the outdated storage format save:
97 | cout("Deleting the old SRAM save due to outdated format.", 0);
98 | deleteValue("SRAM_" + gameboy.name);
99 | }
100 | setValue("B64_SRAM_" + gameboy.name, arrayToBase64(sram));
101 | }
102 | else {
103 | cout("SRAM could not be saved because it was empty.", 1);
104 | }
105 | }
106 | catch (error) {
107 | cout("Could not save the current emulation state(\"" + error.message + "\").", 2);
108 | }
109 | }
110 | else {
111 | cout("Cannot save a game that does not have battery backed SRAM specified.", 1);
112 | }
113 | saveRTC();
114 | }
115 | else {
116 | cout("GameBoy core cannot be saved while it has not been initialized.", 1);
117 | }
118 | }
119 | function saveRTC() { //Execute this when SRAM is being saved as well.
120 | if (GameBoyEmulatorInitialized()) {
121 | if (gameboy.cTIMER) {
122 | try {
123 | cout("Saving the RTC...", 0);
124 | setValue("RTC_" + gameboy.name, gameboy.saveRTCState());
125 | }
126 | catch (error) {
127 | cout("Could not save the RTC of the current emulation state(\"" + error.message + "\").", 2);
128 | }
129 | }
130 | }
131 | else {
132 | cout("GameBoy core cannot be saved while it has not been initialized.", 1);
133 | }
134 | }
135 | function autoSave() {
136 | if (GameBoyEmulatorInitialized()) {
137 | cout("Automatically saving the SRAM.", 0);
138 | saveSRAM();
139 | saveRTC();
140 | }
141 | }
142 | function openSRAM(filename) {
143 | try {
144 | if (findValue("B64_SRAM_" + filename) != null) {
145 | cout("Found a previous SRAM state (Will attempt to load).", 0);
146 | return base64ToArray(findValue("B64_SRAM_" + filename));
147 | }
148 | else if (findValue("SRAM_" + filename) != null) {
149 | cout("Found a previous SRAM state (Will attempt to load).", 0);
150 | return findValue("SRAM_" + filename);
151 | }
152 | else {
153 | cout("Could not find any previous SRAM copy for the current ROM.", 0);
154 | }
155 | }
156 | catch (error) {
157 | cout("Could not open the SRAM of the saved emulation state.", 2);
158 | }
159 | return [];
160 | }
161 | function openRTC(filename) {
162 | try {
163 | if (findValue("RTC_" + filename) != null) {
164 | cout("Found a previous RTC state (Will attempt to load).", 0);
165 | return findValue("RTC_" + filename);
166 | }
167 | else {
168 | cout("Could not find any previous RTC copy for the current ROM.", 0);
169 | }
170 | }
171 | catch (error) {
172 | cout("Could not open the RTC data of the saved emulation state.", 2);
173 | }
174 | return [];
175 | }
176 | function saveState(filename) {
177 | if (GameBoyEmulatorInitialized()) {
178 | try {
179 | setValue(filename, gameboy.saveState());
180 | cout("Saved the current state as: " + filename, 0);
181 | }
182 | catch (error) {
183 | cout("Could not save the current emulation state(\"" + error.message + "\").", 2);
184 | }
185 | }
186 | else {
187 | cout("GameBoy core cannot be saved while it has not been initialized.", 1);
188 | }
189 | }
190 | function openState(filename, canvas) {
191 | try {
192 | if (findValue(filename) != null) {
193 | try {
194 | clearLastEmulation();
195 | cout("Attempting to run a saved emulation state.", 0);
196 | gameboy = new GameBoyCore(canvas, "");
197 | gameboy.savedStateFileName = filename;
198 | gameboy.returnFromState(findValue(filename));
199 | run();
200 | }
201 | catch (error) {
202 | alert(error.message + " file: " + error.fileName + " line: " + error.lineNumber);
203 | }
204 | }
205 | else {
206 | cout("Could not find the save state " + filename + "\".", 2);
207 | }
208 | }
209 | catch (error) {
210 | cout("Could not open the saved emulation state.", 2);
211 | }
212 | }
213 | function import_save(blobData) {
214 | blobData = decodeBlob(blobData);
215 | if (blobData && blobData.blobs) {
216 | if (blobData.blobs.length > 0) {
217 | for (var index = 0; index < blobData.blobs.length; ++index) {
218 | cout("Importing blob \"" + blobData.blobs[index].blobID + "\"", 0);
219 | if (blobData.blobs[index].blobContent) {
220 | if (blobData.blobs[index].blobID.substring(0, 5) == "SRAM_") {
221 | setValue("B64_" + blobData.blobs[index].blobID, base64(blobData.blobs[index].blobContent));
222 | }
223 | else {
224 | setValue(blobData.blobs[index].blobID, JSON.parse(blobData.blobs[index].blobContent));
225 | }
226 | }
227 | else if (blobData.blobs[index].blobID) {
228 | cout("Save file imported had blob \"" + blobData.blobs[index].blobID + "\" with no blob data interpretable.", 2);
229 | }
230 | else {
231 | cout("Blob chunk information missing completely.", 2);
232 | }
233 | }
234 | }
235 | else {
236 | cout("Could not decode the imported file.", 2);
237 | }
238 | }
239 | else {
240 | cout("Could not decode the imported file.", 2);
241 | }
242 | }
243 | function generateBlob(keyName, encodedData) {
244 | //Append the file format prefix:
245 | var saveString = "EMULATOR_DATA";
246 | var consoleID = "GameBoy";
247 | //Figure out the length:
248 | var totalLength = (saveString.length + 4 + (1 + consoleID.length)) + ((1 + keyName.length) + (4 + encodedData.length));
249 | //Append the total length in bytes:
250 | saveString += to_little_endian_dword(totalLength);
251 | //Append the console ID text's length:
252 | saveString += to_byte(consoleID.length);
253 | //Append the console ID text:
254 | saveString += consoleID;
255 | //Append the blob ID:
256 | saveString += to_byte(keyName.length);
257 | saveString += keyName;
258 | //Now append the save data:
259 | saveString += to_little_endian_dword(encodedData.length);
260 | saveString += encodedData;
261 | return saveString;
262 | }
263 | function generateMultiBlob(blobPairs) {
264 | var consoleID = "GameBoy";
265 | //Figure out the initial length:
266 | var totalLength = 13 + 4 + 1 + consoleID.length;
267 | //Append the console ID text's length:
268 | var saveString = to_byte(consoleID.length);
269 | //Append the console ID text:
270 | saveString += consoleID;
271 | var keyName = "";
272 | var encodedData = "";
273 | //Now append all the blobs:
274 | for (var index = 0; index < blobPairs.length; ++index) {
275 | keyName = blobPairs[index][0];
276 | encodedData = blobPairs[index][1];
277 | //Append the blob ID:
278 | saveString += to_byte(keyName.length);
279 | saveString += keyName;
280 | //Now append the save data:
281 | saveString += to_little_endian_dword(encodedData.length);
282 | saveString += encodedData;
283 | //Update the total length:
284 | totalLength += 1 + keyName.length + 4 + encodedData.length;
285 | }
286 | //Now add the prefix:
287 | saveString = "EMULATOR_DATA" + to_little_endian_dword(totalLength) + saveString;
288 | return saveString;
289 | }
290 | function decodeBlob(blobData) {
291 | /*Format is as follows:
292 | - 13 byte string "EMULATOR_DATA"
293 | - 4 byte total size (including these 4 bytes).
294 | - 1 byte Console type ID length
295 | - Console type ID text of 8 bit size
296 | blobs {
297 | - 1 byte blob ID length
298 | - blob ID text (Used to say what the data is (SRAM/freeze state/etc...))
299 | - 4 byte blob length
300 | - blob length of 32 bit size
301 | }
302 | */
303 | var length = blobData.length;
304 | var blobProperties = {};
305 | blobProperties.consoleID = null;
306 | var blobsCount = -1;
307 | blobProperties.blobs = [];
308 | if (length > 17) {
309 | if (blobData.substring(0, 13) == "EMULATOR_DATA") {
310 | var length = Math.min(((blobData.charCodeAt(16) & 0xFF) << 24) | ((blobData.charCodeAt(15) & 0xFF) << 16) | ((blobData.charCodeAt(14) & 0xFF) << 8) | (blobData.charCodeAt(13) & 0xFF), length);
311 | var consoleIDLength = blobData.charCodeAt(17) & 0xFF;
312 | if (length > 17 + consoleIDLength) {
313 | blobProperties.consoleID = blobData.substring(18, 18 + consoleIDLength);
314 | var blobIDLength = 0;
315 | var blobLength = 0;
316 | for (var index = 18 + consoleIDLength; index < length;) {
317 | blobIDLength = blobData.charCodeAt(index++) & 0xFF;
318 | if (index + blobIDLength < length) {
319 | blobProperties.blobs[++blobsCount] = {};
320 | blobProperties.blobs[blobsCount].blobID = blobData.substring(index, index + blobIDLength);
321 | index += blobIDLength;
322 | if (index + 4 < length) {
323 | blobLength = ((blobData.charCodeAt(index + 3) & 0xFF) << 24) | ((blobData.charCodeAt(index + 2) & 0xFF) << 16) | ((blobData.charCodeAt(index + 1) & 0xFF) << 8) | (blobData.charCodeAt(index) & 0xFF);
324 | index += 4;
325 | if (index + blobLength <= length) {
326 | blobProperties.blobs[blobsCount].blobContent = blobData.substring(index, index + blobLength);
327 | index += blobLength;
328 | }
329 | else {
330 | cout("Blob length check failed, blob determined to be incomplete.", 2);
331 | break;
332 | }
333 | }
334 | else {
335 | cout("Blob was incomplete, bailing out.", 2);
336 | break;
337 | }
338 | }
339 | else {
340 | cout("Blob was incomplete, bailing out.", 2);
341 | break;
342 | }
343 | }
344 | }
345 | }
346 | }
347 | return blobProperties;
348 | }
349 | function matchKey(key) { //Maps a keyboard key to a gameboy key.
350 | //Order: Right, Left, Up, Down, A, B, Select, Start
351 | var keymap = ["right", "left", "up", "down", "a", "b", "select", "start"]; //Keyboard button map.
352 | for (var index = 0; index < keymap.length; index++) {
353 | if (keymap[index] == key) {
354 | return index;
355 | }
356 | }
357 | return -1;
358 | }
359 | function GameBoyEmulatorInitialized() {
360 | return (typeof gameboy == "object" && gameboy != null);
361 | }
362 | function GameBoyEmulatorPlaying() {
363 | return ((gameboy.stopEmulator & 2) == 0);
364 | }
365 | function GameBoyKeyDown(key) {
366 | if (GameBoyEmulatorInitialized() && GameBoyEmulatorPlaying()) {
367 | GameBoyJoyPadEvent(matchKey(key), true);
368 | }
369 | }
370 | function GameBoyJoyPadEvent(keycode, down) {
371 | if (GameBoyEmulatorInitialized() && GameBoyEmulatorPlaying()) {
372 | if (keycode >= 0 && keycode < 8) {
373 | gameboy.JoyPadEvent(keycode, down);
374 | }
375 | }
376 | }
377 | function GameBoyKeyUp(key) {
378 | if (GameBoyEmulatorInitialized() && GameBoyEmulatorPlaying()) {
379 | GameBoyJoyPadEvent(matchKey(key), false);
380 | }
381 | }
382 | function GameBoyGyroSignalHandler(e) {
383 | if (GameBoyEmulatorInitialized() && GameBoyEmulatorPlaying()) {
384 | if (e.gamma || e.beta) {
385 | gameboy.GyroEvent(e.gamma * Math.PI / 180, e.beta * Math.PI / 180);
386 | }
387 | else {
388 | gameboy.GyroEvent(e.x, e.y);
389 | }
390 | try {
391 | e.preventDefault();
392 | }
393 | catch (error) { }
394 | }
395 | }
396 | //The emulator will call this to sort out the canvas properties for (re)initialization.
397 | function initNewCanvas() {
398 | if (GameBoyEmulatorInitialized()) {
399 | gameboy.canvas.width = 160;
400 | gameboy.canvas.height = 144;
401 | }
402 | }
403 | //Call this when resizing the canvas:
404 | function initNewCanvasSize() {
405 | if (GameBoyEmulatorInitialized()) {
406 | if (!settings[12]) {
407 | if (gameboy.onscreenWidth != 160 || gameboy.onscreenHeight != 144) {
408 | gameboy.initLCD();
409 | }
410 | }
411 | else {
412 | if (gameboy.onscreenWidth != gameboy.canvas.clientWidth || gameboy.onscreenHeight != gameboy.canvas.clientHeight) {
413 | gameboy.initLCD();
414 | }
415 | }
416 | }
417 | }
418 |
--------------------------------------------------------------------------------
/js/other/XAudioServer.js:
--------------------------------------------------------------------------------
1 | //2010-2013 Grant Galitz - XAudioJS realtime audio output compatibility library:
2 | var XAudioJSscriptsHandle = document.getElementsByTagName("script");
3 | var XAudioJSsourceHandle = XAudioJSscriptsHandle[XAudioJSscriptsHandle.length-1].src;
4 | function XAudioServer(channels, sampleRate, minBufferSize, maxBufferSize, underRunCallback, volume, failureCallback) {
5 | XAudioJSChannelsAllocated = Math.max(channels, 1);
6 | this.XAudioJSSampleRate = Math.abs(sampleRate);
7 | XAudioJSMinBufferSize = (minBufferSize >= (XAudioJSSamplesPerCallback * XAudioJSChannelsAllocated) && minBufferSize < maxBufferSize) ? (minBufferSize & (-XAudioJSChannelsAllocated)) : (XAudioJSSamplesPerCallback * XAudioJSChannelsAllocated);
8 | XAudioJSMaxBufferSize = (Math.floor(maxBufferSize) > XAudioJSMinBufferSize + XAudioJSChannelsAllocated) ? (maxBufferSize & (-XAudioJSChannelsAllocated)) : (XAudioJSMinBufferSize * XAudioJSChannelsAllocated);
9 | this.underRunCallback = (typeof underRunCallback == "function") ? underRunCallback : function () {};
10 | XAudioJSVolume = (volume >= 0 && volume <= 1) ? volume : 1;
11 | this.failureCallback = (typeof failureCallback == "function") ? failureCallback : function () { throw(new Error("XAudioJS has encountered a fatal error.")); };
12 | this.initializeAudio();
13 | }
14 | XAudioServer.prototype.MOZWriteAudioNoCallback = function (buffer) {
15 | //Resample before passing to the moz audio api:
16 | var bufferLength = buffer.length;
17 | for (var bufferIndex = 0; bufferIndex < bufferLength;) {
18 | var sliceLength = Math.min(bufferLength - bufferIndex, XAudioJSMaxBufferSize);
19 | for (var sliceIndex = 0; sliceIndex < sliceLength; ++sliceIndex) {
20 | XAudioJSAudioContextSampleBuffer[sliceIndex] = buffer[bufferIndex++];
21 | }
22 | var resampleLength = XAudioJSResampleControl.resampler(XAudioJSGetArraySlice(XAudioJSAudioContextSampleBuffer, sliceIndex));
23 | if (resampleLength > 0) {
24 | var resampledResult = XAudioJSResampleControl.outputBuffer;
25 | var resampledBuffer = XAudioJSGetArraySlice(resampledResult, resampleLength);
26 | this.samplesAlreadyWritten += this.audioHandleMoz.mozWriteAudio(resampledBuffer);
27 | }
28 | }
29 | }
30 | XAudioServer.prototype.callbackBasedWriteAudioNoCallback = function (buffer) {
31 | //Callback-centered audio APIs:
32 | var length = buffer.length;
33 | for (var bufferCounter = 0; bufferCounter < length && XAudioJSAudioBufferSize < XAudioJSMaxBufferSize;) {
34 | XAudioJSAudioContextSampleBuffer[XAudioJSAudioBufferSize++] = buffer[bufferCounter++];
35 | }
36 | }
37 | /*Pass your samples into here!
38 | Pack your samples as a one-dimenional array
39 | With the channel samples packed uniformly.
40 | examples:
41 | mono - [left, left, left, left]
42 | stereo - [left, right, left, right, left, right, left, right]
43 | */
44 | XAudioServer.prototype.writeAudio = function (buffer) {
45 | switch (this.audioType) {
46 | case 0:
47 | this.MOZWriteAudioNoCallback(buffer);
48 | this.MOZExecuteCallback();
49 | break;
50 | case 2:
51 | this.checkFlashInit();
52 | case 1:
53 | case 3:
54 | this.callbackBasedWriteAudioNoCallback(buffer);
55 | this.callbackBasedExecuteCallback();
56 | break;
57 | default:
58 | this.failureCallback();
59 | }
60 | }
61 | /*Pass your samples into here if you don't want automatic callback calling:
62 | Pack your samples as a one-dimenional array
63 | With the channel samples packed uniformly.
64 | examples:
65 | mono - [left, left, left, left]
66 | stereo - [left, right, left, right, left, right, left, right]
67 | Useful in preventing infinite recursion issues with calling writeAudio inside your callback.
68 | */
69 | XAudioServer.prototype.writeAudioNoCallback = function (buffer) {
70 | switch (this.audioType) {
71 | case 0:
72 | this.MOZWriteAudioNoCallback(buffer);
73 | break;
74 | case 2:
75 | this.checkFlashInit();
76 | case 1:
77 | case 3:
78 | this.callbackBasedWriteAudioNoCallback(buffer);
79 | break;
80 | default:
81 | this.failureCallback();
82 | }
83 | }
84 | //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...)
85 | //If null is returned, then that means metric could not be done.
86 | XAudioServer.prototype.remainingBuffer = function () {
87 | switch (this.audioType) {
88 | case 0:
89 | return Math.floor((this.samplesAlreadyWritten - this.audioHandleMoz.mozCurrentSampleOffset()) * XAudioJSResampleControl.ratioWeight / XAudioJSChannelsAllocated) * XAudioJSChannelsAllocated;
90 | case 2:
91 | this.checkFlashInit();
92 | case 1:
93 | case 3:
94 | return (Math.floor((XAudioJSResampledSamplesLeft() * XAudioJSResampleControl.ratioWeight) / XAudioJSChannelsAllocated) * XAudioJSChannelsAllocated) + XAudioJSAudioBufferSize;
95 | default:
96 | this.failureCallback();
97 | return null;
98 | }
99 | }
100 | XAudioServer.prototype.MOZExecuteCallback = function () {
101 | //mozAudio:
102 | var samplesRequested = XAudioJSMinBufferSize - this.remainingBuffer();
103 | if (samplesRequested > 0) {
104 | this.MOZWriteAudioNoCallback(this.underRunCallback(samplesRequested));
105 | }
106 | }
107 | XAudioServer.prototype.callbackBasedExecuteCallback = function () {
108 | //WebKit /Flash Audio:
109 | var samplesRequested = XAudioJSMinBufferSize - this.remainingBuffer();
110 | if (samplesRequested > 0) {
111 | this.callbackBasedWriteAudioNoCallback(this.underRunCallback(samplesRequested));
112 | }
113 | }
114 | //If you just want your callback called for any possible refill (Execution of callback is still conditional):
115 | XAudioServer.prototype.executeCallback = function () {
116 | switch (this.audioType) {
117 | case 0:
118 | this.MOZExecuteCallback();
119 | break;
120 | case 2:
121 | this.checkFlashInit();
122 | case 1:
123 | case 3:
124 | this.callbackBasedExecuteCallback();
125 | break;
126 | default:
127 | this.failureCallback();
128 | }
129 | }
130 | //DO NOT CALL THIS, the lib calls this internally!
131 | XAudioServer.prototype.initializeAudio = function () {
132 | try {
133 | this.initializeMozAudio();
134 | }
135 | catch (error) {
136 | try {
137 | this.initializeWebAudio();
138 | }
139 | catch (error) {
140 | try {
141 | this.initializeMediaStream();
142 | }
143 | catch (error) {
144 | try {
145 | this.initializeFlashAudio();
146 | }
147 | catch (error) {
148 | this.audioType = -1;
149 | this.failureCallback();
150 | }
151 | }
152 | }
153 | }
154 | }
155 | XAudioServer.prototype.initializeMediaStream = function () {
156 | this.audioHandleMediaStream = new Audio();
157 | this.resetCallbackAPIAudioBuffer(XAudioJSMediaStreamSampleRate);
158 | if (XAudioJSMediaStreamWorker) {
159 | //WebWorker is not GC'd, so manually collect it:
160 | XAudioJSMediaStreamWorker.terminate();
161 | }
162 | XAudioJSMediaStreamWorker = new Worker(XAudioJSsourceHandle.substring(0, XAudioJSsourceHandle.length - 3) + "MediaStreamWorker.js");
163 | this.audioHandleMediaStreamProcessing = new ProcessedMediaStream(XAudioJSMediaStreamWorker, XAudioJSMediaStreamSampleRate, XAudioJSChannelsAllocated);
164 | this.audioHandleMediaStream.src = this.audioHandleMediaStreamProcessing;
165 | this.audioHandleMediaStream.volume = XAudioJSVolume;
166 | XAudioJSMediaStreamWorker.onmessage = XAudioJSMediaStreamPushAudio;
167 | XAudioJSMediaStreamWorker.postMessage([1, XAudioJSResampleBufferSize, XAudioJSChannelsAllocated]);
168 | this.audioHandleMediaStream.play();
169 | this.audioType = 3;
170 | }
171 | XAudioServer.prototype.initializeMozAudio = function () {
172 | this.audioHandleMoz = new Audio();
173 | this.audioHandleMoz.mozSetup(XAudioJSChannelsAllocated, XAudioJSMozAudioSampleRate);
174 | this.audioHandleMoz.volume = XAudioJSVolume;
175 | this.samplesAlreadyWritten = 0;
176 | this.audioType = 0;
177 | //if (navigator.platform != "MacIntel" && navigator.platform != "MacPPC") {
178 | //Add some additional buffering space to workaround a moz audio api issue:
179 | var bufferAmount = (this.XAudioJSSampleRate * XAudioJSChannelsAllocated / 10) | 0;
180 | bufferAmount -= bufferAmount % XAudioJSChannelsAllocated;
181 | this.samplesAlreadyWritten -= bufferAmount;
182 | //}
183 | this.initializeResampler(XAudioJSMozAudioSampleRate);
184 | }
185 | XAudioServer.prototype.initializeWebAudio = function () {
186 | if (!XAudioJSWebAudioLaunchedContext) {
187 | try {
188 | XAudioJSWebAudioContextHandle = new AudioContext(); //Create a system audio context.
189 | }
190 | catch (error) {
191 | XAudioJSWebAudioContextHandle = new webkitAudioContext(); //Create a system audio context.
192 | }
193 | XAudioJSWebAudioLaunchedContext = true;
194 | }
195 | if (XAudioJSWebAudioAudioNode) {
196 | XAudioJSWebAudioAudioNode.disconnect();
197 | XAudioJSWebAudioAudioNode.onaudioprocess = null;
198 | XAudioJSWebAudioAudioNode = null;
199 | }
200 | try {
201 | XAudioJSWebAudioAudioNode = XAudioJSWebAudioContextHandle.createScriptProcessor(XAudioJSSamplesPerCallback, 0, XAudioJSChannelsAllocated); //Create the js event node.
202 | }
203 | catch (error) {
204 | XAudioJSWebAudioAudioNode = XAudioJSWebAudioContextHandle.createJavaScriptNode(XAudioJSSamplesPerCallback, 0, XAudioJSChannelsAllocated); //Create the js event node.
205 | }
206 | XAudioJSWebAudioAudioNode.onaudioprocess = XAudioJSWebAudioEvent; //Connect the audio processing event to a handling function so we can manipulate output
207 | XAudioJSWebAudioAudioNode.connect(XAudioJSWebAudioContextHandle.destination); //Send and chain the output of the audio manipulation to the system audio output.
208 | this.resetCallbackAPIAudioBuffer(XAudioJSWebAudioContextHandle.sampleRate);
209 | this.audioType = 1;
210 | /*
211 | Firefox has a bug in its web audio implementation...
212 | The node may randomly stop playing on Mac OS X for no
213 | good reason. Keep a watchdog timer to restart the failed
214 | node if it glitches. Google Chrome never had this issue.
215 | */
216 | XAudioJSWebAudioWatchDogLast = (new Date()).getTime();
217 | if (navigator.userAgent.indexOf('Gecko/') > -1) {
218 | if (XAudioJSWebAudioWatchDogTimer) {
219 | clearInterval(XAudioJSWebAudioWatchDogTimer);
220 | }
221 | var parentObj = this;
222 | XAudioJSWebAudioWatchDogTimer = setInterval(function () {
223 | var timeDiff = (new Date()).getTime() - XAudioJSWebAudioWatchDogLast;
224 | if (timeDiff > 500) {
225 | parentObj.initializeWebAudio();
226 | }
227 | }, 500);
228 | }
229 | }
230 | XAudioServer.prototype.initializeFlashAudio = function () {
231 | var existingFlashload = document.getElementById("XAudioJS");
232 | this.flashInitialized = false;
233 | this.resetCallbackAPIAudioBuffer(44100);
234 | switch (XAudioJSChannelsAllocated) {
235 | case 1:
236 | XAudioJSFlashTransportEncoder = XAudioJSGenerateFlashMonoString;
237 | break;
238 | case 2:
239 | XAudioJSFlashTransportEncoder = XAudioJSGenerateFlashStereoString;
240 | break;
241 | default:
242 | XAudioJSFlashTransportEncoder = XAudioJSGenerateFlashSurroundString;
243 | }
244 | if (existingFlashload == null) {
245 | this.audioHandleFlash = null;
246 | var thisObj = this;
247 | var mainContainerNode = document.createElement("div");
248 | mainContainerNode.setAttribute("style", "position: fixed; bottom: 0px; right: 0px; margin: 0px; padding: 0px; border: none; width: 8px; height: 8px; overflow: hidden; z-index: -1000; ");
249 | var containerNode = document.createElement("div");
250 | containerNode.setAttribute("style", "position: static; border: none; width: 0px; height: 0px; visibility: hidden; margin: 8px; padding: 0px;");
251 | containerNode.setAttribute("id", "XAudioJS");
252 | mainContainerNode.appendChild(containerNode);
253 | document.getElementsByTagName("body")[0].appendChild(mainContainerNode);
254 | swfobject.embedSWF(
255 | XAudioJSsourceHandle.substring(0, XAudioJSsourceHandle.length - 9) + "JS.swf",
256 | "XAudioJS",
257 | "8",
258 | "8",
259 | "9.0.0",
260 | "",
261 | {},
262 | {"allowscriptaccess":"always"},
263 | {"style":"position: static; visibility: hidden; margin: 8px; padding: 0px; border: none"},
264 | function (event) {
265 | if (event.success) {
266 | thisObj.audioHandleFlash = event.ref;
267 | thisObj.checkFlashInit();
268 | }
269 | else {
270 | thisObj.failureCallback();
271 | thisObj.audioType = -1;
272 | }
273 | }
274 | );
275 | }
276 | else {
277 | this.audioHandleFlash = existingFlashload;
278 | this.checkFlashInit();
279 | }
280 | this.audioType = 2;
281 | }
282 | XAudioServer.prototype.changeVolume = function (newVolume) {
283 | if (newVolume >= 0 && newVolume <= 1) {
284 | XAudioJSVolume = newVolume;
285 | switch (this.audioType) {
286 | case 0:
287 | this.audioHandleMoz.volume = XAudioJSVolume;
288 | case 1:
289 | break;
290 | case 2:
291 | if (this.flashInitialized) {
292 | this.audioHandleFlash.changeVolume(XAudioJSVolume);
293 | }
294 | else {
295 | this.checkFlashInit();
296 | }
297 | break;
298 | case 3:
299 | this.audioHandleMediaStream.volume = XAudioJSVolume;
300 | break;
301 | default:
302 | this.failureCallback();
303 | }
304 | }
305 | }
306 | //Checks to see if the NPAPI Adobe Flash bridge is ready yet:
307 | XAudioServer.prototype.checkFlashInit = function () {
308 | if (!this.flashInitialized) {
309 | try {
310 | if (this.audioHandleFlash && this.audioHandleFlash.initialize) {
311 | this.flashInitialized = true;
312 | this.audioHandleFlash.initialize(XAudioJSChannelsAllocated, XAudioJSVolume);
313 | }
314 | }
315 | catch (error) {
316 | this.flashInitialized = false;
317 | }
318 | }
319 | }
320 | //Set up the resampling:
321 | XAudioServer.prototype.resetCallbackAPIAudioBuffer = function (APISampleRate) {
322 | XAudioJSAudioBufferSize = XAudioJSResampleBufferEnd = XAudioJSResampleBufferStart = 0;
323 | this.initializeResampler(APISampleRate);
324 | XAudioJSResampledBuffer = this.getFloat32(XAudioJSResampleBufferSize);
325 | }
326 | XAudioServer.prototype.initializeResampler = function (sampleRate) {
327 | XAudioJSAudioContextSampleBuffer = this.getFloat32(XAudioJSMaxBufferSize);
328 | XAudioJSResampleBufferSize = Math.max(XAudioJSMaxBufferSize * Math.ceil(sampleRate / this.XAudioJSSampleRate) + XAudioJSChannelsAllocated, XAudioJSSamplesPerCallback * XAudioJSChannelsAllocated);
329 | XAudioJSResampleControl = new Resampler(this.XAudioJSSampleRate, sampleRate, XAudioJSChannelsAllocated, XAudioJSResampleBufferSize, true);
330 | }
331 | XAudioServer.prototype.getFloat32 = function (size) {
332 | try {
333 | return new Float32Array(size);
334 | }
335 | catch (error) {
336 | return [];
337 | }
338 | }
339 | function XAudioJSFlashAudioEvent() { //The callback that flash calls...
340 | XAudioJSResampleRefill();
341 | return XAudioJSFlashTransportEncoder();
342 | }
343 | function XAudioJSGenerateFlashSurroundString() { //Convert the arrays to one long string for speed.
344 | var XAudioJSTotalSamples = XAudioJSSamplesPerCallback << 1;
345 | if (XAudioJSBinaryString.length > XAudioJSTotalSamples) {
346 | XAudioJSBinaryString = [];
347 | }
348 | XAudioJSTotalSamples = 0;
349 | for (var index = 0; index < XAudioJSSamplesPerCallback && XAudioJSResampleBufferStart != XAudioJSResampleBufferEnd; ++index) {
350 | //Sanitize the buffer:
351 | XAudioJSBinaryString[XAudioJSTotalSamples++] = String.fromCharCode(((Math.min(Math.max(XAudioJSResampledBuffer[XAudioJSResampleBufferStart++] + 1, 0), 2) * 0x3FFF) | 0) + 0x3000);
352 | XAudioJSBinaryString[XAudioJSTotalSamples++] = String.fromCharCode(((Math.min(Math.max(XAudioJSResampledBuffer[XAudioJSResampleBufferStart++] + 1, 0), 2) * 0x3FFF) | 0) + 0x3000);
353 | XAudioJSResampleBufferStart += XAudioJSChannelsAllocated - 2;
354 | if (XAudioJSResampleBufferStart == XAudioJSResampleBufferSize) {
355 | XAudioJSResampleBufferStart = 0;
356 | }
357 | }
358 | return XAudioJSBinaryString.join("");
359 | }
360 | function XAudioJSGenerateFlashStereoString() { //Convert the arrays to one long string for speed.
361 | var XAudioJSTotalSamples = XAudioJSSamplesPerCallback << 1;
362 | if (XAudioJSBinaryString.length > XAudioJSTotalSamples) {
363 | XAudioJSBinaryString = [];
364 | }
365 | for (var index = 0; index < XAudioJSTotalSamples && XAudioJSResampleBufferStart != XAudioJSResampleBufferEnd;) {
366 | //Sanitize the buffer:
367 | XAudioJSBinaryString[index++] = String.fromCharCode(((Math.min(Math.max(XAudioJSResampledBuffer[XAudioJSResampleBufferStart++] + 1, 0), 2) * 0x3FFF) | 0) + 0x3000);
368 | XAudioJSBinaryString[index++] = String.fromCharCode(((Math.min(Math.max(XAudioJSResampledBuffer[XAudioJSResampleBufferStart++] + 1, 0), 2) * 0x3FFF) | 0) + 0x3000);
369 | if (XAudioJSResampleBufferStart == XAudioJSResampleBufferSize) {
370 | XAudioJSResampleBufferStart = 0;
371 | }
372 | }
373 | return XAudioJSBinaryString.join("");
374 | }
375 | function XAudioJSGenerateFlashMonoString() { //Convert the array to one long string for speed.
376 | if (XAudioJSBinaryString.length > XAudioJSSamplesPerCallback) {
377 | XAudioJSBinaryString = [];
378 | }
379 | for (var index = 0; index < XAudioJSSamplesPerCallback && XAudioJSResampleBufferStart != XAudioJSResampleBufferEnd;) {
380 | //Sanitize the buffer:
381 | XAudioJSBinaryString[index++] = String.fromCharCode(((Math.min(Math.max(XAudioJSResampledBuffer[XAudioJSResampleBufferStart++] + 1, 0), 2) * 0x3FFF) | 0) + 0x3000);
382 | if (XAudioJSResampleBufferStart == XAudioJSResampleBufferSize) {
383 | XAudioJSResampleBufferStart = 0;
384 | }
385 | }
386 | return XAudioJSBinaryString.join("");
387 | }
388 | //Some Required Globals:
389 | var XAudioJSWebAudioContextHandle = null;
390 | var XAudioJSWebAudioAudioNode = null;
391 | var XAudioJSWebAudioWatchDogTimer = null;
392 | var XAudioJSWebAudioWatchDogLast = false;
393 | var XAudioJSWebAudioLaunchedContext = false;
394 | var XAudioJSAudioContextSampleBuffer = [];
395 | var XAudioJSResampledBuffer = [];
396 | var XAudioJSMinBufferSize = 15000;
397 | var XAudioJSMaxBufferSize = 25000;
398 | var XAudioJSChannelsAllocated = 1;
399 | var XAudioJSVolume = 1;
400 | var XAudioJSResampleControl = null;
401 | var XAudioJSAudioBufferSize = 0;
402 | var XAudioJSResampleBufferStart = 0;
403 | var XAudioJSResampleBufferEnd = 0;
404 | var XAudioJSResampleBufferSize = 0;
405 | var XAudioJSMediaStreamWorker = null;
406 | var XAudioJSMediaStreamBuffer = [];
407 | var XAudioJSMediaStreamSampleRate = 44100;
408 | var XAudioJSMozAudioSampleRate = 44100;
409 | var XAudioJSSamplesPerCallback = 2048; //Has to be between 2048 and 4096 (If over, then samples are ignored, if under then silence is added).
410 | var XAudioJSFlashTransportEncoder = null;
411 | var XAudioJSMediaStreamLengthAliasCounter = 0;
412 | var XAudioJSBinaryString = [];
413 | function XAudioJSWebAudioEvent(event) { //Web Audio API callback...
414 | if (XAudioJSWebAudioWatchDogTimer) {
415 | XAudioJSWebAudioWatchDogLast = (new Date()).getTime();
416 | }
417 | //Find all output channels:
418 | for (var bufferCount = 0, buffers = []; bufferCount < XAudioJSChannelsAllocated; ++bufferCount) {
419 | buffers[bufferCount] = event.outputBuffer.getChannelData(bufferCount);
420 | }
421 | //Make sure we have resampled samples ready:
422 | XAudioJSResampleRefill();
423 | //Copy samples from XAudioJS to the Web Audio API:
424 | for (var index = 0; index < XAudioJSSamplesPerCallback && XAudioJSResampleBufferStart != XAudioJSResampleBufferEnd; ++index) {
425 | for (bufferCount = 0; bufferCount < XAudioJSChannelsAllocated; ++bufferCount) {
426 | buffers[bufferCount][index] = XAudioJSResampledBuffer[XAudioJSResampleBufferStart++] * XAudioJSVolume;
427 | }
428 | if (XAudioJSResampleBufferStart == XAudioJSResampleBufferSize) {
429 | XAudioJSResampleBufferStart = 0;
430 | }
431 | }
432 | //Pad with silence if we're underrunning:
433 | while (index < XAudioJSSamplesPerCallback) {
434 | for (bufferCount = 0; bufferCount < XAudioJSChannelsAllocated; ++bufferCount) {
435 | buffers[bufferCount][index] = 0;
436 | }
437 | ++index;
438 | }
439 | }
440 | //MediaStream API buffer push
441 | function XAudioJSMediaStreamPushAudio(event) {
442 | var index = 0;
443 | var audioLengthRequested = event.data;
444 | var samplesPerCallbackAll = XAudioJSSamplesPerCallback * XAudioJSChannelsAllocated;
445 | var XAudioJSMediaStreamLengthAlias = audioLengthRequested % XAudioJSSamplesPerCallback;
446 | audioLengthRequested = audioLengthRequested - (XAudioJSMediaStreamLengthAliasCounter - (XAudioJSMediaStreamLengthAliasCounter % XAudioJSSamplesPerCallback)) - XAudioJSMediaStreamLengthAlias + XAudioJSSamplesPerCallback;
447 | XAudioJSMediaStreamLengthAliasCounter -= XAudioJSMediaStreamLengthAliasCounter - (XAudioJSMediaStreamLengthAliasCounter % XAudioJSSamplesPerCallback);
448 | XAudioJSMediaStreamLengthAliasCounter += XAudioJSSamplesPerCallback - XAudioJSMediaStreamLengthAlias;
449 | if (XAudioJSMediaStreamBuffer.length != samplesPerCallbackAll) {
450 | XAudioJSMediaStreamBuffer = new Float32Array(samplesPerCallbackAll);
451 | }
452 | XAudioJSResampleRefill();
453 | while (index < audioLengthRequested) {
454 | var index2 = 0;
455 | while (index2 < samplesPerCallbackAll && XAudioJSResampleBufferStart != XAudioJSResampleBufferEnd) {
456 | XAudioJSMediaStreamBuffer[index2++] = XAudioJSResampledBuffer[XAudioJSResampleBufferStart++];
457 | if (XAudioJSResampleBufferStart == XAudioJSResampleBufferSize) {
458 | XAudioJSResampleBufferStart = 0;
459 | }
460 | }
461 | XAudioJSMediaStreamWorker.postMessage([0, XAudioJSMediaStreamBuffer]);
462 | index += XAudioJSSamplesPerCallback;
463 | }
464 | }
465 | function XAudioJSResampleRefill() {
466 | if (XAudioJSAudioBufferSize > 0) {
467 | //Resample a chunk of audio:
468 | var resampleLength = XAudioJSResampleControl.resampler(XAudioJSGetBufferSamples());
469 | var resampledResult = XAudioJSResampleControl.outputBuffer;
470 | for (var index2 = 0; index2 < resampleLength;) {
471 | XAudioJSResampledBuffer[XAudioJSResampleBufferEnd++] = resampledResult[index2++];
472 | if (XAudioJSResampleBufferEnd == XAudioJSResampleBufferSize) {
473 | XAudioJSResampleBufferEnd = 0;
474 | }
475 | if (XAudioJSResampleBufferStart == XAudioJSResampleBufferEnd) {
476 | XAudioJSResampleBufferStart += XAudioJSChannelsAllocated;
477 | if (XAudioJSResampleBufferStart == XAudioJSResampleBufferSize) {
478 | XAudioJSResampleBufferStart = 0;
479 | }
480 | }
481 | }
482 | XAudioJSAudioBufferSize = 0;
483 | }
484 | }
485 | function XAudioJSResampledSamplesLeft() {
486 | return ((XAudioJSResampleBufferStart <= XAudioJSResampleBufferEnd) ? 0 : XAudioJSResampleBufferSize) + XAudioJSResampleBufferEnd - XAudioJSResampleBufferStart;
487 | }
488 | function XAudioJSGetBufferSamples() {
489 | return XAudioJSGetArraySlice(XAudioJSAudioContextSampleBuffer, XAudioJSAudioBufferSize);
490 | }
491 | function XAudioJSGetArraySlice(buffer, lengthOf) {
492 | //Typed array and normal array buffer section referencing:
493 | try {
494 | return buffer.subarray(0, lengthOf);
495 | }
496 | catch (error) {
497 | try {
498 | //Regular array pass:
499 | buffer.length = lengthOf;
500 | return buffer;
501 | }
502 | catch (error) {
503 | //Nightly Firefox 4 used to have the subarray function named as slice:
504 | return buffer.slice(0, lengthOf);
505 | }
506 | }
507 | }
--------------------------------------------------------------------------------