Skip to content

Commit 774d9ab

Browse files
authored
Created a plugin API 🔌 (#287)
* Wrote up the basic plugin API * Added the graphics for tests * Confirmed plugins work, need to simply implement all hooks * Got audio working with the new plugin api * Left a todo * Finished up the new plugin API and implemented it in debugger * Fixed a bug in plugin API for ready
1 parent 06dee7a commit 774d9ab

File tree

10 files changed

+219
-39
lines changed

10 files changed

+219
-39
lines changed

demo/debugger/components/audio/visualizer.js

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export default class AudioVisualizer extends Component {
1818
this.title = title;
1919
this.analyserNodeGetDataKey = analyserNodeGetDataKey;
2020
this.audioVisualizations = {};
21+
22+
// Set up for our plugin
23+
this.pluginAnalysers = {};
24+
this.removePlugin = undefined;
2125
}
2226

2327
componentDidMount() {
@@ -27,7 +31,8 @@ export default class AudioVisualizer extends Component {
2731
// And configure for a plesant looking result
2832
const analyser = audioChannels[audioChannelKey].audioContext.createAnalyser();
2933
analyser.smoothingTimeConstant = 0.4;
30-
audioChannels[audioChannelKey].additionalAudioNodes.push(analyser);
34+
35+
this.pluginAnalysers[audioChannelKey] = analyser;
3136

3237
let visualization;
3338
if (this.analyserNodeGetDataKey.includes('Float')) {
@@ -52,21 +57,25 @@ export default class AudioVisualizer extends Component {
5257
};
5358
});
5459

60+
// Add a plugin for our analysers
61+
this.removePlugin = WasmBoy.addPlugin({
62+
name: 'WasmBoy Debugger Visualizer',
63+
audio: (audioContext, audioNode, channelId) => {
64+
return this.pluginAnalysers[channelId];
65+
}
66+
});
67+
5568
this.update();
5669
// Update at ~30fps
5770
updateInterval = setInterval(() => this.update(), 32);
5871
}
5972

6073
componentWillUnmount() {
6174
clearInterval(updateInterval);
62-
63-
Object.keys(audioChannels).forEach(audioChannelKey => {
64-
// Remove our analyser nodes
65-
audioChannels[audioChannelKey].additionalAudioNodes.splice(
66-
audioChannels[audioChannelKey].additionalAudioNodes.indexOf(this.audioVisualizations[audioChannelKey].analyser),
67-
1
68-
);
69-
});
75+
if (this.removePlugin) {
76+
this.removePlugin();
77+
this.removePlugin = undefined;
78+
}
7079
}
7180

7281
update() {

demo/debugger/wasmboy.js

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,22 +62,9 @@ const WasmBoyDefaultOptions = {
6262
console.log('Save State Callback Called! Only Logging this once... saveStateObject:', saveStateObject);
6363
saveStateCallbackCalled = true;
6464
}
65-
66-
// Function called everytime a savestate occurs
67-
// Used by the WasmBoySystemControls to show screenshots on save states
68-
let canvasElement;
69-
if (isMobileCanvas) {
70-
canvasElement = getMobileCanvasElement();
71-
} else {
72-
canvasElement = getDesktopCanvasElement();
73-
}
74-
75-
if (canvasElement) {
76-
saveStateObject.screenshotCanvasDataURL = canvasElement.toDataURL();
77-
}
7865
},
7966
breakpointCallback: () => {
80-
Pubx.get(PUBX_KEYS.NOTIFICATION).showNotification('Reach Breakpoint! 🛑');
67+
console.log('breakpoint Callback Called!');
8168
},
8269
onReady: () => {
8370
console.log('onReady Callback Called!');
@@ -159,3 +146,53 @@ export const WasmBoyUpdateCanvas = (isMobile, stateUpdateCallback) => {
159146

160147
return true;
161148
};
149+
150+
// WasmBoy Plugin
151+
const pluginCalled = {};
152+
const DebuggerPlugin = {
153+
name: 'WasmBoy Debugger',
154+
graphics: imageDataArray => {
155+
if (!pluginCalled.graphics) {
156+
console.log('Plugin "graphics" called! Only Logging this once...', imageDataArray);
157+
pluginCalled.graphics = true;
158+
}
159+
},
160+
audio: (audioContext, masterAudioNode, channelId) => {
161+
if (!pluginCalled.audio) {
162+
console.log('Plugin "audio" called! Only Logging this once...', audioContext, masterAudioNode, channelId);
163+
pluginCalled.audio = true;
164+
}
165+
},
166+
saveState: saveStateObject => {
167+
if (!pluginCalled.saveState) {
168+
console.log('Plugin "saveState" called! Only Logging this once...', saveStateObject);
169+
pluginCalled.saveState = true;
170+
}
171+
172+
// Function called everytime a savestate occurs
173+
// Used by the WasmBoySystemControls to show screenshots on save states
174+
let canvasElement;
175+
if (isMobileCanvas) {
176+
canvasElement = getMobileCanvasElement();
177+
} else {
178+
canvasElement = getDesktopCanvasElement();
179+
}
180+
181+
if (canvasElement) {
182+
saveStateObject.screenshotCanvasDataURL = canvasElement.toDataURL();
183+
}
184+
},
185+
setCanvas: canvasElement => {
186+
console.log('Plugin "setCanvas" called!', canvasElement);
187+
},
188+
breakpoint: () => {
189+
console.log('Plugin "breakpoint" called!');
190+
Pubx.get(PUBX_KEYS.NOTIFICATION).showNotification('Reach Breakpoint! 🛑');
191+
},
192+
ready: () => console.log('Plugin "ready" called!'),
193+
play: () => console.log('Plugin "play" called!'),
194+
pause: () => console.log('Plugin "pause" called!'),
195+
loadedAndStarted: () => console.log('Plugin "loadedAndStarted" called!')
196+
};
197+
198+
WasmBoy.addPlugin(DebuggerPlugin);

lib/audio/gbchannel.js

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Gameboy Channel Output
22
// With outputting to Web Audio API
33

4+
import { WasmBoyPlugins } from '../plugins/plugins';
5+
46
import toWav from 'audiobuffer-to-wav';
57

68
// Define our performance constants
@@ -38,9 +40,6 @@ export default class GbChannelWebAudio {
3840
this.recordingRightBuffers = undefined;
3941
this.recordingAudioBuffer = undefined;
4042
this.recordingAnchor = undefined;
41-
42-
// Additional Audio Nodes for connecting
43-
this.additionalAudioNodes = [];
4443
}
4544

4645
createAudioContextIfNone() {
@@ -110,27 +109,36 @@ export default class GbChannelWebAudio {
110109
// Set our playback rate for time resetretching
111110
source.playbackRate.setValueAtTime(playbackRate, this.audioContext.currentTime);
112111

113-
let lastAdditionalNode = source;
114-
this.additionalAudioNodes.forEach(node => {
115-
lastAdditionalNode.connect(node);
116-
lastAdditionalNode = node;
117-
});
118-
119-
// Connect to our gain node for volume control
112+
// Set up our "final node", as in the one that will be connected
113+
// to the destination (output)
120114
let finalNode = source;
121-
if (this.gainNode) {
122-
finalNode = this.gainNode;
123-
lastAdditionalNode.connect(this.gainNode);
124-
}
125115

126-
// Call our callback, if we have one
116+
// Call our callback/plugins, if we have one
127117
if (updateAudioCallback) {
128-
const responseNode = updateAudioCallback(this.audioContext, this.gainNode, this.id);
118+
const responseNode = updateAudioCallback(this.audioContext, finalNode, this.id);
129119
if (responseNode) {
130120
finalNode = responseNode;
131121
}
132122
}
133123

124+
// Call our plugins
125+
WasmBoyPlugins.runHook({
126+
key: 'audio',
127+
params: [this.audioContext, finalNode, this.id],
128+
callback: hookResponse => {
129+
if (hookResponse) {
130+
finalNode.connect(hookResponse);
131+
finalNode = hookResponse;
132+
}
133+
}
134+
});
135+
136+
// Lastly, apply our gain node to mute/unmute
137+
if (this.gainNode) {
138+
finalNode.connect(this.gainNode);
139+
finalNode = this.gainNode;
140+
}
141+
134142
// connect the AudioBufferSourceNode to the
135143
// destination so we can hear the sound
136144
finalNode.connect(this.audioContext.destination);

lib/graphics/graphics.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
// Handles rendering graphics using the HTML5 Canvas
2+
3+
import { WasmBoyPlugins } from '../plugins/plugins';
4+
15
import { GAMEBOY_CAMERA_WIDTH, GAMEBOY_CAMERA_HEIGHT } from './constants';
26

37
import { WORKER_MESSAGE_TYPE } from '../worker/constants';
@@ -99,6 +103,12 @@ class WasmBoyGraphicsService {
99103
this.updateGraphicsCallback(this.imageDataArray);
100104
}
101105

106+
// Set the imageDataArray to our plugins
107+
WasmBoyPlugins.runHook({
108+
key: 'graphics',
109+
params: [this.imageDataArray]
110+
});
111+
102112
// Add our new imageData
103113
this.canvasImageData.data.set(this.imageDataArray);
104114

lib/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Build our public lib api
22
import { WasmBoyLib } from './wasmboy/wasmboy';
3+
import { WasmBoyPlugins } from './plugins/plugins';
34
import { WasmBoyAudio } from './audio/audio';
45
import { WasmBoyController } from './controller/controller';
56
import { WasmBoyMemory } from './memory/memory';
@@ -32,6 +33,7 @@ export const WasmBoy = {
3233
play: WasmBoyLib.play.bind(WasmBoyLib),
3334
pause: WasmBoyLib.pause.bind(WasmBoyLib),
3435
reset: WasmBoyLib.reset.bind(WasmBoyLib),
36+
addPlugin: WasmBoyPlugins.addPlugin.bind(WasmBoyPlugins),
3537
isPlaying: () => {
3638
return !WasmBoyLib.paused;
3739
},

lib/memory/state.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Functions here are depedent on WasmBoyMemory state.
22
// Thus me bound using .bind() on functions
33

4+
import { WasmBoyPlugins } from '../plugins/plugins';
5+
46
// Will save the state in parts, to easy memory map changes:
57
// https://docs.google.com/spreadsheets/d/17xrEzJk5-sCB9J2mMJcVnzhbE-XH_NvczVSQH9OHvRk/edit?usp=sharing
68
const WASMBOY_SAVE_STATE_SCHEMA = {
@@ -33,5 +35,10 @@ export function getSaveState() {
3335
this.saveStateCallback(saveState);
3436
}
3537

38+
WasmBoyPlugins.runHook({
39+
key: 'saveState',
40+
params: [saveState]
41+
});
42+
3643
return saveState;
3744
}

lib/plugins/plugins.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// API For adding plugins for WasmBoy
2+
// Should follow the Rollup Plugin API
3+
// https://rollupjs.org/guide/en#plugins
4+
5+
// Plugins have the following supported hooks
6+
// And properties
7+
const WASMBOY_PLUGIN = {
8+
name: 'wasmboy-plugin REQUIRED',
9+
graphics: rgbaArray => {}, // Returns undefined. Edit object in place
10+
audio: (audioContext, headAudioNode, channelId) => {}, // Return AudioNode, which will be connected to the destination node eventually.
11+
saveState: saveStateObject => {}, // Returns undefined. Edit object in place.
12+
setCanvas: canvasElement => {}, // Returns undefined. Edit object in place.
13+
breakpoint: () => {},
14+
ready: () => {},
15+
play: () => {},
16+
pause: () => {},
17+
loadedAndStarted: () => {}
18+
};
19+
20+
class WasmBoyPluginsService {
21+
constructor() {
22+
this.plugins = {};
23+
this.pluginIdCounter = 0;
24+
}
25+
26+
addPlugin(pluginObject) {
27+
// Verify the plugin
28+
if (!pluginObject && typeof pluginObject !== 'object') {
29+
throw new Error('Invalid Plugin Object');
30+
}
31+
32+
if (!pluginObject.name) {
33+
throw new Error('Added plugin must have a "name" property');
34+
}
35+
36+
// Add the plugin to our plugin container
37+
const id = this.pluginIdCounter;
38+
this.plugins[this.pluginIdCounter] = pluginObject;
39+
this.pluginIdCounter++;
40+
41+
// Return a function to remove the plugin
42+
return () => {
43+
this.removePlugin(id);
44+
};
45+
}
46+
47+
removePlugin(id) {
48+
delete this.plugins[id];
49+
}
50+
51+
runHook(hookConfig) {
52+
if (!WASMBOY_PLUGIN[hookConfig.key] || typeof WASMBOY_PLUGIN[hookConfig.key] !== 'function') {
53+
throw new Error('No such hook as ' + hookConfig.key);
54+
}
55+
56+
Object.keys(this.plugins).forEach(pluginKey => {
57+
const plugin = this.plugins[pluginKey];
58+
59+
if (plugin[hookConfig.key]) {
60+
let hookResponse = undefined;
61+
try {
62+
hookResponse = plugin[hookConfig.key].apply(null, hookConfig.params);
63+
} catch (e) {
64+
console.error(`There was an error running the '${hookConfig.key}' hook, on the ${plugin.name} plugin.`);
65+
console.error(e);
66+
}
67+
68+
if (hookConfig.callback) {
69+
hookConfig.callback(hookResponse);
70+
}
71+
}
72+
});
73+
}
74+
}
75+
76+
export const WasmBoyPlugins = new WasmBoyPluginsService();

lib/wasmboy/load.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Functions here are depedent on WasmBoyMemory state.
22
// Thus me bound using .bind() on functions
33

4+
import { WasmBoyPlugins } from '../plugins/plugins';
5+
46
// WasmBoy Modules
57
import { WasmBoyGraphics } from '../graphics/graphics';
68
import { WasmBoyAudio } from '../audio/audio';
@@ -113,6 +115,9 @@ export function loadROMToWasmBoy(ROM, fetchHeaders) {
113115
if (this.options.onReady) {
114116
this.options.onReady();
115117
}
118+
WasmBoyPlugins.runHook({
119+
key: 'ready'
120+
});
116121
} else {
117122
// Finally intialize all of our services
118123
// Initialize our services
@@ -132,6 +137,9 @@ export function loadROMToWasmBoy(ROM, fetchHeaders) {
132137
if (this.options.onReady) {
133138
this.options.onReady();
134139
}
140+
WasmBoyPlugins.runHook({
141+
key: 'ready'
142+
});
135143
}
136144
};
137145

lib/wasmboy/onmessage.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Functions here are depedent on WasmBoyMemory state.
22
// Thus me bound using .bind() on functions
33

4+
import { WasmBoyPlugins } from '../plugins/plugins';
5+
46
import { WORKER_MESSAGE_TYPE } from '../worker/constants';
57
import { getEventData } from '../worker/util';
68
import { runWasmExport, getWasmMemorySection, getWasmConstant } from '../debug/debug';
@@ -42,6 +44,10 @@ export function libWorkerOnMessage(event) {
4244
if (this.options.breakpointCallback) {
4345
this.options.breakpointCallback();
4446
}
47+
48+
WasmBoyPlugins.runHook({
49+
key: 'breakpoint'
50+
});
4551
};
4652
breakpointTask();
4753
return;

0 commit comments

Comments
 (0)