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