1 /* 2 * Copyright (C) 2017 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.audio; 17 18 import com.google.android.exoplayer2.C; 19 import com.google.android.exoplayer2.util.Util; 20 import java.nio.ByteBuffer; 21 22 /** Audio processor for trimming samples from the start/end of data. */ 23 /* package */ final class TrimmingAudioProcessor extends BaseAudioProcessor { 24 25 @C.PcmEncoding private static final int OUTPUT_ENCODING = C.ENCODING_PCM_16BIT; 26 27 private int trimStartFrames; 28 private int trimEndFrames; 29 private boolean reconfigurationPending; 30 31 private int pendingTrimStartBytes; 32 private byte[] endBuffer; 33 private int endBufferSize; 34 private long trimmedFrameCount; 35 36 /** Creates a new audio processor for trimming samples from the start/end of data. */ TrimmingAudioProcessor()37 public TrimmingAudioProcessor() { 38 endBuffer = Util.EMPTY_BYTE_ARRAY; 39 } 40 41 /** 42 * Sets the number of audio frames to trim from the start and end of audio passed to this 43 * processor. After calling this method, call {@link #configure(AudioFormat)} to apply the new 44 * trimming frame counts. 45 * 46 * @param trimStartFrames The number of audio frames to trim from the start of audio. 47 * @param trimEndFrames The number of audio frames to trim from the end of audio. 48 * @see AudioSink#configure(int, int, int, int, int[], int, int) 49 */ setTrimFrameCount(int trimStartFrames, int trimEndFrames)50 public void setTrimFrameCount(int trimStartFrames, int trimEndFrames) { 51 this.trimStartFrames = trimStartFrames; 52 this.trimEndFrames = trimEndFrames; 53 } 54 55 /** Sets the trimmed frame count returned by {@link #getTrimmedFrameCount()} to zero. */ resetTrimmedFrameCount()56 public void resetTrimmedFrameCount() { 57 trimmedFrameCount = 0; 58 } 59 60 /** 61 * Returns the number of audio frames trimmed since the last call to {@link 62 * #resetTrimmedFrameCount()}. 63 */ getTrimmedFrameCount()64 public long getTrimmedFrameCount() { 65 return trimmedFrameCount; 66 } 67 68 @Override onConfigure(AudioFormat inputAudioFormat)69 public AudioFormat onConfigure(AudioFormat inputAudioFormat) 70 throws UnhandledAudioFormatException { 71 if (inputAudioFormat.encoding != OUTPUT_ENCODING) { 72 throw new UnhandledAudioFormatException(inputAudioFormat); 73 } 74 reconfigurationPending = true; 75 return trimStartFrames != 0 || trimEndFrames != 0 ? inputAudioFormat : AudioFormat.NOT_SET; 76 } 77 78 @Override queueInput(ByteBuffer inputBuffer)79 public void queueInput(ByteBuffer inputBuffer) { 80 int position = inputBuffer.position(); 81 int limit = inputBuffer.limit(); 82 int remaining = limit - position; 83 84 if (remaining == 0) { 85 return; 86 } 87 88 // Trim any pending start bytes from the input buffer. 89 int trimBytes = Math.min(remaining, pendingTrimStartBytes); 90 trimmedFrameCount += trimBytes / inputAudioFormat.bytesPerFrame; 91 pendingTrimStartBytes -= trimBytes; 92 inputBuffer.position(position + trimBytes); 93 if (pendingTrimStartBytes > 0) { 94 // Nothing to output yet. 95 return; 96 } 97 remaining -= trimBytes; 98 99 // endBuffer must be kept as full as possible, so that we trim the right amount of media if we 100 // don't receive any more input. After taking into account the number of bytes needed to keep 101 // endBuffer as full as possible, the output should be any surplus bytes currently in endBuffer 102 // followed by any surplus bytes in the new inputBuffer. 103 int remainingBytesToOutput = endBufferSize + remaining - endBuffer.length; 104 ByteBuffer buffer = replaceOutputBuffer(remainingBytesToOutput); 105 106 // Output from endBuffer. 107 int endBufferBytesToOutput = Util.constrainValue(remainingBytesToOutput, 0, endBufferSize); 108 buffer.put(endBuffer, 0, endBufferBytesToOutput); 109 remainingBytesToOutput -= endBufferBytesToOutput; 110 111 // Output from inputBuffer, restoring its limit afterwards. 112 int inputBufferBytesToOutput = Util.constrainValue(remainingBytesToOutput, 0, remaining); 113 inputBuffer.limit(inputBuffer.position() + inputBufferBytesToOutput); 114 buffer.put(inputBuffer); 115 inputBuffer.limit(limit); 116 remaining -= inputBufferBytesToOutput; 117 118 // Compact endBuffer, then repopulate it using the new input. 119 endBufferSize -= endBufferBytesToOutput; 120 System.arraycopy(endBuffer, endBufferBytesToOutput, endBuffer, 0, endBufferSize); 121 inputBuffer.get(endBuffer, endBufferSize, remaining); 122 endBufferSize += remaining; 123 124 buffer.flip(); 125 } 126 127 @Override getOutput()128 public ByteBuffer getOutput() { 129 if (super.isEnded() && endBufferSize > 0) { 130 // Because audio processors may be drained in the middle of the stream we assume that the 131 // contents of the end buffer need to be output. For gapless transitions, configure will 132 // always be called, so the end buffer is cleared in onQueueEndOfStream. 133 replaceOutputBuffer(endBufferSize).put(endBuffer, 0, endBufferSize).flip(); 134 endBufferSize = 0; 135 } 136 return super.getOutput(); 137 } 138 139 @Override isEnded()140 public boolean isEnded() { 141 return super.isEnded() && endBufferSize == 0; 142 } 143 144 @Override onQueueEndOfStream()145 protected void onQueueEndOfStream() { 146 if (reconfigurationPending) { 147 // Trim audio in the end buffer. 148 if (endBufferSize > 0) { 149 trimmedFrameCount += endBufferSize / inputAudioFormat.bytesPerFrame; 150 } 151 endBufferSize = 0; 152 } 153 } 154 155 @Override onFlush()156 protected void onFlush() { 157 if (reconfigurationPending) { 158 // Flushing activates the new configuration, so prepare to trim bytes from the start/end. 159 reconfigurationPending = false; 160 endBuffer = new byte[trimEndFrames * inputAudioFormat.bytesPerFrame]; 161 pendingTrimStartBytes = trimStartFrames * inputAudioFormat.bytesPerFrame; 162 } 163 164 // TODO(internal b/77292509): Flushing occurs to activate a configuration (handled above) but 165 // also when seeking within a stream. This implementation currently doesn't handle seek to start 166 // (where we need to trim at the start again), nor seeks to non-zero positions before start 167 // trimming has occurred (where we should set pendingTrimStartBytes to zero). These cases can be 168 // fixed by trimming in queueInput based on timestamp, once that information is available. 169 170 // Any data in the end buffer should no longer be output if we are playing from a different 171 // position, so discard it and refill the buffer using new input. 172 endBufferSize = 0; 173 } 174 175 @Override onReset()176 protected void onReset() { 177 endBuffer = Util.EMPTY_BYTE_ARRAY; 178 } 179 180 } 181