a45cb95030
- [NEW] music.state() - [NEW] contsants music.INVALID, music.PLAYING, music.PAUSED, music.STOPPED i music.DISABLED - [NEW] El exemple en lua inclos te una secció per a mostrar les funcionalitats de la música
671 lines
23 KiB
C++
671 lines
23 KiB
C++
#if BACKEND == SDL3
|
|
|
|
#include "backends/backend.h"
|
|
#include "state.h"
|
|
#include "external/stb_vorbis.h"
|
|
#include "other/log.h"
|
|
|
|
#include <stdio.h>
|
|
#include <vector>
|
|
|
|
// structs i variables
|
|
// =============================
|
|
namespace backend
|
|
{
|
|
namespace audio
|
|
{
|
|
static SDL_AudioSpec audioSpec { SDL_AUDIO_S16, 2, 48000 };
|
|
SDL_AudioDeviceID sdlAudioDevice { 0 };
|
|
SDL_TimerID timerID { 0 };
|
|
|
|
namespace music
|
|
{
|
|
struct music_t
|
|
{
|
|
SDL_AudioSpec spec { SDL_AUDIO_S16, 2, 48000 };
|
|
SDL_AudioStream* stream { nullptr };
|
|
|
|
const uint8_t* ogg_data { nullptr };
|
|
uint32_t ogg_length { 0 };
|
|
|
|
stb_vorbis* vorbis { nullptr };
|
|
stb_vorbis_info info {};
|
|
|
|
int times { 0 };
|
|
music::state state { music::state::invalid };
|
|
};
|
|
|
|
static int current { -1 };
|
|
static std::vector<music_t> musics;
|
|
static float volume { 1.0f };
|
|
static bool enabled { true };
|
|
|
|
namespace fade
|
|
{
|
|
static bool fading = false;
|
|
static int start_time;
|
|
static int duration;
|
|
static int initial_volume;
|
|
}
|
|
}
|
|
|
|
namespace sound
|
|
{
|
|
struct sound_t
|
|
{
|
|
SDL_AudioSpec spec { SDL_AUDIO_S16, 2, 48000 };
|
|
Uint32 length { 0 };
|
|
Uint8 *buffer { NULL };
|
|
};
|
|
|
|
static std::vector<sound_t> sounds;
|
|
static float volume { 0.5f };
|
|
static bool enabled { true };
|
|
|
|
namespace channel
|
|
{
|
|
struct channel_t
|
|
{
|
|
int sound { -1 };
|
|
int pos { 0 };
|
|
int times { 0 };
|
|
SDL_AudioStream *stream { nullptr };
|
|
channel::state state { channel::state::free };
|
|
};
|
|
|
|
static std::vector<channel_t> channels;
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Funcions
|
|
// ==================
|
|
namespace backend
|
|
{
|
|
namespace audio
|
|
{
|
|
static void updateMusic()
|
|
{
|
|
if (!music::enabled) return;
|
|
if (music::current < 0) return;
|
|
|
|
auto& m = music::musics[music::current];
|
|
if (m.state != music::state::playing) return;
|
|
|
|
// Fade-out
|
|
if (music::fade::fading) {
|
|
int time = SDL_GetTicks();
|
|
if (time > (music::fade::start_time + music::fade::duration)) {
|
|
music::fade::fading = false;
|
|
music::stop();
|
|
return;
|
|
} else {
|
|
float percent = float(time - music::fade::start_time) / float(music::fade::duration);
|
|
SDL_SetAudioStreamGain(m.stream, 1.0f - percent);
|
|
}
|
|
}
|
|
|
|
// ¿Hay suficiente audio en el stream?
|
|
if (SDL_GetAudioStreamAvailable(m.stream) > 48000) return; // 1 segundo
|
|
|
|
// Decodificar un bloque
|
|
const int SAMPLES = 4096;
|
|
static int16_t temp[SAMPLES * 2];
|
|
|
|
int n = stb_vorbis_get_samples_short_interleaved(
|
|
m.vorbis,
|
|
m.info.channels,
|
|
temp,
|
|
SAMPLES
|
|
);
|
|
|
|
if (n == 0) {
|
|
if (m.times != 0) {
|
|
stb_vorbis_seek_start(m.vorbis);
|
|
if (m.times > 0) m.times--;
|
|
return;
|
|
} else {
|
|
music::stop();
|
|
return;
|
|
}
|
|
}
|
|
|
|
SDL_PutAudioStreamData(m.stream, temp, n * m.info.channels * sizeof(int16_t));
|
|
}
|
|
|
|
static void updateSound()
|
|
{
|
|
if (sound::enabled)
|
|
{
|
|
for (int i=0; i < static_cast<int>(sound::channel::channels.size()); ++i) {
|
|
auto &c = sound::channel::channels[i];
|
|
if (c.state == sound::channel::state::playing)
|
|
{
|
|
if (c.times != 0)
|
|
{
|
|
auto &s = sound::sounds[c.sound];
|
|
if (SDL_GetAudioStreamAvailable(c.stream) < static_cast<int>(s.length/2))
|
|
SDL_PutAudioStreamData(c.stream, s.buffer, s.length);
|
|
if (c.times>0) c.times--;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (SDL_GetAudioStreamAvailable(c.stream) == 0) sound::channel::stop(i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Uint32 updateCallback(void *userdata, SDL_TimerID timerID, Uint32 interval)
|
|
{
|
|
updateMusic();
|
|
updateSound();
|
|
return 30;
|
|
}
|
|
|
|
void init()
|
|
{
|
|
#ifdef DEBUG
|
|
SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG);
|
|
#endif
|
|
|
|
audioSpec = {SDL_AUDIO_S16, 1, 48000 };
|
|
if (!sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
|
|
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audioSpec);
|
|
if (!sdlAudioDevice) {
|
|
log_msg(LOG_FAIL, "Failed to initialize SDL audio: %s\n", SDL_GetError());
|
|
} else {
|
|
log_msg(LOG_OK, "Audio subsytem initialized\n");
|
|
}
|
|
//SDL_PauseAudioDevice(sdlAudioDevice);
|
|
timerID = SDL_AddTimer(30, updateCallback, nullptr);
|
|
}
|
|
|
|
void quit()
|
|
{
|
|
if (timerID) SDL_RemoveTimer(timerID);
|
|
|
|
for (int i=0; i<static_cast<int>(music::musics.size());++i) music::destroy(i);
|
|
music::musics.clear();
|
|
|
|
for (int i=0; i<static_cast<int>(sound::channel::channels.size());++i) sound::channel::stop(i);
|
|
sound::channel::channels.clear();
|
|
|
|
for (int i=0; i<static_cast<int>(sound::sounds.size());++i) sound::destroy(i);
|
|
sound::sounds.clear();
|
|
|
|
if (!sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
|
|
sdlAudioDevice = 0;
|
|
}
|
|
|
|
float setVolume(float vol)
|
|
{
|
|
sound::setVolume(music::setVolume(vol) / 2.0f);
|
|
return music::volume;
|
|
}
|
|
|
|
|
|
namespace music
|
|
{
|
|
int load(const uint8_t* buffer, uint32_t length)
|
|
{
|
|
int idx = 0;
|
|
while (idx < (int)musics.size() && musics[idx].state != state::invalid) idx++;
|
|
if (idx == (int)musics.size()) musics.emplace_back();
|
|
|
|
auto& m = musics[idx];
|
|
|
|
m.ogg_data = buffer;
|
|
m.ogg_length = length;
|
|
|
|
int error;
|
|
m.vorbis = stb_vorbis_open_memory(buffer, length, &error, nullptr);
|
|
if (!m.vorbis) {
|
|
log_msg(LOG_FAIL, "stb_vorbis_open_memory failed: %d\n", error);
|
|
return -1;
|
|
}
|
|
|
|
m.info = stb_vorbis_get_info(m.vorbis);
|
|
|
|
m.spec.channels = m.info.channels;
|
|
m.spec.freq = m.info.sample_rate;
|
|
m.spec.format = SDL_AUDIO_S16;
|
|
|
|
m.state = state::stopped;
|
|
|
|
return idx;
|
|
}
|
|
|
|
int load(const char* filename)
|
|
{
|
|
// [RZC 28/08/22] Carreguem primer el arxiu en memòria i després el descomprimim. Es algo més rapid.
|
|
FILE *f = fopen(filename, "rb");
|
|
fseek(f, 0, SEEK_END);
|
|
long fsize = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
Uint8 *buffer = (Uint8*)malloc(fsize + 1);
|
|
if (fread(buffer, fsize, 1, f)!=1) return -1;
|
|
fclose(f);
|
|
|
|
int music = load(buffer, fsize);
|
|
|
|
free(buffer);
|
|
|
|
return music;
|
|
}
|
|
|
|
void play(int mus, int loop)
|
|
{
|
|
stop();
|
|
|
|
current = mus;
|
|
auto& m = musics[mus];
|
|
|
|
m.times = loop;
|
|
m.state = state::playing;
|
|
|
|
stb_vorbis_seek_start(m.vorbis);
|
|
|
|
m.stream = SDL_CreateAudioStream(&m.spec, &audioSpec);
|
|
SDL_SetAudioStreamGain(m.stream, volume);
|
|
SDL_BindAudioStream(sdlAudioDevice, m.stream);
|
|
}
|
|
|
|
void pause()
|
|
{
|
|
if (!music::enabled || (current<0)) return;
|
|
if (current>static_cast<int>(musics.size())) {
|
|
log_msg(LOG_FAIL, "music::pause: Illegal music handle: %i\n", current);
|
|
return;
|
|
}
|
|
auto &m = musics[current];
|
|
if (m.state == state::invalid) {
|
|
log_msg(LOG_FAIL, "music::pause: Invalidated music handle: %i\n", current);
|
|
return;
|
|
}
|
|
|
|
m.state = state::paused;
|
|
//SDL_PauseAudioStreamDevice(current->stream);
|
|
SDL_UnbindAudioStream(m.stream);
|
|
}
|
|
|
|
void resume()
|
|
{
|
|
if (!music::enabled || (current<0)) return;
|
|
if (current>static_cast<int>(musics.size())) {
|
|
log_msg(LOG_FAIL, "music::resume: Illegal music handle: %i\n", current);
|
|
return;
|
|
}
|
|
auto &m = musics[current];
|
|
if (m.state == state::invalid) {
|
|
log_msg(LOG_FAIL, "music::resume: Invalidated music handle: %i\n", current);
|
|
return;
|
|
}
|
|
|
|
m.state = state::playing;
|
|
//SDL_ResumeAudioStreamDevice(current->stream);
|
|
SDL_BindAudioStream(sdlAudioDevice, m.stream);
|
|
}
|
|
|
|
void stop()
|
|
{
|
|
if (current < 0) return;
|
|
|
|
auto& m = musics[current];
|
|
|
|
m.state = state::stopped;
|
|
|
|
if (m.stream) SDL_DestroyAudioStream(m.stream);
|
|
m.stream = nullptr;
|
|
current = -1;
|
|
}
|
|
|
|
void fadeOut(int milliseconds)
|
|
{
|
|
if (!music::enabled || (current<0)) return;
|
|
if (current>static_cast<int>(musics.size())) {
|
|
log_msg(LOG_FAIL, "music::fadeOut: Illegal music handle: %i\n", current);
|
|
return;
|
|
}
|
|
auto &m = musics[current];
|
|
if (m.state == state::invalid) {
|
|
log_msg(LOG_FAIL, "music::fadeOut: Invalidated music handle: %i\n", current);
|
|
return;
|
|
}
|
|
|
|
fade::fading = true;
|
|
fade::start_time = SDL_GetTicks();
|
|
fade::duration = milliseconds;
|
|
fade::initial_volume = volume;
|
|
}
|
|
|
|
music::state getState()
|
|
{
|
|
if (!music::enabled) return music::state::disabled;
|
|
if (current<0 || current>static_cast<int>(musics.size())) return state::invalid;
|
|
return musics[current].state;
|
|
}
|
|
|
|
void destroy(int mus)
|
|
{
|
|
if (mus < 0 || mus >= (int)musics.size()) return;
|
|
|
|
auto& m = musics[mus];
|
|
|
|
if (m.stream) SDL_DestroyAudioStream(m.stream);
|
|
if (m.vorbis) stb_vorbis_close(m.vorbis);
|
|
|
|
m.stream = nullptr;
|
|
m.vorbis = nullptr;
|
|
|
|
m.state = state::invalid;
|
|
}
|
|
|
|
float setVolume(float vol)
|
|
{
|
|
volume = SDL_clamp( vol, 0.0f, 1.0f );
|
|
|
|
if (current<0 || current>static_cast<int>(musics.size())) return vol;
|
|
auto &m = musics[current];
|
|
if (m.state == state::invalid) return vol;
|
|
|
|
SDL_SetAudioStreamGain(m.stream, volume);
|
|
return volume;
|
|
}
|
|
|
|
void setPosition(float value)
|
|
{
|
|
auto& m = musics[current];
|
|
|
|
int sample = int(value * m.info.sample_rate);
|
|
|
|
if (stb_vorbis_seek(m.vorbis, sample)) {
|
|
SDL_ClearAudioStream(m.stream); // importante
|
|
}
|
|
}
|
|
|
|
float getPosition()
|
|
{
|
|
if (!music::enabled) return 0;
|
|
if (current<0 || current>static_cast<int>(musics.size())) {
|
|
//log_msg(LOG_FAIL, "music::getPosition: Illegal music handle: %i\n", current);
|
|
return 0;
|
|
}
|
|
auto &m = musics[current];
|
|
if (m.state == state::invalid) {
|
|
//log_msg(LOG_FAIL, "music::getPosition: Invalidated music handle: %i\n", current);
|
|
return 0;
|
|
}
|
|
int sample = stb_vorbis_get_sample_offset(m.vorbis);
|
|
return float(sample) / float(m.info.sample_rate);
|
|
}
|
|
|
|
float getDuration()
|
|
{
|
|
if (!music::enabled) return 0;
|
|
if (current < 0 || current > static_cast<int>(musics.size())) {
|
|
//log_msg(LOG_FAIL, "music::getDuration: Illegal music handle: %i\n", current);
|
|
return 0;
|
|
}
|
|
|
|
auto &m = musics[current];
|
|
if (m.state == state::invalid) {
|
|
//log_msg(LOG_FAIL, "music::getDuration: Invalidated music handle: %i\n", current);
|
|
return 0;
|
|
}
|
|
|
|
// Total de muestras del stream
|
|
int total_samples = stb_vorbis_stream_length_in_samples(m.vorbis);
|
|
|
|
// Convertir a segundos
|
|
return float(total_samples) / float(m.info.sample_rate);
|
|
}
|
|
|
|
void enable(bool value)
|
|
{
|
|
if (!value && music::enabled && current>=0 && current<static_cast<int>(musics.size()) && musics[current].state==state::playing) stop();
|
|
music::enabled = value;
|
|
}
|
|
|
|
bool isEnabled()
|
|
{
|
|
return enabled;
|
|
}
|
|
|
|
}
|
|
|
|
namespace sound
|
|
{
|
|
int create(uint8_t* buffer, uint32_t length)
|
|
{
|
|
int snd = 0;
|
|
while (snd < static_cast<int>(sounds.size()) && sounds[snd].buffer) { snd++; }
|
|
if (snd == static_cast<int>(sounds.size())) sounds.emplace_back();
|
|
|
|
auto &s = sounds[snd];
|
|
s.buffer = buffer;
|
|
s.length = length;
|
|
return snd;
|
|
}
|
|
|
|
int load(uint8_t* buffer, uint32_t size)
|
|
{
|
|
int snd = 0;
|
|
while (snd < static_cast<int>(sounds.size()) && sounds[snd].buffer) { snd++; }
|
|
if (snd == static_cast<int>(sounds.size())) sounds.emplace_back();
|
|
|
|
auto &s = sounds[snd];
|
|
SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size),1, &s.spec, &s.buffer, &s.length);
|
|
|
|
return snd;
|
|
}
|
|
|
|
int load(const char* filename)
|
|
{
|
|
int snd = 0;
|
|
while (snd < static_cast<int>(sounds.size()) && sounds[snd].buffer) { snd++; }
|
|
if (snd == static_cast<int>(sounds.size())) sounds.emplace_back();
|
|
|
|
auto &s = sounds[snd];
|
|
SDL_LoadWAV(filename, &s.spec, &s.buffer, &s.length);
|
|
|
|
return snd;
|
|
}
|
|
|
|
int play(int snd, int loop)
|
|
{
|
|
if (!sound::enabled) return -1;
|
|
|
|
if (snd<0 || snd>static_cast<int>(sounds.size())) {
|
|
log_msg(LOG_FAIL, "sound::play: Illegal sound handle: %i\n", snd);
|
|
return -1;
|
|
}
|
|
auto &s = sounds[snd];
|
|
if (!s.buffer) {
|
|
log_msg(LOG_FAIL, "sound::play: Invalid sound: %i\n", snd);
|
|
return -1;
|
|
}
|
|
|
|
int chan = 0;
|
|
while (chan < static_cast<int>(channel::channels.size()) && channel::channels[chan].state != channel::state::free) { chan++; }
|
|
if (chan == static_cast<int>(channel::channels.size())) channel::channels.emplace_back();
|
|
channel::stop(chan);
|
|
auto &c = channel::channels[chan];
|
|
c.sound = snd;
|
|
c.times = loop;
|
|
c.pos = 0;
|
|
c.state = channel::state::playing;
|
|
c.stream = SDL_CreateAudioStream(&s.spec, &audioSpec);
|
|
SDL_PutAudioStreamData(c.stream, s.buffer, s.length);
|
|
SDL_SetAudioStreamGain(c.stream, volume);
|
|
SDL_BindAudioStream(sdlAudioDevice, c.stream);
|
|
|
|
return chan;
|
|
}
|
|
|
|
int playOnChannel(int snd, int chan, int loop)
|
|
{
|
|
if (!sound::enabled) return -1;
|
|
|
|
if (snd<0 || snd>static_cast<int>(sounds.size())) {
|
|
log_msg(LOG_FAIL, "sound::playOnChannel: Illegal sound handle: %i\n", snd);
|
|
return -1;
|
|
}
|
|
auto &s = sounds[snd];
|
|
if (!s.buffer) {
|
|
log_msg(LOG_FAIL, "sound::playOnChannel: Invalid sound: %i\n", snd);
|
|
return -1;
|
|
}
|
|
|
|
if (chan<0 || chan>static_cast<int>(channel::channels.size())) {
|
|
log_msg(LOG_FAIL, "sound::playOnChannel: Illegal channel handle: %i\n", chan);
|
|
return -1;
|
|
}
|
|
|
|
channel::stop(chan);
|
|
|
|
auto &c = channel::channels[chan];
|
|
c.sound = snd;
|
|
c.times = loop;
|
|
c.pos = 0;
|
|
c.state = channel::state::playing;
|
|
c.stream = SDL_CreateAudioStream(&s.spec, &audioSpec);
|
|
SDL_PutAudioStreamData(c.stream, s.buffer, s.length);
|
|
SDL_SetAudioStreamGain(c.stream, volume);
|
|
SDL_BindAudioStream(sdlAudioDevice, c.stream);
|
|
|
|
return chan;
|
|
}
|
|
|
|
void destroy(int snd)
|
|
{
|
|
for (int i = 0; i < static_cast<int>(channel::channels.size()); i++) {
|
|
if (channel::channels[i].sound == snd) channel::stop(i);
|
|
}
|
|
|
|
if (snd<0 || snd>static_cast<int>(sounds.size())) {
|
|
log_msg(LOG_FAIL, "sound::destroy: Illegal sound handle: %i\n", snd);
|
|
return;
|
|
}
|
|
auto &s = sounds[snd];
|
|
SDL_free(s.buffer);
|
|
}
|
|
|
|
float setVolume(float vol)
|
|
{
|
|
volume = SDL_clamp( vol, 0.0f, 1.0f );
|
|
|
|
for (int i = 0; i < static_cast<int>(channel::channels.size()); i++) {
|
|
auto &c = channel::channels[i];
|
|
if ( (c.state == channel::state::playing) || (c.state == channel::state::paused) )
|
|
SDL_SetAudioStreamGain(c.stream, volume);
|
|
}
|
|
return volume;
|
|
}
|
|
|
|
void enable(bool value)
|
|
{
|
|
if (!value && sound::enabled) {
|
|
for (int i = 0; i < static_cast<int>(channel::channels.size()); i++) {
|
|
if (channel::channels[i].state == channel::state::playing) channel::stop(i);
|
|
}
|
|
}
|
|
enabled = value;
|
|
}
|
|
|
|
bool isEnabled()
|
|
{
|
|
return enabled;
|
|
}
|
|
|
|
namespace channel
|
|
{
|
|
void pause(const int chan)
|
|
{
|
|
if (!sound::enabled) return;
|
|
|
|
if (chan == -1)
|
|
{
|
|
for (int i = 0; i < static_cast<int>(channels.size()); i++)
|
|
if (channels[i].state == state::playing)
|
|
{
|
|
channels[i].state = state::paused;
|
|
//SDL_PauseAudioStreamDevice(channels[i].stream);
|
|
SDL_UnbindAudioStream(channels[i].stream);
|
|
}
|
|
}
|
|
else if (chan >= 0 && chan < static_cast<int>(channels.size()))
|
|
{
|
|
if (channels[chan].state == state::playing)
|
|
{
|
|
channels[chan].state = state::paused;
|
|
//SDL_PauseAudioStreamDevice(channels[channel].stream);
|
|
SDL_UnbindAudioStream(channels[chan].stream);
|
|
}
|
|
}
|
|
}
|
|
|
|
void resume(int chan)
|
|
{
|
|
if (!sound::enabled) return;
|
|
|
|
if (chan == -1)
|
|
{
|
|
for (int i = 0; i < static_cast<int>(channels.size()); i++)
|
|
if (channels[i].state == state::paused)
|
|
{
|
|
channels[i].state = state::playing;
|
|
//SDL_ResumeAudioStreamDevice(channels[i].stream);
|
|
SDL_BindAudioStream(sdlAudioDevice, channels[i].stream);
|
|
}
|
|
}
|
|
else if (chan >= 0 && chan < static_cast<int>(channels.size()))
|
|
{
|
|
if (channels[chan].state == state::paused)
|
|
{
|
|
channels[chan].state = state::playing;
|
|
//SDL_ResumeAudioStreamDevice(channels[channel].stream);
|
|
SDL_BindAudioStream(sdlAudioDevice, channels[chan].stream);
|
|
}
|
|
}
|
|
}
|
|
|
|
void stop(int chan)
|
|
{
|
|
if (!sound::enabled) return;
|
|
|
|
if (chan == -1)
|
|
{
|
|
for (int i = 0; i < static_cast<int>(channels.size()); i++) {
|
|
if (channels[i].state != state::free) SDL_DestroyAudioStream(channels[i].stream);
|
|
channels[i].stream = nullptr;
|
|
channels[i].state = state::free;
|
|
channels[i].pos = 0;
|
|
channels[i].sound = -1;
|
|
}
|
|
}
|
|
else if (chan >= 0 && chan < static_cast<int>(channels.size()))
|
|
{
|
|
if (channels[chan].state != state::free) SDL_DestroyAudioStream(channels[chan].stream);
|
|
channels[chan].stream = nullptr;
|
|
channels[chan].state = state::free;
|
|
channels[chan].pos = 0;
|
|
channels[chan].sound = -1;
|
|
}
|
|
}
|
|
|
|
channel::state getState(int chan)
|
|
{
|
|
if (!sound::enabled) return state::disabled;
|
|
if (chan < 0 || chan >= static_cast<int>(channels.size())) return state::invalid;
|
|
return channels[chan].state;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif |