Minor - Adds audio component with spatialization - V13.3.0

Adds a new audio component with support for loading, playing, pausing, stopping, and controlling audio properties such as volume, pan, pitch, and looping.

Implements spatialization using FMOD, enabling 3D audio effects based on object and camera positions. Includes file selection dialog and UI controls for audio properties.
This commit is contained in:
2025-09-15 23:15:34 +02:00
parent 5ee88ff932
commit aa8e5d2abd
9 changed files with 479 additions and 35 deletions

View File

@@ -82,6 +82,36 @@ public:
*/
void get_reflection_view_matrix(XMMATRIX&) const;
XMFLOAT3 get_forward() const {
float pitch = XMConvertToRadians(rotation_x_);
float yaw = XMConvertToRadians(rotation_y_);
XMMATRIX rotMatrix = XMMatrixRotationRollPitchYaw(pitch, yaw, 0.0f);
XMVECTOR forward = -rotMatrix.r[2];
forward = XMVector3Normalize(forward);
XMFLOAT3 forwardVec;
XMStoreFloat3(&forwardVec, forward);
return forwardVec;
}
XMFLOAT3 get_up() const {
// Construire matrice rotation <20> partir des angles
XMMATRIX rot = XMMatrixRotationRollPitchYaw(
XMConvertToRadians(rotation_x_),
XMConvertToRadians(rotation_y_),
XMConvertToRadians(rotation_z_));
// Extraire le vecteur up, 2e colonne
XMVECTOR up = rot.r[1]; // colonne up
up = XMVector3Normalize(up);
XMFLOAT3 upF;
XMStoreFloat3(&upF, up);
return upF;
}
private:
float position_x_, position_y_, position_z_;
float rotation_x_, rotation_y_, rotation_z_;

View File

