Browse Source

Added Source:getDuration (thanks Boolsheet!)

For streaming sources, the duration may not always be sample-accurate and may return -1 if it cannot be determined at all.
Alex Szpakowski 9 years ago
parent
commit
0558fcdbd4

+ 2 - 1
src/modules/audio/Source.h

@@ -39,7 +39,7 @@ public:
 		TYPE_STATIC,
 		TYPE_STREAM,
 		TYPE_MAX_ENUM
-	}; // Type
+	};
 
 	enum Unit
 	{
@@ -71,6 +71,7 @@ public:
 
 	virtual void seek(float offset, Unit unit) = 0;
 	virtual float tell(Unit unit) = 0;
+	virtual double getDuration(Unit unit) = 0;
 
 	// all float * v must be of size 3
 	virtual void setPosition(float *v) = 0;

+ 5 - 0
src/modules/audio/null/Source.cpp

@@ -112,6 +112,11 @@ float Source::tell(Source::Unit)
 	return 0.0f;
 }
 
+double Source::getDuration(Unit)
+{
+	return -1.0f;
+}
+
 void Source::setPosition(float *)
 {
 }

+ 1 - 0
src/modules/audio/null/Source.h

@@ -54,6 +54,7 @@ public:
 	virtual float getVolume() const;
 	virtual void seek(float offset, Unit unit);
 	virtual float tell(Unit unit);
+	virtual double getDuration(Unit unit);
 	virtual void setPosition(float *v);
 	virtual void getPosition(float *v) const;
 	virtual void setVelocity(float *v);

+ 6 - 0
src/modules/audio/openal/Pool.cpp

@@ -263,6 +263,12 @@ float Pool::tell(Source *source, void *unit)
 	return source->tellAtomic(unit);
 }
 
+double Pool::getDuration(Source *source, void *unit)
+{
+	thread::Lock lock(mutex);
+	return source->getDurationAtomic(unit);
+}
+
 ALuint Pool::findi(const Source *source) const
 {
 	std::map<Source *, ALuint>::const_iterator i = playing.find((Source *)source);

+ 1 - 0
src/modules/audio/openal/Pool.h

@@ -93,6 +93,7 @@ public:
 	void softRewind(Source *source);
 	void seek(Source *source, float offset, void *unit);
 	float tell(Source *source, void *unit);
+	double getDuration(Source *source, void *unit);
 
 private:
 

+ 34 - 0
src/modules/audio/openal/Source.cpp

@@ -65,6 +65,7 @@ Ensure the Source is not multi-channel before calling this function.")
 };
 
 StaticDataBuffer::StaticDataBuffer(ALenum format, const ALvoid *data, ALsizei size, ALsizei freq)
