Skip to content

Improve Alsa backend buffer slightly - #811

Merged
roderickvd merged 13 commits into
librespot-org:devfrom
JasonLG1979:reuse-buffer-alsa-backend
Jul 6, 2021
Merged

Improve Alsa backend buffer slightly#811
roderickvd merged 13 commits into
librespot-org:devfrom
JasonLG1979:reuse-buffer-alsa-backend

Conversation

@JasonLG1979

@JasonLG1979 JasonLG1979 commented Jun 23, 2021

Copy link
Copy Markdown
Contributor

Reuse the period buffer.
Vec.shrink_to_fit and Vec.reserve_exact have proven to be reliable well past our needs. (https://gist.github.com/JasonLG1979/69ed65d91c64f39de4435b3720258d02)

There's no reason to not reuse the buffer and avoid unnecessary allocations.

Use a loop expression instead of a while loop to chew though the bytes because it's faster.
(https://gist.github.com/JasonLG1979/3efb5646e4bb42309bdda6d83a3da54e)

Always give the PCM a period's worth of audio even when draining the buffer.
This makes for more consistent stop to start latency (pause to play, manually changing tracks) and helps to prevent jarring, "chopped off" sounding transition when changing tracks manually. Overall resulting in a slightly smoother user experience.

Comment thread playback/src/audio_backend/alsa.rs Outdated
@roderickvd roderickvd self-assigned this Jun 23, 2021
Comment thread playback/src/audio_backend/alsa.rs Outdated
@roderickvd

Copy link
Copy Markdown
Member

Thinking about the case where there isn't enough data to feed into the period_buffer for it to become full and trigger a write. You could do something like:

if self.period_buffer.len() == self.period_buffer.capacity() || processed_data == data.len() { 

But I don't think we should. First, when there will be more packets fed afterwards (which write_bytes knows nothing about), doing so would trigger one extra and unnecessary write_buf() per packet. Second, when this really was the last packet because playback is about to finish, a subsequent stop() will flush the buffer anyway.

@JasonLG1979

JasonLG1979 commented Jun 23, 2021

Copy link
Copy Markdown
Contributor Author

Thinking about the case where there isn't enough data to feed into the period_buffer for it to become full and trigger a write. You could do something like:

if self.period_buffer.len() == self.period_buffer.capacity() || processed_data == data.len() { 

But I don't think we should. First, when there will be more packets fed afterwards (which write_bytes knows nothing about), doing so would trigger one extra and unnecessary write_buf() per packet. Second, when this really was the last packet because playback is about to finish, a subsequent stop() will flush the buffer anyway.

I'm not really sure it's even a concern. That would mean that the decoder could not keep up with the PCM. That would indicate either the device isn't powerfull enough to run librespot or that the network speed or connection stability was not good enough to pull down enough packets to decode.

Currently the buffer is recreated every time start is called. This also fixes a previous commit's mistake of making it so the buffer is not drained on stop.
@JasonLG1979 JasonLG1979 changed the title Reuse the buffer for the life of the alsa sink Refactor Alsa backend buffer to make it more robust Jun 25, 2021
* As decided in #811 (comment) don't absolutely depend on `Vec` capacity or growth strategies. Use the period size as the authority not capacity.

* Continuing on the theme of not trusting `Vec` growth strategies, over allocate initially so that our Vec will never actaully reach full capacity thus (hopefully) avoiding future reallocations.

* Move all buffer code to impl AlsaSink.

* Add a new (IMHO more legible) recursive `process_bytes` function for chewing though the bytes sent to us from mod.

* Other small cleanups.
@JasonLG1979

JasonLG1979 commented Jun 27, 2021

Copy link
Copy Markdown
Contributor Author

@roderickvd I'd also like some more reviews of this. It started out as just simply reusing the buffer but turned into a refactor because we came to the conclusion that basically Vec's can't be trusted. It changes very little external behavior other than making manual track changes more smooth so I don't even really think it warrants a mention in the changelog since it's basically all implementation details.

@roderickvd roderickvd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that you've proven Vec::with_capacity(x) does what is says on the tin, unlike resize(x), there are some simplifications that can be made.

I like you addressed that tiny drain bug en passant.

Comment thread playback/src/audio_backend/alsa.rs Outdated
Comment thread playback/src/audio_backend/alsa.rs Outdated
Comment thread playback/src/audio_backend/alsa.rs Outdated
Comment thread playback/src/audio_backend/alsa.rs Outdated
Comment thread playback/src/audio_backend/alsa.rs Outdated
Comment thread playback/src/audio_backend/alsa.rs Outdated
This basically narrows this PR down to:

* Use a loop expression instead of a while loop to chew though the bytes because it's faster.

* Always give the PCM a period's worth of audio even when draining the buffer. This makes for more consistent stop to start latency (pause to play, manually changing tracks) and helps to prevent jarring, "chopped off" sounding transitions when changing tracks manually. Overall resulting in a slightly smoother user experience.
@JasonLG1979 JasonLG1979 changed the title Refactor Alsa backend buffer to make it more robust Improve Alsa backend buffer slightly Jun 28, 2021
@JasonLG1979

Copy link
Copy Markdown
Contributor Author

I think that covers everything. Addressing everything narrowed the scope of the PR A LOT.

@JasonLG1979

JasonLG1979 commented Jun 28, 2021

Copy link
Copy Markdown
Contributor Author

If anyone is curious as to how I came to the conclusion that a loop expression is faster than a while loop:
https://gist.github.com/JasonLG1979/3efb5646e4bb42309bdda6d83a3da54e

@roderickvd

Copy link
Copy Markdown
Member

I think that covers everything. Addressing everything narrowed the scope of the PR A LOT.

Indeed! I'll do some final sanity checking myself and then we can get this in.

If anyone is curious as to how I came to the conclusion that a loop expression is faster than a while loop:
https://gist.github.com/JasonLG1979/3efb5646e4bb42309bdda6d83a3da54e

Having fun with Rust yet? 😄

@JasonLG1979

Copy link
Copy Markdown
Contributor Author

Having fun with Rust yet?

I am. I just know that if I'm going to claim something I better be able to back it up.

@JasonLG1979

Copy link
Copy Markdown
Contributor Author

I googled it and couldn't find any definitive answers so I figured that mocking up a test that simulates our use case was the best way to find out the truth. Again I tested on x86_64, ARMv6 and ARMv8. The result was the same on all of them, loop is about 3 to 5% faster. And as a bonus it's the most idiomatic of the 3 IMHO.

@roderickvd

Copy link
Copy Markdown
Member

Doing some final checking on my own rig now, will merge on success.

@roderickvd

Copy link
Copy Markdown
Member

Unfortunately it panics when playing to dmix. Probably because the period buffer is again not what we ask or assume it to be:

thread '<unnamed>' panicked at 'slice index starts at 4096 but ends at 1792', playback/src/audio_backend/alsa.rs:238:37
stack backtrace:
   0:   0xf7cd0c - std::backtrace_rs::backtrace::libunwind::trace::h27f2387a4162a450
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/../../backtrace/src/backtrace/libunwind.rs:90:5
   1:   0xf7cd0c - std::backtrace_rs::backtrace::trace_unsynchronized::h163a7502bf6b94aa
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5
   2:   0xf7cd0c - std::sys_common::backtrace::_print_fmt::h43e6d4cedc52f23a
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/sys_common/backtrace.rs:67:5
   3:   0xf7cd0c - <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt::hde8f8384e4e6dbf2
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/sys_common/backtrace.rs:46:22
   4:   0xfa468c - core::fmt::write::he9a84ebd1145a5b6
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/core/src/fmt/mod.rs:1094:17
   5:   0xf76658 - std::io::Write::write_fmt::hb674bb2dc2ae8509
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/io/mod.rs:1584:15
   6:   0xf7f4bc - std::sys_common::backtrace::_print::he8ac319d646764e6
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/sys_common/backtrace.rs:49:5
   7:   0xf7f4bc - std::sys_common::backtrace::print::hc642e9974601dbe9
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/sys_common/backtrace.rs:36:9
   8:   0xf7f4bc - std::panicking::default_hook::{{closure}}::ha1230b816c9262c0
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panicking.rs:208:50
   9:   0xf7eea8 - std::panicking::default_hook::h7e50a310a4eb3bfa
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panicking.rs:225:9
  10:   0xf7fb00 - std::panicking::rust_panic_with_hook::hec171a86b5342505
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panicking.rs:591:17
  11:   0xf7f680 - std::panicking::begin_panic_handler::{{closure}}::h88a5c16da59165d8
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panicking.rs:497:13
  12:   0xf7d288 - std::sys_common::backtrace::__rust_end_short_backtrace::h27383fc565bb9434
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/sys_common/backtrace.rs:141:18
  13:   0xf7f5c8 - rust_begin_unwind
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panicking.rs:493:5
  14:   0x4c09e0 - core::panicking::panic_fmt::hf038e00ff0a6dc95
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/core/src/panicking.rs:92:14
  15:   0x4c0c08 - core::slice::index::slice_index_order_fail::hb70601bfb7f6e497
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/core/src/slice/index.rs:48:5
  16:   0x941858 - <core::ops::range::Range<usize> as core::slice::index::SliceIndex<[T]>>::index::h2b30e25ff723d8b1
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/core/src/slice/index.rs:238:13
  17:   0x8e57dc - core::slice::index::<impl core::ops::index::Index<I> for [T]>::index::hb737982a0d857c9b
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/core/src/slice/index.rs:15:9
  18:   0x6e9bb8 - <librespot_playback::audio_backend::alsa::AlsaSink as librespot_playback::audio_backend::SinkAsBytes>::write_bytes::h2196368804dc57c9
                       at /home/pi/librespot/playback/src/audio_backend/alsa.rs:238:37
  19:   0x6eb5ec - <librespot_playback::audio_backend::alsa::AlsaSink as librespot_playback::audio_backend::Sink>::write::h8f0e4ec432bf2a77
                       at /home/pi/librespot/playback/src/audio_backend/mod.rs:57:25
  20:   0x727560 - librespot_playback::player::PlayerInternal::handle_packet::hbc9d52f1a1c45bbf
                       at /home/pi/librespot/playback/src/player.rs:1307:39
  21:   0x725664 - <librespot_playback::player::PlayerInternal as core::future::future::Future>::poll::h1061546f4b24470f
                       at /home/pi/librespot/playback/src/player.rs:995:21
  22:   0x52ab60 - futures_executor::local_pool::block_on::{{closure}}::h89fd3dd95adbe86d
                       at /home/pi/.cargo/registry/src/github.com-1285ae84e5963aae/futures-executor-0.3.15/src/local_pool.rs:315:23
  23:   0x52a9f4 - futures_executor::local_pool::run_executor::{{closure}}::h312636993be95269
                       at /home/pi/.cargo/registry/src/github.com-1285ae84e5963aae/futures-executor-0.3.15/src/local_pool.rs:90:37
  24:   0x565478 - std::thread::local::LocalKey<T>::try_with::ha8749b07a03d58bc
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/thread/local.rs:376:16
  25:   0x564fb8 - std::thread::local::LocalKey<T>::with::hd00ff4e97679fdeb
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/thread/local.rs:352:9
  26:   0x52a93c - futures_executor::local_pool::run_executor::hb68df2bab16ca91f
                       at /home/pi/.cargo/registry/src/github.com-1285ae84e5963aae/futures-executor-0.3.15/src/local_pool.rs:86:5
  27:   0x52aaec - futures_executor::local_pool::block_on::hef5d8bf8f1998a05
                       at /home/pi/.cargo/registry/src/github.com-1285ae84e5963aae/futures-executor-0.3.15/src/local_pool.rs:315:5
  28:   0x5647a4 - librespot_playback::player::Player::new::{{closure}}::h2f7230ad710eabcc
                       at /home/pi/librespot/playback/src/player.rs:334:13
  29:   0x5bd370 - std::sys_common::backtrace::__rust_begin_short_backtrace::h938fdd157964b2c8
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/sys_common/backtrace.rs:125:18
  30:   0x517788 - std::thread::Builder::spawn_unchecked::{{closure}}::{{closure}}::h9081bb9281344860
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/thread/mod.rs:481:17
  31:   0x560a94 - <std::panic::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once::h3aa746918727c167
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panic.rs:344:9
  32:   0x4ee6f4 - std::panicking::try::do_call::h6a4b29d857bf8983
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panicking.rs:379:40
  33:   0x4f2e98 - __rust_try
  34:   0x4eda4c - std::panicking::try::h46c9e3650e4962dc
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panicking.rs:343:19
  35:   0x564d10 - std::panic::catch_unwind::h6b84a09577f4deeb
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panic.rs:431:14
  36:   0x5175ec - std::thread::Builder::spawn_unchecked::{{closure}}::h0dc1f5f83d451e89
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/thread/mod.rs:480:30
  37:   0x5ac440 - core::ops::function::FnOnce::call_once{{vtable.shim}}::hdc61ac410c71bace
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/core/src/ops/function.rs:227:5
  38:   0xf84c0c - <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once::hb0e93ad9455f4a24
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/alloc/src/boxed.rs:1546:9
  39:   0xf84c0c - <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once::h3c13f9f9377ea63d
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/alloc/src/boxed.rs:1546:9
  40:   0xf84c0c - std::sys::unix::thread::Thread::new::thread_start::hdc1cccb8ae31d2bc
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/sys/unix/thread.rs:71:17

@JasonLG1979

JasonLG1979 commented Jun 30, 2021

Copy link
Copy Markdown
Contributor Author

Unfortunately it panics when playing to dmix. Probably because the period buffer is again not what we ask or assume it to be:

thread '<unnamed>' panicked at 'slice index starts at 4096 but ends at 1792', playback/src/audio_backend/alsa.rs:238:37
stack backtrace:
   0:   0xf7cd0c - std::backtrace_rs::backtrace::libunwind::trace::h27f2387a4162a450
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/../../backtrace/src/backtrace/libunwind.rs:90:5
   1:   0xf7cd0c - std::backtrace_rs::backtrace::trace_unsynchronized::h163a7502bf6b94aa
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5
   2:   0xf7cd0c - std::sys_common::backtrace::_print_fmt::h43e6d4cedc52f23a
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/sys_common/backtrace.rs:67:5
   3:   0xf7cd0c - <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt::hde8f8384e4e6dbf2
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/sys_common/backtrace.rs:46:22
   4:   0xfa468c - core::fmt::write::he9a84ebd1145a5b6
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/core/src/fmt/mod.rs:1094:17
   5:   0xf76658 - std::io::Write::write_fmt::hb674bb2dc2ae8509
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/io/mod.rs:1584:15
   6:   0xf7f4bc - std::sys_common::backtrace::_print::he8ac319d646764e6
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/sys_common/backtrace.rs:49:5
   7:   0xf7f4bc - std::sys_common::backtrace::print::hc642e9974601dbe9
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/sys_common/backtrace.rs:36:9
   8:   0xf7f4bc - std::panicking::default_hook::{{closure}}::ha1230b816c9262c0
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panicking.rs:208:50
   9:   0xf7eea8 - std::panicking::default_hook::h7e50a310a4eb3bfa
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panicking.rs:225:9
  10:   0xf7fb00 - std::panicking::rust_panic_with_hook::hec171a86b5342505
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panicking.rs:591:17
  11:   0xf7f680 - std::panicking::begin_panic_handler::{{closure}}::h88a5c16da59165d8
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panicking.rs:497:13
  12:   0xf7d288 - std::sys_common::backtrace::__rust_end_short_backtrace::h27383fc565bb9434
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/sys_common/backtrace.rs:141:18
  13:   0xf7f5c8 - rust_begin_unwind
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panicking.rs:493:5
  14:   0x4c09e0 - core::panicking::panic_fmt::hf038e00ff0a6dc95
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/core/src/panicking.rs:92:14
  15:   0x4c0c08 - core::slice::index::slice_index_order_fail::hb70601bfb7f6e497
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/core/src/slice/index.rs:48:5
  16:   0x941858 - <core::ops::range::Range<usize> as core::slice::index::SliceIndex<[T]>>::index::h2b30e25ff723d8b1
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/core/src/slice/index.rs:238:13
  17:   0x8e57dc - core::slice::index::<impl core::ops::index::Index<I> for [T]>::index::hb737982a0d857c9b
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/core/src/slice/index.rs:15:9
  18:   0x6e9bb8 - <librespot_playback::audio_backend::alsa::AlsaSink as librespot_playback::audio_backend::SinkAsBytes>::write_bytes::h2196368804dc57c9
                       at /home/pi/librespot/playback/src/audio_backend/alsa.rs:238:37
  19:   0x6eb5ec - <librespot_playback::audio_backend::alsa::AlsaSink as librespot_playback::audio_backend::Sink>::write::h8f0e4ec432bf2a77
                       at /home/pi/librespot/playback/src/audio_backend/mod.rs:57:25
  20:   0x727560 - librespot_playback::player::PlayerInternal::handle_packet::hbc9d52f1a1c45bbf
                       at /home/pi/librespot/playback/src/player.rs:1307:39
  21:   0x725664 - <librespot_playback::player::PlayerInternal as core::future::future::Future>::poll::h1061546f4b24470f
                       at /home/pi/librespot/playback/src/player.rs:995:21
  22:   0x52ab60 - futures_executor::local_pool::block_on::{{closure}}::h89fd3dd95adbe86d
                       at /home/pi/.cargo/registry/src/github.com-1285ae84e5963aae/futures-executor-0.3.15/src/local_pool.rs:315:23
  23:   0x52a9f4 - futures_executor::local_pool::run_executor::{{closure}}::h312636993be95269
                       at /home/pi/.cargo/registry/src/github.com-1285ae84e5963aae/futures-executor-0.3.15/src/local_pool.rs:90:37
  24:   0x565478 - std::thread::local::LocalKey<T>::try_with::ha8749b07a03d58bc
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/thread/local.rs:376:16
  25:   0x564fb8 - std::thread::local::LocalKey<T>::with::hd00ff4e97679fdeb
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/thread/local.rs:352:9
  26:   0x52a93c - futures_executor::local_pool::run_executor::hb68df2bab16ca91f
                       at /home/pi/.cargo/registry/src/github.com-1285ae84e5963aae/futures-executor-0.3.15/src/local_pool.rs:86:5
  27:   0x52aaec - futures_executor::local_pool::block_on::hef5d8bf8f1998a05
                       at /home/pi/.cargo/registry/src/github.com-1285ae84e5963aae/futures-executor-0.3.15/src/local_pool.rs:315:5
  28:   0x5647a4 - librespot_playback::player::Player::new::{{closure}}::h2f7230ad710eabcc
                       at /home/pi/librespot/playback/src/player.rs:334:13
  29:   0x5bd370 - std::sys_common::backtrace::__rust_begin_short_backtrace::h938fdd157964b2c8
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/sys_common/backtrace.rs:125:18
  30:   0x517788 - std::thread::Builder::spawn_unchecked::{{closure}}::{{closure}}::h9081bb9281344860
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/thread/mod.rs:481:17
  31:   0x560a94 - <std::panic::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once::h3aa746918727c167
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panic.rs:344:9
  32:   0x4ee6f4 - std::panicking::try::do_call::h6a4b29d857bf8983
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panicking.rs:379:40
  33:   0x4f2e98 - __rust_try
  34:   0x4eda4c - std::panicking::try::h46c9e3650e4962dc
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panicking.rs:343:19
  35:   0x564d10 - std::panic::catch_unwind::h6b84a09577f4deeb
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panic.rs:431:14
  36:   0x5175ec - std::thread::Builder::spawn_unchecked::{{closure}}::h0dc1f5f83d451e89
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/thread/mod.rs:480:30
  37:   0x5ac440 - core::ops::function::FnOnce::call_once{{vtable.shim}}::hdc61ac410c71bace
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/core/src/ops/function.rs:227:5
  38:   0xf84c0c - <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once::hb0e93ad9455f4a24
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/alloc/src/boxed.rs:1546:9
  39:   0xf84c0c - <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once::h3c13f9f9377ea63d
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/alloc/src/boxed.rs:1546:9
  40:   0xf84c0c - std::sys::unix::thread::Thread::new::thread_start::hdc1cccb8ae31d2bc
                       at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/sys/unix/thread.rs:71:17

No that's what happens when you just copy and paste diffs instead of actually checking the code out and you forget self.period_buffer.clear(); at the end of write_buf...

@JasonLG1979

JasonLG1979 commented Jun 30, 2021

Copy link
Copy Markdown
Contributor Author

It to be fair, I've done the same on accident and it took me a little bit to realize my copypasta mistake,lol!!!

Edit: I thoroughly tested this against dmix, real hardware, Pulseaudio and Pipewire.

@JasonLG1979

Copy link
Copy Markdown
Contributor Author

self.period_buffer.clear(); should be here if it's not than that index error will happen every time without fail.

@JasonLG1979

Copy link
Copy Markdown
Contributor Author

I'm listening to this branch right now on my Pi Zero though dmix. No problems.

@JasonLG1979

Copy link
Copy Markdown
Contributor Author

Here is my current asound.conf

pcm.!default {
    type plug
    rate_converter speexrate_medium
    slave.pcm {
        type dmix
        ipc_key {
            @func refer
            name defaults.pcm.ipc_key
        }
        ipc_gid {
            @func refer
            name defaults.pcm.ipc_gid
        }
        ipc_perm {
            @func refer
            name defaults.pcm.ipc_perm
        }
        slave {
            pcm {
                type hw
                card sndrpihifiberry
                device 0
            }
            channels 2
            period_time 125000
            periods 4
            rate 44100
            format S32_LE
        }
        bindings {
            0 0
            1 1
        }
    }
}

And since I specify periods and period_time, cat /proc/asound/card*/pcm*p/sub*/hw_params outputs:

access: MMAP_INTERLEAVED
format: S32_LE
subformat: STD
channels: 2
rate: 44100 (44100/1)
period_size: 5513
buffer_size: 22052

@JasonLG1979

JasonLG1979 commented Jul 1, 2021

Copy link
Copy Markdown
Contributor Author

And if you wanted to skip plug (although most people put a plug in front of dmix) this would also work provided everything you threw at dmix was 44100 / S32:

pcm.!default {
    type dmix
    ipc_key {
        @func refer
        name defaults.pcm.ipc_key
    }
    ipc_gid {
        @func refer
        name defaults.pcm.ipc_gid
    }
    ipc_perm {
        @func refer
        name defaults.pcm.ipc_perm
    }
    slave {
        pcm {
            type hw
            card sndrpihifiberry
            device 0
        }
        channels 2
        period_time 125000
        periods 4
        rate 44100
        format S32_LE
    }
    bindings {
        0 0
        1 1
    }
}

That outputs the same for cat /proc/asound/card*/pcm*p/sub*/hw_param but since there's no plug aplay --dump-hw-params /usr/share/sounds/alsa/Front_Right.wav outputs:

ACCESS:  MMAP_INTERLEAVED MMAP_NONINTERLEAVED RW_INTERLEAVED RW_NONINTERLEAVED
FORMAT:  S32_LE
SUBFORMAT:  STD
SAMPLE_BITS: 32
FRAME_BITS: 64
CHANNELS: 2
RATE: 44100
PERIOD_TIME: (125011 125012)
PERIOD_SIZE: 5513
PERIOD_BYTES: 44104
PERIODS: [2 4]
BUFFER_TIME: (250022 500046)
BUFFER_SIZE: [11026 22052]
BUFFER_BYTES: [88208 176416]
TICK_TIME: ALL

Notice that PERIOD_SIZE and PERIOD_BYTES only have one possible value and everything else buffer related is only available in either 2 or 4 periods. In this configuration it is impossible for librespot to get exactly what it asks for. It will get a 4 period buffer at 44104 bytes each period when it asks for a 0.5 sec buffer with 100ms periods.

@roderickvd

Copy link
Copy Markdown
Member

So I just checked: it's a clean checkout @ 85fb588 with that self.period_buffer.clear() present in the right place.

It still panics for me when launched simply as ./target/debug/librespot --verbose --backend alsa --device dmix --initial-volume 30 whereas the current dev branch doesn't.

/etc/asound.conf mostly yours (though I always use hw:0 myself):

defaults.pcm.dmix.rate 44100

###############################################################################

pcm.hqstereo20 {
    @args [
        SAMPLE_RATE FORMAT BUFFER_PERIODS
        BUFFER_PERIOD_TIME VOL_MIN_DB
        VOL_MAX_DB VOL_RESOLUTION VOL_NAME
    ]
    # Sampling rate in Hz:
    # 44100, 48000, 88200, 96000...
    # Defaults to 44.1 kHz (CD Quality).
    @args.SAMPLE_RATE {
        type integer
        default 44100
    }
    # Format:
    # S16_LE, S24_LE, S24_3LE, S32_LE...
    # Defaults to S16_LE (CD Quality).
    @args.FORMAT {
        type string
        default S16_LE
    }
    # Periods per buffer.
    @args.BUFFER_PERIODS {
        type integer
        default 4
    }
    # Period size in time.
    # Defaults to 125ms (0.125 sec).
    # BUFFER_PERIODS * BUFFER_PERIOD_TIME = buffer time/size
    @args.BUFFER_PERIOD_TIME {
        type integer
        default 125000
    }
    # Minimal dB value of the software volume control.
    @args.VOL_MIN_DB {
        type real
        default -51.0
    }
    # Maximal dB value of the software volume control.
    @args.VOL_MAX_DB {
        type real
        default 0.0
    }
    # How many steps between min and max volume.
    @args.VOL_RESOLUTION {
        type integer
        default 256
    }
    # The name of the software volume control.
    # If your card does not have hardware volume control
    # naming it PCM will cause most apps that use alsa volume
    # to use the software volume control.
    @args.VOL_NAME {
        type string
        default Softvol
    }
    type softvol
    min_dB $VOL_MIN_DB
    max_dB $VOL_MAX_DB
    resolution $VOL_RESOLUTION
    control {
        name $VOL_NAME
        card {
            @func refer
            name defaults.ctl.card
        }
    }
    slave.pcm {
        type plug
        slave.pcm {
            type dmix
            ipc_key {
                @func refer
                name defaults.pcm.ipc_key
            }
            ipc_gid {
                @func refer
                name defaults.pcm.ipc_gid
            }
            ipc_perm {
                @func refer
                name defaults.pcm.ipc_perm
            }
            slowptr 1
            hw_ptr_alignment roundup
            slave {
                pcm {
                    type hw
                    nonblock {
                        @func refer
                        name defaults.pcm.nonblock
                    }
                    card {
                        @func refer
                        name defaults.pcm.card
                    }
                    device {
                        @func refer
                        name defaults.pcm.device
                    }
                    subdevice {
                        @func refer
                        name defaults.pcm.subdevice
                    }
                }
                channels 2
                period_size 0
                buffer_size 0
                buffer_time 0
                period_time $BUFFER_PERIOD_TIME
                periods $BUFFER_PERIODS
                rate $SAMPLE_RATE
                format $FORMAT
            }
            bindings {
                0 0
                1 1
            }
        }
    }
}

###############################################################################

# Find your sound card:
# aplay -l

# Find your card's supported format(s) and sampling rate(s) While no other audio is being played:
# aplay -Dhw:<card #>,<device #> --dump-hw-params /usr/share/sounds/alsa/Front_Right.wav
# For example:
# aplay -Dhw:0,0 --dump-hw-params /usr/share/sounds/alsa/Front_Right.wav

# The output should look something like this if the card and device combination is correct:

# ACCESS:  MMAP_INTERLEAVED RW_INTERLEAVED
# FORMAT:  S16_LE S24_LE S32_LE
# SUBFORMAT:  STD
# SAMPLE_BITS: [16 32]
# FRAME_BITS: [32 64]
# CHANNELS: 2
# RATE: [8000 192000]
# ...

# The above card supports formats S16_LE, S24_LE, and S32_LE, at up to a sampling rate of 192000.

# Check to see if your sound card has hardware volume controls.
# amixer -c<card #> scontrols
# For example:
# amixer -c0 scontrols

# The output should look something like this if your card has hardware volume control:
# Simple mixer control 'PCM',0
# ... 

# Change to the card number that you want to be the default control card.
# Default: 0
defaults.ctl.card 0

# Change to the card number that you want to be the default playback card.
# It should usually be the same as defaults.ctl.card.
# Default: 0
defaults.pcm.card 0

# Change to the device number that you want to be the default device on the default card.
# 0 or 1 is usually the correct device number.
# Default: 0
defaults.pcm.device 0

# Change to the subdevice number that you want to be the default subdevice on the default device.
# Should rarely need to be changed.
# Default: -1
defaults.pcm.subdevice -1

# To install high quality samplerate converters on Debian based systems:
# sudo apt install -y --no-install-recommends libasound2-plugins 

# To list available rate_converter's:
# echo "$(ls /usr/lib/*/alsa-lib | grep "libasound_module_rate_")" | sed -e "s/^libasound_module_rate_//" -e "s/.so$//"

# Uncomment and replace speexrate_medium with the rate_converter of your choice.
# defaults.pcm.rate_converter speexrate_medium

# Once everything is configure close and save this file and test with:
# speaker-test -c2 -l1

# While audio is playing you can verify your settings with:
# cat /proc/asound/card*/pcm*p/sub*/hw_params

pcm.!default {
    type empty
    # Optional args:
    # SAMPLE_RATE: default: 44100 
    # FORMAT: default: S24_LE
    # BUFFER_PERIODS: default: 4
    # BUFFER_PERIOD_TIME: default: 125000
    # VOL_MIN_DB: default: -51.0
    # VOL_MAX_DB: default: 0.0
    # VOL_RESOLUTION: default: 256
    # VOL_NAME: default: Softvol

    # Example:
    # hifiberry dac+ zero on a pi zero.
    # slave.pcm "hqstereo20:FORMAT=S32_LE,BUFFER_PERIOD_TIME=250000,VOL_MIN_DB=-48.0,VOL_NAME=PCM"

    slave.pcm "hqstereo20:FORMAT=S24_LE,VOL_MIN_DB=-60.0"
}

###############################################################################

ctl.!default {
    type hw
    card {
        @func refer
        name defaults.ctl.card
    }
}

@roderickvd

Copy link
Copy Markdown
Member

With dmix on dev:

Advanced Linux Sound Architecture Driver Version k5.10.17-v7+.

Card 0 (sndrpirpidac):

  * Playback Device 0 (RPi-DAC HiFi pcm1794a-codec-0):
    used by: librespot (PID 20062)
    access: MMAP_INTERLEAVED
    format: S16_LE
    subformat: STD
    channels: 2
    rate: 44100 (44100/1)
    period_size: 1024
    buffer_size: 16384

I have no idea what's going on (though I haven't dug deep into your code either) but having opened dmix with dev before, now switching back to your branch doesn't panic but just doesn't produce any sound. Switching to dev plays back sound again.

I'll fire up some older Linux box to see if I can reproduce it there. But this is definitely fishy.

@roderickvd

Copy link
Copy Markdown
Member

Same on my old 2010 MacBook Pro that now runs elementary OS (Ubuntu 18.04 LTS). It works fine on dev, but on your branch the player thread hangs (and so produces no sound). This was the same on my main rig which is a RPi: when you press Ctrl-C it hangs like:

[2021-07-01T18:13:54Z DEBUG librespot_playback::player] Shutting down player thread ...

Only thing in /etc/asound.conf:

defaults.pcm.dmix.rate 44100

@JasonLG1979

JasonLG1979 commented Jul 2, 2021

Copy link
Copy Markdown
Contributor Author

I can not reproduce any of those issues on my Pi Zero. I will dig a little deeper after work. The only panic I got was once from mdns?

You would not normally use dmix as an output with a device with a custom /etc/asound.conf you would use default since you went to so much trouble to define and override default in pcm.!default.

now switching back to your branch doesn't panic but just doesn't produce any sound.

Stupid question, but did you turn it up? Because I thought "Wow, what's going on? I don't have any sound either" then I noticed the volume was at 30 so I turned it up and it was working just fine.

@JasonLG1979

Copy link
Copy Markdown
Contributor Author

To be clear I tested with librespot --verbose --initial-volume 30 --format S32 since aplay -Ddmix --dump-hw-params /usr/share/sounds/alsa/Front_Right.wav reports this for my Pi Zero with just defaults.pcm.dmix.rate 44100 in my /etc/asound.conf:

ACCESS:  MMAP_INTERLEAVED MMAP_NONINTERLEAVED RW_INTERLEAVED RW_NONINTERLEAVED
FORMAT:  S32_LE
SUBFORMAT:  STD
SAMPLE_BITS: 32
FRAME_BITS: 64
CHANNELS: 2
RATE: 44100
PERIOD_TIME: (23219 23220)
PERIOD_SIZE: 1024
PERIOD_BYTES: 8192
PERIODS: [2 16]
BUFFER_TIME: (46439 371520)
BUFFER_SIZE: [2048 16384]
BUFFER_BYTES: [16384 131072]
TICK_TIME: ALL

@JasonLG1979

Copy link
Copy Markdown
Contributor Author

And as to be expected cat /proc/asound/card*/pcm*p/sub*/hw_params reports this during playback:

access: RW_INTERLEAVED
format: S32_LE
subformat: STD
channels: 2
rate: 44100 (44100/1)
period_size: 4410
buffer_size: 22050

@roderickvd

Copy link
Copy Markdown
Member

Stupid question, but did you turn it up? Because I thought "Wow, what's going on? I don't have any sound either" then I noticed the volume was at 30 so I turned it up and it was working just fine.

I did. Also 30% is audible for me (I've got an amp with a lot of headroom), as shows when I play on dev.

And as to be expected cat /proc/asound/card*/pcm*p/sub*/hw_params reports this during playback:

access: RW_INTERLEAVED
format: S32_LE
subformat: STD
channels: 2
rate: 44100 (44100/1)
period_size: 4410
buffer_size: 22050

Note that my period_size is less than a fourth of that. Given the panics on the buffer index, and the later hangs, I suspect that there is something amiss with your loop or the AudioPacket packet size vs. the buffer capacity or length.

I'll have to do some debugging myself to work this one out.

@JasonLG1979

Copy link
Copy Markdown
Contributor Author

I'm back. i'm starting my deep dive. I think at the very least no matter what happens I going to add a few debug!'s in open_device I'm thinking something like:

        debug!("AlsaSink Frames per Buffer: {:?}", frames_per_buffer);
        debug!("AlsaSink Frames per Buffer Period: {:?}", frames_per_period);
        debug!("AlsaSink Buffer size in bytes: {:?}", bytes_per_period);

They may be helpful now and in the future.

@JasonLG1979

Copy link
Copy Markdown
Contributor Author

@roderickvd I apologize for being kinda an ass. You were right. I think that I figured it out. Give that a try.

@ashthespy

Copy link
Copy Markdown
Member

Okay wow, I see #811 (comment) opened up a can of worms!
I swear it was just an innocent question, didn't expect it to explode into such a discussion! 😛

@JasonLG1979

Copy link
Copy Markdown
Contributor Author

Okay wow, I see #811 (comment) opened up a can of worms!
I swear it was just an innocent question, didn't expect it to explode into such a discussion!

Well any question that leads to learning something is a good question.

Vec.shrink_to_fit and Vec.reserve_exact have proven to be reliable well past our needs. (https://gist.github.com/JasonLG1979/69ed65d91c64f39de4435b3720258d02)

There's no reason to not reuse the buffer and avoid unnecessary allocations.
@roderickvd

Copy link
Copy Markdown
Member

No worries, good you got this sorted out. It's working fine now for me. Ready to merge everyone?

@JasonLG1979

Copy link
Copy Markdown
Contributor Author

Ready to merge everyone?

If by "everyone" you mean you and I, sure. Because I don't think anyone else cares,lol!!!

@roderickvd

Copy link
Copy Markdown
Member

At least I try to keep up appearances of democracy 😆 and give everyone the chance to look at something that will potentially be used by quite a large share of users.

I was thinking if we needed the chatter of the debug statements on every sink start/stop, and if there even was a case where this would be different between tracks. While I guess it could be done (calling an alsactl restore) in between stop-n-starts, it's unlikely, I'm going to move these messages to trace level and merge this in.

Thanks!

@roderickvd
roderickvd merged commit 68bec41 into librespot-org:dev Jul 6, 2021
@JasonLG1979

JasonLG1979 commented Jul 6, 2021

Copy link
Copy Markdown
Contributor Author

I was thinking if we needed the chatter of the debug statements on every sink start/stop, and if there even was a case where this would be different between tracks. While I guess it could be done (calling an alsactl restore) in between stop-n-starts, it's unlikely, I'm going to move these messages to trace level and merge this in.

That's fine. I just wanted it to be easily available to users to aid in debugging in issues. Trace works.

@JasonLG1979
JasonLG1979 deleted the reuse-buffer-alsa-backend branch July 6, 2021 16:29
@JasonLG1979

Copy link
Copy Markdown
Contributor Author

I was thinking if we needed the chatter of the debug statements on every sink start/stop, and if there even was a case where this would be different between tracks. While I guess it could be done (calling an alsactl restore) in between stop-n-starts, it's unlikely, I'm going to move these messages to trace level and merge this in.

In the future if Spotify were to ever offer tracks in different formats, channel counts, and sample rates there's a possibility that basically everything could change in between tracks. If that happens we will HAVE to stop, start between tracks. That will make gapless playback between different spec(?) tracks a PINTA. Format would really only change though maybe when lossless goes live and someone wanted to passthrough the PCM audio unmolested. When lossless goes live that really should be an option. As in being able to bypass all audio processing in librespot and go straight from the FLAC decoder to the backends.

At some point if track specs were to change in between tracks we will have to implement some form of resampling so we can still output a consistent format and sampling rate no matter what is thrown at us to avoid situations where librespot mysteriously (as far as the user is concerned) crashes when it plays certain tracks but otherwise works just fine. Resampling would also make gapless easier in that situation.

All of this is just pure musing and speculation though. As it is now realistically nothing changes for the life of librespot.

@JasonLG1979

Copy link
Copy Markdown
Contributor Author

https://github.com/lrbalt/libsoxr-rs might be worth a look? I'm not sure if it's cross-platform but SoX is pretty well regarded on Linux for resampling.

@roderickvd

Copy link
Copy Markdown
Member

We shortly discussed options for resampling on the now-closed PR with the cpal backend: #786. There is a pretty good native Rust library out there, it just doesn't align with our Vec of samples today.

I'd really like to steer away from such sound processing unless absolutely necessary. Even then, we would be approaching what Rodio is doing and it seems pointless to reinvent the wheel.

From the latest protobufs it seems clear that Spotify HiFi will be using FLAC. Earlier press statements said "CD-quality". Assuming that is 44.1 kHz 16-bit, we'll be fine without a resampler.

I've done some preliminary works on getting Symphonia in as second decoder besides lewton, but my attention has shifted towards the new API first.

@JasonLG1979

Copy link
Copy Markdown
Contributor Author

We shortly discussed options for resampling on the now-closed PR with the cpal backend: #786. There is a pretty good native Rust library out there, it just doesn't align with our Vec of samples today.

I'd really like to steer away from such sound processing unless absolutely necessary. Even then, we would be approaching what Rodio is doing and it seems pointless to reinvent the wheel.

From the latest protobufs it seems clear that Spotify HiFi will be using FLAC. Earlier press statements said "CD-quality". Assuming that is 44.1 kHz 16-bit, we'll be fine without a resampler.

I agree. Avoiding as much processing as possible is the way to go. I was just thinking out loud. It's really one of those "we'll cross that bridge when we get to it" type situations.

I've done some preliminary works on getting Symphonia in as second decoder besides lewton, but my attention has shifted towards the new API first.

Symphonia looks nice at a quick glance. Having just one decoder for multiple containers/formats would be a huge win.

paulfariello pushed a commit to paulfariello/librespot that referenced this pull request Sep 23, 2025
* Reuse the buffer for the life of the Alsa sink
* Don't depend on capacity being exact when sizing the buffer
* Always give the PCM a period's worth of audio even when draining the buffer
* Refactoring and code cleanup
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants