cpal icon indicating copy to clipboard operation
cpal copied to clipboard

SupportedStreamConfig(Range) API inconsistencies - can we have a `SupportedStreamConfig.try_with_fixed_buffer_size`?

Open dani-corie opened this issue 10 months ago • 2 comments

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 sizes
  • SupportedStreamConfig - fixed sampling rate, range of buffer sizes
  • StreamConfig - 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_rate to get a SupportedStreamConfig. 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, the config method on this type just returns a StreamConfig with a Default buffersize 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?

dani-corie avatar Feb 23 '25 22:02 dani-corie

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();

roderickvd avatar Dec 10 '25 20:12 roderickvd

IMO it would also be nice to have methods that clamp to the valid range instead of erroring.

edwloef avatar Dec 10 '25 23:12 edwloef