Skip to content

Audio Engine

B0ney edited this page Jun 16, 2023 · 15 revisions

Check out core

TODO: include info from https://hackmd.io/@rdrpenguin/Sk8cOsqCs#RMMS-Song-Engine

TODO: look at how LMMS, Hydrogen drum machine & Polyhhone editor does it.


todo: I forgot where I found this equation.

frames_per_tick = (sample_rate * 60) / beats_per_minute / resolution

Calculating the latency (in ms) of a given buffer size (in frames):

latency = buffer_size * (sample_rate / 1000)
256 * (44100 / 1000) = ~5.8 ms

For optimal performance, it's important to set the buffer size as small as it can without causing xruns.

...what are "xruns"?

( under | over )run, it's when the program fails process the required frames in time. xruns usually come out as audible clicks, pops, crackly audio, or repeating sounds. In most cases, they can be mitigated by increasing the buffer size.

Name Ideas

Give it a cool name

  • Apollo
  • Helios
  • Tritium (Hydrogen was taken)

Components

PlayHandle

(B0ney) Note: This is subject to change as I don't really know if this approach has any glaring flaws. Also, the concept of "PlayHandles" was adopted from LMMS, but it's not a complete carbon copy (nor should it aim to be just for the sake of it).

PlayHandles are a fundamental building block to RMMS' audio engine. Understanding how they work should give you an upper edge when interfacing with it.

You can think of PlayHandles as signal generators. The engine simply needs to request for frames and it could give out frames. The engine can also ask PlayHandles to "reset" its internal state (e.g. starting from the beginning), or even jump to a position in time (e.g. jumping to the last 30 seconds of your track).

A PlayHandle is currently defined in this manner:

pub trait PlayHandle: Send + Sync {
    fn next(&mut self) -> Option<[f32; 2]>;
    fn reset(&mut self);
    fn jump(&mut self, frame: usize);

    /* More on this later */
    fn fill(&mut self, buffer: &mut [[f32; 2]]) -> Option<usize> {
        // TODO
    } 
}

Those who've programmed in Rust may notice something familiar about this trait: it's awfully similar to Rust's iterator trait; and you'd be right! PlayHandles are technically iterators, but with extra methods attached to them.

How the Engine Uses Them

PlayHandles are sent to the audio engine by storing them in an event enum:

pub enum Event {
    /* ... */
    PushPlayHandle(Box<dyn PlayHandle>),
}

When the engine receives a PushPlayHandle event, it will take the inner value and add it to its list of PlayHandles.

For every PlayHandle stored in the engine, it will be asked to produce a frame(s), these then get mixed (added together) by the engine.

If a PlayHandle returns None, the engine will delete it. Deleting PlayHandles are fairly efficient because the order in which they appear does not matter, so swap_remove can be used (it's O(1)).

TODO: discuss the playhandle "fill" method and how it could be used as an alternative to the "next" method.

Examples

Metronome PlayHandle

tick tock tock tock

tick tock tock tock

tick tock tock tock

...

Sample PlayHandle

File Stream PlayHandle

use case:

Previewing samples should be near instant. For that reason, it would be a good idea to stream it rather than loading everything to memory.

Note PlayHandle

Also have a look at core#note

Advanced uses

TODO

  • Chaining PlayHandles, Building Filters, effects and Channels

Resampler

The audio engine's internal sample rate can differ from the output device's.

Automation System

Shared mutable state is hard in Rust.

Idea: explore using a graph data structure

struct Graph {
  vertices: Vec<Node>,
}

struct Node {
  edges: Vec<usize>,
  /* additional data e.g Arc<AtomicUsize>*/
}

Architecture