diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 399b116dcedec391eec972a8efd4202c2ecb9670..0a11b8d400b86630ace923b54ad29ea545e70387 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -12,6 +12,7 @@ jobs:
         rust-version: [
           "1.56.1",
           "1.60.0",
+          "1.61.0",
           "1.62.0",
           "1.63.0",
           "1.64.0",
@@ -45,9 +46,6 @@ jobs:
     strategy:
       matrix:
         rust-version: [
-          "1.56.1",
-          "1.60.0",
-          "1.62.0",
           "1.63.0",
           "1.64.0",
           "1.65.0",
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 56a8f2aa890e62f916b7e29224942fdc9eda76f1..715499972b48f242eb26c1566e7b6c916544632a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -42,7 +42,7 @@ build-wasm:
   stage: build
   parallel:
     matrix:
-      - RUST_VERSION: ["1.60.0"]
+      - RUST_VERSION: ["1.63.0"]
   script:
     - rustup toolchain install $RUST_VERSION
     - rustup override set $RUST_VERSION
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ee5b5a22c1ee39b1263dda8ae75e0d2faf83ecf..3a7ac6225b842c8f5002717f6e0405405e1c01f4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
-* Support for WASM engine version printing
+*
 
 ### Changed
 
@@ -19,6 +19,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 *
 
+## [0.7.1] - 2023-03-02
+
+### Changed
+
+* Bumped emukit, fixing a lot of bugs
+
+## [0.7.0] - 2023-03-01
+
+### Added
+
+* Support for Audio 🔈!!! - [#12](https://gitlab.stage.hive.pt/joamag/boytacean/-/issues/12)
+* Support for WASM engine version printing
+
 ## [0.6.12] - 2023-02-21
 
 ### Fixed
diff --git a/Cargo.toml b/Cargo.toml
index 65a09a8928d7cd4ff834287642e7e77677890399..ab7d0cc5425e3371bb6c18eec6366e2b82813e63 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "boytacean"
 description = "A Game Boy emulator that is written in Rust."
-version = "0.6.12"
+version = "0.7.1"
 authors = ["João Magalhães <joamag@gmail.com>"]
 license = "Apache-2.0"
 repository = "https://github.com/joamag/boytacean"
diff --git a/README.md b/README.md
index dc449caa07a4a76afd5b754904794ae391c47c38..5506448669d561bbf8d6f9487df007f96683e5ef 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@ A Game Boy emulator that is written in Rust 🦀.
 * Game Boy (DMG) emulation
 * Simple navigable source-code
 * Web and SDL front-ends
+* Audio, with a pretty accurate APU
 * Support for multiple MBCs: MBC1, MBC2, MBC3, and MBC5
 * Variable CPU clock speed
 * Accurate PPU - passes [dmg-acid2](https://github.com/mattcurrie/dmg-acid2) tests
@@ -27,7 +28,6 @@ For the Web front-end...
 
 What's missing...
 
-* Audio emulation APU
 * Game Boy Color (GBC) emulation
 
 ## Deployments
diff --git a/frontends/sdl/Cargo.toml b/frontends/sdl/Cargo.toml
index d762d0807012fc37495fef8fbc43c9ee729a7a3b..03177352b9e3ff885fb6eeba816ff0e0608531ff 100644
--- a/frontends/sdl/Cargo.toml
+++ b/frontends/sdl/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "boytacean-sdl"
-version = "0.6.12"
+version = "0.7.1"
 authors = ["João Magalhães <joamag@gmail.com>"]
 description = "An SDL frontend for Boytacen"
 license = "Apache-2.0"
@@ -14,6 +14,10 @@ path = "../.."
 version = "0.35"
 features = ["ttf", "image", "gfx", "mixer", "static-link", "use-vcpkg"]
 
+# If the vcpkg version of SDL2 does not work (eg: display not found error)
+# then try dynamic linking SDL2 using the following features
+# features = ["ttf", "image", "gfx", "mixer", "bundled"]
+
 [package.metadata.vcpkg]
 dependencies = ["sdl2", "sdl2-image[libjpeg-turbo,tiff,libwebp]", "sdl2-ttf", "sdl2-gfx", "sdl2-mixer"]
 git = "https://github.com/microsoft/vcpkg"
diff --git a/frontends/sdl/README.md b/frontends/sdl/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..ac9ab6a5d8636e34a8274ef37750aedebac25738
--- /dev/null
+++ b/frontends/sdl/README.md
@@ -0,0 +1,18 @@
+# Boytacean SDL
+
+## Build
+
+To be able to run the `cargo build` one must first install a local version of `vcpkg` using:
+
+```bash
+cargo install cargo-vcpkg
+cargo vcpkg build
+cargo build
+```
+
+Then you can use the following command to build and run Boytacean SDL:
+
+```bash
+cargo build
+cargo run
+```
diff --git a/frontends/sdl/src/audio.rs b/frontends/sdl/src/audio.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c5c113c0b64aa6b1b465445e5570743a9cefcefc
--- /dev/null
+++ b/frontends/sdl/src/audio.rs
@@ -0,0 +1,34 @@
+use sdl2::{
+    audio::{AudioQueue, AudioSpecDesired},
+    AudioSubsystem, Sdl,
+};
+
+pub struct Audio {
+    pub device: AudioQueue<f32>,
+    pub audio_subsystem: AudioSubsystem,
+}
+
+impl Audio {
+    pub fn new(sdl: &Sdl) -> Self {
+        let audio_subsystem = sdl.audio().unwrap();
+
+        let desired_spec = AudioSpecDesired {
+            freq: Some(44100),
+            channels: Some(2),
+            samples: Some(4096),
+        };
+
+        // creates the queue that is going to be used to update the
+        // audio stream with new values during the main loop
+        let device = audio_subsystem.open_queue(None, &desired_spec).unwrap();
+
+        // starts the playback by resuming the audio
+        // device's activity
+        device.resume();
+
+        Self {
+            device,
+            audio_subsystem,
+        }
+    }
+}
diff --git a/frontends/sdl/src/util.rs b/frontends/sdl/src/graphics.rs
similarity index 79%
rename from frontends/sdl/src/util.rs
rename to frontends/sdl/src/graphics.rs
index df6a550f781e486ccdf4b4b914b1b6501ee131c4..62941ddb9b1af3fa63681236347acfa921b68c50 100644
--- a/frontends/sdl/src/util.rs
+++ b/frontends/sdl/src/graphics.rs
@@ -1,6 +1,6 @@
 use sdl2::{
     render::Canvas, rwops::RWops, surface::Surface, sys::image, ttf::Sdl2TtfContext, video::Window,
-    AudioSubsystem, EventPump, TimerSubsystem, VideoSubsystem,
+    AudioSubsystem, EventPump, Sdl, TimerSubsystem, VideoSubsystem,
 };
 
 /// Structure that provides the complete set of Graphics
@@ -19,10 +19,17 @@ impl Graphics {
     /// Start the SDL sub-system and all of its structure and returns
     /// a structure with all the needed stuff to handle SDL graphics
     /// and sound.
-    pub fn new(title: &str, width: u32, height: u32, scale: f32) -> Self {
+    pub fn new(
+        sdl: &Sdl,
+        title: &str,
+        width: u32,
+        height: u32,
+        scale: f32,
+        accelerated: bool,
+        vsync: bool,
+    ) -> Self {
         // initializes the SDL sub-system, making it ready to be
         // used for display of graphics and audio
-        let sdl = sdl2::init().unwrap();
         let video_subsystem = sdl.video().unwrap();
         let timer_subsystem = sdl.timer().unwrap();
         let audio_subsystem = sdl.audio().unwrap();
@@ -42,14 +49,16 @@ impl Graphics {
             .build()
             .unwrap();
 
-        // creates an accelerated canvas to be used in the drawing
+        // creates a canvas (according to spec) to be used in the drawing
         // then clears it so that is can be presented empty initially
-        let mut canvas = window
-            .into_canvas()
-            .accelerated()
-            .present_vsync()
-            .build()
-            .unwrap();
+        let mut canvas_builder = window.into_canvas();
+        if accelerated {
+            canvas_builder = canvas_builder.accelerated();
+        }
+        if vsync {
+            canvas_builder = canvas_builder.present_vsync();
+        }
+        let mut canvas = canvas_builder.build().unwrap();
         canvas.set_logical_size(width, height).unwrap();
         canvas.clear();
 
diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs
index da532325b882bd72e864848c4cb2fe573837cd72..471e84f59dfafef77fd46fa5563b238f78a51b49 100644
--- a/frontends/sdl/src/main.rs
+++ b/frontends/sdl/src/main.rs
@@ -1,18 +1,18 @@
 #![allow(clippy::uninlined_format_args)]
 
+pub mod audio;
 pub mod data;
-pub mod util;
+pub mod graphics;
 
+use audio::Audio;
 use boytacean::{
-    gb::GameBoy,
+    gb::{AudioProvider, GameBoy},
     pad::PadKey,
     ppu::{PaletteInfo, PpuMode, DISPLAY_HEIGHT, DISPLAY_WIDTH},
 };
-use sdl2::{event::Event, keyboard::Keycode, pixels::PixelFormatEnum};
+use graphics::{surface_from_bytes, Graphics};
+use sdl2::{event::Event, keyboard::Keycode, pixels::PixelFormatEnum, Sdl};
 use std::{cmp::max, time::SystemTime};
-use util::Graphics;
-
-use crate::util::surface_from_bytes;
 
 /// The scale at which the screen is going to be drawn
 /// meaning the ratio between Game Boy resolution and
@@ -22,6 +22,10 @@ const SCREEN_SCALE: f32 = 2.0;
 /// The base title to be used in the window.
 static TITLE: &str = "Boytacean";
 
+/// Base audio volume to be used as the basis of the
+/// amplification level of the volume
+static VOLUME: f32 = 64.0;
+
 pub struct Benchmark {
     count: usize,
 }
@@ -40,29 +44,30 @@ impl Default for Benchmark {
 
 pub struct Emulator {
     system: GameBoy,
-    graphics: Graphics,
+    graphics: Option<Graphics>,
+    audio: Option<Audio>,
+    title: &'static str,
     logic_frequency: u32,
     visual_frequency: f32,
     next_tick_time: f32,
     next_tick_time_i: u32,
+    features: Vec<&'static str>,
     palettes: [PaletteInfo; 3],
     palette_index: usize,
 }
 
 impl Emulator {
-    pub fn new(system: GameBoy, screen_scale: f32) -> Self {
+    pub fn new(system: GameBoy) -> Self {
         Self {
             system,
-            graphics: Graphics::new(
-                TITLE,
-                DISPLAY_WIDTH as u32,
-                DISPLAY_HEIGHT as u32,
-                screen_scale,
-            ),
+            graphics: None,
+            audio: None,
+            title: TITLE,
             logic_frequency: GameBoy::CPU_FREQ,
             visual_frequency: GameBoy::VISUAL_FREQ,
             next_tick_time: 0.0,
             next_tick_time_i: 0,
+            features: vec!["video", "audio", "no-vsync"],
             palettes: [
                 PaletteInfo::new(
                     "basic",
@@ -96,6 +101,32 @@ impl Emulator {
         }
     }
 
+    pub fn start(&mut self, screen_scale: f32) {
+        let sdl = sdl2::init().unwrap();
+        if self.features.contains(&"video") {
+            self.start_graphics(&sdl, screen_scale);
+        }
+        if self.features.contains(&"audio") {
+            self.start_audio(&sdl);
+        }
+    }
+
+    pub fn start_graphics(&mut self, sdl: &Sdl, screen_scale: f32) {
+        self.graphics = Some(Graphics::new(
+            sdl,
+            self.title,
+            DISPLAY_WIDTH as u32,
+            DISPLAY_HEIGHT as u32,
+            screen_scale,
+            !self.features.contains(&"no-accelerated"),
+            !self.features.contains(&"no-vsync"),
+        ));
+    }
+
+    pub fn start_audio(&mut self, sdl: &Sdl) {
+        self.audio = Some(Audio::new(sdl));
+    }
+
     pub fn load_rom(&mut self, path: &str) {
         let rom = self.system.load_rom_file(path);
         println!(
@@ -103,8 +134,10 @@ impl Emulator {
             rom
         );
         self.graphics
+            .as_mut()
+            .unwrap()
             .window_mut()
-            .set_title(format!("{} [{}]", TITLE, rom.title()).as_str())
+            .set_title(format!("{} [{}]", self.title, rom.title()).as_str())
             .unwrap();
     }
 
@@ -129,6 +162,11 @@ impl Emulator {
         );
     }
 
+    pub fn toggle_audio(&mut self) {
+        let apu_enabled = self.system.get_apu_enabled();
+        self.system.set_apu_enabled(!apu_enabled);
+    }
+
     pub fn toggle_palette(&mut self) {
         self.system
             .ppu()
@@ -140,15 +178,19 @@ impl Emulator {
         // updates the icon of the window to reflect the image
         // and style of the emulator
         let surface = surface_from_bytes(&data::ICON);
-        self.graphics.window_mut().set_icon(&surface);
+        self.graphics
+            .as_mut()
+            .unwrap()
+            .window_mut()
+            .set_icon(&surface);
 
         // creates an accelerated canvas to be used in the drawing
         // then clears it and presents it
-        self.graphics.canvas.present();
+        self.graphics.as_mut().unwrap().canvas.present();
 
         // creates a texture creator for the current canvas, required
         // for the creation of dynamic and static textures
-        let texture_creator = self.graphics.canvas.texture_creator();
+        let texture_creator = self.graphics.as_mut().unwrap().canvas.texture_creator();
 
         // creates the texture streaming that is going to be used
         // as the target for the pixel buffer
@@ -177,7 +219,7 @@ impl Emulator {
 
             // obtains an event from the SDL sub-system to be
             // processed under the current emulation context
-            while let Some(event) = self.graphics.event_pump.poll_event() {
+            while let Some(event) = self.graphics.as_mut().unwrap().event_pump.poll_event() {
                 match event {
                     Event::Quit { .. } => break 'main,
                     Event::KeyDown {
@@ -188,6 +230,10 @@ impl Emulator {
                         keycode: Some(Keycode::B),
                         ..
                     } => self.benchmark(Benchmark::default()),
+                    Event::KeyDown {
+                        keycode: Some(Keycode::T),
+                        ..
+                    } => self.toggle_audio(),
                     Event::KeyDown {
                         keycode: Some(Keycode::P),
                         ..
@@ -225,7 +271,7 @@ impl Emulator {
                 }
             }
 
-            let current_time = self.graphics.timer_subsystem.ticks();
+            let current_time = self.graphics.as_mut().unwrap().timer_subsystem.ticks();
 
             if current_time >= self.next_tick_time_i {
                 // re-starts the counter cycles with the number of pending cycles
@@ -249,10 +295,13 @@ impl Emulator {
                         break;
                     }
 
-                    // runs the Game Boy clock, this operations should
-                    // include the advance of both the CPU and the PPU
+                    // runs the Game Boy clock, this operation should
+                    // include the advance of both the CPU, PPU, APU
+                    // and any other frequency based component of the system
                     counter_cycles += self.system.clock() as u32;
 
+                    // in case a V-Blank state has been reached a new frame is available
+                    // then the frame must be pushed into SDL for display
                     if self.system.ppu_mode() == PpuMode::VBlank
                         && self.system.ppu_frame() != last_frame
                     {
@@ -268,6 +317,22 @@ impl Emulator {
                         // is going to be used to detect for new frame presence
                         last_frame = self.system.ppu_frame();
                     }
+
+                    if let Some(audio) = self.audio.as_mut() {
+                        // obtains the new audio buffer and queues it into the audio
+                        // subsystem ready to be processed
+                        let audio_buffer = self
+                            .system
+                            .audio_buffer()
+                            .iter()
+                            .map(|v| *v as f32 / VOLUME)
+                            .collect::<Vec<f32>>();
+                        audio.device.queue_audio(&audio_buffer).unwrap();
+                    }
+
+                    // clears the audio buffer to prevent it from
+                    // "exploding" in size
+                    self.system.clear_audio_buffer();
                 }
 
                 // in case there's at least one new frame that was drawn during
@@ -279,15 +344,20 @@ impl Emulator {
                     // clears the graphics canvas, making sure that no garbage
                     // pixel data remaining in the pixel buffer, not doing this would
                     // create visual glitches in OSs like Mac OS X
-                    self.graphics.canvas.clear();
+                    self.graphics.as_mut().unwrap().canvas.clear();
 
                     // copies the texture that was created for the frame (during
                     // the loop part of the tick) to the canvas
-                    self.graphics.canvas.copy(&texture, None, None).unwrap();
+                    self.graphics
+                        .as_mut()
+                        .unwrap()
+                        .canvas
+                        .copy(&texture, None, None)
+                        .unwrap();
 
                     // presents the canvas effectively updating the screen
                     // information presented to the user
-                    self.graphics.canvas.present();
+                    self.graphics.as_mut().unwrap().canvas.present();
                 }
 
                 // calculates the number of ticks that have elapsed since the
@@ -309,9 +379,13 @@ impl Emulator {
                 self.next_tick_time_i = self.next_tick_time.ceil() as u32;
             }
 
-            let current_time = self.graphics.timer_subsystem.ticks();
+            let current_time = self.graphics.as_mut().unwrap().timer_subsystem.ticks();
             let pending_time = self.next_tick_time_i.saturating_sub(current_time);
-            self.graphics.timer_subsystem.delay(pending_time);
+            self.graphics
+                .as_mut()
+                .unwrap()
+                .timer_subsystem
+                .delay(pending_time);
         }
     }
 }
@@ -322,9 +396,11 @@ fn main() {
     let mut game_boy = GameBoy::new();
     game_boy.load_cgb(true);
 
-    // creates a new generic emulator structure loads the default
+    // creates a new generic emulator structure then starts
+    // both the video and audio sub-systems, loads default
     // ROM file and starts running it
-    let mut emulator = Emulator::new(game_boy, SCREEN_SCALE);
+    let mut emulator = Emulator::new(game_boy);
+    emulator.start(SCREEN_SCALE);
     emulator.load_rom("../../res/roms/pocket.gb");
     emulator.toggle_palette();
     emulator.run();
diff --git a/frontends/web/package.json b/frontends/web/package.json
index e949cc0eaa70a18b3a037165c9a1979628f23493..45278e03938956f263aa6fffd3a58fcf899e2d96 100644
--- a/frontends/web/package.json
+++ b/frontends/web/package.json
@@ -1,6 +1,6 @@
 {
     "name": "boytacean-web",
-    "version": "0.6.12",
+    "version": "0.7.1",
     "description": "The web version of Boytacean",
     "repository": {
         "type": "git",
@@ -21,10 +21,10 @@
         "@parcel/transformer-typescript-tsc": "^2.8.3",
         "@types/react": "^18.0.28",
         "@types/react-dom": "^18.0.11",
-        "@typescript-eslint/eslint-plugin": "^5.53.0",
-        "@typescript-eslint/parser": "^5.53.0",
-        "emukit": "^0.6.5",
-        "eslint": "^8.34.0",
+        "@typescript-eslint/eslint-plugin": "^5.54.0",
+        "@typescript-eslint/parser": "^5.54.0",
+        "emukit": "^0.7.1",
+        "eslint": "^8.35.0",
         "nodemon": "^2.0.20",
         "parcel": "^2.8.3",
         "prettier": "^2.8.4",
diff --git a/frontends/web/react/components/help/help.tsx b/frontends/web/react/components/help/help.tsx
index 42ea60c648b1ba260e1f9f6047485e595cbaae47..c3b0de50c5ffcd7ebe6a0046016847ac9ad1d999 100644
--- a/frontends/web/react/components/help/help.tsx
+++ b/frontends/web/react/components/help/help.tsx
@@ -67,6 +67,12 @@ export const HelpKeyboard: FC = () => (
             </span>
             Toggle on-screen keyboard
         </li>
+        <li>
+            <span className="key-container">
+                <span className="key">Ctrl + P</span>
+            </span>
+            Change screen palette
+        </li>
     </ul>
 );
 
@@ -79,7 +85,15 @@ export const HelpFaqs: FC = () => (
         </p>
         <h3>Why there's no sound?</h3>
         <p>
-            It's under development, I'm hopping to have it before end of 2023.
+            You need to click or touch the screen to start the{" "}
+            <Link
+                href="https://developer.mozilla.org/docs/Web/API/Web_Audio_API"
+                target="_blank"
+            >
+                Web Audio API
+            </Link>{" "}
+            support. That's just the way browsers work these days, security
+            first 😄.
         </p>
         <h3>Can I use my Xbox One/PS4/PS5 Gamepad?</h3>
         <p>
diff --git a/frontends/web/ts/gb.ts b/frontends/web/ts/gb.ts
index f376320f4ff09599c039b7d1075c7fba84d34eb4..dcde0e67798a246778a73e203c21ef4afb77f7a8 100644
--- a/frontends/web/ts/gb.ts
+++ b/frontends/web/ts/gb.ts
@@ -1,4 +1,5 @@
 import {
+    AudioSpecs,
     BenchmarkResult,
     Compilation,
     Compiler,
@@ -105,7 +106,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
     private paused = false;
     private nextTickTime = 0;
     private fps = 0;
-    private frameStart: number = new Date().getTime();
+    private frameStart: number = EmulatorBase.now();
     private frameCount = 0;
     private paletteIndex = 0;
     private storeCycles: number = LOGIC_HZ * STORE_RATE;
@@ -163,7 +164,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
 
             // obtains the current time, this value is going
             // to be used to compute the need for tick computation
-            let currentTime = new Date().getTime();
+            let currentTime = EmulatorBase.now();
 
             try {
                 pending = this.tick(
@@ -214,7 +215,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
 
             // calculates the amount of time until the next draw operation
             // this is the amount of time that is going to be pending
-            currentTime = new Date().getTime();
+            currentTime = EmulatorBase.now();
             const pendingTime = Math.max(this.nextTickTime - currentTime, 0);
 
             // waits a little bit for the next frame to be draw,
@@ -226,6 +227,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 +250,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
@@ -276,6 +281,11 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
             }
         }
 
+        // triggers the audio event, meaning that the audio should be
+        // processed for the current emulator, effectively emptying
+        // the audio buffer that is pending processing
+        this.trigger("audio");
+
         // increments the number of frames rendered in the current
         // section, this value is going to be used to calculate FPS
         this.frameCount += 1;
@@ -284,7 +294,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         // has been reached calculates the number of FPS and
         // flushes the value to the screen
         if (this.frameCount >= this.visualFrequency * FPS_SAMPLE_RATE) {
-            const currentTime = new Date().getTime();
+            const currentTime = EmulatorBase.now();
             const deltaTime = (currentTime - this.frameStart) / 1000;
             const fps = Math.round(this.frameCount / deltaTime);
             this.fps = fps;
@@ -500,6 +510,24 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         return this.gameBoy?.frame_buffer_eager() ?? new Uint8Array();
     }
 
+    get audioSpecs(): AudioSpecs {
+        return {
+            samplingRate: 44100,
+            channels: 2
+        };
+    }
+
+    get audioBuffer(): Float32Array[] {
+        const internalBuffer = this.gameBoy?.audio_buffer_eager(true) ?? [];
+        const leftStream = new Float32Array(internalBuffer.length / 2);
+        const rightStream = new Float32Array(internalBuffer.length / 2);
+        for (let index = 0; index < internalBuffer.length; index += 2) {
+            leftStream[index / 2] = internalBuffer[index] / 100.0;
+            rightStream[index / 2] = internalBuffer[index + 1] / 100.0;
+        }
+        return [leftStream, rightStream];
+    }
+
     get romInfo(): RomInfo {
         return {
             name: this.romName ?? undefined,
@@ -604,7 +632,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
 
     resume() {
         this.paused = false;
-        this.nextTickTime = new Date().getTime();
+        this.nextTickTime = EmulatorBase.now();
     }
 
     reset() {
@@ -623,6 +651,30 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         this.gameBoy?.key_lift(keyCode);
     }
 
+    pauseVideo() {
+        this.gameBoy?.set_ppu_enabled(false);
+    }
+
+    resumeVideo() {
+        this.gameBoy?.set_ppu_enabled(true);
+    }
+
+    getVideoState(): boolean {
+        return this.gameBoy?.get_ppu_enabled() ?? false;
+    }
+
+    pauseAudio() {
+        this.gameBoy?.set_apu_enabled(false);
+    }
+
+    resumeAudio() {
+        this.gameBoy?.set_apu_enabled(true);
+    }
+
+    getAudioState(): boolean {
+        return this.gameBoy?.get_apu_enabled() ?? false;
+    }
+
     getTile(index: number): Uint8Array {
         return this.gameBoy?.get_tile_buffer(index) ?? new Uint8Array();
     }
@@ -638,11 +690,11 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         let cycles = 0;
         this.pause();
         try {
-            const initial = Date.now();
+            const initial = EmulatorBase.now();
             for (let i = 0; i < count; i++) {
                 cycles += this.gameBoy?.clock() ?? 0;
             }
-            const delta = (Date.now() - initial) / 1000;
+            const delta = (EmulatorBase.now() - initial) / 1000;
             const frequency_mhz = cycles / delta / 1000 / 1000;
             return {
                 delta: delta,
diff --git a/src/apu.rs b/src/apu.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8fb3fd7bac5978ae00ab692e312cfee0472ef56a
--- /dev/null
+++ b/src/apu.rs
@@ -0,0 +1,564 @@
+use std::collections::VecDeque;
+
+use crate::warnln;
+
+const DUTY_TABLE: [[u8; 8]; 4] = [
+    [0, 0, 0, 0, 0, 0, 0, 1],
+    [1, 0, 0, 0, 0, 0, 0, 1],
+    [1, 0, 0, 0, 0, 1, 1, 1],
+    [0, 1, 1, 1, 1, 1, 1, 0],
+];
+
+pub enum Channel {
+    Ch1,
+    Ch2,
+    Ch3,
+    Ch4,
+}
+
+pub struct Apu {
+    ch1_timer: u16,
+    ch1_sequence: u8,
+    ch1_envelope_sequence: u8,
+    ch1_envelope_enabled: bool,
+    ch1_sweep_sequence: u8,
+    ch1_output: u8,
+    ch1_sweep_slope: u8,
+    ch1_sweep_increase: bool,
+    ch1_sweep_pace: u8,
+    ch1_length_timer: u8,
+    ch1_wave_duty: u8,
+    ch1_pace: u8,
+    ch1_direction: u8,
+    ch1_volume: u8,
+    ch1_wave_length: u16,
+    ch1_length_stop: bool,
+    ch1_enabled: bool,
+
+    ch2_timer: u16,
+    ch2_sequence: u8,
+    ch2_envelope_sequence: u8,
+    ch2_envelope_enabled: bool,
+    ch2_output: u8,
+    ch2_length_timer: u8,
+    ch2_wave_duty: u8,
+    ch2_pace: u8,
+    ch2_direction: u8,
+    ch2_volume: u8,
+    ch2_wave_length: u16,
+    ch2_length_stop: bool,
+    ch2_enabled: bool,
+
+    ch3_timer: u16,
+    ch3_position: u8,
+    ch3_output: u8,
+    ch3_dac: bool,
+    ch3_length_timer: u8,
+    ch3_output_level: u8,
+    ch3_wave_length: u16,
+    ch3_length_stop: bool,
+    ch3_enabled: bool,
+
+    right_enabled: bool,
+    left_enabled: bool,
+
+    wave_ram: [u8; 16],
+
+    sampling_rate: u16,
+    sequencer: u16,
+    sequencer_step: u8,
+    output_timer: u16,
+    audio_buffer: VecDeque<u8>,
+    audio_buffer_max: usize,
+}
+
+impl Apu {
+    pub fn new(sampling_rate: u16, buffer_size: f32) -> Self {
+        Self {
+            ch1_timer: 0,
+            ch1_sequence: 0,
+            ch1_envelope_sequence: 0,
+            ch1_envelope_enabled: false,
+            ch1_sweep_sequence: 0,
+            ch1_output: 0,
+            ch1_sweep_slope: 0x0,
+            ch1_sweep_increase: false,
+            ch1_sweep_pace: 0x0,
+            ch1_length_timer: 0x0,
+            ch1_wave_duty: 0x0,
+            ch1_pace: 0x0,
+            ch1_direction: 0x0,
+            ch1_volume: 0x0,
+            ch1_wave_length: 0x0,
+            ch1_length_stop: false,
+            ch1_enabled: false,
+
+            ch2_timer: 0,
+            ch2_sequence: 0,
+            ch2_envelope_sequence: 0,
+            ch2_envelope_enabled: false,
+            ch2_output: 0,
+            ch2_length_timer: 0x0,
+            ch2_wave_duty: 0x0,
+            ch2_pace: 0x0,
+            ch2_direction: 0x0,
+            ch2_volume: 0x0,
+            ch2_wave_length: 0x0,
+            ch2_length_stop: false,
+            ch2_enabled: false,
+
+            ch3_timer: 0,
+            ch3_position: 0,
+            ch3_output: 0,
+            ch3_dac: false,
+            ch3_length_timer: 0x0,
+            ch3_output_level: 0x0,
+            ch3_wave_length: 0x0,
+            ch3_length_stop: false,
+            ch3_enabled: false,
+
+            left_enabled: true,
+            right_enabled: true,
+
+            wave_ram: [0u8; 16],
+
+            sampling_rate,
+
+            /// Internal sequencer counter that runs at 512Hz
+            /// used for the activation of the tick actions.
+            sequencer: 0,
+            sequencer_step: 0,
+            output_timer: 0,
+            audio_buffer: VecDeque::with_capacity(
+                (sampling_rate as f32 * buffer_size as f32 * 2.0) as usize,
+            ),
+            audio_buffer_max: (sampling_rate as f32 * buffer_size as f32 * 2.0) as usize,
+        }
+    }
+
+    pub fn reset(&mut self) {
+        self.ch1_timer = 0;
+        self.ch1_sequence = 0;
+        self.ch1_envelope_sequence = 0;
+        self.ch1_envelope_enabled = false;
+        self.ch1_sweep_sequence = 0;
+        self.ch1_output = 0;
+        self.ch1_sweep_slope = 0x0;
+        self.ch1_sweep_increase = false;
+        self.ch1_sweep_pace = 0x0;
+        self.ch1_length_timer = 0x0;
+        self.ch1_wave_duty = 0x0;
+        self.ch1_pace = 0x0;
+        self.ch1_direction = 0x0;
+        self.ch1_volume = 0x0;
+        self.ch1_wave_length = 0x0;
+        self.ch1_length_stop = false;
+        self.ch1_enabled = false;
+
+        self.ch2_timer = 0;
+        self.ch2_sequence = 0;
+        self.ch2_envelope_sequence = 0;
+        self.ch2_envelope_enabled = false;
+        self.ch2_output = 0;
+        self.ch2_length_timer = 0x0;
+        self.ch2_wave_duty = 0x0;
+        self.ch2_pace = 0x0;
+        self.ch2_direction = 0x0;
+        self.ch2_volume = 0x0;
+        self.ch2_wave_length = 0x0;
+        self.ch2_length_stop = false;
+        self.ch2_enabled = false;
+
+        self.ch3_timer = 0;
+        self.ch3_position = 0;
+        self.ch3_output = 0;
+        self.ch3_dac = false;
+        self.ch3_length_timer = 0x0;
+        self.ch3_output_level = 0x0;
+        self.ch3_wave_length = 0x0;
+        self.ch3_length_stop = false;
+        self.ch3_enabled = false;
+
+        self.left_enabled = true;
+        self.right_enabled = true;
+
+        self.sequencer = 0;
+        self.sequencer_step = 0;
+        self.output_timer = 0;
+
+        self.clear_audio_buffer()
+    }
+
+    pub fn clock(&mut self, cycles: u8) {
+        // @TODO the performance here requires improvement
+        for _ in 0..cycles {
+            self.tick();
+        }
+    }
+
+    pub fn read(&mut self, addr: u16) -> u8 {
+        {
+            warnln!("Reading from unknown APU location 0x{:04x}", addr);
+            0xff
+        }
+    }
+
+    pub fn write(&mut self, addr: u16, value: u8) {
+        match addr {
+            // 0xFF10 — NR10: Channel 1 sweep
+            0xff10 => {
+                self.ch1_sweep_slope = value & 0x07;
+                self.ch1_sweep_increase = value & 0x08 == 0x00;
+                self.ch1_sweep_pace = (value & 0x70) >> 4;
+                self.ch1_sweep_sequence = 0;
+            }
+            // 0xFF11 — NR11: Channel 1 length timer & duty cycle
+            0xff11 => {
+                self.ch1_length_timer = value & 0x3f;
+                self.ch1_wave_duty = (value & 0xc0) >> 6;
+            }
+            // 0xFF12 — NR12: Channel 1 volume & envelope
+            0xff12 => {
+                self.ch1_pace = value & 0x07;
+                self.ch1_direction = (value & 0x08) >> 3;
+                self.ch1_volume = (value & 0xf0) >> 4;
+                self.ch1_envelope_enabled = self.ch1_pace > 0;
+                self.ch1_envelope_sequence = 0;
+            }
+            // 0xFF13 — NR13: Channel 1 wavelength low
+            0xff13 => {
+                self.ch1_wave_length = (self.ch1_wave_length & 0xff00) | value as u16;
+            }
+            // 0xFF14 — NR14: Channel 1 wavelength high & control
+            0xff14 => {
+                self.ch1_wave_length =
+                    (self.ch1_wave_length & 0x00ff) | (((value & 0x07) as u16) << 8);
+                self.ch1_length_stop |= value & 0x40 == 0x40;
+                self.ch1_enabled |= value & 0x80 == 0x80;
+            }
+
+            // 0xFF16 — NR21: Channel 2 length timer & duty cycle
+            0xff16 => {
+                self.ch2_length_timer = value & 0x3f;
+                self.ch2_wave_duty = (value & 0xc0) >> 6;
+            }
+            // 0xFF17 — NR22: Channel 2 volume & envelope
+            0xff17 => {
+                self.ch2_pace = value & 0x07;
+                self.ch2_direction = (value & 0x08) >> 3;
+                self.ch2_volume = (value & 0xf0) >> 4;
+            }
+            // 0xFF18 — NR23: Channel 2 wavelength low
+            0xff18 => {
+                self.ch2_wave_length = (self.ch2_wave_length & 0xff00) | value as u16;
+            }
+            // 0xFF19 — NR24: Channel 2 wavelength high & control
+            0xff19 => {
+                self.ch2_wave_length =
+                    (self.ch2_wave_length & 0x00ff) | (((value & 0x07) as u16) << 8);
+                self.ch2_length_stop |= value & 0x40 == 0x40;
+                self.ch2_enabled |= value & 0x80 == 0x80;
+            }
+
+            // 0xFF1A — NR30: Channel 3 DAC enable
+            0xff1a => {
+                self.ch3_dac = value & 0x80 == 0x80;
+            }
+            // 0xFF1B — NR31: Channel 3 length timer
+            0xff1b => {
+                self.ch3_length_timer = value;
+            }
+            // 0xFF1C — NR32: Channel 3 output level
+            0xff1c => {
+                self.ch3_output_level = (value & 0x60) >> 5;
+            }
+            // 0xFF1D — NR33: Channel 3 wavelength low [write-only]
+            0xff1d => {
+                self.ch3_wave_length = (self.ch3_wave_length & 0xff00) | value as u16;
+            }
+            // 0xFF1E — NR34: Channel 3 wavelength high & control
+            0xff1e => {
+                self.ch3_wave_length =
+                    (self.ch3_wave_length & 0x00ff) | (((value & 0x07) as u16) << 8);
+                self.ch3_length_stop |= value & 0x40 == 0x40;
+                self.ch3_enabled |= value & 0x80 == 0x80;
+            }
+
+            // 0xFF30-0xFF3F — Wave pattern RAM
+            0xff30..=0xff3f => {
+                self.wave_ram[addr as usize & 0x000f] = value;
+            }
+
+            _ => warnln!("Writing in unknown APU location 0x{:04x}", addr),
+        }
+    }
+
+    pub fn output(&self) -> u8 {
+        self.ch1_output + self.ch2_output + self.ch3_output
+    }
+
+    pub fn audio_buffer(&self) -> &VecDeque<u8> {
+        &self.audio_buffer
+    }
+
+    pub fn audio_buffer_mut(&mut self) -> &mut VecDeque<u8> {
+        &mut self.audio_buffer
+    }
+
+    pub fn clear_audio_buffer(&mut self) {
+        self.audio_buffer.clear();
+    }
+
+    #[inline(always)]
+    fn tick(&mut self) {
+        self.sequencer += 1;
+        if self.sequencer >= 8192 {
+            // each of these steps runs at 512/8 Hz = 64Hz,
+            // meaning a complete loop runs at 512 Hz
+            match self.sequencer_step {
+                0 => {
+                    self.tick_length_all();
+                }
+                1 => (),
+                2 => {
+                    self.tick_ch1_sweep();
+                    self.tick_length_all();
+                }
+                3 => (),
+                4 => {
+                    self.tick_length_all();
+                }
+                5 => (),
+                6 => {
+                    self.tick_ch1_sweep();
+                    self.tick_length_all();
+                }
+                7 => {
+                    self.tick_envelope_all();
+                }
+                _ => (),
+            }
+
+            self.sequencer = 0;
+            self.sequencer_step = (self.sequencer_step + 1) & 7;
+        }
+
+        self.tick_ch_all();
+
+        self.output_timer = self.output_timer.saturating_sub(1);
+        if self.output_timer == 0 {
+            // verifies if we've reached the maximum allowed size for the
+            // audio buffer and if that's the case an item is removed from
+            // the buffer (avoiding overflow) and then then the new audio
+            // volume item is added to the queue
+            if self.audio_buffer.len() >= self.audio_buffer_max {
+                self.audio_buffer.pop_front();
+                self.audio_buffer.pop_front();
+            }
+            if self.left_enabled {
+                self.audio_buffer.push_back(self.output());
+            }
+            if self.right_enabled {
+                self.audio_buffer.push_back(self.output());
+            }
+
+            // @TODO the CPU clock is hardcoded here, we must handle situations
+            // where there's some kind of overclock
+            self.output_timer = (4194304.0 / self.sampling_rate as f32) as u16;
+        }
+    }
+
+    #[inline(always)]
+    fn tick_length_all(&mut self) {
+        self.tick_length(Channel::Ch1);
+        self.tick_length(Channel::Ch2);
+        self.tick_length(Channel::Ch3);
+        self.tick_length(Channel::Ch4);
+    }
+
+    #[inline(always)]
+    fn tick_length(&mut self, channel: Channel) {
+        match channel {
+            Channel::Ch1 => {
+                if !self.ch1_enabled {
+                    return;
+                }
+                self.ch1_length_timer = self.ch1_length_timer.saturating_add(1);
+                if self.ch1_length_timer >= 64 {
+                    self.ch1_enabled = !self.ch1_length_stop;
+                    self.ch1_length_timer = 0;
+                }
+            }
+            Channel::Ch2 => {
+                self.ch2_length_timer = self.ch2_length_timer.saturating_add(1);
+                if self.ch2_length_timer >= 64 {
+                    self.ch2_enabled = !self.ch2_length_stop;
+                    self.ch2_length_timer = 0;
+                }
+            }
+            Channel::Ch3 => {
+                self.ch3_length_timer = self.ch3_length_timer.saturating_add(1);
+                if self.ch3_length_timer >= 64 {
+                    self.ch3_enabled = !self.ch3_length_stop;
+                    self.ch3_length_timer = 0;
+                }
+            }
+            Channel::Ch4 => (),
+        }
+    }
+
+    #[inline(always)]
+    fn tick_envelope_all(&mut self) {
+        self.tick_envelope(Channel::Ch1);
+    }
+
+    #[inline(always)]
+    fn tick_envelope(&mut self, channel: Channel) {
+        match channel {
+            Channel::Ch1 => {
+                if !self.ch1_enabled || !self.ch1_envelope_enabled {
+                    return;
+                }
+                self.ch1_envelope_sequence += 1;
+                if self.ch1_envelope_sequence >= self.ch1_pace {
+                    if self.ch1_direction == 0x01 {
+                        self.ch1_volume = self.ch1_volume.saturating_add(1);
+                    } else {
+                        self.ch1_volume = self.ch1_volume.saturating_sub(1);
+                    }
+                    if self.ch1_volume == 0 || self.ch1_volume == 15 {
+                        self.ch1_envelope_enabled = false;
+                    }
+                    self.ch1_envelope_sequence = 0;
+                }
+            }
+            Channel::Ch2 => {
+                if !self.ch2_enabled || !self.ch2_envelope_enabled {
+                    return;
+                }
+                self.ch2_envelope_sequence += 1;
+                if self.ch2_envelope_sequence >= self.ch2_pace {
+                    if self.ch2_direction == 0x01 {
+                        self.ch2_volume = self.ch2_volume.saturating_add(1);
+                    } else {
+                        self.ch2_volume = self.ch2_volume.saturating_sub(1);
+                    }
+                    if self.ch2_volume == 0 || self.ch2_volume == 15 {
+                        self.ch2_envelope_enabled = false;
+                    }
+                    self.ch2_envelope_sequence = 0;
+                }
+            }
+            Channel::Ch3 => (),
+            Channel::Ch4 => (),
+        }
+    }
+
+    #[inline(always)]
+    fn tick_ch1_sweep(&mut self) {
+        if self.ch1_sweep_pace == 0x0 {
+            return;
+        }
+        self.ch1_sweep_sequence += 1;
+        if self.ch1_sweep_sequence >= self.ch1_sweep_pace {
+            let divisor = 1u16 << self.ch1_sweep_slope as u16;
+            let delta = (self.ch1_wave_length as f32 / divisor as f32) as u16;
+            if self.ch1_sweep_increase {
+                self.ch1_wave_length = self.ch1_wave_length.saturating_add(delta);
+            } else {
+                self.ch1_wave_length = self.ch1_wave_length.saturating_sub(delta);
+            }
+            if self.ch1_wave_length > 0x07ff {
+                self.ch1_enabled = false;
+                self.ch1_wave_length = 0x07ff;
+            }
+            self.ch1_sweep_sequence = 0;
+        }
+    }
+
+    #[inline(always)]
+    fn tick_ch_all(&mut self) {
+        self.tick_ch1();
+        self.tick_ch2();
+        self.tick_ch3();
+    }
+
+    #[inline(always)]
+    fn tick_ch1(&mut self) {
+        self.ch1_timer = self.ch1_timer.saturating_sub(1);
+        if self.ch1_timer > 0 {
+            return;
+        }
+
+        if self.ch1_enabled {
+            self.ch1_output =
+                if DUTY_TABLE[self.ch1_wave_duty as usize][self.ch1_sequence as usize] == 1 {
+                    self.ch1_volume
+                } else {
+                    0
+                };
+        } else {
+            self.ch1_output = 0;
+        }
+
+        self.ch1_timer = (2048 - self.ch1_wave_length) << 2;
+        self.ch1_sequence = (self.ch1_sequence + 1) & 7;
+    }
+
+    #[inline(always)]
+    fn tick_ch2(&mut self) {
+        self.ch2_timer = self.ch2_timer.saturating_sub(1);
+        if self.ch2_timer > 0 {
+            return;
+        }
+
+        if self.ch2_enabled {
+            self.ch2_output =
+                if DUTY_TABLE[self.ch2_wave_duty as usize][self.ch2_sequence as usize] == 1 {
+                    self.ch2_volume
+                } else {
+                    0
+                };
+        } else {
+            self.ch2_output = 0;
+        }
+
+        self.ch2_timer = (2048 - self.ch2_wave_length) << 2;
+        self.ch2_sequence = (self.ch2_sequence + 1) & 7;
+    }
+
+    #[inline(always)]
+    fn tick_ch3(&mut self) {
+        self.ch3_timer = self.ch3_timer.saturating_sub(1);
+        if self.ch3_timer > 0 {
+            return;
+        }
+
+        if self.ch3_enabled {
+            let wave_index = self.ch3_position >> 1;
+            let mut output = self.wave_ram[wave_index as usize];
+            output = if (self.ch3_position & 0x01) == 0x01 {
+                output & 0x0f
+            } else {
+                (output & 0xf0) >> 4
+            };
+            if self.ch3_output_level > 0 {
+                output >>= self.ch3_output_level - 1;
+            } else {
+                output = 0;
+            }
+            self.ch3_output = output;
+        } else {
+            self.ch3_output = 0;
+        }
+
+        self.ch3_timer = (2048 - self.ch3_wave_length) << 1;
+        self.ch3_position = (self.ch3_position + 1) & 31;
+    }
+}
+
+impl Default for Apu {
+    fn default() -> Self {
+        Self::new(44100, 1.0)
+    }
+}
diff --git a/src/cpu.rs b/src/cpu.rs
index 629691bb69ad76d7eea9300c6a61b69947a618a2..237ef622b82478e4fc6f4433c1eaa051cfd5153c 100644
--- a/src/cpu.rs
+++ b/src/cpu.rs
@@ -1,6 +1,7 @@
 use core::panic;
 
 use crate::{
+    apu::Apu,
     debugln,
     inst::{EXTENDED, INSTRUCTIONS},
     mmu::Mmu,
@@ -266,11 +267,26 @@ impl Cpu {
         &mut self.mmu
     }
 
+    #[inline(always)]
+    pub fn mmu_i(&self) -> &Mmu {
+        &self.mmu
+    }
+
     #[inline(always)]
     pub fn ppu(&mut self) -> &mut Ppu {
         self.mmu().ppu()
     }
 
+    #[inline(always)]
+    pub fn apu(&mut self) -> &mut Apu {
+        self.mmu().apu()
+    }
+
+    #[inline(always)]
+    pub fn apu_i(&self) -> &Apu {
+        self.mmu_i().apu_i()
+    }
+
     #[inline(always)]
     pub fn pad(&mut self) -> &mut Pad {
         self.mmu().pad()
diff --git a/src/gb.rs b/src/gb.rs
index 47cf0ea2bf744aadbb57745479b9f72044c7da9a..399c386ebe3d8286af1378a11c71034ba537003e 100644
--- a/src/gb.rs
+++ b/src/gb.rs
@@ -1,4 +1,5 @@
 use crate::{
+    apu::Apu,
     cpu::Cpu,
     data::{BootRom, CGB_BOOT, DMG_BOOT, DMG_BOOTIX, MGB_BOOTIX, SGB_BOOT},
     gen::{COMPILATION_DATE, COMPILATION_TIME, COMPILER, COMPILER_VERSION},
@@ -10,6 +11,8 @@ use crate::{
     util::read_file,
 };
 
+use std::collections::VecDeque;
+
 #[cfg(feature = "wasm")]
 use wasm_bindgen::prelude::*;
 
@@ -31,6 +34,9 @@ use std::{
 #[cfg_attr(feature = "wasm", wasm_bindgen)]
 pub struct GameBoy {
     cpu: Cpu,
+    ppu_enabled: bool,
+    apu_enabled: bool,
+    timer_enabled: bool,
 }
 
 #[cfg_attr(feature = "wasm", wasm_bindgen)]
@@ -52,28 +58,48 @@ pub struct Registers {
     pub lyc: u8,
 }
 
+pub trait AudioProvider {
+    fn audio_output(&self) -> u8;
+    fn audio_buffer(&self) -> &VecDeque<u8>;
+    fn clear_audio_buffer(&mut self);
+}
+
 #[cfg_attr(feature = "wasm", wasm_bindgen)]
 impl GameBoy {
     #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
     pub fn new() -> Self {
-        let ppu = Ppu::new();
-        let pad = Pad::new();
-        let timer = Timer::new();
-        let mmu = Mmu::new(ppu, pad, timer);
+        let ppu = Ppu::default();
+        let apu = Apu::default();
+        let pad = Pad::default();
+        let timer = Timer::default();
+        let mmu = Mmu::new(ppu, apu, pad, timer);
         let cpu = Cpu::new(mmu);
-        Self { cpu }
+        Self {
+            cpu,
+            ppu_enabled: true,
+            apu_enabled: true,
+            timer_enabled: true,
+        }
     }
 
     pub fn reset(&mut self) {
         self.ppu().reset();
+        self.apu().reset();
         self.mmu().reset();
         self.cpu.reset();
     }
 
     pub fn clock(&mut self) -> u8 {
         let cycles = self.cpu_clock();
-        self.ppu_clock(cycles);
-        self.timer_clock(cycles);
+        if self.ppu_enabled {
+            self.ppu_clock(cycles);
+        }
+        if self.apu_enabled {
+            self.apu_clock(cycles);
+        }
+        if self.timer_enabled {
+            self.timer_clock(cycles);
+        }
         cycles
     }
 
@@ -93,6 +119,10 @@ impl GameBoy {
         self.ppu().clock(cycles)
     }
 
+    pub fn apu_clock(&mut self, cycles: u8) {
+        self.apu().clock(cycles)
+    }
+
     pub fn timer_clock(&mut self, cycles: u8) {
         self.timer().clock(cycles)
     }
@@ -172,6 +202,14 @@ impl GameBoy {
         self.frame_buffer().to_vec()
     }
 
+    pub fn audio_buffer_eager(&mut self, clear: bool) -> Vec<u8> {
+        let buffer = Vec::from(self.audio_buffer().clone());
+        if clear {
+            self.clear_audio_buffer();
+        }
+        buffer
+    }
+
     pub fn cartridge_eager(&mut self) -> Cartridge {
         self.mmu().rom().clone()
     }
@@ -238,6 +276,30 @@ impl GameBoy {
     pub fn get_compilation_time(&self) -> String {
         String::from(COMPILATION_TIME)
     }
+
+    pub fn get_ppu_enabled(&self) -> bool {
+        self.ppu_enabled
+    }
+
+    pub fn set_ppu_enabled(&mut self, value: bool) {
+        self.ppu_enabled = value;
+    }
+
+    pub fn get_apu_enabled(&self) -> bool {
+        self.apu_enabled
+    }
+
+    pub fn set_apu_enabled(&mut self, value: bool) {
+        self.apu_enabled = value;
+    }
+
+    pub fn get_timer_enabled(&self) -> bool {
+        self.apu_enabled
+    }
+
+    pub fn set_timer_enabled(&mut self, value: bool) {
+        self.timer_enabled = value;
+    }
 }
 
 /// Gameboy implementations that are meant with performance
@@ -267,6 +329,14 @@ impl GameBoy {
         self.cpu.ppu()
     }
 
+    pub fn apu(&mut self) -> &mut Apu {
+        self.cpu.apu()
+    }
+
+    pub fn apu_i(&self) -> &Apu {
+        self.cpu.apu_i()
+    }
+
     pub fn pad(&mut self) -> &mut Pad {
         self.cpu.pad()
     }
@@ -279,6 +349,10 @@ impl GameBoy {
         &(self.ppu().frame_buffer)
     }
 
+    pub fn audio_buffer(&mut self) -> &VecDeque<u8> {
+        self.apu().audio_buffer()
+    }
+
     pub fn load_boot_path(&mut self, path: &str) {
         let data = read_file(path);
         self.load_boot(&data);
@@ -382,6 +456,20 @@ pub fn hook_impl(info: &PanicInfo) {
     panic(message.as_str());
 }
 
+impl AudioProvider for GameBoy {
+    fn audio_output(&self) -> u8 {
+        self.apu_i().output()
+    }
+
+    fn audio_buffer(&self) -> &VecDeque<u8> {
+        self.apu_i().audio_buffer()
+    }
+
+    fn clear_audio_buffer(&mut self) {
+        self.apu().clear_audio_buffer()
+    }
+}
+
 impl Default for GameBoy {
     fn default() -> Self {
         Self::new()
diff --git a/src/lib.rs b/src/lib.rs
index 0bc2f37c3e2d1185f3ecec18c9d1d306b39da464..7decfab2b80948c67ae0c8c3765191dda99b7743 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,5 +1,6 @@
 #![allow(clippy::uninlined_format_args)]
 
+pub mod apu;
 pub mod cpu;
 pub mod data;
 pub mod gb;
diff --git a/src/mmu.rs b/src/mmu.rs
index 08399aad8c6848a4f5a9dae0c6ec00ecaffd762a..17de8cb88a7a1d743c7baf291135337bf781d261 100644
--- a/src/mmu.rs
+++ b/src/mmu.rs
@@ -1,4 +1,4 @@
-use crate::{debugln, pad::Pad, ppu::Ppu, rom::Cartridge, timer::Timer};
+use crate::{apu::Apu, debugln, pad::Pad, ppu::Ppu, rom::Cartridge, timer::Timer};
 
 pub const BOOT_SIZE_DMG: usize = 256;
 pub const BOOT_SIZE_CGB: usize = 2304;
@@ -16,6 +16,11 @@ pub struct Mmu {
     /// some of the access operations.
     ppu: Ppu,
 
+    /// Reference to the APU (Audio Processing Unit) that is going
+    /// to be used both for register reading/writing and to forward
+    /// some of the access operations.
+    apu: Apu,
+
     /// Reference to the Gamepad structure that is going to control
     /// the I/O access to this device.
     pad: Pad,
@@ -53,9 +58,10 @@ pub struct Mmu {
 }
 
 impl Mmu {
-    pub fn new(ppu: Ppu, pad: Pad, timer: Timer) -> Self {
+    pub fn new(ppu: Ppu, apu: Apu, pad: Pad, timer: Timer) -> Self {
         Self {
             ppu,
+            apu,
             pad,
             timer,
             rom: Cartridge::new(),
@@ -96,6 +102,14 @@ impl Mmu {
         &mut self.ppu
     }
 
+    pub fn apu(&mut self) -> &mut Apu {
+        &mut self.apu
+    }
+
+    pub fn apu_i(&self) -> &Apu {
+        &self.apu
+    }
+
     pub fn pad(&mut self) -> &mut Pad {
         &mut self.pad
     }
@@ -186,6 +200,7 @@ impl Mmu {
                                 0x00
                             }
                         },
+                        0x10..=26 | 0x30..=0x37 => self.apu.read(addr),
                         0x40 | 0x50 | 0x60 | 0x70 => self.ppu.read(addr),
                         _ => {
                             debugln!("Reading from unknown IO control 0x{:04x}", addr);
@@ -266,6 +281,7 @@ impl Mmu {
                                 0x04..=0x07 => self.timer.write(addr, value),
                                 _ => debugln!("Writing to unknown IO control 0x{:04x}", addr),
                             },
+                            0x10..=26 | 0x30..=0x37 => self.apu.write(addr, value),
                             0x40 | 0x60 | 0x70 => {
                                 match addr & 0x00ff {
                                     // 0xFF46 — DMA: OAM DMA source address & start
diff --git a/src/timer.rs b/src/timer.rs
index e1fb4a1a397180b52fde04e14336b6be66cc60fc..537c14c6714406aa2045ab6a578a8f121f59a119 100644
--- a/src/timer.rs
+++ b/src/timer.rs
@@ -27,6 +27,18 @@ impl Timer {
         }
     }
 
+    pub fn reset(&mut self) {
+        self.div = 0;
+        self.tima = 0;
+        self.tma = 0;
+        self.tac = 0x0;
+        self.div_clock = 0;
+        self.tima_clock = 0;
+        self.tima_enabled = false;
+        self.tima_ratio = 1024;
+        self.int_tima = false;
+    }
+
     pub fn clock(&mut self, cycles: u8) {
         self.div_clock += cycles as u16;
         while self.div_clock >= 256 {