-
Notifications
You must be signed in to change notification settings - Fork 333
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
Prototyping an API for getting the supported min and max buffersizes #401
Prototyping an API for getting the supported min and max buffersizes #401
Conversation
Given you are adding support for AudioClient3 in winapi, are you going to implement support for the "Shared" audio mode added in Windows 10? I was thinking of working on it so maybe we could work together. |
Hi @ishitatsuyuki. It wasn't something that I had planned immediately but I think it could be worth considering. However, it probably makes sense for it to be separate from this work though. |
So i've been making some good progress on this, have support for asio, coreaudio and alsa done. Moving onto wasapi now. So in order to query the buffersize ranges supported by the device we are going to need to query the edit My bad IAudioClient 3 seems to only be supported on Win10. One option would be to give the user an option to enable IAudioClient3 with a feature flag, at least until Win8 is officially not supported....? |
Happy to drop Win 7. |
I'm not actively working on audio things at the moment, but on druid we have quite a number of Windows 7 users (we always hear quickly when we break things) and are choosing to continue supporting it for at least a while. |
Seeing as it looks like the only way to reliably get the min and max buffer size via WASAPI requires Windows 10, I think it makes sense to have WASAPI just return both the min and max as whatever the default buffer size is by default so that users of WASAPI on older versions of Windows can at least use CPAL at all, even if they can't configure the buffer size. Perhaps having some way of allowing users to opt-in to using the Windows-10-only let (host, device) = if cfg!(target_os = "windows") {
let wasapi_host = cpal::WasapiHost::new().unwrap();
let wasapi_device = wasapi_host.default_output_device_iaudioclient3().unwrap();
(wasapi_host.into(), wasapi_device.into())
} else {
let host = cpal::default_host();
let device = host.default_output_device().unwrap();
(host, device)
};
// Enumerate supported configs, create streams, etc... Thoughts? Also, perhaps it might even be worth leaving this opt-in-windows-10-API stuff for a separate future PR and just focusing on landing the default case for now? |
Could |
@Ralith good point! Surely there's a reliable way to check availability of |
IIRC it's idiomatic on Windows to check for APIs simply by trying to dynamically load the function pointer, but I'm not an expert. |
3d63101
to
7c1adce
Compare
Thanks for the feedback everyone, some really great ideas. Because we are still waiting on this PR to be merged in order to get IAudioClient3 support landed in winapi I think we should leave user-configurable buffersizes in Windows for a future PR. As such, the current implementation just returns the default buffersize as the supported min and max buffersize range for the time being. At least this allows us to merge this PR and get support for the other hosts. If someone wanted to implement this in the future, something like the below code would need to be added to the wasapi device.rs file.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me! I've added a note on one small patch that should probably be made for the emscripten host but once that's addressed I'm happy to land this.
src/host/emscripten/mod.rs
Outdated
set_timeout(|| audio_callback_fn::<D, E>(user_data_ptr), 330); | ||
set_timeout( | ||
|| audio_callback_fn::<D, E>(user_data_ptr, config, sample_format, buffer_size_frames), | ||
330, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that the buffer size is configurable, this milliseconds value should correspond with the duration of a buffer, so something like buffer_size_frames * 1000 / sample_rate
. I think the magic value of 330
from before was a hack so that the timeout would be ready just before the previous buffer completes (which was previously hard-coded to a third of a sec).
I've spotted a few problems in the alsa host. I am also wondering if |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are the problems I mentioned.
@@ -890,6 +900,11 @@ fn set_hw_params_from_format<'a>( | |||
hw_params.set_rate(config.sample_rate.0, alsa::ValueOr::Nearest)?; | |||
hw_params.set_channels(config.channels as u32)?; | |||
|
|||
match config.buffer_size { | |||
BufferSize::Fixed(v) => hw_params.set_buffer_size(v as i64)?, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be set_buffer_size_near
.
BufferSize::Fixed(v) => hw_params.set_buffer_size(v as i64)?, | |
BufferSize::Fixed(v) => hw_params.set_buffer_size_near(v as i64)?, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think using set_buffer_size
is the correct approach here. If the user has specified a Fixed
value and that exact value is unsupported, then we should return an error to the user to notify them.
Perhaps we could open an issue to discuss adding a Nearest(FrameCount)
variant to the BufferSize
enum in a follow-up PR that aligns with the semantics of set_buffer_size_near
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason I suggested set_buffer_size_near
is because finding the Fixed
values that actually work would pretty much rely on trial and error (Depending on the device). Especially since the possible range of buffer sizes is connected to the other hardware parameters. There are some Fixed
values that should work most of the time, but relying on it will come to bite you. I think the user would benefit more from being able to query the precise buffer size afterwards (If that actually matters. It might). Nearest(Framecount)
would of course also work for that though leaving the Fixed
for the case in which the user really has to have that precise buffer size.
src/host/alsa/mod.rs
Outdated
BufferSize::Fixed(v) => hw_params.set_buffer_size(v as i64)?, | ||
BufferSize::Default => (), | ||
} | ||
|
||
// If this isn't set manually a overlarge buffer may be used causing audio delay | ||
let mut hw_params_copy = hw_params.clone(); | ||
if let Err(_) = hw_params.set_buffer_time_near(100_000, alsa::ValueOr::Nearest) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will likely clobber the previously set buffer size. As it stands, it should probably be used in the default branch to keep preventing unexpectedly large buffer sizes (And thus latency).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will likely clobber the previously set buffer size.
Good spot!
As it stands, it should probably be used in the default branch to keep preventing unexpectedly large buffer sizes (And thus latency).
I'd prefer we omit this entirely in the case that the Default
variant is specified, and that the user uses the supported stream configs API to select a suitable buffer size if they're concerned about latency. I do understand the desire to have a lower-latency default that is friendlier to users like game/pro-audio devs (especially as ALSA's default buffer size can be large) but I'm not sure that CPAL should take on the responsibility of deciding what this default should be, particularly when the user is in a better position to choose a suitable option for their project. Thoughts?
As a side note, in the case that we do opt for keeping a lower-latency default like this, I think it's important that it never fails and instead falls back to the system default in the case that whatever value we come up with isn't supported for some reason.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well I can see where you're coming from, but if I was writing some cross platform application I would care about consistent behaviour, especially if I did not care about the precise parameters. And this definition of default is far from consistent. Whether the differences would be noticable though: I don't know. If the default parameters are extremely poor they might be.
This is even more important for the period size, since that also affects the default cpu load.
NOTE: I am not arguing for 100ms (That's just what was there), but some consistent value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've opened #447 to discuss the idea of adding a BufferSize::Near
variant.
A given buffer size must also get a corresponding period size for the alsa host. If this is not directly configurable in a cross platform way I suggest using about a quarter of the buffer size. A safe value. And the call to See #431 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am also wondering if BufferSize::Default shouldn't have some meaningful definition. It now appears to depend on the backend (At least for the alsa host and at the moment also on the selected alsa device), which means default behaviour will change across platforms and backends.
@sniperrifle2004 Yes I think this is the intention and that this seems reasonable. Here, Default
implies whatever the default behaviour is on the platform when no specific buffer size is set. Useful for cases where the user does not care or need to know exactly what the buffer size will be. In the case that the user does care (perhaps because they require low-latency), this PR should allow them to determine the valid buffer size range via the supported stream config methods and select a valid buffer size as desired. Perhaps updating the Default
variant docs to reflect this meaning might be enough? I've added a comment at the relevant line.
A given buffer size must also get a corresponding period size for the alsa host. If this is not directly configurable in a cross platform way I suggest using about a quarter of the buffer size. A safe value. And the call to set_period_size_near will need to be done before the call to set_buffer_size_near.
See #431
Thanks for pointing this out! I was unaware of the need to set the period size separately - I agree something along the lines of your suggested solution makes sense. Are you happy for this change to be incorporated in this PR rather than landing #431? Btw @JoshuaBatty this stack overflow question has some interesting discussion/links on what "period" means in ALSA if you're interested.
|
||
#[derive(Clone, Debug, Eq, PartialEq)] | ||
pub enum BufferSize { | ||
Default, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add some docs to this variant explaining that Default
refers to whatever the default behaviour is for the host in the case that no specific buffer size is set. It might be useful to also mention that this could result in potentially large buffer sizes, and that if the user cares about latency they should check specify a fixed buffer size in accordance with the range produced by the supported stream config API.
src/host/alsa/mod.rs
Outdated
BufferSize::Fixed(v) => hw_params.set_buffer_size(v as i64)?, | ||
BufferSize::Default => (), | ||
} | ||
|
||
// If this isn't set manually a overlarge buffer may be used causing audio delay | ||
let mut hw_params_copy = hw_params.clone(); | ||
if let Err(_) = hw_params.set_buffer_time_near(100_000, alsa::ValueOr::Nearest) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will likely clobber the previously set buffer size.
Good spot!
As it stands, it should probably be used in the default branch to keep preventing unexpectedly large buffer sizes (And thus latency).
I'd prefer we omit this entirely in the case that the Default
variant is specified, and that the user uses the supported stream configs API to select a suitable buffer size if they're concerned about latency. I do understand the desire to have a lower-latency default that is friendlier to users like game/pro-audio devs (especially as ALSA's default buffer size can be large) but I'm not sure that CPAL should take on the responsibility of deciding what this default should be, particularly when the user is in a better position to choose a suitable option for their project. Thoughts?
As a side note, in the case that we do opt for keeping a lower-latency default like this, I think it's important that it never fails and instead falls back to the system default in the case that whatever value we come up with isn't supported for some reason.
@@ -890,6 +900,11 @@ fn set_hw_params_from_format<'a>( | |||
hw_params.set_rate(config.sample_rate.0, alsa::ValueOr::Nearest)?; | |||
hw_params.set_channels(config.channels as u32)?; | |||
|
|||
match config.buffer_size { | |||
BufferSize::Fixed(v) => hw_params.set_buffer_size(v as i64)?, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think using set_buffer_size
is the correct approach here. If the user has specified a Fixed
value and that exact value is unsupported, then we should return an error to the user to notify them.
Perhaps we could open an issue to discuss adding a Nearest(FrameCount)
variant to the BufferSize
enum in a follow-up PR that aligns with the semantics of set_buffer_size_near
?
@mitchmindtree I wouldn't mind #431 being incorporated in this. In fact it should be anyway. In fairness I have some use cases that would benefit from #431 right now (The low default period size in the pulse plugin is just bad for less powerful devices), however they will benefit even more if the buffer size can actually be set (100ms is a bit lower than is actually needed). |
@mitchmindtree What's worse though is that the minimum period time will also be chosen, so you will wind up with a worst-case high load/high latency scenario if the values are not restricted. If you are lucky the maximum buffer size will adjust to the period time. For what it's worth: Actual hardware - often by necessity - and software level devices often restrict the maximum buffer size to something reasonable even if it doesn't. The (potentially very) low period time will still be a problem though. So let's at least settle on some (hidden?) default restrictions for the alsa host to avoid that. That way it still depends on the host, but no longer on whatever extremities the underlying device has. As an alternative suggestion to the lowish 100ms PS The pulse plugin does have a pretty awful maximum buffer size: |
1a42d61
to
f285389
Compare
Thanks for sharing more of your thoughts @sniperrifle2004!
I think you're totally right, in this case the kinds of delays that the default buffer size might cause for users in the common case aren't really practical for any application I can think of. For now I'm happy seeing this land with the original 100ms delay as a default (along with your patch from #431), particularly as that's what we already have. We can address this properly in a follow-up PR.
Yeah something along these lines could make sense. Perhaps we can open a follow up issue along the lines of "Reviewing and deciding upon a default buffer size strategy for ALSA". |
Thanks for addressing those issues @JoshuaBatty, this all looks good to me! I think we can address the default buffer size issue discussed above in a follow-up PR. I've opened up #446 to discuss this further. Also I think we will likely want something like |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently there is no way to specify the buffer size.
UPDATE: Sorry for the confusion, rodio uses "SupportedStreamConfig::config(&self) -> StreamConfig" which makes it to ignore the preferred buffer sizes. So the problem is there.
@@ -297,6 +333,7 @@ impl SupportedStreamConfig { | |||
StreamConfig { | |||
channels: self.channels, | |||
sample_rate: self.sample_rate, | |||
buffer_size: BufferSize::Default, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Buffer size from the format is always ignored...
This PR proposes an API for querying the supported min and max buffersizes of the device. I have managed to write successful test cases for ALSA, WASAPI, ASIO, CoreAudio, and Emscripten. I wanted to put this API out there for feedback before I start implementing all of the specific backends.
Note, the current version of winapi currently has no ability to query min/max buffersizes when using WASAPI. I have an open PR on winapi that can be found here that first needs to land until we can merge this work into CPAL.
This PR also removes the
fn from(format: SupportedStreamConfig) -> SupportedStreamConfigRange
function. Currently, it's only being used in wasapi/device line 529. I think we should aim to provide a better way of handling the supported formats returned by WASAPI rather than continue to modify this function to handle buffersizes.Looking forward to hearing your feedback!