94 changes: 94 additions & 0 deletions src/client/sound/ogg_file.h
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);
};
241 changes: 241 additions & 0 deletions src/client/sound/playing_sound.cpp
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;
}
107 changes: 107 additions & 0 deletions src/client/sound/playing_sound.h
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();
}
};
163 changes: 163 additions & 0 deletions src/client/sound/proxy_sound_manager.cpp
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_});
}
71 changes: 71 additions & 0 deletions src/client/sound/proxy_sound_manager.h
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;
};
119 changes: 119 additions & 0 deletions src/client/sound/sound_constants.h
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.");
231 changes: 231 additions & 0 deletions src/client/sound/sound_data.cpp
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};
}
173 changes: 173 additions & 0 deletions src/client/sound/sound_data.h
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);
};
523 changes: 523 additions & 0 deletions src/client/sound/sound_manager.cpp

Large diffs are not rendered by default.

171 changes: 171 additions & 0 deletions src/client/sound/sound_manager.h
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});
}
};
80 changes: 80 additions & 0 deletions src/client/sound/sound_manager_messages.h
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
>;
4 changes: 3 additions & 1 deletion src/client/sound/sound_openal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<SoundManagerSingleton> g_sound_manager_singleton;

Expand Down
1,363 changes: 0 additions & 1,363 deletions src/client/sound/sound_openal_internal.cpp

This file was deleted.

750 changes: 0 additions & 750 deletions src/client/sound/sound_openal_internal.h

This file was deleted.

69 changes: 69 additions & 0 deletions src/client/sound/sound_singleton.cpp
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;
}
60 changes: 60 additions & 0 deletions src/client/sound/sound_singleton.h
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();
};