Skip to content

Conversation

roderickvd
Copy link
Member

Improves audio callback performance through device-aware buffering, pre-allocation and period-aligned writes. This results in more consistent latency control and improved hot path performance.

Also fixes a DC offset issue with unsigned formats by proper silence (equilibrium) handling.

Period Configuration Strategy

Queries device capabilities to determine which approach to use:

  1. Double buffering: Uses 2 periods when user buffer fits within device limits
  2. Multi-period: Calculates optimal period distribution for larger buffers
  3. Fallback: Uses buffer-size-only for maximum device compatibility

Audio Quality Improvements

  • Proper silence handling: Fills buffers with correct equilibrium values for unsigned sample formats instead of zeros (prevents DC offset and pops)
  • Full period writes: Always writes complete periods to ALSA for consistent block-based processing
  • Proper stream draining: Drain audio sink on drop when still playing, essential for common patterns like Rodio's sink.sleep_until_end()

Performance & Real-Time Improvements

  • Zero allocations during playback: Pre-allocate all buffers (transfer_buffer, silence_template, descriptors) at stream creation
  • Cache hot path values: Store period_frames and period_samples in StreamInner to avoid repeated calculations
  • Consistent buffer sizes: Period-aligned writes eliminate variable-size buffer handling overhead

Thanks to @abique for the discussions

Supersedes:

@roderickvd
Copy link
Member Author

@abique this implements the full-block writes & double-buffering strategy, as we discussed.

I expect it should have much better latency too, as there are no longer any (re-)allocations, where previously there were multiple per callback.

@abique
Copy link
Contributor

abique commented Sep 23, 2025

I've tested it and it works fine, though I haven't read the change.

I think that the device list with ALSA is wrong and overwhelming.
It shows up too many different options for the same device.

@abique
Copy link
Contributor

abique commented Sep 23, 2025

When I run in debug, I have lots of:

[2025-09-23T17:02:47Z ERROR titan::engine::model::engine] audio device output stream error: A backend-specific error has occurred: `alsa::poll()` returned POLLERR

Also I get a SIGXCPU:

<core::slice::iter::Split<T,P> as core::iter::traits::iterator::Iterator>::next::{{closure}} (Unknown Source:0)
<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::position (Unknown Source:0)
<core::slice::iter::Split<T,P> as core::iter::traits::iterator::Iterator>::next (Unknown Source:0)
<env_logger::fmt::ConfigurableFormatWriter::write_args::IndentWrapper as std::io::Write>::write (/home/abique/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/env_logger-0.11.8/src/fmt/mod.rs:572)
std::io::Write::write_all (Unknown Source:0)
<std::io::default_write_fmt::Adapter<T> as core::fmt::Write>::write_str (Unknown Source:0)
core::fmt::write (Unknown Source:0)
<&T as core::fmt::Display>::fmt (Unknown Source:0)
core::fmt::write (Unknown Source:0)
std::io::default_write_fmt (Unknown Source:0)
std::io::Write::write_fmt (Unknown Source:0)
env_logger::fmt::ConfigurableFormatWriter::write_args (/home/abique/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/env_logger-0.11.8/src/fmt/mod.rs:600)
env_logger::fmt::ConfigurableFormatWriter::write (/home/abique/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/env_logger-0.11.8/src/fmt/mod.rs:420)
env_logger::fmt::ConfigurableFormat::format (/home/abique/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/env_logger-0.11.8/src/fmt/mod.rs:303)
<env_logger::fmt::ConfigurableFormat as env_logger::fmt::RecordFormat>::format (/home/abique/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/env_logger-0.11.8/src/fmt/mod.rs:398)
<env_logger::logger::Logger as log::Log>::log::{{closure}} (/home/abique/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/env_logger-0.11.8/src/logger.rs:669)
<env_logger::logger::Logger as log::Log>::log::{{closure}} (/home/abique/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/env_logger-0.11.8/src/logger.rs:689)
std::thread::local::LocalKey<T>::try_with (Unknown Source:0)
<env_logger::logger::Logger as log::Log>::log (/home/abique/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/env_logger-0.11.8/src/logger.rs:677)
<log::__private_api::GlobalLogger as log::Log>::log (/home/abique/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/log-0.4.28/src/__private_api.rs:47)
log::__private_api::log_impl (/home/abique/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/log-0.4.28/src/__private_api.rs:81)
log::__private_api::log (/home/abique/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/log-0.4.28/src/__private_api.rs:94)
titan::engine::model::engine::Engine::make_stream::{{closure}} (/home/abique/develop/titan/src/engine/model/engine.rs:476)
cpal::host::alsa::output_stream_worker::{{closure}} (/home/abique/.cargo/git/checkouts/cpal-476cd1dd23dbc279/cbb9b03/src/host/alsa/mod.rs:745)
core::result::Result<T,E>::unwrap_or_else (Unknown Source:0)
cpal::host::alsa::output_stream_worker (/home/abique/.cargo/git/checkouts/cpal-476cd1dd23dbc279/cbb9b03/src/host/alsa/mod.rs:744)
cpal::host::alsa::Stream::new_output::{{closure}} (/home/abique/.cargo/git/checkouts/cpal-476cd1dd23dbc279/cbb9b03/src/host/alsa/mod.rs:1072)
std::sys::backtrace::__rust_begin_short_backtrace (Unknown Source:0)
std::thread::Builder::spawn_unchecked_::{{closure}}::{{closure}} (Unknown Source:0)
<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (Unknown Source:0)
std::panicking::catch_unwind::do_call (Unknown Source:0)
__rust_try (Unknown Source:0)
std::panicking::catch_unwind (Unknown Source:0)
std::panic::catch_unwind (Unknown Source:0)
std::thread::Builder::spawn_unchecked_::{{closure}} (Unknown Source:0)
core::ops::function::FnOnce::call_once{{vtable.shim}} (Unknown Source:0)
std::sys::pal::unix::thread::Thread::new::thread_start (Unknown Source:0)
___lldb_unnamed_symbol3732 (Unknown Source:0)
___lldb_unnamed_symbol4174 (Unknown Source:0)

