Робота зі звуком і бібліотека SuperPowered

Переді мною поставлено завдання: потрібно розробити програму, яка буде здійснювати запис з мікрофону, потім зміна (прискорення або pitch shifting), збереження ефекту в самому файлі і відправка результуючого МР3-файлу на сервері програми. Завдання це виходить комплексна. Причому ще і min-sdk=9 хочуть.
Для запису звуку, щоб по-простіше, зі старту напрошується клас MediaPlayer. Пише з мікрофона, при цьому виключно відразу в стислому вигляді, ААС наприклад. Якщо хто не знає: МР3-енкодера в Андроїд немає, т. к. там ліцуха комерційна, а є тільки декодер МР3, соотв. ніяк не можна записати одразу в МР3, а можна тільки програвати, для чого декодер і потрібен.
Все б добре, та тільки для того, щоб зі звуком можна було щось робити, а саме — накласти який-небудь ефект, його потрібно записувати в первозданному, так би мовити, вигляді, тобто не в стислому до МР3 або ААС, а саме в PCM/WAVE-форматі. А для цього клас MediaPlayer вже не годиться, т. к. пише тільки в стислому вигляді або доведеться додати собі роботи: записувати в MediaPlayer-е в стислому вигляді, а потім розпаковувати до PCM/WAVE, чого в Андроїді не передбачено і тому доведеться ще й цим собі голову морочити — шукати рішення, так і знову ж зайвий раз батарею садити, жадібний до обчислювальних ресурсів процес розпакування, так і користувачеві не сподобається чекати зайвий раз.
З усього цього випливає, що запис треба здійснювати використовуючи інший клас — AudioTrack, він і пише і відтворює і прискорення на ньому не проблема при відтворенні.
Однак на практиці з цим класом проблем теж дуже багато. По-перше AudioTrack пише відразу дані РСМ, а сам заголовок WAVE він не створює, тобто він пише RAW PCM, а щоб потім ці дані можна було використовувати не тільки в АТ, потрібно додати код для створення цього заголовку, плюс не забувати при програванні перестрибнути перші 44 байта (розмір заголовка), щоб АТ їх не намагався відтворювати. Всі рухи по запису і відтворення треба виносити в окремий потік, що ніяк не спрощує розробку. Приклад винесеного в окремий клас рекордера я наведу, так як за більшу частину він все одно не мій, а з надр SO скопійований (включає створення заголовка і може стати в нагоді кому-то):
AudioRecorder
import android.annotation.SuppressLint;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;

import com.stanko.tools.DeviceInfo;
import com.stanko.tools.Log;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;

public class AudioRecorder
{
/**
* INITIALIZING : recorder is initializing;
* READY : recorder has been initialized, recorder not yet started
* RECORDING : recording
* ERROR : reconstruction needed
* STOPPED: reset needed
*/
public enum State {INITIALIZING, READY, RECORDING, ERROR, STOPPED};

public static final boolean RECORDING_UNCOMPRESSED = true;
public static final boolean RECORDING_COMPRESSED = false;

// The interval in which the recorded samples are output to the file
// Used only in uncompressed mode
private static final int TIMER_INTERVAL = 120;

// Toggles uncompressed recording on/off; RECORDING_UNCOMPRESSED / RECORDING_COMPRESSED
private boolean isUncompressed;

// Recorder used for recording uncompressed
private AudioRecord mAudioRecorder = null;
// Recorder used for recording compressed
private MediaRecorder mMediaRecorder = null;

// Stores current amplitude (only in uncompressed mode)
private int cAmplitude= 0;
// Output file path
private String mFilePath = null;

// Recorder state; see State
private State state;

// File writer (only in uncompressed mode)
private RandomAccessFile mFileWriter;

// Number of channels, sample rate, sample size(size in bits), buffer size, audio source, sample size(see AudioFormat)
private short nChannels;
private int nRate;
private short nSamples;
private int nBufferSize;
private int nSource;
private int nFormat;

// Number of frames written to file on each output(only in uncompressed mode)
private int nFramePeriod;

// Buffer for output(only in uncompressed mode)
private byte[] mBuffer;

// Number of bytes written to file after header(only in uncompressed mode)
// after stop() is called, this size is written to the header/data chunk in the wave file
private int nPayloadSize;

/**
* 
* Returns the state of the recorder in a RehearsalAudioRecord.State typed object.
* Useful, as no exceptions are thrown.
* 
* @return recorder state
*/
public State getState()
{
return state;
}

/*
* 
* Method used for recording.
* 
*/
private AudioRecord.OnRecordPositionUpdateListener updateListener = new AudioRecord.OnRecordPositionUpdateListener()
{
public void onPeriodicNotification(AudioRecord recorder)
{
mAudioRecorder.read(mBuffer, 0, mBuffer.length); // Fill buffer
try
{ 
mFileWriter.write(mBuffer); // buffer to Write file
nPayloadSize += mBuffer.length;
if (nSamples == 16)
{
for (int i=0; i<mBuffer.length/2; i++)
{ // 16bit sample size
short curSample = getShort(mBuffer[i*2], mBuffer[i*2+1]);
if (curSample > cAmplitude)
{ // Check amplitude
cAmplitude = curSample;
}
}
}
else
{ // 8bit sample size
for (int i=0; i<mBuffer.length; i++)
{
if (mBuffer[i] > cAmplitude)
{ // Check amplitude
cAmplitude = mBuffer[i];
}
}
}
}
catch (IOException e)
{
Log.e(this, "Error occured in updateListener, recording is aborted");
stop();
}
}

public void onMarkerReached(AudioRecord recorder)
{
// NOT USED
}
};

/** 
* 
* 
* Default constructor
* 
* Instantiates a new recorder, in case of compressed recording the parameters can be left as 0.
* In case of errors, no exception is thrown, but the state is set to ERROR
* 
*/ 
@SuppressLint("InlinedApi")
public AudioRecorder(boolean uncompressed, int audioSource, int sampleRate, int channelConfig, int audioFormat)
{
try
{
isUncompressed = uncompressed;
if (isUncompressed)
{ // RECORDING_UNCOMPRESSED
if (audioFormat == AudioFormat.ENCODING_PCM_16BIT)
{
nSamples = 16;
}
else
{
nSamples = 8;
}

if (channelConfig == AudioFormat.CHANNEL_IN_MONO)
{
nChannels = 1;
}
else
{
nChannels = 2;
}

nSource = audioSource;
nRate = sampleRate;
nFormat = audioFormat;

nFramePeriod = sampleRate * TIMER_INTERVAL / 1000;
nBufferSize = nFramePeriod * 2 * nSamples * nChannels / 8;
if (nBufferSize < AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat))
{ // Check to make sure buffer size is not smaller than the smallest allowed one 
nBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
// Set frame period and interval timer accordingly
nFramePeriod = nBufferSize / ( 2 * nSamples * nChannels / 8 );
Log.w(this, "Increasing buffer size to " + Integer.toString(nBufferSize));
}

mAudioRecorder = new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat, nBufferSize);
if (mAudioRecorder.getState() != AudioRecord.STATE_INITIALIZED)
throw new Exception("AudioRecord initialization failed");
mAudioRecorder.setRecordPositionUpdateListener(updateListener);
mAudioRecorder.setPositionNotificationPeriod(nFramePeriod);
} else
{ // RECORDING_COMPRESSED
mMediaRecorder = new MediaRecorder();
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
if (DeviceInfo.hasAPI10())
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
else
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
}
cAmplitude = 0;
mFilePath = null;
state = State.INITIALIZING;
} catch (Exception e)
{
if (e.getMessage() != null)
{
Log.e(this, e.getMessage());
}
else
{
Log.e(this, "Unknown error occured while initializing recording");
}
state = State.ERROR;
}
}

