@@ -22,8 +22,8 @@ use crate::{
2222 SupportedStreamConfigRange , SupportedStreamConfigsError , I24 , U24 ,
2323} ;
2424
25- // ALSA Latency Model and Period Configuration
26- // ===========================================
25+ // ALSA Buffer Size Behavior
26+ // =========================
2727//
2828// ## ALSA Latency Model
2929//
@@ -35,34 +35,20 @@ use crate::{
3535// period worth of data has been consumed by hardware, ALSA triggers a callback to refill
3636// that period in the software buffer.
3737//
38- // **Effective Latency**: With N periods total, (N-1) periods contain "latency" (data waiting
39- // to be played), while 1 period is always being transferred to/from hardware. Therefore:
40- // `effective_latency = (total_periods - 1) × period_size`
38+ // ## BufferSize::Fixed Behavior
4139//
42- // **User Expectation**: When user requests buffer size X, they expect ~X frames of latency,
43- // not ~X frames of total buffering. Our goal is: `period_size × (periods - 1) ≈ user_buffer`
40+ // When `BufferSize::Fixed(x)` is specified, cpal attempts to configure the period size
41+ // to approximately `x` frames to achieve the requested callback size. However, the
42+ // actual callback size may differ from the request:
4443//
45- // ## Period Configuration Strategy
44+ // - ALSA may round the period size to hardware-supported values
45+ // - Different devices have different period size constraints
46+ // - The callback size is not guaranteed to exactly match the request
47+ // - If the requested size cannot be accommodated, ALSA will choose the nearest
48+ // supported configuration
4649//
47- // **Goal**: Achieve user-requested latency with precision and device compatibility.
48- //
49- // **Step 1 - Query Device Limits**: Check the device's maximum period size to determine
50- // which approaches are viable.
51- //
52- // **Step 2 - Prefer Double Buffering**: When user_buffer ≤ max_period_size, configure
53- // 2 periods of user_buffer size each. This is the simplest configuration with direct
54- // period size control.
55- //
56- // **Step 3 - Multi-Period When Required**: When user_buffer > max_period_size, calculate
57- // the minimum periods needed and distribute the latency across them. This maintains
58- // precision while respecting hardware constraints.
59- //
60- // **Step 4 - Fallback for Compatibility**: If precise approaches fail device validation,
61- // use buffer-size-only configuration. Accept latency deviation to ensure functional audio.
62- //
63- // **Validation**: Accept exact matches for even period sizes, and ±1 for odd period sizes
64- // (due to hardware alignment constraints). Reject results that deviate significantly
65- // from the target latency.
50+ // This mirrors the behavior documented in the cpal API where `BufferSize::Fixed(x)`
51+ // requests but does not guarantee a specific callback size.
6652
6753pub type SupportedInputConfigs = VecIntoIter < SupportedStreamConfigRange > ;
6854pub type SupportedOutputConfigs = VecIntoIter < SupportedStreamConfigRange > ;
@@ -945,7 +931,7 @@ fn process_output(
945931 error_callback ( err. into ( ) ) ;
946932 continue ;
947933 }
948- Ok ( result) if result as usize != stream. period_frames => {
934+ Ok ( result) if result != stream. period_frames => {
949935 let description = format ! (
950936 "unexpected number of frames written: expected {}, \
951937 result {result} (this should never happen)",
@@ -1198,109 +1184,6 @@ fn fill_with_equilibrium(buffer: &mut [u8], sample_format: SampleFormat) {
11981184 }
11991185}
12001186
1201- // Try period configuration with specified period size and count
1202- fn try_period_configuration (
1203- pcm_handle : & alsa:: pcm:: PCM ,
1204- hw_params : & alsa:: pcm:: HwParams ,
1205- target_period_size : u32 ,
1206- target_periods : u32 ,
1207- ) -> Option < usize > {
1208- hw_params
1209- . set_period_size_near ( target_period_size as _ , alsa:: ValueOr :: Nearest )
1210- . ok ( ) ?;
1211- hw_params
1212- . set_periods ( target_periods, alsa:: ValueOr :: Nearest )
1213- . ok ( ) ?;
1214- pcm_handle. hw_params ( hw_params) . ok ( ) ?;
1215-
1216- let device_period_size = hw_params. get_period_size ( ) . ok ( ) ? as u32 ;
1217- let device_periods = hw_params. get_periods ( ) . ok ( ) ? as u32 ;
1218-
1219- // Period count must be exactly what we requested
1220- if device_periods != target_periods {
1221- return None ;
1222- }
1223-
1224- // Period size validation: exact for even, ±1 for odd
1225- let period_size_ok = if target_period_size % 2 == 0 {
1226- device_period_size == target_period_size
1227- } else {
1228- let acceptable_range = ( target_period_size. saturating_sub ( 1 ) ) ..=( target_period_size + 1 ) ;
1229- acceptable_range. contains ( & device_period_size)
1230- } ;
1231-
1232- if period_size_ok {
1233- Some ( device_period_size as usize )
1234- } else {
1235- None // Device constraint issue
1236- }
1237- }
1238-
1239- // Configure periods based on device capabilities
1240- fn configure_periods (
1241- pcm_handle : & alsa:: pcm:: PCM ,
1242- hw_params : & alsa:: pcm:: HwParams ,
1243- user_buffer_frames : u32 ,
1244- config : & StreamConfig ,
1245- ) -> Result < ( ) , BackendSpecificError > {
1246- // Query device maximum period size to determine approach
1247- let max_period_size = hw_params
1248- . get_period_size_max ( )
1249- . map_err ( |_| BackendSpecificError {
1250- description : "Could not query device period size limits" . to_string ( ) ,
1251- } ) ? as u32 ;
1252-
1253- // Approach 1: Double buffering if user buffer fits within device limits
1254- if user_buffer_frames <= max_period_size {
1255- if let Some ( _) = try_period_configuration ( & pcm_handle, & hw_params, user_buffer_frames, 2 ) {
1256- return Ok ( ( ) ) ;
1257- }
1258- }
1259-
1260- // Approach 2: Multi-period with calculated period count and size
1261- if user_buffer_frames > max_period_size {
1262- // Calculate minimum periods needed: ceil(user_buffer / max_period_size) + 1
1263- let min_periods = ( user_buffer_frames + max_period_size - 1 ) / max_period_size + 1 ;
1264- let target_period_size = user_buffer_frames / std:: cmp:: max ( min_periods - 1 , 1 ) ;
1265-
1266- if let Some ( _) =
1267- try_period_configuration ( & pcm_handle, & hw_params, target_period_size, min_periods)
1268- {
1269- return Ok ( ( ) ) ;
1270- }
1271- }
1272-
1273- // Approach 3: Fallback - let ALSA choose everything based on buffer size
1274- fallback_buffer_size ( & pcm_handle, & hw_params, user_buffer_frames, config)
1275- }
1276-
1277- // Fallback: Use ALSA's buffer size approach when period-based approaches fail
1278- fn fallback_buffer_size (
1279- pcm_handle : & alsa:: pcm:: PCM ,
1280- base_hw_params : & alsa:: pcm:: HwParams ,
1281- user_buffer_frames : u32 ,
1282- config : & StreamConfig ,
1283- ) -> Result < ( ) , BackendSpecificError > {
1284- // Create fresh hw_params to avoid inheriting period constraints from previous attempts
1285- let hw_params = alsa:: pcm:: HwParams :: any ( pcm_handle) ?;
1286- hw_params. set_access ( base_hw_params. get_access ( ) ?) ?;
1287- hw_params. set_format ( base_hw_params. get_format ( ) ?) ?;
1288- hw_params. set_rate ( base_hw_params. get_rate ( ) ?, alsa:: ValueOr :: Nearest ) ?;
1289- hw_params. set_channels ( base_hw_params. get_channels ( ) ?) ?;
1290-
1291- // Only set buffer size - let ALSA choose optimal period size and count
1292- hw_params
1293- . set_buffer_size_near ( user_buffer_frames as _ )
1294- . map_err ( |_| BackendSpecificError {
1295- description : format ! (
1296- "Buffer size '{:?}' is not supported by this backend" ,
1297- config. buffer_size
1298- ) ,
1299- } ) ?;
1300-
1301- pcm_handle. hw_params ( & hw_params) . map_err ( Into :: into)
1302- }
1303-
13041187fn set_hw_params_from_format (
13051188 pcm_handle : & alsa:: pcm:: PCM ,
13061189 config : & StreamConfig ,
@@ -1364,14 +1247,22 @@ fn set_hw_params_from_format(
13641247 hw_params. set_rate ( config. sample_rate . 0 , alsa:: ValueOr :: Nearest ) ?;
13651248 hw_params. set_channels ( config. channels as u32 ) ?;
13661249
1367- // Smart period configuration: adapt to device capabilities for consistent latency
1368- if let BufferSize :: Fixed ( user_buffer_frames ) = config . buffer_size {
1369- configure_periods ( & pcm_handle , & hw_params , user_buffer_frames , config ) ? ;
1370- } else {
1371- // Default buffer size - let device choose everything
1372- pcm_handle . hw_params ( & hw_params ) ?;
1250+ // Configure period size based on buffer size request
1251+ // When BufferSize::Fixed(x) is specified, we request a period size of x frames
1252+ // to achieve approximately x-sized callbacks. ALSA may adjust this to the nearest
1253+ // supported value based on hardware constraints.
1254+ if let BufferSize :: Fixed ( buffer_frames ) = config . buffer_size {
1255+ hw_params. set_period_size_near ( buffer_frames as _ , alsa :: ValueOr :: Nearest ) ?;
13731256 }
13741257
1258+ // We shouldn't fail if the driver isn't happy here.
1259+ // `default` pcm sometimes fails here, but there's no reason to as we
1260+ // provide a direction and 2 is strictly the minimum number of periods.
1261+ let _ = hw_params. set_periods ( 2 , alsa:: ValueOr :: Greater ) ;
1262+
1263+ // Apply hardware parameters
1264+ pcm_handle. hw_params ( & hw_params) ?;
1265+
13751266 Ok ( hw_params. can_pause ( ) )
13761267}
13771268
0 commit comments