diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f2dd11..1c94791 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ set(APP_SOURCES # --- core/audio --- source/core/audio/audio.cpp + source/core/audio/audio_adapter.cpp # --- core/input --- source/core/input/define_buttons.cpp diff --git a/source/core/audio/audio.cpp b/source/core/audio/audio.cpp index 7e25f8b..633a767 100644 --- a/source/core/audio/audio.cpp +++ b/source/core/audio/audio.cpp @@ -1,6 +1,6 @@ #include "core/audio/audio.hpp" -#include // Para SDL_LogInfo, SDL_LogCategory, SDL_G... +#include // Para SDL_GetError, SDL_Init #include // Para clamp #include // Para std::cout @@ -8,9 +8,9 @@ // Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp). // clang-format off #undef STB_VORBIS_HEADER_ONLY -#include "external/stb_vorbis.h" -// stb_vorbis.h filtra les macros L, C i R (i PLAYBACK_*) al TU. Les netegem -// perquè xocarien amb noms de paràmetres de plantilla en json.hpp i altres. +#include "external/stb_vorbis.c" +// stb_vorbis.c filtra les macros L, C i R (i PLAYBACK_*) al TU. Les netegem +// perquè xocarien amb noms de paràmetres de plantilla en altres headers. #undef L #undef C #undef R @@ -19,9 +19,9 @@ #undef PLAYBACK_RIGHT // clang-format on -#include "core/audio/jail_audio.hpp" // Para JA_FadeOutMusic, JA_Init, JA_PauseM... -#include "core/resources/resource.hpp" // Para Resource -#include "game/options.hpp" // Para AudioOptions, audio, MusicOptions +#include "core/audio/audio_adapter.hpp" // Para AudioResource::getMusic/getSound +#include "core/audio/jail_audio.hpp" // Para JA_* +#include "game/options.hpp" // Para Options::audio // Singleton Audio* Audio::instance = nullptr; @@ -30,7 +30,10 @@ Audio* Audio::instance = nullptr; void Audio::init() { Audio::instance = new Audio(); } // Libera la instancia -void Audio::destroy() { delete Audio::instance; } +void Audio::destroy() { + delete Audio::instance; + Audio::instance = nullptr; +} // Obtiene la instancia auto Audio::get() -> Audio* { return Audio::instance; } @@ -46,17 +49,57 @@ Audio::~Audio() { // Método principal void Audio::update() { JA_Update(); + + // Sincronizar estado: detectar cuando la música se para (ej. fade-out completado) + if (instance && instance->music_.state == MusicState::PLAYING && JA_GetMusicState() != JA_MUSIC_PLAYING) { + instance->music_.state = MusicState::STOPPED; + } } -// Reproduce la música -void Audio::playMusic(const std::string& name, const int loop) { - music_.name = name; - music_.loop = (loop != 0); +// Reproduce la música por nombre (con crossfade opcional) +void Audio::playMusic(const std::string& name, const int loop, const int crossfade_ms) { + bool new_loop = (loop != 0); - if (music_enabled_ && music_.state != MusicState::PLAYING) { - JA_PlayMusic(Resource::get()->getMusic(name), loop); - music_.state = MusicState::PLAYING; + // Si ya está sonando exactamente la misma pista y mismo modo loop, no hacemos nada + if (music_.state == MusicState::PLAYING && music_.name == name && music_.loop == new_loop) { + return; } + + if (!music_enabled_) return; + + auto* resource = AudioResource::getMusic(name); + if (resource == nullptr) return; + + if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) { + JA_CrossfadeMusic(resource, crossfade_ms, loop); + } else { + if (music_.state == MusicState::PLAYING) { + JA_StopMusic(); + } + JA_PlayMusic(resource, loop); + } + + music_.name = name; + music_.loop = new_loop; + music_.state = MusicState::PLAYING; +} + +// Reproduce la música por puntero (con crossfade opcional) +void Audio::playMusic(JA_Music_t* music, const int loop, const int crossfade_ms) { + if (!music_enabled_ || music == nullptr) return; + + if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) { + JA_CrossfadeMusic(music, crossfade_ms, loop); + } else { + if (music_.state == MusicState::PLAYING) { + JA_StopMusic(); + } + JA_PlayMusic(music, loop); + } + + music_.name.clear(); // nom desconegut quan es passa per punter + music_.loop = (loop != 0); + music_.state = MusicState::PLAYING; } // Pausa la música @@ -83,10 +126,17 @@ void Audio::stopMusic() { } } -// Reproduce un sonido +// Reproduce un sonido por nombre void Audio::playSound(const std::string& name, Group group) const { if (sound_enabled_) { - JA_PlaySound(Resource::get()->getSound(name), 0, static_cast(group)); + JA_PlaySound(AudioResource::getSound(name), 0, static_cast(group)); + } +} + +// Reproduce un sonido por puntero directo +void Audio::playSound(JA_Sound_t* sound, Group group) const { + if (sound_enabled_ && sound != nullptr) { + JA_PlaySound(sound, 0, static_cast(group)); } } @@ -120,20 +170,20 @@ auto Audio::getRealMusicState() -> MusicState { } } -// Establece el volumen de los sonidos -void Audio::setSoundVolume(int sound_volume, Group group) const { +// Establece el volumen de los sonidos (float 0.0..1.0) +void Audio::setSoundVolume(float sound_volume, Group group) const { if (sound_enabled_) { sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME); - const float CONVERTED_VOLUME = (sound_volume / 100.0F) * (Options::audio.volume / 100.0F); + const float CONVERTED_VOLUME = sound_volume * Options::audio.volume; JA_SetSoundVolume(CONVERTED_VOLUME, static_cast(group)); } } -// Establece el volumen de la música -void Audio::setMusicVolume(int music_volume) const { +// Establece el volumen de la música (float 0.0..1.0) +void Audio::setMusicVolume(float music_volume) const { if (music_enabled_) { music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME); - const float CONVERTED_VOLUME = (music_volume / 100.0F) * (Options::audio.volume / 100.0F); + const float CONVERTED_VOLUME = music_volume * Options::audio.volume; JA_SetMusicVolume(CONVERTED_VOLUME); } } @@ -159,4 +209,4 @@ void Audio::initSDLAudio() { JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2); enable(Options::audio.enabled); } -} \ No newline at end of file +} diff --git a/source/core/audio/audio.hpp b/source/core/audio/audio.hpp index 22593fb..9c5ea6f 100644 --- a/source/core/audio/audio.hpp +++ b/source/core/audio/audio.hpp @@ -1,111 +1,114 @@ #pragma once +#include // Para int8_t, uint8_t #include // Para string #include // Para move // --- Clase Audio: gestor de audio (singleton) --- +// Implementació canònica, byte-idèntica entre projectes. +// Els volums es manegen internament com a float 0.0–1.0; la capa de +// presentació (menús, notificacions) usa les helpers toPercent/fromPercent +// per mostrar 0–100 a l'usuari. class Audio { public: // --- Enums --- - enum class Group : int { + enum class Group : std::int8_t { ALL = -1, // Todos los grupos GAME = 0, // Sonidos del juego INTERFACE = 1 // Sonidos de la interfaz }; - enum class MusicState { + enum class MusicState : std::uint8_t { PLAYING, // Reproduciendo música PAUSED, // Música pausada STOPPED, // Música detenida }; // --- Constantes --- - static constexpr int MAX_VOLUME = 100; // Volumen máximo - static constexpr int MIN_VOLUME = 0; // Volumen mínimo - static constexpr int FREQUENCY = 48000; // Frecuencia de audio + static constexpr float MAX_VOLUME = 1.0F; // Volumen máximo (float 0..1) + static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo (float 0..1) + static constexpr float VOLUME_STEP = 0.05F; // Pas estàndard per a UI (5%) + static constexpr int FREQUENCY = 48000; // Frecuencia de audio + static constexpr int DEFAULT_CROSSFADE_MS = 1500; // Duració del crossfade per defecte (ms) - // --- Métodos de singleton --- + // --- Singleton --- static void init(); // Inicializa el objeto Audio static void destroy(); // Libera el objeto Audio static auto get() -> Audio*; // Obtiene el puntero al objeto Audio Audio(const Audio&) = delete; // Evitar copia auto operator=(const Audio&) -> Audio& = delete; // Evitar asignación - // --- Método principal --- - static void update(); + static void update(); // Actualización del sistema de audio - // --- Control de Música --- - void playMusic(const std::string& name, int loop = -1); // Reproducir música en bucle - void pauseMusic(); // Pausar reproducción de música - void resumeMusic(); // Continua la música pausada - void stopMusic(); // Detener completamente la música - void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música + // --- Control de música --- + void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproducir música por nombre (con crossfade opcional) + void playMusic(struct JA_Music_t* music, int loop = -1, int crossfade_ms = 0); // Reproducir música por puntero (con crossfade opcional) + void pauseMusic(); // Pausar reproducción de música + void resumeMusic(); // Continua la música pausada + void stopMusic(); // Detener completamente la música + void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música - // --- Control de Sonidos --- - void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual - void stopAllSounds() const; // Detener todos los sonidos + // --- Control de sonidos --- + void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre + void playSound(struct JA_Sound_t* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero + void stopAllSounds() const; // Detener todos los sonidos - // --- Configuración General --- + // --- Control de volumen (API interna: float 0.0..1.0) --- + void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos + void setMusicVolume(float volume) const; // Ajustar volumen de música + + // --- Helpers de conversió per a la capa de presentació --- + // UI (menús, notificacions) manega enters 0..100; internament viu float 0..1. + static constexpr auto toPercent(float volume) -> int { + return static_cast(volume * 100.0F + 0.5F); + } + static constexpr auto fromPercent(int percent) -> float { + return static_cast(percent) / 100.0F; + } + + // --- Configuración general --- void enable(bool value); // Establecer estado general void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general void applySettings(); // Aplica la configuración - // --- Configuración de Sonidos --- + // --- Configuración de sonidos --- void enableSound() { sound_enabled_ = true; } // Habilitar sonidos void disableSound() { sound_enabled_ = false; } // Deshabilitar sonidos void enableSound(bool value) { sound_enabled_ = value; } // Establecer estado de sonidos void toggleSound() { sound_enabled_ = !sound_enabled_; } // Alternar estado de sonidos - // --- Configuración de Música --- + // --- Configuración de música --- void enableMusic() { music_enabled_ = true; } // Habilitar música void disableMusic() { music_enabled_ = false; } // Deshabilitar música void enableMusic(bool value) { music_enabled_ = value; } // Establecer estado de música void toggleMusic() { music_enabled_ = !music_enabled_; } // Alternar estado de música - // --- Control de Volumen --- - void setSoundVolume(int volume, Group group = Group::ALL) const; // Ajustar volumen de efectos - void setMusicVolume(int volume) const; // Ajustar volumen de música - - // --- Getters para debug --- + // --- Consultas de estado --- [[nodiscard]] auto isEnabled() const -> bool { return enabled_; } [[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; } [[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; } [[nodiscard]] auto getMusicState() const -> MusicState { return music_.state; } - [[nodiscard]] static auto getRealMusicState() -> MusicState; // Consulta directamente a jailaudio + [[nodiscard]] static auto getRealMusicState() -> MusicState; [[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; } private: - // --- Estructuras privadas --- + // --- Tipos anidados --- struct Music { - MusicState state; // Estado actual de la música (reproduciendo, detenido, en pausa) - std::string name; // Última pista de música reproducida - bool loop; // Indica si la última pista de música se debe reproducir en bucle - - // Constructor para inicializar la música con valores predeterminados - Music() - : state(MusicState::STOPPED), - loop(false) {} - - // Constructor para inicializar con valores específicos - Music(MusicState init_state, std::string init_name, bool init_loop) - : state(init_state), - name(std::move(init_name)), - loop(init_loop) {} + MusicState state{MusicState::STOPPED}; // Estado actual de la música + std::string name; // Última pista de música reproducida + bool loop{false}; // Indica si se reproduce en bucle }; - // --- Variables de estado --- - Music music_; // Estado de la música - bool enabled_ = true; // Estado general del audio - bool sound_enabled_ = true; // Estado de los efectos de sonido - bool music_enabled_ = true; // Estado de la música - - // --- Métodos internos --- + // --- Métodos --- + Audio(); // Constructor privado + ~Audio(); // Destructor privado void initSDLAudio(); // Inicializa SDL Audio - // --- Constructores y destructor privados (singleton) --- - Audio(); // Constructor privado - ~Audio(); // Destructor privado - - // --- Instancia singleton --- + // --- Variables miembro --- static Audio* instance; // Instancia única de Audio -}; \ No newline at end of file + + Music music_; // Estado de la música + bool enabled_{true}; // Estado general del audio + bool sound_enabled_{true}; // Estado de los efectos de sonido + bool music_enabled_{true}; // Estado de la música +}; diff --git a/source/core/audio/audio_adapter.cpp b/source/core/audio/audio_adapter.cpp new file mode 100644 index 0000000..c46a008 --- /dev/null +++ b/source/core/audio/audio_adapter.cpp @@ -0,0 +1,13 @@ +#include "core/audio/audio_adapter.hpp" + +#include "core/resources/resource.hpp" + +namespace AudioResource { + JA_Music_t* getMusic(const std::string& name) { + return Resource::get()->getMusic(name); + } + + JA_Sound_t* getSound(const std::string& name) { + return Resource::get()->getSound(name); + } +} // namespace AudioResource diff --git a/source/core/audio/audio_adapter.hpp b/source/core/audio/audio_adapter.hpp new file mode 100644 index 0000000..a5eb16e --- /dev/null +++ b/source/core/audio/audio_adapter.hpp @@ -0,0 +1,17 @@ +#pragma once + +// --- Audio Resource Adapter --- +// Aquest fitxer exposa una interfície comuna a Audio per obtenir JA_Music_t* / +// JA_Sound_t* per nom. Cada projecte la implementa en audio_adapter.cpp +// delegant al seu singleton de recursos (Resource::get(), Resource::Cache::get(), +// etc.). Això permet que audio.hpp/audio.cpp siguin idèntics entre projectes. + +#include // Para string + +struct JA_Music_t; +struct JA_Sound_t; + +namespace AudioResource { + JA_Music_t* getMusic(const std::string& name); + JA_Sound_t* getSound(const std::string& name); +} // namespace AudioResource diff --git a/source/core/audio/jail_audio.hpp b/source/core/audio/jail_audio.hpp index c6ded04..63308e0 100644 --- a/source/core/audio/jail_audio.hpp +++ b/source/core/audio/jail_audio.hpp @@ -3,26 +3,41 @@ // --- Includes --- #include #include // Para uint32_t, uint8_t -#include // Para NULL, fseek, printf, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET +#include // Para NULL, fseek, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET #include // Para free, malloc -#include // Para strcpy, strlen #include // Para std::cout +#include // Para std::unique_ptr +#include // Para std::string +#include // Para std::vector #define STB_VORBIS_HEADER_ONLY -#include "external/stb_vorbis.h" // Para stb_vorbis_decode_memory +#include "external/stb_vorbis.c" // Para stb_vorbis_open_memory i streaming + +// Deleter stateless per a buffers reservats amb `SDL_malloc` / `SDL_LoadWAV*`. +// Compatible amb `std::unique_ptr` — zero size +// overhead gràcies a EBO, igual que un unique_ptr amb default_delete. +struct SDLFreeDeleter { + void operator()(Uint8* p) const noexcept { + if (p) SDL_free(p); + } +}; // --- Public Enums --- -enum JA_Channel_state { JA_CHANNEL_INVALID, +enum JA_Channel_state { + JA_CHANNEL_INVALID, JA_CHANNEL_FREE, JA_CHANNEL_PLAYING, JA_CHANNEL_PAUSED, - JA_SOUND_DISABLED }; -enum JA_Music_state { JA_MUSIC_INVALID, + JA_SOUND_DISABLED, +}; +enum JA_Music_state { + JA_MUSIC_INVALID, JA_MUSIC_PLAYING, JA_MUSIC_PAUSED, JA_MUSIC_STOPPED, - JA_MUSIC_DISABLED }; + JA_MUSIC_DISABLED, +}; // --- Struct Definitions --- #define JA_MAX_SIMULTANEOUS_CHANNELS 20 @@ -31,7 +46,9 @@ enum JA_Music_state { JA_MUSIC_INVALID, struct JA_Sound_t { SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000}; Uint32 length{0}; - Uint8* buffer{NULL}; + // Buffer descomprimit (PCM) propietat del sound. Reservat per SDL_LoadWAV + // via SDL_malloc; el deleter `SDLFreeDeleter` allibera amb SDL_free. + std::unique_ptr buffer; }; struct JA_Channel_t { @@ -46,21 +63,22 @@ struct JA_Channel_t { struct JA_Music_t { SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000}; - // OGG comprimit en memòria. Propietat nostra; es copia des del fitxer una - // sola vegada en JA_LoadMusic i es descomprimix en chunks per streaming. - Uint8* ogg_data{nullptr}; - Uint32 ogg_length{0}; - stb_vorbis* vorbis{nullptr}; // Handle del decoder, viu tot el cicle del JA_Music_t + // OGG comprimit en memòria. Propietat nostra; es copia des del buffer + // d'entrada una sola vegada en JA_LoadMusic i es descomprimix en chunks + // per streaming. Com que stb_vorbis guarda un punter persistent al + // `.data()` d'aquest vector, no el podem resize'jar un cop establert + // (una reallocation invalidaria el punter que el decoder conserva). + std::vector ogg_data; + stb_vorbis* vorbis{nullptr}; // handle del decoder, viu tot el cicle del JA_Music_t - char* filename{nullptr}; + std::string filename; - int times{0}; // Loops restants (-1 = infinit, 0 = un sol play) + int times{0}; // loops restants (-1 = infinit, 0 = un sol play) SDL_AudioStream* stream{nullptr}; JA_Music_state state{JA_MUSIC_INVALID}; }; -// --- Internal Global State --- -// Marcado 'inline' (C++17) para asegurar una única instancia. +// --- Internal Global State (inline, C++17) --- inline JA_Music_t* current_music{nullptr}; inline JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS]; @@ -72,15 +90,27 @@ inline bool JA_musicEnabled{true}; inline bool JA_soundEnabled{true}; inline SDL_AudioDeviceID sdlAudioDevice{0}; -inline bool fading{false}; -inline int fade_start_time{0}; -inline int fade_duration{0}; -inline float fade_initial_volume{0.0f}; // Corregido de 'int' a 'float' +// --- Crossfade / Fade State --- +struct JA_FadeState { + bool active{false}; + Uint64 start_time{0}; + int duration_ms{0}; + float initial_volume{0.0f}; +}; + +struct JA_OutgoingMusic { + SDL_AudioStream* stream{nullptr}; + JA_FadeState fade; +}; + +inline JA_OutgoingMusic outgoing_music; +inline JA_FadeState incoming_fade; // --- Forward Declarations --- inline void JA_StopMusic(); inline void JA_StopChannel(const int channel); inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop = 0, const int group = 0); +inline void JA_CrossfadeMusic(JA_Music_t* music, int crossfade_ms, int loop = -1); // --- Music streaming internals --- // Bytes-per-sample per canal (sempre s16) @@ -98,15 +128,15 @@ inline int JA_FeedMusicChunk(JA_Music_t* music) { if (!music || !music->vorbis || !music->stream) return 0; short chunk[JA_MUSIC_CHUNK_SHORTS]; - const int channels = music->spec.channels; + const int num_channels = music->spec.channels; const int samples_per_channel = stb_vorbis_get_samples_short_interleaved( music->vorbis, - channels, + num_channels, chunk, JA_MUSIC_CHUNK_SHORTS); if (samples_per_channel <= 0) return 0; - const int bytes = samples_per_channel * channels * JA_MUSIC_BYTES_PER_SAMPLE; + const int bytes = samples_per_channel * num_channels * JA_MUSIC_BYTES_PER_SAMPLE; SDL_PutAudioStreamData(music->stream, chunk, bytes); return samples_per_channel; } @@ -133,20 +163,51 @@ inline void JA_PumpMusic(JA_Music_t* music) { } } +// Pre-carrega `duration_ms` de so dins l'stream actual abans que l'stream +// siga robat per outgoing_music (crossfade o fade-out). Imprescindible amb +// streaming: l'stream robat no es pot re-alimentar perquè perd la referència +// al seu vorbis decoder. No aplica loop — si el vorbis s'esgota abans, parem. +inline void JA_PreFillOutgoing(JA_Music_t* music, int duration_ms) { + if (!music || !music->vorbis || !music->stream) return; + + const int bytes_per_second = music->spec.freq * music->spec.channels * JA_MUSIC_BYTES_PER_SAMPLE; + const int needed_bytes = static_cast((static_cast(duration_ms) * bytes_per_second) / 1000); + + while (SDL_GetAudioStreamAvailable(music->stream) < needed_bytes) { + const int decoded = JA_FeedMusicChunk(music); + if (decoded <= 0) break; // EOF: deixem drenar el que hi haja + } +} + // --- Core Functions --- inline void JA_Update() { + // --- Outgoing music fade-out (crossfade o fade-out a silencio) --- + if (outgoing_music.stream && outgoing_music.fade.active) { + Uint64 now = SDL_GetTicks(); + Uint64 elapsed = now - outgoing_music.fade.start_time; + if (elapsed >= (Uint64)outgoing_music.fade.duration_ms) { + SDL_DestroyAudioStream(outgoing_music.stream); + outgoing_music.stream = nullptr; + outgoing_music.fade.active = false; + } else { + float percent = (float)elapsed / (float)outgoing_music.fade.duration_ms; + SDL_SetAudioStreamGain(outgoing_music.stream, outgoing_music.fade.initial_volume * (1.0f - percent)); + } + } + + // --- Current music --- if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) { - if (fading) { - int time = SDL_GetTicks(); - if (time > (fade_start_time + fade_duration)) { - fading = false; - JA_StopMusic(); - return; + // Fade-in (parte de un crossfade) + if (incoming_fade.active) { + Uint64 now = SDL_GetTicks(); + Uint64 elapsed = now - incoming_fade.start_time; + if (elapsed >= (Uint64)incoming_fade.duration_ms) { + incoming_fade.active = false; + SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume); } else { - const int time_passed = time - fade_start_time; - const float percent = (float)time_passed / (float)fade_duration; - SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * (1.0 - percent)); + float percent = (float)elapsed / (float)incoming_fade.duration_ms; + SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * percent); } } @@ -158,12 +219,13 @@ inline void JA_Update() { } } + // --- Sound channels --- if (JA_soundEnabled) { for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) if (channels[i].state == JA_CHANNEL_PLAYING) { if (channels[i].times != 0) { if ((Uint32)SDL_GetAudioStreamAvailable(channels[i].stream) < (channels[i].sound->length / 2)) { - SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer, channels[i].sound->length); + SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer.get(), channels[i].sound->length); if (channels[i].times > 0) channels[i].times--; } } else { @@ -174,12 +236,8 @@ inline void JA_Update() { } inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) { -#ifdef _DEBUG - SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG); -#endif - JA_audioSpec = {format, num_channels, freq}; - if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice + if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec); if (sdlAudioDevice == 0) std::cout << "Failed to initialize SDL audio!" << '\n'; for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE; @@ -187,7 +245,11 @@ inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_ } inline void JA_Quit() { - if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice + if (outgoing_music.stream) { + SDL_DestroyAudioStream(outgoing_music.stream); + outgoing_music.stream = nullptr; + } + if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); sdlAudioDevice = 0; } @@ -196,26 +258,25 @@ inline void JA_Quit() { inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) { if (!buffer || length == 0) return nullptr; - // Còpia del OGG comprimit: stb_vorbis llig de forma persistent aquesta - // memòria mentre el handle estiga viu, així que hem de posseir-la nosaltres. - Uint8* ogg_copy = static_cast(SDL_malloc(length)); - if (!ogg_copy) return nullptr; - SDL_memcpy(ogg_copy, buffer, length); + // Allocem el JA_Music_t primer per aprofitar el seu `std::vector` + // com a propietari del OGG comprimit. stb_vorbis guarda un punter + // persistent al buffer; com que ací no el resize'jem, el .data() és + // estable durant tot el cicle de vida del music. + auto* music = new JA_Music_t(); + music->ogg_data.assign(buffer, buffer + length); int error = 0; - stb_vorbis* vorbis = stb_vorbis_open_memory(ogg_copy, static_cast(length), &error, nullptr); - if (!vorbis) { - SDL_free(ogg_copy); + music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(), + static_cast(length), + &error, + nullptr); + if (!music->vorbis) { std::cout << "JA_LoadMusic: stb_vorbis_open_memory failed (error " << error << ")" << '\n'; + delete music; return nullptr; } - auto* music = new JA_Music_t(); - music->ogg_data = ogg_copy; - music->ogg_length = length; - music->vorbis = vorbis; - - const stb_vorbis_info info = stb_vorbis_get_info(vorbis); + const stb_vorbis_info info = stb_vorbis_get_info(music->vorbis); music->spec.channels = info.channels; music->spec.freq = static_cast(info.sample_rate); music->spec.format = SDL_AUDIO_S16; @@ -224,31 +285,36 @@ inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) { return music; } +// Overload amb filename — els callers l'usen per poder comparar la música +// en curs amb JA_GetMusicFilename() i no rearrancar-la si ja és la mateixa. +inline JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filename) { + JA_Music_t* music = JA_LoadMusic(static_cast(buffer), length); + if (music && filename) music->filename = filename; + return music; +} + inline JA_Music_t* JA_LoadMusic(const char* filename) { - // [RZC 28/08/22] Carreguem primer el arxiu en memòria i després el descomprimim. Es algo més rapid. + // Carreguem primer el arxiu en memòria i després el descomprimim. FILE* f = fopen(filename, "rb"); - if (!f) return NULL; // Añadida comprobación de apertura + if (!f) return nullptr; fseek(f, 0, SEEK_END); long fsize = ftell(f); fseek(f, 0, SEEK_SET); auto* buffer = static_cast(malloc(fsize + 1)); - if (!buffer) { // Añadida comprobación de malloc + if (!buffer) { fclose(f); - return NULL; + return nullptr; } if (fread(buffer, fsize, 1, f) != 1) { fclose(f); free(buffer); - return NULL; + return nullptr; } fclose(f); - JA_Music_t* music = JA_LoadMusic(buffer, fsize); - if (music) { // Comprobar que JA_LoadMusic tuvo éxito - music->filename = static_cast(malloc(strlen(filename) + 1)); - if (music->filename) { - strcpy(music->filename, filename); - } + JA_Music_t* music = JA_LoadMusic(static_cast(buffer), static_cast(fsize)); + if (music) { + music->filename = filename; } free(buffer); @@ -280,18 +346,20 @@ inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) { // Pre-cargem el buffer abans de bindejar per evitar un underrun inicial. JA_PumpMusic(current_music); - if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n"); + if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) { + std::cout << "[ERROR] SDL_BindAudioStream failed!" << '\n'; + } } -inline char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) { +inline const char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) { if (!music) music = current_music; - if (!music) return nullptr; // Añadida comprobación - return music->filename; + if (!music || music->filename.empty()) return nullptr; + return music->filename.c_str(); } inline void JA_PauseMusic() { if (!JA_musicEnabled) return; - if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; // Comprobación mejorada + if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; current_music->state = JA_MUSIC_PAUSED; SDL_UnbindAudioStream(current_music->stream); @@ -299,13 +367,21 @@ inline void JA_PauseMusic() { inline void JA_ResumeMusic() { if (!JA_musicEnabled) return; - if (!current_music || current_music->state != JA_MUSIC_PAUSED) return; // Comprobación mejorada + if (!current_music || current_music->state != JA_MUSIC_PAUSED) return; current_music->state = JA_MUSIC_PLAYING; SDL_BindAudioStream(sdlAudioDevice, current_music->stream); } inline void JA_StopMusic() { + // Limpiar outgoing crossfade si existe + if (outgoing_music.stream) { + SDL_DestroyAudioStream(outgoing_music.stream); + outgoing_music.stream = nullptr; + outgoing_music.fade.active = false; + } + incoming_fade.active = false; + if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return; current_music->state = JA_MUSIC_STOPPED; @@ -318,17 +394,73 @@ inline void JA_StopMusic() { if (current_music->vorbis) { stb_vorbis_seek_start(current_music->vorbis); } - // No liberem filename aquí; es fa en JA_DeleteMusic. } inline void JA_FadeOutMusic(const int milliseconds) { if (!JA_musicEnabled) return; - if (current_music == NULL || current_music->state == JA_MUSIC_INVALID) return; + if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; - fading = true; - fade_start_time = SDL_GetTicks(); - fade_duration = milliseconds; - fade_initial_volume = JA_musicVolume; + // Destruir outgoing anterior si existe + if (outgoing_music.stream) { + SDL_DestroyAudioStream(outgoing_music.stream); + outgoing_music.stream = nullptr; + } + + // Pre-omplim l'stream amb `milliseconds` de so: un cop robat, ja no + // tindrà accés al vorbis decoder i només podrà drenar el que tinga. + JA_PreFillOutgoing(current_music, milliseconds); + + // Robar el stream del current_music al outgoing + outgoing_music.stream = current_music->stream; + outgoing_music.fade = {true, SDL_GetTicks(), milliseconds, JA_musicVolume}; + + // Dejar current_music sin stream (ya lo tiene outgoing) + current_music->stream = nullptr; + current_music->state = JA_MUSIC_STOPPED; + if (current_music->vorbis) stb_vorbis_seek_start(current_music->vorbis); + incoming_fade.active = false; +} + +inline void JA_CrossfadeMusic(JA_Music_t* music, const int crossfade_ms, const int loop) { + if (!JA_musicEnabled || !music || !music->vorbis) return; + + // Destruir outgoing anterior si existe (crossfade durante crossfade) + if (outgoing_music.stream) { + SDL_DestroyAudioStream(outgoing_music.stream); + outgoing_music.stream = nullptr; + outgoing_music.fade.active = false; + } + + // Robar el stream de la musica actual al outgoing para el fade-out. + // Pre-omplim amb `crossfade_ms` de so perquè no es quede en silenci + // abans d'acabar el fade (l'stream robat ja no pot alimentar-se). + if (current_music && current_music->state == JA_MUSIC_PLAYING && current_music->stream) { + JA_PreFillOutgoing(current_music, crossfade_ms); + outgoing_music.stream = current_music->stream; + outgoing_music.fade = {true, SDL_GetTicks(), crossfade_ms, JA_musicVolume}; + current_music->stream = nullptr; + current_music->state = JA_MUSIC_STOPPED; + if (current_music->vorbis) stb_vorbis_seek_start(current_music->vorbis); + } + + // Iniciar la nueva pista con gain=0 (el fade-in la sube gradualmente) + current_music = music; + current_music->state = JA_MUSIC_PLAYING; + current_music->times = loop; + + stb_vorbis_seek_start(current_music->vorbis); + current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &JA_audioSpec); + if (!current_music->stream) { + std::cout << "Failed to create audio stream for crossfade!" << '\n'; + current_music->state = JA_MUSIC_STOPPED; + return; + } + SDL_SetAudioStreamGain(current_music->stream, 0.0f); + JA_PumpMusic(current_music); // pre-carrega abans de bindejar + SDL_BindAudioStream(sdlAudioDevice, current_music->stream); + + // Configurar fade-in + incoming_fade = {true, SDL_GetTicks(), crossfade_ms, 0.0f}; } inline JA_Music_state JA_GetMusicState() { @@ -346,8 +478,8 @@ inline void JA_DeleteMusic(JA_Music_t* music) { } if (music->stream) SDL_DestroyAudioStream(music->stream); if (music->vorbis) stb_vorbis_close(music->vorbis); - SDL_free(music->ogg_data); - free(music->filename); // filename es libera aquí + // ogg_data (std::vector) i filename (std::string) s'alliberen sols + // al destructor de JA_Music_t. delete music; } @@ -360,49 +492,40 @@ inline float JA_SetMusicVolume(float volume) { } inline void JA_SetMusicPosition(float /*value*/) { - // No implementat amb el backend de streaming. Mai va arribar a usar-se - // en el codi existent, així que es manté com a stub. + // No implementat amb el backend de streaming. } inline float JA_GetMusicPosition() { - // Veure nota a JA_SetMusicPosition. return 0.0f; } inline void JA_EnableMusic(const bool value) { if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic(); - JA_musicEnabled = value; } // --- Sound Functions --- -inline JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length) { - JA_Sound_t* sound = new JA_Sound_t(); - sound->buffer = buffer; - sound->length = length; - // Nota: spec se queda con los valores por defecto. - return sound; -} - inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) { - JA_Sound_t* sound = new JA_Sound_t(); - if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length)) { + auto sound = std::make_unique(); + Uint8* raw = nullptr; + if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &raw, &sound->length)) { std::cout << "Failed to load WAV from memory: " << SDL_GetError() << '\n'; - delete sound; return nullptr; } - return sound; + sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer + return sound.release(); } inline JA_Sound_t* JA_LoadSound(const char* filename) { - JA_Sound_t* sound = new JA_Sound_t(); - if (!SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length)) { + auto sound = std::make_unique(); + Uint8* raw = nullptr; + if (!SDL_LoadWAV(filename, &sound->spec, &raw, &sound->length)) { std::cout << "Failed to load WAV file: " << SDL_GetError() << '\n'; - delete sound; return nullptr; } - return sound; + sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer + return sound.release(); } inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = 0) { @@ -422,12 +545,12 @@ inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int if (!JA_soundEnabled || !sound) return -1; if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1; - JA_StopChannel(channel); // Detiene y limpia el canal si estaba en uso + JA_StopChannel(channel); channels[channel].sound = sound; channels[channel].times = loop; channels[channel].pos = 0; - channels[channel].group = group; // Asignar grupo + channels[channel].group = group; channels[channel].state = JA_CHANNEL_PLAYING; channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec); @@ -437,7 +560,7 @@ inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int return -1; } - SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length); + SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer.get(), channels[channel].sound->length); SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]); SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream); @@ -449,7 +572,7 @@ inline void JA_DeleteSound(JA_Sound_t* sound) { for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) { if (channels[i].sound == sound) JA_StopChannel(i); } - SDL_free(sound->buffer); + // buffer es destrueix automàticament via RAII (SDLFreeDeleter). delete sound; } @@ -495,7 +618,7 @@ inline void JA_StopChannel(const int channel) { channels[i].stream = nullptr; channels[i].state = JA_CHANNEL_FREE; channels[i].pos = 0; - channels[i].sound = NULL; + channels[i].sound = nullptr; } } } else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) { @@ -504,7 +627,7 @@ inline void JA_StopChannel(const int channel) { channels[channel].stream = nullptr; channels[channel].state = JA_CHANNEL_FREE; channels[channel].pos = 0; - channels[channel].sound = NULL; + channels[channel].sound = nullptr; } } } @@ -516,8 +639,7 @@ inline JA_Channel_state JA_GetChannelState(const int channel) { return channels[channel].state; } -inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para todos los grupos -{ +inline float JA_SetSoundVolume(float volume, const int group = -1) { const float v = SDL_clamp(volume, 0.0f, 1.0f); if (group == -1) { @@ -527,10 +649,10 @@ inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para t } else if (group >= 0 && group < JA_MAX_GROUPS) { JA_soundVolume[group] = v; } else { - return v; // Grupo inválido + return v; } - // Aplicar volumen a canales activos + // Aplicar volum als canals actius. for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) { if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) { if (group == -1 || channels[i].group == group) { @@ -545,13 +667,13 @@ inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para t inline void JA_EnableSound(const bool value) { if (!value) { - JA_StopChannel(-1); // Detener todos los canales + JA_StopChannel(-1); } JA_soundEnabled = value; } inline float JA_SetVolume(float volume) { float v = JA_SetMusicVolume(volume); - JA_SetSoundVolume(v, -1); // Aplicar a todos los grupos de sonido + JA_SetSoundVolume(v, -1); return v; } diff --git a/source/core/system/defaults.hpp b/source/core/system/defaults.hpp index 2504f25..b0be501 100644 --- a/source/core/system/defaults.hpp +++ b/source/core/system/defaults.hpp @@ -195,17 +195,17 @@ namespace Defaults::Video { namespace Defaults::Music { constexpr bool ENABLED = true; - constexpr int VOLUME = 100; + constexpr float VOLUME = 0.8F; } // namespace Defaults::Music namespace Defaults::Sound { constexpr bool ENABLED = true; - constexpr int VOLUME = 100; + constexpr float VOLUME = 1.0F; } // namespace Defaults::Sound namespace Defaults::Audio { constexpr bool ENABLED = true; - constexpr int VOLUME = 100; + constexpr float VOLUME = 1.0F; } // namespace Defaults::Audio namespace Defaults::Settings { diff --git a/source/external/stb_vorbis.h b/source/external/stb_vorbis.c similarity index 100% rename from source/external/stb_vorbis.h rename to source/external/stb_vorbis.c diff --git a/source/game/options.cpp b/source/game/options.cpp index 084e372..1380742 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -459,7 +459,7 @@ namespace Options { } if (aud.contains("volume")) { try { - audio.volume = std::clamp(aud["volume"].get_value(), 0, 100); + audio.volume = std::clamp(aud["volume"].get_value(), 0.0F, 1.0F); } catch (...) {} } if (aud.contains("music")) { @@ -471,7 +471,7 @@ namespace Options { } if (mus.contains("volume")) { try { - audio.music.volume = std::clamp(mus["volume"].get_value(), 0, 100); + audio.music.volume = std::clamp(mus["volume"].get_value(), 0.0F, 1.0F); } catch (...) {} } } @@ -484,7 +484,7 @@ namespace Options { } if (snd.contains("volume")) { try { - audio.sound.volume = std::clamp(snd["volume"].get_value(), 0, 100); + audio.sound.volume = std::clamp(snd["volume"].get_value(), 0.0F, 1.0F); } catch (...) {} } } diff --git a/source/game/options.hpp b/source/game/options.hpp index 804330a..7070b6f 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -94,19 +94,19 @@ namespace Options { struct Music { bool enabled = Defaults::Music::ENABLED; // Indica si la música suena o no - int volume = Defaults::Music::VOLUME; // Volumen de la música + float volume = Defaults::Music::VOLUME; // Volumen de la música (0.0..1.0) }; struct Sound { bool enabled = Defaults::Sound::ENABLED; // Indica si los sonidos suenan o no - int volume = Defaults::Sound::VOLUME; // Volumen de los sonidos + float volume = Defaults::Sound::VOLUME; // Volumen de los sonidos (0.0..1.0) }; struct Audio { Music music; // Opciones para la música Sound sound; // Opciones para los efectos de sonido bool enabled = Defaults::Audio::ENABLED; // Indica si el audio está activo o no - int volume = Defaults::Audio::VOLUME; // Volumen general del audio + float volume = Defaults::Audio::VOLUME; // Volumen general del audio (0.0..1.0) }; struct Loading { diff --git a/source/game/ui/menu_option.hpp b/source/game/ui/menu_option.hpp index 4a20732..a0657f3 100644 --- a/source/game/ui/menu_option.hpp +++ b/source/game/ui/menu_option.hpp @@ -106,6 +106,40 @@ class IntOption : public MenuOption { int min_value_, max_value_, step_value_; }; +// VolumeOption: emmagatzema un float 0.0..1.0 però es mostra/edita com a int 0..100. +// Pensat per als sliders de volum que l'usuari veu com percentatge però que +// internament viuen en float (API unificada del motor d'àudio). +class VolumeOption : public MenuOption { + public: + VolumeOption(const std::string& cap, ServiceMenu::SettingsGroup grp, float* var, int step_percent = 5) + : MenuOption(cap, grp), + linked_variable_(var), + step_value_(step_percent) {} + + [[nodiscard]] auto getBehavior() const -> Behavior override { return Behavior::ADJUST; } + [[nodiscard]] auto getValueAsString() const -> std::string override { + int pct = static_cast(*linked_variable_ * 100.0F + 0.5F); + return std::to_string(pct); + } + void adjustValue(bool adjust_up) override { + int current = static_cast(*linked_variable_ * 100.0F + 0.5F); + int new_value = std::clamp(current + (adjust_up ? step_value_ : -step_value_), 0, 100); + *linked_variable_ = static_cast(new_value) / 100.0F; + } + auto getMaxValueWidth(Text* text_renderer) const -> int override { + int max_width = 0; + for (int value = 0; value <= 100; value += step_value_) { + int width = text_renderer->length(std::to_string(value), -2); + max_width = std::max(max_width, width); + } + return max_width; + } + + private: + float* linked_variable_; + int step_value_; +}; + class ListOption : public MenuOption { public: ListOption(const std::string& cap, ServiceMenu::SettingsGroup grp, std::vector values, std::function current_value_getter, std::function new_value_setter) diff --git a/source/game/ui/service_menu.cpp b/source/game/ui/service_menu.cpp index 1b6080d..225599a 100644 --- a/source/game/ui/service_menu.cpp +++ b/source/game/ui/service_menu.cpp @@ -490,28 +490,22 @@ void ServiceMenu::initializeOptions() { SettingsGroup::AUDIO, &Options::audio.enabled)); - options_.push_back(std::make_unique( + options_.push_back(std::make_unique( Lang::getText("[SERVICE_MENU] MAIN_VOLUME"), SettingsGroup::AUDIO, &Options::audio.volume, - 0, - 100, 5)); - options_.push_back(std::make_unique( + options_.push_back(std::make_unique( Lang::getText("[SERVICE_MENU] MUSIC_VOLUME"), SettingsGroup::AUDIO, &Options::audio.music.volume, - 0, - 100, 5)); - options_.push_back(std::make_unique( + options_.push_back(std::make_unique( Lang::getText("[SERVICE_MENU] SFX_VOLUME"), SettingsGroup::AUDIO, &Options::audio.sound.volume, - 0, - 100, 5)); // SETTINGS