/**
* Sets output file path, call directly after construction/reset.
* 
* @param output file path
* 
*/
public void setOutputFile(File file){
setOutputFile(file.getAbsolutePath());
}

public void setOutputFile(String argPath)
{
try
{
if (state == State.INITIALIZING)
{
mFilePath = argPath;
if (!isUncompressed)
{
mMediaRecorder.setOutputFile(mFilePath);
}
}
}
catch (Exception e)
{
if (e.getMessage() != null)
{
Log.e(this, e.getMessage());
}
else
{
Log.e(this, "Unknown error occured while setting output path");
}
state = State.ERROR;
}
}

/**
* 
* Returns the largest amplitude sampled since the last call to this method.
* 
* @return returns the largest amplitude since the last call, or 0 when not in recording state. 
* 
*/
public int getMaxAmplitude()
{
if (state == State.RECORDING)
{
if (isUncompressed)
{
int result = cAmplitude;
cAmplitude = 0;
return result;
}
else
{
try
{
return mMediaRecorder.getMaxAmplitude();
}
catch (IllegalStateException e)
{
return 0;
}
}
}
else
{
return 0;
}
}


/**
* 
* Prepares the recorder for recording in the case recorder is not in the INITIALIZING state and the file path was not set
* the recorder is set to the ERROR state, which makes a reconstruction necessary.
* In case uncompressed recording is toggled, the header of the wave file is written.
* In case of an exception, the state is changed to ERROR
* 
*/
public void prepare()
{
try
{
if (state == State.INITIALIZING)
{
if (isUncompressed)
{
if ((mAudioRecorder.getState() == AudioRecord.STATE_INITIALIZED) & (mFilePath != null))
{
// write file header
Log.w(this,"prepare(): nRate: "+nRate+" nChannels: "+nChannels);

mFileWriter = new RandomAccessFile(mFilePath, "rw");

mFileWriter.setLength(0); // Set file length 0 to, to prevent unexpected behavior in case the file already existed
mFileWriter.writeBytes("RIFF"); // 4
mFileWriter.writeInt(0); // 4 Final file size not known yet, write 0 
mFileWriter.writeBytes("WAVE"); // 4
mFileWriter.writeBytes("fmt "); // 4
mFileWriter.writeInt(Integer.reverseBytes(16)); // 4 Sub-chunk size, 16 for PCM
mFileWriter.writeShort(Short.reverseBytes((short) 1)); // 2 AudioFormat, 1 for PCM
mFileWriter.writeShort(Short.reverseBytes(nChannels)); // 2 Number of channels, 1 for mono, 2 for stereo
mFileWriter.writeInt(Integer.reverseBytes(nRate)); // 4 Sample rate
mFileWriter.writeInt(Integer.reverseBytes(nRate*nSamples*nChannels/8)); // 4 Byte rate, SampleRate*NumberOfChannels*BitsPerSample/8
mFileWriter.writeShort(Short.reverseBytes((short)(nChannels*nSamples/8))); // 2 Block align, NumberOfChannels*BitsPerSample/8
mFileWriter.writeShort(Short.reverseBytes(nSamples)); // 2 Bits per sample
mFileWriter.writeBytes("data"); // 4
mFileWriter.writeInt(0); // 4 Data chunk size not known yet, write 0

mBuffer = new byte[nFramePeriod*nSamples/8*nChannels];
state = State.READY;
}
else
{
Log.e(this, "prepare() method called on uninitialized recorder");
state = State.ERROR;
}
}
else
{
mMediaRecorder.prepare();
state = State.READY;
}
}
else
{
Log.e(this, "prepare() method called on illegal state");
release();
state = State.ERROR;
}
}
catch(Exception e)
{
if (e.getMessage() != null)
{
Log.e(this, e.getMessage());
}
else
{
Log.e(this, "Unknown error occured in prepare()");
}
state = State.ERROR;
}
}

/**
* 
* 
* Releases the resources associated with this class and removes the unnecessary files, when necessary
* 
*/
public void release()
{
if (state == State.RECORDING)
{
stop();
}
else
{
if ((state == State.READY) & (isUncompressed))
{
try
{
mFileWriter.close(); // Remove file prepared
}
catch (IOException e)
{
Log.e(this, "I/O exception occured while closing output file");
}
(new File(mFilePath)).delete();
}
}

if (isUncompressed)
{
if (mAudioRecorder != null)
{
mAudioRecorder.release();
}
}
else
{
if (mMediaRecorder != null)
{
mMediaRecorder.release();
}
}
}

