Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix an issue in Audio transcoding where target-sample-rate is less than the source-sample-rate #111

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ public void init(@Nullable Surface outputSurface, @Nullable MediaFormat sourceMe
}
}

@Override
public void onMediaFormatChanged(@Nullable MediaFormat sourceMediaFormat, @Nullable MediaFormat targetMediaFormat) {}

@Override
@Nullable
public Surface getInputSurface() {
Expand All @@ -124,7 +127,7 @@ public Surface getInputSurface() {
}

@Override
public void renderFrame(@Nullable Frame frame, long presentationTimeNs) {
public void renderFrame(@Nullable Frame inputFrame, long presentationTimeNs) {
inputSurface.awaitNewImage();
drawFrame(presentationTimeNs);
outputSurface.setPresentationTime(presentationTimeNs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,27 @@

import com.linkedin.android.litr.codec.Encoder;
import com.linkedin.android.litr.codec.Frame;

import com.linkedin.android.litr.resample.AudioResampler;
import com.linkedin.android.litr.resample.DownsampleAudioResampler;
import com.linkedin.android.litr.resample.PassThroughAudioResampler;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;

/**
* This {@link Renderer} only used with Audio transcoding
*/
public class PassthroughSoftwareRenderer implements Renderer {

@VisibleForTesting static final long FRAME_WAIT_TIMEOUT = TimeUnit.SECONDS.toMicros(0);

private static final String TAG = "PassthroughSwRenderer";

@NonNull public final Encoder encoder;
public final long frameWaitTimeoutUs;
private final long frameWaitTimeoutUs;

private byte[] inputByteBuffer = null;
@NonNull private AudioResampler audioResampler = new PassThroughAudioResampler();
private MediaFormat sourceAudioFormat;
private MediaFormat targetAudioFormat;

public PassthroughSoftwareRenderer(@NonNull Encoder encoder) {
this(encoder, FRAME_WAIT_TIMEOUT);
Expand All @@ -43,7 +50,30 @@ public PassthroughSoftwareRenderer(@NonNull Encoder encoder, long frameWaitTimeo
}

@Override
public void init(@Nullable Surface outputSurface, @Nullable MediaFormat sourceMediaFormat, @Nullable MediaFormat targetMediaFormat) {}
public void init(@Nullable Surface outputSurface, @Nullable MediaFormat sourceMediaFormat,
@Nullable MediaFormat targetMediaFormat) {
onMediaFormatChanged(sourceMediaFormat, targetMediaFormat);
}

@Override
public void onMediaFormatChanged(@Nullable MediaFormat sourceMediaFormat, @Nullable MediaFormat targetMediaFormat) {
this.sourceAudioFormat = sourceMediaFormat;
this.targetAudioFormat = targetMediaFormat;
initAudioResampler();
}

private void initAudioResampler() {
if (sourceAudioFormat == null || targetAudioFormat == null) {
return;
}
int inputSampleRate = audioResampler.getSampleRate(sourceAudioFormat);
int outputSampleRate = audioResampler.getSampleRate(targetAudioFormat);
if (inputSampleRate > outputSampleRate) {
audioResampler = new DownsampleAudioResampler();
} else {
audioResampler = new PassThroughAudioResampler();
}
}

@Nullable
@Override
Expand All @@ -52,14 +82,13 @@ public Surface getInputSurface() {
}

@Override
public void renderFrame(@Nullable Frame frame, long presentationTimeNs) {
if (frame == null || frame.buffer == null) {
public void renderFrame(@Nullable Frame inputFrame, long presentationTimeNs) {
if (inputFrame == null || inputFrame.buffer == null) {
Log.e(TAG, "Null or empty input frame provided");
return;
}

ByteBuffer inputBuffer = null;
int capacity;
boolean isNewInputFrame = true;
boolean areBytesRemaining;
do {
Expand All @@ -71,33 +100,35 @@ public void renderFrame(@Nullable Frame frame, long presentationTimeNs) {
Log.e(TAG, "No input frame returned by an encoder, dropping a frame");
return;
}
ByteBuffer outputBuffer = outputFrame.buffer;

if (isNewInputFrame) {
isNewInputFrame = false;
inputBuffer = frame.buffer.asReadOnlyBuffer();
inputBuffer = inputFrame.buffer.asReadOnlyBuffer();
inputBuffer.rewind();
}

if (outputFrame.buffer.remaining() < inputBuffer.remaining()) {
capacity = outputFrame.buffer.remaining();
if (inputByteBuffer == null || inputByteBuffer.length < capacity) {
inputByteBuffer = new byte[capacity];
}
inputBuffer.get(inputByteBuffer, 0, capacity);
outputFrame.buffer.put(inputByteBuffer, 0, capacity);
} else {
capacity = inputBuffer.remaining();
outputFrame.buffer.put(inputBuffer);
int outSize = outputBuffer.remaining();
int inSize = inputBuffer.remaining();

// check if need to set a new limit for the inputBuffer to fit the outputBuffer, then restore the old limit after writing the data
int inputBufferLimit = inputBuffer.limit();
if (outSize < inSize) {
inputBuffer.limit(inputBuffer.position() + outSize);
}

// Resampling will change the input size based on the sample rate ratio.
audioResampler.resample(inputBuffer, outputBuffer, sourceAudioFormat, targetAudioFormat);

inputBuffer.limit(inputBufferLimit);
areBytesRemaining = inputBuffer.hasRemaining();

MediaCodec.BufferInfo bufferInfo = outputFrame.bufferInfo;
bufferInfo.offset = 0;
bufferInfo.size = capacity;
bufferInfo.size = outputBuffer.position();
bufferInfo.presentationTimeUs = TimeUnit.NANOSECONDS.toMicros(presentationTimeNs);
bufferInfo.flags = frame.bufferInfo.flags;
bufferInfo.flags = inputFrame.bufferInfo.flags;
encoder.queueInputFrame(outputFrame);

areBytesRemaining = inputBuffer.hasRemaining();
} else {
switch (tag) {
case MediaCodec.INFO_TRY_AGAIN_LATER:
Expand Down
12 changes: 10 additions & 2 deletions litr/src/main/java/com/linkedin/android/litr/render/Renderer.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/
package com.linkedin.android.litr.render;

import android.media.MediaCodec;
import android.media.MediaFormat;
import android.view.Surface;
import androidx.annotation.Nullable;
Expand All @@ -25,6 +26,13 @@ public interface Renderer {
*/
void init(@Nullable Surface outputSurface, @Nullable MediaFormat sourceMediaFormat, @Nullable MediaFormat targetMediaFormat);

/**
* This should be called every time the {@link MediaCodec#INFO_OUTPUT_FORMAT_CHANGED} is returned
* @param sourceMediaFormat source {@link MediaFormat}
* @param targetMediaFormat target {@link MediaFormat}
*/
void onMediaFormatChanged(@Nullable MediaFormat sourceMediaFormat, @Nullable MediaFormat targetMediaFormat);

/**
* Get renderer's input surface. Renderer creates it internally.
* @return {@link Surface} to get pixels from, null for non OpenGL renderer
Expand All @@ -33,11 +41,11 @@ public interface Renderer {

/**
* Render a frame
* @param frame {@link Frame} to operate with. Non-null ror non OpenGL renderer, will contain raw pixels.
* @param inputFrame {@link Frame} to operate with. Non-null ror non OpenGL renderer, will contain raw pixels.
* null for GL renderer, which should assume that environment has been set and just invoke Gl calls.
* @param presentationTimeNs frame presentation time in nanoseconds
*/
void renderFrame(@Nullable Frame frame, long presentationTimeNs);
void renderFrame(@Nullable Frame inputFrame, long presentationTimeNs);

/**
* Release the renderer and all it resources.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2021 natario1 Transcoder
*
* 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.
*/
// from: https://github.com/natario1/Transcoder/blob/main/lib/src/main/java/com/otaliastudios/transcoder/resample/AudioResampler.java
// modified: removed all constant fields from this interface
// modified: changed the signature of resample() method
// modified: added two other default methods, getSampleRate() and getChannels()
package com.linkedin.android.litr.resample;

import android.media.MediaFormat;
import androidx.annotation.NonNull;
import java.nio.ByteBuffer;

/**
* Resamples audio data. See {@link DownsampleAudioResampler} for concrete implementations.
*/
public interface AudioResampler {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For all new classes that are not modified classes from other project please add LinkedIn copyright notice


/**
* Resamples input audio from input buffer into the output buffer.
*
* @param inputBuffer the input buffer
* @param outputBuffer the output buffer
* @param sourceMediaFormat the source media format
* @param targetMediaFormat the target media format
*/
void resample(@NonNull final ByteBuffer inputBuffer, @NonNull final ByteBuffer outputBuffer,
@NonNull MediaFormat sourceMediaFormat, @NonNull MediaFormat targetMediaFormat);

/**
* Return the sample rate of an audio format
*/
default int getSampleRate(MediaFormat format) {
return format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
}

/**
* Return the width of the content in a video format.
*/
default int getChannels(MediaFormat format) {
return format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright (C) 2021 natario1 Transcoder
*
* 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.
*/
// from: https://github.com/natario1/Transcoder/blob/main/lib/src/main/java/com/otaliastudios/transcoder/resample/DownsampleAudioResampler.java
// modified: changed the signature of resample() method
// modified: small modification for "outputBuffer.put(inputBuffer.get());" logic
// modified: small modification for "inputBuffer.position(inputBuffer.position() + 1);" logic
package com.linkedin.android.litr.resample;

import android.media.MediaFormat;
import androidx.annotation.NonNull;
import java.nio.ByteBuffer;

/**
* An {@link AudioResampler} that downsamples from a higher sample rate to a lower sample rate.
*/
public class DownsampleAudioResampler implements AudioResampler {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this adapted code from a different open source project? In that case we have to keep their copyright notice and add a note that we made modifications (feel free to add your name as author of modifications)


private float ratio(int remaining, int all) {
return (float) remaining / all;
}

@Override
public void resample(@NonNull ByteBuffer inputBuffer, @NonNull ByteBuffer outputBuffer,
@NonNull MediaFormat sourceMediaFormat, @NonNull MediaFormat targetMediaFormat) {
int inputSampleRate = getSampleRate(sourceMediaFormat);
int outputSampleRate = getSampleRate(targetMediaFormat);
int channels = getChannels(targetMediaFormat);
if (inputSampleRate < outputSampleRate) {
throw new IllegalArgumentException("Illegal use of DownsampleAudioResampler");
}
if (channels != 1 && channels != 2) {
throw new IllegalArgumentException("Illegal use of DownsampleAudioResampler. Channels:" + channels);
}
final int inputSamples = inputBuffer.remaining() / channels;
final int outputSamples = (int) Math.ceil(inputSamples * ((double) outputSampleRate / inputSampleRate));
final int dropSamples = inputSamples - outputSamples;
int remainingOutputSamples = outputSamples;
int remainingDropSamples = dropSamples;
float remainingOutputSamplesRatio = ratio(remainingOutputSamples, outputSamples);
float remainingDropSamplesRatio = ratio(remainingDropSamples, dropSamples);
while (remainingOutputSamples > 0 || remainingDropSamples > 0) {
// Will this be an input sample or a drop sample?
// Choose the one with the bigger ratio.
if (remainingOutputSamplesRatio >= remainingDropSamplesRatio) {
for (int i = 0; i < channels; i++) {
outputBuffer.put(inputBuffer.get());
}
remainingOutputSamples--;
remainingOutputSamplesRatio = ratio(remainingOutputSamples, outputSamples);
} else {
// Drop this - read from input without writing.
for (int i = 0; i < channels; i++) {
inputBuffer.position(inputBuffer.position() + 1);
}
remainingDropSamples--;
remainingDropSamplesRatio = ratio(remainingDropSamples, dropSamples);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (C) 2021 natario1 Transcoder
*
* 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.
*/
// from: https://github.com/natario1/Transcoder/blob/main/lib/src/main/java/com/otaliastudios/transcoder/resample/PassThroughAudioResampler.java
// modified: changed the signature of resample() method
// modified: removed the first if-condition in resample() method
package com.linkedin.android.litr.resample;

import android.media.MediaFormat;
import androidx.annotation.NonNull;
import java.nio.ByteBuffer;

/**
* An {@link AudioResampler} that does nothing, meant to be used when sample
* rates are identical.
*/
public class PassThroughAudioResampler implements AudioResampler {

@Override
public void resample(@NonNull ByteBuffer inputBuffer, @NonNull ByteBuffer outputBuffer,
@NonNull MediaFormat sourceMediaFormat, @NonNull MediaFormat targetMediaFormat) {
outputBuffer.put(inputBuffer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ private void initCodecs() throws TrackTranscoderException {
sourceAudioFormat = mediaSource.getTrackFormat(sourceTrack);

encoder.init(targetFormat);
renderer.init(null, sourceAudioFormat, targetFormat);
decoder.init(sourceAudioFormat, null);
}

Expand Down Expand Up @@ -185,8 +186,9 @@ private int queueDecodedInputFrame() throws TrackTranscoderException {
// Log.d(TAG, "Will try getting decoder output later");
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
MediaFormat outputFormat = decoder.getOutputFormat();
Log.d(TAG, "Decoder output format changed: " + outputFormat);
sourceAudioFormat = decoder.getOutputFormat();
renderer.onMediaFormatChanged(sourceAudioFormat, targetFormat);
Log.d(TAG, "Decoder output format changed: " + sourceAudioFormat);
break;
default:
Log.e(TAG, "Unhandled value " + tag + " when receiving decoded input frame");
Expand Down Expand Up @@ -229,8 +231,10 @@ private int writeEncodedOutputFrame() throws TrackTranscoderException {
// TODO for now, we assume that we only get one media format as a first buffer
MediaFormat outputMediaFormat = encoder.getOutputFormat();
if (!targetTrackAdded) {
targetFormat = outputMediaFormat;
targetTrack = mediaMuxer.addTrack(outputMediaFormat, targetTrack);
targetTrackAdded = true;
renderer.onMediaFormatChanged(sourceAudioFormat, targetFormat);
}
encodeFrameResult = RESULT_OUTPUT_MEDIA_FORMAT_CHANGED;
Log.d(TAG, "Encoder output format received " + outputMediaFormat);
Expand Down
Loading