diff --git a/source/jail_audio.hpp b/source/jail_audio.hpp index 35cac80..fe763ef 100644 --- a/source/jail_audio.hpp +++ b/source/jail_audio.hpp @@ -43,18 +43,22 @@ struct JA_Channel_t { struct JA_Music_t { SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000}; - Uint32 length{0}; - Uint8* buffer{nullptr}; + + // 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 + char* filename{nullptr}; - int pos{0}; - int times{0}; + 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 unica instancia. +// Marcado 'inline' (C++17) para asegurar una única instancia. inline JA_Music_t* current_music{nullptr}; inline JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS]; @@ -76,6 +80,57 @@ 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); +// --- Music streaming internals --- +// Bytes-per-sample per canal (sempre s16) +static constexpr int JA_MUSIC_BYTES_PER_SAMPLE = 2; +// Quants shorts decodifiquem per crida a get_samples_short_interleaved. +// 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz. +static constexpr int JA_MUSIC_CHUNK_SHORTS = 8192; +// Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a +// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns. +static constexpr float JA_MUSIC_LOW_WATER_SECONDS = 0.5f; + +// Decodifica un chunk del vorbis i el volca a l'stream. Retorna samples +// decodificats per canal (0 = EOF de l'stream vorbis). +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 samples_per_channel = stb_vorbis_get_samples_short_interleaved( + music->vorbis, + 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; + SDL_PutAudioStreamData(music->stream, chunk, bytes); + return samples_per_channel; +} + +// Reompli l'stream fins que tinga ≥ JA_MUSIC_LOW_WATER_SECONDS bufferats. +// En arribar a EOF del vorbis, aplica el loop (times) o deixa drenar. +inline void JA_PumpMusic(JA_Music_t* music) { + 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 low_water_bytes = static_cast(JA_MUSIC_LOW_WATER_SECONDS * static_cast(bytes_per_second)); + + while (SDL_GetAudioStreamAvailable(music->stream) < low_water_bytes) { + const int decoded = JA_FeedMusicChunk(music); + if (decoded > 0) continue; + + // EOF: si queden loops, rebobinar; si no, tallar i deixar drenar. + if (music->times != 0) { + stb_vorbis_seek_start(music->vorbis); + if (music->times > 0) music->times--; + } else { + break; + } + } +} + // --- Core Functions --- inline void JA_Update() { @@ -93,13 +148,11 @@ inline void JA_Update() { } } - if (current_music->times != 0) { - if ((Uint32)SDL_GetAudioStreamAvailable(current_music->stream) < (current_music->length / 2)) { - SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length); - } - if (current_music->times > 0) current_music->times--; - } else { - if (SDL_GetAudioStreamAvailable(current_music->stream) == 0) JA_StopMusic(); + // Streaming: rellenem l'stream fins al low-water-mark i parem si el + // vorbis s'ha esgotat i no queden loops. + JA_PumpMusic(current_music); + if (current_music->times == 0 && SDL_GetAudioStreamAvailable(current_music->stream) == 0) { + JA_StopMusic(); } } @@ -139,19 +192,31 @@ inline void JA_Quit() { // --- Music Functions --- inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) { - JA_Music_t* music = new JA_Music_t(); + if (!buffer || length == 0) return nullptr; - int chan, samplerate; - short* output; - music->length = stb_vorbis_decode_memory(buffer, length, &chan, &samplerate, &output) * chan * 2; + // 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); - music->spec.channels = chan; - music->spec.freq = samplerate; + int error = 0; + stb_vorbis* vorbis = stb_vorbis_open_memory(ogg_copy, static_cast(length), &error, nullptr); + if (!vorbis) { + SDL_free(ogg_copy); + SDL_Log("JA_LoadMusic: stb_vorbis_open_memory failed (error %d)", error); + 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); + music->spec.channels = info.channels; + music->spec.freq = static_cast(info.sample_rate); music->spec.format = SDL_AUDIO_S16; - music->buffer = static_cast(SDL_malloc(music->length)); - SDL_memcpy(music->buffer, output, music->length); - free(output); - music->pos = 0; music->state = JA_MUSIC_STOPPED; return music; @@ -189,26 +254,38 @@ inline JA_Music_t* JA_LoadMusic(const char* filename) { } inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) { - if (!JA_musicEnabled || !music) return; + if (!JA_musicEnabled || !music || !music->vorbis) return; JA_StopMusic(); current_music = music; - current_music->pos = 0; current_music->state = JA_MUSIC_PLAYING; current_music->times = loop; + // Rebobinem l'stream de vorbis al principi. Cobreix tant play-per-primera- + // vegada com replays/canvis de track que tornen a la mateixa pista. + stb_vorbis_seek_start(current_music->vorbis); + current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &JA_audioSpec); if (!current_music->stream) { SDL_Log("Failed to create audio stream!"); current_music->state = JA_MUSIC_STOPPED; return; } - if (!SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length)) printf("[ERROR] SDL_PutAudioStreamData failed!\n"); SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume); + + // 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"); } +inline char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) { + if (!music) music = current_music; + if (!music) return nullptr; + return music->filename; +} + inline void JA_PauseMusic() { if (!JA_musicEnabled) return; if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; @@ -228,12 +305,16 @@ inline void JA_ResumeMusic() { inline void JA_StopMusic() { if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return; - current_music->pos = 0; current_music->state = JA_MUSIC_STOPPED; if (current_music->stream) { SDL_DestroyAudioStream(current_music->stream); current_music->stream = nullptr; } + // Deixem el handle de vorbis viu — es tanca en JA_DeleteMusic. + // Rebobinem perquè un futur JA_PlayMusic comence des del principi. + if (current_music->vorbis) { + stb_vorbis_seek_start(current_music->vorbis); + } } inline void JA_FadeOutMusic(const int milliseconds) { @@ -259,8 +340,9 @@ inline void JA_DeleteMusic(JA_Music_t* music) { JA_StopMusic(); current_music = nullptr; } - SDL_free(music->buffer); if (music->stream) SDL_DestroyAudioStream(music->stream); + if (music->vorbis) stb_vorbis_close(music->vorbis); + SDL_free(music->ogg_data); free(music->filename); delete music; } @@ -273,14 +355,14 @@ inline float JA_SetMusicVolume(float volume) { return JA_musicVolume; } -inline void JA_SetMusicPosition(float value) { - if (!current_music) return; - current_music->pos = value * current_music->spec.freq; +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. } inline float JA_GetMusicPosition() { - if (!current_music) return 0; - return float(current_music->pos) / float(current_music->spec.freq); + // Veure nota a JA_SetMusicPosition. + return 0.0f; } inline void JA_EnableMusic(const bool value) {