/* * Copyright 2010-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file 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. */ package com.amazon.blueshift.bluefront.android.audio.encoder; import com.amazonaws.mobileconnectors.lex.interactionkit.internal.audio.encoder.AudioEncoder; import com.amazonaws.mobileconnectors.lex.interactionkit.internal.audio.encoder.AudioEncoderException; import com.google.common.base.Preconditions; import com.google.common.collect.Sets; import com.google.common.net.MediaType; import java.nio.ByteBuffer; import java.util.Set; /** * Opus encoder that encodes 16-bit PCM audio to Opus format. */ public class OpusEncoder implements AudioEncoder { /** * The number of milliseconds per second. */ private static final int MS_PER_SEC = 1000; /** * This encoder only encodes CBR Opus, so VBR is set to false. */ private static final boolean IS_VBR = false; /** * This encoder is used for audio applications. */ private static final int APPLICATION_AUDIO = 2049; /** * This encoder is used for encoding voice. */ private static final int SIGNAL_VOICE = 3001; // Default values. private static final int DEFAULT_FRAME_SIZE_MILLIS = 20; private static final int DEFAULT_SAMPLE_RATE = 16000; private static final int DEFAULT_CHANNELS = 1; private static final int DEFAULT_BIT_RATE = 32000; private static final int DEFAULT_COMPLEXITY = 10; // Values used to validate the input to the constructor. private static final Set<Integer> VALID_FRAME_SIZE_MILLIS = Sets.newHashSet(5, 10, 20, 40, 60); private static final Set<Integer> VALID_SAMPLES_RATES = Sets.newHashSet(8000, 12000, 16000, 24000, 48000); private static final int MIN_COMPLEXITY = 1; private static final int MAX_COMPLEXITY = 10; private static final int BIT_RATE_INTERVAL = 400; private static final int MAX_BIT_RATE = 512000; private static final int MIN_BIT_RATE = 6000; // Values set by client. private final int mFrameSizeMillis; private final int mSampleRate; private final int mChannels; private final int mBitrate; private final int mComplexity; static { System.loadLibrary("blueshift-opus"); } private ByteBuffer mOpusEncoder; private final MediaType mMediaType; private final int mFrameSize; private final int mPacketSize; /** * Constructor with good defaults for the Android mobile library. * @throws AudioEncoderException when there's an error creating the encoder. */ public OpusEncoder() throws AudioEncoderException { this(DEFAULT_FRAME_SIZE_MILLIS, DEFAULT_SAMPLE_RATE, DEFAULT_CHANNELS, DEFAULT_BIT_RATE, DEFAULT_COMPLEXITY); } /** * Create a new Opus encoder with the following settings. * @param frameSizeMillis the frame size in milliseconds, must be 5, 10, 20, 40, or 60. * @param sampleRate the sample rate of the PCM audio in Hz, must be one of 8000, 12000, 16000, * 24000, or 48000. * @param channels the number of channels of audio, can be 1 or 2. * @param bitrate the Opus bitrate, can be between 6000 and 512000 in increments of 400. * @param complexity the complexity of the Opus audio, should be an integer between 1 and 10. * @throws AudioEncoderException when there's an error initializing the encoder. */ public OpusEncoder(final int frameSizeMillis, final int sampleRate, final int channels, final int bitrate, final int complexity) throws AudioEncoderException { Preconditions.checkArgument(VALID_FRAME_SIZE_MILLIS.contains(frameSizeMillis), "Frame size is invalid"); Preconditions.checkArgument(VALID_SAMPLES_RATES.contains(sampleRate), "Sample rate is invalid"); Preconditions.checkArgument(channels == 1 || channels == 2, "Number of channels is invalid"); Preconditions.checkArgument(bitrate >= MIN_BIT_RATE && bitrate <= MAX_BIT_RATE && (bitrate - MIN_BIT_RATE) % BIT_RATE_INTERVAL == 0, "Bitrate is invalid"); Preconditions.checkArgument(complexity >= MIN_COMPLEXITY && complexity <= MAX_COMPLEXITY, "Complexity is invalid"); mFrameSizeMillis = frameSizeMillis; mSampleRate = sampleRate; mChannels = channels; mBitrate = bitrate; mComplexity = complexity; mOpusEncoder = createOpusEncoder(sampleRate, channels, bitrate, complexity, IS_VBR, APPLICATION_AUDIO, SIGNAL_VOICE); mPacketSize = bitrate * frameSizeMillis / (Byte.SIZE * MS_PER_SEC); mFrameSize = sampleRate * frameSizeMillis / MS_PER_SEC; mMediaType = MediaType.parse("audio/x-cbr-opus-with-preamble") .withParameter("bit-rate", Integer.toString(bitrate)) .withParameter("frame-size-milliseconds", Integer.toString(frameSizeMillis)) .withParameter("preamble-size", "0"); } @Override public synchronized byte[] encode(final short[] samples, final int numSamples) throws AudioEncoderException { Preconditions.checkNotNull(mOpusEncoder, "Opus encoder is not initialized"); Preconditions.checkNotNull(samples, "Samples buffer cannot be null"); Preconditions.checkArgument(samples.length >= numSamples, "Number of samples cannot exceed buffer size"); return encodeOpus(mOpusEncoder, samples, mPacketSize); } @Override public MediaType getMediaType() { return mMediaType; } @Override public int getFrameSize() { return mFrameSize; } @Override public int getPacketSize() { return mPacketSize; } @Override public synchronized void close() { if (mOpusEncoder != null) { destroyOpusEncoder(mOpusEncoder); mOpusEncoder = null; } } /** * Create a native Opus Encoder and return a handle to the bytes. * @param sampleRate the sample rate of the audio. * @param channels the number of channels in the audio. * @param bitrate the bitrate of the Opus audio. * @param complexity the complexity of the Opus compression. * @param vbr whether or not to use VBR during compression. * @param application the application of the audio being compressed. * @param signal the type of audio being compressed. * @return the handle to the Opus Encoder. * @throws AudioEncoderException when there's an error initializing the Opus encoder. */ private native ByteBuffer createOpusEncoder(final int sampleRate, final int channels, final int bitrate, final int complexity, final boolean vbr, final int application, final int signal) throws AudioEncoderException; /** * Encode a short array of PCM samples into Opus. * @param opusEncoder the Opus encoder to use for encoding. * @param samples the audio samples to encode. * @param packetSize the packet size of the encoded audio. * @return the encoded Opus bytes. * @throws AudioEncoderException when there's an error encoding the audio. */ private native byte[] encodeOpus(final ByteBuffer opusEncoder, final short[] samples, final int packetSize) throws AudioEncoderException; /** * Free the memory used for an Opus Encoder. * @param opusEncoder the Opus encoder to free. */ private native void destroyOpusEncoder(final ByteBuffer opusEncoder); @Override public final OpusEncoder newEncoder() { try { return new OpusEncoder(mFrameSizeMillis, mSampleRate, mChannels, mBitrate, mComplexity); } catch (final AudioEncoderException e) { return null; } } }