Download
237 lines 5.9 KiBC++Modified 2026-01-21 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
/**
 * Copyright (c) 2006-2026 LOVE Development Team
 *
 * This software is provided 'as-is', without any express or implied
 * warranty.  In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 3. This notice may not be removed or altered from any source distribution.
 **/

#define DR_MP3_IMPLEMENTATION
#define DR_MP3_NO_STDIO
#include "MP3Decoder.h"
#include "common/Exception.h"

namespace love
{
namespace sound
{
namespace lullaby
{

// dr_mp3 looks too far, but it can lead to false-positive. dr_mp3 also doesn't recognize ID3.
// Implement our own "MP3 detection" heuristics that also help dr_mp3 skip ID3 tags.
static int64 findFirstValidHeader(Stream* stream)
{
	// Tweaking this variable has trade-off between false-positive and false-negative. Lesser value
	// means lesser false-positive and false-negative. Larger value means more false-positive AND
	// false-negative.
	constexpr size_t LOOKUP_SIZE = 128;

	std::vector<uint8> data(LOOKUP_SIZE);
	uint8 header[10];
	uint8 *dataPtr = data.data();
	int64 buffer = 0;
	int64 offset = 0;

	if (stream->read(header, 10) < 10)
		return -1;

	// Test for known audio formats which are definitely not MP3.
	if (memcmp(header, "RIFF", 4) == 0)
		return -1;
	if (memcmp(header, "OggS", 4) == 0)
		return -1;
	if (memcmp(header, "fLaC", 4) == 0)
		return -1;

	if (memcmp(header, "TAG", 3) == 0)
	{
		// ID3v1 tag is always 128 bytes long
		if (!stream->seek(128, Stream::SEEKORIGIN_BEGIN))
			return -1;

		buffer = stream->read(dataPtr, LOOKUP_SIZE);
		offset = 128;
	}
	else if (memcmp(header, "ID3", 3) == 0)
	{
		// ID3v2 tag header is 10 bytes long, but we're
		// only interested on how much we should skip.
		int64 off =
			header[9] |
			((int64) header[8] << 7) |
			((int64) header[7] << 14) |
			((int64) header[6] << 21);

		if (!stream->seek(off, Stream::SEEKORIGIN_CURRENT))
			return -1;

		buffer = stream->read(dataPtr, LOOKUP_SIZE);
		offset = off + 10;
	}
	else
	{
		// Copy the rest to data buffer
		memcpy(dataPtr, header, 10);
		buffer = 10 + stream->read(dataPtr + 10, LOOKUP_SIZE - 10);
	}

	// Look for mp3 data
	for (int i = 0; i < buffer - 4; i++, offset++)
	{
		if (drmp3_hdr_valid(dataPtr++))
		{
			stream->seek(offset, Stream::SEEKORIGIN_BEGIN);
			return offset;
		}
	}

	// No valid MP3 frame found in first few bytes of the data.
	return -1;
}

size_t MP3Decoder::onRead(void *pUserData, void *pBufferOut, size_t bytesToRead)
{
	auto decoder = (MP3Decoder *) pUserData;
	int64 read = decoder->stream->read(pBufferOut, bytesToRead);
	return std::max<int64>(0, read);
}

drmp3_bool32 MP3Decoder::onSeek(void *pUserData, int offset, drmp3_seek_origin origin)
{
	auto decoder = (MP3Decoder *) pUserData;
	int64 pos = decoder->offset;

	// Due to possible offsets, we have to calculate the position ourself.
	switch (origin)
	{
	case drmp3_seek_origin_start:
		pos += offset;
		break;
	case drmp3_seek_origin_current:
		pos = decoder->stream->tell() + offset;
		break;
	default:
		return DRMP3_FALSE;
	}

	if (pos < decoder->offset)
		return DRMP3_FALSE;

	return decoder->stream->seek(pos, Stream::SEEKORIGIN_BEGIN) ? DRMP3_TRUE : DRMP3_FALSE;
}

MP3Decoder::MP3Decoder(Stream *stream, int bufferSize)
: Decoder(stream, bufferSize)
{
	// Check for possible ID3 tag and skip it if necessary.
	offset = findFirstValidHeader(stream);
	if (offset == -1)
		throw love::Exception("Could not find first valid mp3 header.");

	// initialize mp3 handle
	if (!drmp3_init(&mp3, onRead, onSeek, this, nullptr))
		throw love::Exception("Could not read mp3 data.");

	sampleRate = mp3.sampleRate;

	// calculate duration
	drmp3_uint64 pcmCount, mp3FrameCount;
	if (!drmp3_get_mp3_and_pcm_frame_count(&mp3, &mp3FrameCount, &pcmCount))
	{
		drmp3_uninit(&mp3);
		throw love::Exception("Could not calculate mp3 duration.");
	}
	duration = ((double) pcmCount) / ((double) mp3.sampleRate);

	// create seek table
	drmp3_uint32 mp3FrameInt = (drmp3_uint32) mp3FrameCount;
	seekTable.resize((size_t) mp3FrameCount, {0ULL, 0ULL, 0, 0});
	if (!drmp3_calculate_seek_points(&mp3, &mp3FrameInt, seekTable.data()))
	{
		drmp3_uninit(&mp3);
		throw love::Exception("Could not calculate mp3 seek table");
	}

	// bind seek table
	if (!drmp3_bind_seek_table(&mp3, mp3FrameInt, seekTable.data()))
	{
		drmp3_uninit(&mp3);
		throw love::Exception("Could not bind mp3 seek table");
	}
}

MP3Decoder::~MP3Decoder()
{
	drmp3_uninit(&mp3);
}

love::sound::Decoder *MP3Decoder::clone()
{
	StrongRef<Stream> s(stream->clone(), Acquire::NORETAIN);
	return new MP3Decoder(s, bufferSize);
}

int MP3Decoder::decode()
{
	// bufferSize is in char
	int maxRead = bufferSize / sizeof(int16_t) / mp3.channels;
	int read = (int) drmp3_read_pcm_frames_s16(&mp3, maxRead, (drmp3_int16 *) buffer);

	if (read < maxRead)
		eof = true;

	return read * sizeof(int16_t) * mp3.channels;
}

bool MP3Decoder::seek(double s)
{
	drmp3_uint64 targetSample = (drmp3_uint64) (s * mp3.sampleRate);
	drmp3_bool32 success = drmp3_seek_to_pcm_frame(&mp3, targetSample);

	if (success)
		eof = false;

	return success;
}

bool MP3Decoder::rewind()
{
	return seek(0.0);
}

bool MP3Decoder::isSeekable()
{
	return true;
}

int MP3Decoder::getChannelCount() const
{
	return mp3.channels;
}

int MP3Decoder::getBitDepth() const
{
	return 16;
}

double MP3Decoder::getDuration()
{
	return duration;
}

} // lullaby
} // sound
} // love