/**
* 
* 
* Resets the recorder to INITIALIZING the state, as if it was just created.
* In case the class was in RECORDING state, the recording is stopped.
* In case of exceptions the class is set to the ERROR state.
* 
*/
public void reset()
{
try
{
if (state != State.ERROR)
{
release();
mFilePath = null; // Reset file path
cAmplitude = 0; // Reset amplitude
if (isUncompressed)
{
mAudioRecorder = new AudioRecord(nSource, nRate, nChannels+1, nFormat, nBufferSize);
}
else
{
mMediaRecorder = new MediaRecorder();
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
}
state = State.INITIALIZING;
}
}
catch (Exception e)
{
Log.e(this, e.getMessage());
state = State.ERROR;
}
}

/**
* 
* 
* Starts the recording, sets and the state to RECORDING.
* Call after prepare().
* 
*/
public void start()
{
if (state == State.READY)
{
if (isUncompressed)
{
nPayloadSize = 0;
mAudioRecorder.startRecording();
mAudioRecorder.read(mBuffer, 0, mBuffer.length);
}
else
{
mMediaRecorder.start();
}
state = State.RECORDING;
}
else
{
Log.e(this, "start() called on illegal state");
state = State.ERROR;
}
}

/**
* 
* 
* Stops the recording, sets and the state to STOPPED.
* In case of further usage, a reset is needed.
* Also finalizes the wave file in case of uncompressed recording.
* 
*/
public void stop()
{
if (state == State.RECORDING)
{
if (isUncompressed)
{
mAudioRecorder.stop();
mAudioRecorder.setRecordPositionUpdateListener(null);
try
{
mFileWriter.seek(4); // size to Write header RIFF
mFileWriter.writeInt(Integer.reverseBytes(36+nPayloadSize));

mFileWriter.seek(40); // Write size to field Subchunk2Size
mFileWriter.writeInt(Integer.reverseBytes(nPayloadSize));

mFileWriter.close();
Log.w(this, "Recording stopped successfully");
}
catch(IOException e)
{
Log.e(this, "I/O exception occured while closing output file");
state = State.ERROR;
}
}
else
{
mMediaRecorder.stop();
}
state = State.STOPPED;
}
else
{
Log.e(this, "stop() called on illegal state");
state = State.ERROR;
}
}

/* 
* 
* Converts a byte[2] to a short, in format LITTLE_ENDIAN
* 
*/
private short getShort(byte argB1, byte argB2)
{
return (short)(argB1 | (argB2 << 8));
}
} 

Приклад використання при запису:
AudioThread
/*
* Thread to manage live recording/playback of voice input from the device's microphone.
*/
private static final int[] sampleRates= {44100, 22050, 16000, 11025, 8000};
protected int usedSampleRate;
private class AudioThread extends Thread {
private final File targetFile;

private static final String TAG = "AudioThread";

/**
* Give the thread high priority so that it's not canceled unexpectedly, and start it
*/
private AudioThread(final File file) {
targetFile = file;
}

@Override
public void run() {
Log.i(TAG, "Running Audio Thread");

Looper.prepare();

int i = 0;
do {
usedSampleRate = sampleRates[i];
if (audioRecorder != null)
audioRecorder.release();

audioRecorder = new AudioRecorder(true,
AudioSource.MIC,
usedSampleRate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT);
}
while ((i ++< sampleRates.length) && !(audioRecorder.getState() == AudioRecorder.State.INITIALIZING));


Log.i(this, "usedSampleRate: " + usedSampleRate + " setOutputFile: " + targetFile);

try {
audioRecorder.setOutputFile(targetFile);

// start the recording
audioRecorder.prepare();
audioRecorder.start();
// if error occurred and thus recording is not started
if (audioRecorder.getState() == AudioRecorder.State.ERROR) {
Toast.makeText(getBaseContext(), "AudioRecorder error", Toast.LENGTH_SHORT).show();
}
} catch (NullPointerException ignored){} // audioRecorder became null since it was canceled
Looper.loop();
}
}

Для відтворення:
playerPlayUsingAudioTrack
/*
* Thread to manage playback of recorded message.
*/
private int bufferSize;
protected int byteOffset;
protected int fileLengh; 
public void playerPlayUsingAudioTrack(File messageFileWav) {
if (messageFileWav == null || !messageFileWav.exists() || !messageFileWav.canRead()) {
Toast.makeText( getBaseContext(),
"Audiofile error: exists(): "
+ messageFileWav.exists() + " canRead(): "
+ messageFileWav.canRead(), Toast.LENGTH_SHORT).show();
return;
}

// is previous thread alive?
if (audioTrackThread!=null){
audioTrackThread.isStopped = true;
audioTrackThread = null;
}

bufferSize = AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT) * 4;

audioTrackThread = new StoppableThread(){

@Override
public void run() {

audioTrack = new AudioTrack(
AudioManager.STREAM_MUSIC,
sampleRate,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT, 
bufferSize,
AudioTrack.MODE_STREAM);

fileLengh = (int) messageFileWav.length();
sbPlayerProgress.setMax(fileLengh / 2);

int byteCount = 4 * 1024; // 4 kb
final byte[] byteData = new byte[byteCount];
// Reading the file..
RandomAccessFile in = null;
try {

in = new RandomAccessFile(messageFileWav, "r");
int ret;
byteOffset = 44;
audioTrack.play();
isPaused = false;
isPlayerPlaying = true;

while (byteOffset < fileLengh) {
if (this.isStopped)
break;
if(isPlayerPaused || this.isPaused)
continue;

in.seek(byteOffset);
ret = in. read(byteData, 0, byteCount);

if (ret != -1) { // Write the byte array to the track
audioTrack.write(byteData, 0, ret);
audioTrack.setPlaybackRate(pitchValue);
byteOffset += ret;
} else
break;
}
} catch (Exception e) {
//IOException, FileNotFoundException, NPE for audioTrack
e.printStackTrace();
} finally {
if (in != null)
try {
in.close();
} catch (IOException ignored) {
}
}
}

};
audioTrackThread.start();
}

