Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loopback mode implementation #84

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open

Loopback mode implementation #84

wants to merge 2 commits into from

Conversation

nikp123
Copy link

@nikp123 nikp123 commented Nov 11, 2022

While not an officially advertised 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) ps: rust is hard

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

Thanks.

@padenot padenot requested a review from ChunMinChang November 14, 2022 12:52
@padenot
Copy link
Collaborator

padenot commented Nov 14, 2022

Tagging @ChunMinChang for initial review if he has some cycles. Thanks for the contribution, I'll run CI now.

@ChunMinChang
Copy link
Member

I am on PTO right now. I can take a look next Monday.

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.
@nikp123
Copy link
Author

nikp123 commented Nov 16, 2022

Forced-pushed the "cargo fmt"-ed version as I was not aware of the tool. I would like a CI re-run.

) {
let r = unsafe { ptr::NonNull::new(u as *mut CallbackData).unwrap().as_mut() };

let name = unsafe { CStr::from_ptr(info.unwrap().default_sink_name) };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does info always Some(_)? What happens if info is None?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be ALWAYS valid, because we control when the callback is called, not the Pulse API. By that I mean me make sure that the PulseAudio API calls our callback ONLY WHEN we want it, otherwise it'd be a massive UB hole to patch.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that familiar with Rust as you can notice, but I assume that the callback function would be invalid if we leave the scope (by that I mean the get_default_sink_name function)?

default_sink_name: String::new(),
};

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This silence the error. I prefer returning an error.

If the loopback functionality still works even if get_server_info fails, assigning a default name to default_sink_name and adding a log here is acceptable for me.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, will be handled the same as the sink string in a Result<> type.

actual_stream_name = Some(sink_name.as_c_str());
}

match PulseStream::stream_init(context, stream_params, actual_stream_name) {
Copy link
Member

@ChunMinChang ChunMinChang Dec 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work for the input-only stream?
This seems to work only for the duplex stream and the input device isn't set?

I am not quite sure what the expected behavior for the following cases:

  1. the stream is input-only stream
  2. the stream is duplex stream but the input device is set by user
  3. the stream is duplex stream but the input device is not set

(IIUC, loopback doesn't work for output-only stream)

@padenot : What are the expected behaviors for the above cases?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, it only fetched whatever default sink device it finds. IN THEORY we could implement it finding any sink device, but I don't think there are many use-cases for such behavior and therefore have avoided it.

Case 1:
For input-only streams, (using the error checking code that you suggested that I make) it will return a cubeb_backend::Error::device_unavailable() because there is no sink device you can grab the source from.
Therefore if the library user tried to request a loopback stream and found none, they should be informed about the fact that there are indeed NO loopback streams available.

Case 2:
That should be invalid (for now, given my first and lazy implementation) but in theory we could make it so it can fetch the .monitor stream of any sink device. Reason being that the code would have to properly parse the input device and activate a sink just so it could grab it's name and use it as a sink. This would require a TON of changes.

Case 3:
It should just grab the default sink device as use it as a source as I've alluded to before.

But yeah, I you want any of these cases to change in behavior, I'd be happy to change. But for now I just made them do whatever my time/intuition could allow for.

mainloop.unlock();

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the .monitor is the key to this magical setting. Can I have the document for this to check?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isn't any official page describing what .monitor does however it's use IS mentioned here and there within PulseAudio's official docs. Example being: https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/Developer/AccessControl/

@nikp123
Copy link
Author

nikp123 commented Dec 13, 2022

I've done some changed to add error checking, however handling of selecting what sinks you want to use is still not implemented (because of the complexities involved).

@nikp123
Copy link
Author

nikp123 commented Dec 13, 2022

Apologies if any of the mistakes in advance seem trivial.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants