atome 0.8.0

An async, `cpal`-based audio engine for Rust. `atome` gives you a small, composable API for building real-time audio applications — sample playback, mixing, routing, and DSP graphs — without wrestling with platform audio callbacks, device enumeration, or buffer management yourself.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
use cpal::traits::DeviceTrait;
use cpal::{Device, Error, ErrorKind, SampleFormat, Stream, StreamConfig};
use ringbuf::traits::{Consumer, Producer, Split};
use ringbuf::{HeapCons, HeapProd, HeapRb};
use std::marker::PhantomData;


pub mod mixer;
pub mod types;
pub mod utils;

pub use mixer::{ClearSignal, MixCommand, Mixer, MixerHandle};
pub use types::{OutputType, SampleRate, SampleType};
pub use utils::{
    default_device, device_name, find_device, get_host_by_id, get_host_by_name, get_host_in_name,
    list_device_names, list_devices, list_hosts,
};

/// Frames assumed when the caller asks for the device's default buffer size.
/// Only used to size the ring buffer — the stream itself still asks the device
/// for its own preferred size.
///
/// The ring holds exactly one buffer size, so this is a guess at a number the
/// device actually decides. If the device then picks a period *larger* than
/// this, the ring cannot hold a whole callback and every callback gets a
/// partially filled buffer. Pass an explicit `buffer_size` to make the two match.
const DEFAULT_BUFFER_FRAMES: i32 = 1024;

/// How many `MixCommand`s can be queued before `add_samples` starts refusing.
const COMMAND_QUEUE_LEN: usize = 256;

/// # OutputClass
///
/// Used to hold a stream to a single output.
///
/// `S` *is* the sample type this output speaks: `OutputClass<f32>`,
/// `OutputClass<i16>`, `OutputClass<cpal::I24>`, and so on. Fixing it at the
/// type level means the callback is picked once, when the stream is built,
/// instead of re-deciding on every audio callback, and `add_samples` can only
/// ever be handed matching data.

pub struct OutputClass<S: SampleType> {
    // Number of channels in this output
    channels: u16,
    // Output device to use
    device: Device,
    // Type of device
    out_type: OutputType,
    // The buffer size in frames, one for all outputs; `None` = device default
    buffer_size: Option<i32>,
    // The sample rate
    sample_rate: SampleRate,
    // stream for cpal
    stream: Option<Stream>,
    // Config the stream is (or will be) built with
    stream_config: StreamConfig,
    // Mixer input queue: `add_samples` writes indexed work here, never audio
    commands: HeapProd<MixCommand<S>>,
    // Consumer half of the audio ring buffer, sized to exactly one buffer size:
    // taken by `build_stream` and moved into the audio callback. The mixer owns
    // the producer half and keeps it topped up.
    consumer: Option<HeapCons<S>>,
    // Keeps the mixer thread running for as long as this output exists
    mixer: MixerHandle,
    // The sample type lives only in the type system; nothing is stored for it.
    sample_type: PhantomData<S>,
}

impl<S: SampleType> OutputClass<S> {
    pub fn new(device: Option<Device>, out_type: OutputType, channels: u16, sample_rate:SampleRate, buffer_size: Option<i32>) -> Self {
        // `None` lets the device pick its own buffer size. We can't know that
        // size until the stream is built, so the ring buffer is sized off a
        // default instead.
        let buffer_size_form = match buffer_size {
            Some(frames) => cpal::BufferSize::Fixed(frames as u32),
            None => cpal::BufferSize::Default,
        };
        let frames = buffer_size.unwrap_or(DEFAULT_BUFFER_FRAMES).max(1) as usize;


        let stream_config = StreamConfig {
            channels,
            sample_rate: sample_rate as u32,
            buffer_size: buffer_size_form,
        };

        // Exactly one buffer size: this ring holds only the samples about to go
        // to CPAL, never more. Everything further ahead stays in the mixer's own
        // buffer, where later commands can still be summed into it.
        let capacity = frames * channels.max(1) as usize;
        let (audio_producer, consumer) = HeapRb::<S>::new(capacity).split();

        // add_samples -> commands -> mixer -> audio ring buffer -> callback.
        let (commands, pending) = HeapRb::<MixCommand<S>>::new(COMMAND_QUEUE_LEN).split();
        let mixer = MixerHandle::spawn(Mixer::new(pending, audio_producer));

        OutputClass {
            channels,
            device: device
                .unwrap_or_else(|| default_device().expect("no default output device available")),
            out_type,
            buffer_size,
            sample_rate,
            stream: None,
            stream_config,
            commands,
            consumer: Some(consumer),
            mixer,
            sample_type: PhantomData,
        }
    }

