Add audio output for the CPAL backend
This commit is contained in:
parent
c47f2139a3
commit
c46a044cb4
1 changed files with 154 additions and 13 deletions
|
@ -1,5 +1,6 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use cpal::{traits::*, Device, SampleFormat, StreamConfig};
|
use cpal::{traits::*, Device, OutputCallbackInfo, Sample, SampleFormat, StreamConfig};
|
||||||
|
use crossbeam::sync::{Parker, Unparker};
|
||||||
|
|
||||||
use super::super::config::WrapperConfig;
|
use super::super::config::WrapperConfig;
|
||||||
use super::Backend;
|
use super::Backend;
|
||||||
|
@ -10,22 +11,70 @@ use crate::plugin::{AuxiliaryIOConfig, BusConfig, Plugin};
|
||||||
|
|
||||||
/// Uses CPAL for audio and midir for MIDI.
|
/// Uses CPAL for audio and midir for MIDI.
|
||||||
pub struct Cpal {
|
pub struct Cpal {
|
||||||
|
config: WrapperConfig,
|
||||||
bus_config: BusConfig,
|
bus_config: BusConfig,
|
||||||
input_device: Option<Device>,
|
|
||||||
input_config: Option<StreamConfig>,
|
input: Option<(Device, StreamConfig, SampleFormat)>,
|
||||||
|
|
||||||
output_device: Device,
|
output_device: Device,
|
||||||
output_config: StreamConfig,
|
output_config: StreamConfig,
|
||||||
|
output_sample_format: SampleFormat,
|
||||||
// TODO: MIDI
|
// TODO: MIDI
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend for Cpal {
|
impl Backend for Cpal {
|
||||||
fn run(
|
fn run(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut cb: impl FnMut(&mut Buffer, Transport, &[NoteEvent], &mut Vec<NoteEvent>) -> bool
|
cb: impl FnMut(&mut Buffer, Transport, &[NoteEvent], &mut Vec<NoteEvent>) -> bool
|
||||||
+ 'static
|
+ 'static
|
||||||
+ Send,
|
+ Send,
|
||||||
) {
|
) {
|
||||||
// TODO:
|
// The CPAL audio devices may not accept floating point samples, so all of the actual audio
|
||||||
|
// handling and buffer management handles in the `build_*_data_callback()` functions defined
|
||||||
|
// below
|
||||||
|
|
||||||
|
// This thread needs to be blocked until audio processing ends as CPAL processes the streams
|
||||||
|
// on another thread instead of blocking
|
||||||
|
// TODO: Move this to the output stream handling
|
||||||
|
// TODO: Input stream
|
||||||
|
// TODO: Block the main thread until this breaky thing
|
||||||
|
let parker = Parker::new();
|
||||||
|
let unparker = parker.unparker().clone();
|
||||||
|
|
||||||
|
let error_cb = {
|
||||||
|
let unparker = unparker.clone();
|
||||||
|
move |err| {
|
||||||
|
nih_error!("Error during playback: {err:#}");
|
||||||
|
unparker.clone().unpark();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let output_stream = match self.output_sample_format {
|
||||||
|
SampleFormat::I16 => self.output_device.build_output_stream(
|
||||||
|
&self.output_config,
|
||||||
|
self.build_output_data_callback::<i16>(unparker, cb),
|
||||||
|
error_cb,
|
||||||
|
),
|
||||||
|
SampleFormat::U16 => self.output_device.build_output_stream(
|
||||||
|
&self.output_config,
|
||||||
|
self.build_output_data_callback::<u16>(unparker, cb),
|
||||||
|
error_cb,
|
||||||
|
),
|
||||||
|
SampleFormat::F32 => self.output_device.build_output_stream(
|
||||||
|
&self.output_config,
|
||||||
|
self.build_output_data_callback::<f32>(unparker, cb),
|
||||||
|
error_cb,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
.expect("Fatal error creating the output stream");
|
||||||
|
|
||||||
|
// TODO: Wait a period before doing this when also reading the input
|
||||||
|
output_stream
|
||||||
|
.play()
|
||||||
|
.expect("Fatal error trying to start the output stream");
|
||||||
|
|
||||||
|
// Wait for the audio thread to exit
|
||||||
|
parker.park();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +87,7 @@ impl Cpal {
|
||||||
// No input device is connected unless requested by the user to avoid feedback loops
|
// No input device is connected unless requested by the user to avoid feedback loops
|
||||||
let input_device = config
|
let input_device = config
|
||||||
.input_device
|
.input_device
|
||||||
|
.as_ref()
|
||||||
.map(|name| -> Result<Device> {
|
.map(|name| -> Result<Device> {
|
||||||
let device = host
|
let device = host
|
||||||
.input_devices()
|
.input_devices()
|
||||||
|
@ -62,7 +112,7 @@ impl Cpal {
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
let output_device = match config.output_device {
|
let output_device = match config.output_device.as_ref() {
|
||||||
Some(name) => host
|
Some(name) => host
|
||||||
.output_devices()
|
.output_devices()
|
||||||
.context("No audio output devices available")?
|
.context("No audio output devices available")?
|
||||||
|
@ -91,9 +141,8 @@ impl Cpal {
|
||||||
let requested_sample_rate = cpal::SampleRate(config.sample_rate as u32);
|
let requested_sample_rate = cpal::SampleRate(config.sample_rate as u32);
|
||||||
let requested_buffer_size = cpal::BufferSize::Fixed(config.period_size);
|
let requested_buffer_size = cpal::BufferSize::Fixed(config.period_size);
|
||||||
|
|
||||||
let input_config = input_device
|
let input = input_device
|
||||||
.as_ref()
|
.map(|device| -> Result<(Device, StreamConfig, SampleFormat)> {
|
||||||
.map(|device| -> Result<StreamConfig> {
|
|
||||||
let input_configs: Vec<_> = device
|
let input_configs: Vec<_> = device
|
||||||
.supported_input_configs()
|
.supported_input_configs()
|
||||||
.context("Could not get supported audio input configurations")?
|
.context("Could not get supported audio input configurations")?
|
||||||
|
@ -122,11 +171,14 @@ impl Cpal {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// We already checked that these settings are valid
|
// We already checked that these settings are valid
|
||||||
Ok(StreamConfig {
|
let input_config = StreamConfig {
|
||||||
channels: input_config_range.channels(),
|
channels: input_config_range.channels(),
|
||||||
sample_rate: requested_sample_rate,
|
sample_rate: requested_sample_rate,
|
||||||
buffer_size: requested_buffer_size.clone(),
|
buffer_size: requested_buffer_size.clone(),
|
||||||
})
|
};
|
||||||
|
let input_sample_format = input_config_range.sample_format();
|
||||||
|
|
||||||
|
Ok((device, input_config, input_sample_format))
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
|
@ -160,13 +212,102 @@ impl Cpal {
|
||||||
sample_rate: requested_sample_rate,
|
sample_rate: requested_sample_rate,
|
||||||
buffer_size: requested_buffer_size,
|
buffer_size: requested_buffer_size,
|
||||||
};
|
};
|
||||||
|
let output_sample_format = output_config_range.sample_format();
|
||||||
|
|
||||||
Ok(Cpal {
|
Ok(Cpal {
|
||||||
|
config,
|
||||||
bus_config,
|
bus_config,
|
||||||
input_device,
|
|
||||||
input_config,
|
input,
|
||||||
|
|
||||||
output_device,
|
output_device,
|
||||||
output_config,
|
output_config,
|
||||||
|
output_sample_format,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_output_data_callback<T: Sample>(
|
||||||
|
&self,
|
||||||
|
unparker: Unparker,
|
||||||
|
mut cb: impl FnMut(&mut Buffer, Transport, &[NoteEvent], &mut Vec<NoteEvent>) -> bool
|
||||||
|
+ 'static
|
||||||
|
+ Send,
|
||||||
|
) -> impl FnMut(&mut [T], &OutputCallbackInfo) + Send + 'static {
|
||||||
|
// We'll receive interlaced input samples from CPAL. These need to converted to deinterlaced
|
||||||
|
// channels, processed, and then copied those back to an interlaced buffer for the output.
|
||||||
|
// This needs to be wrapped in a struct like this and boxed because the `channels` vectors
|
||||||
|
// need to live just as long as `buffer` when they get moved into the closure. FIXME: This
|
||||||
|
// is pretty nasty, come up with a cleaner alternative
|
||||||
|
let mut channels = vec![
|
||||||
|
vec![0.0f32; self.config.period_size as usize];
|
||||||
|
self.bus_config.num_output_channels as usize
|
||||||
|
];
|
||||||
|
let mut buffer = Buffer::default();
|
||||||
|
unsafe {
|
||||||
|
buffer.with_raw_vec(|output_slices| {
|
||||||
|
// Pre-allocate enough storage, the pointers are set in the data callback because
|
||||||
|
// `channels` will have been moved between now and the next callback
|
||||||
|
output_slices.resize_with(channels.len(), || &mut []);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: MIDI input and output
|
||||||
|
let mut midi_input_events = Vec::with_capacity(1024);
|
||||||
|
let mut midi_output_events = Vec::with_capacity(1024);
|
||||||
|
|
||||||
|
// Can't borrow from `self` in the callback
|
||||||
|
let config = self.config.clone();
|
||||||
|
let mut num_processed_samples = 0;
|
||||||
|
|
||||||
|
move |data, _info| {
|
||||||
|
// Things may have been moved in between callbacks, so these pointers need to be set up
|
||||||
|
// agian on each invocation
|
||||||
|
unsafe {
|
||||||
|
buffer.with_raw_vec(|output_slices| {
|
||||||
|
for (output_slice, channel) in output_slices.iter_mut().zip(channels.iter_mut())
|
||||||
|
{
|
||||||
|
// SAFETY: `channels` is no longer used directly after this, and it outlives
|
||||||
|
// the data closure
|
||||||
|
*output_slice = &mut *(channel.as_mut_slice() as *mut [f32]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut transport = Transport::new(config.sample_rate);
|
||||||
|
transport.pos_samples = Some(num_processed_samples);
|
||||||
|
transport.tempo = Some(config.tempo as f64);
|
||||||
|
transport.time_sig_numerator = Some(config.timesig_num as i32);
|
||||||
|
transport.time_sig_denominator = Some(config.timesig_denom as i32);
|
||||||
|
transport.playing = true;
|
||||||
|
|
||||||
|
for channel in buffer.as_slice() {
|
||||||
|
channel.fill(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
midi_output_events.clear();
|
||||||
|
if !cb(
|
||||||
|
&mut buffer,
|
||||||
|
transport,
|
||||||
|
&midi_input_events,
|
||||||
|
&mut midi_output_events,
|
||||||
|
) {
|
||||||
|
// TODO: Some way to immediately terminate the stream here would be nice
|
||||||
|
unparker.unpark();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The buffer's samples need to be written to `data` in an interlaced format
|
||||||
|
for (output_sample, buffer_sample) in data.iter_mut().zip(
|
||||||
|
buffer
|
||||||
|
.iter_samples()
|
||||||
|
.flat_map(|channels| channels.into_iter()),
|
||||||
|
) {
|
||||||
|
*output_sample = T::from(buffer_sample);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Handle MIDI output events
|
||||||
|
|
||||||
|
num_processed_samples += buffer.len() as i64;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue