diff --git a/CMakeLists.txt b/CMakeLists.txt index c073de2..c4fccfb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,6 +46,19 @@ set(COMMON_SOURCES set(CLIENT_ONLY_SOURCES "src/assets/mesh_builder.hpp" "src/assets/mesh_builder.cpp" + "src/audio/defs.hpp" + "src/audio/master.hpp" + "src/audio/master.cpp" + "src/audio/ogg.hpp" + "src/audio/ogg.cpp" + "src/audio/player.hpp" + "src/audio/player.cpp" + "src/audio/sound_source.hpp" + "src/audio/sound_source.cpp" + "src/audio/sound.hpp" + "src/audio/sound.cpp" + "src/audio/source.hpp" + "src/audio/source.cpp" "src/client/app.hpp" "src/client/app.cpp" "src/client/gl.hpp" @@ -117,10 +130,14 @@ endif() # Include directories target_include_directories(${MAIN_NAME} PRIVATE "src") +target_compile_definitions(${MAIN_NAME} PRIVATE CLIENT) + +# add glm add_subdirectory(external/glm) target_link_libraries(${MAIN_NAME} PRIVATE glm) target_include_directories(${MAIN_NAME} PRIVATE "external/stb") +# add bullet3 set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) set(BUILD_BULLET2_DEMOS OFF CACHE BOOL "" FORCE) set(BUILD_UNIT_TESTS OFF CACHE BOOL "" FORCE) @@ -131,7 +148,12 @@ add_subdirectory(external/bullet3) target_link_libraries(${MAIN_NAME} PRIVATE BulletDynamics BulletCollision LinearMath Bullet3Common) target_include_directories(${MAIN_NAME} PRIVATE "external/bullet3/src") -target_compile_definitions(${MAIN_NAME} PRIVATE CLIENT) +# add ogg +add_subdirectory(external/ogg) + +# add vorbis +add_subdirectory(external/vorbis) +target_link_libraries(${MAIN_NAME} PRIVATE vorbisfile vorbis) # Platform-specific SDL2 handling if (CMAKE_SYSTEM_NAME STREQUAL Emscripten) @@ -196,6 +218,11 @@ endif() target_link_libraries(${MAIN_NAME} PRIVATE easywsclient) + # add openal + set(ALSOFT_EXAMPLES OFF) + add_subdirectory(external/openal-soft) + target_link_libraries(${MAIN_NAME} PRIVATE OpenAL) + # build server set(ASIO_INCLUDE_DIR "external/asio/include") include_directories(${ASIO_INCLUDE_DIR}) diff --git a/src/assets/cache.cpp b/src/assets/cache.cpp index b7890b2..e6e2953 100644 --- a/src/assets/cache.cpp +++ b/src/assets/cache.cpp @@ -5,4 +5,5 @@ assets::ModelCache assets::CacheManager::model_cache_; assets::MapCache assets::CacheManager::map_cache_; assets::VehicleCache assets::CacheManager::vehicle_cache_; -CLIENT_ONLY(assets::TextureCache assets::CacheManager::texture_cache_;) \ No newline at end of file +CLIENT_ONLY(assets::TextureCache assets::CacheManager::texture_cache_;) +CLIENT_ONLY(assets::SoundCache assets::CacheManager::sound_cache_;) \ No newline at end of file diff --git a/src/assets/cache.hpp b/src/assets/cache.hpp index af5fff2..d13aa5b 100644 --- a/src/assets/cache.hpp +++ b/src/assets/cache.hpp @@ -8,6 +8,7 @@ #include "utils/defs.hpp" #ifdef CLIENT +#include "audio/sound.hpp" #include "gfx/texture.hpp" #endif @@ -49,6 +50,12 @@ class TextureCache final : public Cache protected: PtrType Load(const std::string& key) override { return gfx::Texture::LoadFromFile(key); } }; + +class SoundCache final : public Cache +{ +protected: + PtrType Load(const std::string& key) override { return audio::Sound::LoadFromFile(key); } +}; #endif // CLIENT class SkeletonCache final : public Cache @@ -86,14 +93,22 @@ public: static std::shared_ptr GetModel(const std::string& filename) { return model_cache_.Get(filename); } static std::shared_ptr GetMap(const std::string& filename) { return map_cache_.Get(filename); } - - static std::shared_ptr GetVehicleModel(const std::string& filename) { return vehicle_cache_.Get(filename); } + + static std::shared_ptr GetVehicleModel(const std::string& filename) + { + return vehicle_cache_.Get(filename); + } #ifdef CLIENT static std::shared_ptr GetTexture(const std::string& filename) { return texture_cache_.Get(filename); } + + static std::shared_ptr GetSound(const std::string& filename) + { + return sound_cache_.Get(filename); + } #endif private: @@ -102,6 +117,7 @@ private: static MapCache map_cache_; static VehicleCache vehicle_cache_; CLIENT_ONLY(static TextureCache texture_cache_;) + CLIENT_ONLY(static SoundCache sound_cache_;) }; } // namespace assets \ No newline at end of file diff --git a/src/audio/defs.hpp b/src/audio/defs.hpp new file mode 100644 index 0000000..9f15726 --- /dev/null +++ b/src/audio/defs.hpp @@ -0,0 +1,3 @@ +#pragma once + +#define AUDIO_DBG(...) __VA_ARGS__ \ No newline at end of file diff --git a/src/audio/master.cpp b/src/audio/master.cpp new file mode 100644 index 0000000..7b65823 --- /dev/null +++ b/src/audio/master.cpp @@ -0,0 +1,125 @@ +#include "master.hpp" + +#include + +#include +#include + +#include "defs.hpp" +#include "sound_source.hpp" +#include "source.hpp" + +static const size_t MAX_CATEGORIES = 32; + +audio::Master::Master() +{ + std::cout << "Initializing audio master...\n"; + + categories_.reserve(MAX_CATEGORIES); + + device_ = alcOpenDevice(nullptr); + + if (!device_) + { + throw std::runtime_error("(AL) Failed to open audio device"); + } + + // get device info + const ALchar* info = alcGetString(device_, ALC_DEVICE_SPECIFIER); + std::cout << "Opened audio device: " << (info ? info : "Unknown") << std::endl; + + context_ = alcCreateContext(device_, nullptr); + + if (!context_) + { + alcCloseDevice(device_); + device_ = nullptr; + throw std::runtime_error("(AL) Failed to create audio context"); + } + + std::cout << "Created audio context" << std::endl; + + if (!alcMakeContextCurrent(context_)) + { + alcDestroyContext(context_); + alcCloseDevice(device_); + context_ = nullptr; + device_ = nullptr; + throw std::runtime_error("(AL) Failed to make audio context current"); + } + + std::cout << "Audio context is now current" << std::endl; +} + +void audio::Master::SetListenerOrientation(const glm::vec3& position, const glm::vec3& forward, const glm::vec3& up) +{ + alListener3f(AL_POSITION, position.x, position.y, position.z); + // alListener3f(AL_VELOCITY, 0.0f, 0.0f, 0.0f); + ALfloat orientation[] = {forward.x, forward.y, forward.z, up.x, up.y, up.z}; + alListenerfv(AL_ORIENTATION, orientation); +} + +void audio::Master::SetListenerOrientation(const glm::mat4& world_matrix) +{ + glm::vec3 position = glm::vec3(world_matrix[3]); + glm::vec3 forward = -glm::normalize(glm::vec3(world_matrix[2])); + glm::vec3 up = glm::normalize(glm::vec3(world_matrix[1])); + SetListenerOrientation(position, forward, up); +} + +audio::Category* audio::Master::GetCategory(const std::string& name) +{ + auto it = category_map_.find(name); + if (it != category_map_.end()) + { + return it->second; + } + + if (categories_.size() >= MAX_CATEGORIES) + { + throw std::runtime_error("(AL) Maximum number of audio categories reached"); + } + + Category* new_category = &categories_.emplace_back(name); + category_map_[name] = new_category; + + AUDIO_DBG(std::cout << "Created new audio category: " << name << std::endl;) + + return new_category; +} + +void audio::Master::SetMasterVolume(float volume) +{ + master_volume_ = volume; + alListenerf(AL_GAIN, master_volume_); + AUDIO_DBG(std::cout << "Set master volume to " << master_volume_ << std::endl;) +} + +audio::Master::~Master() +{ + std::cout << "Shutting down audio master...\n"; + + if (context_) + { + alcMakeContextCurrent(nullptr); + alcDestroyContext(context_); + context_ = nullptr; + } + + if (device_) + { + alcCloseDevice(device_); + device_ = nullptr; + } +} + +audio::Category::Category(const std::string& name) : name_(name) {} + +void audio::Category::SetVolume(float volume) +{ + volume_ = volume; + for (Source* source = first_source_; source; source = source->cat_next_) + { + source->UpdateVolume(); + } +} diff --git a/src/audio/master.hpp b/src/audio/master.hpp new file mode 100644 index 0000000..37dcd18 --- /dev/null +++ b/src/audio/master.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include + +#include "utils/defs.hpp" +#include + +class ALCdevice; +class ALCcontext; + +namespace audio +{ + +class Source; + +class Category +{ +public: + Category(const std::string& name); + DELETE_COPY_MOVE(Category) + + const std::string& GetName() const { return name_; } + + void SetVolume(float volume); + float GetVolume() const { return volume_; } + +private: + float volume_ = 1.0f; + std::string name_; + + Source* first_source_ = nullptr; + friend class Source; +}; + +class Master +{ +public: + Master(); + DELETE_COPY_MOVE(Master) + + void SetListenerOrientation(const glm::vec3& position, const glm::vec3& forward, const glm::vec3& up); + void SetListenerOrientation(const glm::mat4& world_matrix); + + Category* GetCategory(const std::string& name); + + void SetMasterVolume(float volume); + float GetMasterVolume() const { return master_volume_; } + + ~Master(); + +private: + ALCdevice* device_ = nullptr; + ALCcontext* context_ = nullptr; + + std::vector categories_; + std::map category_map_; + + float master_volume_ = 1.0f; +}; + +} // namespace audio \ No newline at end of file diff --git a/src/audio/ogg.cpp b/src/audio/ogg.cpp new file mode 100644 index 0000000..d1f337b --- /dev/null +++ b/src/audio/ogg.cpp @@ -0,0 +1,85 @@ +#include "ogg.hpp" + +#include + +audio::OggFile::OggFile(const char* filename) +{ + int result = ov_fopen(filename, &ogg_file_); + + if (result < 0) + { + throw std::runtime_error("(OGG) Failed to open OGG file: " + std::string(filename)); + } + + try + { + LoadInfo(); + } + catch (const std::exception& e) + { + ov_clear(&ogg_file_); + throw std::runtime_error(std::string("(OGG) Error loading OGG file info: ") + e.what()); + } +} + +size_t audio::OggFile::GetNumChannels() const +{ + return vorbis_info_->channels; +} + +size_t audio::OggFile::GetSampleRate() const +{ + return vorbis_info_->rate; +} + +size_t audio::OggFile::GetNumSamples() +{ + return ov_pcm_total(&ogg_file_, -1); +} + +size_t audio::OggFile::Read(char* buffer, size_t length) +{ + long res = ov_read(&ogg_file_, buffer, static_cast(length), 0, 2, 1, NULL); + + if (res < 0) + { + throw std::runtime_error("Error reading OGG file"); + } + + return static_cast(res); +} + +std::vector audio::OggFile::ReadAll() +{ + size_t total_size = ov_pcm_total(&ogg_file_, -1) * vorbis_info_->channels * 2; + std::vector buffer(total_size); + + size_t bytes_read = 0; + while (bytes_read < total_size) + { + size_t bytes_to_read = total_size - bytes_read; + size_t read = Read(buffer.data() + bytes_read, bytes_to_read); + + if (read == 0) + { + break; // End of file reached + } + + bytes_read += read; + } + + return buffer; +} + +audio::OggFile::~OggFile() +{ + ov_clear(&ogg_file_); // Clear the OGG file resources +} + +void audio::OggFile::LoadInfo() +{ + vorbis_info_ = ov_info(&ogg_file_, -1); // Initialize the OGG file info + + if (vorbis_info_->channels <= 0 || vorbis_info_->channels > 2) + throw std::runtime_error("Unsupported number of channels in OGG file"); +} diff --git a/src/audio/ogg.hpp b/src/audio/ogg.hpp new file mode 100644 index 0000000..8bd81ef --- /dev/null +++ b/src/audio/ogg.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "utils/defs.hpp" +#include +#include + +// class OggVorbis_File; +// class vorbis_info; + +namespace audio +{ + +class OggFile +{ +public: + OggFile(const char* filename); + DELETE_COPY_MOVE(OggFile) + + size_t GetNumChannels() const; + size_t GetSampleRate() const; + size_t GetNumSamples(); + + size_t Read(char* buffer, size_t length); + std::vector ReadAll(); + + ~OggFile(); + +private: + void LoadInfo(); + +private: + OggVorbis_File ogg_file_; + vorbis_info* vorbis_info_ = nullptr; +}; + +} // namespace audio \ No newline at end of file diff --git a/src/audio/player.cpp b/src/audio/player.cpp new file mode 100644 index 0000000..221f72d --- /dev/null +++ b/src/audio/player.cpp @@ -0,0 +1,30 @@ +#include "player.hpp" + +audio::Player::Player(Master& master) : master_(master) {} + +audio::SoundSource* audio::Player::PlaySound(const std::shared_ptr& sound, + const glm::vec3* attach_position) +{ + SoundSource* source = new SoundSource(this, sound); + source->AttachToPosition(attach_position); + + return source; +} + +void audio::Player::Update() +{ + Source* current = first_source_.get(); + while (current) + { + current->Update(); + + Source* next = current->player_next_.get(); + + if (current->ShouldBeDeleted()) + { + current->Delete(); + } + + current = next; + } +} \ No newline at end of file diff --git a/src/audio/player.hpp b/src/audio/player.hpp new file mode 100644 index 0000000..f541caa --- /dev/null +++ b/src/audio/player.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include "master.hpp" +#include "sound_source.hpp" + +namespace audio +{ + +class Player +{ +public: + Player(Master& master); + DELETE_COPY_MOVE(Player) + + SoundSource* PlaySound(const std::shared_ptr& sound, const glm::vec3* attach_position); + + void Update(); + + Master& GetMaster() const { return master_; } + + ~Player() = default; + +private: + Master& master_; + + std::unique_ptr first_source_; + friend class Source; +}; + +} // namespace audio \ No newline at end of file diff --git a/src/audio/sound.cpp b/src/audio/sound.cpp new file mode 100644 index 0000000..5244e34 --- /dev/null +++ b/src/audio/sound.cpp @@ -0,0 +1,66 @@ +#include "sound.hpp" + +#include +#include + +#include "assets/cmdfile.hpp" +#include "utils/files.hpp" + +#include "ogg.hpp" + +audio::Sound::Sound() +{ + alGenBuffers(1, &buffer_); + + if (!buffer_) + throw std::runtime_error("Failed to generate OpenAL buffer"); +} + +static void LoadBufferOGG(ALuint buffer, const char* path) +{ + audio::OggFile ogg_file(path); + std::vector data = ogg_file.ReadAll(); + + ALenum format = ogg_file.GetNumChannels() == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16; + ALsizei sample_rate = ogg_file.GetSampleRate(); + + alBufferData(buffer, format, data.data(), static_cast(data.size()), sample_rate); +} + +std::shared_ptr audio::Sound::LoadFromFile(const std::string& path) +{ + auto sound = std::make_shared(); + + std::string ogg_path; + + assets::LoadCMDFile(path, [&](const std::string& cmd, std::istringstream& iss) { + if (cmd == "ogg") + { + iss >> ogg_path; + } + else if (cmd == "category") + { + iss >> sound->category_name_; + } + else if (cmd == "volume") + { + iss >> sound->volume_; + } + else if (cmd == "pitch") + { + iss >> sound->pitch_; + } + }); + + if (sound->category_name_.empty()) + sound->category_name_ = "default"; + + LoadBufferOGG(sound->GetBufferId(), ogg_path.c_str()); + + return sound; +} + +audio::Sound::~Sound() +{ + alDeleteBuffers(1, &buffer_); +} diff --git a/src/audio/sound.hpp b/src/audio/sound.hpp new file mode 100644 index 0000000..f48f929 --- /dev/null +++ b/src/audio/sound.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "master.hpp" + +#include + +namespace audio +{ + +class Sound +{ +private: + Sound(); + friend std::shared_ptr std::make_shared(); + +public: + static std::shared_ptr LoadFromFile(const std::string& path); + + unsigned int GetBufferId() const { return buffer_; } + + const std::string& GetCategoryName() const { return category_name_; } + + float GetVolume() const { return volume_; } + float GetPitch() const { return pitch_; } + + ~Sound(); + +private: + unsigned int buffer_ = 0; + + std::string category_name_; + + float volume_ = 1.0f; + float pitch_ = 1.0f; +}; + +} // namespace audio \ No newline at end of file diff --git a/src/audio/sound_source.cpp b/src/audio/sound_source.cpp new file mode 100644 index 0000000..941b9bd --- /dev/null +++ b/src/audio/sound_source.cpp @@ -0,0 +1,67 @@ +#include "sound_source.hpp" + +#include +#include + +audio::SoundSource::SoundSource(Player* player, std::shared_ptr sound) + : Super(sound->GetCategoryName(), player), sound_(std::move(sound)) +{ + SetVolume(1.0f); + SetPitch(1.0f); + + alSourcei(source_, AL_BUFFER, sound->GetBufferId()); +} + +void audio::SoundSource::SetLooping(bool looping) +{ + alSourcei(source_, AL_LOOPING, looping ? AL_TRUE : AL_FALSE); + // TsrDebugf(DML_2, "Set source %p looping to %s\n", this, looping ? "true" : "false"); +} + +void audio::SoundSource::SetPitch(float pitch) +{ + Super::SetSourcePitch(sound_->GetPitch() * pitch); +} + +void audio::SoundSource::SetVolume(float volume) +{ + Super::SetSourceVolume(sound_->GetVolume() * volume); +} + +void audio::SoundSource::Update() +{ + Super::Update(); + + ALint state; + alGetSourcei(source_, AL_SOURCE_STATE, &state); + + finished_ = state == AL_STOPPED; + + switch (state) + { + case AL_PLAYING: { + if (!should_play_) + alSourcePause(source_); + + break; + } + + case AL_INITIAL: + case AL_STOPPED: + case AL_PAUSED: { + if (should_play_) + alSourcePlay(source_); + + break; + } + + default: + break; + } +} + +audio::SoundSource::~SoundSource() +{ + alSourceStop(source_); + alSourcei(source_, AL_BUFFER, 0); +} diff --git a/src/audio/sound_source.hpp b/src/audio/sound_source.hpp new file mode 100644 index 0000000..53343a8 --- /dev/null +++ b/src/audio/sound_source.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "sound.hpp" +#include "source.hpp" + +namespace audio +{ + +class SoundSource : public Source +{ + +public: + SoundSource(Player* player, std::shared_ptr sound); + + virtual void SetLooping(bool looping) override; + virtual void SetPitch(float pitch) override; + virtual void SetVolume(float volume) override; + + virtual void Update() override; + + virtual ~SoundSource() override; + +private: + using Super = Source; + + std::shared_ptr sound_; +}; + +} // namespace audio \ No newline at end of file diff --git a/src/audio/source.cpp b/src/audio/source.cpp new file mode 100644 index 0000000..52ea8c6 --- /dev/null +++ b/src/audio/source.cpp @@ -0,0 +1,122 @@ +#include "source.hpp" + +#include + +#include +#include + +#include "defs.hpp" +#include "player.hpp" + +audio::Source::Source(const std::string& category_name, Player* player) +{ + alGenSources(1, &source_); + + if (!source_) + throw std::runtime_error("Failed to create audio source"); + + // link to category + category_ = player->GetMaster().GetCategory(category_name); + cat_next_ = category_->first_source_; + category_->first_source_ = this; + cat_prev_next_ = &category_->first_source_; + if (cat_next_) + cat_next_->cat_prev_next_ = &cat_next_; + + // link to player + player_ = player; + player_next_ = std::move(player->first_source_); + player->first_source_.reset(this); + player_prev_next_ = &player->first_source_; + if (player_next_) + player_next_->player_prev_next_ = &player_next_; + + // TsrDebugf(DML_2, "Created audio source %p in category '%s'\n", this, category->GetName().c_str()); + + // alSourcef(m_source, AL_ROLLOFF_FACTOR, 0.0f); // disable rolloff + // alSourcef(m_source, AL_REFERENCE_DISTANCE, 1.0f); // set reference distance +} + +void audio::Source::SetPosition(const glm::vec3& position) +{ + alSource3f(source_, AL_POSITION, position.x, position.y, position.z); +} + +void audio::Source::SetVelocity(const glm::vec3& velocity) +{ + alSource3f(source_, AL_VELOCITY, velocity.x, velocity.y, velocity.z); +} + +void audio::Source::AttachToPosition(const glm::vec3* position) +{ + attach_position_ = position; + if (attach_position_) + SetPosition(*attach_position_); + // TsrDebugf(DML_2, "Attached source %p to position %p\n", this, position); +} + +void audio::Source::SetRelativeToListener(bool relative) +{ + if (relative) + { + alSourcei(source_, AL_SOURCE_RELATIVE, AL_TRUE); + // TsrDebugf(DML_2, "Set source %p to be relative to listener\n", this); + } + else + { + alSourcei(source_, AL_SOURCE_RELATIVE, AL_FALSE); + // TsrDebugf(DML_2, "Set source %p to be absolute\n", this); + } +} + +void audio::Source::Update() +{ + if (attach_position_) + SetPosition(*attach_position_); +} + +void audio::Source::Delete() +{ + // unlink from player (causes destruction) + if (player_next_) + { + player_next_->player_prev_next_ = player_prev_next_; + } + std::unique_ptr& player_prev_next = *player_prev_next_; + player_prev_next = std::move(player_next_); + + // TsrDebugf(DML_2, "Deleted audio source %p\n", this); +} + +void audio::Source::SetSourceVolume(float volume) +{ + volume_ = volume; + UpdateVolume(); +} + +void audio::Source::SetSourcePitch(float pitch) +{ + alSourcef(source_, AL_PITCH, pitch); + // TsrDebugf(DML_2, "Set source %p pitch to %.2f\n", this, pitch); +} + +void audio::Source::UpdateVolume() +{ + float cat_volume = category_->GetVolume(); + float result_volume = volume_ * cat_volume; + alSourcef(source_, AL_GAIN, result_volume); + // TsrDebugf(DML_2, "Set source %p volume to %.2f (this %.2f, category %.2f)\n", this, result_volume, m_volume, + // cat_volume); +} + +audio::Source::~Source() +{ + alDeleteSources(1, &source_); + + // unlink from category + if (cat_next_) + { + cat_next_->cat_prev_next_ = cat_prev_next_; + } + *cat_prev_next_ = cat_next_; +} \ No newline at end of file diff --git a/src/audio/source.hpp b/src/audio/source.hpp new file mode 100644 index 0000000..2bc9422 --- /dev/null +++ b/src/audio/source.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include "master.hpp" +#include + +namespace audio +{ + +class Player; + +class Source +{ +protected: + Source(const std::string& category_name, Player* player); + +public: + DELETE_COPY_MOVE(Source) + + void SetPlay(bool play) { should_play_ = play; } + void Play() { SetPlay(true); } + void Stop() { SetPlay(false); } + + void SetDeleteOnFinish(bool destroy) { delete_on_finish_ = destroy; } + + void SetPosition(const glm::vec3& position); + void SetVelocity(const glm::vec3& velocity); + void AttachToPosition(const glm::vec3* position); + void SetRelativeToListener(bool relative); + + virtual void SetLooping(bool looping) = 0; + virtual void SetPitch(float pitch) = 0; + virtual void SetVolume(float volume) = 0; + + virtual void Update(); + + bool ShouldBeDeleted() { return finished_ && delete_on_finish_; } + + void Delete(); + + virtual ~Source(); + +protected: + void SetSourceVolume(float volume); + void SetSourcePitch(float pitch); + + unsigned int source_ = 0; + + const glm::vec3* attach_position_ = nullptr; + bool should_play_ = true; // auto play when created + bool finished_ = false; + bool delete_on_finish_ = true; // auto delete when finished + +private: + void UpdateVolume(); // on this or category volume change + + friend class Category; + Category* category_ = nullptr; + Source** cat_prev_next_ = nullptr; + Source* cat_next_ = nullptr; + + friend class Player; + Player* player_ = nullptr; + std::unique_ptr* player_prev_next_ = nullptr; + std::unique_ptr player_next_ = nullptr; + + float volume_ = 1.0f; // volume set by source +}; + +} // namespace audio \ No newline at end of file