    pub fn channels(&self) -> u16 {
        self.channels
    }
    pub fn device(&self) -> Device {
        self.device.clone()
    }
    pub fn name(&self) -> String {
        device_name(&self.device)
    }
    /// The configured buffer size in frames, or `None` if the device picks it.
    pub fn buffer_size(&self) -> Option<i32> {
        self.buffer_size
    }
    pub fn sample_rate(&self) -> i32 {
        self.sample_rate as i32
    }
    /// The value-level tag for `S`, for when a `SampleFormat` is needed at runtime.
    pub fn sample_format(&self) -> SampleFormat {
        S::format()
    }

    pub fn sample_type(&self) -> SampleFormat {
        S::format()
    }
    pub fn out_type(&self) -> OutputType {
        self.out_type
    }
    /// The body of the output callback.
    ///
    /// The ring buffer already holds `S`, so this pops one callback's worth of
    /// interleaved samples straight into the device's buffer — no conversion, no
    /// scratch buffer, no allocation on the audio thread. `add_samples` queues
    /// frames interleaved (L,R,L,R,…) and the ring buffer is FIFO, so they go
    /// out in exactly the order they came in.
    ///
    /// This is also the only place the ring buffer can be emptied — the mixer
    /// holds the producer half and cannot take anything back out — so a
    /// [`stop`](Self::stop) is finished here, not where it is asked for.
    fn data_callback(
        data: &mut [S],
        buffer: &mut HeapCons<S>,
        clear: &ClearSignal,
        cleared: &mut usize,
    ) {
        let epoch = clear.epoch();
        if epoch != *cleared {
            // A stop is in flight: drop what was already committed and play
            // silence instead of it. This keeps happening until the mixer has
            // answered the same stop, because until then it may still be
            // flushing pre-stop samples into the ring behind us.
            buffer.clear();
            data.fill(S::SILENCE);
            if clear.acked() == epoch {
                *cleared = epoch;
            }
            return;
        }

        let read = buffer.pop_slice(data);
        if read < data.len() {
            // Underrun: pad with silence rather than replaying stale audio.
            data[read..].fill(S::SILENCE);
        }
    }

    /// Builds the cpal output stream for this output's device, pulling mixed
    /// samples out of its ring buffer and writing them as `S`. The resulting
    /// `Stream` is stored on `self` (streams must be kept alive to keep playing)
    /// and also returned.
    ///
    /// Consumes the consumer half of the ring buffer, so it can only be called
    /// once per `OutputClass`.
    pub fn build_stream(&mut self) -> Result<&Stream, Error> {
        let mut buffer = self.consumer.take().ok_or_else(|| {
            Error::with_message(
                ErrorKind::UnsupportedOperation,
                "stream already built for this output",
            )
        })?;

        // Shared with the mixer thread so `stop` can reach the samples that are
        // already past it, sitting in the ring buffer.
        let clear = self.mixer.clear_signal();
        let mut cleared = clear.epoch();

        let stream = self.device.build_output_stream(
            self.stream_config,
            move |data: &mut [S], _: &cpal::OutputCallbackInfo| {
                Self::data_callback(data, &mut buffer, &clear, &mut cleared)
            },
            Self::err_fn,
            None,
        )?;

        self.stream = Some(stream);
        Ok(self.stream.as_ref().unwrap())
    }