@abique
Copy link
Contributor

abique commented Sep 23, 2025

I don't really know why, but I can't debug with ALSA, maybe it is because of my buffer size (64).
Yet I can debug with Jack.

@roderickvd
Copy link
Member Author

I don't really know why, but I can't debug with ALSA, maybe it is because of my buffer size (64).

Can you successfully run in debug with cpal v0.16? (the “old” code with ~4 periods)

Yet I can debug with Jack.

JACK sets 3 periods, I believe.

I think that the device list with ALSA is wrong and overwhelming.
It shows up too many different options for the same device.

Yes that’s another thing to look into later. It was recently cleaned up for CoreAudio, we can take a similar approach in a later PR for ALSA.

@abique
Copy link
Contributor

abique commented Sep 24, 2025

The issue in debug is not really about drop outs, it is more that it crashes.
Though it could be due to an error on my side that isn't well handled.

@roderickvd
Copy link
Member Author

I see. Could you let me know if it also happens on v0.16? That would be very helpful to isolate if it’s this PR or something else.

@roderickvd
Copy link
Member Author

@abique or others with more feedback on this? I'd like to include this in v0.17 but only if it's found not to be a regression from v0.16.

…ormance

Implement device-aware period configuration strategy:
- Query device limits to determine optimal approach
- Prefer double buffering when buffer fits within device period size limits
- Use multi-period configuration for larger buffers with calculated period distribution
- Fallback to buffer-size-only configuration for maximum device compatibility

Performance optimizations:
- Pre-allocate worker thread buffers and silence templates
- Cache period_frames and period_samples in StreamInner for hot path performance
- Pre-fill descriptors array once instead of reallocating on each poll
@roderickvd roderickvd force-pushed the refactor/alsa-period-optimization branch from b18b54e to 87137b4 Compare September 28, 2025 20:27
@roderickvd roderickvd force-pushed the refactor/alsa-period-optimization branch from f434bbc to 26b1092 Compare September 28, 2025 20:36
@roderickvd
Copy link
Member Author

Merging this - got things cooking for the other hosts' buffer behavior as well, and need this as basis. Don't hesitate to reopen if this causes regressions from v0.16 (remembering that used four periods and this two - so you may want a larger buffer now).

@roderickvd roderickvd merged commit 636e52e into master Sep 28, 2025
14 checks passed
@roderickvd roderickvd requested a review from Copilot September 28, 2025 20:45
Copilot

This comment was marked as resolved.

@roderickvd roderickvd deleted the refactor/alsa-period-optimization branch September 28, 2025 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants