Skip to content
Snippets Groups Projects
Verified Commit 7894b8c1 authored by João Magalhães's avatar João Magalhães :rocket:
Browse files

feat: initial support for audio in the web front-end

There are a series of hacks involved in making a constant stream of audio data to work OK with web audio.
parent d53ab1df
No related branches found
No related tags found
1 merge request!19Initial tentative audio support 🔉
Pipeline #2276 failed
......@@ -80,6 +80,13 @@ const KEYS_NAME: Record<string, number> = {
const ROM_PATH = require("../../../res/roms/pocket.gb");
// @TODO: check if this is the right place for this struct
type AudioChunk = {
source: AudioBufferSourceNode;
playTime: number;
duration: number;
};
/**
* Top level class that controls the emulator behaviour
* and "joins" all the elements together to bring input/output
......@@ -115,6 +122,13 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
private romSize = 0;
private cartridge: Cartridge | null = null;
// @TODO: try to think where does this belong
private audioContext = new AudioContext({
sampleRate: 44100
});
private audioChunks: AudioChunk[] = [];
private nextPlayTime = 0.0;
/**
* Associative map for extra settings to be used in
* opaque local storage operations, associated setting
......@@ -226,6 +240,10 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
}
tick(currentTime: number, pending: number, cycles = 70224) {
// in case the reference to the system is not set then
// returns the control flow immediately (not possible to tick)
if (!this.gameBoy) return pending;
// in case the time to draw the next frame has not been
// reached the flush of the "tick" logic is skipped
if (currentTime < this.nextTickTime) return pending;
......@@ -245,19 +263,19 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
// runs the Game Boy clock, this operations should
// include the advance of both the CPU and the PPU
const tickCycles = this.gameBoy?.clock() ?? 0;
const tickCycles = this.gameBoy.clock();
counterCycles += tickCycles;
// in case the current PPU mode is VBlank and the
// frame is different from the previously rendered
// one then it's time to update the canvas
if (
this.gameBoy?.ppu_mode() === PpuMode.VBlank &&
this.gameBoy?.ppu_frame() !== lastFrame
this.gameBoy.ppu_mode() === PpuMode.VBlank &&
this.gameBoy.ppu_frame() !== lastFrame
) {
// updates the reference to the last frame index
// to be used for comparison in the next tick
lastFrame = this.gameBoy?.ppu_frame();
lastFrame = this.gameBoy.ppu_frame();
// triggers the frame event indicating that
// a new frame is now available for drawing
......@@ -304,6 +322,61 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
);
ticks = Math.max(ticks, 1);
// --- START OF THE AUDIO CODE
const channels = 2;
const internalBuffer = this.gameBoy.audio_buffer_eager(true);
const audioBuffer = this.audioContext.createBuffer(
channels,
internalBuffer.length,
44100
);
for (let channel = 0; channel < channels; channel++) {
const channelBuffer = audioBuffer.getChannelData(channel);
for (let index = 0; index < internalBuffer.length; index++) {
channelBuffer[index] = internalBuffer[index] / 100.0;
}
}
// @todo check this code so see if it makes sense
// makes sure that we're not too far away from the audio
// and if that's the case drops some of the audio to regain
// some sync, this is required because of time hogging
const audioCurrentTime = this.audioContext.currentTime;
if (
this.nextPlayTime > audioCurrentTime + 0.05 ||
this.nextPlayTime < audioCurrentTime
) {
// @TODO: this is tricky as it cancels most of the code
this.audioChunks.forEach((chunk) => {
chunk.source.disconnect(this.audioContext.destination);
chunk.source.stop();
});
this.audioChunks = [];
this.nextPlayTime = audioCurrentTime;
}
const source = this.audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(this.audioContext.destination);
this.nextPlayTime = this.nextPlayTime || audioCurrentTime;
const chunk: AudioChunk = {
source: source,
playTime: this.nextPlayTime,
duration: audioBuffer.length / 44100.0
};
source.start(chunk.playTime);
this.nextPlayTime += chunk.duration;
this.audioChunks.push(chunk);
// ---- END OF THE AUDIO CODE
// updates the next update time according to the number of ticks
// that have elapsed since the last operation, this way this value
// can better be used to control the game loop
......
......@@ -164,8 +164,12 @@ impl GameBoy {
self.frame_buffer().to_vec()
}
pub fn audio_buffer_eager(&mut self) -> Vec<u8> {
self.audio_buffer().to_vec()
pub fn audio_buffer_eager(&mut self, clear: bool) -> Vec<u8> {
let buffer = self.audio_buffer().to_vec();
if clear {
self.clear_audio_buffer();
}
buffer
}
pub fn cartridge_eager(&mut self) -> Cartridge {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment