| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| /* | ||
| Minetest | ||
| Copyright (C) 2022 DS | ||
| Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com> | ||
| OpenAL support based on work by: | ||
| Copyright (C) 2011 Sebastian 'Bahamada' Rühl | ||
| Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com> | ||
| Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com> | ||
| 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 <vorbis/vorbisfile.h> | ||
| #include <optional> | ||
| #include <string> | ||
|
|
||
| /** | ||
| * 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<OggFileDecodeInfo> 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); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,241 @@ | ||
| /* | ||
| Minetest | ||
| Copyright (C) 2022 DS | ||
| Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com> | ||
| OpenAL support based on work by: | ||
| Copyright (C) 2011 Sebastian 'Bahamada' Rühl | ||
| Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com> | ||
| Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com> | ||
| 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 <cassert> | ||
| #include <cmath> | ||
|
|
||
| PlayingSound::PlayingSound(ALuint source_id, std::shared_ptr<ISoundDataOpen> data, | ||
| bool loop, f32 volume, f32 pitch, f32 start_time, | ||
| const std::optional<std::pair<v3f, v3f>> &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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| /* | ||
| Minetest | ||
| Copyright (C) 2022 DS | ||
| Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com> | ||
| OpenAL support based on work by: | ||
| Copyright (C) 2011 Sebastian 'Bahamada' Rühl | ||
| Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com> | ||
| Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com> | ||
| 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<ISoundDataOpen> m_data; | ||
| ALuint m_next_sample_pos = 0; | ||
| bool m_looping; | ||
| bool m_is_positional; | ||
| bool m_stopped_means_dead = true; | ||
| std::optional<FadeState> m_fade_state = std::nullopt; | ||
|
|
||
| public: | ||
| PlayingSound(ALuint source_id, std::shared_ptr<ISoundDataOpen> data, bool loop, | ||
| f32 volume, f32 pitch, f32 start_time, | ||
| const std::optional<std::pair<v3f, v3f>> &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(); | ||
| } | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<decltype(msg)>; | ||
|
|
||
| if constexpr (std::is_same_v<T, std::monostate>) | ||
| return MsgResult::Empty; | ||
| else if constexpr (std::is_same_v<T, ReportRemovedSound>) | ||
| reportRemovedSound(msg.id); | ||
| else if constexpr (std::is_same_v<T, Stopped>) | ||
| 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_}); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<std::string> 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<SoundFallbackPathProvider> 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; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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."); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,231 @@ | ||
| /* | ||
| Minetest | ||
| Copyright (C) 2022 DS | ||
| Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com> | ||
| OpenAL support based on work by: | ||
| Copyright (C) 2011 Sebastian 'Bahamada' Rühl | ||
| Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com> | ||
| Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com> | ||
| 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> ISoundDataOpen::fromOggFile(std::unique_ptr<RAIIOggFile> oggfile, | ||
| const std::string &filename_for_logging) | ||
| { | ||
| // Get some information about the OGG file | ||
| std::optional<OggFileDecodeInfo> 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<SoundDataOpenBuffer>(std::move(oggfile), *decode_info); | ||
| } else { | ||
| return std::make_shared<SoundDataOpenStream>(std::move(oggfile), *decode_info); | ||
| } | ||
| } | ||
|
|
||
| /* | ||
| * SoundDataUnopenBuffer struct | ||
| */ | ||
|
|
||
| std::shared_ptr<ISoundDataOpen> SoundDataUnopenBuffer::open(const std::string &sound_name) && | ||
| { | ||
| // load from m_buffer | ||
|
|
||
| auto oggfile = std::make_unique<RAIIOggFile>(); | ||
|
|
||
| auto buffer_source = std::make_unique<OggVorbisBufferSource>(); | ||
| 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<ISoundDataOpen> SoundDataUnopenFile::open(const std::string &sound_name) && | ||
| { | ||
| // load from file at m_path | ||
|
|
||
| auto oggfile = std::make_unique<RAIIOggFile>(); | ||
|
|
||
| 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<RAIIOggFile> 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<RAIIOggFile> oggfile, | ||
| const OggFileDecodeInfo &decode_info) : | ||
| ISoundDataOpen(decode_info), m_oggfile(std::move(oggfile)) | ||
| { | ||
| // do nothing here. buffers are loaded at getOrLoadBufferAt | ||
| } | ||
|
|
||
| std::tuple<ALuint, ALuint, ALuint> 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<SoundBufferUntil> &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<ALuint, ALuint, ALuint> SoundDataOpenStream::loadBufferAt(ALuint offset, | ||
| std::vector<ContiguousBuffers>::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}; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| /* | ||
| Minetest | ||
| Copyright (C) 2022 DS | ||
| Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com> | ||
| OpenAL support based on work by: | ||
| Copyright (C) 2011 Sebastian 'Bahamada' Rühl | ||
| Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com> | ||
| Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com> | ||
| 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 <memory> | ||
| #include <tuple> | ||
|
|
||
| /** | ||
| * 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<ALuint, ALuint, ALuint> getOrLoadBufferAt(ALuint offset) = 0; | ||
|
|
||
| static std::shared_ptr<ISoundDataOpen> fromOggFile(std::unique_ptr<RAIIOggFile> 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<ISoundDataOpen> 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<ISoundDataOpen> 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<ISoundDataOpen> 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<RAIIOggFile> oggfile, | ||
| const OggFileDecodeInfo &decode_info); | ||
|
|
||
| bool isStreaming() const noexcept override { return false; } | ||
|
|
||
| std::tuple<ALuint, ALuint, ALuint> 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<SoundBufferUntil> m_buffers; | ||
| }; | ||
|
|
||
| std::unique_ptr<RAIIOggFile> m_oggfile; | ||
| // A sorted vector of non-overlapping, non-contiguous `ContiguousBuffers`s. | ||
| std::vector<ContiguousBuffers> m_bufferss; | ||
|
|
||
| SoundDataOpenStream(std::unique_ptr<RAIIOggFile> oggfile, | ||
| const OggFileDecodeInfo &decode_info); | ||
|
|
||
| bool isStreaming() const noexcept override { return true; } | ||
|
|
||
| std::tuple<ALuint, ALuint, ALuint> 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<ALuint, ALuint, ALuint> loadBufferAt(ALuint offset, | ||
| std::vector<ContiguousBuffers>::iterator after_it); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| /* | ||
| Minetest | ||
| Copyright (C) 2022 DS | ||
| Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com> | ||
| OpenAL support based on work by: | ||
| Copyright (C) 2011 Sebastian 'Bahamada' Rühl | ||
| Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com> | ||
| Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com> | ||
| 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<SoundFallbackPathProvider> 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<std::string, std::unique_ptr<ISoundDataUnopen>> m_sound_datas_unopen; | ||
| std::unordered_map<std::string, std::shared_ptr<ISoundDataOpen>> m_sound_datas_open; | ||
| // sound groups | ||
| std::unordered_map<std::string, std::vector<std::string>> m_sound_groups; | ||
|
|
||
| // currently playing sounds | ||
| std::unordered_map<sound_handle_t, std::shared_ptr<PlayingSound>> m_sounds_playing; | ||
|
|
||
| // streamed sounds | ||
| std::vector<std::weak_ptr<PlayingSound>> m_sounds_streaming_current_bigstep; | ||
| std::vector<std::weak_ptr<PlayingSound>> m_sounds_streaming_next_bigstep; | ||
| // time left until current bigstep finishes | ||
| f32 m_stream_timer = STREAM_BIGSTEP_TIME; | ||
|
|
||
| std::vector<std::weak_ptr<PlayingSound>> 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<SoundManagerMsgToMgr> m_queue_to_mgr; | ||
| MutexedQueue<SoundManagerMsgToProxy> 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<ISoundDataOpen> 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<PlayingSound> createPlayingSound(const std::string &sound_name, | ||
| bool loop, f32 volume, f32 pitch, f32 start_time, | ||
| const std::optional<std::pair<v3f, v3f>> &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<std::pair<v3f, v3f>> &pos_vel_opt); | ||
|
|
||
| /** | ||
| * Deletes sounds that are dead (=finished). | ||
| * | ||
| * @return Number of removed sounds. | ||
| */ | ||
| int removeDeadSounds(); | ||
|
|
||
| public: | ||
| OpenALSoundManager(SoundManagerSingleton *smg, | ||
| std::unique_ptr<SoundFallbackPathProvider> 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}); | ||
| } | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <variant> | ||
|
|
||
| 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 | ||
| >; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| /* | ||
| Minetest | ||
| Copyright (C) 2022 DS | ||
| Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com> | ||
| OpenAL support based on work by: | ||
| Copyright (C) 2011 Sebastian 'Bahamada' Rühl | ||
| Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com> | ||
| Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com> | ||
| 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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| /* | ||
| Minetest | ||
| Copyright (C) 2022 DS | ||
| Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com> | ||
| OpenAL support based on work by: | ||
| Copyright (C) 2011 Sebastian 'Bahamada' Rühl | ||
| Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com> | ||
| Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com> | ||
| 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<ALCdevice, AlcDeviceDeleter>; | ||
| using unique_ptr_alccontext = std::unique_ptr<ALCcontext, AlcContextDeleter>; | ||
|
|
||
| unique_ptr_alcdevice m_device; | ||
| unique_ptr_alccontext m_context; | ||
|
|
||
| public: | ||
| bool init(); | ||
|
|
||
| ~SoundManagerSingleton(); | ||
| }; |