Загалом записати виходить, відтворити — виходить, типу пітч-ефект виходить — audioTrack.setPlaybackRate(pitchValue) — ця змінна у мене прив'язана до SeekBar і користувач може прямо під час відтворення її значення змінювати за смаком. Незрозуміло тільки як зберегти ефект обраного рівня у файлі… Мабуть треба щось на З/NDK ліпити спеціалізоване.
Але взагалі є сюрпризи і крім цього: хто має соотв. досвід, той знає, що всякі девайси від Samsung, HTC та інших брендів не є 100% generic Андроїд сумісними. У кожного бренду є свої «покращення» на рівні вихідного коду ОС, з-за яких документований на гуглі код тупо не буде працювати взагалі, або як очікується, і особливо це пов'язано з медіа, а тому потрібні різного роду милиці споруджувати під ці девайси.
Наприклад, на Samsung-е проблеми з відтворенням потокового аудіо, тобто використовуючи клас MediaPlayer і вказавши йому за джерело HTTP-посилання на файл МР3 (а саме так і планується потім завантажені на сервер додатка аудіо-файли програвати), Samsung-і будуть грати це випадковим чином — то грати, то не грати, хоча будь-які інші девайси з тим же вихідним кодом програми та іншими однаковими умовами будуть завжди відтворювати нормально, а милиця полягає у завантаженні файлу частинами в окремому потоці і згодовування на програвання вже як би локально записаного файлу. А ще Samsung проковтують ідеальні паузи в МР3, ну коли ідеальна тиша (в редакторі навіть wave form не малюється), вони просто їх пропускають, з-за чого 5 — хвилинну доповідь по-перше відтворюється неприродно, а по-друге тривалість виходить не 5 хвилин, а 4 або менше (залежить від тривалості пауз між фразами), чого ніякі інші пристрої не роблять. Милиця: додавати білий шум в паузи.
На деяких моделях НТС — проблеми з записом аудіо, коли запис з допомогою MediaPlayer працює нормально, через AudioTrack — ні, насправді там проблема із записом накопиченого буфера, просто updateListener (див. код AudioRecoder) не спрацьовує, на інших девайсах цей лиснер працює, а на НТС — ні, ну, відрізнятися ж якось треба? Ну і ось. Милицю і тут можна спорудити, так вилазять інші проблеми на різних інших брендах є й інші проблеми, наприклад, непідтримки частоти дискретизації 48 кГц або 44,1 кГц або різних поєднань налаштувань запису, типу моно не пишемо, а тільки стерео, інші — навпаки. Загалом дана тема в андроїді той ще баттхерт і як-то не хочеться знаходити все нові сумісні пристрої і городити нові милиці під них.
Найсмішніше тут те, що будь-які китайські смартфони, крім, звичайно, брендів типу Meizu, будуть сумісні з Андроїд порівняно з брендами ринку, т. к. вони тупо не заморочуються з кастомизированием ОС, ну або грошей на це у них немає.

І ось, в черговий раз під час пошуку якихось ще рішень з цього приводу, бажано дуже альтернативних, а не врапперов навколо згаданих двох класів (я ще не згадав SoundPool, але цей клас просто не годиться для вирішення моєї задачі з-за своїх обмежень), я натрапив на SuperPowered. SKD дають безкоштовно, треба тільки зареєструватися, крос-платформний, що важливо, так як мені додаток якраз треба і під АОS і під iОS.
Подивився я демо-відео на сайті, м'яко кажучи надихнувся (а в реалі просто офігів був украй здивований), зареєструвався, скачав СДК і семпл.
Перше, що хочу відзначити — AndroidStudio в прольоті, оскільки «раптово» проект виявився заточений під NDK і AS з цим виникли проблеми, витрачати час на вирішення яких у мене не було абсолютно ніякого бажання (ну не подужала AS нормально імпортувати проект). Тому проект був відкритий в Eclipse без проблем і без проблем же він був запущений, благо раніше я завантажив і встановив NDK (вказівка шляху до якого AS не допомогло особливо).
Сэмлп я запустив на трупі, за нинішніми мірками — HTC HD2 з встановленим на ньому MIUI з версією відра 2.3.5. Девайс в роботі показує себе тим ще гальмом навіть на абсолютно неважких аппах, навіть на звичайних, де просто відображаються списки з картинками. Antutu на ньому дає якісь абсолютно смішні цифри й радить викинути, а після чергового апгрейда взагалі тупо висне і вішає телефон так, що доводиться виймати батарею. За те на ньому добре видно хто знає про існування ViewHolder, а хто — ні.
Так от, цей семпл на ньому запустився без проблем і працює навіть без натяків на лаги! Тобто я переконався, що таки так, ця бібла дійсно Low Latency! Та й саме прикольне додаток — можна відчути себе DJ-єм на пару хвилин, я з ним тиждень «як із писаної торбою» носився на радощах.
Однак, копнувши глибше, радості мої притихли, т. к. я виявив, що під Андроїд там все і обмежилося цим семплом, хоча для iOS прикладів куди більше, і щоб повноцінно використовувати цю бібліотеку треба самому писати JNI, чого я не вмію і, власне, я пишу цю статтю з метою, що зацікавлені хабровчане розвинуть цю тему. Сам-то я не сишник, але в принципі додав за аналогією ще кілька ефектів до тих трьох, що там є, вони, правда, мало чого цікавого додають, але працюють, плюс дрібний фікс додав — при догляді на фон і повернення в семпли починалася накладка з-за того, що відтворення не зупинялося:
SuperpoweredExample.h
#include "SuperpoweredExample.h"
#include <jni.h>
#include <stdlib.h>
#include < stdio.h>
#include <android/log.h>

static void playerEventCallbackA(void *clientData, SuperpoweredAdvancedAudioPlayerEvent event, void *value) {
if (event == SuperpoweredAdvancedAudioPlayerEvent_loadsuccess) {
SuperpoweredAdvancedAudioPlayer *playerA = *((SuperpoweredAdvancedAudioPlayer **)clientData);
playerA->setBpm(126.0 f);
playerA->setFirstBeatMs(353);
playerA->setPosition(playerA->firstBeatMs, false, false);
};
}

