Files
khaotic-engine-Reborn/enginecustom/src/inc/system/ecs/components/audio_component.h
CatChow0 de05631608 Minor - Enhances ECS component functionality - V13.5.0
Adds serialization/deserialization to components,
allowing saving and loading of entity states.
Provides ImGui widgets for editing component properties,
improving editor usability.
Passes the D3D device and context to entities and
components enabling resource creation within components.
2025-09-16 18:26:33 +02:00

518 lines
18 KiB
C++

// AudioComponent.h
#pragma once
#include "../component.h"
#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 {
public:
AudioComponent() : m_sound(nullptr), m_channel(nullptr) {}
~AudioComponent() override {
if (m_sound) m_sound->release();
// Ne pas libérer m_system ici car il peut être partagé
}
/**
* Initialize the audio component by setting up the FMOD system and camera from the parent entity.
* This method is called when the component is added to an entity.
*/
void Initialize() override
{
auto parent = GetParent();
if (parent && parent->GetCamera())
{
SetCamera(parent->GetCamera());
SetSoundSystem(parent->GetSoundSystem());
}
}
/**
* Load an audio file from the specified path.
* If the FMOD system is not initialized, it will be initialized first.
* Creates a sound object with appropriate settings based on the component's properties.
* @param path The file path to load the audio from.
* @return True if the audio file was loaded successfully, otherwise false.
*/
bool Load(const std::string& path) {
if (!m_system) {
Initialize();
if (!m_system) return false;
}
m_soundPath = path;
if (!std::filesystem::exists(path)) {
m_lastError = "Fichier non trouvé: " + path;
return false;
}
if (m_sound) {
m_sound->release();
m_sound = nullptr;
}
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(), mode, nullptr, &m_sound);
if (result != FMOD_OK) {
m_lastError = "Échec du chargement du son: " + std::to_string(result) +
" (chemin: " + absolutePath.string() + ")";
return false;
}
return true;
}
/**
* Play the loaded audio sound.
* If the sound is already playing, it will be stopped and restarted.
* The channel properties such as volume, pan, pitch, mute, priority, and paused state are applied.
*/
void Play() {
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);
}
}
}
/**
* Pause the currently playing audio sound.
* If the sound is not playing, this method has no effect.
*/
void Pause() {
if (m_channel) {
m_paused = true;
m_channel->setPaused(true);
}
}
/**
* Resume the paused audio sound.
* If the sound is not paused, this method has no effect.
*/
void Resume() {
if (m_channel) {
m_paused = false;
m_channel->setPaused(false);
}
}
/**
* Stop the currently playing audio sound.
* If the sound is not playing, this method has no effect.
*/
void Stop() {
if (m_channel)
m_channel->stop();
}
/**
* Set the camera to be used for 3D audio spatialization.
* This camera is used to update the listener attributes in FMOD.
* @param camera Pointer to the camera_class instance.
*/
void SetCamera(camera_class* camera) {m_camera = camera; }
/**
* Update the audio component.
* @param deltaTime Time since the last update.
*/
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 }; // à améliorer si possible - calculer depuis déplacement temps+position ?
m_channel->set3DAttributes(&m_position, &velocity);
}
// Mise à 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; // é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();
}
/**
* ImGui Widget to control the audio component properties.
* Allows loading/changing/removing audio files, and adjusting properties like volume, pan, pitch, looping, spatialization, etc.
* Displays error messages if loading fails.
* This method is called during the ImGui rendering phase.
*/
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ê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(m_paused ? "Resume" : "Pause")) {
if (m_paused) Resume();
else Pause();
}
ImGui::SameLine();
if (ImGui::Button("Stop")) {
Stop();
}
ImGui::Separator();
if (ImGui::SliderFloat("Volume", &m_volume, 0.0f, 1.0f)) {
if (m_channel) m_channel->setVolume(m_volume);
}
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é
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());
}
}
}
/**
* Set the FMOD system to be used by this audio component.
* This method allows sharing the FMOD system instance across multiple audio components.
* @param system Pointer to the FMOD::System instance.
*/
void SetSoundSystem(FMOD::System* system) {
m_system = system;
}
/**
* Serialize the audio component's state to a string.
* This includes properties like sound path, volume, pan, pitch, looping, muted, paused, priority, spatialization, and use of velocity.
* This method is useful for saving the component's state or debugging.
* @return A string representation of the audio component's state.
*/
std::string Serialize() const override {
std::stringstream ss;
ss << "AudioComponent:"
<< m_soundPath << ":"
<< m_volume << ":"
<< m_pan << ":"
<< m_pitch << ":"
<< m_looping << ":"
<< m_muted << ":"
<< m_paused << ":"
<< m_priority << ":"
<< m_spatialized << ":"
<< m_use_velocity;
return ss.str();
}
/**
* Deserialize the audio component's state from a string.
* This method parses the string and sets the component's properties accordingly.
* If the sound path is valid, it attempts to load the audio file.
* @param data The string representation of the audio component's state.
* @return True if deserialization was successful, otherwise false.
*/
bool Deserialize(const std::string& data) override {
std::stringstream ss(data);
std::string type;
std::getline(ss, type, ':');
if (type != "AudioComponent") return false;
std::string s_volume, s_pan, s_pitch, s_looping, s_muted, s_paused, s_priority, s_spatialized, s_use_velocity;
std::getline(ss, m_soundPath, ':');
std::getline(ss, s_volume, ':');
std::getline(ss, s_pan, ':');
std::getline(ss, s_pitch, ':');
std::getline(ss, s_looping, ':');
std::getline(ss, s_muted, ':');
std::getline(ss, s_paused, ':');
std::getline(ss, s_priority, ':');
std::getline(ss, s_spatialized, ':');
std::getline(ss, s_use_velocity, ':');
m_volume = std::stof(s_volume);
m_pan = std::stof(s_pan);
m_pitch = std::stof(s_pitch);
m_looping = (s_looping == "1");
m_muted = (s_muted == "1");
m_paused = (s_paused == "1");
m_priority = std::stoi(s_priority);
m_spatialized = (s_spatialized == "1");
m_use_velocity = (s_use_velocity == "1");
// Recharger le son si le chemin existe (optional: ou laisser le chargement manuel)
if (!m_soundPath.empty() && m_system) {
Load(m_soundPath);
}
return true;
}
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;
};
}