1
0
mirror of https://github.com/RPCS3/soundtouch.git synced 2024-11-08 12:02:28 +01:00

Developed more refined Android example application that also works in ARM & X86 platforms.

This commit is contained in:
oparviai 2015-05-15 00:07:10 +00:00
parent 1040bd1d28
commit 92973bc18e
9 changed files with 460 additions and 105 deletions

View File

@ -172,6 +172,7 @@ namespace soundtouch
#else
// use c++ standard exceptions
#include <stdexcept>
#include <string>
#define ST_THROW_RT_ERROR(x) {throw std::runtime_error(x);}
#endif

View File

@ -8,6 +8,9 @@
android:minSdkVersion="11"
android:targetSdkVersion="21" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"

View File

@ -45,49 +45,13 @@
open Cygwin/bash shell, go to directory <b>&quot;soundtouch/source/Android-lib/jni&quot;</b> and invoke the NDK
compiler with following command:</p>
<pre> $NDK/ndk-build</pre>
<p>This will build the ARMv5 and ARMv7 versions of SoundTouch library (including
also the example JNI
interface, see below) into the &quot;libs&quot; subdirectory.</p>
<p>This will build binaries for all the supported Android platforms (arm-v5, arm-v7, X86, MIPS etc) of SoundTouch library, plus the JNI wrapper interface as discussed below. The target binaries will be built into the &quot;libs&quot; subdirectory. As long as all these .so binary library versions are included in the APK Application delivery package, the targer Android device can choose the correct library version to use. </p>
<p>Notice that to allow Cygwin/bash to locate the NDK compile scripts, you
need to define the location of the NDK installation defined in environment
variable &quot;NDK&quot;. That's easiest done by adding the NDK path definition at end of
your <b>~/.bash_profile</b> file, for instance as follows:</p>
<pre> NDK=/cygdrive/d/Android/android-ndk-r6</pre>
<hr />
<h2>
Android floating-point performance considerations</h2>
<p>
Default build target for
Android NDK is ARMv5 CPU generation, as that works in
all ARM-based Android devices.<p>
This has a pitfall though: For ideal sound quality SoundTouch should be compiled
to use floating-point algorithms, however, all low-end Android devices do not
have floating-point hardware in their CPUs, and hence the default ARMv5 compilation uses software-emulation for floating-point calculations instead of
hardware floating-point to allow running the binary executables also in low-end devices.<p>
The floating point software-emulation is however several tens of times slower
than real hardware-level floating-point calculations, making
floating-point-intensive applications such as SoundTouch infeasible with low-end
devices.<p>
As workaround, the SoundTouch Android compilation builds two separate versions
of the library:<ul>
<li>ARMv5 version that compiles SoundTouch using integer algorithm version. The integer
algorithm version compromises the sound quality but provides good performance also
with low-end
devices without hardware floating-point support in the CPU level.</li>
<li>ARMv7 version that compiles SoundTouch using hardware floating-point algorithms.
These algorithms provide ideal sound quality yet do not work in simpler CPU
models.</li>
</ul>
<p>
These two library compilations are already defined in file &quot;<b>jni/Application.mk</b>&quot;
so that these two separate library targets are automatically built under the &quot;<b>libs</b>&quot;
directory. As far as you include both these compiled library versions into your
application delivery, the Android devices can automatically select the right
library version based on the available device&#39;s capabilities.<p>
Please yet be aware that depending on capabilities of the Android devices you
will need to provide the SoundTouch routines with samples in either integer or
floating-point format, so build your interface routines to take this into
account.<hr />
<h2>
Calling SoundTouch native routines from Android application</h2>
<p>The NDK tools build the SoundTouch c++ routines into a native binary library, while
@ -96,23 +60,42 @@
Interface (JNI).</p>
<p>
The SoundTouch source code package provides source code example how to
use JNI to call native c++ routines from a Java class through the following
source code file pair:<ul>
<li><b>Android-lib/jni/soundtouch-jni.cpp</b>: This file contains c/c++ routine that
calls SoundTouch library routine to return the library version string to the main
Android application. The NDK compiles this file along with the SoundTouch
use JNI to call native c++ routines from a Java class, and provides source codes also for
a simple example Android application:<ul>
<li><b>ExampleActivity</b>: This is simple Android example application that
utilizes SoundTouch native routines for processing WAV audio files. To build the example
application, use Eclipse Android SDK environment to import the "ExampleActivity" project in the "Android-lib" folder into the Eclipse workspace.
<li><b>Android-lib/jni/soundtouch-jni.cpp</b>: This file contains c/c++ wrapper routines
for performing elementary audio file processing with adjusted tempo/pitch/speed parameters
from the Android application. The wrapper interface is not complete, but provides example
that is easy to extend when necessary. The NDK compiles this file along with the SoundTouch
routines into the native binary library.</li>
<li><b>Android-lib/src/net/surina/soundtouch/SoundTouch.java</b>: This file provides
a Java interface class to load the native library and to invoke the native routine implemented in
the file <b>soundtouch-jni.cpp</b></li>
<li><b>Android-lib/src/net/surina/soundtouch/SoundTouch.java</b>: This file implements
the Java interface class that loasd & accesses the JNI routines in the natively compiled library.
The example Android application uses this class as interface for processing audio files
with SoundTouch.</li>
</ul>
<p>
Feel free to examine and extend the provided cpp/java source code example file pair to
implement and integrate the desired SoundTouch library capabilities into your Android application.</p>
implement and integrate the desired SoundTouch library capabilities into your own Android application.</p>
<hr />
<h2>
Android floating-point performance considerations</h2>
<p>
The make process will build dedicated binaries for each supported Android CPU hardware platform type.
</p><p>SoundTouch uses floating-point algorithms for ideal sound quality on all other platform than in the lowest-end ARMv5. That is because lowest-end Android devices are not guaranteed to
have floating-point hardware in their CPUs, so that the ARMv5 compilation uses by default software-emulation for floating-point calculations to allow running the binary executables also in low-end devices without floating-point hardware.<p>
As floating point software-emulation is however several tens of times slower
than real hardware-level floating-point calculations, that would make running
floating-point-intensive applications such as SoundTouch infeasible in these low-end
devices. As workaround, the SoundTouch Android compilation builds the ARMv5 version using integer algorithm versions. The integer
algorithm version compromises the sound quality but provides good performance also
with low-end devices without hardware floating-point support in the CPU level.</p>
<p>When Android devices with more capable device is used, the device will automatically choose a proper library version for ideal sound quality.</p>
<hr />
<p style="text-align: center"><i>Copyright &copy; Olli Parviainen</i></p>
<!--
$Id$
-->
</body>
</html>
</html>