+	: size(size)
 {
 	alGenBuffers(1, &buffer);
 	alBufferData(buffer, format, data, size, freq);
@@ -95,6 +96,7 @@ Source::Source(Pool *pool, love::sound::SoundData *soundData)
 	, offsetSeconds(0)
 	, sampleRate(soundData->getSampleRate())
 	, channels(soundData->getChannels())
+	, bitDepth(soundData->getBitDepth())
 	, decoder(nullptr)
 	, toLoop(0)
 {
@@ -134,6 +136,7 @@ Source::Source(Pool *pool, love::sound::Decoder *decoder)
 	, offsetSeconds(0)
 	, sampleRate(decoder->getSampleRate())
 	, channels(decoder->getChannels())
+	, bitDepth(decoder->getBitDepth())
 	, decoder(decoder)
 	, toLoop(0)
 {
@@ -169,6 +172,7 @@ Source::Source(const Source &s)
 	, offsetSeconds(0)
 	, sampleRate(s.sampleRate)
 	, channels(s.channels)
+	, bitDepth(s.bitDepth)
 	, decoder(nullptr)
 	, toLoop(0)
 {
@@ -441,6 +445,36 @@ float Source::tell(Source::Unit unit)
 	return pool->tell(this, &unit);
 }
 
+double Source::getDurationAtomic(void *vunit)
+{
+	Unit unit = *(Unit *) vunit;
+
+	if (type == TYPE_STREAM)
+	{
+		double seconds = decoder->getDuration();
+
+		if (unit == UNIT_SECONDS)
+			return seconds;
+		else
+			return seconds * decoder->getSampleRate();
+	}
+	else
+	{
+		ALsizei size = staticBuffer->getSize();
+		ALsizei samples = (size / channels) / (bitDepth / 8);
+
+		if (unit == UNIT_SAMPLES)
+			return (double) samples;
+		else
+			return (double) samples / (double) sampleRate;
+	}
+}
+
+double Source::getDuration(Unit unit)
+{
+	return pool->getDuration(this, &unit);
+}
+
 void Source::setPosition(float *v)
 {
 	if (channels > 1)

+ 9 - 0
src/modules/audio/openal/Source.h

@@ -65,9 +65,15 @@ public:
 		return buffer;
 	}
 
+	inline ALsizei getSize() const
+	{
+		return size;
+	}
+
 private:
 
 	ALuint buffer;
+	ALsizei size;
 
 }; // StaticDataBuffer
 
@@ -98,6 +104,8 @@ public:
 	virtual void seek(float offset, Unit unit);
 	virtual float tellAtomic(void *unit) const;
 	virtual float tell(Unit unit);
+	virtual double getDurationAtomic(void *unit);
+	virtual double getDuration(Unit unit);
 	virtual void setPosition(float *v);
 	virtual void getPosition(float *v) const;
 	virtual void setVelocity(float *v);
@@ -180,6 +188,7 @@ private:
 
 	int sampleRate;
 	int channels;
+	int bitDepth;
 
 	StrongRef<love::sound::Decoder> decoder;
 

+ 14 - 0
src/modules/audio/wrap_Source.cpp

@@ -140,6 +140,19 @@ int w_Source_tell(lua_State *L)
 	return 1;
 }
 
+int w_Source_getDuration(lua_State *L)
+{
+	Source *t = luax_checksource(L, 1);
+
+	Source::Unit u = Source::UNIT_SECONDS;
+	const char *unit = lua_isnoneornil(L, 2) ? 0 : lua_tostring(L, 2);
+	if (unit && !t->getConstant(unit, u))
+		return luaL_error(L, "Invalid Source time unit: %s", unit);
+
+	lua_pushnumber(L, t->getDuration(u));
+	return 1;
+}
+
 int w_Source_setPosition(lua_State *L)
 {
 	Source *t = luax_checksource(L, 1);
@@ -373,6 +386,7 @@ static const luaL_Reg functions[] =
 	{ "getVolume", w_Source_getVolume },
 	{ "seek", w_Source_seek },
 	{ "tell", w_Source_tell },
+	{ "getDuration", w_Source_getDuration },
 	{ "setPosition", w_Source_setPosition },
 	{ "getPosition", w_Source_getPosition },
 	{ "setVelocity", w_Source_setVelocity },

+ 1 - 0
src/modules/audio/wrap_Source.h

@@ -42,6 +42,7 @@ int w_Source_setVolume(lua_State *L);
 int w_Source_getVolume(lua_State *L);
 int w_Source_seek(lua_State *L);
 int w_Source_tell(lua_State *L);
+int w_Source_getDuration(lua_State *L);
 int w_Source_setPosition(lua_State *L);
 int w_Source_getPosition(lua_State *L);
 int w_Source_setVelocity(lua_State *L);

+ 1 - 3
src/modules/graphics/opengl/Mesh.cpp

@@ -581,11 +581,9 @@ void Mesh::draw(float x, float y, float angle, float sx, float sy, float ox, flo
 		enabledattribs |= 1 << uint32(attriblocation);
 	}
 
+	// Not supported on all platforms or GL versions, I believe.
 	if (!(enabledattribs & ATTRIBFLAG_POS))
-	{
-		// Not supported on all platforms or GL versions at least, I believe.
 		throw love::Exception("Mesh must have an enabled VertexPosition attribute to be drawn.");
-	}
 
 	gl.useVertexAttribArrays(enabledattribs);
 

+ 6 - 0
src/modules/sound/Decoder.h

@@ -134,6 +134,12 @@ public:
 	 **/
 	virtual int getSampleRate() const = 0;
 
+	/**
+	 * Gets the estimated total duration of the stream. in seconds. May return
+	 * -1 if the duration cannot be determined.
+	 **/
+	virtual double getDuration() = 0;
+
 }; // Decoder
 
 } // sound

