-
Notifications
You must be signed in to change notification settings - Fork 3
Audio Engine
Nothing here yet... Just dust and echos...
Check out core
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.
(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.
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.
tick tock tock tock
tick tock tock tock
tick tock tock tock
...
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.
Also have a look at core#note
TODO
- Chaining PlayHandles, Building Filters, effects and Channels
The audio engine's internal sample rate can differ from the output device's.
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>*/
}