]> granicus.if.org Git - taglib/commitdiff
MPEG: AudioProperties improvements
authorTsuda Kageyu <tsuda.kageyu@gmail.com>
Thu, 21 May 2015 04:59:32 +0000 (13:59 +0900)
committerTsuda Kageyu <tsuda.kageyu@gmail.com>
Thu, 18 Jun 2015 08:14:04 +0000 (17:14 +0900)
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.

taglib/mpeg/mpegheader.cpp
taglib/mpeg/mpegheader.h
taglib/mpeg/mpegproperties.cpp
taglib/mpeg/mpegproperties.h
taglib/mpeg/xingheader.cpp
taglib/mpeg/xingheader.h
tests/data/bladeenc.mp3 [new file with mode: 0644]
tests/data/lame_cbr.mp3 [new file with mode: 0644]
tests/data/lame_vbr.mp3 [new file with mode: 0644]
tests/data/vbri.mp3 [new file with mode: 0644]
tests/test_mpeg.cpp

index a582f7589fe51141bfdc6fd6b5df0953da0c432c..0c9a0f1c928039d64208e77517bf7954102c1b1e 100644 (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;
index 020ebd060302cd7ec4883aa94f5c4243b074bc00..a55cac0996e1f60eb0dc7680353ab64664e5e93d 100644 (file)
@@ -140,7 +140,7 @@ namespace TagLib {
       bool isOriginal() const;
 
       /*!
-       * Returns the frame length.
+       * Returns the frame length in bytes.
        */
       int frameLength() const;
 
index 3af956643b3476aa324a3b4a05e7342cf015587d..1e8bae5bd66c7394ed3ec491e3c6b7804fd8ff36 100644 (file)
 #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()) {
-
-    long pos = last;
-
-    while(pos > first) {
-
-      pos = d->file->previousFrameOffset(pos);
-
-      if(pos < 0)
-        break;
+  file->seek(first);
+  const Header firstHeader(file->readBlock(4));
 
-      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();
 }
index 72e594ff5b83104e8d150d9227119515e6f29463..f11fad112444cf713928c2cd0c6e340acdfe41ab 100644 (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;
-      virtual int channels() const;
 
       /*!
-       * Returns a pointer to the XingHeader if one exists or null if no
-       * XingHeader was found.
+       * Returns the number of audio channels.
        */
+      virtual int channels() const;
 
+      /*!
+       * 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;
index 9e20127ee8d9c0915d5bede4e052bdf0cbe5fb62..9fae49349221216f4676256f10fc7a71b649c3b8 100644 (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;
+    }
 
-  if(!(data[7] & 0x02)) {
-    debug("MPEG::XingHeader::parse() -- Xing header doesn't contain the total stream size.");
-    return;
+    d->frames = data.toUInt(offset + 8,  true);
+    d->size   = data.toUInt(offset + 12, true);
+    d->type   = Xing;
   }
+  else {
+
+    // Xing header not found. Then look for a VBRI header.
 
-  d->frames = data.toUInt(8U);
-  d->size   = data.toUInt(12U);
+    offset = data.find("VBRI");
 
-  d->valid = true;
+    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;
+    }
+  }
 }
index ffe7494d5524ad0a519ab1a0aaeb04f8303b413c..cd417157b9b1844cf75f2c5728c46c169a5a1eae 100644 (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);
 
diff --git a/tests/data/bladeenc.mp3 b/tests/data/bladeenc.mp3
new file mode 100644 (file)
index 0000000..e3d1a4b
Binary files /dev/null and b/tests/data/bladeenc.mp3 differ
diff --git a/tests/data/lame_cbr.mp3 b/tests/data/lame_cbr.mp3
new file mode 100644 (file)
index 0000000..b7badeb
Binary files /dev/null and b/tests/data/lame_cbr.mp3 differ
diff --git a/tests/data/lame_vbr.mp3 b/tests/data/lame_vbr.mp3
new file mode 100644 (file)
index 0000000..643056e
Binary files /dev/null and b/tests/data/lame_vbr.mp3 differ
diff --git a/tests/data/vbri.mp3 b/tests/data/vbri.mp3
new file mode 100644 (file)
index 0000000..ea14d61
Binary files /dev/null and b/tests/data/vbri.mp3 differ
index d00653656682adc5cb64ddcd9b3d89df69628e6f..77bb22481c84824e2ff7ff547de57b4cabaf4e9e 100644 (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()