diff --git a/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java b/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java index 81c12a4d..c5898348 100644 --- a/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java +++ b/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java @@ -349,7 +349,9 @@ private List buildAudioDataSources() if (dataSource.getTrackFormat(TrackType.AUDIO) != null) { result.add(dataSource); } else { - result.add(new BlankAudioDataSource(dataSource.getDurationUs())); + DataSource blankDataSource = new BlankAudioDataSource(dataSource.getDurationUs()); + blankDataSource.setPostProcessor(dataSource.getPostProcessor()); + result.add(blankDataSource); } } return result; diff --git a/lib/src/main/java/com/otaliastudios/transcoder/engine/Engine.java b/lib/src/main/java/com/otaliastudios/transcoder/engine/Engine.java index 742fd1cf..f7441fae 100644 --- a/lib/src/main/java/com/otaliastudios/transcoder/engine/Engine.java +++ b/lib/src/main/java/com/otaliastudios/transcoder/engine/Engine.java @@ -20,10 +20,12 @@ import com.otaliastudios.transcoder.TranscoderOptions; import com.otaliastudios.transcoder.internal.TrackTypeMap; import com.otaliastudios.transcoder.internal.ValidatorException; +import com.otaliastudios.transcoder.postprocessor.AudioPostProcessor; import com.otaliastudios.transcoder.sink.DataSink; import com.otaliastudios.transcoder.sink.InvalidOutputFormatException; import com.otaliastudios.transcoder.source.DataSource; import com.otaliastudios.transcoder.strategy.TrackStrategy; +import com.otaliastudios.transcoder.time.PresentationTime; import com.otaliastudios.transcoder.time.TimeInterpolator; import com.otaliastudios.transcoder.transcode.AudioTrackTranscoder; import com.otaliastudios.transcoder.transcode.NoOpTrackTranscoder; @@ -70,6 +72,7 @@ public interface ProgressCallback { private final TrackTypeMap mOutputFormats = new TrackTypeMap<>(); private volatile double mProgress; private final ProgressCallback mProgressCallback; + private final PresentationTime mAudioPresentationTime = new PresentationTime(); public Engine(@Nullable ProgressCallback progressCallback) { mProgressCallback = progressCallback; @@ -177,7 +180,9 @@ private void openCurrentStep(@NonNull TrackType type, @NonNull TranscoderOptions transcoder = new AudioTrackTranscoder(dataSource, mDataSink, interpolator, options.getAudioStretcher(), - options.getAudioResampler()); + options.getAudioResampler(), + (AudioPostProcessor)dataSource.getPostProcessor(), + mAudioPresentationTime); break; default: throw new RuntimeException("Unknown type: " + type); @@ -253,17 +258,22 @@ public long interpolate(@NonNull TrackType type, long time) { }; } - private long getTrackDurationUs(@NonNull TrackType type) { + private long getTrackDurationUs(@NonNull TrackType type, boolean processedDuration) { if (!mStatuses.require(type).isTranscoding()) return 0L; int current = mCurrentStep.require(type); long totalDurationUs = 0; for (int i = 0; i < mDataSources.require(type).size(); i++) { DataSource source = mDataSources.require(type).get(i); + long dataSourceDurationUs; if (i < current) { // getReadUs() is a better approximation for sure. - totalDurationUs += source.getReadUs(); + dataSourceDurationUs = source.getReadUs(); } else { - totalDurationUs += source.getDurationUs(); + dataSourceDurationUs = source.getDurationUs(); } + if (processedDuration && source.getPostProcessor() != null) { + dataSourceDurationUs = source.getPostProcessor().calculateNewDurationUs(dataSourceDurationUs); + } + totalDurationUs += dataSourceDurationUs; } return totalDurationUs; } @@ -271,19 +281,23 @@ private long getTrackDurationUs(@NonNull TrackType type) { private long getTotalDurationUs() { boolean hasVideo = hasVideoSources() && mStatuses.requireVideo().isTranscoding(); boolean hasAudio = hasAudioSources() && mStatuses.requireAudio().isTranscoding(); - long video = hasVideo ? getTrackDurationUs(TrackType.VIDEO) : Long.MAX_VALUE; - long audio = hasAudio ? getTrackDurationUs(TrackType.AUDIO) : Long.MAX_VALUE; + long video = hasVideo ? getTrackDurationUs(TrackType.VIDEO, true) : Long.MAX_VALUE; + long audio = hasAudio ? getTrackDurationUs(TrackType.AUDIO, true) : Long.MAX_VALUE; return Math.min(video, audio); } - private long getTrackReadUs(@NonNull TrackType type) { + private long getTrackProgressUs(@NonNull TrackType type, boolean processedDuration) { if (!mStatuses.require(type).isTranscoding()) return 0L; int current = mCurrentStep.require(type); long completedDurationUs = 0; for (int i = 0; i < mDataSources.require(type).size(); i++) { DataSource source = mDataSources.require(type).get(i); if (i <= current) { - completedDurationUs += source.getReadUs(); + long dataSourceReadUs = source.getReadUs(); + if (processedDuration && source.getPostProcessor() != null) { + dataSourceReadUs = source.getPostProcessor().calculateNewDurationUs(dataSourceReadUs); + } + completedDurationUs += dataSourceReadUs; } } return completedDurationUs; @@ -291,8 +305,8 @@ private long getTrackReadUs(@NonNull TrackType type) { private double getTrackProgress(@NonNull TrackType type) { if (!mStatuses.require(type).isTranscoding()) return 0.0D; - long readUs = getTrackReadUs(type); - long totalUs = getTotalDurationUs(); + long readUs = getTrackProgressUs(type, false); + long totalUs = getTrackDurationUs(type, false); LOG.v("getTrackProgress - readUs:" + readUs + ", totalUs:" + totalUs); if (totalUs == 0) totalUs = 1; // Avoid NaN return (double) readUs / (double) totalUs; @@ -361,8 +375,8 @@ public void transcode(@NonNull TranscoderOptions options) throws InterruptedExce // This can happen, for example, if user adds 1 minute (video only) with 20 seconds // of audio. The video track must be stopped once the audio stops. long totalUs = getTotalDurationUs() + 100 /* tolerance */; - forceAudioEos = getTrackReadUs(TrackType.AUDIO) > totalUs; - forceVideoEos = getTrackReadUs(TrackType.VIDEO) > totalUs; + forceAudioEos = getTrackProgressUs(TrackType.AUDIO, true) > totalUs; + forceVideoEos = getTrackProgressUs(TrackType.VIDEO, true) > totalUs; // Now step for transcoders that are not completed. audioCompleted = isCompleted(TrackType.AUDIO); diff --git a/lib/src/main/java/com/otaliastudios/transcoder/postprocessor/AudioPostProcessor.java b/lib/src/main/java/com/otaliastudios/transcoder/postprocessor/AudioPostProcessor.java new file mode 100644 index 00000000..5b07c39e --- /dev/null +++ b/lib/src/main/java/com/otaliastudios/transcoder/postprocessor/AudioPostProcessor.java @@ -0,0 +1,16 @@ +package com.otaliastudios.transcoder.postprocessor; + +import androidx.annotation.NonNull; + +import java.nio.ShortBuffer; + +public interface AudioPostProcessor extends PostProcessor { + /** + * Manipulates the raw audio data inside inputBuffer and put the result in outputBuffer + * @param inputBuffer the input data (as raw audio data) + * @param outputBuffer the data after the manipulation + * @param bufferDurationUs the duration of the input data + * @return the duration of the output data + */ + long postProcess(@NonNull final ShortBuffer inputBuffer, @NonNull final ShortBuffer outputBuffer, long bufferDurationUs); +} diff --git a/lib/src/main/java/com/otaliastudios/transcoder/postprocessor/DefaultAudioPostProcessor.java b/lib/src/main/java/com/otaliastudios/transcoder/postprocessor/DefaultAudioPostProcessor.java new file mode 100644 index 00000000..13f11c1d --- /dev/null +++ b/lib/src/main/java/com/otaliastudios/transcoder/postprocessor/DefaultAudioPostProcessor.java @@ -0,0 +1,17 @@ +package com.otaliastudios.transcoder.postprocessor; + +import androidx.annotation.NonNull; +import java.nio.ShortBuffer; + +public class DefaultAudioPostProcessor implements AudioPostProcessor { + @Override + public long calculateNewDurationUs(long durationUs) { + return durationUs; + } + + @Override + public long postProcess(@NonNull ShortBuffer inputBuffer, @NonNull ShortBuffer outputBuffer, long bufferDurationUs) { + outputBuffer.put(inputBuffer); + return bufferDurationUs; + } +} diff --git a/lib/src/main/java/com/otaliastudios/transcoder/postprocessor/PostProcessor.java b/lib/src/main/java/com/otaliastudios/transcoder/postprocessor/PostProcessor.java new file mode 100644 index 00000000..c2289790 --- /dev/null +++ b/lib/src/main/java/com/otaliastudios/transcoder/postprocessor/PostProcessor.java @@ -0,0 +1,10 @@ +package com.otaliastudios.transcoder.postprocessor; + +public interface PostProcessor { + /** + * Returns the duration of the data source on it has been processed (after calling the postProcess() method) + * @param durationUs the original duratin in Us + * @return the new duration in Us + */ + long calculateNewDurationUs(long durationUs); +} diff --git a/lib/src/main/java/com/otaliastudios/transcoder/postprocessor/VolumeAudioPostProcessor.java b/lib/src/main/java/com/otaliastudios/transcoder/postprocessor/VolumeAudioPostProcessor.java new file mode 100644 index 00000000..d976fcc4 --- /dev/null +++ b/lib/src/main/java/com/otaliastudios/transcoder/postprocessor/VolumeAudioPostProcessor.java @@ -0,0 +1,36 @@ +package com.otaliastudios.transcoder.postprocessor; + +import androidx.annotation.NonNull; + +import java.nio.ShortBuffer; + +public class VolumeAudioPostProcessor implements AudioPostProcessor { + private float mVolume; + + public VolumeAudioPostProcessor(float volume) { + mVolume = volume; + } + + @Override + public long calculateNewDurationUs(long durationUs) { + return durationUs; + } + + private short applyVolume(short sample) { + float sampleAtVolume = sample * mVolume; + if (sampleAtVolume < Short.MIN_VALUE) + sampleAtVolume = Short.MIN_VALUE; + else if (sampleAtVolume > Short.MAX_VALUE) + sampleAtVolume = Short.MAX_VALUE; + return (short)sampleAtVolume; + } + + @Override + public long postProcess(@NonNull ShortBuffer inputBuffer, @NonNull ShortBuffer outputBuffer, long bufferDurationUs) { + int inputRemaining = inputBuffer.remaining(); + for (int i=0; i