From e12db0c1828e84693d2fca2eaf2292006767d1f4 Mon Sep 17 00:00:00 2001 From: DS Date: Sun, 7 Apr 2024 22:06:34 +0200 Subject: [PATCH] Sounds: Queue more than two buffers if pitch is high (#14515) Pitch changes playback speed. So always enqueuing 2 buffers did not suffice (and it was unnecessary complicated). --- src/client/sound/playing_sound.cpp | 79 +++++++++++++++++++----------- src/client/sound/playing_sound.h | 4 +- src/client/sound/sound_constants.h | 12 +---- 3 files changed, 55 insertions(+), 40 deletions(-) diff --git a/src/client/sound/playing_sound.cpp b/src/client/sound/playing_sound.cpp index 3cc41fc50..622dbdef8 100644 --- a/src/client/sound/playing_sound.cpp +++ b/src/client/sound/playing_sound.cpp @@ -26,6 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "al_extensions.h" #include "debug.h" +#include "sound_constants.h" #include #include @@ -77,33 +78,27 @@ PlayingSound::PlayingSound(ALuint source_id, std::shared_ptr dat warn_if_al_error("when creating non-streaming sound"); } else { - // Start with 2 buffers - ALuint buf_ids[2]; + // Start with first buffer - // If m_next_sample_pos >= len_samples (happens only if not looped), one - // or both of buf_ids will be 0. Queuing 0 is a NOP. + // If m_next_sample_pos >= len_samples (happens only if not looped), buf0 + // will be 0. Queuing 0 is a NOP. auto [buf0, buf0_end, offset_in_buf0] = m_data->getOrLoadBufferAt(m_next_sample_pos); - buf_ids[0] = buf0; m_next_sample_pos = buf0_end; - if (m_looping && m_next_sample_pos == len_samples) - m_next_sample_pos = 0; - - auto [buf1, buf1_end, offset_in_buf1] = m_data->getOrLoadBufferAt(m_next_sample_pos); - buf_ids[1] = buf1; - m_next_sample_pos = buf1_end; - assert(offset_in_buf1 == 0); - - alSourceQueueBuffers(m_source_id, 2, buf_ids); + alSourceQueueBuffers(m_source_id, 1, &buf0); alSourcei(m_source_id, AL_SAMPLE_OFFSET, offset_in_buf0); - // We can't use AL_LOOPING because more buffers are queued later - // looping is therefore done manually + // We can't use AL_LOOPING because more buffers are queued later. + // Looping is therefore done manually. + // Sound is not dead if queue runs empty prematurely m_stopped_means_dead = false; warn_if_al_error("when creating streaming sound"); + + // Enqueue more buffers + stepStream(true); } // Set initial pos, volume, pitch @@ -129,23 +124,44 @@ PlayingSound::PlayingSound(ALuint source_id, std::shared_ptr dat setPitch(pitch); } -bool PlayingSound::stepStream() +bool PlayingSound::stepStream(bool playback_speed_changed) { if (isDead()) return false; - // unqueue finished buffers - ALint num_unqueued_bufs = 0; - alGetSourcei(m_source_id, AL_BUFFERS_PROCESSED, &num_unqueued_bufs); - if (num_unqueued_bufs == 0) - return true; - // We always have 2 buffers enqueued at most - SANITY_CHECK(num_unqueued_bufs <= 2); - ALuint unqueued_buffer_ids[2]; - alSourceUnqueueBuffers(m_source_id, num_unqueued_bufs, unqueued_buffer_ids); + // Unqueue finished buffers + ALint num_processed_bufs = 0; + alGetSourcei(m_source_id, AL_BUFFERS_PROCESSED, &num_processed_bufs); + if (num_processed_bufs == 0 && !playback_speed_changed) + return true; // Nothing to do + if (num_processed_bufs > 0) { + ALint num_to_unqueue = num_processed_bufs; + ALuint unqueued_buffer_ids[8]; + while (num_to_unqueue > 8) { + alSourceUnqueueBuffers(m_source_id, 8, unqueued_buffer_ids); + num_to_unqueue -= 8; + } + alSourceUnqueueBuffers(m_source_id, num_to_unqueue, unqueued_buffer_ids); + } - // Fill up again - for (ALint i = 0; i < num_unqueued_bufs; ++i) { + // Find out how many buffers we want to enqueue + f32 pitch = 1.0f; + alGetSourcef(m_source_id, AL_PITCH, &pitch); + ALint num_queued_bufs = 0; + alGetSourcei(m_source_id, AL_BUFFERS_QUEUED, &num_queued_bufs); + // Min. length of untouched buffers + const f32 playback_left = MIN_STREAM_BUFFER_LENGTH * std::max(0, num_queued_bufs - 1); + // Max. time until next stepStream() call, see also [Streaming of sounds] in + // sound_constants.h. + // Multiplied by pitch because pitch makes playback faster than real time. + // (Does not account for doppler effect, if we had that.) + // +0.1 seconds to accommodate hickups. + const f32 playback_until_next_check = (2.0f * STREAM_BIGSTEP_TIME + 0.1f) * pitch; + const f32 playback_to_fill_up = std::max(0.0f, playback_until_next_check - playback_left); + const int num_bufs_to_enqueue = std::ceil(playback_to_fill_up / MIN_STREAM_BUFFER_LENGTH); + + // Fill up + for (int i = 0; i < num_bufs_to_enqueue; ++i) { if (m_next_sample_pos == m_data->m_decode_info.length_samples) { // Reached end if (m_looping) { @@ -256,4 +272,11 @@ f32 PlayingSound::getGain() noexcept return gain; } +void PlayingSound::setPitch(f32 pitch) +{ + alSourcef(m_source_id, AL_PITCH, pitch); + if (isStreaming()) + stepStream(true); +} + } // namespace sound diff --git a/src/client/sound/playing_sound.h b/src/client/sound/playing_sound.h index dcc680e09..0f4c18fcb 100644 --- a/src/client/sound/playing_sound.h +++ b/src/client/sound/playing_sound.h @@ -63,7 +63,7 @@ public: DISABLE_CLASS_COPY(PlayingSound) // return false means streaming finished - bool stepStream(); + bool stepStream(bool playback_speed_changed = false); // retruns true if it wasn't fading already bool fade(f32 step, f32 target_gain) noexcept; @@ -77,7 +77,7 @@ public: f32 getGain() noexcept; - void setPitch(f32 pitch) noexcept { alSourcef(m_source_id, AL_PITCH, pitch); } + void setPitch(f32 pitch); bool isStreaming() const noexcept { return m_data->isStreaming(); } diff --git a/src/client/sound/sound_constants.h b/src/client/sound/sound_constants.h index d55575320..08aa2329b 100644 --- a/src/client/sound/sound_constants.h +++ b/src/client/sound/sound_constants.h @@ -89,14 +89,8 @@ with this program; if not, write to the Free Software Foundation, Inc., * In the worst case, a sound is stepped at the start of one bigstep and in the * end of the next bigstep. So between two stepStream()-calls lie at most * 2 * STREAM_BIGSTEP_TIME seconds. - * As there are always 2 sound buffers enqueued, at least one untouched full buffer - * is still available after the first stepStream(). - * If we take a MIN_STREAM_BUFFER_LENGTH > 2 * STREAM_BIGSTEP_TIME, we can hence - * not run into an empty queue. - * - * The MIN_STREAM_BUFFER_LENGTH needs to be a little bigger because of dtime jitter, - * other sounds that may have taken long to stepStream(), and sounds being played - * faster due to Doppler effect. + * We ensure that there are always enough untouched full buffers left such that + * we do not run into an empty queue in this time period, see stepStream(). * */ @@ -115,8 +109,6 @@ constexpr f32 STREAM_BIGSTEP_TIME = 0.3f; // step duration for the OpenALSoundManager thread, in seconds constexpr f32 SOUNDTHREAD_DTIME = 0.016f; -static_assert(MIN_STREAM_BUFFER_LENGTH > STREAM_BIGSTEP_TIME * 2.0f, - "See [Streaming of sounds]."); static_assert(SOUND_DURATION_MAX_SINGLE >= MIN_STREAM_BUFFER_LENGTH * 2.0f, "There's no benefit in streaming if we can't queue more than 2 buffers.");