1
0
Fork 0
mirror of https://github.com/luanti-org/luanti.git synced 2025-10-10 19:32:10 +00:00

Sound refactor and improvements (#12764)

This commit is contained in:
DS 2023-06-16 20:15:21 +02:00 committed by GitHub
parent 8e1af25738
commit edcbfa31c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 2802 additions and 1211 deletions

View file

@ -1,8 +1,9 @@
set(sound_SRCS "")
set(sound_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/sound.cpp)
if(USE_SOUND)
set(sound_SRCS ${sound_SRCS}
${CMAKE_CURRENT_SOURCE_DIR}/sound_openal.cpp)
${CMAKE_CURRENT_SOURCE_DIR}/sound_openal.cpp
${CMAKE_CURRENT_SOURCE_DIR}/sound_openal_internal.cpp)
set(SOUND_INCLUDE_DIRS
${OPENAL_INCLUDE_DIR}
${VORBIS_INCLUDE_DIR}

View file

@ -30,7 +30,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "settings.h"
#include "wieldmesh.h"
#include "noise.h" // easeCurve
#include "sound.h"
#include "mtevent.h"
#include "nodedef.h"
#include "util/numeric.h"

View file

@ -374,6 +374,11 @@ Client::~Client()
if (m_mod_storage_database)
m_mod_storage_database->endSave();
delete m_mod_storage_database;
// Free sound ids
for (auto &csp : m_sounds_client_to_server)
m_sound->freeId(csp.first);
m_sounds_client_to_server.clear();
}
void Client::connect(Address address, bool is_local_server)
@ -703,12 +708,13 @@ void Client::step(float dtime)
*/
{
for (auto &m_sounds_to_object : m_sounds_to_objects) {
int client_id = m_sounds_to_object.first;
sound_handle_t client_id = m_sounds_to_object.first;
u16 object_id = m_sounds_to_object.second;
ClientActiveObject *cao = m_env.getActiveObject(object_id);
if (!cao)
continue;
m_sound->updateSoundPosition(client_id, cao->getPosition());
m_sound->updateSoundPosVel(client_id, cao->getPosition() * (1.0f/BS),
cao->getVelocity() * (1.0f/BS));
}
}
@ -719,22 +725,24 @@ void Client::step(float dtime)
if(m_removed_sounds_check_timer >= 2.32) {
m_removed_sounds_check_timer = 0;
// Find removed sounds and clear references to them
std::vector<sound_handle_t> removed_client_ids = m_sound->pollRemovedSounds();
std::vector<s32> removed_server_ids;
for (std::unordered_map<s32, int>::iterator i = m_sounds_server_to_client.begin();
i != m_sounds_server_to_client.end();) {
s32 server_id = i->first;
int client_id = i->second;
++i;
if(!m_sound->soundExists(client_id)) {
for (sound_handle_t client_id : removed_client_ids) {
auto client_to_server_id_it = m_sounds_client_to_server.find(client_id);
if (client_to_server_id_it == m_sounds_client_to_server.end())
continue;
s32 server_id = client_to_server_id_it->second;
m_sound->freeId(client_id);
m_sounds_client_to_server.erase(client_to_server_id_it);
if (server_id != -1) {
m_sounds_server_to_client.erase(server_id);
m_sounds_client_to_server.erase(client_id);
m_sounds_to_objects.erase(client_id);
removed_server_ids.push_back(server_id);
}
m_sounds_to_objects.erase(client_id);
}
// Sync to server
if(!removed_server_ids.empty()) {
if (!removed_server_ids.empty()) {
sendRemovedSounds(removed_server_ids);
}
}
@ -800,9 +808,13 @@ bool Client::loadMedia(const std::string &data, const std::string &filename,
};
name = removeStringEnd(filename, sound_ext);
if (!name.empty()) {
TRACESTREAM(<< "Client: Attempting to load sound "
<< "file \"" << filename << "\"" << std::endl);
return m_sound->loadSoundData(name, data);
TRACESTREAM(<< "Client: Attempting to load sound file \""
<< filename << "\"" << std::endl);
if (!m_sound->loadSoundData(filename, std::string(data)))
return false;
// "name[.num].ogg" is in group "name"
m_sound->addSoundToGroup(filename, name);
return true;
}
const char *model_ext[] = {
@ -1205,7 +1217,7 @@ void Client::sendGotBlocks(const std::vector<v3s16> &blocks)
Send(&pkt);
}
void Client::sendRemovedSounds(std::vector<s32> &soundList)
void Client::sendRemovedSounds(const std::vector<s32> &soundList)
{
size_t server_ids = soundList.size();
assert(server_ids <= 0xFFFF);

View file

@ -70,6 +70,7 @@ class NetworkPacket;
namespace con {
class Connection;
}
using sound_handle_t = int;
enum LocalClientState {
LC_Created,
@ -468,7 +469,7 @@ private:
void startAuth(AuthMechanism chosen_auth_mechanism);
void sendDeletedBlocks(std::vector<v3s16> &blocks);
void sendGotBlocks(const std::vector<v3s16> &blocks);
void sendRemovedSounds(std::vector<s32> &soundList);
void sendRemovedSounds(const std::vector<s32> &soundList);
bool canSendChatMessage() const;
@ -564,11 +565,12 @@ private:
// Sounds
float m_removed_sounds_check_timer = 0.0f;
// Mapping from server sound ids to our sound ids
std::unordered_map<s32, int> m_sounds_server_to_client;
std::unordered_map<s32, sound_handle_t> m_sounds_server_to_client;
// And the other way!
std::unordered_map<int, s32> m_sounds_client_to_server;
// This takes ownership for the sound handles.
std::unordered_map<sound_handle_t, s32> m_sounds_client_to_server;
// Relation of client id to object id
std::unordered_map<int, u16> m_sounds_to_objects;
std::unordered_map<sound_handle_t, u16> m_sounds_to_objects;
// Privileges
std::unordered_set<std::string> m_privileges;

View file

@ -47,7 +47,8 @@ public:
virtual bool getCollisionBox(aabb3f *toset) const { return false; }
virtual bool getSelectionBox(aabb3f *toset) const { return false; }
virtual bool collideWithObjects() const { return false; }
virtual const v3f getPosition() const { return v3f(0.0f); }
virtual const v3f getPosition() const { return v3f(0.0f); } // in BS-space
virtual const v3f getVelocity() const { return v3f(0.0f); } // in BS-space
virtual scene::ISceneNode *getSceneNode() const
{ return NULL; }
virtual scene::IAnimatedMeshSceneNode *getAnimatedMeshSceneNode() const

View file

@ -1179,11 +1179,12 @@ void GenericCAO::step(float dtime, ClientEnvironment *env)
v3s16 node_below_pos = floatToInt(foot_pos + v3f(0.0f, -0.5f, 0.0f),
1.0f);
MapNode n = m_env->getMap().getNode(node_below_pos);
SimpleSoundSpec spec = ndef->get(n).sound_footstep;
SoundSpec spec = ndef->get(n).sound_footstep;
// Reduce footstep gain, as non-local-player footsteps are
// somehow louder.
spec.gain *= 0.6f;
m_client->sound()->playSoundAt(spec, foot_pos * BS);
// The footstep-sound doesn't travel with the object. => vel=0
m_client->sound()->playSoundAt(0, spec, foot_pos, v3f(0.0f));
}
}
}

View file

@ -162,7 +162,9 @@ public:
virtual bool getSelectionBox(aabb3f *toset) const;
const v3f getPosition() const;
const v3f getPosition() const override final;
const v3f getVelocity() const override final { return m_velocity; }
inline const v3f &getRotation() const { return m_rotation; }

View file

@ -260,31 +260,25 @@ class SoundMaker
const NodeDefManager *m_ndef;
public:
bool makes_footstep_sound;
float m_player_step_timer;
float m_player_jump_timer;
bool makes_footstep_sound = true;
float m_player_step_timer = 0.0f;
float m_player_jump_timer = 0.0f;
SimpleSoundSpec m_player_step_sound;
SimpleSoundSpec m_player_leftpunch_sound;
SoundSpec m_player_step_sound;
SoundSpec m_player_leftpunch_sound;
// Second sound made on left punch, currently used for item 'use' sound
SimpleSoundSpec m_player_leftpunch_sound2;
SimpleSoundSpec m_player_rightpunch_sound;
SoundSpec m_player_leftpunch_sound2;
SoundSpec m_player_rightpunch_sound;
SoundMaker(ISoundManager *sound, const NodeDefManager *ndef):
m_sound(sound),
m_ndef(ndef),
makes_footstep_sound(true),
m_player_step_timer(0.0f),
m_player_jump_timer(0.0f)
{
}
SoundMaker(ISoundManager *sound, const NodeDefManager *ndef) :
m_sound(sound), m_ndef(ndef) {}
void playPlayerStep()
{
if (m_player_step_timer <= 0 && m_player_step_sound.exists()) {
m_player_step_timer = 0.03;
if (makes_footstep_sound)
m_sound->playSound(m_player_step_sound);
m_sound->playSound(0, m_player_step_sound);
}
}
@ -292,7 +286,7 @@ public:
{
if (m_player_jump_timer <= 0.0f) {
m_player_jump_timer = 0.2f;
m_sound->playSound(SimpleSoundSpec("player_jump", 0.5f));
m_sound->playSound(0, SoundSpec("player_jump", 0.5f));
}
}
@ -317,33 +311,33 @@ public:
static void cameraPunchLeft(MtEvent *e, void *data)
{
SoundMaker *sm = (SoundMaker *)data;
sm->m_sound->playSound(sm->m_player_leftpunch_sound);
sm->m_sound->playSound(sm->m_player_leftpunch_sound2);
sm->m_sound->playSound(0, sm->m_player_leftpunch_sound);
sm->m_sound->playSound(0, sm->m_player_leftpunch_sound2);
}
static void cameraPunchRight(MtEvent *e, void *data)
{
SoundMaker *sm = (SoundMaker *)data;
sm->m_sound->playSound(sm->m_player_rightpunch_sound);
sm->m_sound->playSound(0, sm->m_player_rightpunch_sound);
}
static void nodeDug(MtEvent *e, void *data)
{
SoundMaker *sm = (SoundMaker *)data;
NodeDugEvent *nde = (NodeDugEvent *)e;
sm->m_sound->playSound(sm->m_ndef->get(nde->n).sound_dug);
sm->m_sound->playSound(0, sm->m_ndef->get(nde->n).sound_dug);
}
static void playerDamage(MtEvent *e, void *data)
{
SoundMaker *sm = (SoundMaker *)data;
sm->m_sound->playSound(SimpleSoundSpec("player_damage", 0.5));
sm->m_sound->playSound(0, SoundSpec("player_damage", 0.5));
}
static void playerFallingDamage(MtEvent *e, void *data)
{
SoundMaker *sm = (SoundMaker *)data;
sm->m_sound->playSound(SimpleSoundSpec("player_falling_damage", 0.5));
sm->m_sound->playSound(0, SoundSpec("player_falling_damage", 0.5));
}
void registerReceiver(MtEventManager *mgr)
@ -365,42 +359,6 @@ public:
}
};
// Locally stored sounds don't need to be preloaded because of this
class GameOnDemandSoundFetcher: public OnDemandSoundFetcher
{
std::set<std::string> m_fetched;
private:
void paths_insert(std::set<std::string> &dst_paths,
const std::string &base,
const std::string &name)
{
dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".ogg");
dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".0.ogg");
dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".1.ogg");
dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".2.ogg");
dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".3.ogg");
dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".4.ogg");
dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".5.ogg");
dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".6.ogg");
dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".7.ogg");
dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".8.ogg");
dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".9.ogg");
}
public:
void fetchSounds(const std::string &name,
std::set<std::string> &dst_paths,
std::set<std::string> &dst_datas)
{
if (m_fetched.count(name))
return;
m_fetched.insert(name);
paths_insert(dst_paths, porting::path_share, name);
paths_insert(dst_paths, porting::path_user, name);
}
};
typedef s32 SamplerLayer_t;
@ -936,7 +894,6 @@ private:
IWritableItemDefManager *itemdef_manager = nullptr;
NodeDefManager *nodedef_manager = nullptr;
GameOnDemandSoundFetcher soundfetcher; // useful when testing
std::unique_ptr<ISoundManager> sound_manager;
SoundMaker *soundmaker = nullptr;
@ -1278,10 +1235,13 @@ void Game::run()
if (m_is_paused)
dtime = 0.0f;
if (!was_paused && m_is_paused)
if (!was_paused && m_is_paused) {
pauseAnimation();
else if (was_paused && !m_is_paused)
sound_manager->pauseAll();
} else if (was_paused && !m_is_paused) {
resumeAnimation();
sound_manager->resumeAll();
}
}
if (!m_is_paused)
@ -1397,11 +1357,13 @@ bool Game::initSound()
#if USE_SOUND
if (g_settings->getBool("enable_sound") && g_sound_manager_singleton.get()) {
infostream << "Attempting to use OpenAL audio" << std::endl;
sound_manager.reset(createOpenALSoundManager(g_sound_manager_singleton.get(), &soundfetcher));
sound_manager = createOpenALSoundManager(g_sound_manager_singleton.get(),
std::make_unique<SoundFallbackPathProvider>());
if (!sound_manager)
infostream << "Failed to initialize OpenAL audio" << std::endl;
} else
} else {
infostream << "Sound disabled." << std::endl;
}
#endif
if (!sound_manager) {
@ -3194,10 +3156,13 @@ void Game::updateCamera(f32 dtime)
void Game::updateSound(f32 dtime)
{
// Update sound listener
LocalPlayer *player = client->getEnv().getLocalPlayer();
ClientActiveObject *parent = player->getParent();
v3s16 camera_offset = camera->getOffset();
sound_manager->updateListener(
camera->getCameraNode()->getPosition() + intToFloat(camera_offset, BS),
v3f(0, 0, 0), // velocity
(1.0f/BS) * camera->getCameraNode()->getPosition()
+ intToFloat(camera_offset, 1.0f),
(1.0f/BS) * (parent ? parent->getVelocity() : player->getSpeed()),
camera->getDirection(),
camera->getCameraNode()->getUpVector());
@ -3215,8 +3180,6 @@ void Game::updateSound(f32 dtime)
}
}
LocalPlayer *player = client->getEnv().getLocalPlayer();
// Tell the sound maker whether to make footstep sounds
soundmaker->makes_footstep_sound = player->makes_footstep_sound;
@ -3332,7 +3295,7 @@ void Game::processPlayerInteraction(f32 dtime, bool show_hud)
runData.punching = false;
soundmaker->m_player_leftpunch_sound = SimpleSoundSpec();
soundmaker->m_player_leftpunch_sound = SoundSpec();
soundmaker->m_player_leftpunch_sound2 = pointed.type != POINTEDTHING_NOTHING ?
selected_def.sound_use : selected_def.sound_use_air;
@ -3530,7 +3493,7 @@ void Game::handlePointingAtNode(const PointedThing &pointed,
// Placing animation (always shown for feedback)
camera->setDigging(1);
soundmaker->m_player_rightpunch_sound = SimpleSoundSpec();
soundmaker->m_player_rightpunch_sound = SoundSpec();
// If the wielded item has node placement prediction,
// make that happen

97
src/client/sound.cpp Normal file
View file

@ -0,0 +1,97 @@
/*
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; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "sound.h"
#include "filesys.h"
#include "log.h"
#include "porting.h"
#include "util/numeric.h"
#include <algorithm>
#include <string>
#include <vector>
std::vector<std::string> SoundFallbackPathProvider::
getLocalFallbackPathsForSoundname(const std::string &name)
{
std::vector<std::string> paths;
// only try each name once
if (m_done_names.count(name))
return paths;
m_done_names.insert(name);
addThePaths(name, paths);
// remove duplicates
std::sort(paths.begin(), paths.end());
auto end = std::unique(paths.begin(), paths.end());
paths.erase(end, paths.end());
return paths;
}
void SoundFallbackPathProvider::addAllAlternatives(const std::string &common,
std::vector<std::string> &paths)
{
paths.reserve(paths.size() + 11);
for (auto &&ext : {".ogg", ".0.ogg", ".1.ogg", ".2.ogg", ".3.ogg", ".4.ogg",
".5.ogg", ".6.ogg", ".7.ogg", ".8.ogg", ".9.ogg", }) {
paths.push_back(common + ext);
}
}
void SoundFallbackPathProvider::addThePaths(const std::string &name,
std::vector<std::string> &paths)
{
addAllAlternatives(porting::path_share + DIR_DELIM + "sounds" + DIR_DELIM + name, paths);
addAllAlternatives(porting::path_user + DIR_DELIM + "sounds" + DIR_DELIM + name, paths);
}
void ISoundManager::reportRemovedSound(sound_handle_t id)
{
if (id <= 0)
return;
freeId(id);
m_removed_sounds.push_back(id);
}
sound_handle_t ISoundManager::allocateId(u32 num_owners)
{
while (m_occupied_ids.find(m_next_id) != m_occupied_ids.end()
|| m_next_id == SOUND_HANDLE_T_MAX) {
m_next_id = static_cast<sound_handle_t>(
myrand() % static_cast<u32>(SOUND_HANDLE_T_MAX - 1) + 1);
}
sound_handle_t id = m_next_id++;
m_occupied_ids.emplace(id, num_owners);
return id;
}
void ISoundManager::freeId(sound_handle_t id, u32 num_owners)
{
auto it = m_occupied_ids.find(id);
if (it == m_occupied_ids.end())
return;
if (it->second <= num_owners)
m_occupied_ids.erase(it);
else
it->second -= num_owners;
}

View file

@ -19,72 +19,168 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#pragma once
#include <set>
#include <string>
#include "irr_v3d.h"
#include "../sound.h"
#include <limits>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
class OnDemandSoundFetcher
struct SoundSpec;
class SoundFallbackPathProvider
{
public:
virtual void fetchSounds(const std::string &name,
std::set<std::string> &dst_paths,
std::set<std::string> &dst_datas) = 0;
virtual ~SoundFallbackPathProvider() = default;
std::vector<std::string> getLocalFallbackPathsForSoundname(const std::string &name);
protected:
virtual void addThePaths(const std::string &name, std::vector<std::string> &paths);
// adds <common>.ogg, <common>.1.ogg, ..., <common>.9.ogg to paths
void addAllAlternatives(const std::string &common, std::vector<std::string> &paths);
private:
std::unordered_set<std::string> m_done_names;
};
/**
* IDs for playing sounds.
* 0 is for sounds that are never modified after creation.
* Negative numbers are invalid.
* Positive numbers are allocated via allocateId and are manually reference-counted.
*/
using sound_handle_t = int;
constexpr sound_handle_t SOUND_HANDLE_T_MAX = std::numeric_limits<sound_handle_t>::max();
class ISoundManager
{
private:
std::unordered_map<sound_handle_t, u32> m_occupied_ids;
sound_handle_t m_next_id = 1;
std::vector<sound_handle_t> m_removed_sounds;
protected:
void reportRemovedSound(sound_handle_t id);
public:
virtual ~ISoundManager() = default;
// Multiple sounds can be loaded per name; when played, the sound
// should be chosen randomly from alternatives
// Return value determines success/failure
virtual bool loadSoundFile(
const std::string &name, const std::string &filepath) = 0;
virtual bool loadSoundData(
const std::string &name, const std::string &filedata) = 0;
/**
* Removes finished sounds, steps streamed sounds, and does similar tasks.
* Should not be called while paused.
* @param dtime In seconds.
*/
virtual void step(f32 dtime) = 0;
/**
* Pause all sound playback.
*/
virtual void pauseAll() = 0;
/**
* Resume sound playback after pause.
*/
virtual void resumeAll() = 0;
virtual void updateListener(
const v3f &pos, const v3f &vel, const v3f &at, const v3f &up) = 0;
virtual void setListenerGain(float gain) = 0;
/**
* @param pos In node-space.
* @param vel In node-space.
* @param at Vector in node-space pointing forwards.
* @param up Vector in node-space pointing upwards, orthogonal to `at`.
*/
virtual void updateListener(const v3f &pos, const v3f &vel, const v3f &at,
const v3f &up) = 0;
virtual void setListenerGain(f32 gain) = 0;
// playSound functions return -1 on failure, otherwise a handle to the
// sound. If name=="", call should be ignored without error.
virtual int playSound(const SimpleSoundSpec &spec) = 0;
virtual int playSoundAt(const SimpleSoundSpec &spec, const v3f &pos) = 0;
virtual void stopSound(int sound) = 0;
virtual bool soundExists(int sound) = 0;
virtual void updateSoundPosition(int sound, v3f pos) = 0;
virtual bool updateSoundGain(int id, float gain) = 0;
virtual float getSoundGain(int id) = 0;
virtual void step(float dtime) = 0;
virtual void fadeSound(int sound, float step, float gain) = 0;
/**
* Adds a sound to load from a file (only OggVorbis).
* @param name The name of the sound. Must be unique, otherwise call fails.
* @param filepath The path for
* @return true on success, false on failure (ie. sound was already added or
* file does not exist).
*/
virtual bool loadSoundFile(const std::string &name, const std::string &filepath) = 0;
/**
* Same as `loadSoundFile`, but reads the OggVorbis file from memory.
*/
virtual bool loadSoundData(const std::string &name, std::string &&filedata) = 0;
/**
* Adds sound with name sound_name to group `group_name`. Creates the group
* if non-existent.
* @param sound_name The name of the sound, as used in `loadSoundData`.
* @param group_name The name of the sound group.
*/
virtual void addSoundToGroup(const std::string &sound_name,
const std::string &group_name) = 0;
/**
* Plays a random sound from a sound group (position-less).
* @param id Id for new sound. Move semantics apply if id > 0.
*/
virtual void playSound(sound_handle_t id, const SoundSpec &spec) = 0;
/**
* Same as `playSound`, but at a position.
* @param pos In node-space.
* @param vel In node-space.
*/
virtual void playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos,
const v3f &vel) = 0;
/**
* Request the sound to be stopped.
* The id should be freed afterwards.
*/
virtual void stopSound(sound_handle_t sound) = 0;
virtual void fadeSound(sound_handle_t sound, f32 step, f32 target_gain) = 0;
/**
* Update position and velocity of positional sound.
* @param pos In node-space.
* @param vel In node-space.
*/
virtual void updateSoundPosVel(sound_handle_t sound, const v3f &pos,
const v3f &vel) = 0;
/**
* Get and reset the list of sounds that were stopped.
*/
std::vector<sound_handle_t> pollRemovedSounds()
{
std::vector<sound_handle_t> ret;
std::swap(m_removed_sounds, ret);
return ret;
}
/**
* Returns a positive id.
* The id will be returned again until freeId is called.
* @param num_owners Owner-counter for id. Set this to 2, if you want to play
* a sound and store the id also otherwhere.
*/
sound_handle_t allocateId(u32 num_owners);
/**
* Free an id allocated via allocateId.
* @param num_owners How much the owner-counter should be decreased. Id can
* be reused when counter reaches 0.
*/
void freeId(sound_handle_t id, u32 num_owners = 1);
};
class DummySoundManager : public ISoundManager
class DummySoundManager final : public ISoundManager
{
public:
virtual bool loadSoundFile(const std::string &name, const std::string &filepath)
{
return true;
}
virtual bool loadSoundData(const std::string &name, const std::string &filedata)
{
return true;
}
void updateListener(const v3f &pos, const v3f &vel, const v3f &at, const v3f &up)
{
}
void setListenerGain(float gain) {}
void step(f32 dtime) override {}
void pauseAll() override {}
void resumeAll() override {}
int playSound(const SimpleSoundSpec &spec) { return -1; }
int playSoundAt(const SimpleSoundSpec &spec, const v3f &pos) { return -1; }
void stopSound(int sound) {}
bool soundExists(int sound) { return false; }
void updateSoundPosition(int sound, v3f pos) {}
bool updateSoundGain(int id, float gain) { return false; }
float getSoundGain(int id) { return 0; }
void step(float dtime) {}
void fadeSound(int sound, float step, float gain) {}
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 { return true; }
bool loadSoundData(const std::string &name, std::string &&filedata) override { return true; }
void addSoundToGroup(const std::string &sound_name, const std::string &group_name) override {};
void playSound(sound_handle_t id, const SoundSpec &spec) override { reportRemovedSound(id); }
void playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos,
const v3f &vel) override { reportRemovedSound(id); }
void stopSound(sound_handle_t sound) override {}
void fadeSound(sound_handle_t sound, f32 step, f32 target_gain) override {}
void updateSoundPosVel(sound_handle_t sound, const v3f &pos, const v3f &vel) override {}
};

