Pull to refresh

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

Reading time 29 min
Views 10K
Передо мной поставлена задача: требуется разработать приложение, которое будет осуществлять запись с микрофона, затем изменение (ускорение или pitch shifting), сохранение эффекта в самом файле и отправка результирующего МР3-файла на сервер приложения. Задача эта получается комплексная. Причем еще и min-sdk=9 хотят.
Для записи звука, чтобы по-проще, со старта напрашивается класс MediaPlayer. Пишет с микрофона, при этом сразу в сжатом виде, ААС к примеру. Для справки (если внезапно кто не знает): МР3-енкодера в Андроиде нет, т.к. там лицуха коммерческая, а есть только декодер МР3, соотв. никак нельзя записать сразу в МР3, а можно только проигрывать, для чего декодер и нужен.
Все бы хорошо, да только для того, чтобы со звуком можно было что-то делать, а именно — наложить какой-либо эффект, его требуется записывать в первозданном, так сказать, виде, т.е. не в сжатом до МР3 или ААС, а именно в PCM/WAVE-формате. Да и кроме того при проигрывании надо же в реальном режиме времени «отображать» накладываемый эффект, чтобы можно было на слух подстроить. А для этого класс MediaPlayer уже не годится, т.к. пишет он только в сжатом виде, а наложить при проигрывании ускорение — зача трудноразрешимая, учитывая минимальный СДК. Моджно добавить себе работы: записывать в MediaPlayer-е в ААС, например, а потом распаковывать ААС до PCM/WAVE, чего в Андроиде не предусмотрено и посему придется еще и для этого искать решение. Да и опять же: лишний раз батарею сажать, на жадный до вычислительных ресурсов процесс распаковки против варианта записать сразу в несжатом виде, ну и пользователю не понравится тратить свое драгоценное время на этот процесс (он знать может и не будет зачем, да ждать придется).
Из всего этого вытекает, что запись надо осуществлять используя связку других классов: AudioRecord — он пишет, а AudioTrack — и воспроизводит и ускорение на нем не проблема при воспроизведении.
Однако на практике с этими классами проблем тоже достаточно. Во-первых: AudioRecord пишет сразу данные РСМ, оно мне и надо, но только вот сам заголовок WAVE он не создает, т.е. он пишет RAW PCM, а чтобы потом как-то эти данные можно было использовать не только в AudioTrack, требуется добавить код для создания этого заголовка; плюс надо не забывать при проигрывании перепрыгнуть первые 44 байта (размер заголовка), чтобы AudioTrack их не пытался воспроизводить. Во-вторых: все телодвижения по записи и воспроизведению надо выносить в отдельные потоки, что никак не упрощает разработку.
Однако пример вынесенного в отдельный класс рекордера на основе AudioRecord я приведу, так как по большей часть он все равно не мой, а из недр 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 uncompressed recording
	private AudioRecord 	 mAudioRecorder = null;
	// Recorder used for compressed recording
	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); // Write buffer to 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 timer interval 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 case the 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 to 0, 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 prepared file
				}
				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 the INITIALIZING 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, and sets 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, and sets 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); // Write size to RIFF header
					mFileWriter.writeInt(Integer.reverseBytes(36+nPayloadSize));
				
					mFileWriter.seek(40); // Write size to Subchunk2Size field
					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 LITTLE_ENDIAN format
	 * 
	 */
	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 final static int[] sampleRates = {44100, 22050, 16000, 11025, 8000};
    protected int usedSampleRate;
    private class AudioThread extends Thread {
        private final File targetFile;

        private final static 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 и указав ему за источник НТТР-ссылку на файл МР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 не помогло особо).
Сэмлп я запустил на трупе, по нынешним меркам — НТС 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.0f);
        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.0f);
        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.0f), volB(0.0f), volA(1.0f * 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.5f);
        playerA->play(!masterIsA);
        playerB->play(masterIsA);
    };
    pthread_mutex_unlock(&mutex);
}

void SuperpoweredExample::onCrossfader(int value) {
    pthread_mutex_lock(&mutex);
    crossValue = float(value) * 0.01f;
    if (crossValue < 0.01f) {
        volA = 1.0f * headroom;
        volB = 0.0f;
    } else if (crossValue > 0.99f) {
        volA = 0.0f;
        volB = 1.0f * headroom;
    } else { // constant power curve
        volA = cosf(M_PI_2 * crossValue) * headroom;
        volB = cosf(M_PI_2 * (1.0f - 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.0f
#define MAXFREQ 20000.0f

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

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

    	// filter
        case 1:
            filter->setResonantParameters(floatToFrequency(1.0f - value), 0.2f);
            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.8f) roll->beats = 0.0625f;
            else if (value > 0.6f) roll->beats = 0.125f;
            else if (value > 0.4f) roll->beats = 0.25f;
            else if (value > 0.2f) roll->beats = 0.5f;
            else roll->beats = 1.0f;
            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.0f - 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.8f) gate->beats = 0.0625f;
            else if (value > 0.6f) gate->beats = 0.125f;
            else if (value > 0.4f) gate->beats = 0.25f;
            else if (value > 0.2f) gate->beats = 0.5f;
            else gate->beats = 1.0f;
            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.5f);
    float masterBpm = masterIsA ? playerA->currentBpm : playerB->currentBpm;
    double msElapsedSinceLastBeatA = playerA->msElapsedSinceLastBeat; // When playerB needs it, 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 is ready now, let's put the finished audio 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 "C" {
	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.0f
static const float headroom = powf(10.0f, -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 APK. Get the offset and length 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 file, offset and length of the two resource files, sample rate, audio buffer size.
        
        // crossfader events
    	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(playing);
    	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, если это вообще возможно, чем поможет популяризировать данную библиотеку. О ней крайне мало упоминаний в этих наших интернетах, особенно касательно андроида, а она таки заслуживает внимания!
Tags:
Hubs:
+2
Comments 4
Comments Comments 4

Articles