minetest/src/client/sound_openal_internal.h

614 lines
18 KiB
C++

/*
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 "log.h"
#include "porting.h"
#include "sound_openal.h"
#include "util/basic_macros.h"
#if defined(_WIN32)
#include <al.h>
#include <alc.h>
//#include <alext.h>
#elif defined(__APPLE__)
#define OPENAL_DEPRECATED
#include <OpenAL/al.h>
#include <OpenAL/alc.h>
//#include <OpenAL/alext.h>
#else
#include <AL/al.h>
#include <AL/alc.h>
#include <AL/alext.h>
#endif
#include <vorbis/vorbisfile.h>
#include <optional>
#include <unordered_map>
#include <utility>
#include <vector>
/*
*
* The coordinate space for sounds (sound-space):
* ----------------------------------------------
*
* * The functions from ISoundManager (see sound.h) take spatial vectors in node-space.
* * All other `v3f`s here are, if not told otherwise, in sound-space, which is
* defined as node-space mirrored along the x-axis.
* (This is needed because OpenAL uses a right-handed coordinate system.)
* * Use `swap_handedness()` to convert between those two coordinate spaces.
*
*
* How sounds are loaded:
* ----------------------
*
* * Step 1:
* `loadSoundFile` or `loadSoundFile` is called. This adds an unopen sound with
* the given name to `m_sound_datas_unopen`.
* Unopen / lazy sounds (`ISoundDataUnopen`) are ogg-vorbis files that we did not yet
* start to decode. (Decoding an unopen sound does not fail under normal circumstances
* (because we check whether the file exists at least), if it does fail anyways,
* we should notify the user.)
* * Step 2:
* `addSoundToGroup` is called, to add the name from step 1 to a group. If the
* group does not yet exist, a new one is created. A group can later be played.
* (The mapping is stored in `m_sound_groups`.)
* * Step 3:
* `playSound` or `playSoundAt` is called.
* * Step 3.1:
* If the group with the name `spec.name` does not exist, and `spec.use_local_fallback`
* is true, a new group is created using the user's sound-pack.
* * Step 3.2:
* We choose one random sound name from the given group.
* * Step 3.3:
* We open the sound (see `openSingleSound`).
* If the sound is already open (in `m_sound_datas_open`), we take that one.
* Otherwise we open it by calling `ISoundDataUnopen::open`. We choose (by
* sound length), whether it's a single-buffer (`SoundDataOpenBuffer`) or
* streamed (`SoundDataOpenStream`) sound.
* Single-buffer sounds are always completely loaded. Streamed sounds can be
* partially loaded.
* The sound is erased from `m_sound_datas_unopen` and added to `m_sound_datas_open`.
* Open sounds are kept forever.
* * Step 3.4:
* We create the new `PlayingSound`. It has a `shared_ptr` to its open sound.
* If the open sound is streaming, the playing sound needs to be stepped using
* `PlayingSound::stepStream` for enqueuing buffers. For this purpose, the sound
* is added to `m_sounds_streaming` (as `weak_ptr`).
* If the sound is fading, it is added to `m_sounds_fading` for regular fade-stepping.
* The sound is also added to `m_sounds_playing`, so that one can access it
* via its sound handle.
* * Step 4:
* Streaming sounds are updated. For details see [Streaming of sounds].
* * Step 5:
* At deinitialization, we can just let the destructors do their work.
* Sound sources are deleted (and with this also stopped) by ~PlayingSound.
* Buffers can't be deleted while sound sources using them exist, because
* PlayingSound has a shared_ptr to its ISoundData.
*
*
* Streaming of sounds:
* --------------------
*
* In each "bigstep", all streamed sounds are stepStream()ed. This means a
* sound can be stepped at any point in time in the bigstep's interval.
*
* In the worst case, a sound is stepped at the start of one bigstep and in the
* end of the next bigstep. So between two stepStream()-calls lie at most
* 2 * STREAM_BIGSTEP_TIME seconds.
* As there are always 2 sound buffers enqueued, at least one untouched full buffer
* is still available after the first stepStream().
* If we take a MIN_STREAM_BUFFER_LENGTH > 2 * STREAM_BIGSTEP_TIME, we can hence
* not run into an empty queue.
*
* The MIN_STREAM_BUFFER_LENGTH needs to be a little bigger because of dtime jitter,
* other sounds that may have taken long to stepStream(), and sounds being played
* faster due to Doppler effect.
*
*/
// constants
// in seconds
constexpr f32 REMOVE_DEAD_SOUNDS_INTERVAL = 2.0f;
// maximum length in seconds that a sound can have without being streamed
constexpr f32 SOUND_DURATION_MAX_SINGLE = 3.0f;
// minimum time in seconds of a single buffer in a streamed sound
constexpr f32 MIN_STREAM_BUFFER_LENGTH = 1.0f;
// duration in seconds of one bigstep
constexpr f32 STREAM_BIGSTEP_TIME = 0.3f;
static_assert(MIN_STREAM_BUFFER_LENGTH > STREAM_BIGSTEP_TIME * 2.0f,
"See [Streaming of sounds].");
static_assert(SOUND_DURATION_MAX_SINGLE >= MIN_STREAM_BUFFER_LENGTH * 2.0f,
"There's no benefit in streaming if we can't queue more than 2 buffers.");
/**
* RAII wrapper for openal sound buffers.
*/
struct RAIIALSoundBuffer final
{
RAIIALSoundBuffer() noexcept = default;
explicit RAIIALSoundBuffer(ALuint buffer) noexcept : m_buffer(buffer) {};
~RAIIALSoundBuffer() noexcept { reset(0); }
DISABLE_CLASS_COPY(RAIIALSoundBuffer)
RAIIALSoundBuffer(RAIIALSoundBuffer &&other) noexcept : m_buffer(other.release()) {}
RAIIALSoundBuffer &operator=(RAIIALSoundBuffer &&other) noexcept;
ALuint get() noexcept { return m_buffer; }
ALuint release() noexcept { return std::exchange(m_buffer, 0); }
void reset(ALuint buf) noexcept;
static RAIIALSoundBuffer generate() noexcept;
private:
// According to openal specification:
// > Deleting buffer name 0 is a legal NOP.
//
// and:
// > [...] the NULL buffer (i.e., 0) which can always be queued.
ALuint m_buffer = 0;
};
/**
* For vorbisfile to read from our buffer instead of from a file.
*/
struct OggVorbisBufferSource {
std::string buf;
size_t cur_offset = 0;
static size_t read_func(void *ptr, size_t size, size_t nmemb, void *datasource) noexcept;
static int seek_func(void *datasource, ogg_int64_t offset, int whence) noexcept;
static int close_func(void *datasource) noexcept;
static long tell_func(void *datasource) noexcept;
static const ov_callbacks s_ov_callbacks;
};
/**
* Metadata of an Ogg-Vorbis file, used for decoding.
* We query this information once and store it in this struct.
*/
struct OggFileDecodeInfo {
std::string name_for_logging;
bool is_stereo;
ALenum format; // AL_FORMAT_MONO16 or AL_FORMAT_STEREO16
size_t bytes_per_sample;
ALsizei freq;
ALuint length_samples = 0;
f32 length_seconds = 0.0f;
};
/**
* RAII wrapper for OggVorbis_File.
*/
struct RAIIOggFile {
bool m_needs_clear = false;
OggVorbis_File m_file;
RAIIOggFile() = default;
DISABLE_CLASS_COPY(RAIIOggFile)
~RAIIOggFile() noexcept
{
if (m_needs_clear)
ov_clear(&m_file);
}
OggVorbis_File *get() { return &m_file; }
std::optional<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);
};
/**
* 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();
};
/**
* 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);
};
/**
* 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();
}
};
/*
* The public ISoundManager interface
*/
class OpenALSoundManager final : public ISoundManager
{
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;
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)
/* 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;
};