View File

@ -23,7 +23,7 @@ include $(CLEAR_VARS)
LOCAL_MODULE := soundtouch
LOCAL_SRC_FILES := soundtouch-jni.cpp ../../SoundTouch/AAFilter.cpp ../../SoundTouch/FIFOSampleBuffer.cpp \
../../SoundTouch/FIRFilter.cpp ../../SoundTouch/cpu_detect_x86.cpp \
../../SoundTouch/sse_optimized.cpp \
../../SoundTouch/sse_optimized.cpp ../../SoundStretch/WavFile.cpp \
../../SoundTouch/RateTransposer.cpp ../../SoundTouch/SoundTouch.cpp \
../../SoundTouch/InterpolateCubic.cpp ../../SoundTouch/InterpolateLinear.cpp \
../../SoundTouch/InterpolateShannon.cpp ../../SoundTouch/TDStretch.cpp \
@ -37,7 +37,9 @@ LOCAL_LDLIBS += -llog
# for native asset manager
#LOCAL_LDLIBS += -landroid
# don't export all symbols
# added "-marm" switch to use arm instruction set instead of thumb for improved calculation performance.
LOCAL_CFLAGS += -fvisibility=hidden -I ../../../include -D ST_NO_EXCEPTION_HANDLING -fdata-sections -ffunction-sections
#
# in ARM-only environment could add "-marm" switch to force arm instruction set instead
# of thumb for improved calculation performance.
LOCAL_CFLAGS += -fvisibility=hidden -I ../../../include -fdata-sections -ffunction-sections
include $(BUILD_SHARED_LIBRARY)

View File

@ -5,3 +5,5 @@
APP_ABI := all #armeabi-v7a armeabi
APP_OPTIM := release
APP_STL := stlport_static
APP_CPPFLAGS := -fexceptions

View File

