In this chapter, we’re diving into the fascinating world of sound emulation for our Game Boy project. While often overlooked, a truly accurate emulator needs to replicate the distinct chiptune sounds that define the Game Boy experience. We’ll start by tackling the foundational elements of the Audio Processing Unit (APU), specifically focusing on its two square wave channels.

This milestone is critical because it brings our emulator to life in a new dimension. Hearing the familiar bleeps and boops of a Game Boy game validates our CPU and MMU work in a very tangible way. By the end of this chapter, you’ll have a basic APU implementation capable of generating square wave sounds, hooked into your emulator’s main loop, and outputting audio via SDL2’s direct audio queuing API.

Project Overview

Our overarching goal is to build a functional Game Boy emulator from scratch. So far, we’ve established the CPU core, memory management unit (MMU), and picture processing unit (PPU), allowing us to execute ROMs and render graphics. This chapter extends our emulator’s capabilities by introducing sound, a crucial component for a complete and immersive emulation experience. We’re building a modular system where each hardware component is distinct yet integrated, reflecting real hardware design.

Tech Stack

For this chapter, our primary tools remain consistent with the project:

  • F#: The core language for its functional paradigm, which helps manage complex state transitions predictably.
  • .NET 8.0: The runtime and SDK, providing a robust, cross-platform environment.
  • SDL2: A cross-platform development library designed to provide low-level access to audio, keyboard, mouse, joystick, and graphics hardware. We’ll specifically leverage its audio capabilities. For F#/.NET, we use SDL2-CS bindings, often found via NuGet as VelcroPhysics.SDL.Net.

Latest Versions (as of 2026-05-05):

APU Design and Build Plan

The Game Boy’s APU is a separate hardware component responsible for generating sound. It features four distinct sound channels: two pulse (square) waves, one wave (custom waveform), and one noise channel. Each channel has its own set of registers that control its frequency, volume, duty cycle, envelope, and length.

For this chapter, we’ll concentrate on the two square wave channels (often referred to as Channel 1 and Channel 2). These are controlled by specific memory-mapped I/O registers from 0xFF10 to 0xFF14 (Channel 1) and 0xFF15 to 0xFF19 (Channel 2).

Architecture Overview

The core challenge of APU emulation lies in synchronizing the sound generation with the CPU’s execution. The CPU runs at ~4.19 MHz, while audio typically samples at rates like 44.1 kHz or 48 kHz. We need to generate audio samples at the correct rate based on the APU’s internal state, which is updated by CPU writes to APU registers.

Our design will involve:

  1. ApuState: An F# record to hold the current state of all APU channels and control registers.
  2. Square Wave Channel State: A sub-record for each square wave channel, containing its specific parameters (frequency, duty cycle, volume, timers).
  3. Register Mapping: Functions to handle CPU writes to APU I/O registers, parsing the bitfields and updating the ApuState.
  4. Sample Generation: A function that, given the current APU state, can generate a single audio sample at the desired output sample rate.
  5. Audio Output: Using SDL2’s SDL_QueueAudio function to manage an audio buffer and play the generated samples directly. This is preferred over SDL_Mixer for raw sample streaming as it provides more granular control and avoids overhead designed for pre-recorded audio files.

Here’s a high-level data flow for the APU integration:

flowchart LR CPU --> MMU[Memory Management Unit] MMU -->|Write APU Registers| APU_Registers[APU Registers] APU_Registers --> APU_State[APU State] APU_State -->|Generate Samples| Sample_Buffer[APU Sample Buffer] Sample_Buffer -->|Queue Audio| SDL_QueueAudio[SDL2 Audio Queue] SDL_QueueAudio --> Speaker[Speaker Output]

This diagram illustrates how CPU instructions trigger memory writes, which the MMU routes to the APU. The APU’s internal state is then updated, leading to sample generation and eventual output through SDL2.

F# Domain Modeling

We’ll define immutable records to represent the state of our APU and its individual channels. This aligns perfectly with F#’s functional paradigm.

SquareWaveChannel:

  • Enabled: bool
  • Frequency: int (derived from NR13/NR14 registers)
  • DutyCycle: int (0-3, representing 12.5%, 25%, 50%, 75% high)
  • Volume: int (0-15)
  • EnvelopeVolume: int (current volume after envelope processing)
  • EnvelopeDirection: int (0: decrease, 1: increase)
  • EnvelopePeriod: int (how often volume changes)
  • LengthCounter: int (how many CPU cycles until channel stops)
  • WavePosition: int (internal counter for waveform generation, 0-7 for 8-step duty)
  • Timer: int (internal frequency timer)
  • CurrentTimer: int (current countdown for frequency)
  • SweepPeriod: int (Channel 1 only, how often frequency changes)
  • SweepShift: int (Channel 1 only, how much frequency changes)
  • SweepDirection: int (Channel 1 only, 0: increase, 1: decrease)
  • ShadowFrequency: int (Channel 1 only, used by sweep unit)
  • SweepEnabled: bool (Channel 1 only, true if sweep is active)

ApuState:

  • Channel1: SquareWaveChannel
  • Channel2: SquareWaveChannel
  • MasterVolumeLeft: int
  • MasterVolumeRight: int
  • Enabled: bool (Master APU enable, NR52 bit 7)
  • SampleRate: int (e.g., 44100 Hz)
  • CpuCyclesPerSample: float (how many CPU cycles correspond to one audio sample)
  • PendingCpuCycles: float (accumulator for CPU cycles, to decide when to generate a sample)
  • SampleBuffer: ResizeArray<float> (buffer to store generated samples)

Setup Requirements for SDL2 Audio

To use SDL2 for audio output, you’ll need the core SDL2 library.

  1. Install SDL2:

    • Windows: Download SDL2-devel-2.x.x-vc.zip from the SDL releases page. Extract it and copy SDL2.dll (from lib/x64 or lib/x86) into your project’s output directory (e.g., bin/Debug/net8.0).
    • macOS: brew install sdl2
    • Linux: sudo apt-get install libsdl2-dev
  2. Add NuGet package: In your F# application project (GbEmulator.App or similar), add the VelcroPhysics.SDL.Net NuGet package, which provides the necessary C# bindings for SDL2.

    dotnet add GbEmulator.App package VelcroPhysics.SDL.Net
    

Step-by-Step Implementation

We’ll start by defining our APU state and basic register write logic.

