1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.exoplayer2.extractor; 17 18 import static com.google.android.exoplayer2.util.Util.castNonNull; 19 20 import com.google.android.exoplayer2.Format; 21 import com.google.android.exoplayer2.metadata.Metadata; 22 import com.google.android.exoplayer2.metadata.id3.CommentFrame; 23 import com.google.android.exoplayer2.metadata.id3.InternalFrame; 24 import java.util.regex.Matcher; 25 import java.util.regex.Pattern; 26 27 /** 28 * Holder for gapless playback information. 29 */ 30 public final class GaplessInfoHolder { 31 32 private static final String GAPLESS_DOMAIN = "com.apple.iTunes"; 33 private static final String GAPLESS_DESCRIPTION = "iTunSMPB"; 34 private static final Pattern GAPLESS_COMMENT_PATTERN = 35 Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); 36 37 /** 38 * The number of samples to trim from the start of the decoded audio stream, or 39 * {@link Format#NO_VALUE} if not set. 40 */ 41 public int encoderDelay; 42 43 /** 44 * The number of samples to trim from the end of the decoded audio stream, or 45 * {@link Format#NO_VALUE} if not set. 46 */ 47 public int encoderPadding; 48 49 /** 50 * Creates a new holder for gapless playback information. 51 */ GaplessInfoHolder()52 public GaplessInfoHolder() { 53 encoderDelay = Format.NO_VALUE; 54 encoderPadding = Format.NO_VALUE; 55 } 56 57 /** 58 * Populates the holder with data from an MP3 Xing header, if valid and non-zero. 59 * 60 * @param value The 24-bit value to decode. 61 * @return Whether the holder was populated. 62 */ setFromXingHeaderValue(int value)63 public boolean setFromXingHeaderValue(int value) { 64 int encoderDelay = value >> 12; 65 int encoderPadding = value & 0x0FFF; 66 if (encoderDelay > 0 || encoderPadding > 0) { 67 this.encoderDelay = encoderDelay; 68 this.encoderPadding = encoderPadding; 69 return true; 70 } 71 return false; 72 } 73 74 /** 75 * Populates the holder with data parsed from ID3 {@link Metadata}. 76 * 77 * @param metadata The metadata from which to parse the gapless information. 78 * @return Whether the holder was populated. 79 */ setFromMetadata(Metadata metadata)80 public boolean setFromMetadata(Metadata metadata) { 81 for (int i = 0; i < metadata.length(); i++) { 82 Metadata.Entry entry = metadata.get(i); 83 if (entry instanceof CommentFrame) { 84 CommentFrame commentFrame = (CommentFrame) entry; 85 if (GAPLESS_DESCRIPTION.equals(commentFrame.description) 86 && setFromComment(commentFrame.text)) { 87 return true; 88 } 89 } else if (entry instanceof InternalFrame) { 90 InternalFrame internalFrame = (InternalFrame) entry; 91 if (GAPLESS_DOMAIN.equals(internalFrame.domain) 92 && GAPLESS_DESCRIPTION.equals(internalFrame.description) 93 && setFromComment(internalFrame.text)) { 94 return true; 95 } 96 } 97 } 98 return false; 99 } 100 101 /** 102 * Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header 103 * or MPEG 4 user data), if valid and non-zero. 104 * 105 * @param data The comment's payload data. 106 * @return Whether the holder was populated. 107 */ setFromComment(String data)108 private boolean setFromComment(String data) { 109 Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); 110 if (matcher.find()) { 111 try { 112 int encoderDelay = Integer.parseInt(castNonNull(matcher.group(1)), 16); 113 int encoderPadding = Integer.parseInt(castNonNull(matcher.group(2)), 16); 114 if (encoderDelay > 0 || encoderPadding > 0) { 115 this.encoderDelay = encoderDelay; 116 this.encoderPadding = encoderPadding; 117 return true; 118 } 119 } catch (NumberFormatException e) { 120 // Ignore incorrectly formatted comments. 121 } 122 } 123 return false; 124 } 125 126 /** 127 * Returns whether {@link #encoderDelay} and {@link #encoderPadding} have been set. 128 */ hasGaplessInfo()129 public boolean hasGaplessInfo() { 130 return encoderDelay != Format.NO_VALUE && encoderPadding != Format.NO_VALUE; 131 } 132 133 } 134