From 7894b8c1e8b286e5a5f7f6b90376e434d6a565d3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Magalh=C3=A3es?= <joamag@gmail.com>
Date: Tue, 28 Feb 2023 00:27:36 +0000
Subject: [PATCH] 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.

---
 frontends/web/ts/gb.ts | 81 +++++++++++++++++++++++++++++++++++++++---
 src/gb.rs              |  8 +++--
 2 files changed, 83 insertions(+), 6 deletions(-)

diff --git a/frontends/web/ts/gb.ts b/frontends/web/ts/gb.ts
index 3dec704e..277ab156 100644
--- a/frontends/web/ts/gb.ts
+++ b/frontends/web/ts/gb.ts
@@ -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
diff --git a/src/gb.rs b/src/gb.rs
index 015f0c92..7cbafab9 100644
--- a/src/gb.rs
+++ b/src/gb.rs
@@ -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 {
-- 
GitLab