0. Define Constants in GbEmulator.Domain/Constants.fs

First, ensure you have a Constants.fs file in your GbEmulator.Domain project with common constants, including the CPU speed.

File: GbEmulator.Domain/Constants.fs

namespace GbEmulator.Domain

module Constants =
    let CpuSpeedHz = 4_194_304 // Game Boy CPU runs at ~4.19 MHz
    let FramesPerSecond = 60
    let CyclesPerInstruction = 4 // Average cycles per instruction (varies, but 4 is a common base)
    let WramSize = 0x2000 // 8KB
    let HramSize = 0x80 // 128 bytes
    let OamSize = 0xA0 // 160 bytes
    let IoSize = 0x100 // 256 bytes for I/O registers
    let VramSize = 0x2000 // 8KB

๐Ÿ“Œ Key Idea: Centralizing constants like CpuSpeedHz ensures consistency across the emulator and simplifies future adjustments.

1. Define APU State in GbEmulator.Domain/Apu.fs

Create a new file Apu.fs in your GbEmulator.Domain project. This file will hold all the necessary types and initialization logic for our APU.

File: GbEmulator.Domain/Apu.fs

namespace GbEmulator.Domain

open System
open System.Collections.Generic // For ResizeArray

// --- Square Wave Channel State ---
type SquareWaveChannel = {
    Enabled         : bool
    Frequency       : int // Derived from NR13/NR14. Game Boy frequency is 131072 / (2048 - frequency) Hz
    DutyCycle       : int // 0=12.5%, 1=25%, 2=50%, 3=75%
    Volume          : int // Initial volume for envelope (0-15)
    EnvelopeVolume  : int // Current volume after envelope processing (0-15)
    EnvelopeDirection : int // 0=Decrease, 1=Increase
    EnvelopePeriod  : int // Number of 1/64s of a second between volume changes (0=no envelope)
    LengthCounter   : int // Number of 1/256s of a second before channel stops
    LengthEnabled   : bool // True if length counter is enabled
    WavePosition    : int // Internal counter for waveform generation (0-7 for 8-step duty cycle)
    Timer           : int // Internal frequency timer (reload value)
    CurrentTimer    : int // Current countdown for frequency
    SweepPeriod     : int // Channel 1 only: number of 1/128s of a second between sweep updates
    SweepShift      : int // Channel 1 only: number of bits to shift frequency for sweep
    SweepDirection  : int // Channel 1 only: 0=Increase, 1=Decrease (frequency)
    ShadowFrequency : int // Channel 1 only: used by sweep unit
    SweepEnabled    : bool // Channel 1 only: true if sweep is active
}

// --- APU Master State ---
type ApuState = {
    Channel1        : SquareWaveChannel
    Channel2        : SquareWaveChannel
    MasterVolumeLeft : int // Master volume for left output (0-7)
    MasterVolumeRight : int // Master volume for right output (0-7)
    Enabled         : bool // Master APU enable (NR52 bit 7)
    SampleRate      : int // Target audio sample rate (e.g., 44100 Hz)
    CpuCyclesPerSample : float // How many CPU cycles correspond to one audio sample
    PendingCpuCycles : float // Accumulator for CPU cycles, to decide when to generate a sample
    SampleBuffer    : ResizeArray<float> // Buffer to store generated samples
}

// Initial state for a square wave channel
let private initialSquareWaveChannel = {
    Enabled = false; Frequency = 0; DutyCycle = 0; Volume = 0; EnvelopeVolume = 0;
    EnvelopeDirection = 0; EnvelopePeriod = 0; LengthCounter = 0; LengthEnabled = false;
    WavePosition = 0; Timer = 0; CurrentTimer = 0; SweepPeriod = 0; SweepShift = 0;
    SweepDirection = 0; ShadowFrequency = 0; SweepEnabled = false
}

// Initialize the APU state
let initApu (sampleRate: int) = {
    Channel1 = initialSquareWaveChannel
    Channel2 = initialSquareWaveChannel
    MasterVolumeLeft = 0
    MasterVolumeRight = 0
    Enabled = false
    SampleRate = sampleRate
    CpuCyclesPerSample = Constants.CpuSpeedHz |> float / float sampleRate
    PendingCpuCycles = 0.0
    SampleBuffer = ResizeArray<float>()
}

๐Ÿ“Œ Key Idea: We define detailed state records for each square wave channel and the overall APU. This structured approach makes it easier to manage the complex interactions of the APU registers, leveraging F#’s immutability for predictable state changes. ResizeArray<float> is chosen for the sample buffer to allow efficient appending of new samples without constant reallocations, which is important for performance-critical audio loops.

2. Add APU to Emulator State

To integrate the APU into our emulator, its state must be part of the global EmulatorState. Open GbEmulator.Core/Emulator.fs and add ApuState to your EmulatorState record and the initEmulator function.

File: GbEmulator.Core/Emulator.fs

namespace GbEmulator.Core

open GbEmulator.Domain
open GbEmulator.Domain.Cpu
open GbEmulator.Domain.Memory
open GbEmulator.Domain.Ppu
open GbEmulator.Domain.Apu // Add this line

// ... other record definitions

type EmulatorState = {
    CpuState: CpuState
    MmuState: MmuState
    PpuState: PpuState
    ApuState: ApuState // Add this field
    // ... other fields
    Running: bool
}

// ... in initEmulator function ...
let initEmulator (romBytes: byte[]) (sampleRate: int) =
    let mmu = Mmu.initMmu romBytes
    let cpu = Cpu.initCpu
    let ppu = Ppu.initPpu
    let apu = Apu.initApu sampleRate // Initialize APU with the target sample rate

    { CpuState = cpu; MmuState = mmu; PpuState = ppu; ApuState = apu; Running = true } // Add apu

โšก Quick Note: We pass the sampleRate to Apu.initApu because the APU needs to know the target audio output rate to calculate CpuCyclesPerSample accurately. This is crucial for correct timing synchronization.

3. Handle APU Register Writes in GbEmulator.Domain/Mmu.fs

The MMU is responsible for routing memory writes to the correct hardware components. We need to extend Mmu.writeByte to update the APU state when writes occur to the APU’s I/O registers (0xFF10 - 0xFF3F).

First, in GbEmulator.Domain/Apu.fs, add functions to update the APU state based on register writes. For now, we’ll focus on the raw register values, and later derive the actual sound parameters.