@ -14,17 +14,101 @@
#include <jni.h>
#include <android/log.h>
#include <stdexcept>
#include <string>
using namespace std;
#include "../../../include/SoundTouch.h"
#include "../source/SoundStretch/WavFile.h"
#define LOGV(...) __android_log_print((int)ANDROID_LOG_INFO, "SOUNDTOUCH", __VA_ARGS__)
//#define LOGV(...)
// String for keeping possible c++ exception error messages. Notice that this isn't
// thread-safe but it's expected that exceptions are special situations that won't
// occur in several threads in parallel.
static string _errMsg = "";
#define DLL_PUBLIC __attribute__ ((visibility ("default")))
#define BUFF_SIZE 4096
using namespace soundtouch;
// Set error message to return
static void _setErrmsg(const char *msg)
{
_errMsg = msg;
}
// Processes the sound file
static void _processFile(SoundTouch *pSoundTouch, const char *inFileName, const char *outFileName)
{
int nSamples;
int nChannels;
int buffSizeSamples;
SAMPLETYPE sampleBuffer[BUFF_SIZE];
// open input file
WavInFile inFile(inFileName);
int sampleRate = inFile.getSampleRate();
int bits = inFile.getNumBits();
nChannels = inFile.getNumChannels();
// create output file
WavOutFile outFile(outFileName, sampleRate, bits, nChannels);
pSoundTouch->setSampleRate(sampleRate);
pSoundTouch->setChannels(nChannels);
assert(nChannels > 0);
buffSizeSamples = BUFF_SIZE / nChannels;
// Process samples read from the input file
while (inFile.eof() == 0)
{
int num;
// Read a chunk of samples from the input file
num = inFile.read(sampleBuffer, BUFF_SIZE);
nSamples = num / nChannels;
// Feed the samples into SoundTouch processor
pSoundTouch->putSamples(sampleBuffer, nSamples);
// Read ready samples from SoundTouch processor & write them output file.
// NOTES:
// - 'receiveSamples' doesn't necessarily return any samples at all
// during some rounds!
// - On the other hand, during some round 'receiveSamples' may have more
// ready samples than would fit into 'sampleBuffer', and for this reason
// the 'receiveSamples' call is iterated for as many times as it
// outputs samples.
do
{
nSamples = pSoundTouch->receiveSamples(sampleBuffer, buffSizeSamples);
outFile.write(sampleBuffer, nSamples * nChannels);
} while (nSamples != 0);
}
// Now the input file is processed, yet 'flush' few last samples that are
// hiding in the SoundTouch's internal processing pipeline.
pSoundTouch->flush();
do
{
nSamples = pSoundTouch->receiveSamples(sampleBuffer, buffSizeSamples);
outFile.write(sampleBuffer, nSamples * nChannels);
} while (nSamples != 0);
}
extern "C" DLL_PUBLIC jstring Java_net_surina_soundtouch_SoundTouch_getVersionString(JNIEnv *env, jobject thiz)
{
const char *verStr;
@ -37,3 +121,77 @@ extern "C" DLL_PUBLIC jstring Java_net_surina_soundtouch_SoundTouch_getVersionSt
// return version as string
return env->NewStringUTF(verStr);
}
extern "C" DLL_PUBLIC jlong Java_net_surina_soundtouch_SoundTouch_newInstance(JNIEnv *env, jobject thiz)
{
return (jlong)(new SoundTouch());
}
extern "C" DLL_PUBLIC void Java_net_surina_soundtouch_SoundTouch_deleteInstance(JNIEnv *env, jobject thiz, jlong handle)
{
SoundTouch *ptr = (SoundTouch*)handle;
delete ptr;
}
extern "C" DLL_PUBLIC void Java_net_surina_soundtouch_SoundTouch_setTempo(JNIEnv *env, jobject thiz, jlong handle, jfloat tempo)
{
SoundTouch *ptr = (SoundTouch*)handle;
ptr->setTempo(tempo);
}
extern "C" DLL_PUBLIC void Java_net_surina_soundtouch_SoundTouch_setPitchSemiTones(JNIEnv *env, jobject thiz, jlong handle, jfloat pitch)
{
SoundTouch *ptr = (SoundTouch*)handle;
ptr->setPitchSemiTones(pitch);
}
extern "C" DLL_PUBLIC void Java_net_surina_soundtouch_SoundTouch_setSpeed(JNIEnv *env, jobject thiz, jlong handle, jfloat speed)
{
SoundTouch *ptr = (SoundTouch*)handle;
ptr->setRate(speed);
}
extern "C" DLL_PUBLIC jstring Java_net_surina_soundtouch_SoundTouch_getErrorString(JNIEnv *env, jobject thiz)
{
jstring result = env->NewStringUTF(_errMsg.c_str());
_errMsg.clear();
return result;
}
extern "C" DLL_PUBLIC int Java_net_surina_soundtouch_SoundTouch_processFile(JNIEnv *env, jobject thiz, jlong handle, jstring jinputFile, jstring joutputFile)
{
SoundTouch *ptr = (SoundTouch*)handle;
const char *inputFile = env->GetStringUTFChars(jinputFile, 0);
const char *outputFile = env->GetStringUTFChars(joutputFile, 0);
LOGV("JNI process file %s", inputFile);
try
{
_processFile(ptr, inputFile, outputFile);
}
catch (const runtime_error &e)
{
const char *err = e.what();
// An exception occurred during processing, return the error message
LOGV("JNI exception in SoundTouch::processFile: %s", err);
_setErrmsg(err);
return -1;
}
env->ReleaseStringUTFChars(jinputFile, inputFile);
env->ReleaseStringUTFChars(joutputFile, outputFile);
return 0;
}

