diff --git a/CMakeLists.txt b/CMakeLists.txt index 2def215..bdd6c29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -135,6 +135,10 @@ set(MAIN_HEADERS src/gl-fun.h src/gl-meta.h src/vertex.h + src/soundemitter.h + src/aldatasource.h + src/alstream.h + src/audiostream.h ) set(MAIN_SOURCE @@ -166,6 +170,10 @@ set(MAIN_SOURCE src/gl-fun.cpp src/gl-meta.cpp src/vertex.cpp + src/soundemitter.cpp + src/sdlsoundsource.cpp + src/alstream.cpp + src/audiostream.cpp ) source_group("MKXP Source" FILES ${MAIN_SOURCE} ${MAIN_HEADERS}) @@ -191,6 +199,10 @@ set(EMBEDDED_INPUT ) if (RGSS2) + list(APPEND MAIN_SOURCE + src/vorbissource.cpp + ) + list(APPEND EMBEDDED_INPUT shader/blur.frag shader/blurH.vert diff --git a/mkxp.pro b/mkxp.pro index a438f47..41f1587 100644 --- a/mkxp.pro +++ b/mkxp.pro @@ -126,7 +126,11 @@ HEADERS += \ src/debugwriter.h \ src/gl-fun.h \ src/gl-meta.h \ - src/vertex.h + src/vertex.h \ + src/soundemitter.h \ + src/aldatasource.h \ + src/alstream.h \ + src/audiostream.h SOURCES += \ src/main.cpp \ @@ -156,7 +160,11 @@ SOURCES += \ src/sharedstate.cpp \ src/gl-fun.cpp \ src/gl-meta.cpp \ - src/vertex.cpp + src/vertex.cpp \ + src/soundemitter.cpp \ + src/sdlsoundsource.cpp \ + src/alstream.cpp \ + src/audiostream.cpp EMBED = \ shader/transSimple.frag \ @@ -176,6 +184,9 @@ EMBED = \ assets/liberation.ttf RGSS2 { + SOURCES += \ + src/vorbissource.cpp + EMBED += \ shader/blur.frag \ shader/blurH.vert \ diff --git a/src/al-util.h b/src/al-util.h index 036f18e..9735277 100644 --- a/src/al-util.h +++ b/src/al-util.h @@ -23,6 +23,8 @@ #define ALUTIL_H #include +#include +#include namespace AL { @@ -185,4 +187,51 @@ namespace Source } +inline uint8_t formatSampleSize(int sdlFormat) +{ + switch (sdlFormat) + { + case AUDIO_U8 : + case AUDIO_S8 : + return 1; + + case AUDIO_U16LSB : + case AUDIO_U16MSB : + case AUDIO_S16LSB : + case AUDIO_S16MSB : + return 2; + + default : + assert(!"Unhandled sample format"); + } + + return 0; +} + +inline ALenum chooseALFormat(int sampleSize, int channelCount) +{ + switch (sampleSize) + { + case 1 : + switch (channelCount) + { + case 1 : return AL_FORMAT_MONO8; + case 2 : return AL_FORMAT_STEREO8; + } + case 2 : + switch (channelCount) + { + case 1 : return AL_FORMAT_MONO16; + case 2 : return AL_FORMAT_STEREO16; + } + default : + assert(!"Unhandled sample size / channel count"); + } + + return 0; +} + +#define AUDIO_SLEEP 10 +#define STREAM_BUF_SIZE 32768 + #endif // ALUTIL_H diff --git a/src/aldatasource.h b/src/aldatasource.h new file mode 100644 index 0000000..4292595 --- /dev/null +++ b/src/aldatasource.h @@ -0,0 +1,64 @@ +/* +** aldatasource.h +** +** This file is part of mkxp. +** +** Copyright (C) 2014 Jonas Kulla +** +** mkxp is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 2 of the License, or +** (at your option) any later version. +** +** mkxp is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with mkxp. If not, see . +*/ + +#ifndef ALDATASOURCE_H +#define ALDATASOURCE_H + +#include "al-util.h" + +struct ALDataSource +{ + enum Status + { + NoError, + EndOfStream, + WrapAround, + Error + }; + + virtual ~ALDataSource() {} + + /* Read/process next chunk of data, and attach it + * to provided AL buffer */ + virtual Status fillBuffer(AL::Buffer::ID alBuffer) = 0; + + virtual int sampleRate() = 0; + + virtual void seekToOffset(float seconds) = 0; + + /* Seek back to start */ + virtual void reset() = 0; + + /* The frame count right after wrap around */ + virtual uint32_t loopStartFrames() = 0; +}; + +ALDataSource *createSDLSource(SDL_RWops &ops, + const char *extension, + uint32_t maxBufSize, + bool looped); + +#ifdef RGSS2 +ALDataSource *createVorbisSource(SDL_RWops &ops, + bool looped); +#endif + +#endif // ALDATASOURCE_H diff --git a/src/alstream.cpp b/src/alstream.cpp new file mode 100644 index 0000000..8819579 --- /dev/null +++ b/src/alstream.cpp @@ -0,0 +1,425 @@ +/* +** alstream.cpp +** +** This file is part of mkxp. +** +** Copyright (C) 2014 Jonas Kulla +** +** mkxp is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 2 of the License, or +** (at your option) any later version. +** +** mkxp is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with mkxp. If not, see . +*/ + +#include "alstream.h" + +#include "sharedstate.h" +#include "filesystem.h" +#include "aldatasource.h" + +#include +#include +#include + +ALStream::ALStream(LoopMode loopMode, + const std::string &threadId) + : looped(loopMode == Looped), + state(Closed), + source(0), + thread(0), + preemptPause(false), + streamInited(false), + needsRewind(false) +{ + alSrc = AL::Source::gen(); + + AL::Source::setVolume(alSrc, 1.0); + AL::Source::setPitch(alSrc, 1.0); + AL::Source::detachBuffer(alSrc); + + for (int i = 0; i < STREAM_BUFS; ++i) + alBuf[i] = AL::Buffer::gen(); + + pauseMut = SDL_CreateMutex(); + + threadName = std::string("al_stream (") + threadId + ")"; +} + +ALStream::~ALStream() +{ + close(); + + clearALQueue(); + + AL::Source::del(alSrc); + + for (int i = 0; i < STREAM_BUFS; ++i) + AL::Buffer::del(alBuf[i]); + + SDL_DestroyMutex(pauseMut); +} + +void ALStream::close() +{ + checkStopped(); + + switch (state) + { + case Playing: + case Paused: + stopStream(); + case Stopped: + closeSource(); + state = Closed; + case Closed: + return; + } +} + +void ALStream::open(const std::string &filename) +{ + checkStopped(); + + switch (state) + { + case Playing: + case Paused: + stopStream(); + case Stopped: + closeSource(); + case Closed: + openSource(filename); + } + + state = Stopped; +} + +void ALStream::stop() +{ + checkStopped(); + + switch (state) + { + case Closed: + case Stopped: + return; + case Playing: + case Paused: + stopStream(); + } + + state = Stopped; +} + +void ALStream::play(float offset) +{ + checkStopped(); + + switch (state) + { + case Closed: + case Playing: + return; + case Stopped: + startStream(offset); + break; + case Paused : + resumeStream(); + } + + state = Playing; +} + +void ALStream::pause() +{ + checkStopped(); + + switch (state) + { + case Closed: + case Stopped: + case Paused: + return; + case Playing: + pauseStream(); + } + + state = Paused; +} + +void ALStream::setVolume(float value) +{ + AL::Source::setVolume(alSrc, value); +} + +void ALStream::setPitch(float value) +{ + AL::Source::setPitch(alSrc, value); +} + +ALStream::State ALStream::queryState() +{ + checkStopped(); + + return state; +} + +float ALStream::queryOffset() +{ + if (state == Closed) + return 0; + + float procOffset = static_cast(procFrames) / source->sampleRate(); + + return procOffset + AL::Source::getSecOffset(alSrc); +} + +void ALStream::closeSource() +{ + delete source; +} + +void ALStream::openSource(const std::string &filename) +{ + const char *ext; + shState->fileSystem().openRead(srcOps, filename.c_str(), FileSystem::Audio, false, &ext); + +#ifdef RGSS2 + /* Try to read ogg file signature */ + char sig[5]; + memset(sig, '\0', sizeof(sig)); + SDL_RWread(&srcOps, sig, 1, 4); + SDL_RWseek(&srcOps, 0, RW_SEEK_SET); + + if (!strcmp(sig, "OggS")) + source = createVorbisSource(srcOps, looped); + else + source = createSDLSource(srcOps, ext, STREAM_BUF_SIZE, looped); +#else + source = createSDLSource(srcOps, ext, STREAM_BUF_SIZE, looped); +#endif + + needsRewind = false; +} + +void ALStream::stopStream() +{ + threadTermReq = true; + + AL::Source::stop(alSrc); + + if (thread) + { + SDL_WaitThread(thread, 0); + thread = 0; + needsRewind = true; + } + + procFrames = 0; +} + +void ALStream::startStream(float offset) +{ + clearALQueue(); + + preemptPause = false; + streamInited = false; + sourceExhausted = false; + threadTermReq = false; + + startOffset = offset; + procFrames = offset * source->sampleRate(); + + thread = SDL_CreateThread(streamDataFun, threadName.c_str(), this); +} + +void ALStream::pauseStream() +{ + SDL_LockMutex(pauseMut); + + if (AL::Source::getState(alSrc) != AL_PLAYING) + preemptPause = true; + else + AL::Source::pause(alSrc); + + SDL_UnlockMutex(pauseMut); +} + +void ALStream::resumeStream() +{ + SDL_LockMutex(pauseMut); + + if (preemptPause) + preemptPause = false; + else + AL::Source::play(alSrc); + + SDL_UnlockMutex(pauseMut); +} + +void ALStream::checkStopped() +{ + /* This only concerns the scenario where + * state is still 'Playing', but the stream + * has already ended on its own (EOF, Error) */ + if (state != Playing) + return; + + /* If streaming thread hasn't queued up + * buffers yet there's not point in querying + * the AL source */ + if (!streamInited) + return; + + /* If alSrc isn't playing, but we haven't + * exhausted the data source yet, we're just + * having a buffer underrun */ + if (!sourceExhausted) + return; + + if (AL::Source::getState(alSrc) == AL_PLAYING) + return; + + stopStream(); + state = Stopped; +} + +void ALStream::clearALQueue() +{ + /* Unqueue all buffers */ + ALint queuedBufs = AL::Source::getProcBufferCount(alSrc); + + while (queuedBufs--) + AL::Source::unqueueBuffer(alSrc); +} + +/* thread func */ +void ALStream::streamData() +{ + /* Fill up queue */ + bool firstBuffer = true; + ALDataSource::Status status; + + if (needsRewind) + { + if (startOffset > 0) + source->seekToOffset(startOffset); + else + source->reset(); + } + + for (int i = 0; i < STREAM_BUFS; ++i) + { + AL::Buffer::ID buf = alBuf[i]; + + status = source->fillBuffer(buf); + + if (status == ALDataSource::Error) + return; + + AL::Source::queueBuffer(alSrc, buf); + + if (firstBuffer) + { + resumeStream(); + + firstBuffer = false; + streamInited = true; + } + + if (threadTermReq) + return; + + if (status == ALDataSource::EndOfStream) + { + sourceExhausted = true; + break; + } + } + + /* Wait for buffers to be consumed, then + * refill and queue them up again */ + while (true) + { + ALint procBufs = AL::Source::getProcBufferCount(alSrc); + + while (procBufs--) + { + if (threadTermReq) + break; + + AL::Buffer::ID buf = AL::Source::unqueueBuffer(alSrc); + + /* If something went wrong, try again later */ + if (buf == AL::Buffer::ID(0)) + break; + + if (buf == lastBuf) + { + /* Reset the processed sample count so + * querying the playback offset returns 0.0 again */ + procFrames = source->loopStartFrames(); + lastBuf = AL::Buffer::ID(0); + } + else + { + /* Add the frame count contained in this + * buffer to the total count */ + ALint bits = AL::Buffer::getBits(buf); + ALint size = AL::Buffer::getSize(buf); + ALint chan = AL::Buffer::getChannels(buf); + + if (bits != 0 && chan != 0) + procFrames += ((size / (bits / 8)) / chan); + } + + if (sourceExhausted) + continue; + + status = source->fillBuffer(buf); + + if (status == ALDataSource::Error) + { + sourceExhausted = true; + return; + } + + AL::Source::queueBuffer(alSrc, buf); + + /* In case of buffer underrun, + * start playing again */ + if (AL::Source::getState(alSrc) == AL_STOPPED) + AL::Source::play(alSrc); + + /* If this was the last buffer before the data + * source loop wrapped around again, mark it as + * such so we can catch it and reset the processed + * sample count once it gets unqueued */ + if (status == ALDataSource::WrapAround) + lastBuf = buf; + + if (status == ALDataSource::EndOfStream) + sourceExhausted = true; + } + + if (threadTermReq) + break; + + SDL_Delay(AUDIO_SLEEP); + } +} + +int ALStream::streamDataFun(void *_self) +{ + ALStream &self = *static_cast(_self); + self.streamData(); + return 0; +} diff --git a/src/alstream.h b/src/alstream.h new file mode 100644 index 0000000..537733b --- /dev/null +++ b/src/alstream.h @@ -0,0 +1,122 @@ +/* +** alstream.h +** +** This file is part of mkxp. +** +** Copyright (C) 2014 Jonas Kulla +** +** mkxp is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 2 of the License, or +** (at your option) any later version. +** +** mkxp is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with mkxp. If not, see . +*/ + +#ifndef ALSTREAM_H +#define ALSTREAM_H + +#include "al-util.h" + +#include +#include + +struct SDL_mutex; +struct SDL_thread; +struct ALDataSource; + +#define STREAM_BUFS 3 + +/* State-machine like audio playback stream. + * This class is NOT thread safe */ +struct ALStream +{ + enum State + { + Closed, + Stopped, + Playing, + Paused + }; + + bool looped; + State state; + + ALDataSource *source; + SDL_Thread *thread; + + std::string threadName; + + SDL_mutex *pauseMut; + bool preemptPause; + + /* When this flag isn't set and alSrc is + * in 'STOPPED' state, stream isn't over + * (it just hasn't started yet) */ + bool streamInited; + bool sourceExhausted; + + bool threadTermReq; + + bool needsRewind; + float startOffset; + + AL::Source::ID alSrc; + AL::Buffer::ID alBuf[STREAM_BUFS]; + + uint64_t procFrames; + AL::Buffer::ID lastBuf; + + SDL_RWops srcOps; + + struct + { + ALenum format; + ALsizei freq; + } stream; + + enum LoopMode + { + Looped, + NotLooped + }; + + ALStream(LoopMode loopMode, + const std::string &threadId); + ~ALStream(); + + void close(); + void open(const std::string &filename); + void stop(); + void play(float offset = 0); + void pause(); + + void setVolume(float value); + void setPitch(float value); + State queryState(); + float queryOffset(); + +private: + void closeSource(); + void openSource(const std::string &filename); + + void stopStream(); + void startStream(float offset); + void pauseStream(); + void resumeStream(); + + void checkStopped(); + void clearALQueue(); + + /* thread func */ + void streamData(); + static int streamDataFun(void *); +}; + +#endif // ALSTREAM_H diff --git a/src/audio.cpp b/src/audio.cpp index a2c21f1..c307fd2 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -21,1473 +21,13 @@ #include "audio.h" -#include "sharedstate.h" -#include "util.h" -#include "intrulist.h" -#include "filesystem.h" -#include "exception.h" -#include "al-util.h" -#include "boost-hash.h" -#include "debugwriter.h" +#include "audiostream.h" +#include "soundemitter.h" -#include #include -#include -#include #include -#include #include -#include - -#ifdef RGSS2 -#define OV_EXCLUDE_STATIC_CALLBACKS -#include -#endif - -#include - -#define AUDIO_SLEEP 10 -#define SE_SOURCES 6 -#define SE_CACHE_MEM (10*1024*1024) // 10 MB - -static uint8_t formatSampleSize(int sdlFormat) -{ - switch (sdlFormat) - { - case AUDIO_U8 : - case AUDIO_S8 : - return 1; - - case AUDIO_U16LSB : - case AUDIO_U16MSB : - case AUDIO_S16LSB : - case AUDIO_S16MSB : - return 2; - - default: - Debug() << "Unhandled sample format"; - abort(); - } - - return 0; -} - -static ALenum chooseALFormat(int sampleSize, int channelCount) -{ - switch (sampleSize) - { - case 1 : - switch (channelCount) - { - case 1 : return AL_FORMAT_MONO8; - case 2 : return AL_FORMAT_STEREO8; - default: abort(); - } - case 2 : - switch (channelCount) - { - case 1 : return AL_FORMAT_MONO16; - case 2 : return AL_FORMAT_STEREO16; - default : abort(); - } - default : abort(); - } - - return 0; -} - -static const int streamBufSize = 32768; - -struct SoundBuffer -{ - /* Uniquely identifies this or equal buffer */ - std::string key; - - AL::Buffer::ID alBuffer; - - /* Link into the buffer cache priority list */ - IntruListLink link; - - /* Buffer byte count */ - uint32_t bytes; - - /* Reference count */ - uint8_t refCount; - - SoundBuffer() - : link(this), - refCount(1) - - { - alBuffer = AL::Buffer::gen(); - } - - static SoundBuffer *ref(SoundBuffer *buffer) - { - ++buffer->refCount; - - return buffer; - } - - static void deref(SoundBuffer *buffer) - { - if (--buffer->refCount == 0) - delete buffer; - } - -private: - ~SoundBuffer() - { - AL::Buffer::del(alBuffer); - } -}; - -/* Before: [a][b][c][d], After (index=1): [a][c][d][b] */ -static void -arrayPushBack(size_t array[], size_t size, size_t index) -{ - size_t v = array[index]; - - for (size_t t = index; t < size-1; ++t) - array[t] = array[t+1]; - - array[size-1] = v; -} - -struct SoundEmitter -{ - typedef BoostHash BufferHash; - - IntruList buffers; - BufferHash bufferHash; - - /* Byte count sum of all cached / playing buffers */ - uint32_t bufferBytes; - - AL::Source::ID alSrcs[SE_SOURCES]; - SoundBuffer *atchBufs[SE_SOURCES]; - - /* Indices of sources, sorted by priority (lowest first) */ - size_t srcPrio[SE_SOURCES]; - - SoundEmitter() - : bufferBytes(0) - { - for (int i = 0; i < SE_SOURCES; ++i) - { - alSrcs[i] = AL::Source::gen(); - atchBufs[i] = 0; - srcPrio[i] = i; - } - } - - ~SoundEmitter() - { - for (int i = 0; i < SE_SOURCES; ++i) - { - AL::Source::stop(alSrcs[i]); - AL::Source::del(alSrcs[i]); - - if (atchBufs[i]) - SoundBuffer::deref(atchBufs[i]); - } - - BufferHash::const_iterator iter; - for (iter = bufferHash.cbegin(); iter != bufferHash.cend(); ++iter) - SoundBuffer::deref(iter->second); - } - - void play(const std::string &filename, - int volume, - int pitch) - { - float _volume = clamp(volume, 0, 100) / 100.f; - float _pitch = clamp(pitch, 50, 150) / 100.f; - - SoundBuffer *buffer = allocateBuffer(filename); - - /* Try to find first free source */ - size_t i; - for (i = 0; i < SE_SOURCES; ++i) - if (AL::Source::getState(alSrcs[srcPrio[i]]) != AL_PLAYING) - break; - - /* If we didn't find any, overtake the one with lowest priority */ - if (i == SE_SOURCES) - i = 0; - - /* Push the used source to the back of the priority list */ - size_t srcIndex = srcPrio[i]; - arrayPushBack(srcPrio, SE_SOURCES, i); - - AL::Source::ID src = alSrcs[srcIndex]; - AL::Source::stop(src); - AL::Source::detachBuffer(src); - - SoundBuffer *old = atchBufs[srcIndex]; - - if (old) - SoundBuffer::deref(old); - - atchBufs[srcIndex] = SoundBuffer::ref(buffer); - - AL::Source::attachBuffer(src, buffer->alBuffer); - - AL::Source::setVolume(src, _volume); - AL::Source::setPitch(src, _pitch); - - AL::Source::play(src); - } - - void stop() - { - for (int i = 0; i < SE_SOURCES; i++) - AL::Source::stop(alSrcs[i]); - } - -private: - SoundBuffer *allocateBuffer(const std::string &filename) - { - SoundBuffer *buffer = bufferHash.value(filename, 0); - - if (buffer) - { - /* Buffer still in cashe. - * Move to front of priority list */ - buffers.remove(buffer->link); - buffers.append(buffer->link); - - return buffer; - } - else - { - /* Buffer not in cashe, needs to be loaded */ - SDL_RWops dataSource; - const char *extension; - - shState->fileSystem().openRead(dataSource, filename.c_str(), - FileSystem::Audio, false, &extension); - - Sound_Sample *sampleHandle = Sound_NewSample(&dataSource, extension, 0, streamBufSize); - - if (!sampleHandle) - { - SDL_RWclose(&dataSource); - throw Exception(Exception::SDLError, "SDL_sound: %s", Sound_GetError()); - } - - uint32_t decBytes = Sound_DecodeAll(sampleHandle); - uint8_t sampleSize = formatSampleSize(sampleHandle->actual.format); - uint32_t sampleCount = decBytes / sampleSize; - - buffer = new SoundBuffer; - buffer->key = filename; - buffer->bytes = sampleSize * sampleCount; - - ALenum alFormat = chooseALFormat(sampleSize, sampleHandle->actual.channels); - - AL::Buffer::uploadData(buffer->alBuffer, alFormat, sampleHandle->buffer, - buffer->bytes, sampleHandle->actual.rate); - - Sound_FreeSample(sampleHandle); - - uint32_t wouldBeBytes = bufferBytes + buffer->bytes; - - /* If memory limit is reached, delete lowest priority buffer - * until there is room or no buffers left */ - while (wouldBeBytes > SE_CACHE_MEM && !buffers.isEmpty()) - { - SoundBuffer *last = buffers.tail(); - bufferHash.erase(last->key); - buffers.remove(last->link); - - wouldBeBytes -= last->bytes; - - SoundBuffer::deref(last); - } - - bufferHash.insert(filename, buffer); - buffers.prepend(buffer->link); - - bufferBytes = wouldBeBytes; - - return buffer; - } - } -}; - -static const int streamBufs = 3; - -struct ALDataSource -{ - enum Status - { - NoError, - EndOfStream, - WrapAround, - Error - }; - - virtual ~ALDataSource() {} - - /* Read/process next chunk of data, and attach it - * to provided AL buffer */ - virtual Status fillBuffer(AL::Buffer::ID alBuffer) = 0; - - virtual int sampleRate() = 0; - - virtual void seekToOffset(float seconds) = 0; - - /* Seek back to start */ - virtual void reset() = 0; - - /* The frame count right after wrap around */ - virtual uint32_t loopStartFrames() = 0; -}; - -struct SDLSoundSource : ALDataSource -{ - Sound_Sample *sample; - SDL_RWops &srcOps; - uint8_t sampleSize; - bool looped; - - ALenum alFormat; - ALsizei alFreq; - - SDLSoundSource(SDL_RWops &ops, - const char *extension, - uint32_t maxBufSize, - bool looped) - : srcOps(ops), - looped(looped) - { - sample = Sound_NewSample(&srcOps, extension, 0, maxBufSize); - - if (!sample) - { - SDL_RWclose(&ops); - throw Exception(Exception::SDLError, "SDL_sound: %s", Sound_GetError()); - } - - sampleSize = formatSampleSize(sample->actual.format); - - alFormat = chooseALFormat(sampleSize, sample->actual.channels); - alFreq = sample->actual.rate; - } - - ~SDLSoundSource() - { - /* This also closes 'srcOps' */ - Sound_FreeSample(sample); - } - - Status fillBuffer(AL::Buffer::ID alBuffer) - { - uint32_t decoded = Sound_Decode(sample); - - if (sample->flags & SOUND_SAMPLEFLAG_EAGAIN) - { - /* Try to decode one more time on EAGAIN */ - decoded = Sound_Decode(sample); - - /* Give up */ - if (sample->flags & SOUND_SAMPLEFLAG_EAGAIN) - return ALDataSource::Error; - } - - if (sample->flags & SOUND_SAMPLEFLAG_ERROR) - return ALDataSource::Error; - - AL::Buffer::uploadData(alBuffer, alFormat, sample->buffer, decoded, alFreq); - - if (sample->flags & SOUND_SAMPLEFLAG_EOF) - { - if (looped) - { - Sound_Rewind(sample); - return ALDataSource::WrapAround; - } - else - { - return ALDataSource::EndOfStream; - } - } - - return ALDataSource::NoError; - } - - int sampleRate() - { - return sample->actual.rate; - } - - void seekToOffset(float seconds) - { - Sound_Seek(sample, static_cast(seconds * 1000)); - } - - void reset() - { - Sound_Rewind(sample); - } - - uint32_t loopStartFrames() - { - /* Loops from the beginning of the file */ - return 0; - } -}; - -#ifdef RGSS2 -static size_t vfRead(void *ptr, size_t size, size_t nmemb, void *ops) -{ - return SDL_RWread(static_cast(ops), ptr, size, nmemb); -} - -static int vfSeek(void *ops, ogg_int64_t offset, int whence) -{ - return SDL_RWseek(static_cast(ops), offset, whence); -} - -static long vfTell(void *ops) -{ - return SDL_RWtell(static_cast(ops)); -} - -static ov_callbacks OvCallbacks = -{ - vfRead, - vfSeek, - 0, - vfTell -}; - - -struct VorbisSource : ALDataSource -{ - SDL_RWops &src; - - OggVorbis_File vf; - - uint32_t currentFrame; - - struct - { - uint32_t start; - uint32_t length; - uint32_t end; - bool valid; - bool requested; - } loop; - - struct - { - int channels; - int rate; - int frameSize; - ALenum alFormat; - } info; - - std::vector sampleBuf; - - VorbisSource(SDL_RWops &ops, - bool looped) - : src(ops), - currentFrame(0) - { - int error = ov_open_callbacks(&src, &vf, 0, 0, OvCallbacks); - - if (error) - { - SDL_RWclose(&src); - throw Exception(Exception::MKXPError, - "Vorbisfile: Cannot read ogg file"); - } - - /* Extract bitstream info */ - info.channels = vf.vi->channels; - info.rate = vf.vi->rate; - - if (info.channels > 2) - { - ov_clear(&vf); - SDL_RWclose(&src); - throw Exception(Exception::MKXPError, - "Cannot handle audio with more than 2 channels"); - } - - info.alFormat = chooseALFormat(sizeof(int16_t), info.channels); - info.frameSize = sizeof(int16_t) * info.channels; - - sampleBuf.resize(streamBufSize); - - loop.requested = looped; - loop.valid = false; - loop.start = loop.length = 0; - - if (!loop.requested) - return; - - /* Try to extract loop info */ - for (int i = 0; i < vf.vc->comments; ++i) - { - char *comment = vf.vc->user_comments[i]; - char *sep = strstr(comment, "="); - - /* No '=' found */ - if (!sep) - continue; - - /* Empty value */ - if (!*(sep+1)) - continue; - - *sep = '\0'; - - if (!strcmp(comment, "LOOPSTART")) - loop.start = strtol(sep+1, 0, 10); - - if (!strcmp(comment, "LOOPLENGTH")) - loop.length = strtol(sep+1, 0, 10); - - *sep = '='; - } - - loop.end = loop.start + loop.length; - loop.valid = (loop.start && loop.length); - } - - ~VorbisSource() - { - ov_clear(&vf); - SDL_RWclose(&src); - } - - int sampleRate() - { - return info.rate; - } - - void seekToOffset(float seconds) - { - currentFrame = seconds * info.rate; - - if (loop.valid && currentFrame > loop.end) - currentFrame = loop.start; - - /* If seeking fails, just seek back to start */ - if (ov_pcm_seek(&vf, currentFrame) != 0) - ov_raw_seek(&vf, 0); - } - - Status fillBuffer(AL::Buffer::ID alBuffer) - { - void *bufPtr = sampleBuf.data(); - int availBuf = sampleBuf.size(); - int bufUsed = 0; - - int canRead = availBuf; - - Status retStatus = ALDataSource::NoError; - - bool readAgain = false; - - if (loop.valid) - { - int tilLoopEnd = loop.end * info.frameSize; - - canRead = std::min(availBuf, tilLoopEnd); - } - - while (canRead > 16) - { - long res = ov_read(&vf, static_cast(bufPtr), - canRead, 0, sizeof(int16_t), 1, 0); - - if (res < 0) - { - /* Read error */ - retStatus = ALDataSource::Error; - - break; - } - - if (res == 0) - { - /* EOF */ - if (loop.requested) - { - retStatus = ALDataSource::WrapAround; - reset(); - } - else - { - retStatus = ALDataSource::EndOfStream; - } - - /* If we sought right to the end of the file, - * we might be EOF without actually having read - * any data at all yet (which mustn't happen), - * so we try to continue reading some data. */ - if (bufUsed > 0) - break; - - if (readAgain) - { - /* We're still not getting data though. - * Just error out to prevent an endless loop */ - retStatus = ALDataSource::Error; - break; - } - - readAgain = true; - } - - bufUsed += (res / sizeof(int16_t)); - bufPtr = &sampleBuf[bufUsed]; - currentFrame += (res / info.frameSize); - - if (loop.valid && currentFrame >= loop.end) - { - /* Determine how many frames we're - * over the loop end */ - int discardFrames = currentFrame - loop.end; - bufUsed -= discardFrames * info.channels; - - retStatus = ALDataSource::WrapAround; - - /* Seek to loop start */ - currentFrame = loop.start; - if (ov_pcm_seek(&vf, currentFrame) != 0) - retStatus = ALDataSource::Error; - - break; - } - - canRead -= res; - } - - if (retStatus != ALDataSource::Error) - AL::Buffer::uploadData(alBuffer, info.alFormat, sampleBuf.data(), - bufUsed*sizeof(int16_t), info.rate); - - return retStatus; - } - - void reset() - { - ov_raw_seek(&vf, 0); - currentFrame = 0; - } - - uint32_t loopStartFrames() - { - if (loop.valid) - return loop.start; - else - return 0; - } -}; -#endif - -/* State-machine like audio playback stream. - * This class is NOT thread safe */ -struct ALStream -{ - enum State - { - Closed, - Stopped, - Playing, - Paused - }; - - bool looped; - State state; - - ALDataSource *source; - SDL_Thread *thread; - - std::string threadName; - - SDL_mutex *pauseMut; - bool preemptPause; - - /* When this flag isn't set and alSrc is - * in 'STOPPED' state, stream isn't over - * (it just hasn't started yet) */ - bool streamInited; - bool sourceExhausted; - - bool threadTermReq; - - bool needsRewind; - float startOffset; - - AL::Source::ID alSrc; - AL::Buffer::ID alBuf[streamBufs]; - - uint64_t procFrames; - AL::Buffer::ID lastBuf; - - SDL_RWops srcOps; - - struct - { - ALenum format; - ALsizei freq; - } stream; - - enum LoopMode - { - Looped, - NotLooped - }; - - ALStream(LoopMode loopMode, - const std::string &threadId) - : looped(loopMode == Looped), - state(Closed), - source(0), - thread(0), - preemptPause(false), - streamInited(false), - needsRewind(false) - { - alSrc = AL::Source::gen(); - - AL::Source::setVolume(alSrc, 1.0); - AL::Source::setPitch(alSrc, 1.0); - AL::Source::detachBuffer(alSrc); - - for (int i = 0; i < streamBufs; ++i) - alBuf[i] = AL::Buffer::gen(); - - pauseMut = SDL_CreateMutex(); - - threadName = std::string("al_stream (") + threadId + ")"; - } - - ~ALStream() - { - close(); - - clearALQueue(); - - AL::Source::del(alSrc); - - for (int i = 0; i < streamBufs; ++i) - AL::Buffer::del(alBuf[i]); - - SDL_DestroyMutex(pauseMut); - } - - void close() - { - checkStopped(); - - switch (state) - { - case Playing: - case Paused: - stopStream(); - case Stopped: - closeSource(); - state = Closed; - case Closed: - return; - } - } - - void open(const std::string &filename) - { - checkStopped(); - - switch (state) - { - case Playing: - case Paused: - stopStream(); - case Stopped: - closeSource(); - case Closed: - openSource(filename); - } - - state = Stopped; - } - - void stop() - { - checkStopped(); - - switch (state) - { - case Closed: - case Stopped: - return; - case Playing: - case Paused: - stopStream(); - } - - state = Stopped; - } - - void play(float offset = 0) - { - checkStopped(); - - switch (state) - { - case Closed: - case Playing: - return; - case Stopped: - startStream(offset); - break; - case Paused : - resumeStream(); - } - - state = Playing; - } - - void pause() - { - checkStopped(); - - switch (state) - { - case Closed: - case Stopped: - case Paused: - return; - case Playing: - pauseStream(); - } - - state = Paused; - } - - void setVolume(float value) - { - AL::Source::setVolume(alSrc, value); - } - - void setPitch(float value) - { - AL::Source::setPitch(alSrc, value); - } - - State queryState() - { - checkStopped(); - - return state; - } - - float queryOffset() - { - if (state == Closed) - return 0; - - float procOffset = static_cast(procFrames) / source->sampleRate(); - - return procOffset + AL::Source::getSecOffset(alSrc); - } - -private: - void closeSource() - { - delete source; - } - - void openSource(const std::string &filename) - { - const char *ext; - shState->fileSystem().openRead(srcOps, filename.c_str(), FileSystem::Audio, false, &ext); - -#ifdef RGSS2 - /* Try to read ogg file signature */ - char sig[5]; - memset(sig, '\0', sizeof(sig)); - SDL_RWread(&srcOps, sig, 1, 4); - SDL_RWseek(&srcOps, 0, RW_SEEK_SET); - - if (!strcmp(sig, "OggS")) - source = new VorbisSource(srcOps, looped); - else - source = new SDLSoundSource(srcOps, ext, streamBufSize, looped); -#else - source = new SDLSoundSource(srcOps, ext, streamBufSize, looped); -#endif - - needsRewind = false; - } - - void stopStream() - { - threadTermReq = true; - - AL::Source::stop(alSrc); - - if (thread) - { - SDL_WaitThread(thread, 0); - thread = 0; - needsRewind = true; - } - - procFrames = 0; - } - - void startStream(float offset) - { - clearALQueue(); - - preemptPause = false; - streamInited = false; - sourceExhausted = false; - threadTermReq = false; - - startOffset = offset; - procFrames = offset * source->sampleRate(); - - thread = SDL_CreateThread(streamDataFun, threadName.c_str(), this); - } - - void pauseStream() - { - SDL_LockMutex(pauseMut); - - if (AL::Source::getState(alSrc) != AL_PLAYING) - preemptPause = true; - else - AL::Source::pause(alSrc); - - SDL_UnlockMutex(pauseMut); - } - - void resumeStream() - { - SDL_LockMutex(pauseMut); - - if (preemptPause) - preemptPause = false; - else - AL::Source::play(alSrc); - - SDL_UnlockMutex(pauseMut); - } - - void checkStopped() - { - /* This only concerns the scenario where - * state is still 'Playing', but the stream - * has already ended on its own (EOF, Error) */ - if (state != Playing) - return; - - /* If streaming thread hasn't queued up - * buffers yet there's not point in querying - * the AL source */ - if (!streamInited) - return; - - /* If alSrc isn't playing, but we haven't - * exhausted the data source yet, we're just - * having a buffer underrun */ - if (!sourceExhausted) - return; - - if (AL::Source::getState(alSrc) == AL_PLAYING) - return; - - stopStream(); - state = Stopped; - } - - void clearALQueue() - { - /* Unqueue all buffers */ - ALint queuedBufs = AL::Source::getProcBufferCount(alSrc); - - while (queuedBufs--) - AL::Source::unqueueBuffer(alSrc); - } - - /* thread func */ - void streamData() - { - /* Fill up queue */ - bool firstBuffer = true; - ALDataSource::Status status; - - if (needsRewind) - { - if (startOffset > 0) - source->seekToOffset(startOffset); - else - source->reset(); - } - - for (int i = 0; i < streamBufs; ++i) - { - AL::Buffer::ID buf = alBuf[i]; - - status = source->fillBuffer(buf); - - if (status == ALDataSource::Error) - return; - - AL::Source::queueBuffer(alSrc, buf); - - if (firstBuffer) - { - resumeStream(); - - firstBuffer = false; - streamInited = true; - } - - if (threadTermReq) - return; - - if (status == ALDataSource::EndOfStream) - { - sourceExhausted = true; - break; - } - } - - /* Wait for buffers to be consumed, then - * refill and queue them up again */ - while (true) - { - ALint procBufs = AL::Source::getProcBufferCount(alSrc); - - while (procBufs--) - { - if (threadTermReq) - break; - - AL::Buffer::ID buf = AL::Source::unqueueBuffer(alSrc); - - /* If something went wrong, try again later */ - if (buf == AL::Buffer::ID(0)) - break; - - if (buf == lastBuf) - { - /* Reset the processed sample count so - * querying the playback offset returns 0.0 again */ - procFrames = source->loopStartFrames(); - lastBuf = AL::Buffer::ID(0); - } - else - { - /* Add the frame count contained in this - * buffer to the total count */ - ALint bits = AL::Buffer::getBits(buf); - ALint size = AL::Buffer::getSize(buf); - ALint chan = AL::Buffer::getChannels(buf); - - if (bits != 0 && chan != 0) - procFrames += ((size / (bits / 8)) / chan); - } - - if (sourceExhausted) - continue; - - status = source->fillBuffer(buf); - - if (status == ALDataSource::Error) - { - sourceExhausted = true; - return; - } - - AL::Source::queueBuffer(alSrc, buf); - - /* In case of buffer underrun, - * start playing again */ - if (AL::Source::getState(alSrc) == AL_STOPPED) - AL::Source::play(alSrc); - - /* If this was the last buffer before the data - * source loop wrapped around again, mark it as - * such so we can catch it and reset the processed - * sample count once it gets unqueued */ - if (status == ALDataSource::WrapAround) - lastBuf = buf; - - if (status == ALDataSource::EndOfStream) - sourceExhausted = true; - } - - if (threadTermReq) - break; - - SDL_Delay(AUDIO_SLEEP); - } - } - - static int streamDataFun(void *_self) - { - ALStream &self = *static_cast(_self); - self.streamData(); - return 0; - } -}; - -struct AudioStream -{ - struct - { - std::string filename; - float volume; - float pitch; - } current; - - /* Volume set with 'play()' */ - float baseVolume; - - /* Volume set by external threads, - * such as for fade-in/out. - * Multiplied with intVolume for final - * playback volume. - * fadeVolume: used by fade-out thread. - * extVolume: used by MeWatch. */ - float fadeVolume; - float extVolume; - - /* Note that 'extPaused' and 'noResumeStop' are - * effectively only used with the AudioStream - * instance representing the BGM */ - - /* Flag indicating that the MeWatch paused this - * (BGM) stream because a ME started playing. - * While this flag is set, calls to 'play()' - * might open another file, but will not start - * the playback stream (the MeWatch will start - * it as soon as the ME finished playing). */ - bool extPaused; - - /* Flag indicating that this stream shouldn't be - * started by the MeWatch when it is in stopped - * state (eg. because the BGM stream was explicitly - * stopped by the user script while the ME was playing. - * When a new BGM is started (via 'play()') while an ME - * is playing, the file will be loaded without starting - * the stream, but we want the MeWatch to start it as - * soon as the ME ends, so we unset this flag. */ - bool noResumeStop; - - ALStream stream; - SDL_mutex *streamMut; - - struct - { - /* Fade is in progress */ - bool active; - - /* Request fade thread to finish and - * cleanup (like it normally would) */ - bool reqFini; - - /* Request fade thread to terminate - * immediately */ - bool reqTerm; - - SDL_Thread *thread; - std::string threadName; - - /* Amount of reduced absolute volume - * per ms of fade time */ - float msStep; - - /* Ticks at start of fade */ - uint32_t startTicks; - } fade; - - AudioStream(ALStream::LoopMode loopMode, - const std::string &threadId) - : baseVolume(1.0), - fadeVolume(1.0), - extVolume(1.0), - extPaused(false), - noResumeStop(false), - stream(loopMode, threadId) - { - current.volume = 1.0; - current.pitch = 1.0; - - fade.active = false; - fade.thread = 0; - fade.threadName = std::string("audio_fade (") + threadId + ")"; - - streamMut = SDL_CreateMutex(); - } - - ~AudioStream() - { - if (fade.thread) - { - fade.reqTerm = true; - SDL_WaitThread(fade.thread, 0); - } - - lockStream(); - - stream.stop(); - stream.close(); - - unlockStream(); - - SDL_DestroyMutex(streamMut); - } - - void play(const std::string &filename, - int volume, - int pitch, - float offset = 0) - { - finiFadeInt(); - - lockStream(); - - float _volume = clamp(volume, 0, 100) / 100.f; - float _pitch = clamp(pitch, 50, 150) / 100.f; - - ALStream::State sState = stream.queryState(); - - /* If all parameters match the current ones and we're - * still playing, there's nothing to do */ - if (filename == current.filename - && _volume == current.volume - && _pitch == current.pitch - && (sState == ALStream::Playing || sState == ALStream::Paused)) - { - unlockStream(); - return; - } - - /* If all parameters except volume match the current ones, - * we update the volume and continue streaming */ - if (filename == current.filename - && _pitch == current.pitch - && (sState == ALStream::Playing || sState == ALStream::Paused)) - { - setBaseVolume(_volume); - current.volume = _volume; - unlockStream(); - return; - } - - /* Requested audio file is different from current one */ - bool diffFile = (filename != current.filename); - - switch (sState) - { - case ALStream::Paused : - case ALStream::Playing : - stream.stop(); - case ALStream::Stopped : - if (diffFile) - stream.close(); - case ALStream::Closed : - if (diffFile) - { - try - { - /* This will throw on errors while - * opening the data source */ - stream.open(filename); - } - catch (const Exception &e) - { - unlockStream(); - throw e; - } - } - - break; - } - - setBaseVolume(_volume); - stream.setPitch(_pitch); - - current.filename = filename; - current.volume = _volume; - current.pitch = _pitch; - - if (!extPaused) - stream.play(offset); - else - noResumeStop = false; - - unlockStream(); - } - - void stop() - { - finiFadeInt(); - - lockStream(); - - noResumeStop = true; - - stream.stop(); - - unlockStream(); - } - - void fadeOut(int duration) - { - lockStream(); - - ALStream::State sState = stream.queryState(); - - if (fade.active) - { - unlockStream(); - - return; - } - - if (sState == ALStream::Paused) - { - stream.stop(); - unlockStream(); - - return; - } - - if (sState != ALStream::Playing) - { - unlockStream(); - - return; - } - - if (fade.thread) - { - fade.reqFini = true; - SDL_WaitThread(fade.thread, 0); - fade.thread = 0; - } - - fade.active = true; - fade.msStep = (1.0) / duration; - fade.reqFini = false; - fade.reqTerm = false; - fade.startTicks = SDL_GetTicks(); - - fade.thread = SDL_CreateThread(fadeThreadFun, fade.threadName.c_str(), this); - - unlockStream(); - } - - /* Any access to this classes 'stream' member, - * whether state query or modification, must be - * protected by a 'lock'/'unlock' pair */ - void lockStream() - { - SDL_LockMutex(streamMut); - } - - void unlockStream() - { - SDL_UnlockMutex(streamMut); - } - - void setFadeVolume(float value) - { - fadeVolume = value; - updateVolume(); - } - - void setExtVolume1(float value) - { - extVolume = value; - updateVolume(); - } - - float playingOffset() - { - return stream.queryOffset(); - } - -private: - void finiFadeInt() - { - if (!fade.thread) - return; - - fade.reqFini = true; - SDL_WaitThread(fade.thread, 0); - fade.thread = 0; - } - - void updateVolume() - { - stream.setVolume(baseVolume * fadeVolume * extVolume); - } - - void setBaseVolume(float value) - { - baseVolume = value; - updateVolume(); - } - - void fadeThread() - { - while (true) - { - /* Just immediately terminate on request */ - if (fade.reqTerm) - break; - - lockStream(); - - uint32_t curDur = SDL_GetTicks() - fade.startTicks; - float resVol = 1.0 - (curDur*fade.msStep); - - ALStream::State state = stream.queryState(); - - if (state != ALStream::Playing - || resVol < 0 - || fade.reqFini) - { - if (state != ALStream::Paused) - stream.stop(); - - setFadeVolume(1.0); - unlockStream(); - - break; - } - - setFadeVolume(resVol); - - unlockStream(); - - SDL_Delay(AUDIO_SLEEP); - } - - fade.active = false; - } - - static int fadeThreadFun(void *self) - { - static_cast(self)->fadeThread(); - - return 0; - } -}; struct AudioPrivate { diff --git a/src/audiostream.cpp b/src/audiostream.cpp new file mode 100644 index 0000000..d5975e9 --- /dev/null +++ b/src/audiostream.cpp @@ -0,0 +1,303 @@ +/* +** audiostream.cpp +** +** This file is part of mkxp. +** +** Copyright (C) 2014 Jonas Kulla +** +** mkxp is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 2 of the License, or +** (at your option) any later version. +** +** mkxp is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with mkxp. If not, see . +*/ + +#include "audiostream.h" + +#include "util.h" +#include "exception.h" + +#include +#include +#include + +AudioStream::AudioStream(ALStream::LoopMode loopMode, + const std::string &threadId) + : baseVolume(1.0), + fadeVolume(1.0), + extVolume(1.0), + extPaused(false), + noResumeStop(false), + stream(loopMode, threadId) +{ + current.volume = 1.0; + current.pitch = 1.0; + + fade.active = false; + fade.thread = 0; + fade.threadName = std::string("audio_fade (") + threadId + ")"; + + streamMut = SDL_CreateMutex(); +} + +AudioStream::~AudioStream() +{ + if (fade.thread) + { + fade.reqTerm = true; + SDL_WaitThread(fade.thread, 0); + } + + lockStream(); + + stream.stop(); + stream.close(); + + unlockStream(); + + SDL_DestroyMutex(streamMut); +} + +void AudioStream::play(const std::string &filename, + int volume, + int pitch, + float offset) +{ + finiFadeInt(); + + lockStream(); + + float _volume = clamp(volume, 0, 100) / 100.f; + float _pitch = clamp(pitch, 50, 150) / 100.f; + + ALStream::State sState = stream.queryState(); + + /* If all parameters match the current ones and we're + * still playing, there's nothing to do */ + if (filename == current.filename + && _volume == current.volume + && _pitch == current.pitch + && (sState == ALStream::Playing || sState == ALStream::Paused)) + { + unlockStream(); + return; + } + + /* If all parameters except volume match the current ones, + * we update the volume and continue streaming */ + if (filename == current.filename + && _pitch == current.pitch + && (sState == ALStream::Playing || sState == ALStream::Paused)) + { + setBaseVolume(_volume); + current.volume = _volume; + unlockStream(); + return; + } + + /* Requested audio file is different from current one */ + bool diffFile = (filename != current.filename); + + switch (sState) + { + case ALStream::Paused : + case ALStream::Playing : + stream.stop(); + case ALStream::Stopped : + if (diffFile) + stream.close(); + case ALStream::Closed : + if (diffFile) + { + try + { + /* This will throw on errors while + * opening the data source */ + stream.open(filename); + } + catch (const Exception &e) + { + unlockStream(); + throw e; + } + } + + break; + } + + setBaseVolume(_volume); + stream.setPitch(_pitch); + + current.filename = filename; + current.volume = _volume; + current.pitch = _pitch; + + if (!extPaused) + stream.play(offset); + else + noResumeStop = false; + + unlockStream(); +} + +void AudioStream::stop() +{ + finiFadeInt(); + + lockStream(); + + noResumeStop = true; + + stream.stop(); + + unlockStream(); +} + +void AudioStream::fadeOut(int duration) +{ + lockStream(); + + ALStream::State sState = stream.queryState(); + + if (fade.active) + { + unlockStream(); + + return; + } + + if (sState == ALStream::Paused) + { + stream.stop(); + unlockStream(); + + return; + } + + if (sState != ALStream::Playing) + { + unlockStream(); + + return; + } + + if (fade.thread) + { + fade.reqFini = true; + SDL_WaitThread(fade.thread, 0); + fade.thread = 0; + } + + fade.active = true; + fade.msStep = (1.0) / duration; + fade.reqFini = false; + fade.reqTerm = false; + fade.startTicks = SDL_GetTicks(); + + fade.thread = SDL_CreateThread(fadeThreadFun, fade.threadName.c_str(), this); + + unlockStream(); +} + +/* Any access to this classes 'stream' member, + * whether state query or modification, must be + * protected by a 'lock'/'unlock' pair */ +void AudioStream::lockStream() +{ + SDL_LockMutex(streamMut); +} + +void AudioStream::unlockStream() +{ + SDL_UnlockMutex(streamMut); +} + +void AudioStream::setFadeVolume(float value) +{ + fadeVolume = value; + updateVolume(); +} + +void AudioStream::setExtVolume1(float value) +{ + extVolume = value; + updateVolume(); +} + +float AudioStream::playingOffset() +{ + return stream.queryOffset(); +} + +void AudioStream::finiFadeInt() +{ + if (!fade.thread) + return; + + fade.reqFini = true; + SDL_WaitThread(fade.thread, 0); + fade.thread = 0; +} + +void AudioStream::updateVolume() +{ + stream.setVolume(baseVolume * fadeVolume * extVolume); +} + +void AudioStream::setBaseVolume(float value) +{ + baseVolume = value; + updateVolume(); +} + +void AudioStream::fadeThread() +{ + while (true) + { + /* Just immediately terminate on request */ + if (fade.reqTerm) + break; + + lockStream(); + + uint32_t curDur = SDL_GetTicks() - fade.startTicks; + float resVol = 1.0 - (curDur*fade.msStep); + + ALStream::State state = stream.queryState(); + + if (state != ALStream::Playing + || resVol < 0 + || fade.reqFini) + { + if (state != ALStream::Paused) + stream.stop(); + + setFadeVolume(1.0); + unlockStream(); + + break; + } + + setFadeVolume(resVol); + + unlockStream(); + + SDL_Delay(AUDIO_SLEEP); + } + + fade.active = false; +} + +int AudioStream::fadeThreadFun(void *self) +{ + static_cast(self)->fadeThread(); + + return 0; +} diff --git a/src/audiostream.h b/src/audiostream.h new file mode 100644 index 0000000..79a2509 --- /dev/null +++ b/src/audiostream.h @@ -0,0 +1,135 @@ +/* +** audiostream.h +** +** This file is part of mkxp. +** +** Copyright (C) 2014 Jonas Kulla +** +** mkxp is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 2 of the License, or +** (at your option) any later version. +** +** mkxp is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with mkxp. If not, see . +*/ + +#ifndef AUDIOSTREAM_H +#define AUDIOSTREAM_H + +#include "al-util.h" +#include "alstream.h" + +#include + +struct SDL_mutex; +struct SDL_Thread; + +struct AudioStream +{ + struct + { + std::string filename; + float volume; + float pitch; + } current; + + /* Volume set with 'play()' */ + float baseVolume; + + /* Volume set by external threads, + * such as for fade-in/out. + * Multiplied with intVolume for final + * playback volume. + * fadeVolume: used by fade-out thread. + * extVolume: used by MeWatch. */ + float fadeVolume; + float extVolume; + + /* Note that 'extPaused' and 'noResumeStop' are + * effectively only used with the AudioStream + * instance representing the BGM */ + + /* Flag indicating that the MeWatch paused this + * (BGM) stream because a ME started playing. + * While this flag is set, calls to 'play()' + * might open another file, but will not start + * the playback stream (the MeWatch will start + * it as soon as the ME finished playing). */ + bool extPaused; + + /* Flag indicating that this stream shouldn't be + * started by the MeWatch when it is in stopped + * state (eg. because the BGM stream was explicitly + * stopped by the user script while the ME was playing. + * When a new BGM is started (via 'play()') while an ME + * is playing, the file will be loaded without starting + * the stream, but we want the MeWatch to start it as + * soon as the ME ends, so we unset this flag. */ + bool noResumeStop; + + ALStream stream; + SDL_mutex *streamMut; + + struct + { + /* Fade is in progress */ + bool active; + + /* Request fade thread to finish and + * cleanup (like it normally would) */ + bool reqFini; + + /* Request fade thread to terminate + * immediately */ + bool reqTerm; + + SDL_Thread *thread; + std::string threadName; + + /* Amount of reduced absolute volume + * per ms of fade time */ + float msStep; + + /* Ticks at start of fade */ + uint32_t startTicks; + } fade; + + AudioStream(ALStream::LoopMode loopMode, + const std::string &threadId); + ~AudioStream(); + + void play(const std::string &filename, + int volume, + int pitch, + float offset = 0); + void stop(); + void fadeOut(int duration); + + /* Any access to this classes 'stream' member, + * whether state query or modification, must be + * protected by a 'lock'/'unlock' pair */ + void lockStream(); + void unlockStream(); + + void setFadeVolume(float value); + void setExtVolume1(float value); + + float playingOffset(); + +private: + void finiFadeInt(); + + void updateVolume(); + void setBaseVolume(float value); + + void fadeThread(); + static int fadeThreadFun(void *); +}; + +#endif // AUDIOSTREAM_H diff --git a/src/sdlsoundsource.cpp b/src/sdlsoundsource.cpp new file mode 100644 index 0000000..a90709f --- /dev/null +++ b/src/sdlsoundsource.cpp @@ -0,0 +1,127 @@ +/* +** sdlsoundsource.cpp +** +** This file is part of mkxp. +** +** Copyright (C) 2014 Jonas Kulla +** +** mkxp is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 2 of the License, or +** (at your option) any later version. +** +** mkxp is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with mkxp. If not, see . +*/ + +#include "aldatasource.h" +#include "exception.h" + +#include + +struct SDLSoundSource : ALDataSource +{ + Sound_Sample *sample; + SDL_RWops &srcOps; + uint8_t sampleSize; + bool looped; + + ALenum alFormat; + ALsizei alFreq; + + SDLSoundSource(SDL_RWops &ops, + const char *extension, + uint32_t maxBufSize, + bool looped) + : srcOps(ops), + looped(looped) + { + sample = Sound_NewSample(&srcOps, extension, 0, maxBufSize); + + if (!sample) + { + SDL_RWclose(&ops); + throw Exception(Exception::SDLError, "SDL_sound: %s", Sound_GetError()); + } + + sampleSize = formatSampleSize(sample->actual.format); + + alFormat = chooseALFormat(sampleSize, sample->actual.channels); + alFreq = sample->actual.rate; + } + + ~SDLSoundSource() + { + /* This also closes 'srcOps' */ + Sound_FreeSample(sample); + } + + Status fillBuffer(AL::Buffer::ID alBuffer) + { + uint32_t decoded = Sound_Decode(sample); + + if (sample->flags & SOUND_SAMPLEFLAG_EAGAIN) + { + /* Try to decode one more time on EAGAIN */ + decoded = Sound_Decode(sample); + + /* Give up */ + if (sample->flags & SOUND_SAMPLEFLAG_EAGAIN) + return ALDataSource::Error; + } + + if (sample->flags & SOUND_SAMPLEFLAG_ERROR) + return ALDataSource::Error; + + AL::Buffer::uploadData(alBuffer, alFormat, sample->buffer, decoded, alFreq); + + if (sample->flags & SOUND_SAMPLEFLAG_EOF) + { + if (looped) + { + Sound_Rewind(sample); + return ALDataSource::WrapAround; + } + else + { + return ALDataSource::EndOfStream; + } + } + + return ALDataSource::NoError; + } + + int sampleRate() + { + return sample->actual.rate; + } + + void seekToOffset(float seconds) + { + Sound_Seek(sample, static_cast(seconds * 1000)); + } + + void reset() + { + Sound_Rewind(sample); + } + + uint32_t loopStartFrames() + { + /* Loops from the beginning of the file */ + return 0; + } +}; + +ALDataSource *createSDLSource(SDL_RWops &ops, + const char *extension, + uint32_t maxBufSize, + bool looped) +{ + return new SDLSoundSource(ops, extension, maxBufSize, looped); +} diff --git a/src/soundemitter.cpp b/src/soundemitter.cpp new file mode 100644 index 0000000..b17d2c0 --- /dev/null +++ b/src/soundemitter.cpp @@ -0,0 +1,231 @@ +/* +** soundemitter.cpp +** +** This file is part of mkxp. +** +** Copyright (C) 2014 Jonas Kulla +** +** mkxp is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 2 of the License, or +** (at your option) any later version. +** +** mkxp is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with mkxp. If not, see . +*/ + +#include "soundemitter.h" + +#include "sharedstate.h" +#include "filesystem.h" +#include "exception.h" +#include "util.h" + +#include + +#define SE_CACHE_MEM (10*1024*1024) // 10 MB + +struct SoundBuffer +{ + /* Uniquely identifies this or equal buffer */ + std::string key; + + AL::Buffer::ID alBuffer; + + /* Link into the buffer cache priority list */ + IntruListLink link; + + /* Buffer byte count */ + uint32_t bytes; + + /* Reference count */ + uint8_t refCount; + + SoundBuffer() + : link(this), + refCount(1) + + { + alBuffer = AL::Buffer::gen(); + } + + static SoundBuffer *ref(SoundBuffer *buffer) + { + ++buffer->refCount; + + return buffer; + } + + static void deref(SoundBuffer *buffer) + { + if (--buffer->refCount == 0) + delete buffer; + } + +private: + ~SoundBuffer() + { + AL::Buffer::del(alBuffer); + } +}; + +/* Before: [a][b][c][d], After (index=1): [a][c][d][b] */ +static void +arrayPushBack(size_t array[], size_t size, size_t index) +{ + size_t v = array[index]; + + for (size_t t = index; t < size-1; ++t) + array[t] = array[t+1]; + + array[size-1] = v; +} + +SoundEmitter::SoundEmitter() + : bufferBytes(0) +{ + for (int i = 0; i < SE_SOURCES; ++i) + { + alSrcs[i] = AL::Source::gen(); + atchBufs[i] = 0; + srcPrio[i] = i; + } +} + +SoundEmitter::~SoundEmitter() +{ + for (int i = 0; i < SE_SOURCES; ++i) + { + AL::Source::stop(alSrcs[i]); + AL::Source::del(alSrcs[i]); + + if (atchBufs[i]) + SoundBuffer::deref(atchBufs[i]); + } + + BufferHash::const_iterator iter; + for (iter = bufferHash.cbegin(); iter != bufferHash.cend(); ++iter) + SoundBuffer::deref(iter->second); +} + +void SoundEmitter::play(const std::string &filename, + int volume, + int pitch) +{ + float _volume = clamp(volume, 0, 100) / 100.f; + float _pitch = clamp(pitch, 50, 150) / 100.f; + + SoundBuffer *buffer = allocateBuffer(filename); + + /* Try to find first free source */ + size_t i; + for (i = 0; i < SE_SOURCES; ++i) + if (AL::Source::getState(alSrcs[srcPrio[i]]) != AL_PLAYING) + break; + + /* If we didn't find any, overtake the one with lowest priority */ + if (i == SE_SOURCES) + i = 0; + + /* Push the used source to the back of the priority list */ + size_t srcIndex = srcPrio[i]; + arrayPushBack(srcPrio, SE_SOURCES, i); + + AL::Source::ID src = alSrcs[srcIndex]; + AL::Source::stop(src); + AL::Source::detachBuffer(src); + + SoundBuffer *old = atchBufs[srcIndex]; + + if (old) + SoundBuffer::deref(old); + + atchBufs[srcIndex] = SoundBuffer::ref(buffer); + + AL::Source::attachBuffer(src, buffer->alBuffer); + + AL::Source::setVolume(src, _volume); + AL::Source::setPitch(src, _pitch); + + AL::Source::play(src); +} + +void SoundEmitter::stop() +{ + for (int i = 0; i < SE_SOURCES; i++) + AL::Source::stop(alSrcs[i]); +} + +SoundBuffer *SoundEmitter::allocateBuffer(const std::string &filename) +{ + SoundBuffer *buffer = bufferHash.value(filename, 0); + + if (buffer) + { + /* Buffer still in cashe. + * Move to front of priority list */ + buffers.remove(buffer->link); + buffers.append(buffer->link); + + return buffer; + } + else + { + /* Buffer not in cashe, needs to be loaded */ + SDL_RWops dataSource; + const char *extension; + + shState->fileSystem().openRead(dataSource, filename.c_str(), + FileSystem::Audio, false, &extension); + + Sound_Sample *sampleHandle = Sound_NewSample(&dataSource, extension, 0, STREAM_BUF_SIZE); + + if (!sampleHandle) + { + SDL_RWclose(&dataSource); + throw Exception(Exception::SDLError, "SDL_sound: %s", Sound_GetError()); + } + + uint32_t decBytes = Sound_DecodeAll(sampleHandle); + uint8_t sampleSize = formatSampleSize(sampleHandle->actual.format); + uint32_t sampleCount = decBytes / sampleSize; + + buffer = new SoundBuffer; + buffer->key = filename; + buffer->bytes = sampleSize * sampleCount; + + ALenum alFormat = chooseALFormat(sampleSize, sampleHandle->actual.channels); + + AL::Buffer::uploadData(buffer->alBuffer, alFormat, sampleHandle->buffer, + buffer->bytes, sampleHandle->actual.rate); + + Sound_FreeSample(sampleHandle); + + uint32_t wouldBeBytes = bufferBytes + buffer->bytes; + + /* If memory limit is reached, delete lowest priority buffer + * until there is room or no buffers left */ + while (wouldBeBytes > SE_CACHE_MEM && !buffers.isEmpty()) + { + SoundBuffer *last = buffers.tail(); + bufferHash.erase(last->key); + buffers.remove(last->link); + + wouldBeBytes -= last->bytes; + + SoundBuffer::deref(last); + } + + bufferHash.insert(filename, buffer); + buffers.prepend(buffer->link); + + bufferBytes = wouldBeBytes; + + return buffer; + } +} diff --git a/src/soundemitter.h b/src/soundemitter.h new file mode 100644 index 0000000..70ea48f --- /dev/null +++ b/src/soundemitter.h @@ -0,0 +1,64 @@ +/* +** soundemitter.h +** +** This file is part of mkxp. +** +** Copyright (C) 2014 Jonas Kulla +** +** mkxp is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 2 of the License, or +** (at your option) any later version. +** +** mkxp is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with mkxp. If not, see . +*/ + +#ifndef SOUNDEMITTER_H +#define SOUNDEMITTER_H + +#include "intrulist.h" +#include "al-util.h" +#include "boost-hash.h" + +#include + +#define SE_SOURCES 6 + +struct SoundBuffer; + +struct SoundEmitter +{ + typedef BoostHash BufferHash; + + IntruList buffers; + BufferHash bufferHash; + + /* Byte count sum of all cached / playing buffers */ + uint32_t bufferBytes; + + AL::Source::ID alSrcs[SE_SOURCES]; + SoundBuffer *atchBufs[SE_SOURCES]; + + /* Indices of sources, sorted by priority (lowest first) */ + size_t srcPrio[SE_SOURCES]; + + SoundEmitter(); + ~SoundEmitter(); + + void play(const std::string &filename, + int volume, + int pitch); + + void stop(); + +private: + SoundBuffer *allocateBuffer(const std::string &filename); +}; + +#endif // SOUNDEMITTER_H diff --git a/src/vorbissource.cpp b/src/vorbissource.cpp new file mode 100644 index 0000000..c007ad8 --- /dev/null +++ b/src/vorbissource.cpp @@ -0,0 +1,283 @@ +/* +** vorbissource.cpp +** +** This file is part of mkxp. +** +** Copyright (C) 2014 Jonas Kulla +** +** mkxp is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 2 of the License, or +** (at your option) any later version. +** +** mkxp is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with mkxp. If not, see . +*/ + +#include "aldatasource.h" +#include "exception.h" + +#define OV_EXCLUDE_STATIC_CALLBACKS +#include +#include + +static size_t vfRead(void *ptr, size_t size, size_t nmemb, void *ops) +{ + return SDL_RWread(static_cast(ops), ptr, size, nmemb); +} + +static int vfSeek(void *ops, ogg_int64_t offset, int whence) +{ + return SDL_RWseek(static_cast(ops), offset, whence); +} + +static long vfTell(void *ops) +{ + return SDL_RWtell(static_cast(ops)); +} + +static ov_callbacks OvCallbacks = +{ + vfRead, + vfSeek, + 0, + vfTell +}; + + +struct VorbisSource : ALDataSource +{ + SDL_RWops &src; + + OggVorbis_File vf; + + uint32_t currentFrame; + + struct + { + uint32_t start; + uint32_t length; + uint32_t end; + bool valid; + bool requested; + } loop; + + struct + { + int channels; + int rate; + int frameSize; + ALenum alFormat; + } info; + + std::vector sampleBuf; + + VorbisSource(SDL_RWops &ops, + bool looped) + : src(ops), + currentFrame(0) + { + int error = ov_open_callbacks(&src, &vf, 0, 0, OvCallbacks); + + if (error) + { + SDL_RWclose(&src); + throw Exception(Exception::MKXPError, + "Vorbisfile: Cannot read ogg file"); + } + + /* Extract bitstream info */ + info.channels = vf.vi->channels; + info.rate = vf.vi->rate; + + if (info.channels > 2) + { + ov_clear(&vf); + SDL_RWclose(&src); + throw Exception(Exception::MKXPError, + "Cannot handle audio with more than 2 channels"); + } + + info.alFormat = chooseALFormat(sizeof(int16_t), info.channels); + info.frameSize = sizeof(int16_t) * info.channels; + + sampleBuf.resize(STREAM_BUF_SIZE); + + loop.requested = looped; + loop.valid = false; + loop.start = loop.length = 0; + + if (!loop.requested) + return; + + /* Try to extract loop info */ + for (int i = 0; i < vf.vc->comments; ++i) + { + char *comment = vf.vc->user_comments[i]; + char *sep = strstr(comment, "="); + + /* No '=' found */ + if (!sep) + continue; + + /* Empty value */ + if (!*(sep+1)) + continue; + + *sep = '\0'; + + if (!strcmp(comment, "LOOPSTART")) + loop.start = strtol(sep+1, 0, 10); + + if (!strcmp(comment, "LOOPLENGTH")) + loop.length = strtol(sep+1, 0, 10); + + *sep = '='; + } + + loop.end = loop.start + loop.length; + loop.valid = (loop.start && loop.length); + } + + ~VorbisSource() + { + ov_clear(&vf); + SDL_RWclose(&src); + } + + int sampleRate() + { + return info.rate; + } + + void seekToOffset(float seconds) + { + currentFrame = seconds * info.rate; + + if (loop.valid && currentFrame > loop.end) + currentFrame = loop.start; + + /* If seeking fails, just seek back to start */ + if (ov_pcm_seek(&vf, currentFrame) != 0) + ov_raw_seek(&vf, 0); + } + + Status fillBuffer(AL::Buffer::ID alBuffer) + { + void *bufPtr = sampleBuf.data(); + int availBuf = sampleBuf.size(); + int bufUsed = 0; + + int canRead = availBuf; + + Status retStatus = ALDataSource::NoError; + + bool readAgain = false; + + if (loop.valid) + { + int tilLoopEnd = loop.end * info.frameSize; + + canRead = std::min(availBuf, tilLoopEnd); + } + + while (canRead > 16) + { + long res = ov_read(&vf, static_cast(bufPtr), + canRead, 0, sizeof(int16_t), 1, 0); + + if (res < 0) + { + /* Read error */ + retStatus = ALDataSource::Error; + + break; + } + + if (res == 0) + { + /* EOF */ + if (loop.requested) + { + retStatus = ALDataSource::WrapAround; + reset(); + } + else + { + retStatus = ALDataSource::EndOfStream; + } + + /* If we sought right to the end of the file, + * we might be EOF without actually having read + * any data at all yet (which mustn't happen), + * so we try to continue reading some data. */ + if (bufUsed > 0) + break; + + if (readAgain) + { + /* We're still not getting data though. + * Just error out to prevent an endless loop */ + retStatus = ALDataSource::Error; + break; + } + + readAgain = true; + } + + bufUsed += (res / sizeof(int16_t)); + bufPtr = &sampleBuf[bufUsed]; + currentFrame += (res / info.frameSize); + + if (loop.valid && currentFrame >= loop.end) + { + /* Determine how many frames we're + * over the loop end */ + int discardFrames = currentFrame - loop.end; + bufUsed -= discardFrames * info.channels; + + retStatus = ALDataSource::WrapAround; + + /* Seek to loop start */ + currentFrame = loop.start; + if (ov_pcm_seek(&vf, currentFrame) != 0) + retStatus = ALDataSource::Error; + + break; + } + + canRead -= res; + } + + if (retStatus != ALDataSource::Error) + AL::Buffer::uploadData(alBuffer, info.alFormat, sampleBuf.data(), + bufUsed*sizeof(int16_t), info.rate); + + return retStatus; + } + + void reset() + { + ov_raw_seek(&vf, 0); + currentFrame = 0; + } + + uint32_t loopStartFrames() + { + if (loop.valid) + return loop.start; + else + return 0; + } +}; + +ALDataSource *createVorbisSource(SDL_RWops &ops, + bool looped) +{ + return new VorbisSource(ops, looped); +}