static void playerEventCallbackB(void *clientData, SuperpoweredAdvancedAudioPlayerEvent event, void *value) {
if (event == SuperpoweredAdvancedAudioPlayerEvent_loadsuccess) {
SuperpoweredAdvancedAudioPlayer *playerB = *((SuperpoweredAdvancedAudioPlayer **)clientData);
playerB->setBpm(123.0 f);
playerB->setFirstBeatMs(40);
playerB->setPosition(playerB->firstBeatMs, false, false);
};
}

static void openSLESCallback(SLAndroidSimpleBufferQueueItf caller, void *pContext) {
((SuperpoweredExample *)pContext)->process(caller);
}

static const SLboolean requireds[2] = { SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE };

SuperpoweredExample::SuperpoweredExample(const char *path, int *params) : currentBuffer(0), buffersize(params[5]), activeFx(0), crossValue(0.0 f), volB(0.0 f), volA(1.0 f * headroom) {
pthread_mutex_init(&mutex, NULL); // This will keep our player volumes and playback states in sync.

for (int n = 0; n < NUM_BUFFERS; n++) outputBuffer[n] = (float *)memalign(16, (buffersize + 16) * sizeof(float) * 2);

unsigned int samplerate = params[4];

playerA = new SuperpoweredAdvancedAudioPlayer(&playerA , playerEventCallbackA, samplerate, 0);
playerA->open(path, params[0], params[1]);
playerB = new SuperpoweredAdvancedAudioPlayer(&playerB, playerEventCallbackB, samplerate, 0);
playerB->open(path, params[2], params[3]);

playerA->syncMode = playerB->syncMode = SuperpoweredAdvancedAudioPlayerSyncmode_tempoandbeat;

roll = new SuperpoweredRoll(samplerate);
filter = new SuperpoweredFilter(SuperpoweredFilter_Resonant_Lowpass, samplerate);
flanger = new SuperpoweredFlanger(samplerate);

whoosh = new SuperpoweredWhoosh(samplerate);
gate = new SuperpoweredGate(samplerate);
echo = new SuperpoweredEcho(samplerate);
reverb = new SuperpoweredReverb(samplerate);
//stretch = new SuperpoweredTimeStretching(samplerate);

mixer = new SuperpoweredStereoMixer();

// Create the OpenSL ES engine.
slCreateEngine(&openSLEngine, 0, NULL, 0, NULL, NULL);
(*openSLEngine)->Realize(openSLEngine, SL_BOOLEAN_FALSE);
SLEngineItf openSLEngineInterface = NULL;
(*openSLEngine)->GetInterface(openSLEngine, SL_IID_ENGINE, &openSLEngineInterface);
// Create the output mix.
(*openSLEngineInterface)->CreateOutputMix(openSLEngineInterface, &outputMix, 0, NULL, NULL);
(*outputMix)->Realize(outputMix, SL_BOOLEAN_FALSE);
SLDataLocator_OutputMix outputMixLocator = { SL_DATALOCATOR_OUTPUTMIX, outputMix };

// Create the buffer queue player.
SLDataLocator_AndroidSimpleBufferqueue bufferPlayerLocator = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, NUM_BUFFERS };
SLDataFormat_PCM bufferPlayerFormat = { SL_DATAFORMAT_PCM, 2, samplerate * 1000, SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, SL_BYTEORDER_LITTLEENDIAN };
SLDataSource bufferPlayerSource = { &bufferPlayerLocator, &bufferPlayerFormat };
const SLInterfaceID bufferPlayerInterfaces[1] = { SL_IID_BUFFERQUEUE };
SLDataSink bufferPlayerOutput = { &outputMixLocator, NULL };
(*openSLEngineInterface)->CreateAudioPlayer(openSLEngineInterface, &bufferPlayer, &bufferPlayerSource, &bufferPlayerOutput, 1, bufferPlayerInterfaces, requireds);
(*bufferPlayer)->Realize(bufferPlayer, SL_BOOLEAN_FALSE);

// Initialize and start the buffer queue.
(*bufferPlayer)->GetInterface(bufferPlayer, SL_IID_BUFFERQUEUE, &bufferQueue);
(*bufferQueue)->RegisterCallback(bufferQueue, openSLESCallback, this);
memset(outputBuffer[0], 0, buffersize * 4);
memset(outputBuffer[1], 0, buffersize * 4);
(*bufferQueue)->Enqueue(bufferQueue, outputBuffer[0], buffersize * 4);
(*bufferQueue)->Enqueue(bufferQueue, outputBuffer[1], buffersize * 4);
SLPlayItf bufferPlayerPlayInterface;
(*bufferPlayer)->GetInterface(bufferPlayer, SL_IID_PLAY, &bufferPlayerPlayInterface);
(*bufferPlayerPlayInterface)->SetPlayState(bufferPlayerPlayInterface, SL_PLAYSTATE_PLAYING);
}

SuperpoweredExample::~SuperpoweredExample() {
for (int n = 0; n < NUM_BUFFERS; n++) free(outputBuffer[n]);
delete playerA;
delete playerB;
delete mixer;
pthread_mutex_destroy(&mutex);
}

void SuperpoweredExample::onPlayPause(bool play) {
pthread_mutex_lock(&mutex);
if (!play) {
playerA->pause();
playerB->pause();
} else {
bool masterIsA = (crossValue <= 0.5 f);
playerA->play!masterIsA);
playerB->play(masterIsA);
};
pthread_mutex_unlock(&mutex);
}

void SuperpoweredExample::onCrossfader(int value) {
pthread_mutex_lock(&mutex);
crossValue = float(value) * 0.01 f;
if (crossValue < 0.01 f) {
volA = 1.0 f * headroom;
volB = 0.0 f;
} else if (crossValue > 0.99 f) {
volA = 0.0 f;
volB = 1.0 f * headroom;
} else { // constant power curve
volA = cosf(M_PI_2 * crossValue) * headroom;
volB = cosf(M_PI_2 * (1.0 f - crossValue)) * headroom;
};
pthread_mutex_unlock(&mutex);
}

void SuperpoweredExample::onFxSelect(int value) {
__android_log_print(ANDROID_LOG_VERBOSE, "SuperpoweredExample", "FXSEL %i", value);
activeFx = value;
}

