use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use tono_core::dsl::{Adsr, SeqWave, SoundDoc};
use tono_core::runtime::{
At, AudioSource, Command, Engine, Performance, SCRATCH_FRAMES, StreamSource,
};
use tono_core::song::{CompileOptions, Song, note};
static ENABLED: AtomicBool = AtomicBool::new(false);
static COUNT: AtomicUsize = AtomicUsize::new(0);
struct Counting;
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
if ENABLED.load(Ordering::SeqCst) {
COUNT.fetch_add(1, Ordering::SeqCst);
}
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
if ENABLED.load(Ordering::SeqCst) {
COUNT.fetch_add(1, Ordering::SeqCst);
}
unsafe { System.realloc(ptr, layout, new_size) }
}
}
#[global_allocator]
static ALLOC: Counting = Counting;
static SERIAL: Mutex<()> = Mutex::new(());
fn serial() -> std::sync::MutexGuard<'static, ()> {
SERIAL.lock().unwrap_or_else(|e| e.into_inner())
}
fn start_counting() {
COUNT.store(0, Ordering::SeqCst);
ENABLED.store(true, Ordering::SeqCst);
}
fn stop_counting() -> usize {
ENABLED.store(false, Ordering::SeqCst);
COUNT.load(Ordering::SeqCst)
}
fn amp() -> Adsr {
Adsr {
a: 0.005,
d: 0.1,
s: 0.8,
r: 0.2,
punch: 0.0,
}
}
fn program() -> Arc<tono_core::program::Program> {
let mut song = Song::new("rt-alloc", 120.0);
song.add_track("bass", SeqWave::Bass, amp());
song.add_track("keys", SeqWave::Epiano, amp());
song.add_pattern("riff", 1, vec![note(0, 4, "C2"), note(8, 4, "G2")]);
song.add_pattern("stab", 1, vec![note(0, 2, "C4"), note(6, 2, "D#4")]);
song.arrange_repeat("bass", "riff", 0, 4);
song.arrange_repeat("keys", "stab", 0, 4);
Arc::new(
song.compile(&CompileOptions {
sample_rate: Some(48_000),
..CompileOptions::default()
})
.expect("compiles"),
)
}
fn blip() -> SoundDoc {
serde_json::from_str(
r#"{ "name": "blip", "duration": 0.2, "root": { "type": "mul", "inputs": [
{ "type": "sawtooth", "freq": 880 },
{ "type": "env", "a": 0.0, "d": 0.05, "s": 0.0, "r": 0.01 } ] } }"#,
)
.unwrap()
}
const SIZES: [usize; 5] = [333, 512, 1024, SCRATCH_FRAMES, SCRATCH_FRAMES + 4096];
#[test]
fn performance_fill_is_allocation_free_after_warmup() {
let _guard = serial();
let mut p = Performance::new(program());
p.schedule(Command::Play, At::Immediate).unwrap();
p.schedule(Command::SetGain(0.7), At::Frame(6 * 1024))
.unwrap();
let mut block = vec![0.0f32; SIZES[SIZES.len() - 1] * 2];
for size in SIZES {
p.fill(&mut block[..size * 2]);
}
start_counting();
for _ in 0..3 {
for size in SIZES {
p.fill(&mut block[..size * 2]);
}
}
let allocs = stop_counting();
assert_eq!(allocs, 0, "Performance::fill allocated on the audio path");
}
#[test]
fn performance_first_oversized_fill_grows_scratch_once() {
let _guard = serial();
let mut p = Performance::new(program());
p.schedule(Command::Play, At::Immediate).unwrap();
let mut block = vec![0.0f32; (SCRATCH_FRAMES + 4096) * 2];
p.fill(&mut block[..512 * 2]);
start_counting();
p.fill(&mut block[..SCRATCH_FRAMES * 2]);
assert_eq!(stop_counting(), 0, "scratch-boundary block must not grow");
start_counting();
p.fill(&mut block[..(SCRATCH_FRAMES + 4096) * 2]);
let growth = stop_counting();
assert!(growth > 0, "the first oversized fill grows the scratch");
start_counting();
p.fill(&mut block[..(SCRATCH_FRAMES + 4096) * 2]);
p.fill(&mut block[..(SCRATCH_FRAMES + 4096) * 2]);
assert_eq!(
stop_counting(),
0,
"the oversized scratch is grown once, then reused"
);
}
#[test]
fn performance_fill_firing_a_stinger_is_allocation_free() {
let _guard = serial();
let mut p = Performance::new(program());
p.schedule(Command::Play, At::Immediate).unwrap();
p.stinger(&blip(), 0.8, At::Frame(8 * 1024)).unwrap();
let mut block = vec![0.0f32; 1024 * 2];
for _ in 0..4 {
p.fill(&mut block); }
start_counting();
for _ in 0..12 {
p.fill(&mut block); }
let allocs = stop_counting();
assert_eq!(p.metrics().stingers_fired, 1, "the stinger really fired");
assert_eq!(
allocs, 0,
"firing a stinger inside Performance::fill must not render or allocate \
(the render happened at schedule time)"
);
}
#[test]
fn stream_source_fill_is_allocation_free_after_warmup() {
let _guard = serial();
let program = program();
let mut src = StreamSource::from_doc(&program.doc).expect("the program streams");
let mut block = vec![0.0f32; SIZES[SIZES.len() - 1] * 2];
for size in SIZES {
src.fill(&mut block[..size * 2]); }
start_counting();
for _ in 0..3 {
for size in SIZES {
src.fill(&mut block[..size * 2]);
}
}
assert_eq!(
stop_counting(),
0,
"StreamSource::fill allocated on the audio path"
);
}
#[test]
fn renderer_fill_is_allocation_free() {
let _guard = serial();
let mut engine = Engine::new(48_000);
let patch = engine.load(&blip());
engine.play_looping(patch);
let (mut ctl, mut rend) = engine.split(4096);
ctl.pump(1024); let mut block = vec![0.0f32; 1024 * 2];
rend.fill(&mut block[..512 * 2]); start_counting();
for size in [128usize, 333, 512, 1024] {
ctl.pump(size); rend.fill(&mut block[..size * 2]);
}
assert_eq!(
stop_counting(),
0,
"Renderer::fill allocated on the audio path"
);
}
fn other_program() -> Arc<tono_core::program::Program> {
let mut song = Song::new("rt-alloc-other", 100.0);
song.add_track("lead", SeqWave::Square, amp());
song.tracks[0].notes.push(note(0, 16, "A4"));
Arc::new(
song.compile(&CompileOptions {
sample_rate: Some(48_000),
..CompileOptions::default()
})
.expect("compiles"),
)
}
#[test]
fn performance_fill_across_a_swap_is_allocation_free() {
let _guard = serial();
let mut p = Performance::new(program());
p.schedule(Command::Play, At::Immediate).unwrap();
p.swap_to(other_program(), At::Frame(8 * 1024)).unwrap();
let mut block = vec![0.0f32; 1024 * 2];
for _ in 0..4 {
p.fill(&mut block); }
start_counting();
for _ in 0..12 {
p.fill(&mut block); }
let allocs = stop_counting();
assert_eq!(p.metrics().swaps, 1, "the swap really happened");
assert_eq!(
allocs, 0,
"executing a swap inside Performance::fill must not render or allocate \
(the source built at schedule time)"
);
}