File: GbEmulator.Domain/Apu.fs (continued)

// GbEmulator.Domain/Apu.fs (continued)

// Helper for bit manipulation
let private isBitSet value bit = (value &&& (1 <<< bit)) <> 0

// --- APU Register Write Handlers ---

// NR10 - Channel 1 Sweep register
let writeNr10 (value: byte) (channel: SquareWaveChannel) =
    { channel with
        SweepPeriod = (value >>> 4) &&& 0x7 // Bits 4-6
        SweepDirection = (value >>> 3) &&& 0x1 // Bit 3 (0=increase, 1=decrease)
        SweepShift = value &&& 0x7 // Bits 0-2
    }

// NR11/NR21 - Channel 1/2 Length & Duty Cycle
let writeNrX1 (value: byte) (channel: SquareWaveChannel) =
    { channel with
        DutyCycle = (value >>> 6) &&& 0x3 // Bits 6-7
        LengthCounter = 64 - (value &&& 0x3F) // Bits 0-5. Length is 64 - N.
    }

// NR12/NR22 - Channel 1/2 Volume & Envelope
let writeNrX2 (value: byte) (channel: SquareWaveChannel) =
    { channel with
        Volume = (value >>> 4) &&& 0xF // Bits 4-7
        EnvelopeDirection = (value >>> 3) &&& 0x1 // Bit 3 (0=decrease, 1=increase)
        EnvelopePeriod = value &&& 0x7 // Bits 0-2
        // If initial volume is 0, channel is muted immediately.
        Enabled = ((value >>> 4) &&& 0xF) <> 0 // Channel enabled only if initial volume is not 0
        EnvelopeVolume = (value >>> 4) &&& 0xF // Set current envelope volume to initial volume
    }

// NR13/NR23 - Channel 1/2 Frequency Low (lower 8 bits of 11-bit frequency)
let writeNrX3 (value: byte) (channel: SquareWaveChannel) =
    // The frequency is an 11-bit value, so we combine with the upper bits from NR14
    let newFrequency = (channel.Frequency &&& 0x700) ||| (int value) // Keep upper 3 bits, set lower 8
    { channel with
        Frequency = newFrequency
        // Timer calculation: (2048 - frequency) * 4 CPU cycles. This is the period of one full waveform.
        Timer = (2048 - newFrequency) * 4
        CurrentTimer = (2048 - newFrequency) * 4 // Reset current timer
    }

// NR14/NR24 - Channel 1/2 Frequency High (upper 3 bits) & Control
let writeNrX4 (value: byte) (channel: SquareWaveChannel) =
    // Update upper 3 bits of frequency
    let newFrequency = (channel.Frequency &&& 0xFF) ||| (((int (value &&& 0x7)) <<< 8)) // Bits 0-2
    { channel with
        Frequency = newFrequency
        LengthEnabled = isBitSet value 6 // Bit 6: Use length counter
        // Trigger bit (Bit 7): When set, reloads length counter, volume envelope, and frequency timer.
        // This is where the channel is "restarted".
        Enabled = if isBitSet value 7 then ((channel.Volume &&& 0xF) <> 0) else channel.Enabled // Trigger enables if volume > 0
        LengthCounter = if isBitSet value 7 then (if channel.LengthCounter = 0 then 64 else channel.LengthCounter) else channel.LengthCounter
        EnvelopeVolume = if isBitSet value 7 then channel.Volume else channel.EnvelopeVolume
        CurrentTimer = if isBitSet value 7 then (2048 - newFrequency) * 4 else channel.CurrentTimer
        Timer = if isBitSet value 7 then (2048 - newFrequency) * 4 else channel.Timer
        ShadowFrequency = if isBitSet value 7 then newFrequency else channel.ShadowFrequency // For sweep unit
        SweepEnabled = if isBitSet value 7 then (channel.SweepPeriod <> 0 || channel.SweepShift <> 0) else channel.SweepEnabled
    }

// NR50 - Channel Control (Volume & Vin Panning)
let writeNr50 (value: byte) (apu: ApuState) =
    { apu with
        MasterVolumeLeft = (value >>> 4) &&& 0x7 // Bits 4-6
        MasterVolumeRight = value &&& 0x7 // Bits 0-2
    }

// NR51 - Selection of Sound Output Terminal
let writeNr51 (value: byte) (apu: ApuState) =
    // For now, we'll ignore panning, but this register controls which channels go to which speaker.
    // Bit 0: Channel 1 to Right, Bit 1: Channel 2 to Right, etc.
    // Bit 4: Channel 1 to Left, Bit 5: Channel 2 to Left, etc.
    // This is a simplification; a real implementation would use this to mix channels.
    apu

// NR52 - Sound ON/OFF
let writeNr52 (value: byte) (apu: ApuState) =
    let newEnabled = isBitSet value 7
    if not newEnabled then
        // If master APU is disabled, all channels are disabled and their states are reset.
        // This is a simplification for now. A full reset would zero out all channel registers.
        { apu with
            Enabled = newEnabled
            Channel1 = { apu.Channel1 with Enabled = false }
            Channel2 = { apu.Channel2 with Enabled = false }
        }
    else
        { apu with Enabled = newEnabled }

// Main APU write function: Routes writes to specific channel handlers
let writeByte (addr: int) (value: byte) (apu: ApuState) : ApuState =
    match addr with
    | 0xFF10 -> { apu with Channel1 = writeNr10 value apu.Channel1 } // NR10 - Channel 1 Sweep
    | 0xFF11 -> { apu with Channel1 = writeNrX1 value apu.Channel1 } // NR11 - Channel 1 Length & Duty
    | 0xFF12 -> { apu with Channel1 = writeNrX2 value apu.Channel1 } // NR12 - Channel 1 Volume & Envelope
    | 0xFF13 -> { apu with Channel1 = writeNrX3 value apu.Channel1 } // NR13 - Channel 1 Frequency Low
    | 0xFF14 -> { apu with Channel1 = writeNrX4 value apu.Channel1 } // NR14 - Channel 1 Frequency High & Control
    | 0xFF16 -> { apu with Channel2 = writeNrX1 value apu.Channel2 } // NR21 - Channel 2 Length & Duty
    | 0xFF17 -> { apu with Channel2 = writeNrX2 value apu.Channel2 } // NR22 - Channel 2 Volume & Envelope
    | 0xFF18 -> { apu with Channel2 = writeNrX3 value apu.Channel2 } // NR23 - Channel 2 Frequency Low
    | 0xFF19 -> { apu with Channel2 = writeNrX4 value apu.Channel2 } // NR24 - Channel 2 Frequency High & Control
    | 0xFF20 -> apu // NR30 - Channel 3 Sound On/Off (Wave Channel) - Placeholder
    | 0xFF21 -> apu // NR31 - Channel 3 Length (Wave Channel) - Placeholder
    | 0xFF22 -> apu // NR32 - Channel 3 Volume (Wave Channel) - Placeholder
    | 0xFF23 -> apu // NR33 - Channel 3 Frequency Low (Wave Channel) - Placeholder
    | 0xFF24 -> apu // NR34 - Channel 3 Frequency High (Wave Channel) - Placeholder
    | 0xFF25 -> writeNr50 value apu // NR50 - Channel Control (Volume & Vin Panning)
    | 0xFF26 -> writeNr51 value apu // NR51 - Selection of Sound Output Terminal
    | 0xFF27 -> writeNr52 value apu // NR52 - Sound ON/OFF
    | 0xFF30 .. 0xFF3F -> apu // Wave RAM - Placeholder for Channel 3's waveform data
    | _ -> apu