View File

@ -24,7 +24,7 @@
android:id="@+id/editTextTempo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:ems="5"
android:inputType="numberDecimal"
android:text="100" >
</EditText>
@ -38,45 +38,24 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pitch steps:"
android:text="Pitch half-steps:"
android:textAppearance="?android:attr/textAppearanceMedium" />
<EditText
android:id="@+id/editTextPitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="numberDecimal"
android:ems="5"
android:inputType="numberSigned"
android:text="0" >
</EditText>
</TableRow>
<TableRow
android:id="@+id/tableRow3"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Speed % :"
android:textAppearance="?android:attr/textAppearanceMedium" />
<EditText
android:id="@+id/editTextSpeed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="numberDecimal"
android:text="100" >
</EditText>
</TableRow>
</TableLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="20dp" >
android:layout_marginTop="10dp" >
<TextView
android:layout_width="wrap_content"
@ -88,7 +67,9 @@
android:id="@+id/editTextSrcFileName"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="/sdcard/Download/test.wav"
android:layout_weight="1" />
<Button
android:id="@+id/buttonSelectSrcFile"
@ -111,6 +92,7 @@
android:id="@+id/editTextOutFileName"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="/sdcard/Download/soundtouch-output.wav"
android:layout_weight="1" />
<Button
@ -120,8 +102,15 @@
android:text="Select" />
</LinearLayout>
<CheckBox
android:id="@+id/checkBoxPlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="Play the output file after processing!" />
<Button
android:id="@+id/button1"
android:id="@+id/buttonProcess"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
@ -130,14 +119,22 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="20dp"
android:layout_marginTop="10dp"
android:text="Status console:"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
<ScrollView
android:id="@+id/scrollView1"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/textViewResult"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:text="@string/hello_world" />
</ScrollView>
</LinearLayout>

View File