void SuperpoweredExample::onFxOff() {
filter->enable(false);
roll->enable(false);
flanger->enable(false);
whoosh->enable(false);
gate->enable(false);
echo->enable(false);
reverb->enable(false);
}

#define MINFREQ 60.0 f
#define MAXFREQ 20000.0 f

static inline float floatToFrequency(float value) {
if (value > 0.97 f) return MAXFREQ;
if (value < 0.03 f) return MINFREQ;
value = powf(10.0 f, (value + ((0.4 f - fabsf(value - 0.4 f)) * 0.3 f)) * log10f(MAXFREQ - MINFREQ)) + MINFREQ;
return value < MAXFREQ ? value : MAXFREQ;
}

void SuperpoweredExample::onFxValue(int ivalue) {
float value = float(ivalue) * 0.01 f;
switch (activeFx) {

// filter
case 1:
filter->setResonantParameters(floatToFrequency(1.0 f - value), 0.2 f);
filter->enable(true);
flanger->enable(false);
roll->enable(false);
whoosh->enable(false);
gate->enable(false);
echo->enable(false);
reverb->enable(false);
break;

// roll
case 2:
if (value > 0.8 f) roll->beats = 0.0625 f;
else if (value > 0.6 f) roll->beats = 0.125 f;
else if (value > 0.4 f) roll->beats = 0.25 f;
else if (value > 0.2 f) roll->beats = 0.5 f;
else roll->beats = 1.0 f;
roll->enable(true);
filter->enable(false);
flanger->enable(false);
whoosh->enable(false);
gate->enable(false);
echo->enable(false);
reverb->enable(false);
break;

// echo
case 3:
flanger->enable(false);
filter->enable(false);
roll->enable(false);
whoosh->enable(false);
gate->enable(false);
echo->setMix(value);
echo->enable(true);
reverb->enable(false);
break;

// whoosh
case 4:
flanger->enable(false);
filter->enable(false);
roll->enable(false);
whoosh->setFrequency(floatToFrequency(1.0 f - value));
whoosh->enable(true);
gate->enable(false);
echo->enable(false);
reverb->enable(false);
break;

// gate
case 5:
flanger->enable(false);
filter->enable(false);
roll->enable(false);
whoosh->enable(false);
echo->enable(false);
if (value > 0.8 f) gate->beats = 0.0625 f;
else if (value > 0.6 f) gate->beats = 0.125 f;
else if (value > 0.4 f) gate->beats = 0.25 f;
else if (value > 0.2 f) gate->beats = 0.5 f;
else gate->beats = 1.0 f;
gate->enable(true);
reverb->enable(false);
break;

// reverb
case 6:
flanger->enable(false);
filter->enable(false);
roll->enable(false);
whoosh->enable(false);
echo->enable(false);
gate->enable(false);
reverb->enable(true);
reverb->setRoomSize(value);
break;

// flanger
default:
flanger->setWet(value);
flanger->enable(true);
filter->enable(false);
roll->enable(false);
whoosh->enable(false);
gate->enable(false);
echo->enable(false);
};
}

void SuperpoweredExample::process(SLAndroidSimpleBufferQueueItf caller) {
pthread_mutex_lock(&mutex);
float *stereoBuffer = outputBuffer[currentBuffer];

bool masterIsA = (crossValue <= 0.5 f);
float masterBpm = masterIsA ? playerA->currentBpm : playerB->currentBpm;
double msElapsedSinceLastBeatA = playerA->msElapsedSinceLastBeat; // When playerB it needs, playerA has already stepped this value, so save it now.

bool silence = !playerA->process(stereoBuffer, false, buffersize, volA, masterBpm, playerB->msElapsedSinceLastBeat);
if (playerB->process(stereoBuffer, !silence, buffersize, volB, masterBpm, msElapsedSinceLastBeatA)) silence = false;

roll->bpm = flanger->bpm = gate->bpm = masterBpm; // Syncing fx is one line.

if (roll->process(silence ? NULL : stereoBuffer, stereoBuffer, buffersize) && silence) silence = false;
if (!silence) {
filter->process(stereoBuffer, stereoBuffer, buffersize);
flanger->process(stereoBuffer, stereoBuffer, buffersize);
whoosh->process(stereoBuffer, stereoBuffer, buffersize);
gate->process(stereoBuffer, stereoBuffer, buffersize);
echo->process(stereoBuffer, stereoBuffer, buffersize);
reverb->process(stereoBuffer, stereoBuffer, buffersize);
};

pthread_mutex_unlock(&mutex);

// The stereoBuffer now is ready, let's put the audio out into the requested buffers.
if (silence) memset(stereoBuffer, 0, buffersize * 4); else SuperpoweredStereoMixer::floatToShortInt(stereoBuffer, (short int *)stereoBuffer, buffersize);

(*caller)->Enqueue(caller, stereoBuffer, buffersize * 4);
if (currentBuffer < NUM_BUFFERS - 1) currentBuffer++; else currentBuffer = 0;
}

extern "С" {
JNIEXPORT void Java_com_example_SuperpoweredExample_mainactivity_superpoweredexample(JNIEnv *javaEnvironment, jobject self, jstring apkPath, jlongArray offsetAndLength);
JNIEXPORT void Java_com_example_SuperpoweredExample_mainactivity_onplaypause(JNIEnv *javaEnvironment, jobject self, jboolean play);
JNIEXPORT void Java_com_example_SuperpoweredExample_mainactivity_oncrossfader(JNIEnv *javaEnvironment, jobject self, jint value);
JNIEXPORT void Java_com_example_SuperpoweredExample_mainactivity_onfxselect(JNIEnv *javaEnvironment, jobject self, jint value);
JNIEXPORT void Java_com_example_SuperpoweredExample_mainactivity_onfxoff(JNIEnv *javaEnvironment, jobject self);
JNIEXPORT void Java_com_example_SuperpoweredExample_mainactivity_onfxvalue(JNIEnv *javaEnvironment, jobject self, jint value);
}

static SuperpoweredExample *example = NULL;

