summaryrefslogtreecommitdiffstats
path: root/dom/media/mp3/MP3FrameParser.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/media/mp3/MP3FrameParser.cpp
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media/mp3/MP3FrameParser.cpp')
-rw-r--r--dom/media/mp3/MP3FrameParser.cpp818
1 files changed, 818 insertions, 0 deletions
diff --git a/dom/media/mp3/MP3FrameParser.cpp b/dom/media/mp3/MP3FrameParser.cpp
new file mode 100644
index 0000000000..33f76de9e1
--- /dev/null
+++ b/dom/media/mp3/MP3FrameParser.cpp
@@ -0,0 +1,818 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MP3FrameParser.h"
+
+#include <algorithm>
+#include <inttypes.h>
+
+#include "TimeUnits.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Try.h"
+#include "VideoUtils.h"
+
+extern mozilla::LazyLogModule gMediaDemuxerLog;
+#define MP3LOG(msg, ...) \
+ MOZ_LOG(gMediaDemuxerLog, LogLevel::Debug, ("MP3Demuxer " msg, ##__VA_ARGS__))
+#define MP3LOGV(msg, ...) \
+ MOZ_LOG(gMediaDemuxerLog, LogLevel::Verbose, \
+ ("MP3Demuxer " msg, ##__VA_ARGS__))
+
+namespace mozilla {
+
+// FrameParser
+
+namespace frame_header {
+// FrameHeader mRaw byte offsets.
+static const int SYNC1 = 0;
+static const int SYNC2_VERSION_LAYER_PROTECTION = 1;
+static const int BITRATE_SAMPLERATE_PADDING_PRIVATE = 2;
+static const int CHANNELMODE_MODEEXT_COPY_ORIG_EMPH = 3;
+} // namespace frame_header
+
+FrameParser::FrameParser() = default;
+
+void FrameParser::Reset() {
+ mID3Parser.Reset();
+ mFrame.Reset();
+}
+
+void FrameParser::ResetFrameData() {
+ mFrame.Reset();
+ mFirstFrame.Reset();
+ mPrevFrame.Reset();
+}
+
+void FrameParser::EndFrameSession() {
+ if (!mID3Parser.Header().IsValid()) {
+ // Reset ID3 tags only if we have not parsed a valid ID3 header yet.
+ mID3Parser.Reset();
+ }
+ mPrevFrame = mFrame;
+ mFrame.Reset();
+}
+
+const FrameParser::Frame& FrameParser::CurrentFrame() const { return mFrame; }
+
+const FrameParser::Frame& FrameParser::PrevFrame() const { return mPrevFrame; }
+
+const FrameParser::Frame& FrameParser::FirstFrame() const {
+ return mFirstFrame;
+}
+
+const ID3Parser::ID3Header& FrameParser::ID3Header() const {
+ return mID3Parser.Header();
+}
+
+uint32_t FrameParser::TotalID3HeaderSize() const {
+ uint32_t ID3v1Size = 0;
+ if (mID3v1MetadataFound) {
+ ID3v1Size = 128;
+ }
+ return ID3v1Size + mID3Parser.TotalHeadersSize();
+}
+
+const FrameParser::VBRHeader& FrameParser::VBRInfo() const {
+ return mVBRHeader;
+}
+
+Result<bool, nsresult> FrameParser::Parse(BufferReader* aReader,
+ uint32_t* aBytesToSkip) {
+ MOZ_ASSERT(aReader && aBytesToSkip);
+ *aBytesToSkip = 0;
+
+ if (ID3Parser::IsBufferStartingWithID3v1Tag(aReader)) {
+ // This is usually at the end of the file, and is always 128 bytes, that
+ // can simply be skipped.
+ aReader->Read(128);
+ *aBytesToSkip = 128;
+ mID3v1MetadataFound = true;
+ MP3LOGV("ID3v1 tag detected, skipping 128 bytes past the current buffer");
+ return false;
+ }
+
+ if (ID3Parser::IsBufferStartingWithID3Tag(aReader) && !mFirstFrame.Length()) {
+ // No MP3 frames have been parsed yet, look for ID3v2 headers at file begin.
+ // ID3v1 tags may only be at file end.
+ const size_t prevReaderOffset = aReader->Offset();
+ const uint32_t tagSize = mID3Parser.Parse(aReader);
+ if (!!tagSize) {
+ // ID3 tag found, skip past it.
+ const uint32_t skipSize = tagSize - ID3Parser::ID3Header::SIZE;
+
+ if (skipSize > aReader->Remaining()) {
+ // Skipping across the ID3v2 tag would take us past the end of the
+ // buffer, therefore we return immediately and let the calling function
+ // handle skipping the rest of the tag.
+ MP3LOGV(
+ "ID3v2 tag detected, size=%d,"
+ " needing to skip %zu bytes past the current buffer",
+ tagSize, skipSize - aReader->Remaining());
+ *aBytesToSkip = skipSize - aReader->Remaining();
+ return false;
+ }
+ MP3LOGV("ID3v2 tag detected, size=%d", tagSize);
+ aReader->Read(skipSize);
+ } else {
+ // No ID3v2 tag found, rewinding reader in order to search for a MPEG
+ // frame header.
+ aReader->Seek(prevReaderOffset);
+ }
+ }
+
+ for (auto res = aReader->ReadU8();
+ res.isOk() && !mFrame.ParseNext(res.unwrap()); res = aReader->ReadU8()) {
+ }
+
+ if (mFrame.Length()) {
+ // MP3 frame found.
+ if (!mFirstFrame.Length()) {
+ mFirstFrame = mFrame;
+ }
+ // Indicate success.
+ return true;
+ }
+ return false;
+}
+
+// FrameParser::Header
+
+FrameParser::FrameHeader::FrameHeader() { Reset(); }
+
+uint8_t FrameParser::FrameHeader::Sync1() const {
+ return mRaw[frame_header::SYNC1];
+}
+
+uint8_t FrameParser::FrameHeader::Sync2() const {
+ return 0x7 & mRaw[frame_header::SYNC2_VERSION_LAYER_PROTECTION] >> 5;
+}
+
+uint8_t FrameParser::FrameHeader::RawVersion() const {
+ return 0x3 & mRaw[frame_header::SYNC2_VERSION_LAYER_PROTECTION] >> 3;
+}
+
+uint8_t FrameParser::FrameHeader::RawLayer() const {
+ return 0x3 & mRaw[frame_header::SYNC2_VERSION_LAYER_PROTECTION] >> 1;
+}
+
+uint8_t FrameParser::FrameHeader::RawProtection() const {
+ return 0x1 & mRaw[frame_header::SYNC2_VERSION_LAYER_PROTECTION] >> 6;
+}
+
+uint8_t FrameParser::FrameHeader::RawBitrate() const {
+ return 0xF & mRaw[frame_header::BITRATE_SAMPLERATE_PADDING_PRIVATE] >> 4;
+}
+
+uint8_t FrameParser::FrameHeader::RawSampleRate() const {
+ return 0x3 & mRaw[frame_header::BITRATE_SAMPLERATE_PADDING_PRIVATE] >> 2;
+}
+
+uint8_t FrameParser::FrameHeader::Padding() const {
+ return 0x1 & mRaw[frame_header::BITRATE_SAMPLERATE_PADDING_PRIVATE] >> 1;
+}
+
+uint8_t FrameParser::FrameHeader::Private() const {
+ return 0x1 & mRaw[frame_header::BITRATE_SAMPLERATE_PADDING_PRIVATE];
+}
+
+uint8_t FrameParser::FrameHeader::RawChannelMode() const {
+ return 0x3 & mRaw[frame_header::CHANNELMODE_MODEEXT_COPY_ORIG_EMPH] >> 6;
+}
+
+uint32_t FrameParser::FrameHeader::Layer() const {
+ static const uint8_t LAYERS[4] = {0, 3, 2, 1};
+
+ return LAYERS[RawLayer()];
+}
+
+uint32_t FrameParser::FrameHeader::SampleRate() const {
+ // Sample rates - use [version][srate]
+ static const uint16_t SAMPLE_RATE[4][4] = {
+ // clang-format off
+ { 11025, 12000, 8000, 0 }, // MPEG 2.5
+ { 0, 0, 0, 0 }, // Reserved
+ { 22050, 24000, 16000, 0 }, // MPEG 2
+ { 44100, 48000, 32000, 0 } // MPEG 1
+ // clang-format on
+ };
+
+ return SAMPLE_RATE[RawVersion()][RawSampleRate()];
+}
+
+uint32_t FrameParser::FrameHeader::Channels() const {
+ // 3 is single channel (mono), any other value is some variant of dual
+ // channel.
+ return RawChannelMode() == 3 ? 1 : 2;
+}
+
+uint32_t FrameParser::FrameHeader::SamplesPerFrame() const {
+ // Samples per frame - use [version][layer]
+ static const uint16_t FRAME_SAMPLE[4][4] = {
+ // clang-format off
+ // Layer 3 2 1 Version
+ { 0, 576, 1152, 384 }, // 2.5
+ { 0, 0, 0, 0 }, // Reserved
+ { 0, 576, 1152, 384 }, // 2
+ { 0, 1152, 1152, 384 } // 1
+ // clang-format on
+ };
+
+ return FRAME_SAMPLE[RawVersion()][RawLayer()];
+}
+
+uint32_t FrameParser::FrameHeader::Bitrate() const {
+ // Bitrates - use [version][layer][bitrate]
+ static const uint16_t BITRATE[4][4][16] = {
+ // clang-format off
+ { // Version 2.5
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Reserved
+ { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 }, // Layer 3
+ { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 }, // Layer 2
+ { 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0 } // Layer 1
+ },
+ { // Reserved
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Invalid
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Invalid
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Invalid
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } // Invalid
+ },
+ { // Version 2
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Reserved
+ { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 }, // Layer 3
+ { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 }, // Layer 2
+ { 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0 } // Layer 1
+ },
+ { // Version 1
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Reserved
+ { 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0 }, // Layer 3
+ { 0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0 }, // Layer 2
+ { 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0 }, // Layer 1
+ }
+ // clang-format on
+ };
+
+ return 1000 * BITRATE[RawVersion()][RawLayer()][RawBitrate()];
+}
+
+uint32_t FrameParser::FrameHeader::SlotSize() const {
+ // Slot size (MPEG unit of measurement) - use [layer]
+ static const uint8_t SLOT_SIZE[4] = {0, 1, 1, 4}; // Rsvd, 3, 2, 1
+
+ return SLOT_SIZE[RawLayer()];
+}
+
+bool FrameParser::FrameHeader::ParseNext(uint8_t c) {
+ if (!Update(c)) {
+ Reset();
+ if (!Update(c)) {
+ Reset();
+ }
+ }
+ return IsValid();
+}
+
+bool FrameParser::ID3v1MetadataFound() const { return mID3v1MetadataFound; }
+
+bool FrameParser::FrameHeader::IsValid(int aPos) const {
+ if (aPos >= SIZE) {
+ return true;
+ }
+ if (aPos == frame_header::SYNC1) {
+ return Sync1() == 0xFF;
+ }
+ if (aPos == frame_header::SYNC2_VERSION_LAYER_PROTECTION) {
+ return Sync2() == 7 && RawVersion() != 1 && Layer() == 3;
+ }
+ if (aPos == frame_header::BITRATE_SAMPLERATE_PADDING_PRIVATE) {
+ return RawBitrate() != 0xF && RawBitrate() != 0 && RawSampleRate() != 3;
+ }
+ return true;
+}
+
+bool FrameParser::FrameHeader::IsValid() const { return mPos >= SIZE; }
+
+void FrameParser::FrameHeader::Reset() { mPos = 0; }
+
+bool FrameParser::FrameHeader::Update(uint8_t c) {
+ if (mPos < SIZE) {
+ mRaw[mPos] = c;
+ }
+ return IsValid(mPos++);
+}
+
+// FrameParser::VBRHeader
+
+namespace vbr_header {
+static const char* TYPE_STR[3] = {"NONE", "XING", "VBRI"};
+static const uint32_t TOC_SIZE = 100;
+} // namespace vbr_header
+
+FrameParser::VBRHeader::VBRHeader() : mType(NONE) {}
+
+FrameParser::VBRHeader::VBRHeaderType FrameParser::VBRHeader::Type() const {
+ return mType;
+}
+
+const Maybe<uint32_t>& FrameParser::VBRHeader::NumAudioFrames() const {
+ return mNumAudioFrames;
+}
+
+const Maybe<uint32_t>& FrameParser::VBRHeader::NumBytes() const {
+ return mNumBytes;
+}
+
+const Maybe<uint32_t>& FrameParser::VBRHeader::Scale() const { return mScale; }
+
+bool FrameParser::VBRHeader::IsTOCPresent() const {
+ // This doesn't use VBRI TOC
+ return !mTOC.empty() && mType != VBRI;
+}
+
+bool FrameParser::VBRHeader::IsValid() const { return mType != NONE; }
+
+bool FrameParser::VBRHeader::IsComplete() const {
+ return IsValid() && mNumAudioFrames.valueOr(0) > 0 && mNumBytes.valueOr(0) > 0
+ // We don't care about the scale for any computations here.
+ // && mScale < 101
+ ;
+}
+
+int64_t FrameParser::VBRHeader::Offset(media::TimeUnit aTime,
+ media::TimeUnit aDuration) const {
+ if (!IsTOCPresent()) {
+ return -1;
+ }
+
+ int64_t offset = -1;
+ if (mType == XING) {
+ // Constrain the duration percentage to [0, 99].
+ double percent = 100. * aTime.ToSeconds() / aDuration.ToSeconds();
+ const double durationPer = std::clamp(percent, 0., 99.);
+ double integer;
+ const double fractional = modf(durationPer, &integer);
+ size_t integerPer = AssertedCast<size_t>(integer);
+
+ MOZ_ASSERT(integerPer < mTOC.size());
+ offset = mTOC.at(integerPer);
+ if (fractional > 0.0 && integerPer + 1 < mTOC.size()) {
+ offset += AssertedCast<int64_t>(fractional) *
+ (mTOC.at(integerPer + 1) - offset);
+ }
+ }
+ // TODO: VBRI TOC seeking
+ MP3LOG("VBRHeader::Offset (%s): %f is at byte %" PRId64 "",
+ mType == XING ? "XING" : "VBRI", aTime.ToSeconds(), offset);
+
+ return offset;
+}
+
+Result<bool, nsresult> FrameParser::VBRHeader::ParseXing(BufferReader* aReader,
+ size_t aFrameSize) {
+ static const uint32_t XING_TAG = BigEndian::readUint32("Xing");
+ static const uint32_t INFO_TAG = BigEndian::readUint32("Info");
+ static const uint32_t LAME_TAG = BigEndian::readUint32("LAME");
+ static const uint32_t LAVC_TAG = BigEndian::readUint32("Lavc");
+
+ enum Flags {
+ NUM_FRAMES = 0x01,
+ NUM_BYTES = 0x02,
+ TOC = 0x04,
+ VBR_SCALE = 0x08
+ };
+
+ MOZ_ASSERT(aReader);
+
+ // Seek backward to the original position before leaving this scope.
+ const size_t prevReaderOffset = aReader->Offset();
+ auto scopeExit = MakeScopeExit([&] { aReader->Seek(prevReaderOffset); });
+
+ // We have to search for the Xing header as its position can change.
+ for (auto res = aReader->PeekU32();
+ res.isOk() && res.unwrap() != XING_TAG && res.unwrap() != INFO_TAG;) {
+ aReader->Read(1);
+ res = aReader->PeekU32();
+ }
+
+ // Skip across the VBR header ID tag.
+ MOZ_TRY(aReader->ReadU32());
+ mType = XING;
+
+ uint32_t flags;
+ MOZ_TRY_VAR(flags, aReader->ReadU32());
+
+ if (flags & NUM_FRAMES) {
+ uint32_t frames;
+ MOZ_TRY_VAR(frames, aReader->ReadU32());
+ mNumAudioFrames = Some(frames);
+ }
+ if (flags & NUM_BYTES) {
+ uint32_t bytes;
+ MOZ_TRY_VAR(bytes, aReader->ReadU32());
+ mNumBytes = Some(bytes);
+ }
+ if (flags & TOC && aReader->Remaining() >= vbr_header::TOC_SIZE) {
+ if (!mNumBytes) {
+ // We don't have the stream size to calculate offsets, skip the TOC.
+ aReader->Read(vbr_header::TOC_SIZE);
+ } else {
+ mTOC.clear();
+ mTOC.reserve(vbr_header::TOC_SIZE);
+ uint8_t data;
+ for (size_t i = 0; i < vbr_header::TOC_SIZE; ++i) {
+ MOZ_TRY_VAR(data, aReader->ReadU8());
+ mTOC.push_back(
+ AssertedCast<uint32_t>(1.0f / 256.0f * data * mNumBytes.value()));
+ }
+ }
+ }
+ if (flags & VBR_SCALE) {
+ uint32_t scale;
+ MOZ_TRY_VAR(scale, aReader->ReadU32());
+ mScale = Some(scale);
+ }
+
+ uint32_t lameOrLavcTag;
+ MOZ_TRY_VAR(lameOrLavcTag, aReader->ReadU32());
+
+ if (lameOrLavcTag == LAME_TAG || lameOrLavcTag == LAVC_TAG) {
+ // Skip 17 bytes after the LAME tag:
+ // - http://gabriel.mp3-tech.org/mp3infotag.html
+ // - 5 bytes for the encoder short version string
+ // - 1 byte for the info tag revision + VBR method
+ // - 1 byte for the lowpass filter value
+ // - 8 bytes for the ReplayGain information
+ // - 1 byte for the encoding flags + ATH Type
+ // - 1 byte for the specified bitrate if ABR, else the minimal bitrate
+ if (!aReader->Read(17)) {
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+
+ // The encoder delay is three bytes, for two 12-bits integers are the
+ // encoder delay and the padding.
+ const uint8_t* delayPadding = aReader->Read(3);
+ if (!delayPadding) {
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ mEncoderDelay =
+ uint32_t(delayPadding[0]) << 4 | (delayPadding[1] & 0xf0) >> 4;
+ mEncoderPadding = uint32_t(delayPadding[1] & 0x0f) << 8 | delayPadding[2];
+
+ constexpr uint16_t DEFAULT_DECODER_DELAY = 529;
+ mEncoderDelay += DEFAULT_DECODER_DELAY + aFrameSize; // ignore first frame.
+ mEncoderPadding -= std::min(mEncoderPadding, DEFAULT_DECODER_DELAY);
+
+ MP3LOG("VBRHeader::ParseXing: LAME encoder delay section: delay: %" PRIu16
+ " frames, padding: %" PRIu16 " frames",
+ mEncoderDelay, mEncoderPadding);
+ }
+
+ return mType == XING;
+}
+
+template <typename T>
+int readAndConvertToInt(BufferReader* aReader) {
+ int value = AssertedCast<int>(aReader->ReadType<T>());
+ return value;
+}
+
+Result<bool, nsresult> FrameParser::VBRHeader::ParseVBRI(
+ BufferReader* aReader) {
+ static const uint32_t TAG = BigEndian::readUint32("VBRI");
+ static const uint32_t OFFSET = 32 + FrameParser::FrameHeader::SIZE;
+ static const uint32_t MIN_FRAME_SIZE = OFFSET + 26;
+
+ MOZ_ASSERT(aReader);
+ // ParseVBRI assumes that the ByteReader offset points to the beginning of a
+ // frame, therefore as a simple check, we look for the presence of a frame
+ // sync at that position.
+ auto sync = aReader->PeekU16();
+ if (sync.isOk()) { // To avoid compiler complains 'set but unused'.
+ MOZ_ASSERT((sync.unwrap() & 0xFFE0) == 0xFFE0);
+ }
+
+ // Seek backward to the original position before leaving this scope.
+ const size_t prevReaderOffset = aReader->Offset();
+ auto scopeExit = MakeScopeExit([&] { aReader->Seek(prevReaderOffset); });
+
+ // VBRI have a fixed relative position, so let's check for it there.
+ if (aReader->Remaining() > MIN_FRAME_SIZE) {
+ aReader->Seek(prevReaderOffset + OFFSET);
+ uint32_t tag;
+ MOZ_TRY_VAR(tag, aReader->ReadU32());
+ if (tag == TAG) {
+ uint16_t vbriEncoderVersion, vbriEncoderDelay, vbriQuality;
+ uint32_t vbriBytes, vbriFrames;
+ uint16_t vbriSeekOffsetsTableSize, vbriSeekOffsetsScaleFactor,
+ vbriSeekOffsetsBytesPerEntry, vbriSeekOffsetsFramesPerEntry;
+ MOZ_TRY_VAR(vbriEncoderVersion, aReader->ReadU16());
+ MOZ_TRY_VAR(vbriEncoderDelay, aReader->ReadU16());
+ MOZ_TRY_VAR(vbriQuality, aReader->ReadU16());
+ MOZ_TRY_VAR(vbriBytes, aReader->ReadU32());
+ MOZ_TRY_VAR(vbriFrames, aReader->ReadU32());
+ MOZ_TRY_VAR(vbriSeekOffsetsTableSize, aReader->ReadU16());
+ MOZ_TRY_VAR(vbriSeekOffsetsScaleFactor, aReader->ReadU32());
+ MOZ_TRY_VAR(vbriSeekOffsetsBytesPerEntry, aReader->ReadU16());
+ MOZ_TRY_VAR(vbriSeekOffsetsFramesPerEntry, aReader->ReadU16());
+
+ mTOC.reserve(vbriSeekOffsetsTableSize + 1);
+
+ int (*readFunc)(BufferReader*) = nullptr;
+ switch (vbriSeekOffsetsBytesPerEntry) {
+ case 1:
+ readFunc = &readAndConvertToInt<uint8_t>;
+ break;
+ case 2:
+ readFunc = &readAndConvertToInt<int16_t>;
+ break;
+ case 4:
+ readFunc = &readAndConvertToInt<int32_t>;
+ break;
+ case 8:
+ readFunc = &readAndConvertToInt<int64_t>;
+ break;
+ default:
+ MP3LOG("Unhandled vbriSeekOffsetsBytesPerEntry size of %hd",
+ vbriSeekOffsetsBytesPerEntry);
+ break;
+ }
+ for (uint32_t i = 0; readFunc && i < vbriSeekOffsetsTableSize; i++) {
+ int entry = readFunc(aReader);
+ mTOC.push_back(entry * vbriSeekOffsetsScaleFactor);
+ }
+ MP3LOG(
+ "Header::Parse found valid header: EncoderVersion=%hu "
+ "EncoderDelay=%hu "
+ "Quality=%hu "
+ "Bytes=%u "
+ "Frames=%u "
+ "SeekOffsetsTableSize=%u "
+ "SeekOffsetsScaleFactor=%hu "
+ "SeekOffsetsBytesPerEntry=%hu "
+ "SeekOffsetsFramesPerEntry=%hu",
+ vbriEncoderVersion, vbriEncoderDelay, vbriQuality, vbriBytes,
+ vbriFrames, vbriSeekOffsetsTableSize, vbriSeekOffsetsScaleFactor,
+ vbriSeekOffsetsBytesPerEntry, vbriSeekOffsetsFramesPerEntry);
+ // Adjust the number of frames so it's counted the same way as in the XING
+ // header
+ if (vbriFrames < 1) {
+ return false;
+ }
+ mNumAudioFrames = Some(vbriFrames - 1);
+ mNumBytes = Some(vbriBytes);
+ mEncoderDelay = vbriEncoderDelay;
+ mVBRISeekOffsetsFramesPerEntry = vbriSeekOffsetsFramesPerEntry;
+ MP3LOG("TOC:");
+ for (auto entry : mTOC) {
+ MP3LOG("%" PRId64, entry);
+ }
+
+ mType = VBRI;
+ return true;
+ }
+ }
+ return false;
+}
+
+bool FrameParser::VBRHeader::Parse(BufferReader* aReader, size_t aFrameSize) {
+ auto res = std::make_pair(ParseVBRI(aReader), ParseXing(aReader, aFrameSize));
+ const bool rv = (res.first.isOk() && res.first.unwrap()) ||
+ (res.second.isOk() && res.second.unwrap());
+ if (rv) {
+ MP3LOG(
+ "VBRHeader::Parse found valid VBR/CBR header: type=%s"
+ " NumAudioFrames=%u NumBytes=%u Scale=%u TOC-size=%zu Delay=%u",
+ vbr_header::TYPE_STR[Type()], NumAudioFrames().valueOr(0),
+ NumBytes().valueOr(0), Scale().valueOr(0), mTOC.size(), mEncoderDelay);
+ }
+ return rv;
+}
+
+// FrameParser::Frame
+
+void FrameParser::Frame::Reset() { mHeader.Reset(); }
+
+uint32_t FrameParser::Frame::Length() const {
+ if (!mHeader.IsValid() || !mHeader.SampleRate()) {
+ return 0;
+ }
+
+ const uint32_t bitsPerSample = mHeader.SamplesPerFrame() / 8;
+ const uint32_t frameLen =
+ bitsPerSample * mHeader.Bitrate() / mHeader.SampleRate() +
+ mHeader.Padding() * mHeader.SlotSize();
+ return frameLen;
+}
+
+bool FrameParser::Frame::ParseNext(uint8_t c) { return mHeader.ParseNext(c); }
+
+const FrameParser::FrameHeader& FrameParser::Frame::Header() const {
+ return mHeader;
+}
+
+bool FrameParser::ParseVBRHeader(BufferReader* aReader) {
+ return mVBRHeader.Parse(aReader, CurrentFrame().Header().SamplesPerFrame());
+}
+
+// ID3Parser
+
+// Constants
+namespace id3_header {
+static const int ID_LEN = 3;
+static const int VERSION_LEN = 2;
+static const int FLAGS_LEN = 1;
+static const int SIZE_LEN = 4;
+
+static const int ID_END = ID_LEN;
+static const int VERSION_END = ID_END + VERSION_LEN;
+static const int FLAGS_END = VERSION_END + FLAGS_LEN;
+static const int SIZE_END = FLAGS_END + SIZE_LEN;
+
+static const uint8_t ID[ID_LEN] = {'I', 'D', '3'};
+static const uint8_t IDv1[ID_LEN] = {'T', 'A', 'G'};
+
+static const uint8_t MIN_MAJOR_VER = 2;
+static const uint8_t MAX_MAJOR_VER = 4;
+} // namespace id3_header
+
+bool ID3Parser::IsBufferStartingWithID3v1Tag(BufferReader* aReader) {
+ mozilla::Result<uint32_t, nsresult> res = aReader->PeekU24();
+ if (res.isErr()) {
+ return false;
+ }
+ // If buffer starts with ID3v1 tag, `rv` would be reverse and its content
+ // should be '3' 'D' 'I' from the lowest bit.
+ uint32_t rv = res.unwrap();
+ for (int idx = id3_header::ID_LEN - 1; idx >= 0; idx--) {
+ if ((rv & 0xff) != id3_header::IDv1[idx]) {
+ return false;
+ }
+ rv = rv >> 8;
+ }
+ return true;
+}
+
+/* static */
+bool ID3Parser::IsBufferStartingWithID3Tag(BufferReader* aReader) {
+ mozilla::Result<uint32_t, nsresult> res = aReader->PeekU24();
+ if (res.isErr()) {
+ return false;
+ }
+ // If buffer starts with ID3v2 tag, `rv` would be reverse and its content
+ // should be '3' 'D' 'I' from the lowest bit.
+ uint32_t rv = res.unwrap();
+ for (int idx = id3_header::ID_LEN - 1; idx >= 0; idx--) {
+ if ((rv & 0xff) != id3_header::ID[idx]) {
+ return false;
+ }
+ rv = rv >> 8;
+ }
+ return true;
+}
+
+uint32_t ID3Parser::Parse(BufferReader* aReader) {
+ MOZ_ASSERT(aReader);
+ MOZ_ASSERT(ID3Parser::IsBufferStartingWithID3Tag(aReader));
+
+ if (!mHeader.HasSizeBeenSet()) {
+ return ParseInternal(aReader);
+ }
+
+ // Encounter another possible ID3 header, if that is valid then we would use
+ // it and save the size of previous one in order to report the size of all ID3
+ // headers together in `TotalHeadersSize()`.
+ ID3Header prevHeader = mHeader;
+ mHeader.Reset();
+ uint32_t size = ParseInternal(aReader);
+ if (!size) {
+ // next ID3 is invalid, so revert the header.
+ mHeader = prevHeader;
+ return size;
+ }
+
+ mFormerID3Size += prevHeader.TotalTagSize();
+ return size;
+}
+
+uint32_t ID3Parser::ParseInternal(BufferReader* aReader) {
+ for (auto res = aReader->ReadU8();
+ res.isOk() && !mHeader.ParseNext(res.unwrap());
+ res = aReader->ReadU8()) {
+ }
+ return mHeader.TotalTagSize();
+}
+
+void ID3Parser::Reset() {
+ mHeader.Reset();
+ mFormerID3Size = 0;
+}
+
+uint32_t ID3Parser::TotalHeadersSize() const {
+ return mHeader.TotalTagSize() + mFormerID3Size;
+}
+
+const ID3Parser::ID3Header& ID3Parser::Header() const { return mHeader; }
+
+// ID3Parser::Header
+
+ID3Parser::ID3Header::ID3Header() { Reset(); }
+
+void ID3Parser::ID3Header::Reset() {
+ mSize.reset();
+ mPos = 0;
+}
+
+uint8_t ID3Parser::ID3Header::MajorVersion() const {
+ return mRaw[id3_header::ID_END];
+}
+
+uint8_t ID3Parser::ID3Header::MinorVersion() const {
+ return mRaw[id3_header::ID_END + 1];
+}
+
+uint8_t ID3Parser::ID3Header::Flags() const {
+ return mRaw[id3_header::FLAGS_END - id3_header::FLAGS_LEN];
+}
+
+uint32_t ID3Parser::ID3Header::Size() const {
+ if (!IsValid() || !mSize) {
+ return 0;
+ }
+ return *mSize;
+}
+
+bool ID3Parser::ID3Header::HasSizeBeenSet() const { return !!mSize; }
+
+uint8_t ID3Parser::ID3Header::FooterSize() const {
+ if (Flags() & (1 << 4)) {
+ return SIZE;
+ }
+ return 0;
+}
+
+uint32_t ID3Parser::ID3Header::TotalTagSize() const {
+ if (IsValid()) {
+ // Header found, return total tag size.
+ return ID3Header::SIZE + Size() + FooterSize();
+ }
+ return 0;
+}
+
+bool ID3Parser::ID3Header::ParseNext(uint8_t c) {
+ if (!Update(c)) {
+ Reset();
+ if (!Update(c)) {
+ Reset();
+ }
+ }
+ return IsValid();
+}
+
+bool ID3Parser::ID3Header::IsValid(int aPos) const {
+ if (aPos >= SIZE) {
+ return true;
+ }
+ const uint8_t c = mRaw[aPos];
+ switch (aPos) {
+ case 0:
+ case 1:
+ case 2:
+ // Expecting "ID3".
+ return id3_header::ID[aPos] == c;
+ case 3:
+ return MajorVersion() >= id3_header::MIN_MAJOR_VER &&
+ MajorVersion() <= id3_header::MAX_MAJOR_VER;
+ case 4:
+ return MinorVersion() < 0xFF;
+ case 5:
+ // Validate flags for supported versions, see bug 949036.
+ return ((0xFF >> MajorVersion()) & c) == 0;
+ case 6:
+ case 7:
+ case 8:
+ case 9:
+ return c < 0x80;
+ }
+ return true;
+}
+
+bool ID3Parser::ID3Header::IsValid() const { return mPos >= SIZE; }
+
+bool ID3Parser::ID3Header::Update(uint8_t c) {
+ if (mPos >= id3_header::SIZE_END - id3_header::SIZE_LEN &&
+ mPos < id3_header::SIZE_END) {
+ uint32_t tmp = mSize.valueOr(0) << 7;
+ mSize = Some(tmp | c);
+ }
+ if (mPos < SIZE) {
+ mRaw[mPos] = c;
+ }
+ return IsValid(mPos++);
+}
+
+} // namespace mozilla