๐Ÿง  Important: The writeNrX4 (trigger register) is crucial. When bit 7 is set, it effectively “restarts” the channel, reloading its length counter, volume envelope, and frequency timer. This is a common Game Boy sound programming technique. We also set Enabled = true on trigger, but the channel can still be muted if its initial volume (NRx2) is zero.

Now, integrate this into GbEmulator.Domain/Mmu.fs. This requires modifying the writeByte function to accept and return ApuState alongside MmuState.

File: GbEmulator.Domain/Mmu.fs

namespace GbEmulator.Domain

open GbEmulator.Domain.Apu // Add this line
// ... other open statements

type MmuState = {
    // ... existing fields
    Rom: byte[]
    Wram: byte[]
    Hram: byte[]
    Oam: byte[]
    Io: byte[] // For I/O registers
    Vram: byte[] // Ensure Vram is here if not already
}

// ... in initMmu ...
let initMmu (romBytes: byte[]) =
    {
        // ... existing initialization
        Rom = romBytes
        Wram = Array.zeroCreate Constants.WramSize
        Hram = Array.zeroCreate Constants.HramSize
        Oam = Array.zeroCreate Constants.OamSize
        Io = Array.zeroCreate Constants.IoSize // Initialize I/O registers
        Vram = Array.zeroCreate Constants.VramSize // Initialize Vram
    }

// Modify the writeByte function to handle APU registers
let writeByte (addr: int) (value: byte) (mmu: MmuState) (apu: ApuState) : MmuState * ApuState =
    match addr with
    | 0x0000 .. 0x7FFF -> // ROM
        // Handle MBC banking here later
        (mmu, apu)
    | 0x8000 .. 0x9FFF -> // VRAM
        let newVram = Array.copy mmu.Vram
        newVram.[addr - 0x8000] <- value
        ({ mmu with Vram = newVram }, apu)
    | 0xA000 .. 0xBFFF -> // External RAM
        // Handle external RAM banking here later
        (mmu, apu)
    | 0xC000 .. 0xDFFF -> // WRAM
        let newWram = Array.copy mmu.Wram
        newWram.[addr - 0xC000] <- value
        ({ mmu with Wram = newWram }, apu)
    | 0xE000 .. 0xFDFF -> // Echo RAM (mirror of C000-DDFF)
        // Writes to echo RAM also write to WRAM
        let wramAddr = addr - 0xE000 + 0xC000
        let newWram = Array.copy mmu.Wram
        newWram.[wramAddr - 0xC000] <- value
        ({ mmu with Wram = newWram }, apu)
    | 0xFE00 .. 0xFE9F -> // OAM
        let newOam = Array.copy mmu.Oam
        newOam.[addr - 0xFE00] <- value
        ({ mmu with Oam = newOam }, apu)
    | 0xFF00 -> // P1 - Joypad
        // Joypad is read-only for writes, except for selecting button/direction bits
        ({ mmu with Io = (Array.copy mmu.Io) |> fun arr -> arr.[0x00] <- (arr.[0x00] &&& 0xCF) ||| (value &&& 0x30); arr }, apu)
    | 0xFF01 -> // SB - Serial Transfer Data
        ({ mmu with Io = (Array.copy mmu.Io) |> fun arr -> arr.[0x01] <- value; arr }, apu)
    | 0xFF04 -> // DIV - Divider Register
        // Resetting DIV is a special case, it always writes 0
        ({ mmu with Io = (Array.copy mmu.Io) |> fun arr -> arr.[0x04] <- 0x00; arr }, apu) // DIV is reset to 0 on write
    | 0xFF05 -> // TIMA - Timer Counter
        ({ mmu with Io = (Array.copy mmu.Io) |> fun arr -> arr.[0x05] <- value; arr }, apu)
    | 0xFF06 -> // TMA - Timer Modulo
        ({ mmu with Io = (Array.copy mmu.Io) |> fun arr -> arr.[0x06] <- value; arr }, apu)
    | 0xFF07 -> // TAC - Timer Control
        ({ mmu with Io = (Array.copy mmu.Io) |> fun arr -> arr.[0x07] <- value; arr }, apu)
    | 0xFF0F -> // IF - Interrupt Flag
        ({ mmu with Io = (Array.copy mmu.Io) |> fun arr -> arr.[0x0F] <- value; arr }, apu)
    | 0xFF10 .. 0xFF27 -> // APU Registers
        // Route APU register writes to the APU module
        (mmu, Apu.writeByte addr value apu)
    | 0xFF28 .. 0xFF2F -> // Unused I/O (APU)
        (mmu, apu)
    | 0xFF30 .. 0xFF3F -> // Wave RAM (APU)
        // Route Wave RAM writes to the APU module (for Channel 3)
        // For now, these are placeholders, but Apu.writeByte will handle them eventually.
        (mmu, Apu.writeByte addr value apu)
    | 0xFF40 .. 0xFF4B -> // PPU Registers
        // Update the PPU state based on register writes
        let (newIo, newLcdControl, newScrollY, newScrollX, newWindowY, newWindowX) =
            Ppu.writePpuRegister (addr - 0xFF40) value mmu.Io
        // Update MMU's Io array for the PPU register
        let updatedIo = (Array.copy mmu.Io) |> fun arr -> arr.[addr - 0xFF00] <- value; arr
        ( { mmu with Io = updatedIo }, apu)
    | 0xFF4C .. 0xFF7F -> // Unused I/O, but some are used in CGB mode
        ({ mmu with Io = (Array.copy mmu.Io) |> fun arr -> arr.[addr - 0xFF00] <- value; arr }, apu)
    | 0xFF80 .. 0xFFFE -> // HRAM
        let newHram = Array.copy mmu.Hram
        newHram.[addr - 0xFF80] <- value
        ({ mmu with Hram = newHram }, apu)
    | 0xFFFF -> // IE - Interrupt Enable
        ({ mmu with Io = (Array.copy mmu.Io) |> fun arr -> arr.[0xFF] <- value; arr }, apu)
    | _ ->
        printfn "MMU: Unhandled write to 0x%x with value 0x%x" addr value
        (mmu, apu)

