SupportedStreamConfig(Range) API inconsistencies - can we have a `SupportedStreamConfig.try_with_fixed_buffer_size`?
The SupportedStreamConfigRange, SuportedStreamConfig and StreamConfig types make configuring streams in cpal far more complicated and unpleasant than it would need to be.
SupportedStreamConfigRange- range of sampling rates and buffer sizesSupportedStreamConfig- fixed sampling rate, range of buffer sizesStreamConfig- fixed sampling rate, fixed or 'default' buffer size
I'm not sure why there's even a need for SupportedStreamConfig, but the main point why I opened this issue is that the naively expected chain of Iterator<SupportedStreamConfigRange> -> SupportedStreamConfigRange -> SupportedStreamConfig -> StreamConfig is incomplete.
- I get the iterator, and filter it to get one or more candidates.
- Then on those, I call
try_with_sample_rateto get aSupportedStreamConfig. This is where the first problem is: bad documentation:/// Retrieve a [SupportedStreamConfig] with the given sample rate and buffer size.The signature of the method is(self, sample_rate: SampleRate), no buffer size I could specify. - Getting a
SupportedStreamConfig, I'd then like to narrow down the buffer size, as at least on Linux, most devices return with a buffer size range of between 1 and MAXINT32 or so. However, there is no method for doing this. As mentioned in #401, theconfigmethod on this type just returns aStreamConfigwith aDefaultbuffersize setting.
So in the end, I'm still hand-assembling a StreamConfig.
I guess the first step would be to have a try_with_fixed_buffer_size method on SupportedStreamConfig. Then, if there's no good reason for it, it might be meaningful to roll SupportedStreamConfigRange and SupportedStreamConfig into a single type?
You are right. And when we're on it, I'm thinking if we'd take that first step with try_with_fixed_buffer_size or immediately the next and introduce a builder? Claude came up with this, nice and additive:
/// Builder for creating a validated StreamConfig
pub struct StreamConfigBuilder {
channels: ChannelCount,
sample_rate: SampleRate,
sample_format: SampleFormat,
buffer_size: BufferSize,
// Constraints from the supported config
sample_rate_range: (SampleRate, SampleRate),
buffer_size_range: SupportedBufferSize,
}
impl SupportedStreamConfigRange {
/// Start building a StreamConfig from this range
pub fn build_config(self) -> StreamConfigBuilder {
StreamConfigBuilder {
channels: self.channels,
sample_rate: self.max_sample_rate, // Default to max?
sample_format: self.sample_format,
buffer_size: BufferSize::Default,
sample_rate_range: (self.min_sample_rate, self.max_sample
buffer_size_range: self.buffer_size,
}
}
}
impl SupportedStreamConfig {
/// Start building a StreamConfig from this config
pub fn build_config(self) -> StreamConfigBuilder {
StreamConfigBuilder {
channels: self.channels,
sample_rate: self.sample_rate,
sample_format: self.sample_format,
buffer_size: BufferSize::Default,
sample_rate_range: (self.sample_rate, self.sample_rate),
buffer_size_range: self.buffer_size,
}
}
}
impl StreamConfigBuilder {
/// Set the sample rate (validates against supported range)
pub fn with_sample_rate(mut self, rate: SampleRate) -> Result<Self, ConfigError> {
let (min, max) = self.sample_rate_range;
if rate >= min && rate <= max {
self.sample_rate = rate;
Ok(self)
} else {
Err(ConfigError::SampleRateOutOfRange {
requested: rate,
min,
max
})
}
}
/// Set a fixed buffer size (validates against supported range)
pub fn with_buffer_size(mut self, size: FrameCount) -> Result<Self, ConfigError> {
match &self.buffer_size_range {
SupportedBufferSize::Range { min, max } => {
if size >= *min && size <= *max {
self.buffer_size = BufferSize::Fixed(size);
Ok(self)
} else {
Err(ConfigError::BufferSizeOutOfRange {
requested: size,
min: *min,
max: *max
})
}
}
SupportedBufferSize::Unknown => {
self.buffer_size = BufferSize::Fixed(size);
Ok(self)
}
}
}
/// Use the default buffer size
pub fn with_default_buffer_size(mut self) -> Self {
self.buffer_size = BufferSize::Default;
self
}
/// Build the final StreamConfig
pub fn build(self) -> StreamConfig {
StreamConfig {
channels: self.channels,
sample_rate: self.sample_rate,
buffer_size: self.buffer_size,
}
}
}
How to use:
// Example 1: From SupportedStreamConfigRange
let config = device
.supported_output_configs()?
.next()
.unwrap()
.build_config()
.with_sample_rate(48000)?
.with_buffer_size(512)?
.build();
// Example 2: From default_output_config
let config = device
.default_output_config()?
.build_config()
.with_buffer_size(256)?
.build();
// Example 3: Just use defaults (backward compatible)
let config = device
.default_output_config()?
.build_config()
.build();
// Example 4: Error handling
let config = device
.default_output_config()?
.build_config()
.with_buffer_size(512)
.or_else(|_| {
// Fallback to smaller buffer if 512 not supported
device
.default_output_config()?
.build_config()
.with_buffer_size(256)
})?
.build();
// Example 5: Keep existing API working
let config: StreamConfig = device.default_output_config()?.into();
IMO it would also be nice to have methods that clamp to the valid range instead of erroring.