+ 20 - 0
src/modules/sound/lullaby/CoreAudioDecoder.cpp

@@ -69,6 +69,7 @@ CoreAudioDecoder::CoreAudioDecoder(Data *data, const std::string &ext, int buffe
 	, extAudioFile(nullptr)
 	, inputInfo()
 	, outputInfo()
+	, duration(-2.0)
 {
 	try
 	{
@@ -267,6 +268,25 @@ int CoreAudioDecoder::getBitDepth() const
 	return outputInfo.mBitsPerChannel;
 }
 
+double CoreAudioDecoder::getDuration()
+{
+	// Only calculate the duration if we haven't done so already.
+	if (duration == -2.0)
+	{
+		SInt64 samples = 0;
+		UInt32 psize = (UInt32) sizeof(samples);
+
+		OSStatus err = ExtAudioFileGetProperty(extAudioFile, kExtAudioFileProperty_FileLengthFrames, &psize, &samples);
+
+		if (err == noErr)
+			duration = (double) samples / (double) sampleRate;
+		else
+			duration = -1.0;
+	}
+
+	return duration;
+}
+
 } // lullaby
 } // sound
 } // love

+ 3 - 0
src/modules/sound/lullaby/CoreAudioDecoder.h

@@ -59,6 +59,7 @@ public:
 	bool isSeekable();
 	int getChannels() const;
 	int getBitDepth() const;
+	double getDuration();
 
 private:
 
@@ -70,6 +71,8 @@ private:
 	AudioStreamBasicDescription inputInfo;
 	AudioStreamBasicDescription outputInfo;
 
+	double duration;
+
 }; // CoreAudioDecoder
 
 } // lullaby

+ 5 - 0
src/modules/sound/lullaby/FLACDecoder.cpp

@@ -108,6 +108,11 @@ int FLACDecoder::getSampleRate() const
 	return get_sample_rate();
 }
 
+double FLACDecoder::getDuration()
+{
+	return -1;
+}
+
 FLAC__StreamDecoderReadStatus FLACDecoder::read_callback(FLAC__byte buffer[], size_t *bytes)
 {
 	int size = data->getSize();

+ 1 - 0
src/modules/sound/lullaby/FLACDecoder.h

@@ -52,6 +52,7 @@ public:
 	int getChannels() const;
 	int getBitDepth() const;
 	int getSampleRate() const;
+	double getDuration();
 
 	//needed for FLAC
 	FLAC__StreamDecoderReadStatus read_callback(FLAC__byte buffer[], size_t *bytes);

+ 5 - 0
src/modules/sound/lullaby/GmeDecoder.cpp

@@ -142,6 +142,11 @@ int GmeDecoder::getBitDepth() const
 	return 16;
 }
 
+double GmeDecoder::getDuration()
+{
+	return -1;
+}
+
 } // lullaby
 } // sound
 } // love

+ 1 - 0
src/modules/sound/lullaby/GmeDecoder.h

@@ -56,6 +56,7 @@ public:
 	bool isSeekable();
 	int getChannels() const;
 	int getBitDepth() const;
+	double getDuration();
 
 private:
 	Music_Emu *emu;

+ 17 - 0
src/modules/sound/lullaby/ModPlugDecoder.cpp

@@ -34,6 +34,7 @@ namespace lullaby
 ModPlugDecoder::ModPlugDecoder(Data *data, const std::string &ext, int bufferSize)
 	: Decoder(data, ext, bufferSize)
 	, plug(0)
+	, duration(-2.0)
 {
 
 	// Set some ModPlug settings.
@@ -141,6 +142,22 @@ int ModPlugDecoder::getBitDepth() const
 	return 16;
 }
 
+double ModPlugDecoder::getDuration()
+{
+	// Only calculate the duration if we haven't done so already.
+	if (duration == -2.0)
+	{
+		int lengthms = ModPlug_GetLength(plug);
+
+		if (lengthms < 0)
+			duration = -1.0;
+		else
+			duration = (double) lengthms / 1000.0;
+	}
+
+	return duration;
+}
+
 } // lullaby
 } // sound
 } // love

+ 4 - 0
src/modules/sound/lullaby/ModPlugDecoder.h

@@ -59,11 +59,15 @@ public:
 	bool isSeekable();
 	int getChannels() const;
 	int getBitDepth() const;
+	double getDuration();
 
 private:
 
 	ModPlugFile *plug;
 	ModPlug_Settings settings;
+
+	double duration;
+
 }; // Decoder
 
 } // lullaby

+ 134 - 80
src/modules/sound/lullaby/Mpg123Decoder.cpp

