@@ -49,6 +49,29 @@ use crate::{
4949//
5050// This mirrors the behavior documented in the cpal API where `BufferSize::Fixed(x)`
5151// requests but does not guarantee a specific callback size.
52+ //
53+ // ## BufferSize::Default Behavior
54+ //
55+ // When `BufferSize::Default` is specified, cpal does NOT set explicit period size or
56+ // period count constraints, allowing the device/driver to choose sensible defaults.
57+ //
58+ // **Why not set defaults?** Different audio systems have different behaviors:
59+ //
60+ // - **Native ALSA hardware**: Typically chooses reasonable defaults (e.g., 512-2048
61+ // frame periods with 2-4 periods)
62+ //
63+ // - **PipeWire-ALSA plugin**: Allocates a large ring buffer (~1M frames at 48kHz) but
64+ // uses small periods (512-1024 frames). Critically, if you request `set_periods(2)`
65+ // without specifying period size, PipeWire calculates period = buffer/2, resulting
66+ // in pathologically large periods (~524K frames = 10 seconds). See issues #1029 and
67+ // #1036.
68+ //
69+ // By not constraining period configuration, PipeWire-ALSA can use its optimized defaults
70+ // (small periods with many-period buffer), while native ALSA hardware uses its own defaults.
71+ //
72+ // **Startup latency**: Regardless of buffer size, cpal uses double-buffering for startup
73+ // (start_threshold = 2 periods), ensuring low latency even with large multi-period ring
74+ // buffers.
5275
5376pub type SupportedInputConfigs = VecIntoIter < SupportedStreamConfigRange > ;
5477pub type SupportedOutputConfigs = VecIntoIter < SupportedStreamConfigRange > ;
@@ -743,13 +766,6 @@ fn output_stream_worker(
743766
744767 let mut ctxt = StreamWorkerContext :: new ( & timeout, stream, & rx) ;
745768
746- // As first period, always write one buffer with equilibrium values.
747- // This ensures we start with a full period of silence, giving the user their
748- // requested latency while avoiding underruns on the first callback.
749- if let Err ( err) = stream. channel . io_bytes ( ) . writei ( & ctxt. transfer_buffer ) {
750- error_callback ( err. into ( ) ) ;
751- }
752-
753769 loop {
754770 let flow =
755771 poll_descriptors_and_prepare_buffer ( & rx, stream, & mut ctxt) . unwrap_or_else ( |err| {
@@ -877,7 +893,10 @@ fn poll_descriptors_and_prepare_buffer(
877893 } ;
878894 let available_samples = avail_frames * stream. conf . channels as usize ;
879895
880- // Only go on if there is at least one period's worth of space available.
896+ // ALSA can have spurious wakeups where poll returns but avail < avail_min.
897+ // This is documented to occur with dmix (timer-driven) and other plugins.
898+ // Verify we have room for at least one full period before processing.
899+ // See: https://bugzilla.kernel.org/show_bug.cgi?id=202499
881900 if available_samples < stream. period_samples {
882901 return Ok ( PollDescriptorsFlow :: Continue ) ;
883902 }
@@ -1117,33 +1136,6 @@ impl Drop for Stream {
11171136 self . inner . dropping . set ( true ) ;
11181137 self . trigger . wakeup ( ) ;
11191138 self . thread . take ( ) . unwrap ( ) . join ( ) . unwrap ( ) ;
1120-
1121- // State-based drop behavior: drain if playing, drop if paused. This allows audio to
1122- // complete naturally when stopping during playback, but provides immediate termination
1123- // when already paused.
1124- match self . inner . channel . state ( ) {
1125- alsa:: pcm:: State :: Running => {
1126- // Audio is actively playing - attempt graceful drain.
1127- if let Ok ( ( ) ) = self . inner . channel . drain ( ) {
1128- // TODO: Use SND_PCM_WAIT_DRAIN (-10002) when alsa-rs supports it properly,
1129- // although it requires ALSA 1.2.8+ which may not be available everywhere.
1130- // For now, calculate timeout based on buffer latency.
1131- let buffer_duration_ms = ( ( self . inner . period_frames as f64 * 1000.0 )
1132- / self . inner . conf . sample_rate . 0 as f64 )
1133- as u32 ;
1134-
1135- // This is safe: snd_pcm_wait() checks device state first and returns
1136- // immediately with error codes like -ENODEV for disconnected devices.
1137- let _ = self . inner . channel . wait ( Some ( buffer_duration_ms) ) ;
1138- }
1139- // If drain fails or device has errors, stream terminates naturally
1140- }
1141- _ => {
1142- // Not actively playing (paused, stopped, etc.) - immediate drop and discard any
1143- // buffered audio data for immediate termination.
1144- let _ = self . inner . channel . drop ( ) ;
1145- }
1146- }
11471139 }
11481140}
11491141
@@ -1158,9 +1150,9 @@ impl StreamTrait for Stream {
11581150 }
11591151}
11601152
1161- // Overly safe clamp because alsa Frames are i64
1153+ // Overly safe clamp because alsa Frames are i64 (64-bit) or i32 (32-bit)
11621154fn clamp_frame_count ( buffer_size : alsa:: pcm:: Frames ) -> FrameCount {
1163- buffer_size. clamp ( 1 , FrameCount :: MAX as _ ) as _
1155+ buffer_size. clamp ( 1 , FrameCount :: MAX as alsa :: pcm :: Frames ) as FrameCount
11641156}
11651157
11661158fn hw_params_buffer_size_min_max ( hw_params : & alsa:: pcm:: HwParams ) -> ( FrameCount , FrameCount ) {
@@ -1224,85 +1216,55 @@ fn fill_with_equilibrium(buffer: &mut [u8], sample_format: SampleFormat) {
12241216 }
12251217}
12261218
1227- fn set_hw_params_from_format (
1228- pcm_handle : & alsa:: pcm:: PCM ,
1219+ fn init_hw_params < ' a > (
1220+ pcm_handle : & ' a alsa:: pcm:: PCM ,
12291221 config : & StreamConfig ,
12301222 sample_format : SampleFormat ,
1231- ) -> Result < bool , BackendSpecificError > {
1223+ ) -> Result < alsa :: pcm :: HwParams < ' a > , BackendSpecificError > {
12321224 let hw_params = alsa:: pcm:: HwParams :: any ( pcm_handle) ?;
12331225 hw_params. set_access ( alsa:: pcm:: Access :: RWInterleaved ) ?;
1234-
1235- let sample_format = if cfg ! ( target_endian = "big" ) {
1236- match sample_format {
1237- SampleFormat :: I8 => alsa:: pcm:: Format :: S8 ,
1238- SampleFormat :: I16 => alsa:: pcm:: Format :: S16BE ,
1239- SampleFormat :: I24 => alsa:: pcm:: Format :: S24BE ,
1240- SampleFormat :: I32 => alsa:: pcm:: Format :: S32BE ,
1241- // SampleFormat::I48 => alsa::pcm::Format::S48BE,
1242- // SampleFormat::I64 => alsa::pcm::Format::S64BE,
1243- SampleFormat :: U8 => alsa:: pcm:: Format :: U8 ,
1244- SampleFormat :: U16 => alsa:: pcm:: Format :: U16BE ,
1245- SampleFormat :: U24 => alsa:: pcm:: Format :: U24BE ,
1246- SampleFormat :: U32 => alsa:: pcm:: Format :: U32BE ,
1247- // SampleFormat::U48 => alsa::pcm::Format::U48BE,
1248- // SampleFormat::U64 => alsa::pcm::Format::U64BE,
1249- SampleFormat :: F32 => alsa:: pcm:: Format :: FloatBE ,
1250- SampleFormat :: F64 => alsa:: pcm:: Format :: Float64BE ,
1251- sample_format => {
1252- return Err ( BackendSpecificError {
1253- description : format ! (
1254- "Sample format '{sample_format}' is not supported by this backend"
1255- ) ,
1256- } )
1257- }
1258- }
1259- } else {
1260- match sample_format {
1261- SampleFormat :: I8 => alsa:: pcm:: Format :: S8 ,
1262- SampleFormat :: I16 => alsa:: pcm:: Format :: S16LE ,
1263- SampleFormat :: I24 => alsa:: pcm:: Format :: S24LE ,
1264- SampleFormat :: I32 => alsa:: pcm:: Format :: S32LE ,
1265- // SampleFormat::I48 => alsa::pcm::Format::S48LE,
1266- // SampleFormat::I64 => alsa::pcm::Format::S64LE,
1267- SampleFormat :: U8 => alsa:: pcm:: Format :: U8 ,
1268- SampleFormat :: U16 => alsa:: pcm:: Format :: U16LE ,
1269- SampleFormat :: U24 => alsa:: pcm:: Format :: U24LE ,
1270- SampleFormat :: U32 => alsa:: pcm:: Format :: U32LE ,
1271- // SampleFormat::U48 => alsa::pcm::Format::U48LE,
1272- // SampleFormat::U64 => alsa::pcm::Format::U64LE,
1273- SampleFormat :: F32 => alsa:: pcm:: Format :: FloatLE ,
1274- SampleFormat :: F64 => alsa:: pcm:: Format :: Float64LE ,
1275- sample_format => {
1276- return Err ( BackendSpecificError {
1277- description : format ! (
1278- "Sample format '{sample_format}' is not supported by this backend"
1279- ) ,
1280- } )
1281- }
1282- }
1283- } ;
1284-
1285- // Set the sample format, rate, and channels - if this fails, the format is not supported.
1286- hw_params. set_format ( sample_format) ?;
1226+ hw_params. set_format ( sample_format. try_into ( ) ?) ?;
12871227 hw_params. set_rate ( config. sample_rate . 0 , alsa:: ValueOr :: Nearest ) ?;
12881228 hw_params. set_channels ( config. channels as u32 ) ?;
1229+ Ok ( hw_params)
1230+ }
12891231
1290- // Configure period size based on buffer size request
1291- // When BufferSize::Fixed(x) is specified, we request a period size of x frames
1292- // to achieve approximately x-sized callbacks. ALSA may adjust this to the nearest
1293- // supported value based on hardware constraints.
1232+ fn set_hw_params_from_format (
1233+ pcm_handle : & alsa:: pcm:: PCM ,
1234+ config : & StreamConfig ,
1235+ sample_format : SampleFormat ,
1236+ ) -> Result < bool , BackendSpecificError > {
1237+ let hw_params = init_hw_params ( pcm_handle, config, sample_format) ?;
1238+
1239+ // When BufferSize::Fixed(x) is specified, we configure double-buffering with
1240+ // buffer_size = 2x and period_size = x. This provides consistent low-latency
1241+ // behavior across different ALSA implementations and hardware.
12941242 if let BufferSize :: Fixed ( buffer_frames) = config. buffer_size {
1295- hw_params. set_period_size_near ( buffer_frames as _ , alsa:: ValueOr :: Nearest ) ?;
1243+ hw_params. set_buffer_size_near ( ( 2 * buffer_frames) as alsa:: pcm:: Frames ) ?;
1244+ hw_params
1245+ . set_period_size_near ( buffer_frames as alsa:: pcm:: Frames , alsa:: ValueOr :: Nearest ) ?;
12961246 }
12971247
1298- // We shouldn't fail if the driver isn't happy here.
1299- // `default` pcm sometimes fails here, but there's no reason to as we
1300- // provide a direction and 2 is strictly the minimum number of periods.
1301- let _ = hw_params. set_periods ( 2 , alsa:: ValueOr :: Greater ) ;
1302-
13031248 // Apply hardware parameters
13041249 pcm_handle. hw_params ( & hw_params) ?;
13051250
1251+ // For BufferSize::Default, constrain to device's configured period with 2-period buffering.
1252+ // PipeWire-ALSA picks a good period size but pairs it with many periods (huge buffer).
1253+ // We need to re-initialize hw_params and set BOTH period and buffer to constrain properly.
1254+ if config. buffer_size == BufferSize :: Default {
1255+ if let Ok ( period) = hw_params. get_period_size ( ) {
1256+ // Re-initialize hw_params to clear previous constraints
1257+ let hw_params = init_hw_params ( pcm_handle, config, sample_format) ?;
1258+
1259+ // Set both period (to device's chosen value) and buffer (to 2 periods)
1260+ hw_params. set_period_size_near ( period, alsa:: ValueOr :: Nearest ) ?;
1261+ hw_params. set_buffer_size_near ( 2 * period) ?;
1262+
1263+ // Re-apply with new constraints
1264+ pcm_handle. hw_params ( & hw_params) ?;
1265+ }
1266+ }
1267+
13061268 Ok ( hw_params. can_pause ( ) )
13071269}
13081270
@@ -1320,18 +1282,35 @@ fn set_sw_params_from_format(
13201282 description : "initialization resulted in a null buffer" . to_string ( ) ,
13211283 } ) ;
13221284 }
1323- sw_params. set_avail_min ( period as alsa:: pcm:: Frames ) ?;
1324-
13251285 let start_threshold = match stream_type {
13261286 alsa:: Direction :: Playback => {
1327- // Start when ALSA buffer has enough data to maintain consistent playback
1328- // while preserving user's expected latency across different period counts
1329- buffer - period
1287+ // Always use 2-period double-buffering: one period playing from hardware, one
1288+ // period queued in the software buffer. This ensures consistent low latency
1289+ // regardless of the total buffer size.
1290+ 2 * period
13301291 }
13311292 alsa:: Direction :: Capture => 1 ,
13321293 } ;
13331294 sw_params. set_start_threshold ( start_threshold. try_into ( ) . unwrap ( ) ) ?;
13341295
1296+ // Set avail_min based on stream direction. For playback, "avail" means space available
1297+ // for writing (buffer_size - frames_queued). For capture, "avail" means data available
1298+ // for reading (frames_captured). These opposite semantics require different values.
1299+ let target_avail = match stream_type {
1300+ alsa:: Direction :: Playback => {
1301+ // Wake when buffer level drops to one period remaining (avail >= buffer - period).
1302+ // This maintains double-buffering by refilling when we're down to one period.
1303+ buffer - period
1304+ }
1305+ alsa:: Direction :: Capture => {
1306+ // Wake when one period of data is available to read (avail >= period).
1307+ // Using buffer - period here would cause excessive latency as capture would
1308+ // wait for nearly the entire buffer to fill before reading.
1309+ period
1310+ }
1311+ } ;
1312+ sw_params. set_avail_min ( target_avail as alsa:: pcm:: Frames ) ?;
1313+
13351314 period as usize * config. channels as usize
13361315 } ;
13371316
@@ -1349,6 +1328,56 @@ fn set_sw_params_from_format(
13491328 Ok ( period_samples)
13501329}
13511330
1331+ impl TryFrom < SampleFormat > for alsa:: pcm:: Format {
1332+ type Error = BackendSpecificError ;
1333+
1334+ #[ cfg( target_endian = "big" ) ]
1335+ fn try_from ( sample_format : SampleFormat ) -> Result < Self , Self :: Error > {
1336+ Ok ( match sample_format {
1337+ SampleFormat :: I8 => alsa:: pcm:: Format :: S8 ,
1338+ SampleFormat :: I16 => alsa:: pcm:: Format :: S16BE ,
1339+ SampleFormat :: I24 => alsa:: pcm:: Format :: S24BE ,
1340+ SampleFormat :: I32 => alsa:: pcm:: Format :: S32BE ,
1341+ SampleFormat :: U8 => alsa:: pcm:: Format :: U8 ,
1342+ SampleFormat :: U16 => alsa:: pcm:: Format :: U16BE ,
1343+ SampleFormat :: U24 => alsa:: pcm:: Format :: U24BE ,
1344+ SampleFormat :: U32 => alsa:: pcm:: Format :: U32BE ,
1345+ SampleFormat :: F32 => alsa:: pcm:: Format :: FloatBE ,
1346+ SampleFormat :: F64 => alsa:: pcm:: Format :: Float64BE ,
1347+ sample_format => {
1348+ return Err ( BackendSpecificError {
1349+ description : format ! (
1350+ "Sample format '{sample_format}' is not supported by this backend"
1351+ ) ,
1352+ } )
1353+ }
1354+ } )
1355+ }
1356+
1357+ #[ cfg( target_endian = "little" ) ]
1358+ fn try_from ( sample_format : SampleFormat ) -> Result < Self , Self :: Error > {
1359+ Ok ( match sample_format {
1360+ SampleFormat :: I8 => alsa:: pcm:: Format :: S8 ,
1361+ SampleFormat :: I16 => alsa:: pcm:: Format :: S16LE ,
1362+ SampleFormat :: I24 => alsa:: pcm:: Format :: S24LE ,
1363+ SampleFormat :: I32 => alsa:: pcm:: Format :: S32LE ,
1364+ SampleFormat :: U8 => alsa:: pcm:: Format :: U8 ,
1365+ SampleFormat :: U16 => alsa:: pcm:: Format :: U16LE ,
1366+ SampleFormat :: U24 => alsa:: pcm:: Format :: U24LE ,
1367+ SampleFormat :: U32 => alsa:: pcm:: Format :: U32LE ,
1368+ SampleFormat :: F32 => alsa:: pcm:: Format :: FloatLE ,
1369+ SampleFormat :: F64 => alsa:: pcm:: Format :: Float64LE ,
1370+ sample_format => {
1371+ return Err ( BackendSpecificError {
1372+ description : format ! (
1373+ "Sample format '{sample_format}' is not supported by this backend"
1374+ ) ,
1375+ } )
1376+ }
1377+ } )
1378+ }
1379+ }
1380+
13521381impl From < alsa:: Error > for BackendSpecificError {
13531382 fn from ( err : alsa:: Error ) -> Self {
13541383 BackendSpecificError {
0 commit comments