@ -1,21 +1,50 @@
/////////////////////////////////////////////////////////////////////////////
///
/// Example Android Application/Activity that allows processing WAV
/// audio files with SoundTouch library
///
/// Copyright (c) Olli Parviainen
///
////////////////////////////////////////////////////////////////////////////////
//
// $Id: SoundTouch.java 210 2015-05-14 20:03:56Z oparviai $
//
////////////////////////////////////////////////////////////////////////////////
package net.surina;
import java.io.File;
import net.surina.soundtouch.SoundTouch;
import net.surina.soundtouchexample.R;
import net.surina.soundtouchexample.R.id;
import net.surina.soundtouchexample.R.layout;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
public class ExampleActivity extends Activity
public class ExampleActivity extends Activity implements OnClickListener
{
TextView textViewConsole;
TextView textViewConsole = null;
EditText editSourceFile = null;
EditText editOutputFile = null;
EditText editTempo = null;
EditText editPitch = null;
CheckBox checkBoxPlay = null;
StringBuilder consoleText = new StringBuilder();
/// Called when the activity is created
@Override
protected void onCreate(Bundle savedInstanceState)
{
@ -23,38 +52,164 @@ public class ExampleActivity extends Activity
setContentView(R.layout.activity_example);
textViewConsole = (TextView)findViewById(R.id.textViewResult);
editSourceFile = (EditText)findViewById(R.id.editTextSrcFileName);
editOutputFile = (EditText)findViewById(R.id.editTextOutFileName);
editTempo = (EditText)findViewById(R.id.editTextTempo);
editPitch = (EditText)findViewById(R.id.editTextPitch);
// Check soundtouch
Button buttonFileSrc = (Button)findViewById(R.id.buttonSelectSrcFile);
Button buttonFileOutput = (Button)findViewById(R.id.buttonSelectOutFile);
Button buttonProcess = (Button)findViewById(R.id.buttonProcess);
buttonFileSrc.setOnClickListener(this);
buttonFileOutput.setOnClickListener(this);
buttonProcess.setOnClickListener(this);
checkBoxPlay = (CheckBox)findViewById(R.id.checkBoxPlay);
// Check soundtouch library presence & version
checkLibVersion();
}
@Override
protected void onDestroy()
/// Function to append status text onto "console box" on the Activity
public void appendToConsole(final String text)
{
textViewConsole = null;
// run on UI thread to avoid conflicts
runOnUiThread(new Runnable()
{
public void run()
{
consoleText.append(text);
consoleText.append("\n");
textViewConsole.setText(consoleText);
}
});
}
/// Append text to console on the activity
public void appendToConsole(String text)
{
if (textViewConsole == null) return;
consoleText.append(text);
consoleText.append("\n");
textViewConsole.setText(consoleText);
}
/// print SoundTouch native library version onto console
protected void checkLibVersion()
{
SoundTouch st = new SoundTouch();
String ver = st.getVersionString();
String ver = SoundTouch.getVersionString();
appendToConsole("SoundTouch native library version = " + ver);
}
}
/// Button click handler
@Override
public void onClick(View arg0)
{
switch (arg0.getId())
{
case R.id.buttonSelectSrcFile:
case R.id.buttonSelectOutFile:
// one of the file select buttons clicked ... we've not just implemented them ;-)
Toast.makeText(this, "File selector not implemented, sorry! Enter the file path manually ;-)", Toast.LENGTH_LONG).show();
break;
case R.id.buttonProcess:
// button "process" pushed
process();
break;
}
}
/// Play audio file
protected void playWavFile(String fileName)
{
File file2play = new File(fileName);
Intent i = new Intent();
i.setAction(android.content.Intent.ACTION_VIEW);
i.setDataAndType(Uri.fromFile(file2play), "audio/wav");
startActivity(i);
}
/// Helper class that will execute the SoundTouch processing. As the processing may take
/// some time, run it in background thread to avoid hanging of the UI.
protected class ProcessTask extends AsyncTask<ProcessTask.Parameters, Integer, Long>
{
/// Helper class to store the SoundTouch file processing parameters
public final class Parameters
{
String inFileName;
String outFileName;
float tempo;
float pitch;
}
/// Processing routine
@Override
protected Long doInBackground(Parameters... aparams)
{
Parameters params = aparams[0];
SoundTouch st = new SoundTouch();
st.setTempo(params.tempo);
st.setPitchSemiTones(params.pitch);
Log.i("SoundTouch", "process file " + params.inFileName);
long startTime = System.currentTimeMillis();
int res = st.processFile(params.inFileName, params.outFileName);
long endTime = System.currentTimeMillis();
float duration = (endTime - startTime) * 0.001f;
Log.i("SoundTouch", "process file done, duration = " + duration);
appendToConsole("Processing done, duration " + duration + " sec.");
if (res != 0)
{
String err = SoundTouch.getErrorString();
appendToConsole("Failure: " + err);
return -1L;
}
// Play file if so is desirable
if (checkBoxPlay.isChecked())
{
playWavFile(params.outFileName);
}
return 0L;
}
}
/// process a file with SoundTouch. Do the processing using a background processing
/// task to avoid hanging of the UI
protected void process()
{
try
{
ProcessTask task = new ProcessTask();
ProcessTask.Parameters params = task.new Parameters();
// parse processing parameters
params.inFileName = editSourceFile.getText().toString();
params.outFileName = editOutputFile.getText().toString();
params.tempo = 0.01f * Float.parseFloat(editTempo.getText().toString());
params.pitch = Float.parseFloat(editPitch.getText().toString());
// update UI about status
appendToConsole("Process audio file :" + params.inFileName +" => " + params.outFileName);
appendToConsole("Tempo = " + params.tempo);
appendToConsole("Pitch adjust = " + params.pitch);
Toast.makeText(this, "Starting to process file " + params.inFileName + "...", Toast.LENGTH_SHORT).show();
// start processing task in background
task.execute(params);
}
catch (Exception exp)
{
exp.printStackTrace();
}
}
}

View File

@ -20,7 +20,61 @@ public final class SoundTouch
// Native interface function that returns SoundTouch version string.
// This invokes the native c++ routine defined in "soundtouch-jni.cpp".
public native final static String getVersionString();
private native final void setTempo(long handle, float tempo);
private native final void setPitchSemiTones(long handle, float pitch);
private native final void setSpeed(long handle, float speed);
private native final int processFile(long handle, String inputFile, String outputFile);
public native final static String getErrorString();
private native final static long newInstance();
private native final void deleteInstance(long handle);
long handle = 0;
public SoundTouch()
{
handle = newInstance();
}
public void close()
{
deleteInstance(handle);
handle = 0;
}
public void setTempo(float tempo)
{
setTempo(handle, tempo);
}
public void setPitchSemiTones(float pitch)
{
setPitchSemiTones(handle, pitch);
}
public void setSpeed(float speed)
{
setSpeed(handle, speed);
}
public int processFile(String inputFile, String outputFile)
{
return processFile(handle, inputFile, outputFile);
}
// Load the native library upon startup
static
{