@@ -3,11 +3,13 @@
#include <typeindex>
#include <typeinfo>
#include <imgui.h>
#include "entity.h"
/**
* namespace for the Entity-Component-System (ECS)
*/
namespace ecs {
class Entity;
// Classe de base pour tous les composants
class Component {
@@ -31,6 +33,7 @@ public:
/**
* Virtual function to update the component.
* @param deltaTime Time since the last update.
* @param entity The entity this component is attached to.
*/
virtual void Update(float deltaTime) {}
@@ -52,6 +55,21 @@ public:
* This can be overridden by derived components to provide custom UI.
*/
virtual void OnImGuiRender() { /* Default implementation does nothing */ }
/**
* Set the parent entity of this component.
* @param parent A shared pointer to the parent entity.
*/
void SetParent(std::shared_ptr<Entity> parent) { m_parent = parent; }
/**
* Get the parent entity of this component.
* @return A shared pointer to the parent entity, or nullptr if it has been destroyed.
*/
std::shared_ptr<Entity> GetParent() const { return m_parent.lock(); }
private:
std::weak_ptr<Entity> m_parent;
};

View File

@@ -4,6 +4,14 @@
#include <Fmod/core/inc/fmod.hpp>
#include <string>
#include <filesystem>
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <Shlwapi.h>
#pragma comment(lib, "Shlwapi.lib")
#include <commdlg.h> // Pour GetOpenFileName
#include "camera_class.h"
#include "transform_component.h"
namespace ecs {
class AudioComponent : public Component {
@@ -25,38 +33,51 @@ public:
return;
}
result = m_system->init(512, FMOD_INIT_NORMAL, nullptr);
result = m_system->init(512, FMOD_INIT_NORMAL | FMOD_INIT_3D_RIGHTHANDED, nullptr);
if (result != FMOD_OK) {
m_lastError = "<EFBFBD>chec de l'initialisation du syst<73>me FMOD: " + std::to_string(result);
m_system->release();
m_system = nullptr;
}
}
auto parent = GetParent();
if (parent && parent->GetCamera())
{
SetCamera(parent->GetCamera());
}
}
bool Load(const std::string& path) {
if (!m_system) {
Initialize();
if (!m_system) return false; // L'erreur est d<>j<EFBFBD> d<>finie dans Initialize()
if (!m_system) return false;
}
m_soundPath = path;
// V<>rifier si le fichier existe
if (!std::filesystem::exists(path)) {
m_lastError = "Fichier non trouv<75>: " + path;
return false;
}
// Lib<69>rer le son pr<70>c<EFBFBD>dent s'il existe
if (m_sound) {
m_sound->release();
m_sound = nullptr;
}
// Essayer de charger avec le chemin absolu
FMOD_MODE mode = FMOD_DEFAULT;
if (m_spatialized)
mode |= FMOD_3D;
if (m_looping)
mode |= FMOD_LOOP_NORMAL;
else
mode |= FMOD_LOOP_OFF;
std::filesystem::path absolutePath = std::filesystem::absolute(path);
FMOD_RESULT result = m_system->createSound(absolutePath.string().c_str(), FMOD_DEFAULT, nullptr, &m_sound);
FMOD_RESULT result = m_system->createSound(absolutePath.string().c_str(), mode, nullptr, &m_sound);
if (result != FMOD_OK) {
m_lastError = "<EFBFBD>chec du chargement du son: " + std::to_string(result) +
" (chemin: " + absolutePath.string() + ")";
@@ -67,8 +88,41 @@ public:
}
void Play() {
if (m_system && m_sound)
if (m_system && m_sound) {
bool isPlaying = false;
if (m_channel) {
m_channel->isPlaying(&isPlaying);
}
if (isPlaying) {
m_channel->stop();
}
m_system->playSound(m_sound, nullptr, false, &m_channel);
if (m_channel) {
m_channel->setVolume(m_volume);
m_channel->setPan(m_pan);
m_channel->setPitch(m_pitch);
m_channel->setMute(m_muted);
m_channel->setPriority(m_priority);
m_channel->setPaused(m_paused);
}
}
}
void Pause() {
if (m_channel) {
m_paused = true;
m_channel->setPaused(true);
}
}
void Resume() {
if (m_channel) {
m_paused = false;
m_channel->setPaused(false);
}
}
void Stop() {
@@ -76,49 +130,288 @@ public:
m_channel->stop();
}
void OnImGuiRender() override {
// Afficher le r<>pertoire de travail actuel
std::string currentDir = std::filesystem::current_path().string();
ImGui::Text("R<EFBFBD>pertoire actuel: %s", currentDir.c_str());
void SetCamera(camera_class* camera) {m_camera = camera; }
if (m_sound) {
ImGui::Text("Son charg<72>: %s", m_soundPath.c_str());
if (ImGui::Button("Jouer")) {
void Update(float deltaTime) override {
if (!m_spatialized) return;
if (!m_system || !m_camera) return;
auto parent = GetParent();
if (!parent) return;
auto transform = parent->GetComponent<TransformComponent>();
if (!transform) return;
XMVECTOR pos = transform->GetPosition();
m_position.x = XMVectorGetX(pos);
m_position.y = XMVectorGetY(pos);
m_position.z = XMVectorGetZ(pos);
if (m_channel) {
FMOD_VECTOR velocity = { 0.f, 0.f, 0.f }; // <20> am<61>liorer si possible - calculer depuis d<>placement temps+position ?
m_channel->set3DAttributes(&m_position, &velocity);
}
// Mise <20> jour du listener FMOD
XMFLOAT3 camPos = m_camera->get_position();
XMFLOAT3 camForward = m_camera->get_forward();
XMFLOAT3 camUp = m_camera->get_up();
FMOD_VECTOR listenerPos = { camPos.x, camPos.y, camPos.z };
FMOD_VECTOR forward = { camForward.x, camForward.y, camForward.z };
FMOD_VECTOR up = { camUp.x, camUp.y, camUp.z };
FMOD_VECTOR listenerVel = { 0.f, 0.f, 0.f };
if (m_use_velocity)
{
static XMFLOAT3 lastCamPos = camPos;
XMFLOAT3 currentCamPos = camPos;
float deltaTimeSec = deltaTime > 0.0001f ? deltaTime : 0.016f; // <20>viter division par z<>ro
XMFLOAT3 listenerVelocity = {
(currentCamPos.x - lastCamPos.x) / deltaTimeSec,
(currentCamPos.y - lastCamPos.y) / deltaTimeSec,
(currentCamPos.z - lastCamPos.z) / deltaTimeSec
};
lastCamPos = currentCamPos;
listenerVel = { listenerVelocity.x, listenerVelocity.y, listenerVelocity.z };
}
m_system->set3DListenerAttributes(0, &listenerPos, &listenerVel, &forward, &up);
m_system->update();
}
void OnImGuiRender() override {
if (!m_sound) {
ImGui::Text("No audio file loaded");
if (ImGui::Button("Load audio file")) {
OPENFILENAME ofn;
wchar_t szFile[MAX_PATH] = L"";
ZeroMemory(&ofn, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = NULL; // Renseigner si possible avec handle de la fen<65>tre
ofn.lpstrFile = szFile;
ofn.nMaxFile = MAX_PATH;
ofn.lpstrFilter = L"Audio Files\0*.mp3;*.wav;*.ogg;*.flac\0All Files\0*.*\0";
ofn.nFilterIndex = 1;
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
if (GetOpenFileName(&ofn)) {
wchar_t exePath[MAX_PATH];
GetModuleFileName(NULL, exePath, MAX_PATH);
PathRemoveFileSpec(exePath);
std::wstring targetDir = std::wstring(exePath) + L"\\assets\\sounds";
DWORD ftyp = GetFileAttributes(targetDir.c_str());
if (ftyp == INVALID_FILE_ATTRIBUTES) {
CreateDirectory((std::wstring(exePath) + L"\\assets").c_str(), NULL);
CreateDirectory(targetDir.c_str(), NULL);
}
const wchar_t* filename = wcsrchr(szFile, L'\\');
std::wstring fileNameStr = filename ? (filename + 1) : szFile;
std::wstring targetPath = targetDir + L"\\" + fileNameStr;
if (!CopyFile(szFile, targetPath.c_str(), FALSE)) {
MessageBox(NULL, L"Erreur lors de la copie du fichier audio.", L"Erreur", MB_OK | MB_ICONERROR);
} else {
int utf8size = WideCharToMultiByte(CP_UTF8, 0, targetPath.c_str(), -1, NULL, 0, NULL, NULL);
std::string pathStr(utf8size, 0);
WideCharToMultiByte(CP_UTF8, 0, targetPath.c_str(), -1, &pathStr[0], utf8size, NULL, NULL);
pathStr.resize(utf8size - 1);
if (!Load(pathStr)) {
// erreur dans m_lastError
}
}
}
}
if (!m_lastError.empty()) {
ImGui::TextColored(ImVec4(1, 0, 0, 1), "Error: %s", m_lastError.c_str());
}
} else {
ImGui::Text("Loaded: %s", m_soundPath.c_str());
if (ImGui::Button("Change audio")) {
OPENFILENAME ofn;
wchar_t szFile[MAX_PATH] = L"";
ZeroMemory(&ofn, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = NULL;
ofn.lpstrFile = szFile;
ofn.nMaxFile = MAX_PATH;
ofn.lpstrFilter = L"Audio Files\0*.mp3;*.wav;*.ogg;*.flac\0All Files\0*.*\0";
ofn.nFilterIndex = 1;
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
if (GetOpenFileName(&ofn)) {
wchar_t exePath[MAX_PATH];
GetModuleFileName(NULL, exePath, MAX_PATH);
PathRemoveFileSpec(exePath);
std::wstring targetDir = std::wstring(exePath) + L"\\assets\\sounds";
DWORD ftyp = GetFileAttributes(targetDir.c_str());
if (ftyp == INVALID_FILE_ATTRIBUTES) {
CreateDirectory((std::wstring(exePath) + L"\\assets").c_str(), NULL);
CreateDirectory(targetDir.c_str(), NULL);
}
const wchar_t* filename = wcsrchr(szFile, L'\\');
std::wstring fileNameStr = filename ? (filename + 1) : szFile;
std::wstring targetPath = targetDir + L"\\" + fileNameStr;
if (!CopyFile(szFile, targetPath.c_str(), FALSE)) {
MessageBox(NULL, L"Erreur lors de la copie du fichier audio.", L"Erreur", MB_OK | MB_ICONERROR);
} else {
int utf8size = WideCharToMultiByte(CP_UTF8, 0, targetPath.c_str(), -1, NULL, 0, NULL, NULL);
std::string pathStr(utf8size, 0);
WideCharToMultiByte(CP_UTF8, 0, targetPath.c_str(), -1, &pathStr[0], utf8size, NULL, NULL);
pathStr.resize(utf8size - 1);
if (!Load(pathStr)) {
// erreur dans m_lastError
}
}
}
}
ImGui::SameLine();
if (ImGui::Button("Remove audio")) {
Stop();
if (m_sound) m_sound->release();
m_sound = nullptr;
m_soundPath.clear();
}
ImGui::Separator();
if (ImGui::Button("Play")) {
Play();
}
ImGui::SameLine();
if (ImGui::Button("Arr<EFBFBD>ter")) {
if (ImGui::Button(m_paused ? "Resume" : "Pause")) {
if (m_paused) Resume();
else Pause();
}
ImGui::SameLine();
if (ImGui::Button("Stop")) {
Stop();
}
} else {
ImGui::Text("Aucun son charg<72>");
// Montrer l'erreur s'il y en a une
if (!m_lastError.empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Erreur: %s", m_lastError.c_str());
ImGui::Separator();
if (ImGui::SliderFloat("Volume", &m_volume, 0.0f, 1.0f)) {
if (m_channel) m_channel->setVolume(m_volume);
}
// exe path + assets/sounds/default.mp3
std::filesystem::path defaultSoundPath = std::filesystem::current_path() / "assets" / "sounds" / "default.mp3";
std::string defaultSoundPathStr = defaultSoundPath.string();// Input text with default path
char path[260];
strncpy_s(path, m_soundPath.empty() ? defaultSoundPathStr.c_str() : m_soundPath.c_str(), sizeof(path));
path[sizeof(path) - 1] = '\0'; // Assurer la termination nulle
ImGui::InputText("Chemin du fichier", path, sizeof(path));
if (ImGui::Button("Charger le son")) {
if (!Load(path)) {
// L'erreur est d<>j<EFBFBD> d<>finie dans Load()
if (ImGui::SliderFloat("Pan", &m_pan, -1.0f, 1.0f)) {
if (m_channel) m_channel->setPan(m_pan);
}
if (ImGui::SliderFloat("Pitch", &m_pitch, 0.5f, 2.0f)) {
if (m_channel) m_channel->setPitch(m_pitch);
}
if (ImGui::Checkbox("Looping", &m_looping)) {
// Recharger le son pour appliquer le mode loop
Load(m_soundPath);
}
if (ImGui::Checkbox("Spatialization 3D", &m_spatialized)) {
if (!m_soundPath.empty()) {
Load(m_soundPath); // Recharger le son avec ou sans 3D selon le choix
}
}
// check box to use velocity for doppler effect (only if spatialized)
if (m_spatialized) {
ImGui::SameLine();
ImGui::Checkbox("Use Velocity for Doppler", &m_use_velocity);
}
if (ImGui::Checkbox("Mute", &m_muted)) {
if (m_channel) m_channel->setMute(m_muted);
}
if (ImGui::SliderInt("Priority", &m_priority, 0, 256)) {
if (m_channel) m_channel->setPriority(m_priority);
}
ImGui::Separator();
if (m_channel && m_sound) {
unsigned int pos = 0, len = 0;
m_channel->getPosition(&pos, FMOD_TIMEUNIT_MS);
m_sound->getLength(&len, FMOD_TIMEUNIT_MS);
float progress = (len > 0) ? (float)pos / len : 0.0f;
ImGui::ProgressBar(progress, ImVec2(300, 0), nullptr);
auto to_minsec = [](unsigned int ms) {
unsigned int totalSec = ms / 1000;
return std::make_pair(totalSec / 60, totalSec % 60);
};
auto [curMin, curSec] = to_minsec(pos);
auto [lenMin, lenSec] = to_minsec(len);
ImGui::Text("%02u:%02u / %02u:%02u", curMin, curSec, lenMin, lenSec);
}
// afficher la position de l'objet et de la camera ainsi que la rotaion de la camera si spatialis<69>
if (m_spatialized) {
auto parent = GetParent();
if (parent) {
auto transform = parent->GetComponent<TransformComponent>();
if (transform) {
XMVECTOR pos = transform->GetPosition();
ImGui::Text("Object Position: (%.2f, %.2f, %.2f)", XMVectorGetX(pos), XMVectorGetY(pos), XMVectorGetZ(pos));
}
}
if (m_camera) {
XMFLOAT3 camPos = m_camera->get_position();
XMFLOAT3 camRot = m_camera->get_rotation();
ImGui::Text("Camera Position: (%.2f, %.2f, %.2f)", camPos.x, camPos.y, camPos.z);
ImGui::Text("Camera Rotation: (%.2f, %.2f, %.2f)", camRot.x, camRot.y, camRot.z);
}
}
if (!m_lastError.empty()) {
ImGui::TextColored(ImVec4(1,0,0,1), "Error: %s", m_lastError.c_str());
}
}
}
private:
FMOD::System* m_system;
FMOD::Sound* m_sound;
FMOD::Channel* m_channel;
std::string m_soundPath;
std::string m_lastError;
float m_volume = 1.0f;
float m_pan = 0.0f;
float m_pitch = 1.0f;
bool m_looping = false;
bool m_muted = false;
bool m_paused = false;
int m_priority = 128;
bool m_spatialized = false;
bool m_use_velocity = false;
FMOD_VECTOR m_position = {0.0f, 0.0f, 0.0f};
camera_class* m_camera = nullptr;
};
}

View File

@@ -1,6 +1,7 @@
#pragma once
#include "../component.h"
#include <DirectXMath.h>
#include <sstream>
using namespace DirectX;

View File

@@ -6,13 +6,15 @@
#include <algorithm>
#include <cassert>
#include "camera_class.h"
namespace ecs {
/**
* Type alias for a unique identifier for an entity.
*/
using EntityID = uint32_t;
class Entity {
class Entity : public std::enable_shared_from_this<Entity> {
public:
/**
* Builder for an Entity with a unique ID.
@@ -59,6 +61,9 @@ public:
// Cr<43>er et ajouter le composant
auto component = std::make_shared<T>(std::forward<Args>(args)...);
m_Components[typeID] = component;
component->SetParent(shared_from_this());
// Initialiser le composant
component->Initialize();
@@ -132,6 +137,9 @@ public:
return m_Components;
}
void SetCamera(camera_class* camera) {m_camera = camera; }
camera_class* GetCamera() const { return m_camera; }
private:
/**
@@ -143,6 +151,9 @@ private:
* The key is the type ID of the component, and the value is a shared pointer to the component.
*/
std::unordered_map<ComponentTypeID, ComponentPtr> m_Components;
// camera
camera_class* m_camera = nullptr;
};
} // namespace ecs

View File

@@ -31,6 +31,7 @@ public:
}
auto entity = std::make_shared<Entity>(id);
entity->SetCamera(camera_);
m_Entities[id] = entity;
return entity;
@@ -144,10 +145,14 @@ public:
}
void SetCamera(camera_class* camera) { camera_ = camera; }
camera_class* GetCamera() const { return camera_; }
private:
EntityID m_NextEntityID;
std::unordered_map<EntityID, std::shared_ptr<Entity>> m_Entities;
std::queue<EntityID> m_FreeIDs; // IDs <20> r<>utiliser
camera_class* camera_ = nullptr;
};
} // namespace ecs

View File

@@ -504,6 +504,9 @@ bool application_class::initialize(int screenWidth, int screenHeight, HWND hwnd,
culling_active_ = true;
culling_thread_ = std::thread(&application_class::culling_thread_function, this);
entity_manager_->SetCamera(camera_);
}
catch (const std::exception& e)
{
@@ -858,6 +861,7 @@ bool application_class::frame(input_class* Input)
}
active_camera_->render();
entity_manager_->UpdateEntities(frameTime);
// render the static graphics scene.
result = render(rotation, x, y, z, textureTranslation);