    /// Schedules interleaved samples for playback at absolute sample `index`,
    /// summing them with anything already scheduled there. They stay in this
    /// output's sample type the whole way — `&[i16]` into an `OutputClass<i16>`
    /// is queued, mixed, and played as `i16`, never converted.
    ///
    /// This only hands a [`MixCommand`] to the mixer — no audio touches the
    /// stream's ring buffer here. Returns the index just past the batch, ready
    /// to pass straight back in for the next one.
    ///
    /// Fails if the mixer's command queue is full, which means it is not
    /// draining fast enough; the whole batch is refused rather than half of it.
    pub fn add_samples(&mut self, samples: &[S], index: usize) -> Result<usize, Error> {
        
        
        let command = MixCommand {
            index,
            samples: samples.to_vec(),
        };

        self.commands.try_push(command).map_err(|_| {
            Error::with_message(ErrorKind::ResourceExhausted, "mixer command queue is full")
        })?;

        Ok(index + samples.len())
    }
    /// Schedules interleaved samples for playback at a `time`.
    /// Time is in secounds from now, so it will delay the start of the audio
    ///
    /// The index is `time * sample_rate * channels`, since an index counts
    /// interleaved samples rather than frames — a stereo second is twice as
    /// many indices as a mono one.
    ///
    /// Note that "from now" is measured from the *start of the stream*, not
    /// from wherever playback has reached: the mixer's play cursor is not
    /// visible from here, so calling this after playback has begun schedules
    /// into the past and the samples are dropped as late. Exposing the cursor
    /// through [`MixerHandle`] would be the way to make it truly relative.
    pub fn add_samples_time(&mut self, samples: &[S], time: usize) -> Result<usize, Error> {
        let index = time * self.sample_rate as usize * self.channels.max(1) as usize;

        let command = MixCommand {
            index,
            samples: samples.to_vec(),
        };

        self.commands.try_push(command).map_err(|_| {
            Error::with_message(ErrorKind::ResourceExhausted, "mixer command queue is full")
        })?;

        Ok(index + samples.len())
    }

    /// Deletes all samples that have been scheduled but not yet played, and resets the
    /// mixer to the current time. This is useful if you want to stop all audio
    /// immediately and start fresh.
    ///
    /// Both stages are cleared: the mixer's queued commands and accumulation
    /// buffer, and the ring buffer the audio callback is already reading from —
    /// otherwise up to one buffer of committed audio would still play out.
    ///
    /// The stream keeps running and plays silence; the output stays usable and
    /// [`add_samples`](Self::add_samples) works again straight away. It is not
    /// instant: the mixer answers on its next pass and the callback on its
    /// next, so the tail is at most one buffer long.
    ///
    /// The play cursor does not move: it is an absolute position in the stream,
    /// not a count of what got played, so anything scheduled behind it is still
    /// late after a stop.
    pub fn stop(&mut self) {
        self.mixer.clear_samples();
    }
    /// Drops the stream, stopping playback. The output can be re-used by calling
    /// [`build_stream`](Self::build_stream) again.
    pub fn close(&mut self) {
        self.stream.take();
    }
    /// Handle to the mixer thread feeding this output.
    pub fn mixer(&self) -> &MixerHandle {
        &self.mixer
    }
    /// Aligns the samples to the output's sample rate and channel count.
    ///
    /// Set `interleaved` to describe what you are handing in: `true` for frames
    /// (L,R,L,R,…), `false` for planar data, where each channel is one
    /// contiguous run. Planar input is woven into frames before anything else
    /// happens to it.
    ///
    /// The result is always interleaved at `self.channels` / `self.sample_rate`,
    /// ready to hand to [`add_samples`](Self::add_samples). Fails if `channels`
    /// is zero or `samples` does not hold a whole number of frames.
    pub fn align_samples(&self, samples: &[S], sample_rate: SampleRate, channels: u16, interleaved: bool) -> Result<Vec<S>, Error> {
        if channels == 0 {
            return Err(Error::with_message(
                ErrorKind::InvalidInput,
                "sample data must have at least one channel",
            ));
        }
        let src_channels = channels as usize;
        if samples.len() % src_channels != 0 {
            return Err(Error::with_message(
                ErrorKind::InvalidInput,
                "sample count is not a whole number of frames",
            ));
        }
        // The device always takes interleaved data, so interleaved input that
        // already matches the output needs nothing done to it.
        if sample_rate == self.sample_rate && channels == self.channels && interleaved {
            return Ok(samples.to_vec());
        }

        let dst_channels = self.channels.max(1) as usize;
        let frames = samples.len() / src_channels;

        // Weave first: everything after this point indexes by frame, so planar
        // input has to become frames before it can be remapped or resampled.
        // One channel is the same bytes either way, so it skips the copy.
        let mut buffer = if interleaved || src_channels == 1 {
            samples.to_vec()
        } else {
            Self::weave(samples, frames, src_channels)
        };

        if src_channels != dst_channels {
            buffer = Self::map_channels(&buffer, frames, src_channels, dst_channels);
        }

        if sample_rate != self.sample_rate {
            buffer = Self::resample(
                &buffer,
                frames,
                dst_channels,
                sample_rate as u32,
                self.sample_rate as u32,
            );
        }

        Ok(buffer)
    }