@@ -31,18 +31,88 @@ namespace sound
 namespace lullaby
 {
 
+/**
+ * mpg123 callbacks for seekable streams.
+ **/
+
+static ssize_t read_callback(void *udata, void *buffer, size_t count)
+{
+	DecoderFile *file = (DecoderFile *) udata;
+
+	// Calculates how much data is still left and takes that value or
+	// the buffer size, whichever is lower, as the number of bytes to write.
+	size_t countLeft = file->size - file->offset;
+	size_t countWrite = countLeft < count ? countLeft : count;
+
+	if (countWrite > 0)
+	{
+		memcpy(buffer, file->data + file->offset, countWrite);
+		file->offset += countWrite;
+	}
+
+	// Returns the number of written bytes. 0 means EOF.
+	return countWrite;
+}
+
+static off_t seek_callback(void *udata, off_t offset, int whence)
+{
+	DecoderFile *file = (DecoderFile *) udata;
+
+	switch (whence)
+	{
+	case SEEK_SET:
+		// Negative values are invalid at this point.
+		if (offset < 0)
+			return -1;
+
+		// Prevents the offset from going over EOF.
+		if (file->size > (size_t) offset)
+			file->offset = offset;
+		else
+			file->offset = file->size;
+		break;
+	case SEEK_END:
+		// Offset is set to EOF. Offset calculation is just like SEEK_CUR.
+		file->offset = file->size;
+	case SEEK_CUR:
+		// Prevents the offset from going over EOF or below 0.
+		if (offset > 0)
+		{
+			if (file->size > file->offset + (size_t) offset)
+				file->offset = file->offset + offset;
+			else
+				file->offset = file->size;
+		}
+		else if (offset < 0)
+		{
+			if (file->offset >= (size_t) (-offset))
+				file->offset = file->offset - (size_t) (-offset);
+			else
+				file->offset = 0;
+		}
+		break;
+	default:
+		return -1;
+	};
+	
+	return file->offset;
+}
+
+static void cleanup_callback(void *)
+{
+	// Cleanup is done by the Decoder class.
+}
+
 bool Mpg123Decoder::inited = false;
 
 Mpg123Decoder::Mpg123Decoder(Data *data, const std::string &ext, int bufferSize)
 	: Decoder(data, ext, bufferSize)
+	, decoder_file(data)
 	, handle(0)
 	, channels(MPG123_STEREO)
+	, duration(-2.0)
 {
-
-	data_size = data->getSize();
-	data_offset = 0;
-
-	int ret;
+	int ret = 0;
 
 	if (!inited)
 	{
@@ -52,18 +122,46 @@ Mpg123Decoder::Mpg123Decoder(Data *data, const std::string &ext, int bufferSize)
 		inited = (ret == MPG123_OK);
 	}
 
-	//Intialize the handle
-	handle = mpg123_new(NULL, &ret);
-	if (handle == NULL)
-		throw love::Exception("Could not create handle.");
-	ret = mpg123_open_feed(handle);
-	if (ret != MPG123_OK)
-		throw love::Exception("Could not open feed.");
+	// Intialize the handle.
+	handle = mpg123_new(nullptr, nullptr);
+	if (handle == nullptr)
+		throw love::Exception("Could not create decoder.");
+
+	// Suppressing all mpg123 messages.
+	mpg123_param(handle, MPG123_ADD_FLAGS, MPG123_QUIET, 0);
 
-	ret = feed(16384);
+	try
+	{
+		ret = mpg123_replace_reader_handle(handle, &read_callback, &seek_callback, &cleanup_callback);
+		if (ret != MPG123_OK)
+			throw love::Exception("Could not set decoder callbacks.");
+
+		ret = mpg123_open_handle(handle, &decoder_file);
+		if (ret != MPG123_OK)
+			throw love::Exception("Could not open decoder.");
 
-	if (ret != MPG123_OK && ret != MPG123_DONE)
-		throw love::Exception("Could not feed!");
+		// mpg123_getformat should be able to tell us the properties of the stream's first frame.
+		long rate = 0;
+		ret = mpg123_getformat(handle, &rate, &channels, nullptr);
+		if (ret == MPG123_ERR)
+			throw love::Exception("Could not get stream information.");
+
+		// I forgot what this was about.
+		if (channels == 0)
+			channels = 2;
+
+		// Force signed 16-bit output.
+		mpg123_param(handle, MPG123_FLAGS, (channels == 2 ? MPG123_FORCE_STEREO : MPG123_MONO_MIX), 0);
+		mpg123_format_none(handle);
+		mpg123_format(handle, rate, channels, MPG123_ENC_SIGNED_16);
+
+		sampleRate = rate;
+	}
+	catch (love::Exception &)
+	{
+		mpg123_delete(handle);
+		throw;
+	}
 }
 
 Mpg123Decoder::~Mpg123Decoder()
@@ -102,64 +200,24 @@ love::sound::Decoder *Mpg123Decoder::clone()
 int Mpg123Decoder::decode()
 {
 	int size = 0;
-	bool done = false;
 
-	while (size < bufferSize && !done && !eof)
+	while (size < bufferSize && !eof)
 	{
 		size_t numbytes = 0;
+		int res = mpg123_read(handle, (unsigned char *) buffer + size, bufferSize - size, &numbytes);
 
-		int r = mpg123_read(handle, (unsigned char *) buffer + size, bufferSize - size, &numbytes);
-
-		switch (r)
+		switch (res)
 		{
-		case MPG123_NEW_FORMAT:
-		{
-			size += numbytes;
-			long rate = 0;
-			int encoding = 0;
-			mpg123_getformat(handle, &rate, &channels, &encoding);
-			if (rate == 0)
-				rate = sampleRate;
-			else
-				sampleRate = rate;
-			if (channels == 0)
-				channels = MPG123_STEREO;
-			if (encoding == 0)
-				encoding = MPG123_ENC_SIGNED_16;
-			int ret = mpg123_format(handle, rate, channels, encoding);
-			if (ret != MPG123_OK)
-				throw love::Exception("Could not set output format.");
-		}
-		continue;
 		case MPG123_NEED_MORE:
-		{
-			size += numbytes;
-			int v = feed(8192);
-
-			switch (v)
-			{
-			case MPG123_OK:
-				continue;
-			case MPG123_DONE:
-				if (numbytes == 0)
-					eof = true;
-				break;
-			default:
-				done = true;
-			}
-
-			continue;
-		}
+		case MPG123_NEW_FORMAT:
 		case MPG123_OK:
 			size += numbytes;
 			continue;
 		case MPG123_DONE:
-			// Apparently, mpg123_read does not return MPG123_DONE, but
-			// let's keep it here anyway.
+			size += numbytes;
 			eof = true;
 		default:
-			done = true;
-			break;
+			return size;
 		}
 	}
 
@@ -168,14 +226,13 @@ int Mpg123Decoder::decode()
 
 bool Mpg123Decoder::seek(float s)
 {
-	off_t offset = static_cast<off_t>(s * static_cast<float>(sampleRate));
+	off_t offset = (off_t) (s * (double) sampleRate);
 
 	if (offset < 0)
 		return false;
 
-	if (mpg123_feedseek(handle, offset, SEEK_SET, &offset) >= 0)
+	if (mpg123_seek(handle, offset, SEEK_SET) >= 0)
 	{
-		data_offset = offset;
 		eof = false;
 		return true;
 	}
@@ -186,13 +243,9 @@ bool Mpg123Decoder::seek(float s)
 bool Mpg123Decoder::rewind()
 {
 	eof = false;
-	off_t offset;
 
-	if (mpg123_feedseek(handle, 0, SEEK_SET, &offset) >= 0)
-	{
-		data_offset = offset;
+	if (mpg123_seek(handle, 0, SEEK_SET) >= 0)
 		return true;
-	}
 	else
 		return false;
 }
@@ -212,21 +265,22 @@ int Mpg123Decoder::getBitDepth() const
 	return 16;
 }
 
-int Mpg123Decoder::feed(int bytes)
+double Mpg123Decoder::getDuration()
 {
-	int remaining = data_size - data_offset;
-
-	if (remaining <= 0)
-		return MPG123_DONE;
-
-	int feed_bytes = remaining < bytes ? remaining : bytes;
+	// Only calculate the duration if we haven't done so already.
+	if (duration == -2.0)
+	{
+		mpg123_scan(handle);
 
-	int r = mpg123_feed(handle, (unsigned char *)data->getData() + data_offset, feed_bytes);
+		off_t length = mpg123_length(handle);
 
-	if (r == MPG123_OK || r == MPG123_DONE)
-		data_offset += feed_bytes;
+		if (length == MPG123_ERR || length < 0)
+			duration = -1.0;
+		else
+			duration = (double) length / (double) sampleRate;
+	}
 
-	return r;
+	return duration;
 }
 
 } // lullaby

+ 17 - 4
src/modules/sound/lullaby/Mpg123Decoder.h

@@ -39,6 +39,19 @@ namespace sound
 namespace lullaby
 {
 
+struct DecoderFile
+{
+	unsigned char *data;
+	size_t size;
+	size_t offset;
+
+	DecoderFile(Data *d)
+		: data((unsigned char *) d->getData())
+		, size(d->getSize())
+		, offset(0)
+	{}
+};
+
 class Mpg123Decoder : public Decoder
 {
 public:
@@ -56,19 +69,19 @@ public:
 	bool isSeekable();
 	int getChannels() const;
 	int getBitDepth() const;
+	double getDuration();
 
 private:
 
-	int feed(int bytes);
+	DecoderFile decoder_file;
 
 	mpg123_handle *handle;
 	static bool inited;
 
-	int data_offset;
-	int data_size;
-
 	int channels;
 
+	double duration;
+
 }; // Decoder
 
 } // lullaby

+ 1 - 1
src/modules/sound/lullaby/Sound.cpp

@@ -66,7 +66,7 @@ sound::Decoder *Sound::newDecoder(love::filesystem::FileData *data, int bufferSi
 	std::string ext = data->getExtension();
 	std::transform(ext.begin(), ext.end(), ext.begin(), tolower);
 
-	sound::Decoder *decoder = 0;
+	sound::Decoder *decoder = nullptr;
 
 	// Find a suitable decoder here, and return it.
 	if (false)

+ 15 - 0
src/modules/sound/lullaby/VorbisDecoder.cpp

@@ -130,6 +130,7 @@ static long vorbisTell(void *datasource	/* ptr to the data that the vorbis files
 
 VorbisDecoder::VorbisDecoder(Data *data, const std::string &ext, int bufferSize)
 	: Decoder(data, ext, bufferSize)
+	, duration(-2.0)
 {
 	// Initialize callbacks
 	vorbisCallbacks.close_func = vorbisClose;
@@ -264,6 +265,20 @@ int VorbisDecoder::getSampleRate() const
 	return (int) vorbisInfo->rate;
 }
 
+double VorbisDecoder::getDuration()
+{
+	// Only calculate the duration if we haven't done so already.
+	if (duration == -2.0)
+	{
+		duration = ov_time_total(&handle, -1);
+
+		if (duration == OV_EINVAL || duration < 0.0)
+			duration = -1.0;
+	}
+
+	return duration;
+}
+
 } // lullaby
 } // sound
 } // love

+ 2 - 0
src/modules/sound/lullaby/VorbisDecoder.h

@@ -62,6 +62,7 @@ public:
 	int getChannels() const;
 	int getBitDepth() const;
 	int getSampleRate() const;
+	double getDuration();
 
 private:
 	SOggFile oggFile;				// (see struct)
@@ -70,6 +71,7 @@ private:
 	vorbis_info *vorbisInfo;		// Info
 	vorbis_comment *vorbisComment;	// Comments
 	int endian;						// Endianness
+	double duration;
 }; // VorbisDecoder
 
 } // lullaby

+ 5 - 0
src/modules/sound/lullaby/WaveDecoder.cpp

@@ -189,6 +189,11 @@ int WaveDecoder::getSampleRate() const
 	return info.sample_rate;
 }
 
+double WaveDecoder::getDuration()
+{
+	return (double) info.length / (double) info.sample_rate;
+}
+
 } // lullaby
 } // sound
 } // love

+ 2 - 1
src/modules/sound/lullaby/WaveDecoder.h

@@ -59,11 +59,12 @@ public:
 	int getChannels() const;
 	int getBitDepth() const;
 	int getSampleRate() const;
+	double getDuration();
 
 private:
 
 	WaveFile dataFile;
-	wuff_handle * handle;
+	wuff_handle *handle;
 	wuff_info info;
 
 }; // WaveDecoder

+ 8 - 0
src/modules/sound/wrap_Decoder.cpp

@@ -51,11 +51,19 @@ int w_Decoder_getSampleRate(lua_State *L)
 	return 1;
 }
 
+int w_Decoder_getDuration(lua_State *L)
+{
+	Decoder *t = luax_checkdecoder(L, 1);
+	lua_pushnumber(L, t->getDuration());
+	return 1;
+}
+
 static const luaL_Reg functions[] =
 {
 	{ "getChannels", w_Decoder_getChannels },
 	{ "getBitDepth", w_Decoder_getBitDepth },
 	{ "getSampleRate", w_Decoder_getSampleRate },
+	{ "getDuration", w_Decoder_getDuration },
 	{ 0, 0 }
 };
 

+ 1 - 0
src/modules/sound/wrap_Decoder.h

@@ -34,6 +34,7 @@ Decoder *luax_checkdecoder(lua_State *L, int idx);
 int w_Decoder_getChannels(lua_State *L);
 int w_Decoder_getBitDepth(lua_State *L);
 int w_Decoder_getSampleRate(lua_State *L);
+int w_Decoder_getDuration(lua_State *L);
 extern "C" int luaopen_decoder(lua_State *L);
 
 } // sound