View file

@ -22,703 +22,10 @@ with this program; ifnot, write to the Free Software Foundation, Inc.,
*/
#include "sound_openal.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 <cmath>
#include <vorbis/vorbisfile.h>
#include <cassert>
#include "log.h"
#include "util/numeric.h" // myrand()
#include "porting.h"
#include <vector>
#include <fstream>
#include <unordered_map>
#include <unordered_set>
#define BUFFER_SIZE 30000
#include "sound_openal_internal.h"
std::shared_ptr<SoundManagerSingleton> g_sound_manager_singleton;
typedef std::unique_ptr<ALCdevice, void (*)(ALCdevice *p)> unique_ptr_alcdevice;
typedef std::unique_ptr<ALCcontext, void(*)(ALCcontext *p)> unique_ptr_alccontext;
static void delete_alcdevice(ALCdevice *p)
{
if (p)
alcCloseDevice(p);
}
static void delete_alccontext(ALCcontext *p)
{
if (p) {
alcMakeContextCurrent(nullptr);
alcDestroyContext(p);
}
}
static const char *alErrorString(ALenum err)
{
switch (err) {
case AL_NO_ERROR:
return "no error";
case AL_INVALID_NAME:
return "invalid name";
case AL_INVALID_ENUM:
return "invalid enum";
case AL_INVALID_VALUE:
return "invalid value";
case AL_INVALID_OPERATION:
return "invalid operation";
case AL_OUT_OF_MEMORY:
return "out of memory";
default:
return "<unknown OpenAL error>";
}
}
static ALenum warn_if_error(ALenum err, const char *desc)
{
if(err == AL_NO_ERROR)
return err;
warningstream<<desc<<": "<<alErrorString(err)<<std::endl;
return err;
}
void f3_set(ALfloat *f3, v3f v)
{
f3[0] = v.X;
f3[1] = v.Y;
f3[2] = v.Z;
}
struct SoundBuffer
{
ALenum format;
ALsizei freq;
ALuint buffer_id;
std::vector<char> buffer;
};
SoundBuffer *load_opened_ogg_file(OggVorbis_File *oggFile,
const std::string &filename_for_logging)
{
int endian = 0; // 0 for Little-Endian, 1 for Big-Endian
int bitStream;
long bytes;
char array[BUFFER_SIZE]; // Local fixed size array
vorbis_info *pInfo;
SoundBuffer *snd = new SoundBuffer;
// Get some information about the OGG file
pInfo = ov_info(oggFile, -1);
// Check the number of channels... always use 16-bit samples
if(pInfo->channels == 1)
snd->format = AL_FORMAT_MONO16;
else
snd->format = AL_FORMAT_STEREO16;
// The frequency of the sampling rate
snd->freq = pInfo->rate;
// Keep reading until all is read
do
{
// Read up to a buffer's worth of decoded sound data
bytes = ov_read(oggFile, array, BUFFER_SIZE, endian, 2, 1, &bitStream);
if(bytes < 0)
{
ov_clear(oggFile);
infostream << "Audio: Error decoding "
<< filename_for_logging << std::endl;
delete snd;
return nullptr;
}
// Append to end of buffer
snd->buffer.insert(snd->buffer.end(), array, array + bytes);
} while (bytes > 0);
alGenBuffers(1, &snd->buffer_id);
alBufferData(snd->buffer_id, snd->format,
&(snd->buffer[0]), snd->buffer.size(),
snd->freq);
ALenum error = alGetError();
if(error != AL_NO_ERROR){
infostream << "Audio: OpenAL error: " << alErrorString(error)
<< "preparing sound buffer" << std::endl;
}
//infostream << "Audio file "
// << filename_for_logging << " loaded" << std::endl;
// Clean up!
ov_clear(oggFile);
return snd;
}
SoundBuffer *load_ogg_from_file(const std::string &path)
{
OggVorbis_File oggFile;
// Try opening the given file.
// This requires libvorbis >= 1.3.2, as
// previous versions expect a non-const char *
if (ov_fopen(path.c_str(), &oggFile) != 0) {
infostream << "Audio: Error opening " << path
<< " for decoding" << std::endl;
return nullptr;
}
return load_opened_ogg_file(&oggFile, path);
}
struct BufferSource {
const char *buf;
size_t cur_offset;
size_t len;
};
size_t buffer_sound_read_func(void *ptr, size_t size, size_t nmemb, void *datasource)
{
BufferSource *s = (BufferSource *)datasource;
size_t copied_size = MYMIN(s->len - s->cur_offset, size);
memcpy(ptr, s->buf + s->cur_offset, copied_size);
s->cur_offset += copied_size;
return copied_size;
}
int buffer_sound_seek_func(void *datasource, ogg_int64_t offset, int whence)
{
BufferSource *s = (BufferSource *)datasource;
if (whence == SEEK_SET) {
if (offset < 0 || (size_t)MYMAX(offset, 0) >= s->len) {
// offset out of bounds
return -1;
}
s->cur_offset = offset;
return 0;
} else if (whence == SEEK_CUR) {
if ((size_t)MYMIN(-offset, 0) > s->cur_offset
|| s->cur_offset + offset > s->len) {
// offset out of bounds
return -1;
}
s->cur_offset += offset;
return 0;
}
// invalid whence param (SEEK_END doesn't have to be supported)
return -1;
}
long BufferSourceell_func(void *datasource)
{
BufferSource *s = (BufferSource *)datasource;
return s->cur_offset;
}
static ov_callbacks g_buffer_ov_callbacks = {
&buffer_sound_read_func,
&buffer_sound_seek_func,
nullptr,
&BufferSourceell_func
};
SoundBuffer *load_ogg_from_buffer(const std::string &buf, const std::string &id_for_log)
{
OggVorbis_File oggFile;
BufferSource s;
s.buf = buf.c_str();
s.cur_offset = 0;
s.len = buf.size();
if (ov_open_callbacks(&s, &oggFile, nullptr, 0, g_buffer_ov_callbacks) != 0) {
infostream << "Audio: Error opening " << id_for_log
<< " for decoding" << std::endl;
return nullptr;
}
return load_opened_ogg_file(&oggFile, id_for_log);
}
struct PlayingSound
{
ALuint source_id;
bool loop;
};
class SoundManagerSingleton
{
public:
unique_ptr_alcdevice m_device;
unique_ptr_alccontext m_context;
public:
SoundManagerSingleton() :
m_device(nullptr, delete_alcdevice),
m_context(nullptr, delete_alccontext)
{
}
bool init()
{
if (!(m_device = unique_ptr_alcdevice(alcOpenDevice(nullptr), delete_alcdevice))) {
errorstream << "Audio: Global Initialization: Failed to open device" << std::endl;
return false;
}
if (!(m_context = unique_ptr_alccontext(
alcCreateContext(m_device.get(), nullptr), delete_alccontext))) {
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);
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()
{
infostream << "Audio: Global Deinitialized." << std::endl;
}
};
class OpenALSoundManager: public ISoundManager
{
private:
OnDemandSoundFetcher *m_fetcher;
ALCdevice *m_device;
ALCcontext *m_context;
u16 m_last_used_id = 0; // only access within getFreeId() !
std::unordered_map<std::string, std::vector<SoundBuffer*>> m_buffers;
std::unordered_map<int, PlayingSound*> m_sounds_playing;
struct FadeState {
FadeState() = default;
FadeState(float step, float current_gain, float target_gain):
step(step),
current_gain(current_gain),
target_gain(target_gain) {}
float step;
float current_gain;
float target_gain;
};
std::unordered_map<int, FadeState> m_sounds_fading;
public:
OpenALSoundManager(SoundManagerSingleton *smg, OnDemandSoundFetcher *fetcher):
m_fetcher(fetcher),
m_device(smg->m_device.get()),
m_context(smg->m_context.get())
{
infostream << "Audio: Initialized: OpenAL " << std::endl;
}
~OpenALSoundManager()
{
infostream << "Audio: Deinitializing..." << std::endl;
std::unordered_set<int> source_del_list;
for (const auto &sp : m_sounds_playing)
source_del_list.insert(sp.first);
for (const auto &id : source_del_list)
deleteSound(id);
for (auto &buffer : m_buffers) {
for (SoundBuffer *sb : buffer.second) {
alDeleteBuffers(1, &sb->buffer_id);
ALenum error = alGetError();
if (error != AL_NO_ERROR) {
warningstream << "Audio: Failed to free stream for "
<< buffer.first << ": " << alErrorString(error) << std::endl;
}
delete sb;
}
buffer.second.clear();
}
m_buffers.clear();
infostream << "Audio: Deinitialized." << std::endl;
}
u16 getFreeId()
{
u16 startid = m_last_used_id;
while (!isFreeId(++m_last_used_id)) {
if (m_last_used_id == startid)
return 0;
}
return m_last_used_id;
}
inline bool isFreeId(int id) const
{
return id > 0 && m_sounds_playing.find(id) == m_sounds_playing.end();
}
void step(float dtime)
{
doFades(dtime);
}
void addBuffer(const std::string &name, SoundBuffer *buf)
{
std::unordered_map<std::string, std::vector<SoundBuffer*>>::iterator i =
m_buffers.find(name);
if(i != m_buffers.end()){
i->second.push_back(buf);
return;
}
std::vector<SoundBuffer*> bufs;
bufs.push_back(buf);
m_buffers[name] = std::move(bufs);
}
SoundBuffer* getBuffer(const std::string &name)
{
std::unordered_map<std::string, std::vector<SoundBuffer*>>::iterator i =
m_buffers.find(name);
if(i == m_buffers.end())
return nullptr;
std::vector<SoundBuffer*> &bufs = i->second;
int j = myrand() % bufs.size();
return bufs[j];
}
PlayingSound* createPlayingSound(SoundBuffer *buf, bool loop,
float volume, float pitch)
{
infostream << "OpenALSoundManager: Creating playing sound" << std::endl;
assert(buf);
PlayingSound *sound = new PlayingSound;
assert(sound);
warn_if_error(alGetError(), "before createPlayingSound");
alGenSources(1, &sound->source_id);
alSourcei(sound->source_id, AL_BUFFER, buf->buffer_id);
alSourcei(sound->source_id, AL_SOURCE_RELATIVE, true);
alSource3f(sound->source_id, AL_POSITION, 0, 0, 0);
alSource3f(sound->source_id, AL_VELOCITY, 0, 0, 0);
alSourcei(sound->source_id, AL_LOOPING, loop ? AL_TRUE : AL_FALSE);
volume = std::fmax(0.0f, volume);
alSourcef(sound->source_id, AL_GAIN, volume);
alSourcef(sound->source_id, AL_PITCH, pitch);
alSourcePlay(sound->source_id);
warn_if_error(alGetError(), "createPlayingSound");
return sound;
}
PlayingSound* createPlayingSoundAt(SoundBuffer *buf, bool loop,
float volume, v3f pos, float pitch)
{
infostream << "OpenALSoundManager: Creating positional playing sound"
<< std::endl;
assert(buf);
PlayingSound *sound = new PlayingSound;
warn_if_error(alGetError(), "before createPlayingSoundAt");
alGenSources(1, &sound->source_id);
alSourcei(sound->source_id, AL_BUFFER, buf->buffer_id);
alSourcei(sound->source_id, AL_SOURCE_RELATIVE, false);
alSource3f(sound->source_id, AL_POSITION, pos.X, pos.Y, pos.Z);
alSource3f(sound->source_id, AL_VELOCITY, 0, 0, 0);
// Use alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED) and set reference
// distance to clamp gain at <1 node distance, to avoid excessive
// volume when closer
alSourcef(sound->source_id, AL_REFERENCE_DISTANCE, 10.0f);
alSourcei(sound->source_id, AL_LOOPING, loop ? AL_TRUE : AL_FALSE);
// Multiply by 3 to compensate for reducing AL_REFERENCE_DISTANCE from
// the previous value of 30 to the new value of 10
volume = std::fmax(0.0f, volume * 3.0f);
alSourcef(sound->source_id, AL_GAIN, volume);
alSourcef(sound->source_id, AL_PITCH, pitch);
alSourcePlay(sound->source_id);
warn_if_error(alGetError(), "createPlayingSoundAt");
return sound;
}
int playSoundRaw(SoundBuffer *buf, bool loop, float volume, float pitch)
{
assert(buf);
PlayingSound *sound = createPlayingSound(buf, loop, volume, pitch);
if (!sound)
return -1;
int handle = getFreeId();
m_sounds_playing[handle] = sound;
return handle;
}
void deleteSound(int id)
{
auto i = m_sounds_playing.find(id);
if(i == m_sounds_playing.end())
return;
PlayingSound *sound = i->second;
alDeleteSources(1, &sound->source_id);
delete sound;
m_sounds_playing.erase(id);
}
/* If buffer does not exist, consult the fetcher */
SoundBuffer* getFetchBuffer(const std::string &name)
{
SoundBuffer *buf = getBuffer(name);
if(buf)
return buf;
if(!m_fetcher)
return nullptr;
std::set<std::string> paths;
std::set<std::string> datas;
m_fetcher->fetchSounds(name, paths, datas);
for (const std::string &path : paths) {
loadSoundFile(name, path);
}
for (const std::string &data : datas) {
loadSoundData(name, data);
}
return getBuffer(name);
}
// Remove stopped sounds
void maintain()
{
if (!m_sounds_playing.empty()) {
verbosestream << "OpenALSoundManager::maintain(): "
<< m_sounds_playing.size() <<" playing sounds, "
<< m_buffers.size() <<" sound names loaded"<<std::endl;
}
std::unordered_set<int> del_list;
for (const auto &sp : m_sounds_playing) {
int id = sp.first;
PlayingSound *sound = sp.second;
// If not playing, remove it
{
ALint state;
alGetSourcei(sound->source_id, AL_SOURCE_STATE, &state);
if(state != AL_PLAYING){
del_list.insert(id);
}
}
}
if(!del_list.empty())
verbosestream<<"OpenALSoundManager::maintain(): deleting "
<<del_list.size()<<" playing sounds"<<std::endl;
for (int i : del_list) {
deleteSound(i);
}
}
/* Interface */
bool loadSoundFile(const std::string &name,
const std::string &filepath)
{
SoundBuffer *buf = load_ogg_from_file(filepath);
if (buf)
addBuffer(name, buf);
return !!buf;
}
bool loadSoundData(const std::string &name,
const std::string &filedata)
{
SoundBuffer *buf = load_ogg_from_buffer(filedata, name);
if (buf)
addBuffer(name, buf);
return !!buf;
}
void updateListener(const v3f &pos, const v3f &vel, const v3f &at, const v3f &up)
{
alListener3f(AL_POSITION, pos.X, pos.Y, pos.Z);
alListener3f(AL_VELOCITY, vel.X, vel.Y, vel.Z);
ALfloat f[6];
f3_set(f, at);
f3_set(f+3, -up);
alListenerfv(AL_ORIENTATION, f);
warn_if_error(alGetError(), "updateListener");
}
void setListenerGain(float gain)
{
alListenerf(AL_GAIN, gain);
}
int playSound(const SimpleSoundSpec &spec)
{
maintain();
if (spec.name.empty())
return 0;
SoundBuffer *buf = getFetchBuffer(spec.name);
if(!buf){
infostream << "OpenALSoundManager: \"" << spec.name << "\" not found."
<< std::endl;
return -1;
}
int handle = -1;
if (spec.fade > 0) {
handle = playSoundRaw(buf, spec.loop, 0.0f, spec.pitch);
fadeSound(handle, spec.fade, spec.gain);
} else {
handle = playSoundRaw(buf, spec.loop, spec.gain, spec.pitch);
}
return handle;
}
int playSoundAt(const SimpleSoundSpec &spec, const v3f &pos)
{
maintain();
if (spec.name.empty())
return 0;
SoundBuffer *buf = getFetchBuffer(spec.name);
if (!buf) {
infostream << "OpenALSoundManager: \"" << spec.name << "\" not found."
<< std::endl;
return -1;
}
PlayingSound *sound = createPlayingSoundAt(buf, spec.loop, spec.gain, pos, spec.pitch);
if (!sound)
return -1;
int handle = getFreeId();
m_sounds_playing[handle] = sound;
return handle;
}
void stopSound(int sound)
{
maintain();
deleteSound(sound);
}
void fadeSound(int soundid, float step, float gain)
{
// Ignore the command if step isn't valid.
if (step == 0 || soundid < 0)
return;
float current_gain = getSoundGain(soundid);
step = gain - current_gain > 0 ? abs(step) : -abs(step);
if (m_sounds_fading.find(soundid) != m_sounds_fading.end()) {
auto current_fade = m_sounds_fading[soundid];
// Do not replace the fade if it's equivalent.
if (current_fade.target_gain == gain && current_fade.step == step)
return;
m_sounds_fading.erase(soundid);
}
gain = rangelim(gain, 0, 1);
m_sounds_fading[soundid] = FadeState(step, current_gain, gain);
}
void doFades(float dtime)
{
for (auto i = m_sounds_fading.begin(); i != m_sounds_fading.end();) {
FadeState& fade = i->second;
assert(fade.step != 0);
fade.current_gain += (fade.step * dtime);
if (fade.step < 0.f)
fade.current_gain = std::max(fade.current_gain, fade.target_gain);
else
fade.current_gain = std::min(fade.current_gain, fade.target_gain);
if (fade.current_gain <= 0.f)
stopSound(i->first);
else
updateSoundGain(i->first, fade.current_gain);
// The increment must happen during the erase call, or else it'll segfault.
if (fade.current_gain == fade.target_gain)
m_sounds_fading.erase(i++);
else
i++;
}
}
bool soundExists(int sound)
{
maintain();
return (m_sounds_playing.count(sound) != 0);
}
void updateSoundPosition(int id, v3f pos)
{
auto i = m_sounds_playing.find(id);
if (i == m_sounds_playing.end())
return;
PlayingSound *sound = i->second;
alSourcei(sound->source_id, AL_SOURCE_RELATIVE, false);
alSource3f(sound->source_id, AL_POSITION, pos.X, pos.Y, pos.Z);
alSource3f(sound->source_id, AL_VELOCITY, 0.0f, 0.0f, 0.0f);
alSourcef(sound->source_id, AL_REFERENCE_DISTANCE, 10.0f);
}
bool updateSoundGain(int id, float gain)
{
auto i = m_sounds_playing.find(id);
if (i == m_sounds_playing.end())
return false;
PlayingSound *sound = i->second;
alSourcef(sound->source_id, AL_GAIN, gain);
return true;
}
float getSoundGain(int id)
{
auto i = m_sounds_playing.find(id);
if (i == m_sounds_playing.end())
return 0;
PlayingSound *sound = i->second;
ALfloat gain;
alGetSourcef(sound->source_id, AL_GAIN, &gain);
return gain;
}
};
std::shared_ptr<SoundManagerSingleton> createSoundManagerSingleton()
{
auto smg = std::make_shared<SoundManagerSingleton>();
@ -728,7 +35,8 @@ std::shared_ptr<SoundManagerSingleton> createSoundManagerSingleton()
return smg;
}
ISoundManager *createOpenALSoundManager(SoundManagerSingleton *smg, OnDemandSoundFetcher *fetcher)
std::unique_ptr<ISoundManager> createOpenALSoundManager(SoundManagerSingleton *smg,
std::unique_ptr<SoundFallbackPathProvider> fallback_path_provider)
{
return new OpenALSoundManager(smg, fetcher);
return std::make_unique<OpenALSoundManager>(smg, std::move(fallback_path_provider));
};

View file

@ -19,13 +19,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#pragma once
#include <memory>
#include "sound.h"
#include <memory>
class SoundManagerSingleton;
extern std::shared_ptr<SoundManagerSingleton> g_sound_manager_singleton;
std::shared_ptr<SoundManagerSingleton> createSoundManagerSingleton();
ISoundManager *createOpenALSoundManager(
SoundManagerSingleton *smg, OnDemandSoundFetcher *fetcher);
std::unique_ptr<ISoundManager> createOpenALSoundManager(
SoundManagerSingleton *smg,
std::unique_ptr<SoundFallbackPathProvider> fallback_path_provider);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,613 @@
/*
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;
};