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