All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sheinbergon.aac.encoder.AACAudioEncoder Maven / Gradle / Ivy

package org.sheinbergon.aac.encoder;

import com.sun.jna.Memory;
import com.sun.jna.ptr.IntByReference;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.commons.lang3.Range;
import org.sheinbergon.aac.encoder.util.AACAudioEncoderException;
import org.sheinbergon.aac.encoder.util.AACEncodingChannelMode;
import org.sheinbergon.aac.encoder.util.AACEncodingProfile;
import org.sheinbergon.aac.encoder.util.WAVAudioSupport;
import org.sheinbergon.aac.jna.FdkAACLibFacade;
import org.sheinbergon.aac.jna.structure.AACEncBufDesc;
import org.sheinbergon.aac.jna.structure.AACEncInfo;
import org.sheinbergon.aac.jna.structure.AACEncoder;
import org.sheinbergon.aac.jna.util.AACEncParam;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Optional;
import java.util.Set;

@Accessors(fluent = true)
public class AACAudioEncoder implements AutoCloseable {

    /**
     * Safe, reasonable boundaries according to @see fdk-aac/libAACenc/include/aacenc_lib.h
     */
    private final static Range BITRATE_RANGE = Range.between(64000, 640000);

    private final static Set SAMPLE_RATES = Set.of(8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000);

    private final static int WAV_INPUT_CHANNEL_ORDER = 1;
    private final static int MAX_ENCODER_CHANNELS = 0;
    private final static int ENCODER_MODULES_MASK = 0;

    // This buffer just needs to be big enough to contain the encoded data
    private final static int OUT_BUFFER_SIZE = 20480;

    private final AACEncoder encoder;
    @Getter
    private final int inputBufferSize;
    // Hard references are advised for memory buffers
    private final Memory inBuffer;
    private final Memory outBuffer;
    private final AACEncBufDesc inBufferDescriptor;
    private final AACEncBufDesc outBufferDescriptor;

    private AACAudioEncoder(AACEncoder encoder, AACEncInfo info) {
        this.encoder = encoder;
        this.inputBufferSize = info.inputChannels * info.frameLength * 2;
        this.inBuffer = new Memory(inputBufferSize);
        this.outBuffer = new Memory(OUT_BUFFER_SIZE);
        this.inBufferDescriptor = FdkAACLibFacade.inBufferDescriptor(inBuffer);
        this.outBufferDescriptor = FdkAACLibFacade.outBufferDescriptor(outBuffer);
        disableStructureSynchronization();
    }

    public static Builder builder() {
        return new Builder();
    }

    @Setter
    @Accessors(chain = true, fluent = true)
    @NoArgsConstructor(access = AccessLevel.PRIVATE)
    public static class Builder {

        // Arbitrary quality factor safety measure. This might change in the future
        private final static float SAMPLES_TO_BIT_RATE_FACTOR = 1.5f;

        // Defaults
        private boolean afterBurner = true;
        private AACEncodingProfile profile = AACEncodingProfile.AAC_LC;
        private int channels = 2;
        private int bitRate = 64000;
        private int sampleRate = 44100;

        private void setEncoderParams(AACEncoder encoder) {
            FdkAACLibFacade.setEncoderParam(encoder, AACEncParam.AACENC_AFTERBURNER, afterBurner ? 1 : 0);
            FdkAACLibFacade.setEncoderParam(encoder, AACEncParam.AACENC_SAMPLERATE, sampleRate);
            FdkAACLibFacade.setEncoderParam(encoder, AACEncParam.AACENC_BITRATE, bitRate);
            FdkAACLibFacade.setEncoderParam(encoder, AACEncParam.AACENC_TRANSMUX, 2);
            FdkAACLibFacade.setEncoderParam(encoder, AACEncParam.AACENC_AOT, profile.getAot());
            FdkAACLibFacade.setEncoderParam(encoder, AACEncParam.AACENC_CHANNELORDER, WAV_INPUT_CHANNEL_ORDER);
            FdkAACLibFacade.setEncoderParam(encoder, AACEncParam.AACENC_CHANNELMODE, AACEncodingChannelMode.valueOf(channels).getMode());
        }