// Android is not passing more than 2 custom parameters, so we had to pack file offsets and lengths into an array.
JNIEXPORT void Java_com_example_SuperpoweredExample_mainactivity_superpoweredexample(JNIEnv *javaEnvironment, jobject self, jstring apkPath, jlongArray params) {
// Convert the input jlong array to a regular int array.
jlong *longParams = javaEnvironment->GetLongArrayElements(params, JNI_FALSE);
int arr[6];
for (int n = 0; n < 6; n++) arr[n] = longParams[n];
javaEnvironment->ReleaseLongArrayElements(params, longParams, JNI_ABORT);

const char *path = javaEnvironment->GetStringUTFChars(apkPath, JNI_FALSE);
example = new SuperpoweredExample(path, arr);
javaEnvironment->ReleaseStringUTFChars(apkPath, path);

}

JNIEXPORT void Java_com_example_SuperpoweredExample_mainactivity_onplaypause(JNIEnv *javaEnvironment, jobject self, jboolean play) {
example->onPlayPause(play);
}

JNIEXPORT void Java_com_example_SuperpoweredExample_mainactivity_oncrossfader(JNIEnv *javaEnvironment, jobject self, jint value) {
example->onCrossfader(value);
}

JNIEXPORT void Java_com_example_SuperpoweredExample_mainactivity_onfxselect(JNIEnv *javaEnvironment, jobject self, jint value) {
example->onFxSelect(value);
}

JNIEXPORT void Java_com_example_SuperpoweredExample_mainactivity_onfxoff(JNIEnv *javaEnvironment, jobject self) {
example->onFxOff();
}

JNIEXPORT void Java_com_example_SuperpoweredExample_mainactivity_onfxvalue(JNIEnv *javaEnvironment, jobject self, jint value) {
example->onFxValue(value);
}
SuperpoweredExample.cpp
#ifndef Header_SuperpoweredExample
#define Header_SuperpoweredExample

#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
#include <math.h>
#include <pthread.h>

#include "SuperpoweredExample.h"
#include "SuperpoweredAdvancedAudioPlayer.h"
#include "SuperpoweredFilter.h"
#include "SuperpoweredRoll.h"
#include "SuperpoweredFlanger.h"
#include "SuperpoweredMixer.h"

#include "SuperpoweredWhoosh.h"
#include "SuperpoweredGate.h"
#include "SuperpoweredEcho.h"
#include "SuperpoweredReverb.h"
#include "SuperpoweredTimeStretching.h"


#define NUM_BUFFERS 2
#define HEADROOM_DECIBEL 3.0 f
static const float headroom = powf(f 10.0, -HEADROOM_DECIBEL * 0.025);

class SuperpoweredExample {
public:

SuperpoweredExample(const char *path, int *params);
~SuperpoweredExample();

void process(SLAndroidSimpleBufferQueueItf caller);
void onPlayPause(bool play);
void onCrossfader(int value);
void onFxSelect(int value);
void onFxOff();
void onFxValue(int value);

private:
SLObjectItf openSLEngine, outputMix, bufferPlayer;
SLAndroidSimpleBufferQueueItf bufferQueue;

SuperpoweredAdvancedAudioPlayer *playerA, *playerB;
SuperpoweredRoll *roll;
SuperpoweredFilter *filter;
SuperpoweredFlanger *flanger;
SuperpoweredStereoMixer *mixer;

SuperpoweredWhoosh *whoosh;
SuperpoweredGate *gate;
SuperpoweredEcho *echo;
SuperpoweredReverb *reverb;
SuperpoweredTimeStretching *stretch;

unsigned char activeFx;
float crossValue, volA, volB;
pthread_mutex_t mutex;

float *outputBuffer[NUM_BUFFERS];
int currentBuffer, buffersize;
};

#endif
MainActivity
package com.example.SuperpoweredExample;

import java.io.IOException;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.media.AudioManager;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.RadioGroup.OnCheckedChangeListener;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;

public class MainActivity extends Activity {
boolean playing =false;

RadioGroup group1;
RadioGroup group2;

OnCheckedChangeListener rgCheckedChanged = new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
RadioButton checkedRadioButton = (RadioButton)group.findViewById(checkedId);
final int delta = group==group2 ? 4:0; 
if (group==group1){
group2.setOnCheckedChangeListener(null);
group2.clearCheck();
group2.setOnCheckedChangeListener(rgCheckedChanged);
} else {
group1.setOnCheckedChangeListener(null);
group1.clearCheck();
group1.setOnCheckedChangeListener(rgCheckedChanged);
}
onFxSelect(group.indexOfChild(checkedRadioButton)+delta);
}
}; 

@SuppressLint("NewApi")
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R. layout.main);

// Get the device's sample rate and buffer size to enable low-latency Android audio output, if available.
AudioManager audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE);
String samplerateString=null, buffersizeString=null;
try {
samplerateString = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
} catch (NoSuchMethodError ignored){}
try {
buffersizeString = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
} catch (NoSuchMethodError ignored){}
if (samplerateString == null) samplerateString = "44100";
if (buffersizeString == null) buffersizeString = "512";

// Files under res/raw are not compressed, just copied into the АПК. Get the offset length and to know where our files are located.
AssetFileDescriptor fd0 = getResources().openRawResourceFd(R. raw.lycka), fd1 = getResources().openRawResourceFd(R. raw.nuyorica);
long[] params = { fd0.getStartOffset(), fd0.getLength(), fd1.getStartOffset(), fd1.getLength(), Integer.parseInt(samplerateString), Integer.parseInt(buffersizeString) };
try {
fd0.getParcelFileDescriptor().close();
} catch (IOException e) {}
try {
fd1.getParcelFileDescriptor().close();
} catch (IOException e) {}

SuperpoweredExample(getPackageResourcePath(), params); // Arguments: path to the APK файл, offset and length of the two resource files, sample rate, audio buffer size.

// events crossfader
final SeekBar crossfader = (SeekBar)findViewById(R. id.crossFader); 
crossfader.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {

public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
onCrossfader(progress);
}

public void onStartTrackingTouch(SeekBar seekBar){}

public void onStopTrackingTouch(SeekBar seekBar) {}

});

// fx fader events
final SeekBar fxfader = (SeekBar)findViewById(R. id.fxFader); 
fxfader.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {

public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
onFxValue(progress);
}

public void onStartTrackingTouch(SeekBar seekBar) {
onFxValue(seekBar.getProgress());
}

