@soapbox.pub/wasmboy
Version:
Soapbox fork of Wasmboy.
1,811 lines (1,454 loc) • 211 kB
JavaScript
// API For adding plugins for WasmBoy
// Should follow the Rollup Plugin API
// https://rollupjs.org/guide/en#plugins
// Plugins have the following supported hooks
// And properties
const WASMBOY_PLUGIN = {
name: 'wasmboy-plugin REQUIRED',
graphics: rgbaArray => {},
// Returns undefined. Edit object in place
audio: (audioContext, headAudioNode, channelId) => {},
// Return AudioNode, which will be connected to the destination node eventually.
saveState: saveStateObject => {},
// Returns undefined. Edit object in place.
canvas: (canvasElement, canvasContext, canvasImageData) => {},
// Returns undefined. Edit object in place.
breakpoint: () => {},
ready: () => {},
play: () => {},
pause: () => {},
loadedAndStarted: () => {}
};
class WasmBoyPluginsService {
constructor() {
this.plugins = {};
this.pluginIdCounter = 0;
}
addPlugin(pluginObject) {
// Verify the plugin
if (!pluginObject && typeof pluginObject !== 'object') {
throw new Error('Invalid Plugin Object');
}
if (!pluginObject.name) {
throw new Error('Added plugin must have a "name" property');
} // Add the plugin to our plugin container
const id = this.pluginIdCounter;
this.plugins[this.pluginIdCounter] = pluginObject;
this.pluginIdCounter++; // Return a function to remove the plugin
return () => {
this.removePlugin(id);
};
}
removePlugin(id) {
delete this.plugins[id];
}
runHook(hookConfig) {
if (!WASMBOY_PLUGIN[hookConfig.key] || typeof WASMBOY_PLUGIN[hookConfig.key] !== 'function') {
throw new Error('No such hook as ' + hookConfig.key);
}
Object.keys(this.plugins).forEach(pluginKey => {
const plugin = this.plugins[pluginKey];
if (plugin[hookConfig.key]) {
let hookResponse = undefined;
try {
hookResponse = plugin[hookConfig.key].apply(null, hookConfig.params);
} catch (e) {
console.error(`There was an error running the '${hookConfig.key}' hook, on the ${plugin.name} plugin.`);
console.error(e);
}
if (hookConfig.callback) {
hookConfig.callback(hookResponse);
}
}
});
}
}
const WasmBoyPlugins = new WasmBoyPluginsService();
// Some shared constants by the graphics lib and worker
const GAMEBOY_CAMERA_WIDTH = 160;
const GAMEBOY_CAMERA_HEIGHT = 144;
const WORKER_MESSAGE_TYPE = {
CONNECT: 'CONNECT',
INSTANTIATE_WASM: 'INSTANTIATE_WASM',
CLEAR_MEMORY: 'CLEAR_MEMORY',
CLEAR_MEMORY_DONE: 'CLEAR_MEMORY_DONE',
GET_MEMORY: 'GET_MEMORY',
SET_MEMORY: 'SET_MEMORY',
SET_MEMORY_DONE: 'SET_MEMORY_DONE',
GET_CONSTANTS: 'GET_CONSTANTS',
GET_CONSTANTS_DONE: 'GET_CONSTANTS_DONE',
CONFIG: 'CONFIG',
RESET_AUDIO_QUEUE: 'RESET_AUDIO_QUEUE',
PLAY: 'PLAY',
BREAKPOINT: 'BREAKPOINT',
PAUSE: 'PAUSE',
UPDATED: 'UPDATED',
CRASHED: 'CRASHED',
SET_JOYPAD_STATE: 'SET_JOYPAD_STATE',
AUDIO_LATENCY: 'AUDIO_LATENCY',
RUN_WASM_EXPORT: 'RUN_WASM_EXPORT',
GET_WASM_MEMORY_SECTION: 'GET_WASM_MEMORY_SECTION',
GET_WASM_CONSTANT: 'GET_WASM_CONSTANT',
FORCE_OUTPUT_FRAME: 'FORCE_OUTPUT_FRAME',
SET_SPEED: 'SET_SPEED',
IS_GBC: 'IS_GBC'
};
const WORKER_ID = {
LIB: 'LIB',
GRAPHICS: 'GRAPHICS',
MEMORY: 'MEMORY',
CONTROLLER: 'CONTROLLER',
AUDIO: 'AUDIO'
};
const MEMORY_TYPE = {
BOOT_ROM: 'BOOT_ROM',
CARTRIDGE_RAM: 'CARTRIDGE_RAM',
CARTRIDGE_ROM: 'CARTRIDGE_ROM',
CARTRIDGE_HEADER: 'CARTRIDGE_HEADER',
GAMEBOY_MEMORY: 'GAMEBOY_MEMORY',
PALETTE_MEMORY: 'PALETTE_MEMORY',
INTERNAL_STATE: 'INTERNAL_STATE'
};
function getEventData(event) {
if (event.data) {
return event.data;
}
return event;
}
// Handles rendering graphics using the HTML5 Canvas
// https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas
class WasmBoyGraphicsService {
constructor() {
this.worker = undefined;
this.updateGraphicsCallback = undefined;
this.frameQueue = undefined;
this.frameQueueRenderPromise = undefined;
this.canvasElement = undefined;
this.canvasContext = undefined;
this.canvasImageData = undefined;
this.imageDataArray = undefined;
this.imageDataArrayChanged = false;
}
initialize(canvasElement, updateGraphicsCallback) {
this.updateGraphicsCallback = updateGraphicsCallback; // Initialiuze our cached wasm constants
// WASMBOY_CURRENT_FRAME_OUTPUT_LOCATION = this.wasmInstance.exports.frameInProgressGRAPHICS_OUTPUT_LOCATION.valueOf();
// Reset our frame queue and render promises
this.frameQueue = [];
const initializeTask = async () => {
// Prepare our canvas
this.canvasElement = canvasElement;
this.canvasContext = this.canvasElement.getContext('2d');
this.canvasElement.width = GAMEBOY_CAMERA_WIDTH;
this.canvasElement.height = GAMEBOY_CAMERA_HEIGHT;
this.canvasImageData = this.canvasContext.createImageData(this.canvasElement.width, this.canvasElement.height); // Add some css for smooth 8-bit canvas scaling
// https://stackoverflow.com/questions/7615009/disable-interpolation-when-scaling-a-canvas
// https://caniuse.com/#feat=css-crisp-edges
this.canvasElement.style = `
image-rendering: optimizeSpeed;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: -o-crisp-edges;
image-rendering: pixelated;
-ms-interpolation-mode: nearest-neighbor;
`; // Fill the canvas with a blank screen
// using client width since we are not requiring a width and height oin the canvas
// https://developer.mozilla.org/en-US/docs/Web/API/Element/clientWidth
// TODO: Mention respopnsive canvas scaling in the docs
this.canvasContext.clearRect(0, 0, this.canvasElement.width, this.canvasElement.height); // Doing set canvas here, as multiple sources can re-initialize the graphics
// TODO: Move setCanvas out of initialize :p
WasmBoyPlugins.runHook({
key: 'canvas',
params: [this.canvasElement, this.canvasContext, this.canvasImageData],
callback: response => {
if (!response) {
return;
}
if (response.canvasElement) {
this.canvasElement = response.canvasElement;
}
if (response.canvasContext) {
this.canvasContext = response.canvasContext;
}
if (response.canvasImageData) {
this.canvasImageData = response.canvasImageData;
}
}
}); // Finally make sure we set our constants for our worker
if (this.worker) {
await this.worker.postMessage({
type: WORKER_MESSAGE_TYPE.GET_CONSTANTS
});
}
};
return initializeTask();
} // Function to set our worker
setWorker(worker) {
this.worker = worker;
this.worker.addMessageListener(event => {
const eventData = getEventData(event);
switch (eventData.message.type) {
case WORKER_MESSAGE_TYPE.UPDATED:
{
this.imageDataArray = new Uint8ClampedArray(eventData.message.imageDataArrayBuffer);
this.imageDataArrayChanged = true;
return;
}
}
});
} // Function to render a frame
// Will add the frame to the frame queue to be rendered
// Returns the promise from this.drawFrameQueue
// Which resolves once all frames are rendered
renderFrame() {
// Check if we have new graphics to show
if (!this.imageDataArrayChanged) {
return;
}
this.imageDataArrayChanged = false; // Check for a callback for accessing image data
if (this.updateGraphicsCallback) {
this.updateGraphicsCallback(this.imageDataArray);
} // Set the imageDataArray to our plugins
WasmBoyPlugins.runHook({
key: 'graphics',
params: [this.imageDataArray],
callback: response => {
if (response) {
this.imageDataArray = response;
}
}
}); // Add our new imageData
this.canvasImageData.data.set(this.imageDataArray);
this.canvasContext.clearRect(0, 0, this.canvasElement.width, this.canvasElement.height);
this.canvasContext.putImageData(this.canvasImageData, 0, 0);
}
}
const WasmBoyGraphics = new WasmBoyGraphicsService();
function index(buffer, opt) {
opt = opt || {};
var numChannels = buffer.numberOfChannels;
var sampleRate = buffer.sampleRate;
var format = opt.float32 ? 3 : 1;
var bitDepth = format === 3 ? 32 : 16;
var result;
if (numChannels === 2) {
result = interleave(buffer.getChannelData(0), buffer.getChannelData(1));
} else {
result = buffer.getChannelData(0);
}
return encodeWAV(result, format, sampleRate, numChannels, bitDepth)
}
function encodeWAV (samples, format, sampleRate, numChannels, bitDepth) {
var bytesPerSample = bitDepth / 8;
var blockAlign = numChannels * bytesPerSample;
var buffer = new ArrayBuffer(44 + samples.length * bytesPerSample);
var view = new DataView(buffer);
/* RIFF identifier */
writeString(view, 0, 'RIFF');
/* RIFF chunk length */
view.setUint32(4, 36 + samples.length * bytesPerSample, true);
/* RIFF type */
writeString(view, 8, 'WAVE');
/* format chunk identifier */
writeString(view, 12, 'fmt ');
/* format chunk length */
view.setUint32(16, 16, true);
/* sample format (raw) */
view.setUint16(20, format, true);
/* channel count */
view.setUint16(22, numChannels, true);
/* sample rate */
view.setUint32(24, sampleRate, true);
/* byte rate (sample rate * block align) */
view.setUint32(28, sampleRate * blockAlign, true);
/* block align (channel count * bytes per sample) */
view.setUint16(32, blockAlign, true);
/* bits per sample */
view.setUint16(34, bitDepth, true);
/* data chunk identifier */
writeString(view, 36, 'data');
/* data chunk length */
view.setUint32(40, samples.length * bytesPerSample, true);
if (format === 1) { // Raw PCM
floatTo16BitPCM(view, 44, samples);
} else {
writeFloat32(view, 44, samples);
}
return buffer
}
function interleave (inputL, inputR) {
var length = inputL.length + inputR.length;
var result = new Float32Array(length);
var index = 0;
var inputIndex = 0;
while (index < length) {
result[index++] = inputL[inputIndex];
result[index++] = inputR[inputIndex];
inputIndex++;
}
return result
}
function writeFloat32 (output, offset, input) {
for (var i = 0; i < input.length; i++, offset += 4) {
output.setFloat32(offset, input[i], true);
}
}
function floatTo16BitPCM (output, offset, input) {
for (var i = 0; i < input.length; i++, offset += 2) {
var s = Math.max(-1, Math.min(1, input[i]));
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
}
function writeString (view, offset, string) {
for (var i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
// Gameboy Channel Output
// Both of these make it sound off
// Latency controls how much delay audio has, larger = more delay, goal is to be as small as possible
// Time remaining controls how far ahead we can be., larger = more frames rendered before playing a new set of samples. goal is to be as small as possible. May want to adjust this number according to performance of device
// These magic numbers just come from preference, can be set as options
const DEFAULT_AUDIO_LATENCY_IN_MILLI = 100; // Some constants that use the ones above that will allow for faster performance
const DEFAULT_AUDIO_LATENCY_IN_SECONDS = DEFAULT_AUDIO_LATENCY_IN_MILLI / 1000; // Seems like the super quiet popping, and the wace form spikes in the visualizer,
// are caused by the sample rate :P
// Thus need to figure out why that is.
const WASMBOY_SAMPLE_RATE = 44100;
class GbChannelWebAudio {
constructor(id) {
this.id = id;
this.audioContext = undefined;
this.audioBuffer = undefined; // The play time for our audio samples
this.audioPlaytime = undefined;
this.audioSources = []; // Gain Node for muting
this.gainNode = undefined;
this.muted = false;
this.libMuted = false; // Our buffer for recording PCM Samples as they come
this.recording = false;
this.recordingLeftBuffers = undefined;
this.recordingRightBuffers = undefined;
this.recordingAudioBuffer = undefined;
this.recordingAnchor = undefined;
}
createAudioContextIfNone() {
if (!this.audioContext && typeof window !== 'undefined') {
// Get our Audio context
this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); // Set up our nodes
// Seems like closure compiler will optimize this out
// Thus, need to do a very specifc type check if statement here.
if (!!this.audioContext === true) {
this.gainNode = this.audioContext.createGain();
}
}
}
getCurrentTime() {
this.createAudioContextIfNone();
if (!this.audioContext) {
return;
}
return this.audioContext.currentTime;
}
getPlayTime() {
return this.audioPlaytime;
}
resumeAudioContext() {
this.createAudioContextIfNone();
if (!this.audioContext) {
return;
}
if (this.audioContext.state === 'suspended') {
this.audioContext.resume();
this.audioPlaytime = this.audioContext.currentTime;
}
}
playAudio(numberOfSamples, leftChannelBuffer, rightChannelBuffer, playbackRate, updateAudioCallback) {
if (!this.audioContext) {
return;
} // Get our buffers as floats
const leftChannelBufferAsFloat = new Float32Array(leftChannelBuffer);
const rightChannelBufferAsFloat = new Float32Array(rightChannelBuffer); // Create an audio buffer, with a left and right channel
this.audioBuffer = this.audioContext.createBuffer(2, numberOfSamples, WASMBOY_SAMPLE_RATE);
this._setSamplesToAudioBuffer(this.audioBuffer, leftChannelBufferAsFloat, rightChannelBufferAsFloat);
if (this.recording) {
this.recordingLeftBuffers.push(leftChannelBufferAsFloat);
this.recordingRightBuffers.push(rightChannelBufferAsFloat);
} // Get an AudioBufferSourceNode.
// This is the AudioNode to use when we want to play an AudioBuffer
let source = this.audioContext.createBufferSource(); // set the buffer in the AudioBufferSourceNode
source.buffer = this.audioBuffer; // Set our playback rate for time resetretching
source.playbackRate.setValueAtTime(playbackRate, this.audioContext.currentTime); // Set up our "final node", as in the one that will be connected
// to the destination (output)
let finalNode = source; // Call our callback/plugins, if we have one
if (updateAudioCallback) {
const responseNode = updateAudioCallback(this.audioContext, finalNode, this.id);
if (responseNode) {
finalNode = responseNode;
}
} // Call our plugins
WasmBoyPlugins.runHook({
key: 'audio',
params: [this.audioContext, finalNode, this.id],
callback: hookResponse => {
if (hookResponse) {
finalNode.connect(hookResponse);
finalNode = hookResponse;
}
}
}); // Lastly, apply our gain node to mute/unmute
if (this.gainNode) {
finalNode.connect(this.gainNode);
finalNode = this.gainNode;
} // connect the AudioBufferSourceNode to the
// destination so we can hear the sound
finalNode.connect(this.audioContext.destination); // Check if we made it in time
// Idea from: https://github.com/binji/binjgb/blob/master/demo/demo.js
let audioContextCurrentTime = this.audioContext.currentTime;
let audioContextCurrentTimeWithLatency = audioContextCurrentTime + DEFAULT_AUDIO_LATENCY_IN_SECONDS;
this.audioPlaytime = this.audioPlaytime || audioContextCurrentTimeWithLatency;
if (this.audioPlaytime < audioContextCurrentTime) {
// We took too long, or something happen and hiccup'd the emulator, reset audio playback times
this.cancelAllAudio();
this.audioPlaytime = audioContextCurrentTimeWithLatency;
} // start the source playing
source.start(this.audioPlaytime); // Set our new audio playtime goal
const sourcePlaybackLength = numberOfSamples / (WASMBOY_SAMPLE_RATE * playbackRate);
this.audioPlaytime = this.audioPlaytime + sourcePlaybackLength; // Cancel all audio sources on the tail that play before us
while (this.audioSources[this.audioSources.length - 1] && this.audioSources[this.audioSources.length - 1].playtime <= this.audioPlaytime) {
this.audioSources[this.audioSources.length - 1].source.stop();
this.audioSources.pop();
} // Add the source so we can stop this if needed
this.audioSources.push({
source: source,
playTime: this.audioPlaytime
}); // Shift ourselves out when finished
const timeUntilSourceEnds = this.audioPlaytime - this.audioContext.currentTime + 500;
setTimeout(() => {
this.audioSources.shift();
}, timeUntilSourceEnds);
}
cancelAllAudio(stopCurrentAudio) {
if (!this.audioContext) {
return;
} // Cancel all audio That was queued to play
for (let i = 0; i < this.audioSources.length; i++) {
if (stopCurrentAudio || this.audioSources[i].playTime > this.audioPlaytime) {
this.audioSources[i].source.stop();
}
}
this.audioSources = []; // Reset our audioPlaytime
this.audioPlaytime = this.audioContext.currentTime + DEFAULT_AUDIO_LATENCY_IN_SECONDS;
}
mute() {
if (!this.muted) {
this._setGain(0);
this.muted = true;
}
}
unmute() {
if (this.muted) {
this._setGain(1);
this.muted = false;
}
}
hasRecording() {
return !!this.recordingAudioBuffer;
}
startRecording() {
if (!this.recording) {
this.recording = true;
this.recordingLeftBuffers = [];
this.recordingRightBuffers = [];
this.recordingAudioBuffer = undefined;
}
}
stopRecording() {
// Check if we were recoridng
if (!this.recording) {
return;
}
this.recording = false; // Create a left/right buffer from all the buffers stored
const createBufferFromBuffers = buffers => {
let totalLength = 0;
buffers.forEach(buffer => {
totalLength += buffer.length;
});
const totalBuffer = new Float32Array(totalLength);
let currentLength = 0;
buffers.forEach(buffer => {
totalBuffer.set(buffer, currentLength);
currentLength += buffer.length;
});
return totalBuffer;
};
const totalLeftBuffer = createBufferFromBuffers(this.recordingLeftBuffers);
const totalRightBuffer = createBufferFromBuffers(this.recordingRightBuffers);
this.recordingAudioBuffer = this.audioContext.createBuffer(2, totalLeftBuffer.length, WASMBOY_SAMPLE_RATE);
this._setSamplesToAudioBuffer(this.recordingAudioBuffer, totalLeftBuffer, totalRightBuffer);
this.recordingLeftBuffer = undefined;
this.recordingRightBuffer = undefined;
}
downloadRecordingAsWav(filename) {
if (!this.recordingAudioBuffer) {
return;
} // Check if we need to create our anchor tag
// Which is used to download the audio
if (!this.recordingAnchor) {
this.recordingAnchor = document.createElement('a');
document.body.appendChild(this.recordingAnchor);
this.recordingAnchor.style = 'display: none';
} // Create our wav as a downloadable blob
const wav = index(this.recordingAudioBuffer);
const blob = new window.Blob([new DataView(wav)], {
type: 'audio/wav'
}); // Create our url / download name
const url = window.URL.createObjectURL(blob);
this.recordingAnchor.href = url;
let downloadName;
if (filename) {
downloadName = `${filename}.wav`;
} else {
const shortDateWithTime = new Date().toLocaleDateString(undefined, {
month: '2-digit',
day: '2-digit',
year: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
downloadName = `wasmboy-${shortDateWithTime}.wav`;
}
this.recordingAnchor.download = downloadName; // Download our wav
this.recordingAnchor.click();
window.URL.revokeObjectURL(url);
}
getRecordingAsWavBase64EncodedString() {
if (!this.recordingAudioBuffer) {
return;
} // Create our wav as a downloadable blob
const wav = index(this.recordingAudioBuffer);
const base64String = this._arrayBufferToBase64(wav);
return `data:audio/wav;base64,${base64String}`;
}
getRecordingAsAudioBuffer() {
return this.recordingAudioBuffer;
}
_libMute() {
this._setGain(0);
this.libMuted = true;
}
_libUnmute() {
if (this.libMuted) {
this._setGain(1);
this.libMuted = false;
}
}
_setGain(gain) {
this.createAudioContextIfNone();
if (this.gainNode) {
this.gainNode.gain.setValueAtTime(gain, this.audioContext.currentTime);
}
}
_setSamplesToAudioBuffer(audioBuffer, leftChannelSamples, rightChannelSamples) {
if (audioBuffer.copyToChannel) {
audioBuffer.copyToChannel(leftChannelSamples, 0, 0);
audioBuffer.copyToChannel(rightChannelSamples, 1, 0);
} else {
// Safari fallback
audioBuffer.getChannelData(0).set(leftChannelSamples);
audioBuffer.getChannelData(1).set(rightChannelSamples);
}
} // https://stackoverflow.com/questions/9267899/arraybuffer-to-base64-encoded-string/38858127
_arrayBufferToBase64(buffer) {
let binary = '';
let bytes = new Uint8Array(buffer);
let len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
}
// Tons of help from:
const SLOW_TIME_STRETCH_MIN_FPS = 57;
class WasmBoyAudioService {
constructor() {
// Wasmboy instance and memory
this.worker = undefined;
this.updateAudioCallback = undefined; // Our Channels
this.gbChannels = {
master: new GbChannelWebAudio('master'),
channel1: new GbChannelWebAudio('channel1'),
channel2: new GbChannelWebAudio('channel2'),
channel3: new GbChannelWebAudio('channel3'),
channel4: new GbChannelWebAudio('channel4')
};
this._createAudioContextIfNone(); // Mute all the child channels,
// As we will assume all channels are enabled
if (typeof window !== 'undefined') {
this.gbChannels.channel1._libMute();
this.gbChannels.channel2._libMute();
this.gbChannels.channel3._libMute();
this.gbChannels.channel4._libMute();
} // Average fps for time stretching
this.averageTimeStretchFps = [];
this.speed = 1.0; // Our sound output Location, we will initialize this in init
this.WASMBOY_SOUND_OUTPUT_LOCATION = 0;
this.WASMBOY_CHANNEL_1_OUTPUT_LOCATION = 0;
this.WASMBOY_CHANNEL_2_OUTPUT_LOCATION = 0;
this.WASMBOY_CHANNEL_3_OUTPUT_LOCATION = 0;
this.WASMBOY_CHANNEL_4_OUTPUT_LOCATION = 0;
}
initialize(updateAudioCallback) {
const initializeTask = async () => {
this.updateAudioCallback = updateAudioCallback;
this.averageTimeStretchFps = [];
this.speed = 1.0;
this._createAudioContextIfNone();
this.cancelAllAudio(); // Lastly get our audio constants
return this.worker.postMessage({
type: WORKER_MESSAGE_TYPE.GET_CONSTANTS
});
};
return initializeTask();
}
setWorker(worker) {
this.worker = worker;
this.worker.addMessageListener(event => {
const eventData = getEventData(event);
switch (eventData.message.type) {
case WORKER_MESSAGE_TYPE.UPDATED:
{
// Dont wait for raf.
// Audio being shown is not dependent on the browser drawing a frame :)
// Just send the message directly
this.playAudio(eventData.message); // Next, send back how much forward latency
// we have
let latency = 0;
let currentTime = this.gbChannels.master.getCurrentTime();
let playtime = this.gbChannels.master.getPlayTime();
if (currentTime && currentTime > 0) {
latency = playtime - currentTime;
}
this.worker.postMessageIgnoreResponse({
type: WORKER_MESSAGE_TYPE.AUDIO_LATENCY,
latency
});
return;
}
}
});
}
getAudioChannels() {
return this.gbChannels;
}
setSpeed(speed) {
this.speed = speed;
this.cancelAllAudio(true);
this.resetTimeStretch();
}
resetTimeStretch() {
// Simply reset our average FPS counter array
this.averageTimeStretchFps = [];
} // Function to queue up and audio buyffer to be played
// Returns a promise so that we may "sync by audio"
// https://www.reddit.com/r/EmuDev/comments/5gkwi5/gb_apu_sound_emulation/dau8e2w/
playAudio(audioMessage) {
let currentFps = audioMessage.fps;
let allowFastSpeedStretching = audioMessage.allowFastSpeedStretching;
let numberOfSamples = audioMessage.numberOfSamples; // Find our averageFps
let fps = currentFps || 60; // Check if we got a huge fps outlier.
// If so, let's just reset our average.
// This will fix the slow gradual ramp down
const fpsDifference = Math.abs(currentFps - this.averageTimeStretchFps[this.averageTimeStretchFps.length - 1]);
if (fpsDifference && fpsDifference >= 15) {
this.resetTimeStretch();
} // Find our average fps for time stretching
this.averageTimeStretchFps.push(currentFps); // TODO Make the multiplier Const the timeshift speed
if (this.averageTimeStretchFps.length > Math.floor(SLOW_TIME_STRETCH_MIN_FPS * 3)) {
this.averageTimeStretchFps.shift();
} // Make sure we have a minimum number of time stretch fps timestamps to judge the average time
if (this.averageTimeStretchFps.length >= SLOW_TIME_STRETCH_MIN_FPS) {
fps = this.averageTimeStretchFps.reduce((accumulator, currentValue) => {
return accumulator + currentValue;
});
fps = Math.floor(fps / this.averageTimeStretchFps.length);
} // Find if we should time stretch this sample or not from our current fps
let playbackRate = 1.0;
let shouldTimeStretch = (fps < SLOW_TIME_STRETCH_MIN_FPS || allowFastSpeedStretching) && this.speed === 1.0;
if (shouldTimeStretch) {
// Has to be 60 to get accurent playback regarless of fps cap
playbackRate = playbackRate * (fps / 60);
if (playbackRate <= 0) {
playbackRate = 0.01;
}
} // Apply our speed to the playback rate
playbackRate = playbackRate * this.speed; // Play the master channel
this.gbChannels.master.playAudio(numberOfSamples, audioMessage.audioBuffer.left, audioMessage.audioBuffer.right, playbackRate, this.updateAudioCallback); // Play on all of our channels if we have buffers for them
for (let i = 0; i < 4; i++) {
let channelNumber = i + 1;
if (audioMessage[`channel${channelNumber}Buffer`]) {
this.gbChannels[`channel${channelNumber}`].playAudio(numberOfSamples, audioMessage[`channel${channelNumber}Buffer`].left, audioMessage[`channel${channelNumber}Buffer`].right, playbackRate, this.updateAudioCallback);
}
}
let playingAllChannels = !this.gbChannels.channel1.muted && !this.gbChannels.channel2.muted && !this.gbChannels.channel3.muted && !this.gbChannels.channel4.muted; // Mute and unmute accordingly
if (this.gbChannels.master.muted && playingAllChannels) {
this.gbChannels.master.unmute(); // We want to "force" mute here
// Because master is secretly playing all the audio,
// But we want the channels to appear not muted :)
this.gbChannels.channel1._libMute();
this.gbChannels.channel2._libMute();
this.gbChannels.channel3._libMute();
this.gbChannels.channel4._libMute();
} else if (!this.gbChannels.master.muted && !playingAllChannels) {
this.gbChannels.master.mute();
this.gbChannels.channel1._libUnmute();
this.gbChannels.channel2._libUnmute();
this.gbChannels.channel3._libUnmute();
this.gbChannels.channel4._libUnmute();
}
} // Functions to simply run on all of our channels
// Ensure that Audio is blessed.
// Meaning, the audioContext won't be
// affected by any autoplay issues.
// https://www.chromium.org/audio-video/autoplay
resumeAudioContext() {
this._applyOnAllChannels('resumeAudioContext');
}
cancelAllAudio(stopCurrentAudio) {
this._applyOnAllChannels('cancelAllAudio', [stopCurrentAudio]);
}
_createAudioContextIfNone() {
this._applyOnAllChannels('createAudioContextIfNone');
}
_applyOnAllChannels(functionKey, argsArray) {
Object.keys(this.gbChannels).forEach(gbChannelKey => {
this.gbChannels[gbChannelKey][functionKey].apply(this.gbChannels[gbChannelKey], argsArray);
});
}
}
const WasmBoyAudio = new WasmBoyAudioService();
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function ownKeys(object, enumerableOnly) {
var keys = Object.keys(object);
if (Object.getOwnPropertySymbols) {
var symbols = Object.getOwnPropertySymbols(object);
if (enumerableOnly) symbols = symbols.filter(function (sym) {
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
});
keys.push.apply(keys, symbols);
}
return keys;
}
function _objectSpread2(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? arguments[i] : {};
if (i % 2) {
ownKeys(Object(source), true).forEach(function (key) {
_defineProperty(target, key, source[key]);
});
} else if (Object.getOwnPropertyDescriptors) {
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
} else {
ownKeys(Object(source)).forEach(function (key) {
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
});
}
}
return target;
}
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function createCommonjsModule(fn, module) {
return module = { exports: {} }, fn(module, module.exports), module.exports;
}
var idb = createCommonjsModule(function (module) {
(function() {
function toArray(arr) {
return Array.prototype.slice.call(arr);
}
function promisifyRequest(request) {
return new Promise(function(resolve, reject) {
request.onsuccess = function() {
resolve(request.result);
};
request.onerror = function() {
reject(request.error);
};
});
}
function promisifyRequestCall(obj, method, args) {
var request;
var p = new Promise(function(resolve, reject) {
request = obj[method].apply(obj, args);
promisifyRequest(request).then(resolve, reject);
});
p.request = request;
return p;
}
function promisifyCursorRequestCall(obj, method, args) {
var p = promisifyRequestCall(obj, method, args);
return p.then(function(value) {
if (!value) return;
return new Cursor(value, p.request);
});
}
function proxyProperties(ProxyClass, targetProp, properties) {
properties.forEach(function(prop) {
Object.defineProperty(ProxyClass.prototype, prop, {
get: function() {
return this[targetProp][prop];
},
set: function(val) {
this[targetProp][prop] = val;
}
});
});
}
function proxyRequestMethods(ProxyClass, targetProp, Constructor, properties) {
properties.forEach(function(prop) {
if (!(prop in Constructor.prototype)) return;
ProxyClass.prototype[prop] = function() {
return promisifyRequestCall(this[targetProp], prop, arguments);
};
});
}
function proxyMethods(ProxyClass, targetProp, Constructor, properties) {
properties.forEach(function(prop) {
if (!(prop in Constructor.prototype)) return;
ProxyClass.prototype[prop] = function() {
return this[targetProp][prop].apply(this[targetProp], arguments);
};
});
}
function proxyCursorRequestMethods(ProxyClass, targetProp, Constructor, properties) {
properties.forEach(function(prop) {
if (!(prop in Constructor.prototype)) return;
ProxyClass.prototype[prop] = function() {
return promisifyCursorRequestCall(this[targetProp], prop, arguments);
};
});
}
function Index(index) {
this._index = index;
}
proxyProperties(Index, '_index', [
'name',
'keyPath',
'multiEntry',
'unique'
]);
proxyRequestMethods(Index, '_index', IDBIndex, [
'get',
'getKey',
'getAll',
'getAllKeys',
'count'
]);
proxyCursorRequestMethods(Index, '_index', IDBIndex, [
'openCursor',
'openKeyCursor'
]);
function Cursor(cursor, request) {
this._cursor = cursor;
this._request = request;
}
proxyProperties(Cursor, '_cursor', [
'direction',
'key',
'primaryKey',
'value'
]);
proxyRequestMethods(Cursor, '_cursor', IDBCursor, [
'update',
'delete'
]);
// proxy 'next' methods
['advance', 'continue', 'continuePrimaryKey'].forEach(function(methodName) {
if (!(methodName in IDBCursor.prototype)) return;
Cursor.prototype[methodName] = function() {
var cursor = this;
var args = arguments;
return Promise.resolve().then(function() {
cursor._cursor[methodName].apply(cursor._cursor, args);
return promisifyRequest(cursor._request).then(function(value) {
if (!value) return;
return new Cursor(value, cursor._request);
});
});
};
});
function ObjectStore(store) {
this._store = store;
}
ObjectStore.prototype.createIndex = function() {
return new Index(this._store.createIndex.apply(this._store, arguments));
};
ObjectStore.prototype.index = function() {
return new Index(this._store.index.apply(this._store, arguments));
};
proxyProperties(ObjectStore, '_store', [
'name',
'keyPath',
'indexNames',
'autoIncrement'
]);
proxyRequestMethods(ObjectStore, '_store', IDBObjectStore, [
'put',
'add',
'delete',
'clear',
'get',
'getAll',
'getKey',
'getAllKeys',
'count'
]);
proxyCursorRequestMethods(ObjectStore, '_store', IDBObjectStore, [
'openCursor',
'openKeyCursor'
]);
proxyMethods(ObjectStore, '_store', IDBObjectStore, [
'deleteIndex'
]);
function Transaction(idbTransaction) {
this._tx = idbTransaction;
this.complete = new Promise(function(resolve, reject) {
idbTransaction.oncomplete = function() {
resolve();
};
idbTransaction.onerror = function() {
reject(idbTransaction.error);
};
idbTransaction.onabort = function() {
reject(idbTransaction.error);
};
});
}
Transaction.prototype.objectStore = function() {
return new ObjectStore(this._tx.objectStore.apply(this._tx, arguments));
};
proxyProperties(Transaction, '_tx', [
'objectStoreNames',
'mode'
]);
proxyMethods(Transaction, '_tx', IDBTransaction, [
'abort'
]);
function UpgradeDB(db, oldVersion, transaction) {
this._db = db;
this.oldVersion = oldVersion;
this.transaction = new Transaction(transaction);
}
UpgradeDB.prototype.createObjectStore = function() {
return new ObjectStore(this._db.createObjectStore.apply(this._db, arguments));
};
proxyProperties(UpgradeDB, '_db', [
'name',
'version',
'objectStoreNames'
]);
proxyMethods(UpgradeDB, '_db', IDBDatabase, [
'deleteObjectStore',
'close'
]);
function DB(db) {
this._db = db;
}
DB.prototype.transaction = function() {
return new Transaction(this._db.transaction.apply(this._db, arguments));
};
proxyProperties(DB, '_db', [
'name',
'version',
'objectStoreNames'
]);
proxyMethods(DB, '_db', IDBDatabase, [
'close'
]);
// Add cursor iterators
// TODO: remove this once browsers do the right thing with promises
['openCursor', 'openKeyCursor'].forEach(function(funcName) {
[ObjectStore, Index].forEach(function(Constructor) {
// Don't create iterateKeyCursor if openKeyCursor doesn't exist.
if (!(funcName in Constructor.prototype)) return;
Constructor.prototype[funcName.replace('open', 'iterate')] = function() {
var args = toArray(arguments);
var callback = args[args.length - 1];
var nativeObject = this._store || this._index;
var request = nativeObject[funcName].apply(nativeObject, args.slice(0, -1));
request.onsuccess = function() {
callback(request.result);
};
};
});
});
// polyfill getAll
[Index, ObjectStore].forEach(function(Constructor) {
if (Constructor.prototype.getAll) return;
Constructor.prototype.getAll = function(query, count) {
var instance = this;
var items = [];
return new Promise(function(resolve) {
instance.iterateCursor(query, function(cursor) {
if (!cursor) {
resolve(items);
return;
}
items.push(cursor.value);
if (count !== undefined && items.length == count) {
resolve(items);
return;
}
cursor.continue();
});
});
};
});
var exp = {
open: function(name, version, upgradeCallback) {
var p = promisifyRequestCall(indexedDB, 'open', [name, version]);
var request = p.request;
if (request) {
request.onupgradeneeded = function(event) {
if (upgradeCallback) {
upgradeCallback(new UpgradeDB(request.result, event.oldVersion, request.transaction));
}
};
}
return p.then(function(db) {
return new DB(db);
});
},
delete: function(name) {
return promisifyRequestCall(indexedDB, 'deleteDatabase', [name]);
}
};
{
module.exports = exp;
module.exports.default = module.exports;
}
}());
});
var node = createCommonjsModule(function (module) {
if (typeof indexedDB != 'undefined') {
module.exports = idb;
}
else {
module.exports = {
open: function () {
return Promise.reject('IDB requires a browser environment');
},
delete: function () {
return Promise.reject('IDB requires a browser environment');
}
};
}
});
var node_1 = node.open;
// Get our idb instance, and initialize to asn idb-keyval
let keyval = false; // Get our idb dPromise
if (typeof window !== 'undefined') {
const dbPromise = node.open('wasmboy', 1, upgradeDB => {
upgradeDB.createObjectStore('keyval');
}); // Get our idb-keyval instance
keyval = {
get(key) {
return dbPromise.then(db => {
return db.transaction('keyval').objectStore('keyval').get(key);
});
},
set(key, val) {
return dbPromise.then(db => {
const tx = db.transaction('keyval', 'readwrite');
tx.objectStore('keyval').put(val, key);
return tx.complete;
});
},
delete(key) {
return dbPromise.then(db => {
const tx = db.transaction('keyval', 'readwrite');
tx.objectStore('keyval').delete(key);
return tx.complete;
});
},
clear() {
return dbPromise.then(db => {
const tx = db.transaction('keyval', 'readwrite');
tx.objectStore('keyval').clear();
return tx.complete;
});
},
keys() {
return dbPromise.then(db => {
const tx = db.transaction('keyval');
const keys = [];
const store = tx.objectStore('keyval'); // This would be store.getAllKeys(), but it isn't supported by Edge or Safari.
// openKeyCursor isn't supported by Safari, so we fall back
(store.iterateKeyCursor || store.iterateCursor).call(store, cursor => {
if (!cursor) return;
keys.push(cursor.key);
cursor.continue();
});
return tx.complete.then(() => keys);
});
}
};
} else {
// Create a mock keyval for node
keyval = {
get: () => {},
set: () => {},
delete: () => {},
clear: () => {},
keys: () => {}
};
}
const idbKeyval = keyval;
// Taken/Modified From: https://github.com/photopea/UZIP.js
let UZIP = {}; // Make it a hacky es module
const uzip = UZIP;
UZIP['parse'] = function (buf // ArrayBuffer
) {
let rUs = UZIP.bin.readUshort,
rUi = UZIP.bin.readUint,
o = 0,
out = {};
let data = new Uint8Array(buf);
let eocd = data.length - 4;
while (rUi(data, eocd) != 0x06054b50) eocd--;
o = eocd;
o += 4; // sign = 0x06054b50
o += 4; // disks = 0;
let cnu = rUs(data, o);
o += 2;
let cnt = rUs(data, o);
o += 2;
let csize = rUi(data, o);
o += 4;
let coffs = rUi(data, o);
o += 4;
o = coffs;
for (let i = 0; i < cnu; i++) {
let sign = rUi(data, o);
o += 4;
o += 4; // versions;
o += 4; // flag + compr
o += 4; // time
let crc32 = rUi(data, o);
o += 4;
let csize = rUi(data, o);
o += 4;
let usize = rUi(data, o);
o += 4;
let nl = rUs(data, o),
el = rUs(data, o + 2),
cl = rUs(data, o + 4);
o += 6; // name, extra, comment
o += 8; // disk, attribs
let roff = rUi(data, o);
o += 4;
o += nl + el + cl;
UZIP._readLocal(data, roff, out, csize, usize);
} //console.log(out);
return out;
};
UZIP._readLocal = function (data, o, out, csize, usize) {
let rUs = UZIP.bin.readUshort,
rUi = UZIP.bin.readUint;
let sign = rUi(data, o);
o += 4;
let ver = rUs(data, o);
o += 2;
let gpflg = rUs(data, o);
o += 2; //if((gpflg&8)!=0) throw "unknown sizes";
let cmpr = rUs(data, o);
o += 2;
let time = rUi(data, o);
o += 4;
let crc32 = rUi(data, o);
o += 4; //let csize = rUi(data, o); o+=4;
//let usize = rUi(data, o); o+=4;
o += 8;
let nlen = rUs(data, o);
o += 2;
let elen = rUs(data, o);
o += 2;
let name = UZIP.bin.readUTF8(data, o, nlen);
o += nlen;
o += elen; //console.log(sign.toString(16), ver, gpflg, cmpr, crc32.toString(16), "csize, usize", csize, usize, nlen, elen, name, o);
let file = new Uint8Array(data.buffer, o);
if (cmpr == 0) out[name] = new Uint8Array(file.buffer.slice(o, o + csize));else if (cmpr == 8) {
let buf = new Uint8Array(usize);
UZIP.inflateRaw(file, buf); //let nbuf = pako["inflateRaw"](file);
//for(let i=0; i<buf.length; i++) if(buf[i]!=nbuf[i]) { console.log(buf.length, nbuf.length, usize, i); throw "e"; }
out[name] = buf;
} else throw 'unknown compression method: ' + cmpr;
};
UZIP.inflateRaw = function (file, buf) {
return UZIP.F.inflate(file, buf);
};
UZIP.inflate = function (file, buf) {
let CMF = file[0],
FLG = file[1];
return UZIP.inflateRaw(new Uint8Array(file.buffer, file.byteOffset + 2, file.length - 6), buf);
};
UZIP.deflate = function (data, opts
/*, buf, off*/
) {
if (opts == null) opts = {
level: 6
};
let off = 0,
buf = new Uint8Array(50 + Math.floor(data.length * 1.1));
buf[off] = 120;
buf[off + 1] = 156;
off += 2;
off = UZIP.F.deflateRaw(data, buf, off, opts.level);
let crc = UZIP.adler(data, 0, data.length);
buf[off + 0] = crc >>> 24 & 255;
buf[off + 1] = crc >>> 16 & 255;
buf[off + 2] = crc >>> 8 & 255;
buf[off + 3] = crc >>> 0 & 255;
return new Uint8Array(buf.buffer, 0, off + 4);
};
UZIP.deflateRaw = function (data, opts) {
if (opts == null) opts = {
level: 6
};
let buf = new Uint8Array(50 + Math.floor(data.length * 1.1));
let off;
off = UZIP.F.deflateRaw(data, buf, off, opts.level);
return new Uint8Array(buf.buffer, 0, off);
};
UZIP.encode = function (obj) {
let tot = 0,
wUi = UZIP.bin.writeUint,
wUs = UZIP.bin.writeUshort;
let zpd = {};
for (let p in obj) {
let cpr = !UZIP._noNeed(p),
buf = obj[p],
crc = UZIP.crc.crc(buf, 0, buf.length);
zpd[p] = {
cpr: cpr,
usize: buf.length,
crc: crc,
file: cpr ? UZIP.deflateRaw(buf) : buf
};
}
for (let p in zpd) tot += zpd[p].file.length + 30 + 46 + 2 * UZIP.bin.sizeUTF8(p);
tot += 22;
let data = new Uint8Array(tot),
o = 0;
let fof = [];
for (let p in zpd) {
let file = zpd[p];
fof.push(o);
o = UZIP._writeHeader(data, o, p, file, 0);
}
let i = 0,
ioff = o;
for (let p in zpd) {
let file = zpd[p];
fof.push(o);
o = UZIP._writeHeader(data, o, p, file, 1, fof[i++]);
}
let csize = o - ioff;
wUi(data, o, 0x06054b50);
o += 4;
o += 4; // disks
wUs(data, o, i);
o += 2;
wUs(data, o, i);
o += 2; // number of c d records
wUi(data, o, csize);
o += 4;
wUi(data, o, ioff);
o += 4;
o += 2;
return data.buffer;
}; // no need to compress .PNG, .ZIP, .JPEG ....
UZIP._noNeed = function (fn) {
let ext = fn.split('.').pop().toLowerCase();
return 'png,jpg,jpeg,zip'.indexOf(ext) != -1;
};
UZIP._writeHeader = function (data, o, p, obj, t, roff) {
let wUi = UZIP.bin.writeUint,
wUs = UZIP.bin.writeUshort;
let file = obj.file;
wUi(data, o, t == 0 ? 0x04034b50 : 0x02014b50);
o += 4; // sign
if (t == 1) o += 2; // ver made by
wUs(data, o, 20);
o += 2; // ver
wUs(data, o, 0);
o += 2; // gflip
wUs(data, o, obj.cpr ? 8 : 0);
o += 2; // cmpr
wUi(data, o, 0);
o += 4; // time
wUi(data, o, obj.crc);
o += 4; // crc32
wUi(data, o, file.length);
o += 4; // csize
wUi(data, o, obj.usize);
o += 4; // usize
wUs(data, o, UZIP.bin.sizeUTF8(p));
o += 2; // nlen
wUs(data, o, 0);
o += 2; // elen
if (t == 1) {
o += 2; // comment length
o += 2; // disk number
o += 6; // attributes
wUi(data, o, roff);
o += 4; // usize
}
let nlen = UZIP.bin.writeUTF8(data, o, p);
o += nlen;
if (t == 0) {
data.set(file, o);
o += file.length;
}
return o;
};
UZIP.crc = {
table: function () {
let tab = new Uint32Array(256);
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) {
if (c & 1) c = 0xedb88320 ^ c >>> 1;else c = c >>> 1;
}
tab[n] = c;
}
return tab;
}(),
update: function (c, buf, off, len) {
for (let i = 0; i < len; i++) c = UZIP.crc.table[(c ^ buf[off + i]) & 0xff] ^ c >>> 8;
return c;
},
crc: function (b, o, l) {
return UZIP.crc.update(0xffffffff, b, o, l) ^ 0xffffffff;
}
};
UZIP.adler = function (data, o, len) {
let a = 1,
b = 0;
let off = o,
end = o + len;
while (off < end) {
let eend = Math.min(off + 5552, end);
while (off < eend) {
a += data[off++];
b += a;
}
a = a % 65521;
b = b % 65521;
}
return b << 16 | a;
};
UZIP.bin = {
readUshort: function (buff, p) {
return buff[p] | buff[p + 1] << 8;
},
writeUshort: function (buff, p, n) {
buff[p] = n & 255;
buff[p + 1] = n >> 8 & 255;
},
readUint: function (buff, p) {
return buff[p + 3] * (256 * 256 * 256) + (buff[p + 2] << 16 | buff[p + 1] << 8 | buff[p]);
},
writeUint: function (buff, p, n) {
buff[p] = n & 255;
buff[p + 1] = n >> 8 & 255;
buff[p + 2] = n >> 16 & 255;
buff[p + 3] = n >> 24 & 255;
},
readASCII: function (buff, p, l) {
let s = '';
for (let i = 0; i < l; i++) s += String.fromCharCode(buff[p + i]);
return s;
},
writeASCII: function (data, p, s) {
for (let i = 0; i < s.length; i++) data[p + i] = s.charCodeAt(i);
},
pad: function (n) {
return n.length < 2 ? '0' + n : n;
},
readUTF8: function (buff, p, l) {
let s = '',
ns;
for (let i = 0; i < l; i++) s += '%' + UZIP.bin.pad(buff[p + i].toString(16));
try {
ns = decodeURIComponent(s);
} catch (e) {
return UZIP.bin.readASCII(buff, p, l);
}
return ns;
},
writeUTF8: function (buff, p, str) {
let strl = str.length,
i = 0;
for (let ci = 0; ci < strl; ci++) {
let code = str.charCodeAt(ci);
if ((code & 0xffffffff - (1 << 7) + 1) == 0) {
buff[p + i] = code;
i++;
} else if ((code & 0xffffffff - (1 << 11) + 1) == 0) {
buff[p + i] = 192 | code >> 6;
buff[p + i + 1] = 128 | code >> 0 & 63;
i += 2;
} else if ((code & 0xffffffff - (1 << 16) + 1) == 0) {
buff[p + i] = 224 | code >> 12;
buff[p + i + 1] = 128 | code >> 6 & 63;
buff[p + i + 2] = 128 | code >> 0 & 63;
i += 3;
} else if ((code & 0xffffffff - (1 << 21) + 1) == 0) {
buff[p + i] = 240 | code >> 18;
buff[p + i + 1] = 128 | code >> 12 & 63;
buff[p + i + 2] = 128 | code >> 6 & 63;
buff[p + i + 3] = 128 | code >> 0 & 63;
i += 4;
} else throw 'e';
}
return i;
},
sizeUTF8: function (str) {
let strl = str.length,
i = 0;
for (let ci = 0; ci < strl; ci++) {
let code = str.charCodeAt(ci);
if ((code & 0xffffffff - (1 << 7) + 1) == 0) {
i++;
} else if ((code & 0xffffffff - (1 << 11) + 1) == 0) {
i += 2;
} else if ((code & 0xffffffff - (1 << 16) + 1) == 0) {
i += 3;
} else if ((code & 0xffffffff - (1 << 21) + 1) == 0) {
i += 4;
} else throw 'e';
}
return i;
}
};
UZIP.F = {};
UZIP.F.deflateRaw = function (data, out, opos, lvl) {
let opts = [
/*
ush good_length; /* reduce lazy search above this match length
ush max_lazy; /* do not perform lazy search above this match length
ush nice_length; /* quit search above this match length
*/
/* good lazy nice chain */
/* 0 *