        private void adaptBitRate() {
            float minimalBitRate = channels * sampleRate / SAMPLES_TO_BIT_RATE_FACTOR;
            bitRate = minimalBitRate > bitRate ? (int) minimalBitRate : bitRate;
        }

        // TODO - add AAC profile verification
        public AACAudioEncoder build() {
            adaptBitRate();
            if (!SAMPLE_RATES.contains(sampleRate)) {
                throw new AACAudioEncoderException("sampleRate", sampleRate);
            } else if (!BITRATE_RANGE.contains(bitRate)) {
                throw new AACAudioEncoderException("bitRate", bitRate);
            } else if (AACEncodingChannelMode.valueOf(channels) == AACEncodingChannelMode.MODE_INVALID) {
                throw new AACAudioEncoderException("channels", channels);
            } else {
                AACEncoder encoder = FdkAACLibFacade.openEncoder(ENCODER_MODULES_MASK, MAX_ENCODER_CHANNELS);
                setEncoderParams(encoder);
                FdkAACLibFacade.initEncoder(encoder);
                AACEncInfo info = FdkAACLibFacade.getEncoderInfo(encoder);
                return new AACAudioEncoder(encoder, info);
            }
        }
    }

    public final AACAudioOutput encode(WAVAudioInput input, boolean conclude) throws AACAudioEncoderException {
        int read;
        AACAudioOutput.Accumulator accumlator = AACAudioOutput.accumulator();
        try {
            ByteArrayInputStream inputStream = new ByteArrayInputStream(input.data());
            byte[] buffer = new byte[inputBufferSize()];
            while ((read = inputStream.read(buffer)) != WAVAudioSupport.EOS) {
                populateInputBuffer(buffer, read);
                byte[] encoded = FdkAACLibFacade.encode(encoder, inBufferDescriptor, outBufferDescriptor, read)
                        .orElseThrow(() -> new IllegalStateException("No encoded audio data returned"));
                accumlator.accumulate(encoded);
            }
            if (conclude) {
                accumlator.accumulate(conclude().data());
            }
            return accumlator.done();
        } catch (IOException | RuntimeException x) {
            throw new AACAudioEncoderException("Could not encode WAV audio to AAC audio", x);
        }
    }

    public AACAudioOutput conclude() throws AACAudioEncoderException {
        Optional optional;
        try {
            inBufferDescriptor.clear();
            AACAudioOutput.Accumulator accumlator = AACAudioOutput.accumulator();
            while ((optional = FdkAACLibFacade.encode(encoder, inBufferDescriptor, outBufferDescriptor, WAVAudioSupport.EOS)).isPresent()) {
                accumlator.accumulate(optional.get());
            }
            return accumlator.done();
        } catch (RuntimeException x) {
            throw new AACAudioEncoderException("Could not conclude WAV audio to AAC audio", x);
        }
    }

    private void populateInputBuffer(byte[] buffer, int size) {
        inBuffer.write(0, buffer, 0, size);
        if (size != inputBufferSize) {
            inBufferDescriptor.bufSizes = new IntByReference(size);
            inBufferDescriptor.writeField("bufSizes");
        }
    }

    /*
     * This disables non-crucial synchronization for irrelevant structs.
     * In order to dramatically boost performance and solve JNA memory pressure issues
     */
    private void disableStructureSynchronization() {
        encoder.write();
        encoder.setAutoSynch(false);
        inBufferDescriptor.write();
        inBufferDescriptor.setAutoSynch(false);
        outBufferDescriptor.write();
        outBufferDescriptor.setAutoSynch(false);
    }

    @Override
    public void close() {
        FdkAACLibFacade.closeEncoder(encoder);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy