Skip to content

Commit

Permalink
Merge pull request #6 from sheinbergon/jmh-integration
Browse files Browse the repository at this point in the history
Jmh integration + Bug fixes
  • Loading branch information
sheinbergon authored Apr 3, 2018
2 parents ec21c2b + aab1c28 commit 991d4a4
Show file tree
Hide file tree
Showing 27 changed files with 322 additions and 127 deletions.
40 changes: 31 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ Artifacts are available on maven central:

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

Expand All @@ -40,15 +40,17 @@ for both Windows(64bit) and Linux(64bit) are provided through the use of *_class

##### Windows(64 bit)
```groovy
compile 'org.sheinbergon:jna-aac-encoder:0.1.2:win32-x86-64'
compile 'org.sheinbergon:jna-aac-encoder:0.1.3:win32-x86-64'
```
##### Linux(64 bit)
```groovy
compile 'org.sheinbergon:jna-aac-encoder:0.1.2:linux-x86-64'
compile 'org.sheinbergon:jna-aac-encoder:0.1.3:linux-x86-64'
```
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.

#### Additional information
* Provided fdk-aac version is 0.1.6
* This library was tested against 0.1.5/6
* 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.

### Encoding using the JVM AudioSystem
```java
Expand All @@ -57,24 +59,44 @@ File output = new File(...);
AudioSystem.write(input, AACFileTypes.AAC_LC, output);
```

