431 lines
7.7 KiB
C++
431 lines
7.7 KiB
C++
/*
|
|
** alstream.cpp
|
|
**
|
|
** This file is part of mkxp.
|
|
**
|
|
** Copyright (C) 2014 Jonas Kulla <Nyocurio@gmail.com>
|
|
**
|
|
** 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "alstream.h"
|
|
|
|
#include "sharedstate.h"
|
|
#include "sharedmidistate.h"
|
|
#include "filesystem.h"
|
|
#include "aldatasource.h"
|
|
#include "fluid-fun.h"
|
|
#include "sdl-util.h"
|
|
|
|
#include <SDL_mutex.h>
|
|
#include <SDL_thread.h>
|
|
#include <SDL_timer.h>
|
|
|
|
ALStream::ALStream(LoopMode loopMode,
|
|
const std::string &threadId)
|
|
: looped(loopMode == Looped),
|
|
state(Closed),
|
|
source(0),
|
|
thread(0),
|
|
preemptPause(false),
|
|
pitch(1.0)
|
|
{
|
|
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();
|
|
|
|
AL::Source::clearQueue(alSrc);
|
|
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)
|
|
{
|
|
/* If the source supports setting pitch natively,
|
|
* we don't have to do it via OpenAL */
|
|
if (source && source->setPitch(value))
|
|
AL::Source::setPitch(alSrc, 1.0);
|
|
else
|
|
AL::Source::setPitch(alSrc, value);
|
|
}
|
|
|
|
ALStream::State ALStream::queryState()
|
|
{
|
|
checkStopped();
|
|
|
|
return state;
|
|
}
|
|
|
|
float ALStream::queryOffset()
|
|
{
|
|
if (state == Closed)
|
|
return 0;
|
|
|
|
float procOffset = static_cast<float>(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);
|
|
needsRewind.clear();
|
|
|
|
/* Try to read ogg file signature */
|
|
char sig[5] = { 0 };
|
|
SDL_RWread(&srcOps, sig, 1, 4);
|
|
SDL_RWseek(&srcOps, 0, RW_SEEK_SET);
|
|
|
|
if (!strcmp(sig, "OggS"))
|
|
{
|
|
source = createVorbisSource(srcOps, looped);
|
|
return;
|
|
}
|
|
|
|
if (!strcmp(sig, "MThd"))
|
|
{
|
|
shState->midiState().initIfNeeded(shState->config());
|
|
|
|
if (HAVE_FLUID)
|
|
{
|
|
source = createMidiSource(srcOps, looped);
|
|
return;
|
|
}
|
|
}
|
|
|
|
source = createSDLSource(srcOps, ext, STREAM_BUF_SIZE, looped);
|
|
}
|
|
|
|
void ALStream::stopStream()
|
|
{
|
|
threadTermReq.set();
|
|
|
|
if (thread)
|
|
{
|
|
SDL_WaitThread(thread, 0);
|
|
thread = 0;
|
|
needsRewind.set();
|
|
}
|
|
|
|
/* Need to stop the source _after_ the thread has terminated,
|
|
* because it might have accidentally started it again before
|
|
* seeing the term request */
|
|
AL::Source::stop(alSrc);
|
|
|
|
procFrames = 0;
|
|
}
|
|
|
|
void ALStream::startStream(float offset)
|
|
{
|
|
AL::Source::clearQueue(alSrc);
|
|
|
|
preemptPause = false;
|
|
streamInited.clear();
|
|
sourceExhausted.clear();
|
|
threadTermReq.clear();
|
|
|
|
startOffset = offset;
|
|
procFrames = offset * source->sampleRate();
|
|
|
|
thread = createSDLThread
|
|
<ALStream, &ALStream::streamData>(this, threadName);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/* thread func */
|
|
void ALStream::streamData()
|
|
{
|
|
/* Fill up queue */
|
|
bool firstBuffer = true;
|
|
ALDataSource::Status status;
|
|
|
|
if (threadTermReq)
|
|
return;
|
|
|
|
if (needsRewind)
|
|
{
|
|
source->seekToOffset(startOffset);
|
|
}
|
|
|
|
for (int i = 0; i < STREAM_BUFS; ++i)
|
|
{
|
|
if (threadTermReq)
|
|
return;
|
|
|
|
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.set();
|
|
}
|
|
|
|
if (threadTermReq)
|
|
return;
|
|
|
|
if (status == ALDataSource::EndOfStream)
|
|
{
|
|
sourceExhausted.set();
|
|
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.set();
|
|
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.set();
|
|
}
|
|
|
|
if (threadTermReq)
|
|
break;
|
|
|
|
SDL_Delay(AUDIO_SLEEP);
|
|
}
|
|
}
|