โšก Quick Note: Mmu.writeByte now returns MmuState * ApuState. This is a common functional pattern for updating multiple related states. This means the Cpu.executeInstruction function will need to be refactored to accept and return ApuState as well, ensuring APU state is always current. We also added a small fix for the joypad write to only allow writing to the upper nibble (direction/button select).

4. Implement Sample Generation and APU Tick

Back in GbEmulator.Domain/Apu.fs, we need functions to advance the APU’s internal state and generate audio samples.

File: GbEmulator.Domain/Apu.fs (continued)

// GbEmulator.Domain/Apu.fs (continued)

// Duty cycle waveforms (8 steps, true=high, false=low)
let private dutyCycles =
    [|
        [|false; false; false; false; false; false; false; true|] // 12.5%
        [|true; false; false; false; false; false; false; true|] // 25%
        [|true; false; false; false; false; true; false; true|] // 50%
        [|false; true; true; true; true; true; true; false|] // 75%
    |]

// Generate a single sample for a square wave channel
let generateSquareWaveSample (channel: SquareWaveChannel) : float =
    if not channel.Enabled || channel.EnvelopeVolume = 0 then
        0.0 // Muted if channel disabled or volume is 0
    else
        let dutyPattern = dutyCycles.[channel.DutyCycle]
        // Normalize volume to a float between 0 and 1
        let normalizedVolume = float channel.EnvelopeVolume / 15.0
        if dutyPattern.[channel.WavePosition] then
            normalizedVolume // High state
        else
            -normalizedVolume // Low state (for bipolar output, centered at 0)

// Update internal timers and generate samples
let tick (cpuCycles: int) (apu: ApuState) : ApuState =
    if not apu.Enabled then
        // If master APU is disabled, clear sample buffer and return
        apu.SampleBuffer.Clear()
        { apu with PendingCpuCycles = 0.0 }
    else
        let mutable currentApu = apu
        let mutable currentChannel1 = apu.Channel1
        let mutable currentChannel2 = apu.Channel2

        // Accumulate CPU cycles
        currentApu <- { currentApu with PendingCpuCycles = currentApu.PendingCpuCycles + float cpuCycles }

        // Process frame sequencer (length counter, envelope, sweep - simplified for now)
        // This is a complex part of the APU, typically running at 512 Hz (every 8192 CPU cycles)
        // For simplicity, we'll implement a basic frequency timer and waveform step.
        // Full frame sequencer logic is for a later, more accurate APU chapter.

        // --- Channel 1 & 2 Frequency Timer and Wave Position Update ---
        let updateChannelTimer (channel: SquareWaveChannel) =
            if channel.Enabled && channel.Timer > 0 then // Only tick if enabled and timer is valid
                let newCurrentTimer = channel.CurrentTimer - cpuCycles
                if newCurrentTimer <= 0 then
                    // Timer expired, reload and advance wave position
                    let timerReload = channel.Timer // Use the pre-calculated timer value
                    let newWavePosition = (channel.WavePosition + 1) % 8
                    { channel with CurrentTimer = timerReload + newCurrentTimer; WavePosition = newWavePosition }
                else
                    { channel with CurrentTimer = newCurrentTimer }
            else
                channel // Not enabled or timer is 0, no change

        currentChannel1 <- updateChannelTimer currentChannel1
        currentChannel2 <- updateChannelTimer currentChannel2

        // --- Sample Generation Loop ---
        while currentApu.PendingCpuCycles >= currentApu.CpuCyclesPerSample do
            // Generate a sample from each enabled channel
            let sample1 = generateSquareWaveSample currentChannel1
            let sample2 = generateSquareWaveSample currentChannel2

            // Basic mixing: sum and clip (or normalize)
            let mixedSample = (sample1 + sample2) / 2.0 // Simple average for now
            currentApu.SampleBuffer.Add(mixedSample)

            currentApu <- { currentApu with PendingCpuCycles = currentApu.PendingCpuCycles - currentApu.CpuCyclesPerSample }

        { currentApu with Channel1 = currentChannel1; Channel2 = currentChannel2 }

โš ๏ธ What can go wrong: The tick function for the APU is extremely simplified here. A real Game Boy APU has a complex “frame sequencer” that manages length counters, volume envelopes, and sweep units at specific clock rates (e.g., 512 Hz). Our current tick only advances the frequency timer and waveform position. This will produce static square waves, not the dynamic sounds with fading volumes and changing pitches found in real games. We’ll address this in a future chapter for higher accuracy.

โšก Real-world insight: The Game Boy’s APU is notoriously tricky to emulate accurately due to its tight synchronization requirements and the specific behavior of its frame sequencer. Many emulators start with this simplified approach and gradually add more cycle-accurate logic for length, envelope, and sweep.

5. Update Emulator’s Main Loop

To ensure ApuState is consistently updated, we need to pass it through functions that modify memory-mapped I/O registers. This means refactoring Cpu.executeInstruction to accept and return ApuState.

First, refactor GbEmulator.Domain/Cpu.fs:

Locate your Cpu.executeInstruction function and modify its signature and internal calls to Mmu.writeByte.

File: GbEmulator.Domain/Cpu.fs (partial, illustrating refactor)

namespace GbEmulator.Domain

open GbEmulator.Domain.Memory
open GbEmulator.Domain.Apu
// ... other open statements

// Define the return type to include ApuState
type CpuExecutionResult = {
    CpuState: CpuState
    MmuState: MmuState
    ApuState: ApuState // New: Include ApuState in the result
    Cycles: int
}

// Refactor executeInstruction signature
let executeInstruction (cpu: CpuState) (mmu: MmuState) (apu: ApuState) : CpuExecutionResult =
    // ... existing instruction fetch and decode logic ...

    // Example: When an instruction performs a memory write (e.g., LD (HL), A)
    // Assume `addr` and `value` are determined by the instruction
    let (newMmu, newApu) = Mmu.writeByte addr value currentMmu currentApu // Pass and receive ApuState

    // ... continue with instruction execution, updating cpu state, mmu, and apu ...

    // When returning the result:
    { CpuState = updatedCpu; MmuState = newMmu; ApuState = newApu; Cycles = instructionCycles }

โšก Real-world insight: Propagating state updates through multiple modules (Cpu, Mmu, Apu) in a functional way requires careful thought. In F#, you typically pass the current state to a function and it returns a new state. This is why Mmu.writeByte now returns MmuState * ApuState, and Cpu.executeInstruction needs to incorporate ApuState in its signature and return type. This ensures the ApuState reflects any register writes immediately.

Now, update GbEmulator.Core/Emulator.fs:

Modify the step function to correctly call the refactored Cpu.executeInstruction and handle the updated ApuState.

File: GbEmulator.Core/Emulator.fs (continued)

namespace GbEmulator.Core

open GbEmulator.Domain
open GbEmulator.Domain.Cpu
open GbEmulator.Domain.Memory
open GbEmulator.Domain.Ppu
open GbEmulator.Domain.Apu
open System

let step (state: EmulatorState) : EmulatorState =
    // ... existing code ...

    // Call the refactored Cpu.executeInstruction with ApuState
    let cpuExecResult = Cpu.executeInstruction state.CpuState state.MmuState state.ApuState
    let newCpuState = cpuExecResult.CpuState
    let newMmuState = cpuExecResult.MmuState
    let updatedApuFromCpu = cpuExecResult.ApuState // APU state after CPU writes (via MMU)

    let newPpuState = Ppu.tick cpuExecResult.Cycles state.PpuState newMmuState.Io // PPU tick
    let finalApuState = Apu.tick cpuExecResult.Cycles updatedApuFromCpu // APU tick for internal timing

    { state with
        CpuState = newCpuState
        MmuState = newMmuState
        PpuState = newPpuState
        ApuState = finalApuState // Update with the final APU state
    }

6. SDL2 Audio Integration in GbEmulator.App/Program.fs

This is where we actually play the sound. We’ll use SDL2’s direct audio queuing.

File: GbEmulator.App/Program.fs

namespace GbEmulator.App

open System
open System.Runtime.InteropServices
open SDL2
open GbEmulator.Core
open GbEmulator.Domain
open GbEmulator.Domain.Ppu
open System.IO // For File.ReadAllBytes

// --- SDL2 Audio P/Invoke declarations ---
[<DllImport("SDL2.dll", CallingConvention = CallingConvention.Cdecl)>]
extern uint SDL_OpenAudioDevice(string device, int iscapture, ref SDL.SDL_AudioSpec desired, out SDL.SDL_AudioSpec obtained, int allowed_changes)

[<DllImport("SDL2.dll", CallingConvention = CallingConvention.Cdecl)>]
extern int SDL_QueueAudio(uint dev, IntPtr data, uint len)

[<DllImport("SDL2.dll", CallingConvention = CallingConvention.Cdecl)>]
extern uint SDL_GetQueuedAudioSize(uint dev)

[<DllImport("SDL2.dll", CallingConvention = CallingConvention.Cdecl)>]
extern void SDL_PauseAudioDevice(uint dev, int pause_on)

[<DllImport("SDL2.dll", CallingConvention = CallingConvention.Cdecl)>]
extern void SDL_CloseAudioDevice(uint dev)

// Define audio parameters
let AudioFrequency = 44100 // Hz, common sample rate
let AudioChannels = 2 // Stereo output
let AudioBufferSize = 2048 // Samples per internal SDL buffer (power of 2 is good)
let AudioFormat = 0x8010us // AUDIO_S16SYS (Signed 16-bit, system endian)

// --- Main Program Loop ---
[<EntryPoint>]
let main argv =
    // Check for ROM path argument
    if argv.Length = 0 then
        printfn "Usage: dotnet run --project GbEmulator.App -- <path_to_rom.gb>"
        exit 1

    // ... existing SDL initialization ...
    if SDL.SDL_Init(SDL.SDL_INIT_VIDEO ||| SDL.SDL_INIT_AUDIO) < 0 then
        printfn "SDL_Init Error: %s" (SDL.SDL_GetError())
        exit 1

    // Global variable for audio device ID
    let mutable audioDeviceId: uint = 0u

    let desiredAudioSpec =
        { SDL.SDL_AudioSpec.freq = AudioFrequency
          SDL.SDL_AudioSpec.format = AudioFormat
          SDL.SDL_AudioSpec.channels = byte AudioChannels
          SDL.SDL_AudioSpec.samples = UInt16 AudioBufferSize // Buffer size in samples
          SDL.SDL_AudioSpec.padding = 0us
          SDL.SDL_AudioSpec.size = 0u
          SDL.SDL_AudioSpec.callback = IntPtr.Zero // No callback for queueing
          SDL.SDL_AudioSpec.userdata = IntPtr.Zero
        }

    let mutable obtainedAudioSpec = desiredAudioSpec // Will be filled by SDL
    audioDeviceId <- SDL_OpenAudioDevice(null, 0, &desiredAudioSpec, &obtainedAudioSpec, SDL.SDL_AUDIO_ALLOW_FORMAT_CHANGE)

    if audioDeviceId = 0u then
        printfn "SDL_OpenAudioDevice Error: %s" (SDL.SDL_GetError())
        SDL.SDL_Quit()
        exit 1

    printfn "SDL_AudioDevice opened successfully. Frequency: %d, Channels: %d" obtainedAudioSpec.freq (int obtainedAudioSpec.channels)

    // Start audio playback
    SDL_PauseAudioDevice(audioDeviceId, 0) // Unpause

    // ... create window and renderer ...
    let window = SDL.SDL_CreateWindow("Game Boy Emulator", SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED, Ppu.ScreenWidth * 3, Ppu.ScreenHeight * 3, SDL.SDL_WindowFlags.SDL_WINDOW_SHOWN)
    let renderer = SDL.SDL_CreateRenderer(window, -1, SDL.SDL_RendererFlags.SDL_RENDERER_ACCELERATED)
    let texture = SDL.SDL_CreateTexture(renderer, SDL.SDL_PIXELFORMAT_ARGB8888, int SDL.SDL_TextureAccess.SDL_TEXTUREACCESS_STREAMING, Ppu.ScreenWidth, Ppu.ScreenHeight)

    let romPath = argv.[0]
    let romBytes = File.ReadAllBytes romPath
    let mutable emulatorState = Emulator.initEmulator romBytes AudioFrequency // Pass sample rate to APU

    let mutable lastFrameTime = DateTime.Now

    let rec loop (state: EmulatorState) =
        // ... event handling ...
        let event = SDL.SDL_Event()
        if SDL.SDL_PollEvent(&event) <> 0 then
            match event.type with
            | SDL.SDL_EventType.SDL_QUIT -> { state with Running = false }
            | SDL.SDL_EventType.SDL_KEYDOWN ->
                match event.key.keysym.sym with
                | SDL.SDL_Keycode.SDLK_ESCAPE -> { state with Running = false }
                | _ -> state // Handle other key presses for input
            | _ -> state
        else
            let now = DateTime.Now
            let deltaTime = (now - lastFrameTime).TotalSeconds
            lastFrameTime <- now

            // Run emulator steps
            let mutable currentState = state
            // Aim for 60 FPS. CPU Speed Hz / 60 frames per second = cycles per frame.
            // Run in chunks to avoid single massive loop.
            let cyclesPerFrame = Constants.CpuSpeedHz / Constants.FramesPerSecond
            let mutable cyclesThisFrame = 0

            while cyclesThisFrame < cyclesPerFrame do
                // Execute one CPU instruction, updating CPU, MMU, and APU states
                let execResult = Cpu.executeInstruction currentState.CpuState currentState.MmuState currentState.ApuState
                
                // Update PPU and APU with the cycles consumed by the instruction
                let newPpuState = Ppu.tick execResult.Cycles currentState.PpuState currentState.MmuState.Io
                let newApuState = Apu.tick execResult.Cycles execResult.ApuState // APU tick after CPU cycles

                currentState <- { currentState with
                                    CpuState = execResult.CpuState
                                    MmuState = execResult.MmuState
                                    PpuState = newPpuState
                                    ApuState = newApuState // Update with the final APU state
                                 }
                cyclesThisFrame <- cyclesThisFrame + execResult.Cycles

            // PPU rendering
            Ppu.renderFrame currentState.PpuState renderer texture

            // Audio output: Copy samples from APU buffer to SDL2 audio queue
            let apuState = currentState.ApuState
            let bytesQueued = SDL_GetQueuedAudioSize(audioDeviceId) // Get amount of audio currently in SDL's queue
            let maxQueueBytes = uint32 (AudioFrequency * AudioChannels * sizeof<int16> * 2 / 60) // Roughly 2 frames of audio
            // Queue more audio only if the buffer isn't too full, to avoid latency
            if apuState.SampleBuffer.Count > 0 && bytesQueued < maxQueueBytes then
                // Convert float samples to int16
                let samplesToCopy = apuState.SampleBuffer.Count
                let audioData = Array.zeroCreate<int16>(samplesToCopy * AudioChannels) // Stereo output

                for i = 0 to samplesToCopy - 1 do
                    let sampleFloat = apuState.SampleBuffer.[i]
                    let sampleInt16 = (sampleFloat * 32767.0) |> int16 // Scale to 16-bit signed range
                    audioData.[i * AudioChannels] <- sampleInt16 // Left channel
                    audioData.[i * AudioChannels + 1] <- sampleInt16 // Right channel (simple stereo)

                // Queue audio data
                use handle = GCHandle.alloc audioData GCHandleType.Pinned
                let ptr = handle.AddrOfPinnedObject()
                let result = SDL_QueueAudio(audioDeviceId, ptr, uint (audioData.Length * sizeof<int16>()))
                if result < 0 then
                    printfn "SDL_QueueAudio Error: %s" (SDL.SDL_GetError())

                apuState.SampleBuffer.Clear() // Clear APU's internal buffer after queueing

            // Limit frame rate to 60 FPS
            let frameDuration = (DateTime.Now - now).TotalMilliseconds
            let targetFrameTime = 1000.0 / float Constants.FramesPerSecond
            if frameDuration < targetFrameTime then
                SDL.SDL_Delay(uint32 (targetFrameTime - frameDuration))

            { currentState with Running = true } // Continue loop

    // Start the main emulator loop
    let finalState = loop emulatorState

    // ... existing SDL cleanup ...
    SDL_PauseAudioDevice(audioDeviceId, 1) // Pause audio
    SDL_CloseAudioDevice(audioDeviceId) // Close audio device
    SDL.SDL_DestroyTexture(texture)
    SDL.SDL_DestroyRenderer(renderer)
    SDL.SDL_DestroyWindow(window)
    SDL.SDL_Quit()
    0

๐Ÿ”ฅ Optimization / Pro tip: Using SDL_QueueAudio directly is generally preferred for emulators because it gives you precise control over the raw sample data and streaming. SDL_mixer is more suited for playing pre-made sound effects and music files, not for generating samples on the fly. The GCHandle.alloc is used to pin the F# array in memory so its address can be passed to the unmanaged SDL function. The use keyword ensures handle.Free() is called automatically when the handle goes out of scope, preventing memory leaks. We’ve also added a check for SDL_GetQueuedAudioSize to prevent overfilling the audio buffer, which can cause latency or glitches.

Testing & Verification

