MPEG: AudioProperties improvements

Add lengthInSeconds(), lengthInMilliseconds() properties. (#503)
Support VBRI header in addition to Xing. (#136)
Fix MPEG frame seeker functions. (maybe #190)
Calculate MPEG frame length accurately.
Remove some data members which are not needed to carry.
Add some tests for audio properties.
Add some supplementary comments.
This commit is contained in:
Tsuda Kageyu 2015-05-21 13:59:32 +09:00
parent 447a4739c5
commit 9ec6d28239
11 changed files with 293 additions and 152 deletions

View File

@ -213,8 +213,8 @@ void MPEG::Header::parse(const ByteVector &data)
}
};
const int versionIndex = d->version == Version1 ? 0 : 1;
const int layerIndex = d->layer > 0 ? d->layer - 1 : 0;
const int versionIndex = (d->version == Version1) ? 0 : 1;
const int layerIndex = (d->layer > 0) ? d->layer - 1 : 0;
// The bitrate index is encoded as the first 4 bits of the 3rd byte,
// i.e. 1111xxxx
@ -253,13 +253,6 @@ void MPEG::Header::parse(const ByteVector &data)
d->isCopyrighted = flags[3];
d->isPadded = flags[9];
// Calculate the frame length
if(d->layer == 1)
d->frameLength = 24000 * 2 * d->bitrate / d->sampleRate + int(d->isPadded);
else
d->frameLength = 72000 * d->bitrate / d->sampleRate + int(d->isPadded);
// Samples per frame
static const int samplesPerFrame[3][2] = {
@ -271,6 +264,15 @@ void MPEG::Header::parse(const ByteVector &data)
d->samplesPerFrame = samplesPerFrame[layerIndex][versionIndex];
// Calculate the frame length
static const int paddingSize[3] = { 4, 1, 1 };
d->frameLength = d->samplesPerFrame * d->bitrate * 125 / d->sampleRate;
if(d->isPadded)
d->frameLength += paddingSize[layerIndex];
// Now that we're done parsing, set this to be a valid frame.
d->isValid = true;

View File

@ -140,7 +140,7 @@ namespace TagLib {
bool isOriginal() const;
/*!
* Returns the frame length.
* Returns the frame length in bytes.
*/
int frameLength() const;

View File

@ -29,16 +29,18 @@
#include "mpegproperties.h"
#include "mpegfile.h"
#include "xingheader.h"
#include "id3v2tag.h"
#include "id3v2header.h"
#include "apetag.h"
#include "apefooter.h"
using namespace TagLib;
class MPEG::Properties::PropertiesPrivate
{
public:
PropertiesPrivate(File *f, ReadStyle s) :
file(f),
PropertiesPrivate() :
xingHeader(0),
style(s),
length(0),
bitrate(0),
sampleRate(0),
@ -55,9 +57,7 @@ public:
delete xingHeader;
}
File *file;
XingHeader *xingHeader;
ReadStyle style;
int length;
int bitrate;
int sampleRate;
@ -74,12 +74,11 @@ public:
// public members
////////////////////////////////////////////////////////////////////////////////
MPEG::Properties::Properties(File *file, ReadStyle style) : AudioProperties(style)
MPEG::Properties::Properties(File *file, ReadStyle style) :
AudioProperties(style),
d(new PropertiesPrivate())
{
d = new PropertiesPrivate(file, style);
if(file && file->isOpen())
read();
read(file);
}
MPEG::Properties::~Properties()
@ -88,6 +87,16 @@ MPEG::Properties::~Properties()
}
int MPEG::Properties::length() const
{
return lengthInSeconds();
}
int MPEG::Properties::lengthInSeconds() const
{
return d->length / 1000;
}
int MPEG::Properties::lengthInMilliseconds() const
{
return d->length;
}
@ -146,109 +155,73 @@ bool MPEG::Properties::isOriginal() const
// private members
////////////////////////////////////////////////////////////////////////////////
void MPEG::Properties::read()
void MPEG::Properties::read(File *file)
{
// Since we've likely just looked for the ID3v1 tag, start at the end of the
// file where we're least likely to have to have to move the disk head.
long last = d->file->lastFrameOffset();
if(last < 0) {
debug("MPEG::Properties::read() -- Could not find a valid last MPEG frame in the stream.");
return;
}
d->file->seek(last);
Header lastHeader(d->file->readBlock(4));
long first = d->file->firstFrameOffset();
// Only the first frame is required if we have a VBR header.
const long first = file->firstFrameOffset();
if(first < 0) {
debug("MPEG::Properties::read() -- Could not find a valid first MPEG frame in the stream.");
return;
}
if(!lastHeader.isValid()) {
file->seek(first);
const Header firstHeader(file->readBlock(4));
long pos = last;
while(pos > first) {
pos = d->file->previousFrameOffset(pos);
if(pos < 0)
break;
d->file->seek(pos);
Header header(d->file->readBlock(4));
if(header.isValid()) {
lastHeader = header;
last = pos;
break;
}
}
}
// Now jump back to the front of the file and read what we need from there.
d->file->seek(first);
Header firstHeader(d->file->readBlock(4));
if(!firstHeader.isValid() || !lastHeader.isValid()) {
debug("MPEG::Properties::read() -- Page headers were invalid.");
if(!firstHeader.isValid()) {
debug("MPEG::Properties::read() -- The first page header is invalid.");
return;
}
// Check for a Xing header that will help us in gathering information about a
// Check for a VBR header that will help us in gathering information about a
// VBR stream.
int xingHeaderOffset = MPEG::XingHeader::xingHeaderOffset(firstHeader.version(),
firstHeader.channelMode());
d->file->seek(first + xingHeaderOffset);
d->xingHeader = new XingHeader(d->file->readBlock(16));
// Read the length and the bitrate from the Xing header.
file->seek(first + 4);
d->xingHeader = new XingHeader(file->readBlock(firstHeader.frameLength() - 4));
if(d->xingHeader->isValid() &&
firstHeader.sampleRate() > 0 &&
d->xingHeader->totalFrames() > 0)
{
double timePerFrame =
double(firstHeader.samplesPerFrame()) / firstHeader.sampleRate();
firstHeader.samplesPerFrame() > 0 &&
firstHeader.sampleRate() > 0) {
double length = timePerFrame * d->xingHeader->totalFrames();
// Read the length and the bitrate from the VBR header.
d->length = int(length);
d->bitrate = d->length > 0 ? (int)(d->xingHeader->totalSize() * 8 / length / 1000) : 0;
const double timePerFrame = firstHeader.samplesPerFrame() * 1000.0 / firstHeader.sampleRate();
const double length = timePerFrame * d->xingHeader->totalFrames();
d->length = static_cast<int>(length + 0.5);
d->bitrate = static_cast<int>(d->xingHeader->totalSize() * 8.0 / length + 0.5);
}
else {
// Since there was no valid Xing header found, we hope that we're in a constant
// bitrate file.
else if(firstHeader.bitrate() > 0) {
delete d->xingHeader;
d->xingHeader = 0;
// Since there was no valid VBR header found, we hope that we're in a constant
// bitrate file.
// TODO: Make this more robust with audio property detection for VBR without a
// Xing header.
if(firstHeader.frameLength() > 0 && firstHeader.bitrate() > 0) {
int frames = (last - first) / firstHeader.frameLength() + 1;
d->bitrate = firstHeader.bitrate();
d->length = int(float(firstHeader.frameLength() * frames) /
float(firstHeader.bitrate() * 125) + 0.5);
d->bitrate = firstHeader.bitrate();
}
long long streamLength = file->length();
if(file->hasID3v1Tag())
streamLength -= 128;
if(file->hasID3v2Tag())
streamLength -= file->ID3v2Tag()->header()->completeTagSize();
if(file->hasAPETag())
streamLength -= file->APETag()->footer()->completeTagSize();
if(streamLength > 0)
d->length = static_cast<int>(streamLength * 8.0 / d->bitrate + 0.5);
}
d->sampleRate = firstHeader.sampleRate();
d->channels = firstHeader.channelMode() == Header::SingleChannel ? 1 : 2;
d->version = firstHeader.version();
d->layer = firstHeader.layer();
d->sampleRate = firstHeader.sampleRate();
d->channels = firstHeader.channelMode() == Header::SingleChannel ? 1 : 2;
d->version = firstHeader.version();
d->layer = firstHeader.layer();
d->protectionEnabled = firstHeader.protectionEnabled();
d->channelMode = firstHeader.channelMode();
d->isCopyrighted = firstHeader.isCopyrighted();
d->isOriginal = firstHeader.isOriginal();
d->channelMode = firstHeader.channelMode();
d->isCopyrighted = firstHeader.isCopyrighted();
d->isOriginal = firstHeader.isOriginal();
}

View File

@ -59,18 +59,52 @@ namespace TagLib {
*/
virtual ~Properties();
// Reimplementations.
/*!
* Returns the length of the file in seconds. The length is rounded down to
* the nearest whole second.
*
* \note This method is just an alias of lengthInSeconds().
*
* \deprecated
*/
virtual int length() const;
/*!
* Returns the length of the file in seconds. The length is rounded down to
* the nearest whole second.
*
* \see lengthInMilliseconds()
*/
// BIC: make virtual
int lengthInSeconds() const;
/*!
* Returns the length of the file in milliseconds.
*
* \see lengthInSeconds()
*/
// BIC: make virtual
int lengthInMilliseconds() const;
/*!
* Returns the average bit rate of the file in kb/s.
*/
virtual int bitrate() const;
/*!
* Returns the sample rate in Hz.
*/
virtual int sampleRate() const;
/*!
* Returns the number of audio channels.
*/
virtual int channels() const;
/*!
* Returns a pointer to the XingHeader if one exists or null if no
* XingHeader was found.
* Returns a pointer to the Xing/VBRI header if one exists or null if no
* Xing/VBRI header was found.
*/
const XingHeader *xingHeader() const;
/*!
@ -107,7 +141,7 @@ namespace TagLib {
Properties(const Properties &);
Properties &operator=(const Properties &);
void read();
void read(File *file);
class PropertiesPrivate;
PropertiesPrivate *d;

View File

@ -28,6 +28,7 @@
#include <tdebug.h>
#include "xingheader.h"
#include "mpegfile.h"
using namespace TagLib;
@ -37,17 +38,21 @@ public:
XingHeaderPrivate() :
frames(0),
size(0),
valid(false)
{}
type(MPEG::XingHeader::Invalid) {}
uint frames;
uint size;
bool valid;
MPEG::XingHeader::HeaderType type;
};
MPEG::XingHeader::XingHeader(const ByteVector &data)
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////
MPEG::XingHeader::XingHeader(const ByteVector &data) :
d(new XingHeaderPrivate())
{
d = new XingHeaderPrivate;
parse(data);
}
@ -58,7 +63,7 @@ MPEG::XingHeader::~XingHeader()
bool MPEG::XingHeader::isValid() const
{
return d->valid;
return (d->type != Invalid && d->frames > 0 && d->size > 0);
}
TagLib::uint MPEG::XingHeader::totalFrames() const
@ -71,45 +76,65 @@ TagLib::uint MPEG::XingHeader::totalSize() const
return d->size;
}
int MPEG::XingHeader::xingHeaderOffset(TagLib::MPEG::Header::Version v,
TagLib::MPEG::Header::ChannelMode c)
MPEG::XingHeader::HeaderType MPEG::XingHeader::type() const
{
if(v == MPEG::Header::Version1) {
if(c == MPEG::Header::SingleChannel)
return 0x15;
else
return 0x24;
}
else {
if(c == MPEG::Header::SingleChannel)
return 0x0D;
else
return 0x15;
}
return d->type;
}
int MPEG::XingHeader::xingHeaderOffset(TagLib::MPEG::Header::Version /*v*/,
TagLib::MPEG::Header::ChannelMode /*c*/)
{
return 0;
}
////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////
void MPEG::XingHeader::parse(const ByteVector &data)
{
// Check to see if a valid Xing header is available.
// Look for a Xing header.
if(!data.startsWith("Xing") && !data.startsWith("Info"))
return;
long offset = data.find("Xing");
if(offset < 0)
offset = data.find("Info");
// If the XingHeader doesn't contain the number of frames and the total stream
// info it's invalid.
if(offset >= 0) {
if(!(data[7] & 0x01)) {
debug("MPEG::XingHeader::parse() -- Xing header doesn't contain the total number of frames.");
return;
// Xing header found.
if(data.size() < offset + 16) {
debug("MPEG::XingHeader::parse() -- Xing header found but too short.");
return;
}
if((data[offset + 7] & 0x03) != 0x03) {
debug("MPEG::XingHeader::parse() -- Xing header doesn't contain the required information.");
return;
}
d->frames = data.toUInt(offset + 8, true);
d->size = data.toUInt(offset + 12, true);
d->type = Xing;
}
else {
if(!(data[7] & 0x02)) {
debug("MPEG::XingHeader::parse() -- Xing header doesn't contain the total stream size.");
return;
// Xing header not found. Then look for a VBRI header.
offset = data.find("VBRI");
if(offset >= 0) {
// VBRI header found.
if(data.size() < offset + 32) {
debug("MPEG::XingHeader::parse() -- VBRI header found but too short.");
return;
}
d->frames = data.toUInt(offset + 14, true);
d->size = data.toUInt(offset + 10, true);
d->type = VBRI;
}
}
d->frames = data.toUInt(8U);
d->size = data.toUInt(12U);
d->valid = true;
}

View File

@ -35,24 +35,47 @@ namespace TagLib {
namespace MPEG {
//! An implementation of the Xing VBR headers
class File;
//! An implementation of the Xing/VBRI headers
/*!
* This is a minimalistic implementation of the Xing VBR headers. Xing
* headers are often added to VBR (variable bit rate) MP3 streams to make it
* easy to compute the length and quality of a VBR stream. Our implementation
* is only concerned with the total size of the stream (so that we can
* calculate the total playing time and the average bitrate). It uses
* <a href="http://home.pcisys.net/~melanson/codecs/mp3extensions.txt">this text</a>
* and the XMMS sources as references.
* This is a minimalistic implementation of the Xing/VBRI VBR headers.
* Xing/VBRI headers are often added to VBR (variable bit rate) MP3 streams
* to make it easy to compute the length and quality of a VBR stream. Our
* implementation is only concerned with the total size of the stream (so
* that we can calculate the total playing time and the average bitrate).
* It uses <a href="http://home.pcisys.net/~melanson/codecs/mp3extensions.txt">
* this text</a> and the XMMS sources as references.
*/
class TAGLIB_EXPORT XingHeader
{
public:
/*!
* Parses a Xing header based on \a data. The data must be at least 16
* bytes long (anything longer than this is discarded).
* The type of the VBR header.
*/
enum HeaderType
{
/*!
* Invalid header or no VBR header found.
*/
Invalid = 0,
/*!
* Xing header.
*/
Xing = 1,
/*!
* VBRI header.
*/
VBRI = 2,
};
/*!
* Parses an Xing/VBRI header based on \a data which contains the entire
* first MPEG frame.
*/
XingHeader(const ByteVector &data);
@ -63,7 +86,7 @@ namespace TagLib {
/*!
* Returns true if the data was parsed properly and if there is a valid
* Xing header present.
* Xing/VBRI header present.
*/
bool isValid() const;
@ -77,11 +100,17 @@ namespace TagLib {
*/
uint totalSize() const;
/*!
* Returns the type of the VBR header.
*/
HeaderType type() const;
/*!
* Returns the offset for the start of this Xing header, given the
* version and channels of the frame
*
* \deprecated Always returns 0.
*/
// BIC: rename to offset()
static int xingHeaderOffset(TagLib::MPEG::Header::Version v,
TagLib::MPEG::Header::ChannelMode c);

BIN
tests/data/bladeenc.mp3 Normal file

Binary file not shown.

BIN
tests/data/lame_cbr.mp3 Normal file

Binary file not shown.

BIN
tests/data/lame_vbr.mp3 Normal file

Binary file not shown.

BIN
tests/data/vbri.mp3 Normal file

Binary file not shown.

View File

@ -3,6 +3,9 @@
#include <tstring.h>
#include <mpegfile.h>
#include <id3v2tag.h>
#include <mpegproperties.h>
#include <xingheader.h>
#include <mpegheader.h>
#include <cppunit/extensions/HelperMacros.h>
#include "utils.h"
@ -12,6 +15,10 @@ using namespace TagLib;
class TestMPEG : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(TestMPEG);
CPPUNIT_TEST(testAudioPropertiesXingHeaderCBR);
CPPUNIT_TEST(testAudioPropertiesXingHeaderVBR);
CPPUNIT_TEST(testAudioPropertiesVBRIHeader);
CPPUNIT_TEST(testAudioPropertiesNoVBRHeaders);
CPPUNIT_TEST(testVersion2DurationWithXingHeader);
CPPUNIT_TEST(testSaveID3v24);
CPPUNIT_TEST(testSaveID3v24WrongParam);
@ -23,10 +30,81 @@ class TestMPEG : public CppUnit::TestFixture
public:
void testAudioPropertiesXingHeaderCBR()
{
MPEG::File f(TEST_FILE_PATH_C("lame_cbr.mp3"));
CPPUNIT_ASSERT(f.audioProperties());
CPPUNIT_ASSERT_EQUAL(1887, f.audioProperties()->length());
CPPUNIT_ASSERT_EQUAL(1887, f.audioProperties()->lengthInSeconds());
CPPUNIT_ASSERT_EQUAL(1887164, f.audioProperties()->lengthInMilliseconds());
CPPUNIT_ASSERT_EQUAL(64, f.audioProperties()->bitrate());
CPPUNIT_ASSERT_EQUAL(1, f.audioProperties()->channels());
CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate());
CPPUNIT_ASSERT_EQUAL(MPEG::XingHeader::Xing, f.audioProperties()->xingHeader()->type());
}
void testAudioPropertiesXingHeaderVBR()
{
MPEG::File f(TEST_FILE_PATH_C("lame_vbr.mp3"));
CPPUNIT_ASSERT(f.audioProperties());
CPPUNIT_ASSERT_EQUAL(1887, f.audioProperties()->length());
CPPUNIT_ASSERT_EQUAL(1887, f.audioProperties()->lengthInSeconds());
CPPUNIT_ASSERT_EQUAL(1887164, f.audioProperties()->lengthInMilliseconds());
CPPUNIT_ASSERT_EQUAL(70, f.audioProperties()->bitrate());
CPPUNIT_ASSERT_EQUAL(1, f.audioProperties()->channels());
CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate());
CPPUNIT_ASSERT_EQUAL(MPEG::XingHeader::Xing, f.audioProperties()->xingHeader()->type());
}
void testAudioPropertiesVBRIHeader()
{
MPEG::File f(TEST_FILE_PATH_C("vbri.mp3"));
CPPUNIT_ASSERT(f.audioProperties());
CPPUNIT_ASSERT_EQUAL(222, f.audioProperties()->length());
CPPUNIT_ASSERT_EQUAL(222, f.audioProperties()->lengthInSeconds());
CPPUNIT_ASSERT_EQUAL(222198, f.audioProperties()->lengthInMilliseconds());
CPPUNIT_ASSERT_EQUAL(233, f.audioProperties()->bitrate());
CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels());
CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate());
CPPUNIT_ASSERT_EQUAL(MPEG::XingHeader::VBRI, f.audioProperties()->xingHeader()->type());
}
void testAudioPropertiesNoVBRHeaders()
{
MPEG::File f(TEST_FILE_PATH_C("bladeenc.mp3"));
CPPUNIT_ASSERT(f.audioProperties());
CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->length());
CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->lengthInSeconds());
CPPUNIT_ASSERT_EQUAL(3553, f.audioProperties()->lengthInMilliseconds());
CPPUNIT_ASSERT_EQUAL(64, f.audioProperties()->bitrate());
CPPUNIT_ASSERT_EQUAL(1, f.audioProperties()->channels());
CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate());
CPPUNIT_ASSERT(!f.audioProperties()->xingHeader()->isValid());
long last = f.lastFrameOffset();
f.seek(last);
MPEG::Header lastHeader(f.readBlock(4));
while (!lastHeader.isValid()) {
last = f.previousFrameOffset(last);
f.seek(last);
lastHeader = MPEG::Header(f.readBlock(4));
}
CPPUNIT_ASSERT_EQUAL(28213L, last);
CPPUNIT_ASSERT_EQUAL(209, lastHeader.frameLength());
}
void testVersion2DurationWithXingHeader()
{
MPEG::File f(TEST_FILE_PATH_C("mpeg2.mp3"));
CPPUNIT_ASSERT(f.audioProperties());
CPPUNIT_ASSERT_EQUAL(5387, f.audioProperties()->length());
CPPUNIT_ASSERT_EQUAL(5387, f.audioProperties()->lengthInSeconds());
CPPUNIT_ASSERT_EQUAL(5387285, f.audioProperties()->lengthInMilliseconds());
}
void testSaveID3v24()