From bbc64a2eb5ed8a01324379636b5708e317f39087 Mon Sep 17 00:00:00 2001 From: Desour Date: Thu, 28 Sep 2023 18:20:53 +0200 Subject: [PATCH] Split sound_openal_internal into serval files --- src/client/CMakeLists.txt | 9 +- src/client/sound/al_helpers.cpp | 53 + src/client/sound/al_helpers.h | 118 ++ src/client/sound/ogg_file.cpp | 179 +++ src/client/sound/ogg_file.h | 94 ++ src/client/sound/playing_sound.cpp | 241 ++++ src/client/sound/playing_sound.h | 107 ++ src/client/sound/proxy_sound_manager.cpp | 163 +++ src/client/sound/proxy_sound_manager.h | 71 + src/client/sound/sound_constants.h | 119 ++ src/client/sound/sound_data.cpp | 231 ++++ src/client/sound/sound_data.h | 173 +++ src/client/sound/sound_manager.cpp | 523 ++++++++ src/client/sound/sound_manager.h | 171 +++ src/client/sound/sound_manager_messages.h | 80 ++ src/client/sound/sound_openal.cpp | 4 +- src/client/sound/sound_openal_internal.cpp | 1363 -------------------- src/client/sound/sound_openal_internal.h | 750 ----------- src/client/sound/sound_singleton.cpp | 69 + src/client/sound/sound_singleton.h | 60 + 20 files changed, 2463 insertions(+), 2115 deletions(-) create mode 100644 src/client/sound/al_helpers.cpp create mode 100644 src/client/sound/al_helpers.h create mode 100644 src/client/sound/ogg_file.cpp create mode 100644 src/client/sound/ogg_file.h create mode 100644 src/client/sound/playing_sound.cpp create mode 100644 src/client/sound/playing_sound.h create mode 100644 src/client/sound/proxy_sound_manager.cpp create mode 100644 src/client/sound/proxy_sound_manager.h create mode 100644 src/client/sound/sound_constants.h create mode 100644 src/client/sound/sound_data.cpp create mode 100644 src/client/sound/sound_data.h create mode 100644 src/client/sound/sound_manager.cpp create mode 100644 src/client/sound/sound_manager.h create mode 100644 src/client/sound/sound_manager_messages.h delete mode 100644 src/client/sound/sound_openal_internal.cpp delete mode 100644 src/client/sound/sound_openal_internal.h create mode 100644 src/client/sound/sound_singleton.cpp create mode 100644 src/client/sound/sound_singleton.h diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 2c64f62d2..5ec61795a 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -2,8 +2,15 @@ set(sound_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/sound.cpp) if(USE_SOUND) set(sound_SRCS ${sound_SRCS} + ${CMAKE_CURRENT_SOURCE_DIR}/sound/al_helpers.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/sound/ogg_file.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/sound/playing_sound.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/sound/proxy_sound_manager.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/sound/sound_data.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/sound/sound_manager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/sound/sound_openal.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/sound/sound_openal_internal.cpp) + ${CMAKE_CURRENT_SOURCE_DIR}/sound/sound_singleton.cpp + ) set(SOUND_INCLUDE_DIRS ${OPENAL_INCLUDE_DIR} ${VORBIS_INCLUDE_DIR} diff --git a/src/client/sound/al_helpers.cpp b/src/client/sound/al_helpers.cpp new file mode 100644 index 000000000..3b104a0b9 --- /dev/null +++ b/src/client/sound/al_helpers.cpp @@ -0,0 +1,53 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "al_helpers.h" + +/* + * RAIIALSoundBuffer + */ + +RAIIALSoundBuffer &RAIIALSoundBuffer::operator=(RAIIALSoundBuffer &&other) noexcept +{ + if (&other != this) + reset(other.release()); + return *this; +} + +void RAIIALSoundBuffer::reset(ALuint buf) noexcept +{ + if (m_buffer != 0) { + alDeleteBuffers(1, &m_buffer); + warn_if_al_error("Failed to free sound buffer"); + } + + m_buffer = buf; +} + +RAIIALSoundBuffer RAIIALSoundBuffer::generate() noexcept +{ + ALuint buf; + alGenBuffers(1, &buf); + return RAIIALSoundBuffer(buf); +} diff --git a/src/client/sound/al_helpers.h b/src/client/sound/al_helpers.h new file mode 100644 index 000000000..8e12db673 --- /dev/null +++ b/src/client/sound/al_helpers.h @@ -0,0 +1,118 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "log.h" +#include "util/basic_macros.h" +#include "irr_v3d.h" + +#if defined(_WIN32) + #include + #include + //#include +#elif defined(__APPLE__) + #define OPENAL_DEPRECATED + #include + #include + //#include +#else + #include + #include + #include +#endif + +#include + +inline const char *getAlErrorString(ALenum err) noexcept +{ + switch (err) { + case AL_NO_ERROR: + return "no error"; + case AL_INVALID_NAME: + return "invalid name"; + case AL_INVALID_ENUM: + return "invalid enum"; + case AL_INVALID_VALUE: + return "invalid value"; + case AL_INVALID_OPERATION: + return "invalid operation"; + case AL_OUT_OF_MEMORY: + return "out of memory"; + default: + return ""; + } +} + +inline ALenum warn_if_al_error(const char *desc) +{ + ALenum err = alGetError(); + if (err == AL_NO_ERROR) + return err; + warningstream << "[OpenAL Error] " << desc << ": " << getAlErrorString(err) + << std::endl; + return err; +} + +/** + * Transforms vectors from a left-handed coordinate system to a right-handed one + * and vice-versa. + * (Needed because Minetest uses a left-handed one and OpenAL a right-handed one.) + */ +inline v3f swap_handedness(v3f v) noexcept +{ + return v3f(-v.X, v.Y, v.Z); +} + +/** + * RAII wrapper for openal sound buffers. + */ +struct RAIIALSoundBuffer final +{ + RAIIALSoundBuffer() noexcept = default; + explicit RAIIALSoundBuffer(ALuint buffer) noexcept : m_buffer(buffer) {}; + + ~RAIIALSoundBuffer() noexcept { reset(0); } + + DISABLE_CLASS_COPY(RAIIALSoundBuffer) + + RAIIALSoundBuffer(RAIIALSoundBuffer &&other) noexcept : m_buffer(other.release()) {} + RAIIALSoundBuffer &operator=(RAIIALSoundBuffer &&other) noexcept; + + ALuint get() noexcept { return m_buffer; } + + ALuint release() noexcept { return std::exchange(m_buffer, 0); } + + void reset(ALuint buf) noexcept; + + static RAIIALSoundBuffer generate() noexcept; + +private: + // According to openal specification: + // > Deleting buffer name 0 is a legal NOP. + // + // and: + // > [...] the NULL buffer (i.e., 0) which can always be queued. + ALuint m_buffer = 0; +}; diff --git a/src/client/sound/ogg_file.cpp b/src/client/sound/ogg_file.cpp new file mode 100644 index 000000000..729cf7d53 --- /dev/null +++ b/src/client/sound/ogg_file.cpp @@ -0,0 +1,179 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "ogg_file.h" + +#include // memcpy + +/* + * OggVorbisBufferSource struct + */ + +size_t OggVorbisBufferSource::read_func(void *ptr, size_t size, size_t nmemb, + void *datasource) noexcept +{ + OggVorbisBufferSource *s = (OggVorbisBufferSource *)datasource; + size_t copied_size = MYMIN(s->buf.size() - s->cur_offset, size); + memcpy(ptr, s->buf.data() + s->cur_offset, copied_size); + s->cur_offset += copied_size; + return copied_size; +} + +int OggVorbisBufferSource::seek_func(void *datasource, ogg_int64_t offset, int whence) noexcept +{ + OggVorbisBufferSource *s = (OggVorbisBufferSource *)datasource; + if (whence == SEEK_SET) { + if (offset < 0 || (size_t)offset > s->buf.size()) { + // offset out of bounds + return -1; + } + s->cur_offset = offset; + return 0; + } else if (whence == SEEK_CUR) { + if ((size_t)MYMIN(-offset, 0) > s->cur_offset + || s->cur_offset + offset > s->buf.size()) { + // offset out of bounds + return -1; + } + s->cur_offset += offset; + return 0; + } else if (whence == SEEK_END) { + if (offset > 0 || (size_t)-offset > s->buf.size()) { + // offset out of bounds + return -1; + } + s->cur_offset = s->buf.size() - offset; + return 0; + } + return -1; +} + +int OggVorbisBufferSource::close_func(void *datasource) noexcept +{ + auto s = reinterpret_cast(datasource); + delete s; + return 0; +} + +long OggVorbisBufferSource::tell_func(void *datasource) noexcept +{ + OggVorbisBufferSource *s = (OggVorbisBufferSource *)datasource; + return s->cur_offset; +} + +const ov_callbacks OggVorbisBufferSource::s_ov_callbacks = { + &OggVorbisBufferSource::read_func, + &OggVorbisBufferSource::seek_func, + &OggVorbisBufferSource::close_func, + &OggVorbisBufferSource::tell_func +}; + +/* + * RAIIOggFile struct + */ + +std::optional RAIIOggFile::getDecodeInfo(const std::string &filename_for_logging) +{ + OggFileDecodeInfo ret; + + vorbis_info *pInfo = ov_info(&m_file, -1); + if (!pInfo) + return std::nullopt; + + ret.name_for_logging = filename_for_logging; + + if (pInfo->channels == 1) { + ret.is_stereo = false; + ret.format = AL_FORMAT_MONO16; + ret.bytes_per_sample = 2; + } else if (pInfo->channels == 2) { + ret.is_stereo = true; + ret.format = AL_FORMAT_STEREO16; + ret.bytes_per_sample = 4; + } else { + warningstream << "Audio: Can't decode. Sound is neither mono nor stereo: " + << ret.name_for_logging << std::endl; + return std::nullopt; + } + + ret.freq = pInfo->rate; + + ret.length_samples = static_cast(ov_pcm_total(&m_file, -1)); + ret.length_seconds = static_cast(ov_time_total(&m_file, -1)); + + return ret; +} + +RAIIALSoundBuffer RAIIOggFile::loadBuffer(const OggFileDecodeInfo &decode_info, + ALuint pcm_start, ALuint pcm_end) +{ + constexpr int endian = 0; // 0 for Little-Endian, 1 for Big-Endian + constexpr int word_size = 2; // we use s16 samples + constexpr int word_signed = 1; // ^ + + // seek + if (ov_pcm_tell(&m_file) != pcm_start) { + if (ov_pcm_seek(&m_file, pcm_start) != 0) { + warningstream << "Audio: Error decoding (could not seek) " + << decode_info.name_for_logging << std::endl; + return RAIIALSoundBuffer(); + } + } + + const size_t size = static_cast(pcm_end - pcm_start) + * decode_info.bytes_per_sample; + + std::unique_ptr snd_buffer(new char[size]); + + // read size bytes + size_t read_count = 0; + int bitStream; + while (read_count < size) { + // Read up to a buffer's worth of decoded sound data + long num_bytes = ov_read(&m_file, &snd_buffer[read_count], size - read_count, + endian, word_size, word_signed, &bitStream); + + if (num_bytes <= 0) { + warningstream << "Audio: Error decoding " + << decode_info.name_for_logging << std::endl; + return RAIIALSoundBuffer(); + } + + read_count += num_bytes; + } + + // load buffer to openal + RAIIALSoundBuffer snd_buffer_id = RAIIALSoundBuffer::generate(); + alBufferData(snd_buffer_id.get(), decode_info.format, &(snd_buffer[0]), size, + decode_info.freq); + + ALenum error = alGetError(); + if (error != AL_NO_ERROR) { + warningstream << "Audio: OpenAL error: " << getAlErrorString(error) + << "preparing sound buffer for sound \"" + << decode_info.name_for_logging << "\"" << std::endl; + } + + return snd_buffer_id; +} diff --git a/src/client/sound/ogg_file.h b/src/client/sound/ogg_file.h new file mode 100644 index 000000000..fe41239e0 --- /dev/null +++ b/src/client/sound/ogg_file.h @@ -0,0 +1,94 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "al_helpers.h" +#include +#include +#include + +/** + * For vorbisfile to read from our buffer instead of from a file. + */ +struct OggVorbisBufferSource { + std::string buf; + size_t cur_offset = 0; + + static size_t read_func(void *ptr, size_t size, size_t nmemb, void *datasource) noexcept; + static int seek_func(void *datasource, ogg_int64_t offset, int whence) noexcept; + static int close_func(void *datasource) noexcept; + static long tell_func(void *datasource) noexcept; + + static const ov_callbacks s_ov_callbacks; +}; + +/** + * Metadata of an Ogg-Vorbis file, used for decoding. + * We query this information once and store it in this struct. + */ +struct OggFileDecodeInfo { + std::string name_for_logging; + bool is_stereo; + ALenum format; // AL_FORMAT_MONO16 or AL_FORMAT_STEREO16 + size_t bytes_per_sample; + ALsizei freq; + ALuint length_samples = 0; + f32 length_seconds = 0.0f; +}; + +/** + * RAII wrapper for OggVorbis_File. + */ +struct RAIIOggFile { + bool m_needs_clear = false; + OggVorbis_File m_file; + + RAIIOggFile() = default; + + DISABLE_CLASS_COPY(RAIIOggFile) + + ~RAIIOggFile() noexcept + { + if (m_needs_clear) + ov_clear(&m_file); + } + + OggVorbis_File *get() { return &m_file; } + + std::optional getDecodeInfo(const std::string &filename_for_logging); + + /** + * Main function for loading ogg vorbis sounds. + * Loads exactly the specified interval of PCM-data, and creates an OpenAL + * buffer with it. + * + * @param decode_info Cached meta information of the file. + * @param pcm_start First sample in the interval. + * @param pcm_end One after last sample of the interval (=> exclusive). + * @return An AL sound buffer, or a 0-buffer on failure. + */ + RAIIALSoundBuffer loadBuffer(const OggFileDecodeInfo &decode_info, ALuint pcm_start, + ALuint pcm_end); +}; diff --git a/src/client/sound/playing_sound.cpp b/src/client/sound/playing_sound.cpp new file mode 100644 index 000000000..033020e9b --- /dev/null +++ b/src/client/sound/playing_sound.cpp @@ -0,0 +1,241 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "playing_sound.h" + +#include "debug.h" +#include +#include + +PlayingSound::PlayingSound(ALuint source_id, std::shared_ptr data, + bool loop, f32 volume, f32 pitch, f32 start_time, + const std::optional> &pos_vel_opt) + : m_source_id(source_id), m_data(std::move(data)), m_looping(loop), + m_is_positional(pos_vel_opt.has_value()) +{ + // Calculate actual start_time (see lua_api.txt for specs) + f32 len_seconds = m_data->m_decode_info.length_seconds; + f32 len_samples = m_data->m_decode_info.length_samples; + if (!m_looping) { + if (start_time < 0.0f) { + start_time = std::fmax(start_time + len_seconds, 0.0f); + } else if (start_time >= len_seconds) { + // No sound + m_next_sample_pos = len_samples; + return; + } + } else { + // Modulo offset to be within looping time + start_time = start_time - std::floor(start_time / len_seconds) * len_seconds; + } + + // Queue first buffers + + m_next_sample_pos = std::min((start_time / len_seconds) * len_samples, len_samples); + + if (m_looping && m_next_sample_pos == len_samples) + m_next_sample_pos = 0; + + if (!m_data->isStreaming()) { + // If m_next_sample_pos >= len_samples, buf will be 0, and setting it as + // AL_BUFFER is a NOP (source stays AL_UNDETERMINED). => No sound will be + // played. + + auto [buf, buf_end, offset_in_buf] = m_data->getOrLoadBufferAt(m_next_sample_pos); + m_next_sample_pos = buf_end; + + alSourcei(m_source_id, AL_BUFFER, buf); + alSourcei(m_source_id, AL_SAMPLE_OFFSET, offset_in_buf); + + alSourcei(m_source_id, AL_LOOPING, m_looping ? AL_TRUE : AL_FALSE); + + warn_if_al_error("when creating non-streaming sound"); + + } else { + // Start with 2 buffers + ALuint buf_ids[2]; + + // 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. + + 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); + 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 + + m_stopped_means_dead = false; + + warn_if_al_error("when creating streaming sound"); + } + + // Set initial pos, volume, pitch + if (m_is_positional) { + updatePosVel(pos_vel_opt->first, pos_vel_opt->second); + } else { + // Make position-less + alSourcei(m_source_id, AL_SOURCE_RELATIVE, true); + alSource3f(m_source_id, AL_POSITION, 0.0f, 0.0f, 0.0f); + alSource3f(m_source_id, AL_VELOCITY, 0.0f, 0.0f, 0.0f); + warn_if_al_error("PlayingSound::PlayingSound at making position-less"); + } + setGain(volume); + setPitch(pitch); +} + +bool PlayingSound::stepStream() +{ + 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); + + // Fill up again + for (ALint i = 0; i < num_unqueued_bufs; ++i) { + if (m_next_sample_pos == m_data->m_decode_info.length_samples) { + // Reached end + if (m_looping) { + m_next_sample_pos = 0; + } else { + m_stopped_means_dead = true; + return false; + } + } + + auto [buf, buf_end, offset_in_buf] = m_data->getOrLoadBufferAt(m_next_sample_pos); + m_next_sample_pos = buf_end; + assert(offset_in_buf == 0); + + alSourceQueueBuffers(m_source_id, 1, &buf); + + // Start again if queue was empty and resulted in stop + if (getState() == AL_STOPPED) { + play(); + warningstream << "PlayingSound::stepStream: Sound queue ran empty for \"" + << m_data->m_decode_info.name_for_logging << "\"" << std::endl; + } + } + + return true; +} + +bool PlayingSound::fade(f32 step, f32 target_gain) noexcept +{ + bool already_fading = m_fade_state.has_value(); + + target_gain = MYMAX(target_gain, 0.0f); // 0.0f if nan + step = target_gain - getGain() > 0.0f ? std::abs(step) : -std::abs(step); + + m_fade_state = FadeState{step, target_gain}; + + return !already_fading; +} + +bool PlayingSound::doFade(f32 dtime) noexcept +{ + if (!m_fade_state || isDead()) + return false; + + FadeState &fade = *m_fade_state; + assert(fade.step != 0.0f); + + f32 current_gain = getGain(); + current_gain += fade.step * dtime; + + if (fade.step < 0.0f) + current_gain = std::max(current_gain, fade.target_gain); + else + current_gain = std::min(current_gain, fade.target_gain); + + if (current_gain <= 0.0f) { + // stop sound + m_stopped_means_dead = true; + alSourceStop(m_source_id); + + m_fade_state = std::nullopt; + return false; + } + + setGain(current_gain); + + if (current_gain == fade.target_gain) { + m_fade_state = std::nullopt; + return false; + } else { + return true; + } +} + +void PlayingSound::updatePosVel(const v3f &pos, const v3f &vel) noexcept +{ + alSourcei(m_source_id, AL_SOURCE_RELATIVE, false); + alSource3f(m_source_id, AL_POSITION, pos.X, pos.Y, pos.Z); + alSource3f(m_source_id, AL_VELOCITY, vel.X, vel.Y, vel.Z); + // Using alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED) and setting reference + // distance to clamp gain at <1 node distance avoids excessive volume when + // closer. + alSourcef(m_source_id, AL_REFERENCE_DISTANCE, 1.0f); + + warn_if_al_error("PlayingSound::updatePosVel"); +} + +void PlayingSound::setGain(f32 gain) noexcept +{ + // AL_REFERENCE_DISTANCE was once reduced from 3 nodes to 1 node. + // We compensate this by multiplying the volume by 3. + if (m_is_positional) + gain *= 3.0f; + + alSourcef(m_source_id, AL_GAIN, gain); +} + +f32 PlayingSound::getGain() noexcept +{ + ALfloat gain; + alGetSourcef(m_source_id, AL_GAIN, &gain); + // Same as above, but inverse. + if (m_is_positional) + gain *= 1.0f/3.0f; + return gain; +} diff --git a/src/client/sound/playing_sound.h b/src/client/sound/playing_sound.h new file mode 100644 index 000000000..066e1af42 --- /dev/null +++ b/src/client/sound/playing_sound.h @@ -0,0 +1,107 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "sound_data.h" + +/** + * A sound that is currently played. + * Can be streaming. + * Can be fading. + */ +class PlayingSound final +{ + struct FadeState { + f32 step; + f32 target_gain; + }; + + ALuint m_source_id; + std::shared_ptr m_data; + ALuint m_next_sample_pos = 0; + bool m_looping; + bool m_is_positional; + bool m_stopped_means_dead = true; + std::optional m_fade_state = std::nullopt; + +public: + PlayingSound(ALuint source_id, std::shared_ptr data, bool loop, + f32 volume, f32 pitch, f32 start_time, + const std::optional> &pos_vel_opt); + + ~PlayingSound() noexcept + { + alDeleteSources(1, &m_source_id); + } + + DISABLE_CLASS_COPY(PlayingSound) + + // return false means streaming finished + bool stepStream(); + + // retruns true if it wasn't fading already + bool fade(f32 step, f32 target_gain) noexcept; + + // returns true if more fade is needed later + bool doFade(f32 dtime) noexcept; + + void updatePosVel(const v3f &pos, const v3f &vel) noexcept; + + void setGain(f32 gain) noexcept; + + f32 getGain() noexcept; + + void setPitch(f32 pitch) noexcept { alSourcef(m_source_id, AL_PITCH, pitch); } + + bool isStreaming() const noexcept { return m_data->isStreaming(); } + + void play() noexcept { alSourcePlay(m_source_id); } + + // returns one of AL_INITIAL, AL_PLAYING, AL_PAUSED, AL_STOPPED + ALint getState() noexcept + { + ALint state; + alGetSourcei(m_source_id, AL_SOURCE_STATE, &state); + return state; + } + + bool isDead() noexcept + { + // streaming sounds can (but should not) stop because the queue runs empty + return m_stopped_means_dead && getState() == AL_STOPPED; + } + + void pause() noexcept + { + // this is a NOP if state != AL_PLAYING + alSourcePause(m_source_id); + } + + void resume() noexcept + { + if (getState() == AL_PAUSED) + play(); + } +}; diff --git a/src/client/sound/proxy_sound_manager.cpp b/src/client/sound/proxy_sound_manager.cpp new file mode 100644 index 000000000..810776502 --- /dev/null +++ b/src/client/sound/proxy_sound_manager.cpp @@ -0,0 +1,163 @@ +/* +Minetest +Copyright (C) 2023 DS + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "proxy_sound_manager.h" + +#include "filesys.h" + +ProxySoundManager::MsgResult ProxySoundManager::handleMsg(SoundManagerMsgToProxy &&msg) +{ + using namespace sound_manager_messages_to_proxy; + + return std::visit([&](auto &&msg) { + using T = std::decay_t; + + if constexpr (std::is_same_v) + return MsgResult::Empty; + else if constexpr (std::is_same_v) + reportRemovedSound(msg.id); + else if constexpr (std::is_same_v) + return MsgResult::Stopped; + + return MsgResult::Ok; + }, + std::move(msg)); +} + +ProxySoundManager::~ProxySoundManager() +{ + if (m_sound_manager.isRunning()) { + send(sound_manager_messages_to_mgr::PleaseStop{}); + + // recv until it stopped + auto recv = [&] { + return m_sound_manager.m_queue_to_proxy.pop_frontNoEx(); + }; + + while (true) { + if (handleMsg(recv()) == MsgResult::Stopped) + break; + } + + // join + m_sound_manager.stop(); + SANITY_CHECK(m_sound_manager.wait()); + } +} + +void ProxySoundManager::step(f32 dtime) +{ + auto recv = [&] { + return m_sound_manager.m_queue_to_proxy.pop_frontNoEx(0); + }; + + while (true) { + MsgResult res = handleMsg(recv()); + if (res == MsgResult::Empty) + break; + else if (res == MsgResult::Stopped) + throw std::runtime_error("OpenALSoundManager stopped unexpectedly"); + } +} + +void ProxySoundManager::pauseAll() +{ + send(sound_manager_messages_to_mgr::PauseAll{}); +} + +void ProxySoundManager::resumeAll() +{ + send(sound_manager_messages_to_mgr::ResumeAll{}); +} + +void ProxySoundManager::updateListener(const v3f &pos_, const v3f &vel_, + const v3f &at_, const v3f &up_) +{ + send(sound_manager_messages_to_mgr::UpdateListener{pos_, vel_, at_, up_}); +} + +void ProxySoundManager::setListenerGain(f32 gain) +{ + send(sound_manager_messages_to_mgr::SetListenerGain{gain}); +} + +bool ProxySoundManager::loadSoundFile(const std::string &name, + const std::string &filepath) +{ + // do not add twice + if (m_known_sound_names.count(name) != 0) + return false; + + // coarse check + if (!fs::IsFile(filepath)) + return false; + + send(sound_manager_messages_to_mgr::LoadSoundFile{name, filepath}); + + m_known_sound_names.insert(name); + return true; +} + +bool ProxySoundManager::loadSoundData(const std::string &name, std::string &&filedata) +{ + // do not add twice + if (m_known_sound_names.count(name) != 0) + return false; + + send(sound_manager_messages_to_mgr::LoadSoundData{name, std::move(filedata)}); + + m_known_sound_names.insert(name); + return true; +} + +void ProxySoundManager::addSoundToGroup(const std::string &sound_name, + const std::string &group_name) +{ + send(sound_manager_messages_to_mgr::AddSoundToGroup{sound_name, group_name}); +} + +void ProxySoundManager::playSound(sound_handle_t id, const SoundSpec &spec) +{ + if (id == 0) + id = allocateId(1); + send(sound_manager_messages_to_mgr::PlaySound{id, spec}); +} + +void ProxySoundManager::playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos_, + const v3f &vel_) +{ + if (id == 0) + id = allocateId(1); + send(sound_manager_messages_to_mgr::PlaySoundAt{id, spec, pos_, vel_}); +} + +void ProxySoundManager::stopSound(sound_handle_t sound) +{ + send(sound_manager_messages_to_mgr::StopSound{sound}); +} + +void ProxySoundManager::fadeSound(sound_handle_t soundid, f32 step, f32 target_gain) +{ + send(sound_manager_messages_to_mgr::FadeSound{soundid, step, target_gain}); +} + +void ProxySoundManager::updateSoundPosVel(sound_handle_t sound, const v3f &pos_, const v3f &vel_) +{ + send(sound_manager_messages_to_mgr::UpdateSoundPosVel{sound, pos_, vel_}); +} diff --git a/src/client/sound/proxy_sound_manager.h b/src/client/sound/proxy_sound_manager.h new file mode 100644 index 000000000..4f376d635 --- /dev/null +++ b/src/client/sound/proxy_sound_manager.h @@ -0,0 +1,71 @@ +/* +Minetest +Copyright (C) 2023 DS + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "sound_manager.h" + +/* + * The public ISoundManager interface + */ + +class ProxySoundManager final : public ISoundManager +{ + OpenALSoundManager m_sound_manager; + // sound names from loadSoundData and loadSoundFile + std::unordered_set m_known_sound_names; + + void send(SoundManagerMsgToMgr msg) + { + m_sound_manager.m_queue_to_mgr.push_back(std::move(msg)); + } + + enum class MsgResult { Ok, Empty, Stopped}; + MsgResult handleMsg(SoundManagerMsgToProxy &&msg); + +public: + ProxySoundManager(SoundManagerSingleton *smg, + std::unique_ptr fallback_path_provider) : + m_sound_manager(smg, std::move(fallback_path_provider)) + { + m_sound_manager.start(); + } + + ~ProxySoundManager() override; + + /* Interface */ + + void step(f32 dtime) override; + void pauseAll() override; + void resumeAll() override; + + void updateListener(const v3f &pos_, const v3f &vel_, const v3f &at_, const v3f &up_) override; + void setListenerGain(f32 gain) override; + + bool loadSoundFile(const std::string &name, const std::string &filepath) override; + bool loadSoundData(const std::string &name, std::string &&filedata) override; + void addSoundToGroup(const std::string &sound_name, const std::string &group_name) override; + + void playSound(sound_handle_t id, const SoundSpec &spec) override; + void playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos_, + const v3f &vel_) override; + void stopSound(sound_handle_t sound) override; + void fadeSound(sound_handle_t soundid, f32 step, f32 target_gain) override; + void updateSoundPosVel(sound_handle_t sound, const v3f &pos_, const v3f &vel_) override; +}; diff --git a/src/client/sound/sound_constants.h b/src/client/sound/sound_constants.h new file mode 100644 index 000000000..b16f5204f --- /dev/null +++ b/src/client/sound/sound_constants.h @@ -0,0 +1,119 @@ +/* +Minetest +Copyright (C) 2022 DS + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +/* + * + * The coordinate space for sounds (sound-space): + * ---------------------------------------------- + * + * * The functions from ISoundManager (see sound.h) take spatial vectors in node-space. + * * All other `v3f`s here are, if not told otherwise, in sound-space, which is + * defined as node-space mirrored along the x-axis. + * (This is needed because OpenAL uses a right-handed coordinate system.) + * * Use `swap_handedness()` from `al_helpers.h` to convert between those two + * coordinate spaces. + * + * + * How sounds are loaded: + * ---------------------- + * + * * Step 1: + * `loadSoundFile` or `loadSoundFile` is called. This adds an unopen sound with + * the given name to `m_sound_datas_unopen`. + * Unopen / lazy sounds (`ISoundDataUnopen`) are ogg-vorbis files that we did not yet + * start to decode. (Decoding an unopen sound does not fail under normal circumstances + * (because we check whether the file exists at least), if it does fail anyways, + * we should notify the user.) + * * Step 2: + * `addSoundToGroup` is called, to add the name from step 1 to a group. If the + * group does not yet exist, a new one is created. A group can later be played. + * (The mapping is stored in `m_sound_groups`.) + * * Step 3: + * `playSound` or `playSoundAt` is called. + * * Step 3.1: + * If the group with the name `spec.name` does not exist, and `spec.use_local_fallback` + * is true, a new group is created using the user's sound-pack. + * * Step 3.2: + * We choose one random sound name from the given group. + * * Step 3.3: + * We open the sound (see `openSingleSound`). + * If the sound is already open (in `m_sound_datas_open`), we take that one. + * Otherwise we open it by calling `ISoundDataUnopen::open`. We choose (by + * sound length), whether it's a single-buffer (`SoundDataOpenBuffer`) or + * streamed (`SoundDataOpenStream`) sound. + * Single-buffer sounds are always completely loaded. Streamed sounds can be + * partially loaded. + * The sound is erased from `m_sound_datas_unopen` and added to `m_sound_datas_open`. + * Open sounds are kept forever. + * * Step 3.4: + * We create the new `PlayingSound`. It has a `shared_ptr` to its open sound. + * If the open sound is streaming, the playing sound needs to be stepped using + * `PlayingSound::stepStream` for enqueuing buffers. For this purpose, the sound + * is added to `m_sounds_streaming` (as `weak_ptr`). + * If the sound is fading, it is added to `m_sounds_fading` for regular fade-stepping. + * The sound is also added to `m_sounds_playing`, so that one can access it + * via its sound handle. + * * Step 4: + * Streaming sounds are updated. For details see [Streaming of sounds]. + * * Step 5: + * At deinitialization, we can just let the destructors do their work. + * Sound sources are deleted (and with this also stopped) by ~PlayingSound. + * Buffers can't be deleted while sound sources using them exist, because + * PlayingSound has a shared_ptr to its ISoundData. + * + * + * Streaming of sounds: + * -------------------- + * + * In each "bigstep", all streamed sounds are stepStream()ed. This means a + * sound can be stepped at any point in time in the bigstep's interval. + * + * 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. + * + */ + +// constants + +// in seconds +constexpr f32 REMOVE_DEAD_SOUNDS_INTERVAL = 2.0f; +// maximum length in seconds that a sound can have without being streamed +constexpr f32 SOUND_DURATION_MAX_SINGLE = 3.0f; +// minimum time in seconds of a single buffer in a streamed sound +constexpr f32 MIN_STREAM_BUFFER_LENGTH = 1.0f; +// duration in seconds of one bigstep +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."); diff --git a/src/client/sound/sound_data.cpp b/src/client/sound/sound_data.cpp new file mode 100644 index 000000000..258bf8836 --- /dev/null +++ b/src/client/sound/sound_data.cpp @@ -0,0 +1,231 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "sound_data.h" + +#include "sound_constants.h" + +/* + * ISoundDataOpen struct + */ + +std::shared_ptr ISoundDataOpen::fromOggFile(std::unique_ptr oggfile, + const std::string &filename_for_logging) +{ + // Get some information about the OGG file + std::optional decode_info = oggfile->getDecodeInfo(filename_for_logging); + if (!decode_info.has_value()) { + warningstream << "Audio: Error decoding " + << filename_for_logging << std::endl; + return nullptr; + } + + // use duration (in seconds) to decide whether to load all at once or to stream + if (decode_info->length_seconds <= SOUND_DURATION_MAX_SINGLE) { + return std::make_shared(std::move(oggfile), *decode_info); + } else { + return std::make_shared(std::move(oggfile), *decode_info); + } +} + +/* + * SoundDataUnopenBuffer struct + */ + +std::shared_ptr SoundDataUnopenBuffer::open(const std::string &sound_name) && +{ + // load from m_buffer + + auto oggfile = std::make_unique(); + + auto buffer_source = std::make_unique(); + buffer_source->buf = std::move(m_buffer); + + oggfile->m_needs_clear = true; + if (ov_open_callbacks(buffer_source.release(), oggfile->get(), nullptr, 0, + OggVorbisBufferSource::s_ov_callbacks) != 0) { + warningstream << "Audio: Error opening " << sound_name << " for decoding" + << std::endl; + return nullptr; + } + + return ISoundDataOpen::fromOggFile(std::move(oggfile), sound_name); +} + +/* + * SoundDataUnopenFile struct + */ + +std::shared_ptr SoundDataUnopenFile::open(const std::string &sound_name) && +{ + // load from file at m_path + + auto oggfile = std::make_unique(); + + if (ov_fopen(m_path.c_str(), oggfile->get()) != 0) { + warningstream << "Audio: Error opening " << m_path << " for decoding" + << std::endl; + return nullptr; + } + oggfile->m_needs_clear = true; + + return ISoundDataOpen::fromOggFile(std::move(oggfile), sound_name); +} + +/* + * SoundDataOpenBuffer struct + */ + +SoundDataOpenBuffer::SoundDataOpenBuffer(std::unique_ptr oggfile, + const OggFileDecodeInfo &decode_info) : ISoundDataOpen(decode_info) +{ + m_buffer = oggfile->loadBuffer(m_decode_info, 0, m_decode_info.length_samples); + if (m_buffer.get() == 0) { + warningstream << "SoundDataOpenBuffer: Failed to load sound \"" + << m_decode_info.name_for_logging << "\"" << std::endl; + return; + } +} + +/* + * SoundDataOpenStream struct + */ + +SoundDataOpenStream::SoundDataOpenStream(std::unique_ptr oggfile, + const OggFileDecodeInfo &decode_info) : + ISoundDataOpen(decode_info), m_oggfile(std::move(oggfile)) +{ + // do nothing here. buffers are loaded at getOrLoadBufferAt +} + +std::tuple SoundDataOpenStream::getOrLoadBufferAt(ALuint offset) +{ + if (offset >= m_decode_info.length_samples) + return {0, m_decode_info.length_samples, 0}; + + // find the right-most ContiguousBuffers, such that `m_start <= offset` + // equivalent: the first element from the right such that `!(m_start > offset)` + // (from the right, `offset` is a lower bound to the `m_start`s) + auto lower_rit = std::lower_bound(m_bufferss.rbegin(), m_bufferss.rend(), offset, + [](const ContiguousBuffers &bufs, ALuint offset) { + return bufs.m_start > offset; + }); + + if (lower_rit != m_bufferss.rend()) { + std::vector &bufs = lower_rit->m_buffers; + // find the left-most SoundBufferUntil, such that `m_end > offset` + // equivalent: the first element from the left such that `m_end > offset` + // (returns first element where comp gives true) + auto upper_it = std::upper_bound(bufs.begin(), bufs.end(), offset, + [](ALuint offset, const SoundBufferUntil &buf) { + return offset < buf.m_end; + }); + + if (upper_it != bufs.end()) { + ALuint start = upper_it == bufs.begin() ? lower_rit->m_start + : (upper_it - 1)->m_end; + return {upper_it->m_buffer.get(), upper_it->m_end, offset - start}; + } + } + + // no loaded buffer starts before or at `offset` + // or no loaded buffer (that starts before or at `offset`) ends after `offset` + + // lower_rit, but not reverse and 1 farther + auto after_it = m_bufferss.begin() + (m_bufferss.rend() - lower_rit); + + return loadBufferAt(offset, after_it); +} + +std::tuple SoundDataOpenStream::loadBufferAt(ALuint offset, + std::vector::iterator after_it) +{ + bool has_before = after_it != m_bufferss.begin(); + bool has_after = after_it != m_bufferss.end(); + + ALuint end_before = has_before ? (after_it - 1)->m_buffers.back().m_end : 0; + ALuint start_after = has_after ? after_it->m_start : m_decode_info.length_samples; + + const ALuint min_buf_len_samples = m_decode_info.freq * MIN_STREAM_BUFFER_LENGTH; + + // + // 1) Find the actual start and end of the new buffer + // + + ALuint new_buf_start = offset; + ALuint new_buf_end = offset + min_buf_len_samples; + + // Don't load into next buffer, or past the end + if (new_buf_end > start_after) { + new_buf_end = start_after; + // Also move start (for min buf size) (but not *into* previous buffer) + if (new_buf_end - new_buf_start < min_buf_len_samples) { + new_buf_start = std::max( + end_before, + new_buf_end < min_buf_len_samples ? 0 + : new_buf_end - min_buf_len_samples + ); + } + } + + // Widen if space to right or left is smaller than min buf size + if (new_buf_start - end_before < min_buf_len_samples) + new_buf_start = end_before; + if (start_after - new_buf_end < min_buf_len_samples) + new_buf_end = start_after; + + // + // 2) Load [new_buf_start, new_buf_end) + // + + // If it fails, we get a 0-buffer. we store it and won't try loading again + RAIIALSoundBuffer new_buf = m_oggfile->loadBuffer(m_decode_info, new_buf_start, + new_buf_end); + + // + // 3) Insert before after_it + // + + // Choose ContiguousBuffers to add the new SoundBufferUntil into: + // * `after_it - 1` (=before) if existent and if there's no space between its + // last buffer and the new buffer + // * A new ContiguousBuffers otherwise + auto it = has_before && new_buf_start == end_before ? after_it - 1 + : m_bufferss.insert(after_it, ContiguousBuffers{new_buf_start, {}}); + + // Add the new SoundBufferUntil + size_t new_buf_i = it->m_buffers.size(); + it->m_buffers.push_back(SoundBufferUntil{new_buf_end, std::move(new_buf)}); + + if (has_after && new_buf_end == start_after) { + // Merge after into my ContiguousBuffers + auto &bufs = it->m_buffers; + auto &bufs_after = (it + 1)->m_buffers; + bufs.insert(bufs.end(), std::make_move_iterator(bufs_after.begin()), + std::make_move_iterator(bufs_after.end())); + it = m_bufferss.erase(it + 1) - 1; + } + + return {it->m_buffers[new_buf_i].m_buffer.get(), new_buf_end, offset - new_buf_start}; +} diff --git a/src/client/sound/sound_data.h b/src/client/sound/sound_data.h new file mode 100644 index 000000000..f2c06b939 --- /dev/null +++ b/src/client/sound/sound_data.h @@ -0,0 +1,173 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "ogg_file.h" +#include +#include + +/** + * Stores sound pcm data buffers. + */ +struct ISoundDataOpen +{ + OggFileDecodeInfo m_decode_info; + + explicit ISoundDataOpen(const OggFileDecodeInfo &decode_info) : + m_decode_info(decode_info) {} + + virtual ~ISoundDataOpen() = default; + + /** + * Iff the data is streaming, there is more than one buffer. + * @return Whether it's streaming data. + */ + virtual bool isStreaming() const noexcept = 0; + + /** + * Load a buffer containing data starting at the given offset. Or just get it + * if it was already loaded. + * + * This function returns multiple values: + * * `buffer`: The OpenAL buffer. + * * `buffer_end`: The offset (in the file) where `buffer` ends (exclusive). + * * `offset_in_buffer`: Offset relative to `buffer`'s start where the requested + * `offset` is. + * `offset_in_buffer == 0` is guaranteed if some loaded buffer ends at + * `offset`. + * + * @param offset The start of the buffer. + * @return `{buffer, buffer_end, offset_in_buffer}` or `{0, sound_data_end, 0}` + * if `offset` is invalid. + */ + virtual std::tuple getOrLoadBufferAt(ALuint offset) = 0; + + static std::shared_ptr fromOggFile(std::unique_ptr oggfile, + const std::string &filename_for_logging); +}; + +/** + * Will be opened lazily when first used. + */ +struct ISoundDataUnopen +{ + virtual ~ISoundDataUnopen() = default; + + // Note: The ISoundDataUnopen is moved (see &&). It is not meant to be kept + // after opening. + virtual std::shared_ptr open(const std::string &sound_name) && = 0; +}; + +/** + * Sound file is in a memory buffer. + */ +struct SoundDataUnopenBuffer final : ISoundDataUnopen +{ + std::string m_buffer; + + explicit SoundDataUnopenBuffer(std::string &&buffer) : m_buffer(std::move(buffer)) {} + + std::shared_ptr open(const std::string &sound_name) && override; +}; + +/** + * Sound file is in file system. + */ +struct SoundDataUnopenFile final : ISoundDataUnopen +{ + std::string m_path; + + explicit SoundDataUnopenFile(const std::string &path) : m_path(path) {} + + std::shared_ptr open(const std::string &sound_name) && override; +}; + +/** + * Non-streaming opened sound data. + * All data is completely loaded in one buffer. + */ +struct SoundDataOpenBuffer final : ISoundDataOpen +{ + RAIIALSoundBuffer m_buffer; + + SoundDataOpenBuffer(std::unique_ptr oggfile, + const OggFileDecodeInfo &decode_info); + + bool isStreaming() const noexcept override { return false; } + + std::tuple getOrLoadBufferAt(ALuint offset) override + { + if (offset >= m_decode_info.length_samples) + return {0, m_decode_info.length_samples, 0}; + return {m_buffer.get(), m_decode_info.length_samples, offset}; + } +}; + +/** + * Streaming opened sound data. + * + * Uses a sorted list of contiguous sound data regions (`ContiguousBuffers`s) for + * efficient seeking. + */ +struct SoundDataOpenStream final : ISoundDataOpen +{ + /** + * An OpenAL buffer that goes until `m_end` (exclusive). + */ + struct SoundBufferUntil final + { + ALuint m_end; + RAIIALSoundBuffer m_buffer; + }; + + /** + * A sorted non-empty vector of contiguous buffers. + * The start (inclusive) of each buffer is the end of its predecessor, or + * `m_start` for the first buffer. + */ + struct ContiguousBuffers final + { + ALuint m_start; + std::vector m_buffers; + }; + + std::unique_ptr m_oggfile; + // A sorted vector of non-overlapping, non-contiguous `ContiguousBuffers`s. + std::vector m_bufferss; + + SoundDataOpenStream(std::unique_ptr oggfile, + const OggFileDecodeInfo &decode_info); + + bool isStreaming() const noexcept override { return true; } + + std::tuple getOrLoadBufferAt(ALuint offset) override; + +private: + // offset must be before after_it's m_start and after (after_it-1)'s last m_end + // new buffer will be inserted into m_bufferss before after_it + // returns same as getOrLoadBufferAt + std::tuple loadBufferAt(ALuint offset, + std::vector::iterator after_it); +}; diff --git a/src/client/sound/sound_manager.cpp b/src/client/sound/sound_manager.cpp new file mode 100644 index 000000000..0791995c4 --- /dev/null +++ b/src/client/sound/sound_manager.cpp @@ -0,0 +1,523 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "sound_manager.h" + +#include "sound_singleton.h" +#include "util/numeric.h" // myrand() +#include "filesys.h" +#include "porting.h" + +void OpenALSoundManager::stepStreams(f32 dtime) +{ + // spread work across steps + const size_t num_issued_sounds = std::min( + m_sounds_streaming_current_bigstep.size(), + (size_t)std::ceil(m_sounds_streaming_current_bigstep.size() + * dtime / m_stream_timer) + ); + + for (size_t i = 0; i < num_issued_sounds; ++i) { + auto wptr = std::move(m_sounds_streaming_current_bigstep.back()); + m_sounds_streaming_current_bigstep.pop_back(); + + std::shared_ptr snd = wptr.lock(); + if (!snd) + continue; + + if (!snd->stepStream()) + continue; + + // sound still lives and needs more stream-stepping => add to next bigstep + m_sounds_streaming_next_bigstep.push_back(std::move(wptr)); + } + + m_stream_timer -= dtime; + if (m_stream_timer <= 0.0f) { + m_stream_timer = STREAM_BIGSTEP_TIME; + using std::swap; + swap(m_sounds_streaming_current_bigstep, m_sounds_streaming_next_bigstep); + } +} + +void OpenALSoundManager::doFades(f32 dtime) +{ + for (size_t i = 0; i < m_sounds_fading.size();) { + std::shared_ptr snd = m_sounds_fading[i].lock(); + if (snd) { + if (snd->doFade(dtime)) { + // needs more fading later, keep in m_sounds_fading + ++i; + continue; + } + } + + // sound no longer needs to be faded + m_sounds_fading[i] = std::move(m_sounds_fading.back()); + m_sounds_fading.pop_back(); + // continue with same i + } +} + +std::shared_ptr OpenALSoundManager::openSingleSound(const std::string &sound_name) +{ + // if already open, nothing to do + auto it = m_sound_datas_open.find(sound_name); + if (it != m_sound_datas_open.end()) + return it->second; + + // find unopened data + auto it_unopen = m_sound_datas_unopen.find(sound_name); + if (it_unopen == m_sound_datas_unopen.end()) + return nullptr; + std::unique_ptr unopn_snd = std::move(it_unopen->second); + m_sound_datas_unopen.erase(it_unopen); + + // open + std::shared_ptr opn_snd = std::move(*unopn_snd).open(sound_name); + if (!opn_snd) + return nullptr; + m_sound_datas_open.emplace(sound_name, opn_snd); + return opn_snd; +} + +std::string OpenALSoundManager::getLoadedSoundNameFromGroup(const std::string &group_name) +{ + std::string chosen_sound_name = ""; + + auto it_groups = m_sound_groups.find(group_name); + if (it_groups == m_sound_groups.end()) + return ""; + + std::vector &group_sounds = it_groups->second; + while (!group_sounds.empty()) { + // choose one by random + int j = myrand() % group_sounds.size(); + chosen_sound_name = group_sounds[j]; + + // find chosen one + std::shared_ptr snd = openSingleSound(chosen_sound_name); + if (snd) + return chosen_sound_name; + + // it doesn't exist + // remove it from the group and try again + group_sounds[j] = std::move(group_sounds.back()); + group_sounds.pop_back(); + } + + return ""; +} + +std::string OpenALSoundManager::getOrLoadLoadedSoundNameFromGroup(const std::string &group_name) +{ + std::string sound_name = getLoadedSoundNameFromGroup(group_name); + if (!sound_name.empty()) + return sound_name; + + // load + std::vector paths = m_fallback_path_provider + ->getLocalFallbackPathsForSoundname(group_name); + for (const std::string &path : paths) { + if (loadSoundFile(path, path)) + addSoundToGroup(path, group_name); + } + return getLoadedSoundNameFromGroup(group_name); +} + +std::shared_ptr OpenALSoundManager::createPlayingSound( + const std::string &sound_name, bool loop, f32 volume, f32 pitch, + f32 start_time, const std::optional> &pos_vel_opt) +{ + infostream << "OpenALSoundManager: Creating playing sound \"" << sound_name + << "\"" << std::endl; + warn_if_al_error("before createPlayingSound"); + + std::shared_ptr lsnd = openSingleSound(sound_name); + if (!lsnd) { + // does not happen because of the call to getLoadedSoundNameFromGroup + errorstream << "OpenALSoundManager::createPlayingSound: Sound \"" + << sound_name << "\" disappeared." << std::endl; + return nullptr; + } + + if (lsnd->m_decode_info.is_stereo && pos_vel_opt.has_value()) { + warningstream << "OpenALSoundManager::createPlayingSound: " + << "Creating positional stereo sound \"" << sound_name << "\"." + << std::endl; + } + + ALuint source_id; + alGenSources(1, &source_id); + if (warn_if_al_error("createPlayingSound (alGenSources)") != AL_NO_ERROR) { + // happens ie. if there are too many sources (out of memory) + return nullptr; + } + + auto sound = std::make_shared(source_id, std::move(lsnd), loop, + volume, pitch, start_time, pos_vel_opt); + + sound->play(); + if (m_is_paused) + sound->pause(); + warn_if_al_error("createPlayingSound"); + return sound; +} + +void OpenALSoundManager::playSoundGeneric(sound_handle_t id, const std::string &group_name, + bool loop, f32 volume, f32 fade, f32 pitch, bool use_local_fallback, + f32 start_time, const std::optional> &pos_vel_opt) +{ + assert(id != 0); + + if (group_name.empty()) { + reportRemovedSound(id); + return; + } + + // choose random sound name from group name + std::string sound_name = use_local_fallback ? + getOrLoadLoadedSoundNameFromGroup(group_name) : + getLoadedSoundNameFromGroup(group_name); + if (sound_name.empty()) { + infostream << "OpenALSoundManager: \"" << group_name << "\" not found." + << std::endl; + reportRemovedSound(id); + return; + } + + volume = std::max(0.0f, volume); + f32 target_fade_volume = volume; + if (fade > 0.0f) + volume = 0.0f; + + if (!(pitch > 0.0f)) { + warningstream << "OpenALSoundManager::playSoundGeneric: Illegal pitch value: " + << start_time << std::endl; + pitch = 1.0f; + } + + if (!std::isfinite(start_time)) { + warningstream << "OpenALSoundManager::playSoundGeneric: Illegal start_time value: " + << start_time << std::endl; + start_time = 0.0f; + } + + // play it + std::shared_ptr sound = createPlayingSound(sound_name, loop, + volume, pitch, start_time, pos_vel_opt); + if (!sound) { + reportRemovedSound(id); + return; + } + + // add to streaming sounds if streaming + if (sound->isStreaming()) + m_sounds_streaming_next_bigstep.push_back(sound); + + m_sounds_playing.emplace(id, std::move(sound)); + + if (fade > 0.0f) + fadeSound(id, fade, target_fade_volume); +} + +int OpenALSoundManager::removeDeadSounds() +{ + int num_deleted_sounds = 0; + + for (auto it = m_sounds_playing.begin(); it != m_sounds_playing.end();) { + sound_handle_t id = it->first; + PlayingSound &sound = *it->second; + // If dead, remove it + if (sound.isDead()) { + it = m_sounds_playing.erase(it); + reportRemovedSound(id); + ++num_deleted_sounds; + } else { + ++it; + } + } + + return num_deleted_sounds; +} + +OpenALSoundManager::OpenALSoundManager(SoundManagerSingleton *smg, + std::unique_ptr fallback_path_provider) : + Thread("OpenALSoundManager"), + m_fallback_path_provider(std::move(fallback_path_provider)), + m_device(smg->m_device.get()), + m_context(smg->m_context.get()) +{ + SANITY_CHECK(!!m_fallback_path_provider); + + infostream << "Audio: Initialized: OpenAL " << std::endl; +} + +OpenALSoundManager::~OpenALSoundManager() +{ + infostream << "Audio: Deinitializing..." << std::endl; +} + +/* Interface */ + +void OpenALSoundManager::step(f32 dtime) +{ + m_time_until_dead_removal -= dtime; + if (m_time_until_dead_removal <= 0.0f) { + if (!m_sounds_playing.empty()) { + verbosestream << "OpenALSoundManager::step(): " + << m_sounds_playing.size() << " playing sounds, " + << m_sound_datas_unopen.size() << " unopen sounds, " + << m_sound_datas_open.size() << " open sounds and " + << m_sound_groups.size() << " sound groups loaded." + << std::endl; + } + + int num_deleted_sounds = removeDeadSounds(); + + if (num_deleted_sounds != 0) + verbosestream << "OpenALSoundManager::step(): Deleted " + << num_deleted_sounds << " dead playing sounds." << std::endl; + + m_time_until_dead_removal = REMOVE_DEAD_SOUNDS_INTERVAL; + } + + doFades(dtime); + stepStreams(dtime); +} + +void OpenALSoundManager::pauseAll() +{ + for (auto &snd_p : m_sounds_playing) { + PlayingSound &snd = *snd_p.second; + snd.pause(); + } + m_is_paused = true; +} + +void OpenALSoundManager::resumeAll() +{ + for (auto &snd_p : m_sounds_playing) { + PlayingSound &snd = *snd_p.second; + snd.resume(); + } + m_is_paused = false; +} + +void OpenALSoundManager::updateListener(const v3f &pos_, const v3f &vel_, + const v3f &at_, const v3f &up_) +{ + v3f pos = swap_handedness(pos_); + v3f vel = swap_handedness(vel_); + v3f at = swap_handedness(at_); + v3f up = swap_handedness(up_); + ALfloat orientation[6] = {at.X, at.Y, at.Z, up.X, up.Y, up.Z}; + + alListener3f(AL_POSITION, pos.X, pos.Y, pos.Z); + alListener3f(AL_VELOCITY, vel.X, vel.Y, vel.Z); + alListenerfv(AL_ORIENTATION, orientation); + warn_if_al_error("updateListener"); +} + +void OpenALSoundManager::setListenerGain(f32 gain) +{ + alListenerf(AL_GAIN, gain); +} + +bool OpenALSoundManager::loadSoundFile(const std::string &name, const std::string &filepath) +{ + // do not add twice + if (m_sound_datas_open.count(name) != 0 || m_sound_datas_unopen.count(name) != 0) + return false; + + // coarse check + if (!fs::IsFile(filepath)) + return false; + + loadSoundFileNoCheck(name, filepath); + return true; +} + +bool OpenALSoundManager::loadSoundData(const std::string &name, std::string &&filedata) +{ + // do not add twice + if (m_sound_datas_open.count(name) != 0 || m_sound_datas_unopen.count(name) != 0) + return false; + + loadSoundDataNoCheck(name, std::move(filedata)); + return true; +} + +void OpenALSoundManager::loadSoundFileNoCheck(const std::string &name, const std::string &filepath) +{ + // remember for lazy loading + m_sound_datas_unopen.emplace(name, std::make_unique(filepath)); +} + +void OpenALSoundManager::loadSoundDataNoCheck(const std::string &name, std::string &&filedata) +{ + // remember for lazy loading + m_sound_datas_unopen.emplace(name, std::make_unique(std::move(filedata))); +} + +void OpenALSoundManager::addSoundToGroup(const std::string &sound_name, const std::string &group_name) +{ + auto it_groups = m_sound_groups.find(group_name); + if (it_groups != m_sound_groups.end()) + it_groups->second.push_back(sound_name); + else + m_sound_groups.emplace(group_name, std::vector{sound_name}); +} + +void OpenALSoundManager::playSound(sound_handle_t id, const SoundSpec &spec) +{ + return playSoundGeneric(id, spec.name, spec.loop, spec.gain, spec.fade, spec.pitch, + spec.use_local_fallback, spec.start_time, std::nullopt); +} + +void OpenALSoundManager::playSoundAt(sound_handle_t id, const SoundSpec &spec, + const v3f &pos_, const v3f &vel_) +{ + std::optional> pos_vel_opt({ + swap_handedness(pos_), + swap_handedness(vel_) + }); + + return playSoundGeneric(id, spec.name, spec.loop, spec.gain, spec.fade, spec.pitch, + spec.use_local_fallback, spec.start_time, pos_vel_opt); +} + +void OpenALSoundManager::stopSound(sound_handle_t sound) +{ + m_sounds_playing.erase(sound); + reportRemovedSound(sound); +} + +void OpenALSoundManager::fadeSound(sound_handle_t soundid, f32 step, f32 target_gain) +{ + // Ignore the command if step isn't valid. + if (step == 0.0f) + return; + auto sound_it = m_sounds_playing.find(soundid); + if (sound_it == m_sounds_playing.end()) + return; // No sound to fade + PlayingSound &sound = *sound_it->second; + if (sound.fade(step, target_gain)) + m_sounds_fading.emplace_back(sound_it->second); +} + +void OpenALSoundManager::updateSoundPosVel(sound_handle_t id, const v3f &pos_, + const v3f &vel_) +{ + v3f pos = swap_handedness(pos_); + v3f vel = swap_handedness(vel_); + + auto i = m_sounds_playing.find(id); + if (i == m_sounds_playing.end()) + return; + i->second->updatePosVel(pos, vel); +} + +/* Thread stuff */ + +void *OpenALSoundManager::run() +{ + using namespace sound_manager_messages_to_mgr; + + struct MsgVisitor { + enum class Result { Ok, Empty, StopRequested }; + + OpenALSoundManager &mgr; + + Result operator()(std::monostate &&) { + return Result::Empty; } + + Result operator()(PauseAll &&) { + mgr.pauseAll(); return Result::Ok; } + Result operator()(ResumeAll &&) { + mgr.resumeAll(); return Result::Ok; } + + Result operator()(UpdateListener &&msg) { + mgr.updateListener(msg.pos_, msg.vel_, msg.at_, msg.up_); return Result::Ok; } + Result operator()(SetListenerGain &&msg) { + mgr.setListenerGain(msg.gain); return Result::Ok; } + + Result operator()(LoadSoundFile &&msg) { + mgr.loadSoundFileNoCheck(msg.name, msg.filepath); return Result::Ok; } + Result operator()(LoadSoundData &&msg) { + mgr.loadSoundDataNoCheck(msg.name, std::move(msg.filedata)); return Result::Ok; } + Result operator()(AddSoundToGroup &&msg) { + mgr.addSoundToGroup(msg.sound_name, msg.group_name); return Result::Ok; } + + Result operator()(PlaySound &&msg) { + mgr.playSound(msg.id, msg.spec); return Result::Ok; } + Result operator()(PlaySoundAt &&msg) { + mgr.playSoundAt(msg.id, msg.spec, msg.pos_, msg.vel_); return Result::Ok; } + Result operator()(StopSound &&msg) { + mgr.stopSound(msg.sound); return Result::Ok; } + Result operator()(FadeSound &&msg) { + mgr.fadeSound(msg.soundid, msg.step, msg.target_gain); return Result::Ok; } + Result operator()(UpdateSoundPosVel &&msg) { + mgr.updateSoundPosVel(msg.sound, msg.pos_, msg.vel_); return Result::Ok; } + + Result operator()(PleaseStop &&msg) { + return Result::StopRequested; } + }; + + u64 t_step_start = porting::getTimeMs(); + while (true) { + auto get_time_since_last_step = [&] { + return (f32)(porting::getTimeMs() - t_step_start); + }; + auto get_remaining_timeout = [&] { + return (s32)((1.0e3f * SOUNDTHREAD_DTIME) - get_time_since_last_step()); + }; + + bool stop_requested = false; + + while (true) { + SoundManagerMsgToMgr msg = + m_queue_to_mgr.pop_frontNoEx(std::max(get_remaining_timeout(), 0)); + + MsgVisitor::Result res = std::visit(MsgVisitor{*this}, std::move(msg)); + + if (res == MsgVisitor::Result::Empty && get_remaining_timeout() <= 0) { + break; // finished sleeping + } else if (res == MsgVisitor::Result::StopRequested) { + stop_requested = true; + break; + } + } + if (stop_requested) + break; + + f32 dtime = get_time_since_last_step(); + t_step_start = porting::getTimeMs(); + step(dtime); + } + + send(sound_manager_messages_to_proxy::Stopped{}); + + return nullptr; +} diff --git a/src/client/sound/sound_manager.h b/src/client/sound/sound_manager.h new file mode 100644 index 000000000..b69bbf870 --- /dev/null +++ b/src/client/sound/sound_manager.h @@ -0,0 +1,171 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "playing_sound.h" +#include "sound_constants.h" +#include "sound_manager_messages.h" +#include "../sound.h" +#include "threading/thread.h" +#include "util/container.h" // MutexedQueue + +class SoundManagerSingleton; + +/* + * The SoundManager thread + * + * It's not an ISoundManager. It doesn't allocate ids, and doesn't accept id 0. + * All sound loading and interaction with OpenAL happens in this thread. + * Access from other threads happens via ProxySoundManager. + * + * See sound_constants.h for more details. + */ + +class OpenALSoundManager final : public Thread +{ +private: + std::unique_ptr m_fallback_path_provider; + + ALCdevice *m_device; + ALCcontext *m_context; + + // time in seconds until which removeDeadSounds will be called again + f32 m_time_until_dead_removal = REMOVE_DEAD_SOUNDS_INTERVAL; + + // loaded sounds + std::unordered_map> m_sound_datas_unopen; + std::unordered_map> m_sound_datas_open; + // sound groups + std::unordered_map> m_sound_groups; + + // currently playing sounds + std::unordered_map> m_sounds_playing; + + // streamed sounds + std::vector> m_sounds_streaming_current_bigstep; + std::vector> m_sounds_streaming_next_bigstep; + // time left until current bigstep finishes + f32 m_stream_timer = STREAM_BIGSTEP_TIME; + + std::vector> m_sounds_fading; + + // if true, all sounds will be directly paused after creation + bool m_is_paused = false; + +public: + // used for communication with ProxySoundManager + MutexedQueue m_queue_to_mgr; + MutexedQueue m_queue_to_proxy; + +private: + void stepStreams(f32 dtime); + void doFades(f32 dtime); + + /** + * Gives the open sound for a loaded sound. + * Opens the sound if currently unopened. + * + * @param sound_name Name of the sound. + * @return The open sound. + */ + std::shared_ptr openSingleSound(const std::string &sound_name); + + /** + * Gets a random sound name from a group. + * + * @param group_name The name of the sound group. + * @return The name of a sound in the group, or "" on failure. Getting the + * sound with `openSingleSound` directly afterwards will not fail. + */ + std::string getLoadedSoundNameFromGroup(const std::string &group_name); + + /** + * Same as `getLoadedSoundNameFromGroup`, but if sound does not exist, try to + * load from local files. + */ + std::string getOrLoadLoadedSoundNameFromGroup(const std::string &group_name); + + std::shared_ptr createPlayingSound(const std::string &sound_name, + bool loop, f32 volume, f32 pitch, f32 start_time, + const std::optional> &pos_vel_opt); + + void playSoundGeneric(sound_handle_t id, const std::string &group_name, bool loop, + f32 volume, f32 fade, f32 pitch, bool use_local_fallback, f32 start_time, + const std::optional> &pos_vel_opt); + + /** + * Deletes sounds that are dead (=finished). + * + * @return Number of removed sounds. + */ + int removeDeadSounds(); + +public: + OpenALSoundManager(SoundManagerSingleton *smg, + std::unique_ptr fallback_path_provider); + + ~OpenALSoundManager() override; + + DISABLE_CLASS_COPY(OpenALSoundManager) + +private: + /* Similar to ISoundManager */ + + void step(f32 dtime); + void pauseAll(); + void resumeAll(); + + void updateListener(const v3f &pos_, const v3f &vel_, const v3f &at_, const v3f &up_); + void setListenerGain(f32 gain); + + bool loadSoundFile(const std::string &name, const std::string &filepath); + bool loadSoundData(const std::string &name, std::string &&filedata); + void loadSoundFileNoCheck(const std::string &name, const std::string &filepath); + void loadSoundDataNoCheck(const std::string &name, std::string &&filedata); + void addSoundToGroup(const std::string &sound_name, const std::string &group_name); + + void playSound(sound_handle_t id, const SoundSpec &spec); + void playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos_, + const v3f &vel_); + void stopSound(sound_handle_t sound); + void fadeSound(sound_handle_t soundid, f32 step, f32 target_gain); + void updateSoundPosVel(sound_handle_t sound, const v3f &pos_, const v3f &vel_); + +protected: + /* Thread stuff */ + + void *run() override; + +private: + void send(SoundManagerMsgToProxy msg) + { + m_queue_to_proxy.push_back(std::move(msg)); + } + + void reportRemovedSound(sound_handle_t id) + { + send(sound_manager_messages_to_proxy::ReportRemovedSound{id}); + } +}; diff --git a/src/client/sound/sound_manager_messages.h b/src/client/sound/sound_manager_messages.h new file mode 100644 index 000000000..14d254c98 --- /dev/null +++ b/src/client/sound/sound_manager_messages.h @@ -0,0 +1,80 @@ +/* +Minetest +Copyright (C) 2023 DS + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "../sound.h" +#include "../../sound.h" +#include + +namespace sound_manager_messages_to_mgr { + struct PauseAll {}; + struct ResumeAll {}; + + struct UpdateListener { v3f pos_; v3f vel_; v3f at_; v3f up_; }; + struct SetListenerGain { f32 gain; }; + + struct LoadSoundFile { std::string name; std::string filepath; }; + struct LoadSoundData { std::string name; std::string filedata; }; + struct AddSoundToGroup { std::string sound_name; std::string group_name; }; + + struct PlaySound { sound_handle_t id; SoundSpec spec; }; + struct PlaySoundAt { sound_handle_t id; SoundSpec spec; v3f pos_; v3f vel_; }; + struct StopSound { sound_handle_t sound; }; + struct FadeSound { sound_handle_t soundid; f32 step; f32 target_gain; }; + struct UpdateSoundPosVel { sound_handle_t sound; v3f pos_; v3f vel_; }; + + struct PleaseStop {}; +} + +using SoundManagerMsgToMgr = std::variant< + std::monostate, + + sound_manager_messages_to_mgr::PauseAll, + sound_manager_messages_to_mgr::ResumeAll, + + sound_manager_messages_to_mgr::UpdateListener, + sound_manager_messages_to_mgr::SetListenerGain, + + sound_manager_messages_to_mgr::LoadSoundFile, + sound_manager_messages_to_mgr::LoadSoundData, + sound_manager_messages_to_mgr::AddSoundToGroup, + + sound_manager_messages_to_mgr::PlaySound, + sound_manager_messages_to_mgr::PlaySoundAt, + sound_manager_messages_to_mgr::StopSound, + sound_manager_messages_to_mgr::FadeSound, + sound_manager_messages_to_mgr::UpdateSoundPosVel, + + sound_manager_messages_to_mgr::PleaseStop + >; + +namespace sound_manager_messages_to_proxy { + struct ReportRemovedSound { sound_handle_t id; }; + + struct Stopped {}; +} + +using SoundManagerMsgToProxy = std::variant< + std::monostate, + + sound_manager_messages_to_proxy::ReportRemovedSound, + + sound_manager_messages_to_proxy::Stopped + >; diff --git a/src/client/sound/sound_openal.cpp b/src/client/sound/sound_openal.cpp index e8bb6290e..5d5d46a95 100644 --- a/src/client/sound/sound_openal.cpp +++ b/src/client/sound/sound_openal.cpp @@ -22,7 +22,9 @@ with this program; ifnot, write to the Free Software Foundation, Inc., */ #include "sound_openal.h" -#include "sound_openal_internal.h" + +#include "sound_singleton.h" +#include "proxy_sound_manager.h" std::shared_ptr g_sound_manager_singleton; diff --git a/src/client/sound/sound_openal_internal.cpp b/src/client/sound/sound_openal_internal.cpp deleted file mode 100644 index 208c33e45..000000000 --- a/src/client/sound/sound_openal_internal.cpp +++ /dev/null @@ -1,1363 +0,0 @@ -/* -Minetest -Copyright (C) 2022 DS -Copyright (C) 2013 celeron55, Perttu Ahola -OpenAL support based on work by: -Copyright (C) 2011 Sebastian 'Bahamada' Rühl -Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits -Copyright (C) 2011 Giuseppe Bilotta - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; ifnot, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#include "sound_openal_internal.h" - -#include "util/numeric.h" // myrand() -#include "filesys.h" -#include "settings.h" -#include -#include - -/* - * Helpers - */ - -static const char *getAlErrorString(ALenum err) noexcept -{ - switch (err) { - case AL_NO_ERROR: - return "no error"; - case AL_INVALID_NAME: - return "invalid name"; - case AL_INVALID_ENUM: - return "invalid enum"; - case AL_INVALID_VALUE: - return "invalid value"; - case AL_INVALID_OPERATION: - return "invalid operation"; - case AL_OUT_OF_MEMORY: - return "out of memory"; - default: - return ""; - } -} - -static ALenum warn_if_al_error(const char *desc) -{ - ALenum err = alGetError(); - if (err == AL_NO_ERROR) - return err; - warningstream << "[OpenAL Error] " << desc << ": " << getAlErrorString(err) - << std::endl; - return err; -} - -/** - * Transforms vectors from a left-handed coordinate system to a right-handed one - * and vice-versa. - * (Needed because Minetest uses a left-handed one and OpenAL a right-handed one.) - */ -static inline v3f swap_handedness(v3f v) noexcept -{ - return v3f(-v.X, v.Y, v.Z); -} - -/* - * RAIIALSoundBuffer struct - */ - -RAIIALSoundBuffer &RAIIALSoundBuffer::operator=(RAIIALSoundBuffer &&other) noexcept -{ - if (&other != this) - reset(other.release()); - return *this; -} - -void RAIIALSoundBuffer::reset(ALuint buf) noexcept -{ - if (m_buffer != 0) { - alDeleteBuffers(1, &m_buffer); - warn_if_al_error("Failed to free sound buffer"); - } - - m_buffer = buf; -} - -RAIIALSoundBuffer RAIIALSoundBuffer::generate() noexcept -{ - ALuint buf; - alGenBuffers(1, &buf); - return RAIIALSoundBuffer(buf); -} - -/* - * OggVorbisBufferSource struct - */ - -size_t OggVorbisBufferSource::read_func(void *ptr, size_t size, size_t nmemb, - void *datasource) noexcept -{ - OggVorbisBufferSource *s = (OggVorbisBufferSource *)datasource; - size_t copied_size = MYMIN(s->buf.size() - s->cur_offset, size); - memcpy(ptr, s->buf.data() + s->cur_offset, copied_size); - s->cur_offset += copied_size; - return copied_size; -} - -int OggVorbisBufferSource::seek_func(void *datasource, ogg_int64_t offset, int whence) noexcept -{ - OggVorbisBufferSource *s = (OggVorbisBufferSource *)datasource; - if (whence == SEEK_SET) { - if (offset < 0 || (size_t)offset > s->buf.size()) { - // offset out of bounds - return -1; - } - s->cur_offset = offset; - return 0; - } else if (whence == SEEK_CUR) { - if ((size_t)MYMIN(-offset, 0) > s->cur_offset - || s->cur_offset + offset > s->buf.size()) { - // offset out of bounds - return -1; - } - s->cur_offset += offset; - return 0; - } else if (whence == SEEK_END) { - if (offset > 0 || (size_t)-offset > s->buf.size()) { - // offset out of bounds - return -1; - } - s->cur_offset = s->buf.size() - offset; - return 0; - } - return -1; -} - -int OggVorbisBufferSource::close_func(void *datasource) noexcept -{ - auto s = reinterpret_cast(datasource); - delete s; - return 0; -} - -long OggVorbisBufferSource::tell_func(void *datasource) noexcept -{ - OggVorbisBufferSource *s = (OggVorbisBufferSource *)datasource; - return s->cur_offset; -} - -const ov_callbacks OggVorbisBufferSource::s_ov_callbacks = { - &OggVorbisBufferSource::read_func, - &OggVorbisBufferSource::seek_func, - &OggVorbisBufferSource::close_func, - &OggVorbisBufferSource::tell_func -}; - -/* - * RAIIOggFile struct - */ - -std::optional RAIIOggFile::getDecodeInfo(const std::string &filename_for_logging) -{ - OggFileDecodeInfo ret; - - vorbis_info *pInfo = ov_info(&m_file, -1); - if (!pInfo) - return std::nullopt; - - ret.name_for_logging = filename_for_logging; - - if (pInfo->channels == 1) { - ret.is_stereo = false; - ret.format = AL_FORMAT_MONO16; - ret.bytes_per_sample = 2; - } else if (pInfo->channels == 2) { - ret.is_stereo = true; - ret.format = AL_FORMAT_STEREO16; - ret.bytes_per_sample = 4; - } else { - warningstream << "Audio: Can't decode. Sound is neither mono nor stereo: " - << ret.name_for_logging << std::endl; - return std::nullopt; - } - - ret.freq = pInfo->rate; - - ret.length_samples = static_cast(ov_pcm_total(&m_file, -1)); - ret.length_seconds = static_cast(ov_time_total(&m_file, -1)); - - return ret; -} - -RAIIALSoundBuffer RAIIOggFile::loadBuffer(const OggFileDecodeInfo &decode_info, - ALuint pcm_start, ALuint pcm_end) -{ - constexpr int endian = 0; // 0 for Little-Endian, 1 for Big-Endian - constexpr int word_size = 2; // we use s16 samples - constexpr int word_signed = 1; // ^ - - // seek - if (ov_pcm_tell(&m_file) != pcm_start) { - if (ov_pcm_seek(&m_file, pcm_start) != 0) { - warningstream << "Audio: Error decoding (could not seek) " - << decode_info.name_for_logging << std::endl; - return RAIIALSoundBuffer(); - } - } - - const size_t size = static_cast(pcm_end - pcm_start) - * decode_info.bytes_per_sample; - - std::unique_ptr snd_buffer(new char[size]); - - // read size bytes - size_t read_count = 0; - int bitStream; - while (read_count < size) { - // Read up to a buffer's worth of decoded sound data - long num_bytes = ov_read(&m_file, &snd_buffer[read_count], size - read_count, - endian, word_size, word_signed, &bitStream); - - if (num_bytes <= 0) { - warningstream << "Audio: Error decoding " - << decode_info.name_for_logging << std::endl; - return RAIIALSoundBuffer(); - } - - read_count += num_bytes; - } - - // load buffer to openal - RAIIALSoundBuffer snd_buffer_id = RAIIALSoundBuffer::generate(); - alBufferData(snd_buffer_id.get(), decode_info.format, &(snd_buffer[0]), size, - decode_info.freq); - - ALenum error = alGetError(); - if (error != AL_NO_ERROR) { - warningstream << "Audio: OpenAL error: " << getAlErrorString(error) - << "preparing sound buffer for sound \"" - << decode_info.name_for_logging << "\"" << std::endl; - } - - return snd_buffer_id; -} - -/* - * SoundManagerSingleton class - */ - -bool SoundManagerSingleton::init() -{ - if (!(m_device = unique_ptr_alcdevice(alcOpenDevice(nullptr)))) { - errorstream << "Audio: Global Initialization: Failed to open device" << std::endl; - return false; - } - - if (!(m_context = unique_ptr_alccontext(alcCreateContext(m_device.get(), nullptr)))) { - errorstream << "Audio: Global Initialization: Failed to create context" << std::endl; - return false; - } - - if (!alcMakeContextCurrent(m_context.get())) { - errorstream << "Audio: Global Initialization: Failed to make current context" << std::endl; - return false; - } - - alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); - - // Speed of sound in nodes per second - // FIXME: This value assumes 1 node sidelength = 1 meter, and "normal" air. - // Ideally this should be mod-controlled. - alSpeedOfSound(343.3f); - - // doppler effect turned off for now, for best backwards compatibility - alDopplerFactor(0.0f); - - if (alGetError() != AL_NO_ERROR) { - errorstream << "Audio: Global Initialization: OpenAL Error " << alGetError() << std::endl; - return false; - } - - infostream << "Audio: Global Initialized: OpenAL " << alGetString(AL_VERSION) - << ", using " << alcGetString(m_device.get(), ALC_DEVICE_SPECIFIER) - << std::endl; - - return true; -} - -SoundManagerSingleton::~SoundManagerSingleton() -{ - infostream << "Audio: Global Deinitialized." << std::endl; -} - -/* - * ISoundDataOpen struct - */ - -std::shared_ptr ISoundDataOpen::fromOggFile(std::unique_ptr oggfile, - const std::string &filename_for_logging) -{ - // Get some information about the OGG file - std::optional decode_info = oggfile->getDecodeInfo(filename_for_logging); - if (!decode_info.has_value()) { - warningstream << "Audio: Error decoding " - << filename_for_logging << std::endl; - return nullptr; - } - - // use duration (in seconds) to decide whether to load all at once or to stream - if (decode_info->length_seconds <= SOUND_DURATION_MAX_SINGLE) { - return std::make_shared(std::move(oggfile), *decode_info); - } else { - return std::make_shared(std::move(oggfile), *decode_info); - } -} - -/* - * SoundDataUnopenBuffer struct - */ - -std::shared_ptr SoundDataUnopenBuffer::open(const std::string &sound_name) && -{ - // load from m_buffer - - auto oggfile = std::make_unique(); - - auto buffer_source = std::make_unique(); - buffer_source->buf = std::move(m_buffer); - - oggfile->m_needs_clear = true; - if (ov_open_callbacks(buffer_source.release(), oggfile->get(), nullptr, 0, - OggVorbisBufferSource::s_ov_callbacks) != 0) { - warningstream << "Audio: Error opening " << sound_name << " for decoding" - << std::endl; - return nullptr; - } - - return ISoundDataOpen::fromOggFile(std::move(oggfile), sound_name); -} - -/* - * SoundDataUnopenFile struct - */ - -std::shared_ptr SoundDataUnopenFile::open(const std::string &sound_name) && -{ - // load from file at m_path - - auto oggfile = std::make_unique(); - - if (ov_fopen(m_path.c_str(), oggfile->get()) != 0) { - warningstream << "Audio: Error opening " << m_path << " for decoding" - << std::endl; - return nullptr; - } - oggfile->m_needs_clear = true; - - return ISoundDataOpen::fromOggFile(std::move(oggfile), sound_name); -} - -/* - * SoundDataOpenBuffer struct - */ - -SoundDataOpenBuffer::SoundDataOpenBuffer(std::unique_ptr oggfile, - const OggFileDecodeInfo &decode_info) : ISoundDataOpen(decode_info) -{ - m_buffer = oggfile->loadBuffer(m_decode_info, 0, m_decode_info.length_samples); - if (m_buffer.get() == 0) { - warningstream << "SoundDataOpenBuffer: Failed to load sound \"" - << m_decode_info.name_for_logging << "\"" << std::endl; - return; - } -} - -/* - * SoundDataOpenStream struct - */ - -SoundDataOpenStream::SoundDataOpenStream(std::unique_ptr oggfile, - const OggFileDecodeInfo &decode_info) : - ISoundDataOpen(decode_info), m_oggfile(std::move(oggfile)) -{ - // do nothing here. buffers are loaded at getOrLoadBufferAt -} - -std::tuple SoundDataOpenStream::getOrLoadBufferAt(ALuint offset) -{ - if (offset >= m_decode_info.length_samples) - return {0, m_decode_info.length_samples, 0}; - - // find the right-most ContiguousBuffers, such that `m_start <= offset` - // equivalent: the first element from the right such that `!(m_start > offset)` - // (from the right, `offset` is a lower bound to the `m_start`s) - auto lower_rit = std::lower_bound(m_bufferss.rbegin(), m_bufferss.rend(), offset, - [](const ContiguousBuffers &bufs, ALuint offset) { - return bufs.m_start > offset; - }); - - if (lower_rit != m_bufferss.rend()) { - std::vector &bufs = lower_rit->m_buffers; - // find the left-most SoundBufferUntil, such that `m_end > offset` - // equivalent: the first element from the left such that `m_end > offset` - // (returns first element where comp gives true) - auto upper_it = std::upper_bound(bufs.begin(), bufs.end(), offset, - [](ALuint offset, const SoundBufferUntil &buf) { - return offset < buf.m_end; - }); - - if (upper_it != bufs.end()) { - ALuint start = upper_it == bufs.begin() ? lower_rit->m_start - : (upper_it - 1)->m_end; - return {upper_it->m_buffer.get(), upper_it->m_end, offset - start}; - } - } - - // no loaded buffer starts before or at `offset` - // or no loaded buffer (that starts before or at `offset`) ends after `offset` - - // lower_rit, but not reverse and 1 farther - auto after_it = m_bufferss.begin() + (m_bufferss.rend() - lower_rit); - - return loadBufferAt(offset, after_it); -} - -std::tuple SoundDataOpenStream::loadBufferAt(ALuint offset, - std::vector::iterator after_it) -{ - bool has_before = after_it != m_bufferss.begin(); - bool has_after = after_it != m_bufferss.end(); - - ALuint end_before = has_before ? (after_it - 1)->m_buffers.back().m_end : 0; - ALuint start_after = has_after ? after_it->m_start : m_decode_info.length_samples; - - const ALuint min_buf_len_samples = m_decode_info.freq * MIN_STREAM_BUFFER_LENGTH; - - // - // 1) Find the actual start and end of the new buffer - // - - ALuint new_buf_start = offset; - ALuint new_buf_end = offset + min_buf_len_samples; - - // Don't load into next buffer, or past the end - if (new_buf_end > start_after) { - new_buf_end = start_after; - // Also move start (for min buf size) (but not *into* previous buffer) - if (new_buf_end - new_buf_start < min_buf_len_samples) { - new_buf_start = std::max( - end_before, - new_buf_end < min_buf_len_samples ? 0 - : new_buf_end - min_buf_len_samples - ); - } - } - - // Widen if space to right or left is smaller than min buf size - if (new_buf_start - end_before < min_buf_len_samples) - new_buf_start = end_before; - if (start_after - new_buf_end < min_buf_len_samples) - new_buf_end = start_after; - - // - // 2) Load [new_buf_start, new_buf_end) - // - - // If it fails, we get a 0-buffer. we store it and won't try loading again - RAIIALSoundBuffer new_buf = m_oggfile->loadBuffer(m_decode_info, new_buf_start, - new_buf_end); - - // - // 3) Insert before after_it - // - - // Choose ContiguousBuffers to add the new SoundBufferUntil into: - // * `after_it - 1` (=before) if existent and if there's no space between its - // last buffer and the new buffer - // * A new ContiguousBuffers otherwise - auto it = has_before && new_buf_start == end_before ? after_it - 1 - : m_bufferss.insert(after_it, ContiguousBuffers{new_buf_start, {}}); - - // Add the new SoundBufferUntil - size_t new_buf_i = it->m_buffers.size(); - it->m_buffers.push_back(SoundBufferUntil{new_buf_end, std::move(new_buf)}); - - if (has_after && new_buf_end == start_after) { - // Merge after into my ContiguousBuffers - auto &bufs = it->m_buffers; - auto &bufs_after = (it + 1)->m_buffers; - bufs.insert(bufs.end(), std::make_move_iterator(bufs_after.begin()), - std::make_move_iterator(bufs_after.end())); - it = m_bufferss.erase(it + 1) - 1; - } - - return {it->m_buffers[new_buf_i].m_buffer.get(), new_buf_end, offset - new_buf_start}; -} - -/* - * PlayingSound class - */ - -PlayingSound::PlayingSound(ALuint source_id, std::shared_ptr data, - bool loop, f32 volume, f32 pitch, f32 start_time, - const std::optional> &pos_vel_opt) - : m_source_id(source_id), m_data(std::move(data)), m_looping(loop), - m_is_positional(pos_vel_opt.has_value()) -{ - // Calculate actual start_time (see lua_api.txt for specs) - f32 len_seconds = m_data->m_decode_info.length_seconds; - f32 len_samples = m_data->m_decode_info.length_samples; - if (!m_looping) { - if (start_time < 0.0f) { - start_time = std::fmax(start_time + len_seconds, 0.0f); - } else if (start_time >= len_seconds) { - // No sound - m_next_sample_pos = len_samples; - return; - } - } else { - // Modulo offset to be within looping time - start_time = start_time - std::floor(start_time / len_seconds) * len_seconds; - } - - // Queue first buffers - - m_next_sample_pos = std::min((start_time / len_seconds) * len_samples, len_samples); - - if (m_looping && m_next_sample_pos == len_samples) - m_next_sample_pos = 0; - - if (!m_data->isStreaming()) { - // If m_next_sample_pos >= len_samples, buf will be 0, and setting it as - // AL_BUFFER is a NOP (source stays AL_UNDETERMINED). => No sound will be - // played. - - auto [buf, buf_end, offset_in_buf] = m_data->getOrLoadBufferAt(m_next_sample_pos); - m_next_sample_pos = buf_end; - - alSourcei(m_source_id, AL_BUFFER, buf); - alSourcei(m_source_id, AL_SAMPLE_OFFSET, offset_in_buf); - - alSourcei(m_source_id, AL_LOOPING, m_looping ? AL_TRUE : AL_FALSE); - - warn_if_al_error("when creating non-streaming sound"); - - } else { - // Start with 2 buffers - ALuint buf_ids[2]; - - // 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. - - 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); - 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 - - m_stopped_means_dead = false; - - warn_if_al_error("when creating streaming sound"); - } - - // Set initial pos, volume, pitch - if (m_is_positional) { - updatePosVel(pos_vel_opt->first, pos_vel_opt->second); - } else { - // Make position-less - alSourcei(m_source_id, AL_SOURCE_RELATIVE, true); - alSource3f(m_source_id, AL_POSITION, 0.0f, 0.0f, 0.0f); - alSource3f(m_source_id, AL_VELOCITY, 0.0f, 0.0f, 0.0f); - warn_if_al_error("PlayingSound::PlayingSound at making position-less"); - } - setGain(volume); - setPitch(pitch); -} - -bool PlayingSound::stepStream() -{ - 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); - - // Fill up again - for (ALint i = 0; i < num_unqueued_bufs; ++i) { - if (m_next_sample_pos == m_data->m_decode_info.length_samples) { - // Reached end - if (m_looping) { - m_next_sample_pos = 0; - } else { - m_stopped_means_dead = true; - return false; - } - } - - auto [buf, buf_end, offset_in_buf] = m_data->getOrLoadBufferAt(m_next_sample_pos); - m_next_sample_pos = buf_end; - assert(offset_in_buf == 0); - - alSourceQueueBuffers(m_source_id, 1, &buf); - - // Start again if queue was empty and resulted in stop - if (getState() == AL_STOPPED) { - play(); - warningstream << "PlayingSound::stepStream: Sound queue ran empty for \"" - << m_data->m_decode_info.name_for_logging << "\"" << std::endl; - } - } - - return true; -} - -bool PlayingSound::fade(f32 step, f32 target_gain) noexcept -{ - bool already_fading = m_fade_state.has_value(); - - target_gain = MYMAX(target_gain, 0.0f); // 0.0f if nan - step = target_gain - getGain() > 0.0f ? std::abs(step) : -std::abs(step); - - m_fade_state = FadeState{step, target_gain}; - - return !already_fading; -} - -bool PlayingSound::doFade(f32 dtime) noexcept -{ - if (!m_fade_state || isDead()) - return false; - - FadeState &fade = *m_fade_state; - assert(fade.step != 0.0f); - - f32 current_gain = getGain(); - current_gain += fade.step * dtime; - - if (fade.step < 0.0f) - current_gain = std::max(current_gain, fade.target_gain); - else - current_gain = std::min(current_gain, fade.target_gain); - - if (current_gain <= 0.0f) { - // stop sound - m_stopped_means_dead = true; - alSourceStop(m_source_id); - - m_fade_state = std::nullopt; - return false; - } - - setGain(current_gain); - - if (current_gain == fade.target_gain) { - m_fade_state = std::nullopt; - return false; - } else { - return true; - } -} - -void PlayingSound::updatePosVel(const v3f &pos, const v3f &vel) noexcept -{ - alSourcei(m_source_id, AL_SOURCE_RELATIVE, false); - alSource3f(m_source_id, AL_POSITION, pos.X, pos.Y, pos.Z); - alSource3f(m_source_id, AL_VELOCITY, vel.X, vel.Y, vel.Z); - // Using alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED) and setting reference - // distance to clamp gain at <1 node distance avoids excessive volume when - // closer. - alSourcef(m_source_id, AL_REFERENCE_DISTANCE, 1.0f); - - warn_if_al_error("PlayingSound::updatePosVel"); -} - -void PlayingSound::setGain(f32 gain) noexcept -{ - // AL_REFERENCE_DISTANCE was once reduced from 3 nodes to 1 node. - // We compensate this by multiplying the volume by 3. - if (m_is_positional) - gain *= 3.0f; - - alSourcef(m_source_id, AL_GAIN, gain); -} - -f32 PlayingSound::getGain() noexcept -{ - ALfloat gain; - alGetSourcef(m_source_id, AL_GAIN, &gain); - // Same as above, but inverse. - if (m_is_positional) - gain *= 1.0f/3.0f; - return gain; -} - -/* - * OpenALSoundManager class - */ - -void OpenALSoundManager::stepStreams(f32 dtime) -{ - // spread work across steps - const size_t num_issued_sounds = std::min( - m_sounds_streaming_current_bigstep.size(), - (size_t)std::ceil(m_sounds_streaming_current_bigstep.size() - * dtime / m_stream_timer) - ); - - for (size_t i = 0; i < num_issued_sounds; ++i) { - auto wptr = std::move(m_sounds_streaming_current_bigstep.back()); - m_sounds_streaming_current_bigstep.pop_back(); - - std::shared_ptr snd = wptr.lock(); - if (!snd) - continue; - - if (!snd->stepStream()) - continue; - - // sound still lives and needs more stream-stepping => add to next bigstep - m_sounds_streaming_next_bigstep.push_back(std::move(wptr)); - } - - m_stream_timer -= dtime; - if (m_stream_timer <= 0.0f) { - m_stream_timer = STREAM_BIGSTEP_TIME; - using std::swap; - swap(m_sounds_streaming_current_bigstep, m_sounds_streaming_next_bigstep); - } -} - -void OpenALSoundManager::doFades(f32 dtime) -{ - for (size_t i = 0; i < m_sounds_fading.size();) { - std::shared_ptr snd = m_sounds_fading[i].lock(); - if (snd) { - if (snd->doFade(dtime)) { - // needs more fading later, keep in m_sounds_fading - ++i; - continue; - } - } - - // sound no longer needs to be faded - m_sounds_fading[i] = std::move(m_sounds_fading.back()); - m_sounds_fading.pop_back(); - // continue with same i - } -} - -std::shared_ptr OpenALSoundManager::openSingleSound(const std::string &sound_name) -{ - // if already open, nothing to do - auto it = m_sound_datas_open.find(sound_name); - if (it != m_sound_datas_open.end()) - return it->second; - - // find unopened data - auto it_unopen = m_sound_datas_unopen.find(sound_name); - if (it_unopen == m_sound_datas_unopen.end()) - return nullptr; - std::unique_ptr unopn_snd = std::move(it_unopen->second); - m_sound_datas_unopen.erase(it_unopen); - - // open - std::shared_ptr opn_snd = std::move(*unopn_snd).open(sound_name); - if (!opn_snd) - return nullptr; - m_sound_datas_open.emplace(sound_name, opn_snd); - return opn_snd; -} - -std::string OpenALSoundManager::getLoadedSoundNameFromGroup(const std::string &group_name) -{ - std::string chosen_sound_name = ""; - - auto it_groups = m_sound_groups.find(group_name); - if (it_groups == m_sound_groups.end()) - return ""; - - std::vector &group_sounds = it_groups->second; - while (!group_sounds.empty()) { - // choose one by random - int j = myrand() % group_sounds.size(); - chosen_sound_name = group_sounds[j]; - - // find chosen one - std::shared_ptr snd = openSingleSound(chosen_sound_name); - if (snd) - return chosen_sound_name; - - // it doesn't exist - // remove it from the group and try again - group_sounds[j] = std::move(group_sounds.back()); - group_sounds.pop_back(); - } - - return ""; -} - -std::string OpenALSoundManager::getOrLoadLoadedSoundNameFromGroup(const std::string &group_name) -{ - std::string sound_name = getLoadedSoundNameFromGroup(group_name); - if (!sound_name.empty()) - return sound_name; - - // load - std::vector paths = m_fallback_path_provider - ->getLocalFallbackPathsForSoundname(group_name); - for (const std::string &path : paths) { - if (loadSoundFile(path, path)) - addSoundToGroup(path, group_name); - } - return getLoadedSoundNameFromGroup(group_name); -} - -std::shared_ptr OpenALSoundManager::createPlayingSound( - const std::string &sound_name, bool loop, f32 volume, f32 pitch, - f32 start_time, const std::optional> &pos_vel_opt) -{ - infostream << "OpenALSoundManager: Creating playing sound \"" << sound_name - << "\"" << std::endl; - warn_if_al_error("before createPlayingSound"); - - std::shared_ptr lsnd = openSingleSound(sound_name); - if (!lsnd) { - // does not happen because of the call to getLoadedSoundNameFromGroup - errorstream << "OpenALSoundManager::createPlayingSound: Sound \"" - << sound_name << "\" disappeared." << std::endl; - return nullptr; - } - - if (lsnd->m_decode_info.is_stereo && pos_vel_opt.has_value()) { - warningstream << "OpenALSoundManager::createPlayingSound: " - << "Creating positional stereo sound \"" << sound_name << "\"." - << std::endl; - } - - ALuint source_id; - alGenSources(1, &source_id); - if (warn_if_al_error("createPlayingSound (alGenSources)") != AL_NO_ERROR) { - // happens ie. if there are too many sources (out of memory) - return nullptr; - } - - auto sound = std::make_shared(source_id, std::move(lsnd), loop, - volume, pitch, start_time, pos_vel_opt); - - sound->play(); - if (m_is_paused) - sound->pause(); - warn_if_al_error("createPlayingSound"); - return sound; -} - -void OpenALSoundManager::playSoundGeneric(sound_handle_t id, const std::string &group_name, - bool loop, f32 volume, f32 fade, f32 pitch, bool use_local_fallback, - f32 start_time, const std::optional> &pos_vel_opt) -{ - assert(id != 0); - - if (group_name.empty()) { - reportRemovedSound(id); - return; - } - - // choose random sound name from group name - std::string sound_name = use_local_fallback ? - getOrLoadLoadedSoundNameFromGroup(group_name) : - getLoadedSoundNameFromGroup(group_name); - if (sound_name.empty()) { - infostream << "OpenALSoundManager: \"" << group_name << "\" not found." - << std::endl; - reportRemovedSound(id); - return; - } - - volume = std::max(0.0f, volume); - f32 target_fade_volume = volume; - if (fade > 0.0f) - volume = 0.0f; - - if (!(pitch > 0.0f)) { - warningstream << "OpenALSoundManager::playSoundGeneric: Illegal pitch value: " - << start_time << std::endl; - pitch = 1.0f; - } - - if (!std::isfinite(start_time)) { - warningstream << "OpenALSoundManager::playSoundGeneric: Illegal start_time value: " - << start_time << std::endl; - start_time = 0.0f; - } - - // play it - std::shared_ptr sound = createPlayingSound(sound_name, loop, - volume, pitch, start_time, pos_vel_opt); - if (!sound) { - reportRemovedSound(id); - return; - } - - // add to streaming sounds if streaming - if (sound->isStreaming()) - m_sounds_streaming_next_bigstep.push_back(sound); - - m_sounds_playing.emplace(id, std::move(sound)); - - if (fade > 0.0f) - fadeSound(id, fade, target_fade_volume); -} - -int OpenALSoundManager::removeDeadSounds() -{ - int num_deleted_sounds = 0; - - for (auto it = m_sounds_playing.begin(); it != m_sounds_playing.end();) { - sound_handle_t id = it->first; - PlayingSound &sound = *it->second; - // If dead, remove it - if (sound.isDead()) { - it = m_sounds_playing.erase(it); - reportRemovedSound(id); - ++num_deleted_sounds; - } else { - ++it; - } - } - - return num_deleted_sounds; -} - -OpenALSoundManager::OpenALSoundManager(SoundManagerSingleton *smg, - std::unique_ptr fallback_path_provider) : - Thread("OpenALSoundManager"), - m_fallback_path_provider(std::move(fallback_path_provider)), - m_device(smg->m_device.get()), - m_context(smg->m_context.get()) -{ - SANITY_CHECK(!!m_fallback_path_provider); - - infostream << "Audio: Initialized: OpenAL " << std::endl; -} - -OpenALSoundManager::~OpenALSoundManager() -{ - infostream << "Audio: Deinitializing..." << std::endl; -} - -/* Interface */ - -void OpenALSoundManager::step(f32 dtime) -{ - m_time_until_dead_removal -= dtime; - if (m_time_until_dead_removal <= 0.0f) { - if (!m_sounds_playing.empty()) { - verbosestream << "OpenALSoundManager::step(): " - << m_sounds_playing.size() << " playing sounds, " - << m_sound_datas_unopen.size() << " unopen sounds, " - << m_sound_datas_open.size() << " open sounds and " - << m_sound_groups.size() << " sound groups loaded." - << std::endl; - } - - int num_deleted_sounds = removeDeadSounds(); - - if (num_deleted_sounds != 0) - verbosestream << "OpenALSoundManager::step(): Deleted " - << num_deleted_sounds << " dead playing sounds." << std::endl; - - m_time_until_dead_removal = REMOVE_DEAD_SOUNDS_INTERVAL; - } - - doFades(dtime); - stepStreams(dtime); -} - -void OpenALSoundManager::pauseAll() -{ - for (auto &snd_p : m_sounds_playing) { - PlayingSound &snd = *snd_p.second; - snd.pause(); - } - m_is_paused = true; -} - -void OpenALSoundManager::resumeAll() -{ - for (auto &snd_p : m_sounds_playing) { - PlayingSound &snd = *snd_p.second; - snd.resume(); - } - m_is_paused = false; -} - -void OpenALSoundManager::updateListener(const v3f &pos_, const v3f &vel_, - const v3f &at_, const v3f &up_) -{ - v3f pos = swap_handedness(pos_); - v3f vel = swap_handedness(vel_); - v3f at = swap_handedness(at_); - v3f up = swap_handedness(up_); - ALfloat orientation[6] = {at.X, at.Y, at.Z, up.X, up.Y, up.Z}; - - alListener3f(AL_POSITION, pos.X, pos.Y, pos.Z); - alListener3f(AL_VELOCITY, vel.X, vel.Y, vel.Z); - alListenerfv(AL_ORIENTATION, orientation); - warn_if_al_error("updateListener"); -} - -void OpenALSoundManager::setListenerGain(f32 gain) -{ - alListenerf(AL_GAIN, gain); -} - -bool OpenALSoundManager::loadSoundFile(const std::string &name, const std::string &filepath) -{ - // do not add twice - if (m_sound_datas_open.count(name) != 0 || m_sound_datas_unopen.count(name) != 0) - return false; - - // coarse check - if (!fs::IsFile(filepath)) - return false; - - loadSoundFileNoCheck(name, filepath); - return true; -} - -bool OpenALSoundManager::loadSoundData(const std::string &name, std::string &&filedata) -{ - // do not add twice - if (m_sound_datas_open.count(name) != 0 || m_sound_datas_unopen.count(name) != 0) - return false; - - loadSoundDataNoCheck(name, std::move(filedata)); - return true; -} - -void OpenALSoundManager::loadSoundFileNoCheck(const std::string &name, const std::string &filepath) -{ - // remember for lazy loading - m_sound_datas_unopen.emplace(name, std::make_unique(filepath)); -} - -void OpenALSoundManager::loadSoundDataNoCheck(const std::string &name, std::string &&filedata) -{ - // remember for lazy loading - m_sound_datas_unopen.emplace(name, std::make_unique(std::move(filedata))); -} - -void OpenALSoundManager::addSoundToGroup(const std::string &sound_name, const std::string &group_name) -{ - auto it_groups = m_sound_groups.find(group_name); - if (it_groups != m_sound_groups.end()) - it_groups->second.push_back(sound_name); - else - m_sound_groups.emplace(group_name, std::vector{sound_name}); -} - -void OpenALSoundManager::playSound(sound_handle_t id, const SoundSpec &spec) -{ - return playSoundGeneric(id, spec.name, spec.loop, spec.gain, spec.fade, spec.pitch, - spec.use_local_fallback, spec.start_time, std::nullopt); -} - -void OpenALSoundManager::playSoundAt(sound_handle_t id, const SoundSpec &spec, - const v3f &pos_, const v3f &vel_) -{ - std::optional> pos_vel_opt({ - swap_handedness(pos_), - swap_handedness(vel_) - }); - - return playSoundGeneric(id, spec.name, spec.loop, spec.gain, spec.fade, spec.pitch, - spec.use_local_fallback, spec.start_time, pos_vel_opt); -} - -void OpenALSoundManager::stopSound(sound_handle_t sound) -{ - m_sounds_playing.erase(sound); - reportRemovedSound(sound); -} - -void OpenALSoundManager::fadeSound(sound_handle_t soundid, f32 step, f32 target_gain) -{ - // Ignore the command if step isn't valid. - if (step == 0.0f) - return; - auto sound_it = m_sounds_playing.find(soundid); - if (sound_it == m_sounds_playing.end()) - return; // No sound to fade - PlayingSound &sound = *sound_it->second; - if (sound.fade(step, target_gain)) - m_sounds_fading.emplace_back(sound_it->second); -} - -void OpenALSoundManager::updateSoundPosVel(sound_handle_t id, const v3f &pos_, - const v3f &vel_) -{ - v3f pos = swap_handedness(pos_); - v3f vel = swap_handedness(vel_); - - auto i = m_sounds_playing.find(id); - if (i == m_sounds_playing.end()) - return; - i->second->updatePosVel(pos, vel); -} - -void *OpenALSoundManager::run() -{ - using namespace sound_manager_messages_to_mgr; - - struct MsgVisitor { - enum class Result { Ok, Empty, StopRequested }; - - OpenALSoundManager &mgr; - - Result operator()(std::monostate &&) { - return Result::Empty; } - - Result operator()(PauseAll &&) { - mgr.pauseAll(); return Result::Ok; } - Result operator()(ResumeAll &&) { - mgr.resumeAll(); return Result::Ok; } - - Result operator()(UpdateListener &&msg) { - mgr.updateListener(msg.pos_, msg.vel_, msg.at_, msg.up_); return Result::Ok; } - Result operator()(SetListenerGain &&msg) { - mgr.setListenerGain(msg.gain); return Result::Ok; } - - Result operator()(LoadSoundFile &&msg) { - mgr.loadSoundFileNoCheck(msg.name, msg.filepath); return Result::Ok; } - Result operator()(LoadSoundData &&msg) { - mgr.loadSoundDataNoCheck(msg.name, std::move(msg.filedata)); return Result::Ok; } - Result operator()(AddSoundToGroup &&msg) { - mgr.addSoundToGroup(msg.sound_name, msg.group_name); return Result::Ok; } - - Result operator()(PlaySound &&msg) { - mgr.playSound(msg.id, msg.spec); return Result::Ok; } - Result operator()(PlaySoundAt &&msg) { - mgr.playSoundAt(msg.id, msg.spec, msg.pos_, msg.vel_); return Result::Ok; } - Result operator()(StopSound &&msg) { - mgr.stopSound(msg.sound); return Result::Ok; } - Result operator()(FadeSound &&msg) { - mgr.fadeSound(msg.soundid, msg.step, msg.target_gain); return Result::Ok; } - Result operator()(UpdateSoundPosVel &&msg) { - mgr.updateSoundPosVel(msg.sound, msg.pos_, msg.vel_); return Result::Ok; } - - Result operator()(PleaseStop &&msg) { - return Result::StopRequested; } - }; - - u64 t_step_start = porting::getTimeMs(); - while (true) { - auto get_time_since_last_step = [&] { - return (f32)(porting::getTimeMs() - t_step_start); - }; - auto get_remaining_timeout = [&] { - return (s32)((1.0e3f * PROXYSOUNDMGR_DTIME) - get_time_since_last_step()); - }; - - bool stop_requested = false; - - while (true) { - SoundManagerMsgToMgr msg = - m_queue_to_mgr.pop_frontNoEx(std::max(get_remaining_timeout(), 0)); - - MsgVisitor::Result res = std::visit(MsgVisitor{*this}, std::move(msg)); - - if (res == MsgVisitor::Result::Empty && get_remaining_timeout() <= 0) { - break; // finished sleeping - } else if (res == MsgVisitor::Result::StopRequested) { - stop_requested = true; - break; - } - } - if (stop_requested) - break; - - f32 dtime = get_time_since_last_step(); - t_step_start = porting::getTimeMs(); - step(dtime); - } - - send(sound_manager_messages_to_proxy::Stopped{}); - - return nullptr; -} - -/* - * ProxySoundManager class - */ - -ProxySoundManager::MsgResult ProxySoundManager::handleMsg(SoundManagerMsgToProxy &&msg) -{ - using namespace sound_manager_messages_to_proxy; - - return std::visit([&](auto &&msg) { - using T = std::decay_t; - - if constexpr (std::is_same_v) - return MsgResult::Empty; - else if constexpr (std::is_same_v) - reportRemovedSound(msg.id); - else if constexpr (std::is_same_v) - return MsgResult::Stopped; - - return MsgResult::Ok; - }, - std::move(msg)); -} - -ProxySoundManager::~ProxySoundManager() -{ - if (m_sound_manager.isRunning()) { - send(sound_manager_messages_to_mgr::PleaseStop{}); - - // recv until it stopped - auto recv = [&] { - return m_sound_manager.m_queue_to_proxy.pop_frontNoEx(); - }; - - while (true) { - if (handleMsg(recv()) == MsgResult::Stopped) - break; - } - - // join - m_sound_manager.stop(); - SANITY_CHECK(m_sound_manager.wait()); - } -} - -void ProxySoundManager::step(f32 dtime) -{ - auto recv = [&] { - return m_sound_manager.m_queue_to_proxy.pop_frontNoEx(0); - }; - - while (true) { - MsgResult res = handleMsg(recv()); - if (res == MsgResult::Empty) - break; - else if (res == MsgResult::Stopped) - throw std::runtime_error("OpenALSoundManager stopped unexpectedly"); - } -} - -void ProxySoundManager::pauseAll() -{ - send(sound_manager_messages_to_mgr::PauseAll{}); -} - -void ProxySoundManager::resumeAll() -{ - send(sound_manager_messages_to_mgr::ResumeAll{}); -} - -void ProxySoundManager::updateListener(const v3f &pos_, const v3f &vel_, - const v3f &at_, const v3f &up_) -{ - send(sound_manager_messages_to_mgr::UpdateListener{pos_, vel_, at_, up_}); -} - -void ProxySoundManager::setListenerGain(f32 gain) -{ - send(sound_manager_messages_to_mgr::SetListenerGain{gain}); -} - -bool ProxySoundManager::loadSoundFile(const std::string &name, - const std::string &filepath) -{ - // do not add twice - if (m_known_sound_names.count(name) != 0) - return false; - - // coarse check - if (!fs::IsFile(filepath)) - return false; - - send(sound_manager_messages_to_mgr::LoadSoundFile{name, filepath}); - - m_known_sound_names.insert(name); - return true; -} - -bool ProxySoundManager::loadSoundData(const std::string &name, std::string &&filedata) -{ - // do not add twice - if (m_known_sound_names.count(name) != 0) - return false; - - send(sound_manager_messages_to_mgr::LoadSoundData{name, std::move(filedata)}); - - m_known_sound_names.insert(name); - return true; -} - -void ProxySoundManager::addSoundToGroup(const std::string &sound_name, - const std::string &group_name) -{ - send(sound_manager_messages_to_mgr::AddSoundToGroup{sound_name, group_name}); -} - -void ProxySoundManager::playSound(sound_handle_t id, const SoundSpec &spec) -{ - if (id == 0) - id = allocateId(1); - send(sound_manager_messages_to_mgr::PlaySound{id, spec}); -} - -void ProxySoundManager::playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos_, - const v3f &vel_) -{ - if (id == 0) - id = allocateId(1); - send(sound_manager_messages_to_mgr::PlaySoundAt{id, spec, pos_, vel_}); -} - -void ProxySoundManager::stopSound(sound_handle_t sound) -{ - send(sound_manager_messages_to_mgr::StopSound{sound}); -} - -void ProxySoundManager::fadeSound(sound_handle_t soundid, f32 step, f32 target_gain) -{ - send(sound_manager_messages_to_mgr::FadeSound{soundid, step, target_gain}); -} - -void ProxySoundManager::updateSoundPosVel(sound_handle_t sound, const v3f &pos_, const v3f &vel_) -{ - send(sound_manager_messages_to_mgr::UpdateSoundPosVel{sound, pos_, vel_}); -} diff --git a/src/client/sound/sound_openal_internal.h b/src/client/sound/sound_openal_internal.h deleted file mode 100644 index 19ba411ef..000000000 --- a/src/client/sound/sound_openal_internal.h +++ /dev/null @@ -1,750 +0,0 @@ -/* -Minetest -Copyright (C) 2022 DS -Copyright (C) 2013 celeron55, Perttu Ahola -OpenAL support based on work by: -Copyright (C) 2011 Sebastian 'Bahamada' Rühl -Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits -Copyright (C) 2011 Giuseppe Bilotta - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; ifnot, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#pragma once - -#include "log.h" -#include "porting.h" -#include "sound_openal.h" -#include "../../sound.h" -#include "threading/thread.h" -#include "util/basic_macros.h" -#include "util/container.h" - -#if defined(_WIN32) - #include - #include - //#include -#elif defined(__APPLE__) - #define OPENAL_DEPRECATED - #include - #include - //#include -#else - #include - #include - #include -#endif -#include - -#include -#include -#include -#include -#include - - -/* - * - * The coordinate space for sounds (sound-space): - * ---------------------------------------------- - * - * * The functions from ISoundManager (see sound.h) take spatial vectors in node-space. - * * All other `v3f`s here are, if not told otherwise, in sound-space, which is - * defined as node-space mirrored along the x-axis. - * (This is needed because OpenAL uses a right-handed coordinate system.) - * * Use `swap_handedness()` to convert between those two coordinate spaces. - * - * - * How sounds are loaded: - * ---------------------- - * - * * Step 1: - * `loadSoundFile` or `loadSoundFile` is called. This adds an unopen sound with - * the given name to `m_sound_datas_unopen`. - * Unopen / lazy sounds (`ISoundDataUnopen`) are ogg-vorbis files that we did not yet - * start to decode. (Decoding an unopen sound does not fail under normal circumstances - * (because we check whether the file exists at least), if it does fail anyways, - * we should notify the user.) - * * Step 2: - * `addSoundToGroup` is called, to add the name from step 1 to a group. If the - * group does not yet exist, a new one is created. A group can later be played. - * (The mapping is stored in `m_sound_groups`.) - * * Step 3: - * `playSound` or `playSoundAt` is called. - * * Step 3.1: - * If the group with the name `spec.name` does not exist, and `spec.use_local_fallback` - * is true, a new group is created using the user's sound-pack. - * * Step 3.2: - * We choose one random sound name from the given group. - * * Step 3.3: - * We open the sound (see `openSingleSound`). - * If the sound is already open (in `m_sound_datas_open`), we take that one. - * Otherwise we open it by calling `ISoundDataUnopen::open`. We choose (by - * sound length), whether it's a single-buffer (`SoundDataOpenBuffer`) or - * streamed (`SoundDataOpenStream`) sound. - * Single-buffer sounds are always completely loaded. Streamed sounds can be - * partially loaded. - * The sound is erased from `m_sound_datas_unopen` and added to `m_sound_datas_open`. - * Open sounds are kept forever. - * * Step 3.4: - * We create the new `PlayingSound`. It has a `shared_ptr` to its open sound. - * If the open sound is streaming, the playing sound needs to be stepped using - * `PlayingSound::stepStream` for enqueuing buffers. For this purpose, the sound - * is added to `m_sounds_streaming` (as `weak_ptr`). - * If the sound is fading, it is added to `m_sounds_fading` for regular fade-stepping. - * The sound is also added to `m_sounds_playing`, so that one can access it - * via its sound handle. - * * Step 4: - * Streaming sounds are updated. For details see [Streaming of sounds]. - * * Step 5: - * At deinitialization, we can just let the destructors do their work. - * Sound sources are deleted (and with this also stopped) by ~PlayingSound. - * Buffers can't be deleted while sound sources using them exist, because - * PlayingSound has a shared_ptr to its ISoundData. - * - * - * Streaming of sounds: - * -------------------- - * - * In each "bigstep", all streamed sounds are stepStream()ed. This means a - * sound can be stepped at any point in time in the bigstep's interval. - * - * 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. - * - */ - -// constants - -// in seconds -constexpr f32 REMOVE_DEAD_SOUNDS_INTERVAL = 2.0f; -// maximum length in seconds that a sound can have without being streamed -constexpr f32 SOUND_DURATION_MAX_SINGLE = 3.0f; -// minimum time in seconds of a single buffer in a streamed sound -constexpr f32 MIN_STREAM_BUFFER_LENGTH = 1.0f; -// duration in seconds of one bigstep -constexpr f32 STREAM_BIGSTEP_TIME = 0.3f; -// step duration for the ProxySoundManager, in seconds -constexpr f32 PROXYSOUNDMGR_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."); - - -/** - * RAII wrapper for openal sound buffers. - */ -struct RAIIALSoundBuffer final -{ - RAIIALSoundBuffer() noexcept = default; - explicit RAIIALSoundBuffer(ALuint buffer) noexcept : m_buffer(buffer) {}; - - ~RAIIALSoundBuffer() noexcept { reset(0); } - - DISABLE_CLASS_COPY(RAIIALSoundBuffer) - - RAIIALSoundBuffer(RAIIALSoundBuffer &&other) noexcept : m_buffer(other.release()) {} - RAIIALSoundBuffer &operator=(RAIIALSoundBuffer &&other) noexcept; - - ALuint get() noexcept { return m_buffer; } - - ALuint release() noexcept { return std::exchange(m_buffer, 0); } - - void reset(ALuint buf) noexcept; - - static RAIIALSoundBuffer generate() noexcept; - -private: - // According to openal specification: - // > Deleting buffer name 0 is a legal NOP. - // - // and: - // > [...] the NULL buffer (i.e., 0) which can always be queued. - ALuint m_buffer = 0; -}; - -/** - * For vorbisfile to read from our buffer instead of from a file. - */ -struct OggVorbisBufferSource { - std::string buf; - size_t cur_offset = 0; - - static size_t read_func(void *ptr, size_t size, size_t nmemb, void *datasource) noexcept; - static int seek_func(void *datasource, ogg_int64_t offset, int whence) noexcept; - static int close_func(void *datasource) noexcept; - static long tell_func(void *datasource) noexcept; - - static const ov_callbacks s_ov_callbacks; -}; - -/** - * Metadata of an Ogg-Vorbis file, used for decoding. - * We query this information once and store it in this struct. - */ -struct OggFileDecodeInfo { - std::string name_for_logging; - bool is_stereo; - ALenum format; // AL_FORMAT_MONO16 or AL_FORMAT_STEREO16 - size_t bytes_per_sample; - ALsizei freq; - ALuint length_samples = 0; - f32 length_seconds = 0.0f; -}; - -/** - * RAII wrapper for OggVorbis_File. - */ -struct RAIIOggFile { - bool m_needs_clear = false; - OggVorbis_File m_file; - - RAIIOggFile() = default; - - DISABLE_CLASS_COPY(RAIIOggFile) - - ~RAIIOggFile() noexcept - { - if (m_needs_clear) - ov_clear(&m_file); - } - - OggVorbis_File *get() { return &m_file; } - - std::optional getDecodeInfo(const std::string &filename_for_logging); - - /** - * Main function for loading ogg vorbis sounds. - * Loads exactly the specified interval of PCM-data, and creates an OpenAL - * buffer with it. - * - * @param decode_info Cached meta information of the file. - * @param pcm_start First sample in the interval. - * @param pcm_end One after last sample of the interval (=> exclusive). - * @return An AL sound buffer, or a 0-buffer on failure. - */ - RAIIALSoundBuffer loadBuffer(const OggFileDecodeInfo &decode_info, ALuint pcm_start, - ALuint pcm_end); -}; - - -/** - * Class for the openal device and context - */ -class SoundManagerSingleton -{ -public: - struct AlcDeviceDeleter { - void operator()(ALCdevice *p) - { - alcCloseDevice(p); - } - }; - - struct AlcContextDeleter { - void operator()(ALCcontext *p) - { - alcMakeContextCurrent(nullptr); - alcDestroyContext(p); - } - }; - - using unique_ptr_alcdevice = std::unique_ptr; - using unique_ptr_alccontext = std::unique_ptr; - - unique_ptr_alcdevice m_device; - unique_ptr_alccontext m_context; - -public: - bool init(); - - ~SoundManagerSingleton(); -}; - - -/** - * Stores sound pcm data buffers. - */ -struct ISoundDataOpen -{ - OggFileDecodeInfo m_decode_info; - - explicit ISoundDataOpen(const OggFileDecodeInfo &decode_info) : - m_decode_info(decode_info) {} - - virtual ~ISoundDataOpen() = default; - - /** - * Iff the data is streaming, there is more than one buffer. - * @return Whether it's streaming data. - */ - virtual bool isStreaming() const noexcept = 0; - - /** - * Load a buffer containing data starting at the given offset. Or just get it - * if it was already loaded. - * - * This function returns multiple values: - * * `buffer`: The OpenAL buffer. - * * `buffer_end`: The offset (in the file) where `buffer` ends (exclusive). - * * `offset_in_buffer`: Offset relative to `buffer`'s start where the requested - * `offset` is. - * `offset_in_buffer == 0` is guaranteed if some loaded buffer ends at - * `offset`. - * - * @param offset The start of the buffer. - * @return `{buffer, buffer_end, offset_in_buffer}` or `{0, sound_data_end, 0}` - * if `offset` is invalid. - */ - virtual std::tuple getOrLoadBufferAt(ALuint offset) = 0; - - static std::shared_ptr fromOggFile(std::unique_ptr oggfile, - const std::string &filename_for_logging); -}; - -/** - * Will be opened lazily when first used. - */ -struct ISoundDataUnopen -{ - virtual ~ISoundDataUnopen() = default; - - // Note: The ISoundDataUnopen is moved (see &&). It is not meant to be kept - // after opening. - virtual std::shared_ptr open(const std::string &sound_name) && = 0; -}; - -/** - * Sound file is in a memory buffer. - */ -struct SoundDataUnopenBuffer final : ISoundDataUnopen -{ - std::string m_buffer; - - explicit SoundDataUnopenBuffer(std::string &&buffer) : m_buffer(std::move(buffer)) {} - - std::shared_ptr open(const std::string &sound_name) && override; -}; - -/** - * Sound file is in file system. - */ -struct SoundDataUnopenFile final : ISoundDataUnopen -{ - std::string m_path; - - explicit SoundDataUnopenFile(const std::string &path) : m_path(path) {} - - std::shared_ptr open(const std::string &sound_name) && override; -}; - -/** - * Non-streaming opened sound data. - * All data is completely loaded in one buffer. - */ -struct SoundDataOpenBuffer final : ISoundDataOpen -{ - RAIIALSoundBuffer m_buffer; - - SoundDataOpenBuffer(std::unique_ptr oggfile, - const OggFileDecodeInfo &decode_info); - - bool isStreaming() const noexcept override { return false; } - - std::tuple getOrLoadBufferAt(ALuint offset) override - { - if (offset >= m_decode_info.length_samples) - return {0, m_decode_info.length_samples, 0}; - return {m_buffer.get(), m_decode_info.length_samples, offset}; - } -}; - -/** - * Streaming opened sound data. - * - * Uses a sorted list of contiguous sound data regions (`ContiguousBuffers`s) for - * efficient seeking. - */ -struct SoundDataOpenStream final : ISoundDataOpen -{ - /** - * An OpenAL buffer that goes until `m_end` (exclusive). - */ - struct SoundBufferUntil final - { - ALuint m_end; - RAIIALSoundBuffer m_buffer; - }; - - /** - * A sorted non-empty vector of contiguous buffers. - * The start (inclusive) of each buffer is the end of its predecessor, or - * `m_start` for the first buffer. - */ - struct ContiguousBuffers final - { - ALuint m_start; - std::vector m_buffers; - }; - - std::unique_ptr m_oggfile; - // A sorted vector of non-overlapping, non-contiguous `ContiguousBuffers`s. - std::vector m_bufferss; - - SoundDataOpenStream(std::unique_ptr oggfile, - const OggFileDecodeInfo &decode_info); - - bool isStreaming() const noexcept override { return true; } - - std::tuple getOrLoadBufferAt(ALuint offset) override; - -private: - // offset must be before after_it's m_start and after (after_it-1)'s last m_end - // new buffer will be inserted into m_bufferss before after_it - // returns same as getOrLoadBufferAt - std::tuple loadBufferAt(ALuint offset, - std::vector::iterator after_it); -}; - - -/** - * A sound that is currently played. - * Can be streaming. - * Can be fading. - */ -class PlayingSound final -{ - struct FadeState { - f32 step; - f32 target_gain; - }; - - ALuint m_source_id; - std::shared_ptr m_data; - ALuint m_next_sample_pos = 0; - bool m_looping; - bool m_is_positional; - bool m_stopped_means_dead = true; - std::optional m_fade_state = std::nullopt; - -public: - PlayingSound(ALuint source_id, std::shared_ptr data, bool loop, - f32 volume, f32 pitch, f32 start_time, - const std::optional> &pos_vel_opt); - - ~PlayingSound() noexcept - { - alDeleteSources(1, &m_source_id); - } - - DISABLE_CLASS_COPY(PlayingSound) - - // return false means streaming finished - bool stepStream(); - - // retruns true if it wasn't fading already - bool fade(f32 step, f32 target_gain) noexcept; - - // returns true if more fade is needed later - bool doFade(f32 dtime) noexcept; - - void updatePosVel(const v3f &pos, const v3f &vel) noexcept; - - void setGain(f32 gain) noexcept; - - f32 getGain() noexcept; - - void setPitch(f32 pitch) noexcept { alSourcef(m_source_id, AL_PITCH, pitch); } - - bool isStreaming() const noexcept { return m_data->isStreaming(); } - - void play() noexcept { alSourcePlay(m_source_id); } - - // returns one of AL_INITIAL, AL_PLAYING, AL_PAUSED, AL_STOPPED - ALint getState() noexcept - { - ALint state; - alGetSourcei(m_source_id, AL_SOURCE_STATE, &state); - return state; - } - - bool isDead() noexcept - { - // streaming sounds can (but should not) stop because the queue runs empty - return m_stopped_means_dead && getState() == AL_STOPPED; - } - - void pause() noexcept - { - // this is a NOP if state != AL_PLAYING - alSourcePause(m_source_id); - } - - void resume() noexcept - { - if (getState() == AL_PAUSED) - play(); - } -}; - - -/* - * The SoundManager thread - */ - -namespace sound_manager_messages_to_mgr { - struct PauseAll {}; - struct ResumeAll {}; - - struct UpdateListener { v3f pos_; v3f vel_; v3f at_; v3f up_; }; - struct SetListenerGain { f32 gain; }; - - struct LoadSoundFile { std::string name; std::string filepath; }; - struct LoadSoundData { std::string name; std::string filedata; }; - struct AddSoundToGroup { std::string sound_name; std::string group_name; }; - - struct PlaySound { sound_handle_t id; SoundSpec spec; }; - struct PlaySoundAt { sound_handle_t id; SoundSpec spec; v3f pos_; v3f vel_; }; - struct StopSound { sound_handle_t sound; }; - struct FadeSound { sound_handle_t soundid; f32 step; f32 target_gain; }; - struct UpdateSoundPosVel { sound_handle_t sound; v3f pos_; v3f vel_; }; - - struct PleaseStop {}; -} - -using SoundManagerMsgToMgr = std::variant< - std::monostate, - - sound_manager_messages_to_mgr::PauseAll, - sound_manager_messages_to_mgr::ResumeAll, - - sound_manager_messages_to_mgr::UpdateListener, - sound_manager_messages_to_mgr::SetListenerGain, - - sound_manager_messages_to_mgr::LoadSoundFile, - sound_manager_messages_to_mgr::LoadSoundData, - sound_manager_messages_to_mgr::AddSoundToGroup, - - sound_manager_messages_to_mgr::PlaySound, - sound_manager_messages_to_mgr::PlaySoundAt, - sound_manager_messages_to_mgr::StopSound, - sound_manager_messages_to_mgr::FadeSound, - sound_manager_messages_to_mgr::UpdateSoundPosVel, - - sound_manager_messages_to_mgr::PleaseStop - >; - -namespace sound_manager_messages_to_proxy { - struct ReportRemovedSound { sound_handle_t id; }; - - struct Stopped {}; -} - -using SoundManagerMsgToProxy = std::variant< - std::monostate, - - sound_manager_messages_to_proxy::ReportRemovedSound, - - sound_manager_messages_to_proxy::Stopped - >; - -// not an ISoundManager. doesn't allocate ids, and doesn't accept id 0 -class OpenALSoundManager final : public Thread -{ -private: - std::unique_ptr m_fallback_path_provider; - - ALCdevice *m_device; - ALCcontext *m_context; - - // time in seconds until which removeDeadSounds will be called again - f32 m_time_until_dead_removal = REMOVE_DEAD_SOUNDS_INTERVAL; - - // loaded sounds - std::unordered_map> m_sound_datas_unopen; - std::unordered_map> m_sound_datas_open; - // sound groups - std::unordered_map> m_sound_groups; - - // currently playing sounds - std::unordered_map> m_sounds_playing; - - // streamed sounds - std::vector> m_sounds_streaming_current_bigstep; - std::vector> m_sounds_streaming_next_bigstep; - // time left until current bigstep finishes - f32 m_stream_timer = STREAM_BIGSTEP_TIME; - - std::vector> m_sounds_fading; - - // if true, all sounds will be directly paused after creation - bool m_is_paused = false; - -public: - // used for communication with ProxySoundManager - MutexedQueue m_queue_to_mgr; - MutexedQueue m_queue_to_proxy; - -private: - void stepStreams(f32 dtime); - void doFades(f32 dtime); - - /** - * Gives the open sound for a loaded sound. - * Opens the sound if currently unopened. - * - * @param sound_name Name of the sound. - * @return The open sound. - */ - std::shared_ptr openSingleSound(const std::string &sound_name); - - /** - * Gets a random sound name from a group. - * - * @param group_name The name of the sound group. - * @return The name of a sound in the group, or "" on failure. Getting the - * sound with `openSingleSound` directly afterwards will not fail. - */ - std::string getLoadedSoundNameFromGroup(const std::string &group_name); - - /** - * Same as `getLoadedSoundNameFromGroup`, but if sound does not exist, try to - * load from local files. - */ - std::string getOrLoadLoadedSoundNameFromGroup(const std::string &group_name); - - std::shared_ptr createPlayingSound(const std::string &sound_name, - bool loop, f32 volume, f32 pitch, f32 start_time, - const std::optional> &pos_vel_opt); - - void playSoundGeneric(sound_handle_t id, const std::string &group_name, bool loop, - f32 volume, f32 fade, f32 pitch, bool use_local_fallback, f32 start_time, - const std::optional> &pos_vel_opt); - - /** - * Deletes sounds that are dead (=finished). - * - * @return Number of removed sounds. - */ - int removeDeadSounds(); - -public: - OpenALSoundManager(SoundManagerSingleton *smg, - std::unique_ptr fallback_path_provider); - - ~OpenALSoundManager() override; - - DISABLE_CLASS_COPY(OpenALSoundManager) - -private: - /* Similar to ISoundManager */ - - void step(f32 dtime); - void pauseAll(); - void resumeAll(); - - void updateListener(const v3f &pos_, const v3f &vel_, const v3f &at_, const v3f &up_); - void setListenerGain(f32 gain); - - bool loadSoundFile(const std::string &name, const std::string &filepath); - bool loadSoundData(const std::string &name, std::string &&filedata); - void loadSoundFileNoCheck(const std::string &name, const std::string &filepath); - void loadSoundDataNoCheck(const std::string &name, std::string &&filedata); - void addSoundToGroup(const std::string &sound_name, const std::string &group_name); - - void playSound(sound_handle_t id, const SoundSpec &spec); - void playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos_, - const v3f &vel_); - void stopSound(sound_handle_t sound); - void fadeSound(sound_handle_t soundid, f32 step, f32 target_gain); - void updateSoundPosVel(sound_handle_t sound, const v3f &pos_, const v3f &vel_); - -protected: - /* Thread stuff */ - - void *run() override; - -private: - void send(SoundManagerMsgToProxy msg) - { - m_queue_to_proxy.push_back(std::move(msg)); - } - - void reportRemovedSound(sound_handle_t id) - { - send(sound_manager_messages_to_proxy::ReportRemovedSound{id}); - } -}; - - -/* - * The public ISoundManager interface - */ - -class ProxySoundManager final : public ISoundManager -{ - OpenALSoundManager m_sound_manager; - // sound names from loadSoundData and loadSoundFile - std::unordered_set m_known_sound_names; - - void send(SoundManagerMsgToMgr msg) - { - m_sound_manager.m_queue_to_mgr.push_back(std::move(msg)); - } - - enum class MsgResult { Ok, Empty, Stopped}; - MsgResult handleMsg(SoundManagerMsgToProxy &&msg); - -public: - ProxySoundManager(SoundManagerSingleton *smg, - std::unique_ptr fallback_path_provider) : - m_sound_manager(smg, std::move(fallback_path_provider)) - { - m_sound_manager.start(); - } - - ~ProxySoundManager() override; - - /* Interface */ - - void step(f32 dtime) override; - void pauseAll() override; - void resumeAll() override; - - void updateListener(const v3f &pos_, const v3f &vel_, const v3f &at_, const v3f &up_) override; - void setListenerGain(f32 gain) override; - - bool loadSoundFile(const std::string &name, const std::string &filepath) override; - bool loadSoundData(const std::string &name, std::string &&filedata) override; - void addSoundToGroup(const std::string &sound_name, const std::string &group_name) override; - - void playSound(sound_handle_t id, const SoundSpec &spec) override; - void playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos_, - const v3f &vel_) override; - void stopSound(sound_handle_t sound) override; - void fadeSound(sound_handle_t soundid, f32 step, f32 target_gain) override; - void updateSoundPosVel(sound_handle_t sound, const v3f &pos_, const v3f &vel_) override; -}; diff --git a/src/client/sound/sound_singleton.cpp b/src/client/sound/sound_singleton.cpp new file mode 100644 index 000000000..26264fe76 --- /dev/null +++ b/src/client/sound/sound_singleton.cpp @@ -0,0 +1,69 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "sound_singleton.h" + +bool SoundManagerSingleton::init() +{ + if (!(m_device = unique_ptr_alcdevice(alcOpenDevice(nullptr)))) { + errorstream << "Audio: Global Initialization: Failed to open device" << std::endl; + return false; + } + + if (!(m_context = unique_ptr_alccontext(alcCreateContext(m_device.get(), nullptr)))) { + errorstream << "Audio: Global Initialization: Failed to create context" << std::endl; + return false; + } + + if (!alcMakeContextCurrent(m_context.get())) { + errorstream << "Audio: Global Initialization: Failed to make current context" << std::endl; + return false; + } + + alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); + + // Speed of sound in nodes per second + // FIXME: This value assumes 1 node sidelength = 1 meter, and "normal" air. + // Ideally this should be mod-controlled. + alSpeedOfSound(343.3f); + + // doppler effect turned off for now, for best backwards compatibility + alDopplerFactor(0.0f); + + if (alGetError() != AL_NO_ERROR) { + errorstream << "Audio: Global Initialization: OpenAL Error " << alGetError() << std::endl; + return false; + } + + infostream << "Audio: Global Initialized: OpenAL " << alGetString(AL_VERSION) + << ", using " << alcGetString(m_device.get(), ALC_DEVICE_SPECIFIER) + << std::endl; + + return true; +} + +SoundManagerSingleton::~SoundManagerSingleton() +{ + infostream << "Audio: Global Deinitialized." << std::endl; +} diff --git a/src/client/sound/sound_singleton.h b/src/client/sound/sound_singleton.h new file mode 100644 index 000000000..a3eb2dfa3 --- /dev/null +++ b/src/client/sound/sound_singleton.h @@ -0,0 +1,60 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; ifnot, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "al_helpers.h" + +/** + * Class for the openal device and context + */ +class SoundManagerSingleton +{ +public: + struct AlcDeviceDeleter { + void operator()(ALCdevice *p) + { + alcCloseDevice(p); + } + }; + + struct AlcContextDeleter { + void operator()(ALCcontext *p) + { + alcMakeContextCurrent(nullptr); + alcDestroyContext(p); + } + }; + + using unique_ptr_alcdevice = std::unique_ptr; + using unique_ptr_alccontext = std::unique_ptr; + + unique_ptr_alcdevice m_device; + unique_ptr_alccontext m_context; + +public: + bool init(); + + ~SoundManagerSingleton(); +};