With the APU integrated, it’s time to hear our work!

  1. Run with a ROM that has simple sounds:
    • Good candidates: Tetris, Dr. Mario, Alleyway. These games use square waves extensively for their music and sound effects.
    • Command: dotnet run --project GbEmulator.App -- path/to/tetris.gb
  2. Expected Behavior:
    • You should hear basic square wave sounds. The pitch might be correct, but the volume envelopes and length counters will be very basic or non-existent in this initial implementation.
    • Music might sound flat or continuous, lacking the dynamic changes you’d expect.
  3. Debugging Checks:
    • No sound at all?
      • Check SDL_Init and SDL_OpenAudioDevice return values in Program.fs. Any errors?
      • Is apu.Enabled (NR52 bit 7) being set by the ROM? Log its value in Apu.fs’s writeNr52.
      • Are square wave channels Enabled (NRx2 bit 7, and NRx4 bit 7)? Log channel Enabled state in Apu.fs.
      • Is apuState.SampleBuffer.Count increasing? If not, Apu.tick isn’t generating samples.
      • Are SDL_QueueAudio calls successful? Any error messages printed?
    • Garbled/choppy sound?
      • โš ๏ธ What can go wrong: This often indicates timing issues or buffer underruns/overruns. Is AudioFrequency matching what Emulator.initEmulator and Apu.initApu expect?
      • Is CpuCyclesPerSample calculated correctly?
      • Is the amount of data (in bytes) passed to SDL_QueueAudio correct? (It should be samples * channels * sizeof<int16>).
      • Are you calling Emulator.step enough times per frame to generate enough samples to keep the audio buffer full? Use SDL_GetQueuedAudioSize to monitor the buffer level. If it’s consistently low, your emulator isn’t feeding enough audio fast enough.
    • Incorrect pitch?
      • Double-check the Frequency calculation from NR13/NR14 registers (131072 / (2048 - frequency)).
      • Verify the Timer reload value ((2048 - channel.Frequency) * 4).
      • Ensure WavePosition is advancing correctly.

Production Considerations

  • Performance: Audio generation is a constant, real-time task. Our Apu.tick function must be extremely lightweight. The generateSquareWaveSample is simple enough for now, but more complex channels (wave, noise) and accurate frame sequencer logic will add overhead. Profile your Apu.tick function if you encounter performance bottlenecks.
  • Synchronization: The most critical aspect. The CpuCyclesPerSample calculation and PendingCpuCycles accumulator are vital for smoothly bridging the CPU’s irregular cycle counts with the fixed audio sample rate. Incorrect synchronization leads to crackling, popping, or incorrect pitch. Aim for cycle-accurate APU updates to match the Game Boy’s behavior.
  • Buffer Management: The SampleBuffer and SDL_QueueAudio implicitly manage buffers. Ensuring a steady stream of samples without underruns (starving the audio device) or overruns (too much data, leading to latency) is key. The AudioBufferSize for SDL_OpenAudioDevice and the maxQueueBytes logic are important here. Too small a buffer can lead to underruns, too large can lead to noticeable audio latency.
  • Accuracy vs. Simplicity: We’ve made significant simplifications (no full frame sequencer, basic mixing). A production-quality emulator would require cycle-accurate implementation of all APU components, including sweep, envelope, length counters, and complex mixing logic. This iterative approach allows us to get basic functionality working before tackling the more complex, nuanced behaviors.

Common Issues & Solutions

  1. Issue: No sound output at all.
    • Solution: Verify SDL initialization for audio, check SDL_OpenAudioDevice return value. Ensure APU.Enabled (NR52 bit 7) is set to 1 by the game, and individual channel enable bits (NRx2 bit 7, NRx4 bit 7) are also set. Log the SampleBuffer.Count in ApuState to confirm samples are being generated. Check if SDL_QueueAudio is reporting errors.
  2. Issue: Sound is choppy, distorted, or has clicks/pops.
    • Solution: This is almost always a timing or buffering issue. Ensure your AudioFrequency and AudioBufferSize are reasonable (e.g., 44100 Hz, 2048 samples). Verify that SDL_QueueAudio is being called consistently and that the amount of data queued is appropriate for the audio device’s buffer. If SDL_GetQueuedAudioSize reports too little data, you’re underrunning. Increase your PendingCpuCycles threshold before queuing, or process more CPU cycles per audio tick. Ensure float to int16 conversion handles clipping correctly (* 32767.0 is good for full range).
  3. Issue: Pitch is off or sounds static.
    • Solution: Check your Frequency and Timer calculations in Apu.fs. Consult Pan Docs for the exact formulas. A common mistake is incorrect division or multiplication factors. If sounds are static (no volume changes, no pitch slides), it means the length counter, envelope, and sweep units are not being emulated correctly (which is expected with our current basic implementation, but good to identify). This indicates a need for more advanced APU frame sequencer logic.

๐Ÿง  Check Your Understanding

  • What are the primary challenges when synchronizing the Game Boy’s APU with its CPU clock for accurate sound output?
  • Explain the purpose of the NRx4 register’s trigger bit (bit 7) and why it’s crucial for sound programming.
  • How does the functional approach in F# (using immutable records and functions returning new state) help manage the complexity of APU state updates?

โšก Mini Task

  • Add logging to your Apu.fs writeByte function to print the register address and value being written. Observe these logs when running a game like Tetris to see how the APU registers are manipulated by the game. Focus on 0xFF14 and 0xFF19 to see trigger events.

๐Ÿš€ Scenario

You’re running a Game Boy ROM in your emulator, and the music sounds correct in pitch, but it never fades out or changes volume dynamically. It just plays at a constant loudness until the song changes or the channel is explicitly turned off. What specific components of the Game Boy APU are most likely missing or simplified in your current emulator implementation? How would you begin to diagnose and address this in the code?

๐Ÿ“Œ TL;DR

  • The Game Boy APU has four sound channels; we started with two square wave channels.
  • APU state is modeled with F# records, and updated via CPU writes to I/O registers, with state propagated through Cpu.executeInstruction and Mmu.writeByte.
  • SDL_QueueAudio is used for direct, streaming output of generated audio samples, preferred for its control over raw sample data.
  • Synchronization between CPU cycles and audio sample rate is critical for smooth sound.

๐Ÿง  Core Flow

  1. CPU executes instructions, potentially writing to APU registers, returning updated CpuState, MmuState, and ApuState.
  2. MMU routes register writes to the Apu.writeByte function, updating ApuState.
  3. Emulator.step calls Apu.tick, advancing APU’s internal frequency timers and accumulating audio samples based on consumed CPU cycles.
  4. Accumulated samples are converted to int16 and queued to the SDL audio device for playback, managing buffer levels to prevent glitches.

๐Ÿš€ Key Takeaway

Emulating sound requires not only understanding hardware registers but also mastering the delicate balance of real-time synchronization and buffering to translate discrete, cycle-accurate updates into a continuous, smooth audio stream. This initial implementation provides basic sound, but true fidelity demands deeper emulation of the APU’s dynamic components.

References

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.