Skip to content

Commit

Permalink
Loopback mode implementation
Browse files Browse the repository at this point in the history
While not an officially advertized feature in the PulseAudio API,
all PulseAudio "sink"-s have a "monitor mode" where in any of the
audio output devices there's a "loopback" input device which
replicates all audio for said output. This allows us to effectively
achieve recording any audio output as input within cubeb.

This commit is a continuation of said Draft shown in mozilla/cubeb#691
but this time written in Rust. (yes it took me half a year but we're
here)

As I'm quite inexperienced when it comes to Rust (*especially so when it's
demanding code like FFI interfaces*) comments on the code or any
improvements that may be done are greatly appreciated.

Thanks.
  • Loading branch information
nikp123 committed Nov 11, 2022
1 parent cf48897 commit a2b66d1
Showing 1 changed file with 55 additions and 7 deletions.
62 changes: 55 additions & 7 deletions src/backend/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use backend::cork_state::CorkState;
use backend::*;
use cubeb_backend::{
ffi, log_enabled, ChannelLayout, DeviceId, DeviceRef, Error, Result, SampleFormat, StreamOps,
StreamParamsRef, StreamPrefs,
StreamParamsRef, StreamPrefs, Context,
};
use pulse::{self, CVolumeExt, ChannelMapExt, SampleSpecExt, StreamLatency, USecExt};
use pulse_ffi::*;
Expand Down Expand Up @@ -414,6 +414,48 @@ impl<'ctx> PulseStream<'ctx> {
}
}

fn get_default_sink_name(mainloop: &pulse::ThreadedMainloop,
context: &pulse::Context) -> String {

struct CallbackData<'a> {
mainloop: &'a pulse::ThreadedMainloop,
default_sink_name: String,
}

fn server_info_cb(_context: &pulse::Context,
info: Option<&pulse::ServerInfo>,
u: *mut c_void) {
let r = unsafe {
ptr::NonNull::new(
u as *mut CallbackData
).unwrap().as_mut()
};

let name = unsafe { CStr::from_ptr(info.unwrap().default_sink_name) };
let name_slice = name.to_bytes_with_nul();
r.default_sink_name = std::str::from_utf8(name_slice).unwrap().to_string();

r.mainloop.signal();
}

mainloop.lock();

let mut data = CallbackData {
mainloop,
default_sink_name: String::new(),
};

_ = context.get_server_info(server_info_cb, ptr::addr_of!(data) as *mut c_void);

mainloop.wait();
mainloop.unlock();

_ = data.default_sink_name.pop();
data.default_sink_name.push_str(".monitor");

data.default_sink_name
}

let mut stm = Box::new(PulseStream {
context,
output_stream: None,
Expand All @@ -432,6 +474,11 @@ impl<'ctx> PulseStream<'ctx> {
});

if let Some(ref context) = stm.context.context {
let string = get_default_sink_name(
&stm.context.mainloop,
stm.context.context.as_ref().unwrap());
let sink_name = CString::new(string).unwrap();

stm.context.mainloop.lock();

// Setup output stream
Expand Down Expand Up @@ -480,7 +527,13 @@ impl<'ctx> PulseStream<'ctx> {

// Set up input stream
if let Some(stream_params) = input_stream_params {
match PulseStream::stream_init(context, stream_params, stream_name) {
// Handle loopback audio name
let mut actual_stream_name = stream_name;
if stream_params.prefs() == StreamPrefs::LOOPBACK {
actual_stream_name = Some(sink_name.as_c_str());
}

match PulseStream::stream_init(context, stream_params, actual_stream_name) {
Ok(s) => {
stm.input_sample_spec = *s.get_sample_spec();

Expand Down Expand Up @@ -866,11 +919,6 @@ impl<'ctx> PulseStream<'ctx> {
stream_params: &StreamParamsRef,
stream_name: Option<&CStr>,
) -> Result<pulse::Stream> {
if stream_params.prefs() == StreamPrefs::LOOPBACK {
cubeb_log!("Error: StreamPref::LOOPBACK unimplemented");
return Err(not_supported());
}

fn to_pulse_format(format: SampleFormat) -> pulse::SampleFormat {
match format {
SampleFormat::S16LE => pulse::SampleFormat::Signed16LE,
Expand Down

0 comments on commit a2b66d1

Please sign in to comment.