/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.media3.extractor; import android.util.Base64; import androidx.annotation.Nullable; import androidx.media3.common.Format; import androidx.media3.common.Metadata; import androidx.media3.common.Metadata.Entry; import androidx.media3.common.ParserException; import androidx.media3.common.util.Log; import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.extractor.metadata.flac.PictureFrame; import androidx.media3.extractor.metadata.vorbis.VorbisComment; import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** Utility methods for parsing Vorbis streams. */ @UnstableApi public final class VorbisUtil { /** Vorbis comment header. */ public static final class CommentHeader { public final String vendor; public final String[] comments; public final int length; public CommentHeader(String vendor, String[] comments, int length) { this.vendor = vendor; this.comments = comments; this.length = length; } } /** * Vorbis identification header. * *

See the Vorbis * spec/Identification header */ public static final class VorbisIdHeader { /** The {@code vorbis_version} field. */ public final int version; /** The {@code audio_channels} field. */ public final int channels; /** The {@code audio_sample_rate} field. */ public final int sampleRate; /** The {@code bitrate_maximum} field, or {@link Format#NO_VALUE} if not greater than zero. */ public final int bitrateMaximum; /** The {@code bitrate_nominal} field, or {@link Format#NO_VALUE} if not greater than zero. */ public final int bitrateNominal; /** The {@code bitrate_minimum} field, or {@link Format#NO_VALUE} if not greater than zero. */ public final int bitrateMinimum; /** The {@code blocksize_0} field. */ public final int blockSize0; /** The {@code blocksize_1} field. */ public final int blockSize1; /** The {@code framing_flag} field. */ public final boolean framingFlag; /** The raw header data. */ public final byte[] data; /** * @param version See {@link #version}. * @param channels See {@link #channels}. * @param sampleRate See {@link #sampleRate}. * @param bitrateMaximum See {@link #bitrateMaximum}. * @param bitrateNominal See {@link #bitrateNominal}. * @param bitrateMinimum See {@link #bitrateMinimum}. * @param blockSize0 See {@link #version}. * @param blockSize1 See {@link #blockSize1}. * @param framingFlag See {@link #framingFlag}. * @param data See {@link #data}. */ public VorbisIdHeader( int version, int channels, int sampleRate, int bitrateMaximum, int bitrateNominal, int bitrateMinimum, int blockSize0, int blockSize1, boolean framingFlag, byte[] data) { this.version = version; this.channels = channels; this.sampleRate = sampleRate; this.bitrateMaximum = bitrateMaximum; this.bitrateNominal = bitrateNominal; this.bitrateMinimum = bitrateMinimum; this.blockSize0 = blockSize0; this.blockSize1 = blockSize1; this.framingFlag = framingFlag; this.data = data; } } /** Vorbis setup header modes. */ public static final class Mode { public final boolean blockFlag; public final int windowType; public final int transformType; public final int mapping; public Mode(boolean blockFlag, int windowType, int transformType, int mapping) { this.blockFlag = blockFlag; this.windowType = windowType; this.transformType = transformType; this.mapping = mapping; } } private static final String TAG = "VorbisUtil"; /** * Returns the mapping from VORBIS channel layout to the channel layout expected by Android, or * null if the mapping is unchanged. * *

See https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-140001.2.3 and * https://developer.android.com/reference/android/media/AudioFormat#channelMask. */ @Nullable public static int[] getVorbisToAndroidChannelLayoutMapping(int channelCount) { switch (channelCount) { case 3: return new int[] {0, 2, 1}; case 5: return new int[] {0, 2, 1, 3, 4}; case 6: return new int[] {0, 2, 1, 5, 3, 4}; case 7: return new int[] {0, 2, 1, 6, 5, 3, 4}; case 8: return new int[] {0, 2, 1, 7, 5, 6, 3, 4}; default: return null; } } /** * Returns ilog(x), which is the index of the highest set bit in {@code x}. * *

See the Vorbis * spec * * @param x the value of which the ilog should be calculated. * @return ilog(x) */ public static int iLog(int x) { int val = 0; while (x > 0) { val++; x >>>= 1; } return val; } /** * Returns codec-specific data for configuring a media codec for decoding Vorbis. * * @param initializationData The initialization data from the ESDS box. * @return Codec-specific data for configuring a media codec for decoding Vorbis. */ public static ImmutableList parseVorbisCsdFromEsdsInitializationData( byte[] initializationData) { ParsableByteArray buffer = new ParsableByteArray(initializationData); buffer.skipBytes(1); // 0x02 for vorbis audio int identificationHeaderLength = 0; while (buffer.bytesLeft() > 0 && buffer.peekUnsignedByte() == 0xFF) { identificationHeaderLength += 0xFF; buffer.skipBytes(1); } identificationHeaderLength += buffer.readUnsignedByte(); int commentHeaderLength = 0; while (buffer.bytesLeft() > 0 && buffer.peekUnsignedByte() == 0xFF) { commentHeaderLength += 0xFF; buffer.skipBytes(1); } commentHeaderLength += buffer.readUnsignedByte(); // csd-0 is the identification header. byte[] csd0 = new byte[identificationHeaderLength]; int identificationHeaderOffset = buffer.getPosition(); System.arraycopy( /* src= */ initializationData, /* srcPos= */ identificationHeaderOffset, /* dest= */ csd0, /* destPos= */ 0, /* length= */ identificationHeaderLength); // csd-1 is the setup header, which is the remaining data after the identification and comment // headers. int setupHeaderOffset = identificationHeaderOffset + identificationHeaderLength + commentHeaderLength; int setupHeaderLength = initializationData.length - setupHeaderOffset; byte[] csd1 = new byte[setupHeaderLength]; System.arraycopy( /* src= */ initializationData, /* srcPos= */ setupHeaderOffset, /* dest= */ csd1, /* destPos= */ 0, /* length= */ setupHeaderLength); return ImmutableList.of(csd0, csd1); } /** * Reads a Vorbis identification header from {@code headerData}. * *

See the Vorbis * spec/Identification header * * @param headerData a {@link ParsableByteArray} wrapping the header data. * @return a {@link VorbisUtil.VorbisIdHeader} with meta data. * @throws ParserException thrown if invalid capture pattern is detected. */ public static VorbisIdHeader readVorbisIdentificationHeader(ParsableByteArray headerData) throws ParserException { verifyVorbisHeaderCapturePattern(0x01, headerData, false); int version = headerData.readLittleEndianUnsignedIntToInt(); int channels = headerData.readUnsignedByte(); int sampleRate = headerData.readLittleEndianUnsignedIntToInt(); int bitrateMaximum = headerData.readLittleEndianInt(); if (bitrateMaximum <= 0) { bitrateMaximum = Format.NO_VALUE; } int bitrateNominal = headerData.readLittleEndianInt(); if (bitrateNominal <= 0) { bitrateNominal = Format.NO_VALUE; } int bitrateMinimum = headerData.readLittleEndianInt(); if (bitrateMinimum <= 0) { bitrateMinimum = Format.NO_VALUE; } int blockSize = headerData.readUnsignedByte(); int blockSize0 = (int) Math.pow(2, blockSize & 0x0F); int blockSize1 = (int) Math.pow(2, (blockSize & 0xF0) >> 4); boolean framingFlag = (headerData.readUnsignedByte() & 0x01) > 0; // raw data of Vorbis setup header has to be passed to decoder as CSD buffer #1 byte[] data = Arrays.copyOf(headerData.getData(), headerData.limit()); return new VorbisIdHeader( version, channels, sampleRate, bitrateMaximum, bitrateNominal, bitrateMinimum, blockSize0, blockSize1, framingFlag, data); } /** * Reads a Vorbis comment header. * *

See the Vorbis * spec/Comment header * * @param headerData A {@link ParsableByteArray} wrapping the header data. * @return A {@link VorbisUtil.CommentHeader} with all the comments. * @throws ParserException If an error occurs parsing the comment header. */ public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData) throws ParserException { return readVorbisCommentHeader( headerData, /* hasMetadataHeader= */ true, /* hasFramingBit= */ true); } /** * Reads a Vorbis comment header. * *

The data provided may not contain the Vorbis metadata common header and the framing bit. * *

See the Vorbis * spec/Comment header * * @param headerData A {@link ParsableByteArray} wrapping the header data. * @param hasMetadataHeader Whether the {@code headerData} contains a Vorbis metadata common * header preceding the comment header. * @param hasFramingBit Whether the {@code headerData} contains a framing bit. * @return A {@link VorbisUtil.CommentHeader} with all the comments. * @throws ParserException If an error occurs parsing the comment header. */ public static CommentHeader readVorbisCommentHeader( ParsableByteArray headerData, boolean hasMetadataHeader, boolean hasFramingBit) throws ParserException { if (hasMetadataHeader) { verifyVorbisHeaderCapturePattern(/* headerType= */ 0x03, headerData, /* quiet= */ false); } int length = 7; int len = (int) headerData.readLittleEndianUnsignedInt(); length += 4; String vendor = headerData.readString(len); length += vendor.length(); long commentListLen = headerData.readLittleEndianUnsignedInt(); String[] comments = new String[(int) commentListLen]; length += 4; for (int i = 0; i < commentListLen; i++) { len = (int) headerData.readLittleEndianUnsignedInt(); length += 4; comments[i] = headerData.readString(len); length += comments[i].length(); } if (hasFramingBit && (headerData.readUnsignedByte() & 0x01) == 0) { throw ParserException.createForMalformedContainer( "framing bit expected to be set", /* cause= */ null); } length += 1; return new CommentHeader(vendor, comments, length); } /** * Builds a {@link Metadata} instance from a list of Vorbis Comments. * *

METADATA_BLOCK_PICTURE comments will be transformed into {@link PictureFrame} entries. All * others will be transformed into {@link VorbisComment} entries. * * @param vorbisComments The raw input of comments, as a key-value pair KEY=VAL. * @return The fully parsed Metadata instance. Null if no vorbis comments could be parsed. */ @Nullable public static Metadata parseVorbisComments(List vorbisComments) { List metadataEntries = new ArrayList<>(); for (int i = 0; i < vorbisComments.size(); i++) { String vorbisComment = vorbisComments.get(i); String[] keyAndValue = Util.splitAtFirst(vorbisComment, "="); if (keyAndValue.length != 2) { Log.w(TAG, "Failed to parse Vorbis comment: " + vorbisComment); continue; } if (keyAndValue[0].equals("METADATA_BLOCK_PICTURE")) { // This tag is a special cover art tag, outlined by // https://wiki.xiph.org/index.php/VorbisComment#Cover_art. // Decode it from Base64 and transform it into a PictureFrame. try { byte[] decoded = Base64.decode(keyAndValue[1], Base64.DEFAULT); metadataEntries.add(PictureFrame.fromPictureBlock(new ParsableByteArray(decoded))); } catch (RuntimeException e) { Log.w(TAG, "Failed to parse vorbis picture", e); } } else { VorbisComment entry = new VorbisComment(keyAndValue[0], keyAndValue[1]); metadataEntries.add(entry); } } return metadataEntries.isEmpty() ? null : new Metadata(metadataEntries); } /** * Verifies whether the next bytes in {@code header} are a Vorbis header of the given {@code * headerType}. * * @param headerType the type of the header expected. * @param header the alleged header bytes. * @param quiet if {@code true} no exceptions are thrown. Instead {@code false} is returned. * @return Whether the header is a Vorbis header. * @throws ParserException thrown if header type or capture pattern is not as expected. */ public static boolean verifyVorbisHeaderCapturePattern( int headerType, ParsableByteArray header, boolean quiet) throws ParserException { if (header.bytesLeft() < 7) { if (quiet) { return false; } else { throw ParserException.createForMalformedContainer( "too short header: " + header.bytesLeft(), /* cause= */ null); } } if (header.readUnsignedByte() != headerType) { if (quiet) { return false; } else { throw ParserException.createForMalformedContainer( "expected header type " + Integer.toHexString(headerType), /* cause= */ null); } } if (!(header.readUnsignedByte() == 'v' && header.readUnsignedByte() == 'o' && header.readUnsignedByte() == 'r' && header.readUnsignedByte() == 'b' && header.readUnsignedByte() == 'i' && header.readUnsignedByte() == 's')) { if (quiet) { return false; } else { throw ParserException.createForMalformedContainer( "expected characters 'vorbis'", /* cause= */ null); } } return true; } /** * This method reads the modes which are located at the very end of the Vorbis setup header. * That's why we need to partially decode or at least read the entire setup header to know where * to start reading the modes. * *

See the Vorbis * spec/Setup header * * @param headerData a {@link ParsableByteArray} containing setup header data. * @param channels the number of channels. * @return an array of {@link Mode}s. * @throws ParserException thrown if bit stream is invalid. */ public static Mode[] readVorbisModes(ParsableByteArray headerData, int channels) throws ParserException { verifyVorbisHeaderCapturePattern(0x05, headerData, false); int numberOfBooks = headerData.readUnsignedByte() + 1; VorbisBitArray bitArray = new VorbisBitArray(headerData.getData()); bitArray.skipBits(headerData.getPosition() * 8); for (int i = 0; i < numberOfBooks; i++) { skipBook(bitArray); } int timeCount = bitArray.readBits(6) + 1; for (int i = 0; i < timeCount; i++) { if (bitArray.readBits(16) != 0x00) { throw ParserException.createForMalformedContainer( "placeholder of time domain transforms not zeroed out", /* cause= */ null); } } readFloors(bitArray); readResidues(bitArray); readMappings(channels, bitArray); Mode[] modes = readModes(bitArray); if (!bitArray.readBit()) { throw ParserException.createForMalformedContainer( "framing bit after modes not set as expected", /* cause= */ null); } return modes; } private static Mode[] readModes(VorbisBitArray bitArray) { int modeCount = bitArray.readBits(6) + 1; Mode[] modes = new Mode[modeCount]; for (int i = 0; i < modeCount; i++) { boolean blockFlag = bitArray.readBit(); int windowType = bitArray.readBits(16); int transformType = bitArray.readBits(16); int mapping = bitArray.readBits(8); modes[i] = new Mode(blockFlag, windowType, transformType, mapping); } return modes; } private static void readMappings(int channels, VorbisBitArray bitArray) throws ParserException { int mappingsCount = bitArray.readBits(6) + 1; for (int i = 0; i < mappingsCount; i++) { int mappingType = bitArray.readBits(16); if (mappingType != 0) { Log.e(TAG, "mapping type other than 0 not supported: " + mappingType); continue; } int submaps; if (bitArray.readBit()) { submaps = bitArray.readBits(4) + 1; } else { submaps = 1; } int couplingSteps; if (bitArray.readBit()) { couplingSteps = bitArray.readBits(8) + 1; for (int j = 0; j < couplingSteps; j++) { bitArray.skipBits(iLog(channels - 1)); // magnitude bitArray.skipBits(iLog(channels - 1)); // angle } } /*else { couplingSteps = 0; }*/ if (bitArray.readBits(2) != 0x00) { throw ParserException.createForMalformedContainer( "to reserved bits must be zero after mapping coupling steps", /* cause= */ null); } if (submaps > 1) { for (int j = 0; j < channels; j++) { bitArray.skipBits(4); // mappingMux } } for (int j = 0; j < submaps; j++) { bitArray.skipBits(8); // discard bitArray.skipBits(8); // submapFloor bitArray.skipBits(8); // submapResidue } } } private static void readResidues(VorbisBitArray bitArray) throws ParserException { int residueCount = bitArray.readBits(6) + 1; for (int i = 0; i < residueCount; i++) { int residueType = bitArray.readBits(16); if (residueType > 2) { throw ParserException.createForMalformedContainer( "residueType greater than 2 is not decodable", /* cause= */ null); } else { bitArray.skipBits(24); // begin bitArray.skipBits(24); // end bitArray.skipBits(24); // partitionSize (add one) int classifications = bitArray.readBits(6) + 1; bitArray.skipBits(8); // classbook int[] cascade = new int[classifications]; for (int j = 0; j < classifications; j++) { int highBits = 0; int lowBits = bitArray.readBits(3); if (bitArray.readBit()) { highBits = bitArray.readBits(5); } cascade[j] = highBits * 8 + lowBits; } for (int j = 0; j < classifications; j++) { for (int k = 0; k < 8; k++) { if ((cascade[j] & (0x01 << k)) != 0) { bitArray.skipBits(8); // discard } } } } } } private static void readFloors(VorbisBitArray bitArray) throws ParserException { int floorCount = bitArray.readBits(6) + 1; for (int i = 0; i < floorCount; i++) { int floorType = bitArray.readBits(16); switch (floorType) { case 0: bitArray.skipBits(8); // order bitArray.skipBits(16); // rate bitArray.skipBits(16); // barkMapSize bitArray.skipBits(6); // amplitudeBits bitArray.skipBits(8); // amplitudeOffset int floorNumberOfBooks = bitArray.readBits(4) + 1; for (int j = 0; j < floorNumberOfBooks; j++) { bitArray.skipBits(8); } break; case 1: int partitions = bitArray.readBits(5); int maximumClass = -1; int[] partitionClassList = new int[partitions]; for (int j = 0; j < partitions; j++) { partitionClassList[j] = bitArray.readBits(4); if (partitionClassList[j] > maximumClass) { maximumClass = partitionClassList[j]; } } int[] classDimensions = new int[maximumClass + 1]; for (int j = 0; j < classDimensions.length; j++) { classDimensions[j] = bitArray.readBits(3) + 1; int classSubclasses = bitArray.readBits(2); if (classSubclasses > 0) { bitArray.skipBits(8); // classMasterbooks } for (int k = 0; k < (1 << classSubclasses); k++) { bitArray.skipBits(8); // subclassBook (subtract 1) } } bitArray.skipBits(2); // multiplier (add one) int rangeBits = bitArray.readBits(4); int count = 0; for (int j = 0, k = 0; j < partitions; j++) { int idx = partitionClassList[j]; count += classDimensions[idx]; for (; k < count; k++) { bitArray.skipBits(rangeBits); // floorValue } } break; default: throw ParserException.createForMalformedContainer( "floor type greater than 1 not decodable: " + floorType, /* cause= */ null); } } } private static void skipBook(VorbisBitArray bitArray) throws ParserException { if (bitArray.readBits(24) != 0x564342) { throw ParserException.createForMalformedContainer( "expected code book to start with [0x56, 0x43, 0x42] at " + bitArray.getPosition(), /* cause= */ null); } int dimensions = bitArray.readBits(16); int entries = bitArray.readBits(24); boolean isOrdered = bitArray.readBit(); if (!isOrdered) { boolean isSparse = bitArray.readBit(); for (int i = 0; i < entries; i++) { if (isSparse) { if (bitArray.readBit()) { bitArray.skipBits(5); // lengthMap entry } } else { // not sparse bitArray.skipBits(5); // lengthMap entry } } } else { bitArray.skipBits(5); // length for (int i = 0; i < entries; ) { i += bitArray.readBits(iLog(entries - i)); // num } } int lookupType = bitArray.readBits(4); if (lookupType > 2) { throw ParserException.createForMalformedContainer( "lookup type greater than 2 not decodable: " + lookupType, /* cause= */ null); } else if (lookupType == 1 || lookupType == 2) { bitArray.skipBits(32); // minimumValue bitArray.skipBits(32); // deltaValue int valueBits = bitArray.readBits(4) + 1; bitArray.skipBits(1); // sequenceP long lookupValuesCount; if (lookupType == 1) { if (dimensions != 0) { lookupValuesCount = mapType1QuantValues(entries, dimensions); } else { lookupValuesCount = 0; } } else { lookupValuesCount = (long) entries * dimensions; } // discard (no decoding required yet) bitArray.skipBits((int) (lookupValuesCount * valueBits)); } } /** * See the _book_maptype1_quantvals */ private static long mapType1QuantValues(long entries, long dimension) { return (long) Math.floor(Math.pow(entries, 1.d / dimension)); } private VorbisUtil() { // Prevent instantiation. } }