## Performance
Performance benchmarks comparing JNA to a BINARY application(`aac-enc`) are available using [JMH](http://openjdk.java.net/projects/code-tools/jmh/) and [JMH Visualizer](https://github.com/jzillmann/jmh-visualizer):

![alt text](perf/jmh-results-04042018.png)

To run the benchmarks locally:
* Clone this repository to a Linux host
* Ensure that you have `libfdk-aac.so` library installed (either from an external repository or manually compiled) loadable
* Ensure that you have the `aac-enc` binary installed (either from an external repository or manually compiled)
* To execute the benchmark, run the following gradle command
```groovy
./gradlew -b perf.gradle jmh jmhReport
```
* If the aac-enc binary is not installed in /usr/bin/aac-enc, you can a custom path path by adding this gradle property:
```groovy
-PaacEncBin=/CUSTOM/PATH/TO/AAC-ENC
```
* The JMH reports can be viewed by opening `build/reports/perf/index.html` in your browser.

## Limitations
Currently, only pcm_s16le WAV input is supported, meaning:
* Sample size - 16 bit(signed)
* WAV format - PCM
* Byte order - Little Endian

While this seems to the common raw-audio formatting, it's important
While this seems to be the common raw-audio formatting, it's important
to note that providing input audio with different formatting will cause
the encoding process to fail.

Additional restrictions:
* A maximum of 6 audio input/output channels
* Only the AAC-LC/HE-AAC/HE-AACv2 encoding profiles are suuported


## Roadmap
* Improved lower-level interface (with examples).
* Performance tests/comparison (JMH).
* Support additiona WAV audio formats.
* Support additional WAV audio formats.
* Meta-data insertion.
* MacOS cross-compiling?
* AAC Decoding???
19 changes: 18 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.2'
version '0.1.3'

sourceCompatibility = 1.9

Expand All @@ -20,10 +20,20 @@ ext.testDepVersions = [
mediaInfo : '1.0.0.RELEASE'
]

ext.perfDepVersions = [
commonsIO: '2.6',
jmh : '1.20'
]

task wrapper(type: Wrapper) {
gradleVersion = '4.4.1'
}

sourceSets {
perf
}


dependencies {
// JNA
compile "net.java.dev.jna:jna:${depVersions.jna}"
Expand All @@ -42,4 +52,11 @@ dependencies {

// MediaInfo
testCompile "com.abercap:mediainfo-java-api:${testDepVersions.mediaInfo}"

// JMH Benchmarking
perfCompile project
perfCompile "org.openjdk.jmh:jmh-core:${perfDepVersions.jmh}"
perfCompile "org.openjdk.jmh:jmh-generator-annprocess:${perfDepVersions.jmh}"
perfCompile "commons-io:commons-io:${perfDepVersions.commonsIO}"

}
36 changes: 36 additions & 0 deletions perf.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import org.gradle.internal.os.OperatingSystem;

plugins {
id "java"
id "io.morethan.jmhreport" version "0.7.0"
}

apply from: 'common.gradle'

jmhReport {
jmhResultPath = project.file('build/reports/perf/result.json')
jmhReportOutput = project.file('build/reports/perf')
}

task jmh(type: JavaExec, description: 'Executing JMH benchmarks') {

classpath = sourceSets.perf.runtimeClasspath
main = 'org.openjdk.jmh.Main'

assert OperatingSystem.current().isLinux()

def aacEncBin = project.properties.get('aacEncBin', "/usr/bin/aac-enc")
assert file(aacEncBin).exists()

def format = project.properties.get('format', 'json');
def resultFile = file("build/reports/perf/result.${format}")
resultFile.parentFile.mkdirs()

systemProperties = [
'perf.aac.enc.bin': aacEncBin
]

args 'Benchmark.*'
args '-rf', format
args '-rff', resultFile
}
Binary file added perf/jmh-results-04042018.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 35 additions & 29 deletions src/main/java/org/sheinbergon/aac/encoder/AACAudioEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,29 @@
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.structure.*;
import org.sheinbergon.aac.jna.util.AACEncParam;

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

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

/**
* Safe, reasonable boundaries according to @see <a href="https://github.com/mstorsjo/fdk-aac/blob/v0.1.5/libAACenc/include/aacenc_lib.h">fdk-aac/libAACenc/include/aacenc_lib.h</a>
*/
private final static Range<Integer> BITRATE_RANGE = Range.between(64000, 640000);

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

// Some fdk-aac internal constants
private final static int PARAMETRIC_STEREO_CHANNEL_COUNT = 2;
private final static int ADTS_TRANSMUX = 2;
private final static int WAV_INPUT_CHANNEL_ORDER = 1;
private final static int MAX_ENCODER_CHANNELS = 0;
Expand All @@ -53,16 +47,20 @@ public class AACAudioEncoder implements AutoCloseable {
// Hard references are advised for memory buffers
private final Memory inBuffer;
private final Memory outBuffer;
private final AACEncInArgs inArgs;
private final AACEncOutArgs outArgs;
private final AACEncBufDesc inBufferDescriptor;
private final AACEncBufDesc outBufferDescriptor;

private boolean closed = false;
private volatile boolean closed = false;

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.inArgs = new AACEncInArgs();
this.outArgs = new AACEncOutArgs();
this.inBufferDescriptor = FdkAACLibFacade.inBufferDescriptor(inBuffer);
this.outBufferDescriptor = FdkAACLibFacade.outBufferDescriptor(outBuffer);
disableStructureSynchronization();
Expand All @@ -77,40 +75,42 @@ public static Builder builder() {
@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;
/**
* Reasonable minimal ratios according to @see <a href="https://github.com/mstorsjo/fdk-aac/blob/v0.1.6/libAACenc/include/aacenc_lib.h">fdk-aac/libAACenc/include/aacenc_lib.h</a>
*/
private final static Map<AACEncodingProfile, Float> SAMPLES_TO_BIT_RATE_RATIO = Map.of(
AACEncodingProfile.AAC_LC, 1.5f,
AACEncodingProfile.HE_AAC, 0.625f,
AACEncodingProfile.HE_AAC_V2, 0.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_BITRATE, deduceBitRate());
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());
}

private void adaptBitRate() {
float minimalBitRate = channels * sampleRate / SAMPLES_TO_BIT_RATE_FACTOR;
bitRate = minimalBitRate > bitRate ? (int) minimalBitRate : bitRate;
private int deduceBitRate() {
return (int) (channels * sampleRate * SAMPLES_TO_BIT_RATE_RATIO.get(profile));
}

// 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 if (profile == AACEncodingProfile.HE_AAC_V2 && channels != PARAMETRIC_STEREO_CHANNEL_COUNT) {
throw new AACAudioEncoderException("HE-AACv2 only supports 2 channels (stereo) mode");
} else {
AACEncoder encoder = FdkAACLibFacade.openEncoder(ENCODER_MODULES_MASK, MAX_ENCODER_CHANNELS);
setEncoderParams(encoder);
Expand All @@ -125,16 +125,17 @@ public final AACAudioOutput encode(WAVAudioInput input) throws AACAudioEncoderEx
int read;
verifyState();
try {
AACAudioOutput.Accumulator accumlator = AACAudioOutput.accumulator();
AACAudioOutput.Accumulator accumulator = AACAudioOutput.accumulator();
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)
byte[] encoded = FdkAACLibFacade.encode(encoder, inBufferDescriptor, outBufferDescriptor, inArgs, outArgs, read)
.orElseThrow(() -> new IllegalStateException("No encoded audio data returned"));
accumlator.accumulate(encoded);
accumulator.accumulate(encoded);
}
return accumlator.done();

return accumulator.done();
} catch (IOException | RuntimeException x) {
throw new AACAudioEncoderException("Could not encode WAV audio to AAC audio", x);
}
Expand All @@ -145,11 +146,11 @@ public final AACAudioOutput conclude() throws AACAudioEncoderException {
verifyState();
try {
inBufferDescriptor.clear();
AACAudioOutput.Accumulator accumlator = AACAudioOutput.accumulator();
while ((optional = FdkAACLibFacade.encode(encoder, inBufferDescriptor, outBufferDescriptor, WAVAudioSupport.EOS)).isPresent()) {
accumlator.accumulate(optional.get());
AACAudioOutput.Accumulator accumulator = AACAudioOutput.accumulator();
while ((optional = FdkAACLibFacade.encode(encoder, inBufferDescriptor, outBufferDescriptor, inArgs, outArgs, WAVAudioSupport.EOS)).isPresent()) {
accumulator.accumulate(optional.get());
}
return accumlator.done();
return accumulator.done();
} catch (RuntimeException x) {
throw new AACAudioEncoderException("Could not conclude WAV audio to AAC audio", x);
} finally {
Expand All @@ -170,12 +171,17 @@ private void populateInputBuffer(byte[] buffer, int size) {
* In order to dramatically(!!!) boost performance and solve JNA memory pressure issues
*/
private void disableStructureSynchronization() {
// These require writing them initialy prior to disable automatic synchronization
encoder.write();
encoder.setAutoSynch(false);
inBufferDescriptor.write();
inBufferDescriptor.setAutoSynch(false);
outBufferDescriptor.write();
outBufferDescriptor.setAutoSynch(false);

// In/Out args do not contain anything worth writing initially
inArgs.setAutoSynch(false);
outArgs.setAutoSynch(false);
}

private void verifyState() {
Expand Down
17 changes: 5 additions & 12 deletions src/main/java/org/sheinbergon/aac/encoder/AACAudioOutput.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import org.apache.commons.lang3.ArrayUtils;
import org.sheinbergon.aac.encoder.util.AACAudioOutputException;

@Getter
@Accessors(chain = true, fluent = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class AACAudioOutput {

public static Accumulator accumulator() {
Expand All @@ -32,17 +32,10 @@ void accumulate(byte[] data) {
}

AACAudioOutput done() {
AACAudioOutput output = new AACAudioOutput();
if (ArrayUtils.isNotEmpty(data)) {
output.data = data;
} else {
throw new AACAudioOutputException("data", "Empty/Null array");
}
output.length = length;
return output;
return new AACAudioOutput(data, length);
}
}

private byte[] data;
private int length;
private final byte[] data;
private final int length;
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ public static WAVAudioInput pcms16le(byte[] data, int length) {
.build();
}


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

This file was deleted.

Loading

0 comments on commit 991d4a4

Please sign in to comment.