1pub mod audio;
2pub mod clip;
3pub mod cpal_backend;
4pub mod engine;
5pub mod metronome;
6pub mod mixer;
7pub mod project;
8pub mod transport;
9
10use serde::{Deserialize, Serialize};
11
12#[cfg(test)]
26pub(crate) mod alloc_count {
27 use std::alloc::{GlobalAlloc, Layout, System};
28 use std::cell::Cell;
29
30 thread_local! {
31 static ALLOCATIONS: Cell<u64> = const { Cell::new(0) };
32 }
33
34 struct Counting;
35
36 fn note_allocation() {
37 let _ = ALLOCATIONS.try_with(|c| c.set(c.get() + 1));
38 }
39
40 unsafe impl GlobalAlloc for Counting {
45 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
46 note_allocation();
47 System.alloc(layout)
48 }
49 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
50 System.dealloc(ptr, layout);
51 }
52 unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
53 note_allocation();
54 System.alloc_zeroed(layout)
55 }
56 unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
57 note_allocation();
58 System.realloc(ptr, layout, new_size)
59 }
60 }
61
62 #[global_allocator]
63 static COUNTING: Counting = Counting;
64
65 pub(crate) fn allocations_during(body: impl FnOnce()) -> u64 {
68 let before = ALLOCATIONS.with(Cell::get);
69 body();
70 ALLOCATIONS.with(Cell::get) - before
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
76pub struct EngineConfig {
77 pub buffer_size: u32,
80 pub sample_rate: u32,
82}
83
84impl Default for EngineConfig {
85 fn default() -> Self {
95 Self {
96 buffer_size: 64,
97 sample_rate: 44100,
98 }
99 }
100}
101
102#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
114pub struct AudioRequest {
115 pub sample_rate: Option<u32>,
117 pub buffer_size: Option<u32>,
119}
120
121impl AudioRequest {
122 #[must_use]
124 pub const fn follow_device() -> Self {
125 Self { sample_rate: None, buffer_size: None }
126 }
127
128 #[must_use]
132 pub fn without_device(self) -> EngineConfig {
133 let fallback = EngineConfig::default();
134 EngineConfig {
135 sample_rate: self.sample_rate.unwrap_or(fallback.sample_rate),
136 buffer_size: self.buffer_size.unwrap_or(fallback.buffer_size),
137 }
138 }
139}
140
141impl From<EngineConfig> for AudioRequest {
142 fn from(config: EngineConfig) -> Self {
145 Self {
146 sample_rate: Some(config.sample_rate),
147 buffer_size: Some(config.buffer_size),
148 }
149 }
150}
151
152impl From<crate::cpal_backend::StreamFormat> for EngineConfig {
153 fn from(format: crate::cpal_backend::StreamFormat) -> Self {
161 Self {
162 buffer_size: format.buffer_size.unwrap_or(format.max_buffer_frames),
163 sample_rate: format.sample_rate,
164 }
165 }
166}
167
168impl EngineConfig {
169 pub fn buffer_duration_secs(&self) -> f64 {
171 self.buffer_size as f64 / self.sample_rate as f64
172 }
173
174 pub fn buffer_duration_ms(&self) -> f64 {
176 self.buffer_duration_secs() * 1000.0
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn default_config_is_sensible() {
186 let config = EngineConfig::default();
187 assert_eq!(config.buffer_size, 64);
188 assert_eq!(config.sample_rate, 44100);
189 }
190
191 #[test]
192 fn an_empty_request_asks_for_nothing() {
193 let request = AudioRequest::follow_device();
194 assert_eq!(request.sample_rate, None);
195 assert_eq!(request.buffer_size, None);
196 assert_eq!(request, AudioRequest::default());
197 }
198
199 #[test]
201 fn without_a_device_the_gaps_are_filled_from_the_default() {
202 assert_eq!(
203 AudioRequest::follow_device().without_device(),
204 EngineConfig::default()
205 );
206 }
207
208 #[test]
211 fn without_a_device_what_was_asked_for_is_still_honoured() {
212 let request = AudioRequest { sample_rate: Some(96000), buffer_size: None };
213 let config = request.without_device();
214 assert_eq!(config.sample_rate, 96000);
215 assert_eq!(config.buffer_size, EngineConfig::default().buffer_size);
216 }
217
218 #[test]
219 fn a_concrete_config_converts_to_a_request_for_exactly_it() {
220 let config = EngineConfig { buffer_size: 256, sample_rate: 96000 };
221 assert_eq!(
222 AudioRequest::from(config),
223 AudioRequest { sample_rate: Some(96000), buffer_size: Some(256) }
224 );
225 assert_eq!(AudioRequest::from(config).without_device(), config);
226 }
227
228 #[test]
231 fn a_device_chosen_block_size_reports_the_worst_case() {
232 use crate::cpal_backend::{Requested, StreamFormat};
233 let format = StreamFormat {
234 sample_rate: 48000,
235 buffer_size: None,
236 max_buffer_frames: 4096,
237 channels: 2,
238 sample_rate_request: Requested::Unasked,
239 buffer_size_request: Requested::Unasked,
240 };
241 let config = EngineConfig::from(format);
242 assert_eq!(config.sample_rate, 48000);
243 assert_eq!(config.buffer_size, 4096);
244 }
245
246 #[test]
247 fn buffer_duration_calculation() {
248 let config = EngineConfig {
249 buffer_size: 64,
250 sample_rate: 44100,
251 };
252 let ms = config.buffer_duration_ms();
253 assert!((ms - 1.451).abs() < 0.01, "Expected ~1.45ms, got {ms}ms");
254 }
255
256 #[test]
257 fn buffer_duration_various_sizes() {
258 for (size, rate, expected_ms) in [
259 (64, 44100, 1.451),
260 (128, 44100, 2.902),
261 (256, 48000, 5.333),
262 (64, 96000, 0.667),
263 ] {
264 let config = EngineConfig {
265 buffer_size: size,
266 sample_rate: rate,
267 };
268 let ms = config.buffer_duration_ms();
269 assert!(
270 (ms - expected_ms).abs() < 0.01,
271 "size={size} rate={rate}: expected {expected_ms}ms, got {ms}ms"
272 );
273 }
274 }
275}