Skip to content

Commit

Permalink
Merge pull request #2 from sheinbergon/stable-release
Browse files Browse the repository at this point in the history
Some bug fixes/behavioral guarantees.
  • Loading branch information
sheinbergon authored Mar 22, 2018
2 parents f159297 + 3218220 commit 5ac7656
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 30 deletions.
78 changes: 70 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,78 @@
# jna-aac-encoder

[![Build Status](https://travis-ci.org/sheinbergon/jna-aac-encoder.svg?branch=master)](https://travis-ci.org/sheinbergon/jna-aac-encoder) [![Coverage Status](https://coveralls.io/repos/github/sheinbergon/jna-aac-encoder/badge.svg)](https://coveralls.io/github/sheinbergon/jna-aac-encoder) [![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
[![Build Status](https://travis-ci.org/sheinbergon/jna-aac-encoder.svg?branch=master)](https://travis-ci.org/sheinbergon/jna-aac-encoder) [![Coverage Status](https://coveralls.io/repos/github/sheinbergon/jna-aac-encoder/badge.svg)](https://coveralls.io/github/sheinbergon/jna-aac-encoder) [![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.sheinbergon/jna-aac-encoder/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.sheinbergon/jna-aac-encoder)

This library provides AAC encoding capabilities for the JVM.
This library provides AAC encoding capabilities for the JVM.
It utilizes the [FDK AAC](https://github.com/mstorsjo/fdk-aac) library via JNA in order to do so.


## License
**Important!** While this library uses LGPL-3, please see
the [FDK AAC license](NOTICE) for additional information
regarding re/distribution and licensing limitations
regarding re/distribution and licensing limitations.

## Usage

#### Dependencies

Artifacts are available on maven central:

**_Gradle_**
```groovy
compile 'org.sheinbergon:jna-aac-encoder:0.1.1'
```
**_Maven_**
```xml
<dependency>
<groupId>org.sheinbergon</groupId>
<artifactId>jna-aac-encoder</artifactId>
<version>0.1.1</version>
</dependency>
```

#### Notice!!!

The **_libfdk-aac_** shared library so/dll/dylib file is required to be accessible
for dynamic loading upon execution. If using the above depdencey, you
need to make sure the shared library is installed as part of the runtime OS enviroment
and accessible to JNA. See [this](https://github.com/java-native-access/jna/blob/master/www/FrequentlyAskedQuestions.md#calling-nativeloadlibrary-causes-an-unsatisfiedlinkerror) link for additional information

To make things easier, cross compilations artifacts (containing the shared library)
for both Windows (64bit) and Linux(64bit) are provided through maven _classifiers_ :

##### Windows 64
**_Gradle_**
```groovy
compile 'org.sheinbergon:jna-aac-encoder:0.1.1:win32-x86-64'
```
**_Maven_**
```xml
<dependency>
<groupId>org.sheinbergon</groupId>
<artifactId>jna-aac-encoder</artifactId>
<version>0.1.1</version>
<classifier>win32-x86-64</classifier>
</dependency>
```
##### Linux 64
**_Gradle_**
```groovy
compile 'org.sheinbergon:jna-aac-encoder:0.1.1:linux-x86-64'
```
**_Maven_**
```xml
<dependency>
<groupId>org.sheinbergon</groupId>
<artifactId>jna-aac-encoder</artifactId>
<version>0.1.1</version>
<classifier>linux-x86-64</classifier>
</dependency>
```

32bit platform won't be supported for now.
OSX/Macos toolchain is a bit trickier, so you'll just have to pre-install the dylib in advance.


#### Java AudioSystem
```java
AudioInputStream input = AudioSystem.getAudioInputStream(...);
Expand All @@ -31,12 +92,13 @@ the encoding process to fail.

Additional restrictions:
* A maximum of 6 audio input/output channels
* Only AAC-LC(**L**ow **C**omplaxity) encoding profile is suuported
* Only the AAC-LC(**L**ow **C**omplaxity) encoding profile is suuported


## Roadmap
* Maven central artifacts.
* Support for AAC HE & HEv2.
* Improved lower-level interface(with examples).
* Improved lower-level interface (with examples).
* Performance tests/comparison (JMH).
* AAC Decoding ???
* Support for AAC HE & HEv2.
* Meta-data insertion.
* MacOS cross-compiling?
* AAC Decoding???
4 changes: 3 additions & 1 deletion common.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
group 'org.sheinbergon'
version '0.1.0'
version '0.1.1'

sourceCompatibility = 1.9

Expand All @@ -10,6 +10,7 @@ repositories {
ext.depVersions = [
jna : '4.5.1',
commonsLang3: '3.7',
jsr305 : '3.0.2',
lombok : '1.16.20'
]

Expand All @@ -29,6 +30,7 @@ dependencies {

// Utility
compile "org.apache.commons:commons-lang3:${depVersions.commonsLang3}"
compile "com.google.code.findbugs:jsr305:${depVersions.jsr305}"

// Lombok
compileOnly "org.projectlombok:lombok:${depVersions.lombok}"
Expand Down
37 changes: 28 additions & 9 deletions src/main/java/org/sheinbergon/aac/encoder/AACAudioEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
import org.sheinbergon.aac.jna.structure.AACEncoder;
import org.sheinbergon.aac.jna.util.AACEncParam;

import javax.annotation.concurrent.NotThreadSafe;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Optional;
import java.util.Set;

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

Expand All @@ -33,6 +35,8 @@ public class AACAudioEncoder implements AutoCloseable {

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

// Some fdk-aac internal constants
private final static int ADTS_TRANSMUX = 2;
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;
Expand All @@ -43,12 +47,17 @@ public class AACAudioEncoder implements AutoCloseable {
private final AACEncoder encoder;
@Getter
private final int inputBufferSize;

// These are all created upon construciton/build in order to utilize memory allocations efficiently
// ----------------------------------------------
// Hard references are advised for memory buffers
private final Memory inBuffer;
private final Memory outBuffer;
private final AACEncBufDesc inBufferDescriptor;
private final AACEncBufDesc outBufferDescriptor;

private boolean closed = false;

private AACAudioEncoder(AACEncoder encoder, AACEncInfo info) {
this.encoder = encoder;
this.inputBufferSize = info.inputChannels * info.frameLength * 2;
Expand Down Expand Up @@ -82,7 +91,7 @@ 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_TRANSMUX, ADTS_TRANSMUX);
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());
Expand Down Expand Up @@ -112,10 +121,11 @@ public AACAudioEncoder build() {
}
}

public final AACAudioOutput encode(WAVAudioInput input, boolean conclude) throws AACAudioEncoderException {
public final AACAudioOutput encode(WAVAudioInput input) throws AACAudioEncoderException {
int read;
AACAudioOutput.Accumulator accumlator = AACAudioOutput.accumulator();
verifyState();
try {
AACAudioOutput.Accumulator accumlator = AACAudioOutput.accumulator();
ByteArrayInputStream inputStream = new ByteArrayInputStream(input.data());
byte[] buffer = new byte[inputBufferSize()];
while ((read = inputStream.read(buffer)) != WAVAudioSupport.EOS) {
Expand All @@ -124,17 +134,15 @@ public final AACAudioOutput encode(WAVAudioInput input, boolean conclude) throws
.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 {
public final AACAudioOutput conclude() throws AACAudioEncoderException {
Optional<byte[]> optional;
verifyState();
try {
inBufferDescriptor.clear();
AACAudioOutput.Accumulator accumlator = AACAudioOutput.accumulator();
Expand All @@ -144,6 +152,8 @@ public AACAudioOutput conclude() throws AACAudioEncoderException {
return accumlator.done();
} catch (RuntimeException x) {
throw new AACAudioEncoderException("Could not conclude WAV audio to AAC audio", x);
} finally {
close(); // Once conclusion has taken place, this encoder instance should be discarded
}
}

Expand All @@ -157,7 +167,7 @@ private void populateInputBuffer(byte[] buffer, int size) {

/*
* This disables non-crucial synchronization for irrelevant structs.
* In order to dramatically boost performance and solve JNA memory pressure issues
* In order to dramatically(!!!) boost performance and solve JNA memory pressure issues
*/
private void disableStructureSynchronization() {
encoder.write();
Expand All @@ -168,8 +178,17 @@ private void disableStructureSynchronization() {
outBufferDescriptor.setAutoSynch(false);
}

private void verifyState() {
if (closed) {
throw new AACAudioEncoderException("Encoder instance already closed");
}
}

@Override
public void close() {
FdkAACLibFacade.closeEncoder(encoder);
if (!closed) {
FdkAACLibFacade.closeEncoder(encoder);
closed = true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ static class Accumulator {
private byte[] data = null;
private int length = 0;

// It is guaranteed the encoded bytes array is properly sized to exactly match its actual content
void accumulate(byte[] data) {
if (data.length != 0) {
this.data = ArrayUtils.addAll(this.data, data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ public class AACAudioEncoderException extends RuntimeException {
public AACAudioEncoderException(String message, Throwable cause) {
super(message, cause);
}


public AACAudioEncoderException(String message) {
super(message);
}

public AACAudioEncoderException(String paramter, int value) {
super(String.format("Invalid encoder parameter '%s' - %d", paramter, value));
}
Expand Down
1 change: 0 additions & 1 deletion src/main/java/org/sheinbergon/aac/jna/FdkAACLib.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ public class FdkAACLib {
@RequiredArgsConstructor
enum Methods {
INFO("aacEncInfo"),
GET_LIB_INFO("aacEncGetLibInfo"),
SET_PARAM("aacEncoder_SetParam"),
OPEN("accEncOpen"),
CLOSE("accEncClose"),
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/org/sheinbergon/aac/jna/FdkAACLibFacade.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,15 @@ public static void initEncoder(AACEncoder encoder) {
verifyResult(result, FdkAACLib.Methods.ENCODE);
}

// TODO - There's really no reason why in/out args structs would be reallocated over and over again. They should be allocated once per encoder externally
public static Optional<byte[]> encode(AACEncoder encoder, AACEncBufDesc inBufferDescriptor, AACEncBufDesc outBufferDescriptor, int size) {
AACEncInArgs inArgs = new AACEncInArgs();
AACEncOutArgs outArgs = new AACEncOutArgs();
inArgs.numInSamples = (size == WAVAudioSupport.EOS) ? size : size / IN_SAMPLES_DIVISOR;
return Optional.ofNullable(AACEncError.valueOf(FdkAACLib.aacEncEncode(encoder, inBufferDescriptor, outBufferDescriptor, inArgs, outArgs)))
.filter(result -> result != AACEncError.AACENC_ENCODE_EOF)
.map(result -> {
verifyResult(result, FdkAACLib.Methods.GET_LIB_INFO);
verifyResult(result, FdkAACLib.Methods.ENCODE);
return outBufferDescriptor.bufs
.getValue().getByteArray(0, outArgs.numOutBytes);
});
Expand Down
15 changes: 11 additions & 4 deletions src/main/java/org/sheinbergon/aac/sound/AACFileWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,23 @@ private int readBufferSize(AudioFormat format, AACAudioEncoder encoder) {
}

private int encodeAndWrite(AudioInputStream input, AudioFileFormat.Type type, OutputStream output) throws IOException {
boolean concluded = false;
int read, encoded = 0;
AudioFormat format = input.getFormat();
AACAudioEncoder encoder = encoder(format, type);
try (encoder) {
int readBufferSize = readBufferSize(format, encoder);
byte[] readBuffer = new byte[readBufferSize];
while ((read = input.read(readBuffer)) != WAVAudioSupport.EOS) {
WAVAudioInput audioInput = WAVAudioInput.pcms16le(readBuffer, read);
boolean conclude = read < readBufferSize;
AACAudioOutput audioOutput = encoder.encode(audioInput, conclude);
AACAudioOutput audioOutput;
while (!concluded) {
read = input.read(readBuffer);
if (read == WAVAudioSupport.EOS) {
audioOutput = encoder.conclude();
concluded = true;
} else {
WAVAudioInput audioInput = WAVAudioInput.pcms16le(readBuffer, read);
audioOutput = encoder.encode(audioInput);
}
encoded += audioOutput.length();
output.write(audioOutput.data());
}
Expand Down
22 changes: 18 additions & 4 deletions src/test/java/org/sheinbergon/aac/encoder/AACAudioEncoderTest.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package org.sheinbergon.aac.encoder;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.*;
import org.sheinbergon.aac.encoder.util.AACAudioEncoderException;

import java.util.Optional;


@DisplayName("AAC audio encoder")
public class AACAudioEncoderTest {
Expand All @@ -18,12 +17,18 @@ public class AACAudioEncoderTest {
private final static int VALID_BITRATE = 64000;

private AACAudioEncoder.Builder builder;
private AACAudioEncoder encoder;

@BeforeEach
public void setup() {
builder = AACAudioEncoder.builder();
}

@AfterEach
public void teardown() {
Optional.ofNullable(encoder).ifPresent(AACAudioEncoder::close);
}

@Test
public void invalidSampleRate() {
Assertions.assertThrows(AACAudioEncoderException.class, () ->
Expand All @@ -47,4 +52,13 @@ public void invalidChannelCount() {
.channels(INVALID_CHANNEL_COUNT)
.build());
}

@Test
public void closedEncoderEncoding() {
Assertions.assertThrows(AACAudioEncoderException.class, () -> {
encoder = builder.build();
encoder.close();
encoder.conclude();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public void supportedAudioOutputStreamEncoding() throws UnsupportedAudioFileExce
MediaInfoSupport.assertAACOutput(aac, input.getFormat(), AACEncodingProfile.AAC_LC);
} finally {
if (aac.exists()) {
Files.delete(aac.toPath());
// Files.delete(aac.toPath());
}
}
}
Expand Down

0 comments on commit 5ac7656

Please sign in to comment.