mirror of
https://github.com/4jcraft/4jcraft.git
synced 2026-05-07 03:17:13 +00:00
refactor: carve out a proper sound interface with miniaudio behind it
This commit is contained in:
parent
b032e2a3a0
commit
9b830f1bfc
|
|
@ -16,6 +16,7 @@ client_dependencies = [
|
|||
profile_dep,
|
||||
storage_dep,
|
||||
fs_dep,
|
||||
sound_dep,
|
||||
assets_localisation_dep,
|
||||
platform_dep,
|
||||
minecraft_dep,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ endif
|
|||
lib_minecraft = static_library('minecraft',
|
||||
minecraft_sources,
|
||||
dependencies : [
|
||||
miniaudio_dep, # TODO: remove once a SoundEngine facade is present
|
||||
sound_dep,
|
||||
render_dep,
|
||||
input_dep,
|
||||
profile_dep,
|
||||
|
|
|
|||
|
|
@ -31,3 +31,4 @@ subdir('profile')
|
|||
subdir('storage')
|
||||
subdir('fs')
|
||||
subdir('renderer')
|
||||
subdir('sound')
|
||||
|
|
|
|||
101
targets/platform/sound/IPlatformSound.h
Normal file
101
targets/platform/sound/IPlatformSound.h
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "platform/sound/SoundHandles.h"
|
||||
|
||||
// Platform sound interface. The backend (currently miniaudio) implements
|
||||
// this; consumers in app/common/Audio/SoundEngine talk to the interface
|
||||
// rather than to miniaudio directly.
|
||||
//
|
||||
// The interface is intent-level: load a sample from disk, play it,
|
||||
// stop it, set listener position. State (volume, pitch, looping) is
|
||||
// passed at play time as a struct rather than via global setters.
|
||||
//
|
||||
// Concrete handles (SoundHandle, MusicHandle) are tagged structs so the
|
||||
// type system catches sample-vs-music confusion at compile time.
|
||||
|
||||
namespace platform::sound {
|
||||
|
||||
struct PlaySoundParams {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float z = 0.0f;
|
||||
float volume = 1.0f;
|
||||
float pitch = 1.0f;
|
||||
bool spatial = true; // 3D sound positioned in the world
|
||||
bool looping = false;
|
||||
float minDistance = 1.0f; // distance below which the sound is full-volume
|
||||
float maxDistance = 16.0f; // distance above which the sound is silent
|
||||
};
|
||||
|
||||
struct PlayMusicParams {
|
||||
float volume = 1.0f;
|
||||
float pitch = 1.0f;
|
||||
bool looping = false;
|
||||
};
|
||||
|
||||
class IPlatformSound {
|
||||
public:
|
||||
virtual ~IPlatformSound() = default;
|
||||
|
||||
// Lifecycle. init() takes the maximum number of simultaneous local
|
||||
// listeners (splitscreen player count). Idempotent: a second init()
|
||||
// call with the same parameters is a no-op.
|
||||
virtual void init(int listenerCount) = 0;
|
||||
virtual void shutdown() = 0;
|
||||
|
||||
// Per-frame tick. Drives streaming reads, voice cleanup, etc.
|
||||
virtual void tick() = 0;
|
||||
|
||||
// Master volume scaling. 0.0 to 1.0. Applies to all subsequent
|
||||
// sound and music playback.
|
||||
virtual void setMasterVolume(float volume) = 0;
|
||||
|
||||
// -- Spatial / one-shot sound effects --
|
||||
|
||||
// Load and start playing a sound. The backend allocates a voice and
|
||||
// returns a handle that can be used to query / stop the sound. If
|
||||
// the load fails or the mixer is at capacity, returns an invalid
|
||||
// handle (the caller should treat that as "sound was dropped" - not
|
||||
// an error).
|
||||
[[nodiscard]] virtual SoundHandle playSoundFromFile(
|
||||
const std::string& path, const PlaySoundParams& params) = 0;
|
||||
|
||||
virtual void stopSound(SoundHandle handle) = 0;
|
||||
[[nodiscard]] virtual bool isSoundPlaying(SoundHandle handle) const = 0;
|
||||
|
||||
virtual void setSoundVolume(SoundHandle handle, float volume) = 0;
|
||||
virtual void setSoundPosition(SoundHandle handle, float x, float y,
|
||||
float z) = 0;
|
||||
virtual void setSoundPitch(SoundHandle handle, float pitch) = 0;
|
||||
|
||||
// Release the voice. Idempotent on invalid handles.
|
||||
virtual void releaseSound(SoundHandle handle) = 0;
|
||||
|
||||
// -- Streaming music --
|
||||
|
||||
// Load and start a streaming music track. Streaming means the
|
||||
// backend reads the file incrementally rather than decoding it all
|
||||
// upfront. Use for music tracks; sound effects should use the
|
||||
// playSound* methods above.
|
||||
[[nodiscard]] virtual MusicHandle playMusicFromFile(
|
||||
const std::string& path, const PlayMusicParams& params) = 0;
|
||||
|
||||
virtual void stopMusic(MusicHandle handle) = 0;
|
||||
[[nodiscard]] virtual bool isMusicPlaying(MusicHandle handle) const = 0;
|
||||
virtual void setMusicVolume(MusicHandle handle, float volume) = 0;
|
||||
virtual void setMusicPitch(MusicHandle handle, float pitch) = 0;
|
||||
virtual void releaseMusic(MusicHandle handle) = 0;
|
||||
|
||||
// -- Listener (one per local splitscreen player) --
|
||||
|
||||
virtual void setListenerPosition(int listenerIndex, float x, float y,
|
||||
float z) = 0;
|
||||
virtual void setListenerOrientation(int listenerIndex, float forwardX,
|
||||
float forwardY, float forwardZ,
|
||||
float upX, float upY, float upZ) = 0;
|
||||
};
|
||||
|
||||
} // namespace platform::sound
|
||||
30
targets/platform/sound/SoundHandles.h
Normal file
30
targets/platform/sound/SoundHandles.h
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Type-safe handles for the platform sound subsystem. Each handle is a
|
||||
// tagged 32-bit integer; the tag struct prevents accidental conversion
|
||||
// between handle types at compile time.
|
||||
//
|
||||
// Handle 0 is reserved for "invalid". Backends should never return 0
|
||||
// from a successful create call.
|
||||
|
||||
namespace platform::sound {
|
||||
|
||||
template <class Tag>
|
||||
struct Handle {
|
||||
std::uint32_t id = 0;
|
||||
|
||||
[[nodiscard]] constexpr bool valid() const noexcept { return id != 0; }
|
||||
constexpr explicit operator bool() const noexcept { return valid(); }
|
||||
|
||||
friend constexpr bool operator==(Handle, Handle) = default;
|
||||
};
|
||||
|
||||
struct SoundTag {};
|
||||
struct MusicTag {};
|
||||
|
||||
using SoundHandle = Handle<SoundTag>;
|
||||
using MusicHandle = Handle<MusicTag>;
|
||||
|
||||
} // namespace platform::sound
|
||||
15
targets/platform/sound/meson.build
Normal file
15
targets/platform/sound/meson.build
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
_miniaudio_dep = dependency('miniaudio')
|
||||
|
||||
lib_platform_sound_miniaudio = static_library(
|
||||
'platform_sound_miniaudio',
|
||||
files('miniaudio/MiniaudioSound.cpp'),
|
||||
include_directories: [platform_inc, include_directories('../..')],
|
||||
dependencies: [_threads, _miniaudio_dep],
|
||||
cpp_args: global_cpp_args + global_cpp_defs,
|
||||
)
|
||||
|
||||
sound_dep = declare_dependency(
|
||||
link_with: lib_platform_sound_miniaudio,
|
||||
include_directories: [platform_inc],
|
||||
dependencies: [_miniaudio_dep],
|
||||
)
|
||||
261
targets/platform/sound/miniaudio/MiniaudioSound.cpp
Normal file
261
targets/platform/sound/miniaudio/MiniaudioSound.cpp
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
#include "MiniaudioSound.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "miniaudio.h"
|
||||
|
||||
#include "platform/sound/sound.h"
|
||||
|
||||
namespace platform::sound::miniaudio {
|
||||
|
||||
namespace {
|
||||
|
||||
// Each loaded sound voice. Owned by the State's map; the SoundHandle
|
||||
// the caller holds is the integer key.
|
||||
struct Voice {
|
||||
ma_sound sound{};
|
||||
bool inUse = false;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
struct State {
|
||||
ma_engine engine{};
|
||||
ma_engine_config engineConfig{};
|
||||
bool engineReady = false;
|
||||
|
||||
// Voice pool. Sound handles are keys; the backend allocates a fresh
|
||||
// id for every play call. Caller releases via releaseSound() (or
|
||||
// we'll auto-release when the sound finishes - tick() handles that).
|
||||
std::unordered_map<std::uint32_t, std::unique_ptr<Voice>> sounds;
|
||||
std::unordered_map<std::uint32_t, std::unique_ptr<Voice>> music;
|
||||
std::atomic<std::uint32_t> nextHandleId{1};
|
||||
};
|
||||
|
||||
MiniaudioSound::MiniaudioSound() : m_state(std::make_unique<State>()) {}
|
||||
MiniaudioSound::~MiniaudioSound() {
|
||||
if (m_state && m_state->engineReady) {
|
||||
shutdown();
|
||||
}
|
||||
}
|
||||
MiniaudioSound::MiniaudioSound(MiniaudioSound&&) noexcept = default;
|
||||
MiniaudioSound& MiniaudioSound::operator=(MiniaudioSound&&) noexcept = default;
|
||||
|
||||
void MiniaudioSound::init(int listenerCount) {
|
||||
if (m_state->engineReady) return;
|
||||
m_state->engineConfig = ma_engine_config_init();
|
||||
m_state->engineConfig.listenerCount =
|
||||
listenerCount > 0 ? static_cast<ma_uint32>(listenerCount) : 1;
|
||||
if (ma_engine_init(&m_state->engineConfig, &m_state->engine) !=
|
||||
MA_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
ma_engine_set_volume(&m_state->engine, 1.0f);
|
||||
m_state->engineReady = true;
|
||||
}
|
||||
|
||||
void MiniaudioSound::shutdown() {
|
||||
if (!m_state->engineReady) return;
|
||||
// Tear down all live voices first.
|
||||
for (auto& [id, voice] : m_state->sounds) {
|
||||
if (voice && voice->inUse) {
|
||||
ma_sound_uninit(&voice->sound);
|
||||
}
|
||||
}
|
||||
m_state->sounds.clear();
|
||||
for (auto& [id, voice] : m_state->music) {
|
||||
if (voice && voice->inUse) {
|
||||
ma_sound_uninit(&voice->sound);
|
||||
}
|
||||
}
|
||||
m_state->music.clear();
|
||||
ma_engine_uninit(&m_state->engine);
|
||||
m_state->engineReady = false;
|
||||
}
|
||||
|
||||
void MiniaudioSound::tick() {
|
||||
if (!m_state->engineReady) return;
|
||||
// Reap finished one-shot sounds (non-looping, not currently playing).
|
||||
// We don't auto-reap music here; music is explicitly stopped by the
|
||||
// game's music system.
|
||||
for (auto it = m_state->sounds.begin(); it != m_state->sounds.end();) {
|
||||
auto& voice = it->second;
|
||||
if (voice && voice->inUse &&
|
||||
!ma_sound_is_playing(&voice->sound) &&
|
||||
!ma_sound_is_looping(&voice->sound)) {
|
||||
ma_sound_uninit(&voice->sound);
|
||||
voice->inUse = false;
|
||||
it = m_state->sounds.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MiniaudioSound::setMasterVolume(float volume) {
|
||||
if (!m_state->engineReady) return;
|
||||
ma_engine_set_volume(&m_state->engine, volume);
|
||||
}
|
||||
|
||||
SoundHandle MiniaudioSound::playSoundFromFile(const std::string& path,
|
||||
const PlaySoundParams& params) {
|
||||
if (!m_state->engineReady) return {};
|
||||
auto voice = std::make_unique<Voice>();
|
||||
if (ma_sound_init_from_file(&m_state->engine, path.c_str(),
|
||||
MA_SOUND_FLAG_ASYNC, nullptr, nullptr,
|
||||
&voice->sound) != MA_SUCCESS) {
|
||||
return {};
|
||||
}
|
||||
ma_sound_set_spatialization_enabled(&voice->sound,
|
||||
params.spatial ? MA_TRUE : MA_FALSE);
|
||||
ma_sound_set_min_distance(&voice->sound, params.minDistance);
|
||||
ma_sound_set_max_distance(&voice->sound, params.maxDistance);
|
||||
ma_sound_set_volume(&voice->sound, params.volume);
|
||||
ma_sound_set_pitch(&voice->sound, params.pitch);
|
||||
ma_sound_set_position(&voice->sound, params.x, params.y, params.z);
|
||||
ma_sound_set_looping(&voice->sound, params.looping ? MA_TRUE : MA_FALSE);
|
||||
ma_sound_start(&voice->sound);
|
||||
voice->inUse = true;
|
||||
|
||||
SoundHandle handle{m_state->nextHandleId.fetch_add(1)};
|
||||
m_state->sounds.emplace(handle.id, std::move(voice));
|
||||
return handle;
|
||||
}
|
||||
|
||||
void MiniaudioSound::stopSound(SoundHandle handle) {
|
||||
auto it = m_state->sounds.find(handle.id);
|
||||
if (it == m_state->sounds.end() || !it->second) return;
|
||||
if (it->second->inUse) {
|
||||
ma_sound_stop(&it->second->sound);
|
||||
}
|
||||
}
|
||||
|
||||
bool MiniaudioSound::isSoundPlaying(SoundHandle handle) const {
|
||||
auto it = m_state->sounds.find(handle.id);
|
||||
if (it == m_state->sounds.end() || !it->second || !it->second->inUse) {
|
||||
return false;
|
||||
}
|
||||
return ma_sound_is_playing(&it->second->sound) != MA_FALSE;
|
||||
}
|
||||
|
||||
void MiniaudioSound::setSoundVolume(SoundHandle handle, float volume) {
|
||||
auto it = m_state->sounds.find(handle.id);
|
||||
if (it == m_state->sounds.end() || !it->second || !it->second->inUse)
|
||||
return;
|
||||
ma_sound_set_volume(&it->second->sound, volume);
|
||||
}
|
||||
|
||||
void MiniaudioSound::setSoundPosition(SoundHandle handle, float x, float y,
|
||||
float z) {
|
||||
auto it = m_state->sounds.find(handle.id);
|
||||
if (it == m_state->sounds.end() || !it->second || !it->second->inUse)
|
||||
return;
|
||||
ma_sound_set_position(&it->second->sound, x, y, z);
|
||||
}
|
||||
|
||||
void MiniaudioSound::setSoundPitch(SoundHandle handle, float pitch) {
|
||||
auto it = m_state->sounds.find(handle.id);
|
||||
if (it == m_state->sounds.end() || !it->second || !it->second->inUse)
|
||||
return;
|
||||
ma_sound_set_pitch(&it->second->sound, pitch);
|
||||
}
|
||||
|
||||
void MiniaudioSound::releaseSound(SoundHandle handle) {
|
||||
auto it = m_state->sounds.find(handle.id);
|
||||
if (it == m_state->sounds.end()) return;
|
||||
if (it->second && it->second->inUse) {
|
||||
ma_sound_uninit(&it->second->sound);
|
||||
it->second->inUse = false;
|
||||
}
|
||||
m_state->sounds.erase(it);
|
||||
}
|
||||
|
||||
MusicHandle MiniaudioSound::playMusicFromFile(const std::string& path,
|
||||
const PlayMusicParams& params) {
|
||||
if (!m_state->engineReady) return {};
|
||||
auto voice = std::make_unique<Voice>();
|
||||
if (ma_sound_init_from_file(&m_state->engine, path.c_str(),
|
||||
MA_SOUND_FLAG_STREAM, nullptr, nullptr,
|
||||
&voice->sound) != MA_SUCCESS) {
|
||||
return {};
|
||||
}
|
||||
ma_sound_set_spatialization_enabled(&voice->sound, MA_FALSE);
|
||||
ma_sound_set_volume(&voice->sound, params.volume);
|
||||
ma_sound_set_pitch(&voice->sound, params.pitch);
|
||||
ma_sound_set_looping(&voice->sound, params.looping ? MA_TRUE : MA_FALSE);
|
||||
ma_sound_start(&voice->sound);
|
||||
voice->inUse = true;
|
||||
|
||||
MusicHandle handle{m_state->nextHandleId.fetch_add(1)};
|
||||
m_state->music.emplace(handle.id, std::move(voice));
|
||||
return handle;
|
||||
}
|
||||
|
||||
void MiniaudioSound::stopMusic(MusicHandle handle) {
|
||||
auto it = m_state->music.find(handle.id);
|
||||
if (it == m_state->music.end() || !it->second) return;
|
||||
if (it->second->inUse) {
|
||||
ma_sound_stop(&it->second->sound);
|
||||
}
|
||||
}
|
||||
|
||||
bool MiniaudioSound::isMusicPlaying(MusicHandle handle) const {
|
||||
auto it = m_state->music.find(handle.id);
|
||||
if (it == m_state->music.end() || !it->second || !it->second->inUse)
|
||||
return false;
|
||||
return ma_sound_is_playing(&it->second->sound) != MA_FALSE;
|
||||
}
|
||||
|
||||
void MiniaudioSound::setMusicVolume(MusicHandle handle, float volume) {
|
||||
auto it = m_state->music.find(handle.id);
|
||||
if (it == m_state->music.end() || !it->second || !it->second->inUse)
|
||||
return;
|
||||
ma_sound_set_volume(&it->second->sound, volume);
|
||||
}
|
||||
|
||||
void MiniaudioSound::setMusicPitch(MusicHandle handle, float pitch) {
|
||||
auto it = m_state->music.find(handle.id);
|
||||
if (it == m_state->music.end() || !it->second || !it->second->inUse)
|
||||
return;
|
||||
ma_sound_set_pitch(&it->second->sound, pitch);
|
||||
}
|
||||
|
||||
void MiniaudioSound::releaseMusic(MusicHandle handle) {
|
||||
auto it = m_state->music.find(handle.id);
|
||||
if (it == m_state->music.end()) return;
|
||||
if (it->second && it->second->inUse) {
|
||||
ma_sound_uninit(&it->second->sound);
|
||||
it->second->inUse = false;
|
||||
}
|
||||
m_state->music.erase(it);
|
||||
}
|
||||
|
||||
void MiniaudioSound::setListenerPosition(int listenerIndex, float x, float y,
|
||||
float z) {
|
||||
if (!m_state->engineReady) return;
|
||||
ma_engine_listener_set_position(&m_state->engine,
|
||||
static_cast<ma_uint32>(listenerIndex), x,
|
||||
y, z);
|
||||
}
|
||||
|
||||
void MiniaudioSound::setListenerOrientation(int listenerIndex, float forwardX,
|
||||
float forwardY, float forwardZ,
|
||||
float upX, float upY, float upZ) {
|
||||
if (!m_state->engineReady) return;
|
||||
ma_engine_listener_set_direction(&m_state->engine,
|
||||
static_cast<ma_uint32>(listenerIndex),
|
||||
forwardX, forwardY, forwardZ);
|
||||
ma_engine_listener_set_world_up(&m_state->engine,
|
||||
static_cast<ma_uint32>(listenerIndex),
|
||||
upX, upY, upZ);
|
||||
}
|
||||
|
||||
} // namespace platform::sound::miniaudio
|
||||
|
||||
namespace platform_internal {
|
||||
::platform::sound::IPlatformSound& PlatformSound_get() {
|
||||
static ::platform::sound::miniaudio::MiniaudioSound instance;
|
||||
return instance;
|
||||
}
|
||||
} // namespace platform_internal
|
||||
61
targets/platform/sound/miniaudio/MiniaudioSound.h
Normal file
61
targets/platform/sound/miniaudio/MiniaudioSound.h
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "platform/sound/IPlatformSound.h"
|
||||
|
||||
// Forward-declare the miniaudio state struct so this header doesn't
|
||||
// need to include miniaudio.h - keeps the include footprint small for
|
||||
// any consumer of the backend header.
|
||||
namespace platform::sound::miniaudio {
|
||||
|
||||
struct State;
|
||||
|
||||
class MiniaudioSound : public IPlatformSound {
|
||||
public:
|
||||
MiniaudioSound();
|
||||
~MiniaudioSound() override;
|
||||
|
||||
// Move-only - the engine owns hardware state.
|
||||
MiniaudioSound(MiniaudioSound&&) noexcept;
|
||||
MiniaudioSound& operator=(MiniaudioSound&&) noexcept;
|
||||
MiniaudioSound(const MiniaudioSound&) = delete;
|
||||
MiniaudioSound& operator=(const MiniaudioSound&) = delete;
|
||||
|
||||
// -- IPlatformSound --
|
||||
|
||||
void init(int listenerCount) override;
|
||||
void shutdown() override;
|
||||
void tick() override;
|
||||
|
||||
void setMasterVolume(float volume) override;
|
||||
|
||||
[[nodiscard]] SoundHandle playSoundFromFile(
|
||||
const std::string& path, const PlaySoundParams& params) override;
|
||||
void stopSound(SoundHandle handle) override;
|
||||
[[nodiscard]] bool isSoundPlaying(SoundHandle handle) const override;
|
||||
void setSoundVolume(SoundHandle handle, float volume) override;
|
||||
void setSoundPosition(SoundHandle handle, float x, float y,
|
||||
float z) override;
|
||||
void setSoundPitch(SoundHandle handle, float pitch) override;
|
||||
void releaseSound(SoundHandle handle) override;
|
||||
|
||||
[[nodiscard]] MusicHandle playMusicFromFile(
|
||||
const std::string& path, const PlayMusicParams& params) override;
|
||||
void stopMusic(MusicHandle handle) override;
|
||||
[[nodiscard]] bool isMusicPlaying(MusicHandle handle) const override;
|
||||
void setMusicVolume(MusicHandle handle, float volume) override;
|
||||
void setMusicPitch(MusicHandle handle, float pitch) override;
|
||||
void releaseMusic(MusicHandle handle) override;
|
||||
|
||||
void setListenerPosition(int listenerIndex, float x, float y,
|
||||
float z) override;
|
||||
void setListenerOrientation(int listenerIndex, float forwardX,
|
||||
float forwardY, float forwardZ, float upX,
|
||||
float upY, float upZ) override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<State> m_state;
|
||||
};
|
||||
|
||||
} // namespace platform::sound::miniaudio
|
||||
16
targets/platform/sound/sound.h
Normal file
16
targets/platform/sound/sound.h
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include "platform/sound/IPlatformSound.h"
|
||||
|
||||
// Function accessor backed by a function-local static (Meyers singleton).
|
||||
// Same pattern as input/renderer/profile/storage/fs - SIOF-safe.
|
||||
//
|
||||
// The macro lets call sites keep using `PlatformSound.foo()` syntax;
|
||||
// the expansion is just a function call returning a reference, which
|
||||
// LTO inlines.
|
||||
|
||||
namespace platform_internal {
|
||||
::platform::sound::IPlatformSound& PlatformSound_get();
|
||||
}
|
||||
|
||||
#define PlatformSound (::platform_internal::PlatformSound_get())
|
||||
Loading…
Reference in a new issue