Firewheel Audio Graph Engine

repository·main·Indexed 43 days ago

https://github.com/billydm/firewheel

A modular, real-time safe audio graph engine written in Rust for games and applications. Version 0.14.0 supports directed acyclic graphs (DAG), multiple platform backends (including cpal and rtaudio), and is designed for extensibility via custom nodes and ECS-friendly parameter APIs. It features precise timing via AudioClock, musical transport management, and no_std compatibility.

Tokens
40K
Snippets
119
Records
168
Agent score
55%

What's inside Firewheel

  1. What is Firewheel?

    main

    Firewheel is an open-source audio graph engine written in Rust, designed for games and other applications. It can be used as a standalone engine or as a foundation for building higher-level audio engines. It is designed to be modular, allowing it to run on any backend that provides an audio stream.

    Key characteristics include:

    • Flexible Audio Graph: Supports directed acyclic graphs (DAG) with both one-to-many and many-to-one connections.
    • Real-time Safety: Respects real-time constraints by avoiding mutexes.
    • Performance Optimizations: Includes silence optimizations to skip processing for buffers containing only zeros.
    • Fault Tolerance: Designed to handle audio stream interruptions (e.g., unplugging headphones) without crashing the application.
    • Extensibility: Provides a custom audio node API for 3rd party generators and effects, and an optional data-driven parameter API compatible with Entity Component Systems (ECS).
  2. Overview of Firewheel Architecture

    main

    Firewheel is a modular audio engine designed for games, built to run on any backend that provides an audio stream. The architecture is split into several specialized crates:

    • firewheel-core: Common types, utilities, and the Audio Node API.
    • firewheel-cpal: The default backend using the CPAL library (supports Windows, Mac, Linux, Android, iOS, and WebAssembly).
    • firewheel-graph: The core audio graph engine.
    • firewheel-macros: Macros for parameter diffing and patching.
    • firewheel-nodes: Built-in factory nodes (e.g., volume, panning, sampler).
    • firewheel-pool: Enables node pools for dynamic work assignment.
  3. Key features of Firewheel

    main

    Firewheel provides several core capabilities for audio processing:

    • Multi-platform Backends: Support for Windows, Mac, Linux, Android, iOS, and WebAssembly.
    • Built-in Nodes: A suite of essential audio nodes is included out-of-the-box.
    • File Loading: Supports loading various audio file formats via the Symphonium crate.
    • no_std Compatibility: The engine is compatible with no_std environments (though some features may require the standard library).
    • Modular Design: Can be integrated into different environments, including a planned upstreaming into the Bevy game engine.
  4. Use Clocks for Timing and Synchronization

    main

    Firewheel provides three types of clocks to handle different timing requirements within the audio stream:

    Seconds Clock

    Recommended for most general use cases. It tracks total elapsed seconds as an f64. It is highly accurate as it reads from the OS's native audio API and accounts for output underflows.

    Usage Pattern:

    1. Retrieve current time: AudioGraph::clock_now().
    2. Schedule an event: Use EventDelay::DelayUntilSeconds(current_time + delay).

    Sample Clock

    Tracks the total number of samples processed since the stream started. It is very accurate but does not account for output underflows.

    Musical Clock

    A user-controlled clock (start, pause, resume, stop) that counts musical beats as an f64. Ideal for syncing events to a tempo. It does not account for underflows; users should poll the clock from the context to maintain game sync.

  5. Implement silence optimizations in audio nodes

    main

    To prevent unnecessary processing in the audio graph, nodes can skip processing if their inputs are silent.

    1. Check Inputs: Audio nodes can read ProcInfo::in_silence_mask to determine which input buffers contain silence. If all input buffers are silent, the node can skip its processing step.
    2. Notify Graph: Nodes that output audio must notify the graph about which output channels contain silence. This is handled via the ProcessStatus type defined in crates/firewheel-core/src/node.rs.
  6. WebAssembly (WASM) platform constraints

    main

    When targeting WebAssembly, the following constraints and behaviors apply to the Firewheel engine:

    • No C or System Library Dependencies: CLAP plugin hosting is disabled when compiling to WASM.
    • No File I/O: Asset loading is not handled by the engine; the host game engine is responsible for loading assets.
    • Thread Management:
      • The engine will not spawn its own threads. The audio backend (e.g., CPAL) is responsible for spawning the audio thread.
      • The creek crate is disabled for WASM targets because it requires threads and file operations which are unsupported.
    • Non-blocking: The engine must not block threads.
  7. Understand the Firewheel Engine Lifecycle

    main

    The engine follows a specific lifecycle to ensure real-time safety and synchronization:

    1. Initialization: A context containing an audio graph is initialized.
    2. Activation: The context is activated using an audio stream from a backend. A real-time safe message channel is created, and a processor (executor) is sent to the audio stream. The graph is then "compiled" into a schedule and sent to the executor. If compilation fails, the context is deactivated.
    3. Active State: The user must periodically call the update method on the context (typically once per frame). This method:
      • Flushes queued events to the audio thread in a single group to ensure they occur in the same process cycle.
      • Checks for graph changes and recompiles the schedule if necessary.
    4. Deactivation: Occurs if the user requests it (e.g., changing audio settings) or if the audio stream is interrupted (e.g., unplugging headphones).
  8. Configure firewheel-rtaudio prerequisites

    main

    All platforms require CMake to be installed. Depending on your operating system, follow these steps:

    Linux

    Install the necessary system dependencies using apt:

    apt install cmake pkg-config libasound2-dev libpulse-dev

    If you plan to use the jack_linux feature, you must also install the JACK development headers:

    apt install libjack-dev

    MacOS

    Install cmake using one of these methods:

    • Option 1: Download directly from https://cmake.org/.
    • Option 2: Use Homebrew:
      brew install cmake

    Windows

    Install cmake by downloading it from https://cmake.org/.

  9. Install the firewheel-rtaudio backend

    main

    The firewheel-rtaudio backend uses RtAudio (via rtaudio-rs) to provide better support for "full duplex" audio devices compared to the default CPAL backend, resulting in lower latency between input and output streams.

    Note: This backend only supports Windows, MacOS, and Linux desktop platforms.

  10. What is a Notify<T> and when to use it

    main

    A Notify<T> is a lightweight wrapper around a value of type T that guarantees an event will be generated every time the inner value is accessed mutably, even if the value itself does not change.

    This is particularly useful for types where periodic updates are meaningful even if the data is identical, such as a play head position.

    Key behaviors:

    • Mutation triggers updates: Any mutable access via DerefMut, AsMut, or calling .notify() increments an internal NotifyID, signaling a change.
    • Identity-based diffing: When using Diff::diff, a change is detected if the NotifyID differs, regardless of whether the inner T is equal to the baseline.
    • Unsync access: You can use .as_mut_unsync() to get mutable access to the inner value without triggering a new ID/notification.
    // Diffing two separate instances with the same value will produce an event
    let a = Notify::new(1);
    let b = Notify::new(1);
    // a.diff(&b, ...) -> produces event
    
    // Diffing a clone will NOT produce an event
    let c = b.clone();
    // b.diff(&c, ...) -> no event