    /// Weaves planar samples — channel 0's `frames` samples, then channel 1's,
    /// and so on — into interleaved frames (L,R,L,R,…), the only layout the
    /// mixer and the device ever see.
    fn weave(samples: &[S], frames: usize, channels: usize) -> Vec<S> {
        let mut out = Vec::with_capacity(samples.len());
        for frame in 0..frames {
            for channel in 0..channels {
                out.push(samples[(channel * frames) + frame]);
            }
        }
        out
    }

    /// Re-lays `frames` interleaved frames of `src_channels` into `dst_channels`.
    ///
    /// Mono input fans out to every output channel and a mono output sums the
    /// input down; anything else keeps channel `n` as channel `n`, dropping
    /// the extras when narrowing and filling with silence when widening. That
    /// is deliberately dumb about channel *meaning* — a 5.1 stream narrowed to
    /// stereo keeps front-left/front-right and loses the rest rather than
    /// folding them in.
    fn map_channels(samples: &[S], frames: usize, src_channels: usize, dst_channels: usize) -> Vec<S> {
        let mut out = Vec::with_capacity(frames * dst_channels);
        for frame in 0..frames {
            let start = frame * src_channels;
            if src_channels == 1 {
                out.extend(std::iter::repeat(samples[start]).take(dst_channels));
            } else if dst_channels == 1 {
                // Summed with the format's own `mix`, so an `i16` source stays
                // `i16` the whole way down — no float round-trip. Folding N
                // channels into one is correspondingly louder and can overflow
                // the format, so attenuate before calling if the source is hot.
                let mut sum = S::SILENCE;
                for sample in &samples[start..start + src_channels] {
                    sum = sum.mix(*sample);
                }
                out.push(sum);
            } else {
                for channel in 0..dst_channels {
                    out.push(if channel < src_channels {
                        samples[start + channel]
                    } else {
                        S::SILENCE
                    });
                }
            }
        }
        out
    }

    /// Resamples interleaved `frames` from `from` Hz to `to` Hz, linearly
    /// interpolating between the two input frames each output frame falls
    /// between.
    ///
    /// Linear interpolation is cheap and has no pre-ring, but it is not
    /// band-limited: downsampling folds anything above the new Nyquist back
    /// into the audible range, so low-pass filter first if the source has real
    /// content up there.
    fn resample(samples: &[S], frames: usize, channels: usize, from: u32, to: u32) -> Vec<S> {
        if frames == 0 {
            return Vec::new();
        }
        let ratio = to as f64 / from as f64;
        let out_frames = ((frames as f64) * ratio).round().max(1.0) as usize;
        let last = frames - 1;

        let mut out = Vec::with_capacity(out_frames * channels);
        for frame in 0..out_frames {
            // Where this output frame lands on the input timeline.
            let position = frame as f64 / ratio;
            let left = (position.floor() as usize).min(last);
            let right = (left + 1).min(last);
            let t = (position - position.floor()) as f32;
            for channel in 0..channels {
                let a = samples[left * channels + channel].to_f32();
                let b = samples[right * channels + channel].to_f32();
                out.push(S::from_f32(a + (b - a) * t));
            }
        }
        out
    }

    fn err_fn(err: cpal::Error) {
        eprintln!("audio output stream error: {}", err);
    }

}