public void onStopTrackingTouch(SeekBar seekBar) {
onFxOff();
}

});

group1 = (RadioGroup)findViewById(R. id.radioGroup1);
group1.setOnCheckedChangeListener(rgCheckedChanged);
group2 = (RadioGroup)findViewById(R. id.radioGroup2);
group2.setOnCheckedChangeListener(rgCheckedChanged);

// // fx select event
// group.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
// public void onCheckedChanged(RadioGroup radioGroup, int checkedId) {
// RadioButton checkedRadioButton = (RadioButton)radioGroup.findViewById(checkedId);
// onFxSelect(radioGroup.indexOfChild(checkedRadioButton));
// group2.clearCheck();
// }
// });
// group2.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
// public void onCheckedChanged(RadioGroup radioGroup, int checkedId) {
// RadioButton checkedRadioButton = (RadioButton)radioGroup.findViewById(checkedId);
// onFxSelect(radioGroup.indexOfChild(checkedRadioButton)+4);
// group.clearCheck();
// }
// });

}

public void SuperpoweredExample_PlayPause(View button) { // Play/pause.
playing = !playing;
onPlayPause(відтворення);
Button b = (Button) findViewById(R. id.playPause);
b.setText(playing ? "Pause" : "Play");
}

private native void SuperpoweredExample(String apkPath, long[] offsetAndLength);
private native void onPlayPause(boolean play);
private native void onCrossfader(int value);
private native void onFxSelect(int value);
private native void onFxOff();
private native void onFxValue(int value);

static {
System.дзвінки на loadlibrary("SuperpoweredExample");
}

@Override
protected void onDestroy() {
super.onDestroy();
onPlayPause(false);
}
}
main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android1="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >

<Button
android1:id="@+id/playPause"
android1:layout_width="wrap_content"
android1:layout_height="wrap_content"
android1:layout_alignParentTop="true"
android1:layout_centerHorizontal="true"
android1:layout_marginLeft="5dp"
android1:layout_marginRight="5dp"
android1:layout_marginTop="15dp"
android1:onClick="SuperpoweredExample_PlayPause"
android1:text="@string/play" />

<SeekBar
android1:id="@+id/crossFader"
android1:layout_width="match_parent"
android1:layout_height="wrap_content"
android1:layout_alignParentLeft="true"
android1:layout_below="@+id/playPause"
android1:layout_marginLeft="5dp"
android1:layout_marginRight="5dp"
android1:layout_marginTop="15dp" />

<RadioGroup
android1:id="@+id/radioGroup1"
android1:layout_width="wrap_content"
android1:layout_height="wrap_content"
android1:layout_below="@+id/crossFader"
android1:layout_centerHorizontal="true"
android1:layout_marginTop="15dp"
android1:orientation="horizontal" >

<RadioButton
android1:id="@+id/radio0"
android1:layout_width="wrap_content"
android1:layout_height="wrap_content"
android1:checked="true"
android1:text="@string/flanger" />

<RadioButton
android1:id="@+id/radio1"
android1:layout_width="wrap_content"
android1:layout_height="wrap_content"
android1:text="@string/filter" />

<RadioButton
android1:id="@+id/radio2"
android1:layout_width="wrap_content"
android1:layout_height="wrap_content"
android1:text="@string/roll" />

<RadioButton
android1:id="@+id/radio3"
android1:layout_width="wrap_content"
android1:layout_height="wrap_content"
android1:text="@string/echo" />
</RadioGroup>

<RadioGroup
android1:id="@+id/radioGroup2"
android1:layout_width="wrap_content"
android1:layout_height="wrap_content"
android1:layout_below="@+id/radioGroup1"
android1:layout_centerHorizontal="true"
android1:layout_marginTop="5dp"
android1:orientation="horizontal" >

<RadioButton
android1:id="@+id/radio4"
android1:layout_width="wrap_content"
android1:layout_height="wrap_content"
android1:text="@string/whoosh" />

<RadioButton
android1:id="@+id/radio5"
android1:layout_width="wrap_content"
android1:layout_height="wrap_content"
android1:text="@string/gate" />

<RadioButton
android1:id="@+id/radio6"
android1:layout_width="wrap_content"
android1:layout_height="wrap_content"
android1:text="@string/reverb" />
</RadioGroup>

<SeekBar
android1:id="@+id/fxFader"
android1:layout_width="match_parent"
android1:layout_height="wrap_content"
android1:layout_alignParentLeft="true"
android1:layout_below="@+id/radioGroup2"
android1:layout_marginLeft="5dp"
android1:layout_marginRight="5dp"
android1:layout_marginTop="15dp" />

</RelativeLayout>

Що мені стало зовсім очевидно, так це те, що ця бібліотека дозволяє вирішити:
— вилучення аудіо, якщо буде потрібно, наприклад з ААС в WAVE/РСМ (немає прикладу для андроїда і немає JNI під це діло, але є приклад як це робити для iOS);
— додавання ефектів у файл і збереження файлу з доданими/накладеними ефектами (немає прикладу для андроїда і немає JNI під це діло, але є приклад як це робити для iOS);
— SuperpoweredAdvancedAudioPlayer вміє " на льоту " змінювати pitch shift і накладати інші ефекти абсолютно безболісно (без лагів);
— кілька копій SuperpoweredAdvancedAudioPlayer можуть без проблем грати одночасно, що дозволить підмішувати встановлені звукові ефекти.

Чого не вирішити за допомогою цього SDK з того, що мені потрібно:
— не можна упакувати результуючий WAV-файл в MP3, вище я вже про це згадував, але для цього можна використовувати форк LAME для андроїда.

Що залишається під питанням:
— невідомо вміє SuperpoweredAdvancedAudioPlayer грати з НТТР і наскільки він буде однаково добре працювати на усіляких Samsung та HTC;
— невідомо вміє SDK записувати з мікрофона.

Загалом чекаю Ваших коментарів, порад, а може хто спроможеться навіть JNI написати на всі функції SDK, ніж допоможе популяризувати цю бібліотеку. Про неї вкрай мало згадок в цих наших інтернетах, особливо щодо андроїда, а вона таки заслуговує уваги!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.