Implemented OpenAL streaming. Fixed bugs in SampleReader and elsewhere.

This commit is contained in:
Nicolay Korslund 2010-08-17 13:17:39 +02:00
parent 932465442b
commit cd4ed4e6bf
6 changed files with 217 additions and 46 deletions

View file

@ -11,6 +11,20 @@ using namespace Mangle::Sound;
// ---- Helper functions and classes ---- // ---- Helper functions and classes ----
// Static buffer used to shuffle sound data from the input into
// OpenAL. The data is only stored temporarily and then immediately
// shuffled off to the library. This is not thread safe, but it works
// fine with multiple sounds in one thread. It could be made thread
// safe simply by using thread local storage.
const size_t BSIZE = 32*1024;
static char tmp_buffer[BSIZE];
// Number of buffers used (per sound) for streaming sounds. Each
// buffer is of size BSIZE. Increasing this will make streaming sounds
// more fault tolerant against temporary lapses in call to update(),
// but will also increase memory usage. 4 should be ok.
const int STREAM_BUF_NUM = 4;
static void fail(const std::string &msg) static void fail(const std::string &msg)
{ throw str_exception("OpenAL exception: " + msg); } { throw str_exception("OpenAL exception: " + msg); }
@ -73,10 +87,18 @@ static void getALFormat(SampleSourcePtr inp, int &fmt, int &rate)
} }
/// OpenAL sound output /// OpenAL sound output
class OpenAL_Sound : public Sound class Mangle::Sound::OpenAL_Sound : public Sound
{ {
ALuint inst; ALuint inst;
ALuint bufferID;
// Buffers. Only the first is used for non-streaming sounds.
ALuint bufferID[4];
// Number of buffers used
int bufNum;
// Parameters used for filling buffers
int fmt, rate;
// Poor mans reference counting. Might improve this later. When // Poor mans reference counting. Might improve this later. When
// NULL, the buffer has not been set up yet. // NULL, the buffer has not been set up yet.
@ -87,18 +109,74 @@ class OpenAL_Sound : public Sound
// Input stream // Input stream
SampleSourcePtr input; SampleSourcePtr input;
OpenAL_Factory *owner;
bool ownerAlive;
// Used for streamed sound list
OpenAL_Sound *next, *prev;
void setupBuffer(); void setupBuffer();
// Fill data into the given buffer and queue it, if there is any
// data left to queue. Assumes the buffer is already unqueued, if
// necessary.
void queueBuffer(ALuint buf)
{
// If there is no more data, do nothing
if(!input) return;
if(input->eof())
{
input.reset();
return;
}
// Get some new data
size_t bytes = input->read(tmp_buffer, BSIZE);
if(bytes == 0)
{
input.reset();
return;
}
// Move data into the OpenAL buffer
alBufferData(buf, fmt, tmp_buffer, bytes, rate);
// Queue it
alSourceQueueBuffers(inst, 1, &buf);
checkALError("Queueing buffer data");
}
public: public:
/// Read samples from the given input buffer /// Read samples from the given input buffer
OpenAL_Sound(SampleSourcePtr input); OpenAL_Sound(SampleSourcePtr input, OpenAL_Factory *fact);
/// Play an existing buffer, with a given ref counter. Used /// Play an existing buffer, with a given ref counter. Used
/// internally for cloning. /// internally for cloning.
OpenAL_Sound(ALuint buf, int *ref); OpenAL_Sound(ALuint buf, int *ref, OpenAL_Factory *fact);
~OpenAL_Sound(); ~OpenAL_Sound();
// Must be called regularly on streamed sounds
void update()
{
if(!streaming) return;
if(!input) return;
// Get the number of processed buffers
ALint count;
alGetSourcei(inst, AL_BUFFERS_PROCESSED, &count);
checkALError("getting number of unprocessed buffers");
for(int i=0; i<count; i++)
{
ALuint buf;
// Unqueue one of the processed buffer
alSourceUnqueueBuffers(inst, 1, &buf);
// Then reload it with data (if any) and queue it up
queueBuffer(buf);
}
}
void play(); void play();
void stop(); void stop();
void pause(); void pause();
@ -107,7 +185,14 @@ class OpenAL_Sound : public Sound
void setPos(float x, float y, float z); void setPos(float x, float y, float z);
void setPitch(float); void setPitch(float);
void setRepeat(bool); void setRepeat(bool);
void setStreaming(bool s) { streaming = s; }
void notifyOwnerDeath()
{ ownerAlive = false; }
// We can enable streaming, but never disable it.
void setStreaming(bool s)
{ if(s) streaming = true; }
SoundPtr clone(); SoundPtr clone();
// a = AL_REFERENCE_DISTANCE // a = AL_REFERENCE_DISTANCE
@ -123,11 +208,7 @@ class OpenAL_Sound : public Sound
SoundPtr OpenAL_Factory::loadRaw(SampleSourcePtr input) SoundPtr OpenAL_Factory::loadRaw(SampleSourcePtr input)
{ {
return SoundPtr(new OpenAL_Sound(input)); return SoundPtr(new OpenAL_Sound(input, this));
}
void OpenAL_Factory::update()
{
} }
void OpenAL_Factory::setListenerPos(float x, float y, float z, void OpenAL_Factory::setListenerPos(float x, float y, float z,
@ -173,8 +254,33 @@ OpenAL_Factory::OpenAL_Factory(bool doSetup)
} }
} }
void OpenAL_Factory::update()
{
// Loop through all streaming sounds and update them
StreamList::iterator it = streaming.begin();
for(;it != streaming.end(); it++)
(*it)->update();
}
void OpenAL_Factory::notifyStreaming(OpenAL_Sound *snd)
{
// Add the sound to the streaming list
streaming.push_back(snd);
}
void OpenAL_Factory::notifyDelete(OpenAL_Sound *snd)
{
// Remove the sound from the stream list
streaming.remove(snd);
}
OpenAL_Factory::~OpenAL_Factory() OpenAL_Factory::~OpenAL_Factory()
{ {
// Notify remaining streamed sounds that we're dying
StreamList::iterator it = streaming.begin();
for(;it != streaming.end(); it++)
(*it)->notifyOwnerDeath();
// Deinitialize sound system // Deinitialize sound system
if(didSetup) if(didSetup)
{ {
@ -249,27 +355,31 @@ SoundPtr OpenAL_Sound::clone()
{ {
setupBuffer(); setupBuffer();
assert(!streaming && "cloning streamed sounds not supported"); assert(!streaming && "cloning streamed sounds not supported");
return SoundPtr(new OpenAL_Sound(bufferID, refCnt)); return SoundPtr(new OpenAL_Sound(bufferID[0], refCnt, owner));
} }
// Constructor used for cloned sounds // Constructor used for cloned sounds
OpenAL_Sound::OpenAL_Sound(ALuint buf, int *ref) OpenAL_Sound::OpenAL_Sound(ALuint buf, int *ref, OpenAL_Factory *fact)
: bufferID(buf), refCnt(ref), streaming(false) : refCnt(ref), streaming(false), owner(fact), ownerAlive(false)
{ {
// Increase the reference count // Increase the reference count
assert(ref != NULL); assert(ref != NULL);
*refCnt++; *refCnt++;
// Set up buffer
bufferID[0] = buf;
bufNum = 1;
// Create a source // Create a source
alGenSources(1, &inst); alGenSources(1, &inst);
checkALError("creating instance (clone)"); checkALError("creating instance (clone)");
alSourcei(inst, AL_BUFFER, bufferID); alSourcei(inst, AL_BUFFER, bufferID[0]);
checkALError("assigning buffer (clone)"); checkALError("assigning buffer (clone)");
} }
// Constructor used for original (non-cloned) sounds // Constructor used for original (non-cloned) sounds
OpenAL_Sound::OpenAL_Sound(SampleSourcePtr _input) OpenAL_Sound::OpenAL_Sound(SampleSourcePtr _input, OpenAL_Factory *fact)
: bufferID(0), refCnt(NULL), streaming(false), input(_input) : refCnt(NULL), streaming(false), input(_input), owner(fact), ownerAlive(false)
{ {
// Create a source // Create a source
alGenSources(1, &inst); alGenSources(1, &inst);
@ -287,46 +397,60 @@ void OpenAL_Sound::setupBuffer()
assert(input); assert(input);
// Get the format // Get the format
int fmt, rate;
getALFormat(input, fmt, rate); getALFormat(input, fmt, rate);
// Create a cheap reference counter for the buffer
refCnt = new int;
*refCnt = 1;
if(streaming) bufNum = STREAM_BUF_NUM;
else bufNum = 1;
// Set up the OpenAL buffer(s)
alGenBuffers(bufNum, bufferID);
checkALError("generating buffer(s)");
assert(bufferID[0] != 0);
// STREAMING.
if(streaming) if(streaming)
{ {
// To be done // Just queue all the buffers with data and exit. queueBuffer()
// will work correctly also in the case where there is not
// enough data to fill all the buffers.
for(int i=0; i<bufNum; i++)
queueBuffer(bufferID[i]);
// Notify the manager what we're doing
owner->notifyStreaming(this);
ownerAlive = true;
return;
} }
// NON-STREAMING // NON-STREAMING. We have to load all the data and shove it into the
// buffer.
// Set up the OpenAL buffer
alGenBuffers(1, &bufferID);
checkALError("generating buffer");
assert(bufferID != 0);
// Does the stream support pointer operations? // Does the stream support pointer operations?
if(input->hasPtr) if(input->hasPtr)
{ {
// If so, we can read the data directly from the stream // If so, we can read the data directly from the stream
alBufferData(bufferID, fmt, input->getPtr(), input->size(), rate); alBufferData(bufferID[0], fmt, input->getPtr(), input->size(), rate);
} }
else else
{ {
// Read the entire stream into a temporary buffer first // Read the entire stream into a temporary buffer first
Mangle::Stream::BufferStream buf(input, streaming?1024*1024:32*1024); Mangle::Stream::BufferStream buf(input, 128*1024);
// Then copy that into OpenAL // Then copy that into OpenAL
alBufferData(bufferID, fmt, buf.getPtr(), buf.size(), rate); alBufferData(bufferID[0], fmt, buf.getPtr(), buf.size(), rate);
} }
checkALError("loading sound buffer"); checkALError("loading sound data");
// We're done with the input stream, release the pointer // We're done with the input stream, release the pointer
input.reset(); input.reset();
alSourcei(inst, AL_BUFFER, bufferID); alSourcei(inst, AL_BUFFER, bufferID[0]);
checkALError("assigning buffer"); checkALError("assigning buffer");
// Create a cheap reference counter for the buffer
refCnt = new int;
*refCnt = 1;
} }
OpenAL_Sound::~OpenAL_Sound() OpenAL_Sound::~OpenAL_Sound()
@ -337,12 +461,19 @@ OpenAL_Sound::~OpenAL_Sound()
// Return sound // Return sound
alDeleteSources(1, &inst); alDeleteSources(1, &inst);
// Notify the factory that we quit. You will hear from our union
// rep. The bool check is to handle cases where the manager goes out
// of scope before the sounds do. In that case, don't try to contact
// the factory.
if(ownerAlive)
owner->notifyDelete(this);
// Decrease the reference counter // Decrease the reference counter
if((-- *refCnt) == 0) if((-- *refCnt) == 0)
{ {
// We're the last owner. Delete the buffer and the counter // We're the last owner. Delete the buffer(s) and the counter
// itself. // itself.
alDeleteBuffers(1, &bufferID); alDeleteBuffers(bufNum, bufferID);
checkALError("deleting buffer"); checkALError("deleting buffer");
delete refCnt; delete refCnt;
} }

View file

@ -2,16 +2,27 @@
#define MANGLE_SOUND_OPENAL_OUT_H #define MANGLE_SOUND_OPENAL_OUT_H
#include "../output.hpp" #include "../output.hpp"
#include <list>
namespace Mangle { namespace Mangle {
namespace Sound { namespace Sound {
class OpenAL_Sound;
class OpenAL_Factory : public SoundFactory class OpenAL_Factory : public SoundFactory
{ {
void *device; void *device;
void *context; void *context;
bool didSetup; bool didSetup;
// List of streaming sounds that need to be updated every frame.
typedef std::list<OpenAL_Sound*> StreamList;
StreamList streaming;
friend class OpenAL_Sound;
void notifyStreaming(OpenAL_Sound*);
void notifyDelete(OpenAL_Sound*);
public: public:
/// Initialize object. Pass true (default) if you want the /// Initialize object. Pass true (default) if you want the
/// constructor to set up the current ALCdevice and ALCcontext for /// constructor to set up the current ALCdevice and ALCcontext for

View file

@ -19,9 +19,9 @@ void SndFileSource::getInfo(int32_t *_rate, int32_t *_channels, int32_t *_bits)
size_t SndFileSource::readSamples(void *data, size_t length) size_t SndFileSource::readSamples(void *data, size_t length)
{ {
// Read frames. We count channels as part of the frame. Even though // Read frames. We count channels as part of the frame, even though
// libsndfile does not, since it still requires the number of frames // libsndfile does not. This is because the library still requires
// read to be a multiple of channels. // the number of frames read to be a multiple of channels.
return channels*sf_read_short((SNDFILE*)handle, (short*)data, length*channels); return channels*sf_read_short((SNDFILE*)handle, (short*)data, length*channels);
} }

View file

@ -22,15 +22,36 @@ size_t SampleReader::read(void *_data, size_t length)
if(isEof) return 0; if(isEof) return 0;
char *data = (char*)_data; char *data = (char*)_data;
// Move the remains from the last operation first // Pullsize holds the number of bytes that were copied "extra" at
// the end of LAST round. If non-zero, it also means there is data
// left in the pullOver buffer.
if(pullSize) if(pullSize)
{ {
// pullSize is how much was stored the last time. The data is // Amount of data left
// stored at the end of the buffer. size_t doRead = frameSize - pullSize;
memcpy(data, pullOver+(frameSize-pullSize), pullSize); assert(doRead > 0);
length -= pullSize;
data += pullSize; // Make sure we don't read more than we're supposed to
pullSize = 0; if(doRead > length) doRead = length;
memcpy(data, pullOver+pullSize, doRead);
// Update the number of bytes now copied
pullSize += doRead;
assert(pullSize <= frameSize);
if(pullSize < frameSize)
{
// There is STILL data left in the pull buffer, and we've
// done everything we were supposed to. Leave it and return.
assert(doRead == length);
return doRead;
}
// Set up variables for further reading below. No need to update
// pullSize, it is overwritten anyway.
length -= doRead;
data += doRead;
} }
// Number of whole frames // Number of whole frames
@ -38,6 +59,7 @@ size_t SampleReader::read(void *_data, size_t length)
// Read the data // Read the data
size_t res = readSamples(data, frames); size_t res = readSamples(data, frames);
assert(res <= frames);
// Total bytes read // Total bytes read
size_t num = res*frameSize; size_t num = res*frameSize;
@ -53,12 +75,14 @@ size_t SampleReader::read(void *_data, size_t length)
// Determine the overshoot // Determine the overshoot
pullSize = length - num; pullSize = length - num;
assert(pullSize < frameSize && pullSize >= 0);
// Are we missing data? // Are we missing data?
if(pullSize) if(pullSize)
{ {
// Fill in one sample // Fill in one sample
res = readSamples(pullOver,1); res = readSamples(pullOver,1);
assert(res == 1 || res == 0);
if(res) if(res)
{ {
// Move as much as we can into the output buffer // Move as much as we can into the output buffer

View file

@ -39,11 +39,16 @@ int main()
snd->setVolume(0.8); snd->setVolume(0.8);
snd->setPitch(0.9); snd->setPitch(0.9);
// Also test streaming, since all the other examples test
// non-streaming sounds.
snd->setStreaming(true);
snd->play(); snd->play();
while(snd->isPlaying()) while(snd->isPlaying())
{ {
usleep(10000); usleep(10000);
mg.update();
} }
} }
catch(exception &e) catch(exception &e)

View file

@ -29,7 +29,7 @@ class StdStream : public Stream
size_t read(void* buf, size_t len) size_t read(void* buf, size_t len)
{ {
inf->read((char*)buf, len); inf->read((char*)buf, len);
if(inf->fail()) if(inf->bad())
fail("error reading from stream"); fail("error reading from stream");
return inf->gcount(); return inf->gcount();
} }