datas = new ArrayList<>();
+ private short max = 300;
+ private float mWidth;
+ private float mHeight;
+ private float space = 1f;
+ private Paint mWavePaint;
+ private Paint baseLinePaint;
+ private int mWaveColor = Color.BLACK;
+ private int mBaseLineColor = Color.BLACK;
+ private float waveStrokeWidth = 1f;
+ private int invalidateTime = 1000 / 200;
+ private long drawTime;
+ private boolean isMaxConstant = false;
+
+ public WaveView(Context context) {
+ this(context, null);
+ }
+
+ public WaveView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(attrs, defStyleAttr);
+ }
+
+ private void init(AttributeSet attrs, int defStyle) {
+
+ final TypedArray a = getContext().obtainStyledAttributes(
+ attrs, R.styleable.WaveView, defStyle, 0);
+
+
+ mWaveColor = a.getColor(
+ R.styleable.WaveView_waveColor,
+ mWaveColor);
+ mBaseLineColor = a.getColor(
+ R.styleable.WaveView_baselineColor,
+ mBaseLineColor);
+
+ waveStrokeWidth = a.getDimension(
+ R.styleable.WaveView_waveStokeWidth,
+ waveStrokeWidth);
+
+ max = (short) a.getInt(R.styleable.WaveView_maxValue, max);
+ invalidateTime = a.getInt(R.styleable.WaveView_invalidateTime, invalidateTime);
+
+ space = a.getDimension(R.styleable.WaveView_space, space);
+ a.recycle();
+ initPainters();
+
+ }
+
+ private void initPainters() {
+ mWavePaint = new Paint();
+ mWavePaint.setColor(mWaveColor);// 画笔为color
+ mWavePaint.setStrokeWidth(waveStrokeWidth);// 设置画笔粗细
+ mWavePaint.setAntiAlias(true);
+ mWavePaint.setFilterBitmap(true);
+ mWavePaint.setStyle(Paint.Style.FILL);
+
+ baseLinePaint = new Paint();
+ baseLinePaint.setColor(mBaseLineColor);// 画笔为color
+ baseLinePaint.setStrokeWidth(1f);// 设置画笔粗细
+ baseLinePaint.setAntiAlias(true);
+ baseLinePaint.setFilterBitmap(true);
+ baseLinePaint.setStyle(Paint.Style.FILL);
+ }
+
+ public short getMax() {
+ return max;
+ }
+
+ public void setMax(short max) {
+ this.max = max;
+ }
+
+ public float getSpace() {
+ return space;
+ }
+
+ public void setSpace(float space) {
+ this.space = space;
+ }
+
+ public int getmWaveColor() {
+ return mWaveColor;
+ }
+
+ public void setmWaveColor(int mWaveColor) {
+ this.mWaveColor = mWaveColor;
+ invalidateNow();
+ }
+
+ public int getmBaseLineColor() {
+ return mBaseLineColor;
+ }
+
+ public void setmBaseLineColor(int mBaseLineColor) {
+ this.mBaseLineColor = mBaseLineColor;
+ invalidateNow();
+ }
+
+ public float getWaveStrokeWidth() {
+ return waveStrokeWidth;
+ }
+
+ public void setWaveStrokeWidth(float waveStrokeWidth) {
+ this.waveStrokeWidth = waveStrokeWidth;
+ invalidateNow();
+ }
+
+ public int getInvalidateTime() {
+ return invalidateTime;
+ }
+
+ public void setInvalidateTime(int invalidateTime) {
+ this.invalidateTime = invalidateTime;
+ }
+
+ public boolean isMaxConstant() {
+ return isMaxConstant;
+ }
+
+ public void setMaxConstant(boolean maxConstant) {
+ isMaxConstant = maxConstant;
+ }
+
+ /**
+ * 如果改变相应配置 需要刷新相应的paint设置
+ */
+ public void invalidateNow() {
+ initPainters();
+ invalidate();
+ }
+
+ public void addData(short data) {
+
+ if (data < 0) {
+ data = (short) -data;
+ }
+ if (data > max && !isMaxConstant) {
+ max = data;
+ }
+ if (datas.size() > mWidth / space) {
+ synchronized (this) {
+ datas.remove(0);
+ datas.add(data);
+ }
+ } else {
+ datas.add(data);
+ }
+ if (System.currentTimeMillis() - drawTime > invalidateTime) {
+ invalidate();
+ drawTime = System.currentTimeMillis();
+ }
+
+ }
+
+ public void clear() {
+ datas.clear();
+ invalidateNow();
+ }
+
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ canvas.translate(0, mHeight / 2);
+ drawBaseLine(canvas);
+ drawWave(canvas);
+
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ mWidth = w;
+ mHeight = h;
+ }
+
+ private void drawWave(Canvas mCanvas) {
+ for (int i = 0; i < datas.size(); i++) {
+ float x = (i) * space;
+ float y = (float) datas.get(i) / max * mHeight / 2;
+ mCanvas.drawLine(x, -y, x, y, mWavePaint);
+ }
+
+ }
+
+ private void drawBaseLine(Canvas mCanvas) {
+ mCanvas.drawLine(0, 0, mWidth, 0, baseLinePaint);
+ }
+}
diff --git a/app/src/main/res/drawable/recorder_btn.xml b/app/src/main/res/drawable/recorder_btn.xml
new file mode 100644
index 0000000..5c89fdd
--- /dev/null
+++ b/app/src/main/res/drawable/recorder_btn.xml
@@ -0,0 +1,18 @@
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..e7e7d3f
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..9a078e3
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..efc028a
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..3af2608
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..9bec2e6
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..aee44e1
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..34947cd
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/values/attrs_wave_view.xml b/app/src/main/res/values/attrs_wave_view.xml
new file mode 100644
index 0000000..8973506
--- /dev/null
+++ b/app/src/main/res/values/attrs_wave_view.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..dc183a0
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,8 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+ #3FB838
+ #00000000
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..08da887
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ IdealRecorder
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..5885930
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/app/src/test/java/tech/oom/idealrecorder/ExampleUnitTest.java b/app/src/test/java/tech/oom/idealrecorder/ExampleUnitTest.java
new file mode 100644
index 0000000..4a6c4ba
--- /dev/null
+++ b/app/src/test/java/tech/oom/idealrecorder/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package tech.oom.idealrecorder;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..1be9f26
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,24 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.3.3'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ jcenter()
+ maven { url 'https://jitpack.io' }
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..aac7c9b
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..13372ae
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..130ab37
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Dec 06 10:56:48 CST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..9d82f78
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/library/.gitignore b/library/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/library/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/library/build.gradle b/library/build.gradle
new file mode 100644
index 0000000..3a903c6
--- /dev/null
+++ b/library/build.gradle
@@ -0,0 +1,31 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 26
+ buildToolsVersion "26.0.0"
+
+ defaultConfig {
+ minSdkVersion 15
+ targetSdkVersion 26
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
+ exclude group: 'com.android.support', module: 'support-annotations'
+ })
+ compile 'com.android.support:appcompat-v7:26.+'
+ testCompile 'junit:junit:4.12'
+}
diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro
new file mode 100644
index 0000000..ba6c74c
--- /dev/null
+++ b/library/proguard-rules.pro
@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in D:\tools\Android\sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/library/src/androidTest/java/tech/oom/idealrecorder/ExampleInstrumentedTest.java b/library/src/androidTest/java/tech/oom/idealrecorder/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..e4b4173
--- /dev/null
+++ b/library/src/androidTest/java/tech/oom/idealrecorder/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package tech.oom.idealrecorder;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() throws Exception {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getTargetContext();
+
+ assertEquals("tech.oom.idealrecorder.test", appContext.getPackageName());
+ }
+}
diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..f339239
--- /dev/null
+++ b/library/src/main/AndroidManifest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/library/src/main/java/tech/oom/idealrecorder/IdealConst.java b/library/src/main/java/tech/oom/idealrecorder/IdealConst.java
new file mode 100644
index 0000000..8a9882c
--- /dev/null
+++ b/library/src/main/java/tech/oom/idealrecorder/IdealConst.java
@@ -0,0 +1,29 @@
+package tech.oom.idealrecorder;
+
+/**
+ * SDK的相关常量
+ */
+
+public class IdealConst {
+
+
+ /**
+ * 录音错误时的返回码
+ */
+ public static final class RecorderErrorCode {
+ /**
+ * 启动或录音时抛出异常
+ */
+ public static final int RECORDER_EXCEPTION_OCCUR = 0;
+
+ /**
+ * Recorder.read 过程中发生错误
+ */
+ public static final int RECORDER_READ_ERROR = 1;
+
+ /**
+ * 当前录音没有权限或者录音功能被占用
+ */
+ public static final int RECORDER_PERMISSION_ERROR = 3;
+ }
+}
diff --git a/library/src/main/java/tech/oom/idealrecorder/IdealRecorder.java b/library/src/main/java/tech/oom/idealrecorder/IdealRecorder.java
new file mode 100644
index 0000000..901653d
--- /dev/null
+++ b/library/src/main/java/tech/oom/idealrecorder/IdealRecorder.java
@@ -0,0 +1,467 @@
+package tech.oom.idealrecorder;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.media.AudioFormat;
+import android.media.MediaRecorder;
+import android.os.Handler;
+import android.support.v4.content.ContextCompat;
+import android.text.TextUtils;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import tech.oom.idealrecorder.file.AudioFileHelper;
+import tech.oom.idealrecorder.file.AudioFileListener;
+import tech.oom.idealrecorder.record.Recorder;
+import tech.oom.idealrecorder.record.RecorderCallback;
+import tech.oom.idealrecorder.utils.BytesTransUtil;
+import tech.oom.idealrecorder.utils.Log;
+
+/**
+ * 录音功能的核心类
+ */
+
+public class IdealRecorder implements RecorderCallback, AudioFileListener {
+ private static final String TAG = "IdealRecorder";
+ private static Context context;
+ private Handler idealHandler;
+ private RecordConfig config;
+
+ private AudioFileHelper audioFileHelper;
+ private boolean isAudioFileHelperInit;
+ private Recorder recorder;
+ private StatusListener statusListener;
+ private long maxRecordTime = 6000L;
+ private long volumeInterval = 200L;
+ private int count;
+
+ private AtomicBoolean isStarted = new AtomicBoolean(false);
+
+ private IdealRecorder() {
+ idealHandler = new Handler();
+ recorder = new Recorder(config, this);
+ audioFileHelper = new AudioFileHelper(this);
+ }
+
+ /**
+ * 初始化当前实例
+ *
+ * @param mContext 当前应用的application context
+ */
+ public static void init(Context mContext) {
+ context = mContext;
+
+ }
+
+ public static IdealRecorder getInstance() {
+ return IdealRecorderHolder.instance;
+ }
+
+ public IdealRecorder setRecordConfig(RecordConfig config) {
+ this.config = config;
+ audioFileHelper.setRecorderConfig(config);
+ recorder.setRecordConfig(config);
+ return this;
+ }
+
+ /**
+ * 设置最长语音
+ *
+ * @param maxRecordTimeMillis 最长录音时间 单位 毫秒
+ * @return
+ */
+ public IdealRecorder setMaxRecordTime(long maxRecordTimeMillis) {
+ this.maxRecordTime = maxRecordTimeMillis;
+ return this;
+ }
+
+
+ /**
+ * 设置音量回调时长 单位毫秒 必须为100毫秒的整数倍
+ *
+ * @param intervalMillis 音量回调间隔时长
+ * @return
+ */
+ public IdealRecorder setVolumeInterval(long intervalMillis) {
+ if (intervalMillis < 100) {
+ Log.e(TAG, "Volume interval should at least 100 Millisecond .Current set will not take effect, default interval is 200ms");
+ return this;
+ }
+ if (intervalMillis % Recorder.TIMER_INTERVAL != 0) {
+ intervalMillis = intervalMillis / Recorder.TIMER_INTERVAL * Recorder.TIMER_INTERVAL;
+ Log.e(TAG, "Current interval is changed to " + intervalMillis);
+ }
+ this.volumeInterval = intervalMillis;
+ return this;
+ }
+
+
+ /**
+ * 设置录音保存路径 保存格式为wav
+ *
+ * @param path 文件保存绝对路径
+ */
+ public IdealRecorder setRecordFilePath(String path) {
+ if (!TextUtils.isEmpty(path) && audioFileHelper != null) {
+ if (!isWriteExternalStoragePermissionGranted()) {
+ Log.e(TAG, "set recorder file path failed,because no WRITE_EXTERNAL_STORAGE permission was granted");
+
+ return this;
+ }
+ isAudioFileHelperInit = true;
+ audioFileHelper.setSavePath(path);
+ } else {
+ isAudioFileHelperInit = false;
+ audioFileHelper.setSavePath(null);
+ }
+ return this;
+ }
+
+ /**
+ * 设置录音时各种状态的监听
+ *
+ * @param statusListener statusListener
+ * @return
+ */
+ public IdealRecorder setStatusListener(StatusListener statusListener) {
+ this.statusListener = statusListener;
+ return this;
+ }
+
+ /**
+ * 判断是否有录音权限
+ *
+ * @return
+ */
+ public boolean isRecordAudioPermissionGranted() {
+ return ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
+ }
+
+ /**
+ * 判断是否有读写存储权限
+ *
+ * @return
+ */
+ public boolean isWriteExternalStoragePermissionGranted() {
+ return ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
+ }
+
+ /**
+ * 开始录音
+ *
+ * @return
+ */
+ public boolean start() {
+ if (isStarted.compareAndSet(false, true)) {
+ recorder.start();
+ Log.d(TAG, "Ideal Recorder Started");
+ return true;
+ } else {
+ Log.e(TAG, "Start failed , Because the Ideal Recorder already started");
+ return false;
+ }
+ }
+
+ /**
+ * 停止录音
+ */
+ public void stop() {
+ Log.d(TAG, "Stop Ideal Recorder is called");
+ if (this.isStarted.get()) {
+ this.isStarted.set(false);
+ this.recorder.immediateStop();
+
+ } else if (this.recorder != null) {
+ this.recorder.immediateStop();
+ }
+ }
+
+ /**
+ * 在UI线程执行
+ *
+ * @param runnable 需要执行的runnable
+ */
+ private void runOnUi(Runnable runnable) {
+ idealHandler.post(runnable);
+ }
+
+
+ @Override
+ public boolean onRecorderReady() {
+ if (!isRecordAudioPermissionGranted()) {
+ Log.e(TAG, "set recorder failed,because no RECORD_AUDIO permission was granted");
+ onRecordedFail(IdealConst.RecorderErrorCode.RECORDER_PERMISSION_ERROR);
+ }
+ return isRecordAudioPermissionGranted();
+ }
+
+ @Override
+ public boolean onRecorderStart() {
+ if (isAudioFileHelperInit) {
+ audioFileHelper.start();
+ }
+ count = 0;
+ runOnUi(new Runnable() {
+ public void run() {
+ if (statusListener != null) {
+ statusListener.onStartRecording();
+ }
+ Log.d(TAG, "onRecorderStart");
+ }
+ });
+ return true;
+ }
+
+ @Override
+ public void onRecorded(final short[] wave) {
+ count++;
+ final byte[] bytes = BytesTransUtil.getInstance().Shorts2Bytes(wave);
+ if (isAudioFileHelperInit) {
+
+ audioFileHelper.save(bytes, 0, bytes.length);
+ }
+
+ runOnUi(new Runnable() {
+ @Override
+ public void run() {
+ if (statusListener != null) {
+ statusListener.onRecordData(wave, wave == null ? 0 : wave.length);
+ }
+ }
+ });
+
+ long recordedTime = count * Recorder.TIMER_INTERVAL;
+ if (recordedTime >= volumeInterval && recordedTime % volumeInterval == 0) {
+ onRecorderVolume(calculateVolume(wave));
+ }
+ if (recordedTime >= maxRecordTime) {
+ recorder.stop();
+ isStarted.set(false);
+ }
+
+ }
+
+ private void onRecorderVolume(final int volume) {
+
+ runOnUi(new Runnable() {
+ public void run() {
+ if (statusListener != null) {
+ statusListener.onVoiceVolume(volume);
+ }
+ }
+ });
+
+
+ }
+
+ @Override
+ public void onRecordedFail(final int paramInt) {
+ if (isAudioFileHelperInit) {
+
+ audioFileHelper.cancel();
+ }
+ runOnUi(new Runnable() {
+ public void run() {
+ String errorMsg = "";
+ switch (paramInt) {
+ case IdealConst.RecorderErrorCode.RECORDER_EXCEPTION_OCCUR:
+ errorMsg = "启动或录音时抛出异常Exception";
+ break;
+ case IdealConst.RecorderErrorCode.RECORDER_READ_ERROR:
+ errorMsg = "Recorder.read() 过程中发生错误";
+ break;
+ case IdealConst.RecorderErrorCode.RECORDER_PERMISSION_ERROR:
+ errorMsg = "当前应用没有录音权限或者录音功能被占用";
+ break;
+ default:
+ errorMsg = "未知错误";
+ }
+ if (statusListener != null) {
+ statusListener.onRecordError(paramInt, errorMsg);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onRecorderStop() {
+ if (isAudioFileHelperInit) {
+ audioFileHelper.finish();
+ }
+ runOnUi(new Runnable() {
+ @Override
+ public void run() {
+ if (statusListener != null) {
+ statusListener.onStopRecording();
+ }
+ }
+ });
+ }
+
+ private int calculateVolume(short[] wave) {
+ long v = 0;
+ // 将 buffer 内容取出,进行平方和运算
+ for (int i = 0; i < wave.length; i++) {
+ v += wave[i] * wave[i];
+ }
+ // 平方和除以数据总长度,得到音量大小。
+ double mean = v / (double) wave.length;
+ double volume = 10 * Math.log10(mean);
+ return (int) volume;
+ }
+
+
+ /**
+ * 保存文件失败
+ */
+ @Override
+ public void onFailure(final String reason) {
+
+ Log.d(TAG, "save record file failure, this reason is " + reason);
+
+ runOnUi(new Runnable() {
+ public void run() {
+ if (statusListener != null) {
+ statusListener.onFileSaveFailed(reason);
+ }
+ }
+ });
+ }
+
+ /**
+ * 保存文件成功
+ */
+ @Override
+ public void onSuccess(final String savePath) {
+ Log.d(TAG, "save record file success, the file path is" + savePath);
+ runOnUi(new Runnable() {
+ public void run() {
+ if (statusListener != null) {
+ statusListener.onFileSaveSuccess(savePath);
+ }
+ }
+ });
+
+ }
+
+ /**
+ * 录音的配置信息 默认配置为16K采样率 单通道 16位
+ *
+ * audioSource = MediaRecorder.AudioSource.MIC;
+ * sampleRate = SAMPLE_RATE_16K_HZ;
+ * channelConfig = AudioFormat.CHANNEL_IN_MONO;
+ * audioFormat = AudioFormat.ENCODING_PCM_16BIT;
+ *
+ */
+ public static class RecordConfig {
+ public static final int SAMPLE_RATE_44K_HZ = 44100;
+ public static final int SAMPLE_RATE_22K_HZ = 22050;
+ public static final int SAMPLE_RATE_16K_HZ = 16000;
+ public static final int SAMPLE_RATE_11K_HZ = 11025;
+ public static final int SAMPLE_RATE_8K_HZ = 8000;
+ private int audioSource = MediaRecorder.AudioSource.MIC;
+ private int sampleRate = SAMPLE_RATE_16K_HZ;
+ private int channelConfig = AudioFormat.CHANNEL_IN_MONO;
+ private int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
+
+ /**
+ * 录音配置的构造方法
+ *
+ * @param audioSource the recording source.
+ * See {@link MediaRecorder.AudioSource} for the recording source definitions.
+ * recommend {@link MediaRecorder.AudioSource#MIC}
+ * @param sampleRate the sample rate expressed in Hertz. {@link RecordConfig#SAMPLE_RATE_44K_HZ} is Recommended ,
+ * @param channelConfig describes the configuration of the audio channels.
+ * See {@link AudioFormat#CHANNEL_IN_MONO} and
+ * {@link AudioFormat#CHANNEL_IN_STEREO}. {@link AudioFormat#CHANNEL_IN_MONO} is guaranteed
+ * to work on all devices.
+ * @param audioFormat the format in which the audio data is to be returned.
+ * See {@link AudioFormat#ENCODING_PCM_8BIT}, {@link AudioFormat#ENCODING_PCM_16BIT},
+ * and {@link AudioFormat#ENCODING_PCM_FLOAT}. @link RecordConfig#SAMPLE_RATE_22K_HZ},@link RecordConfig#SAMPLE_RATE_16K_HZ},@link RecordConfig#SAMPLE_RATE_11K_HZ},@link RecordConfig#SAMPLE_RATE_8K_HZ}
+ * {@link AudioFormat#SAMPLE_RATE_UNSPECIFIED} means to use a route-dependent value
+ * which is usually the sample rate of the source.
+ */
+ public RecordConfig(int audioSource, int sampleRate, int channelConfig, int audioFormat) {
+ this.audioSource = audioSource;
+ this.sampleRate = sampleRate;
+ this.channelConfig = channelConfig;
+ this.audioFormat = audioFormat;
+ }
+
+ /**
+ * 录音配置的构造方法
+ */
+ public RecordConfig() {
+
+ }
+
+ public int getAudioSource() {
+ return audioSource;
+ }
+
+ /**
+ * @param audioSource the recording source.
+ * See {@link MediaRecorder.AudioSource} for the recording source definitions.
+ * recommend {@link MediaRecorder.AudioSource#MIC}
+ */
+ public RecordConfig setAudioSource(int audioSource) {
+ this.audioSource = audioSource;
+ return this;
+ }
+
+ public int getSampleRate() {
+ return sampleRate;
+ }
+
+ /**
+ * @param sampleRate the sample rate expressed in Hertz. {@link RecordConfig#SAMPLE_RATE_44K_HZ} is Recommended ,
+ * @link RecordConfig#SAMPLE_RATE_22K_HZ},@link RecordConfig#SAMPLE_RATE_16K_HZ},@link RecordConfig#SAMPLE_RATE_11K_HZ},@link RecordConfig#SAMPLE_RATE_8K_HZ}
+ * {@link AudioFormat#SAMPLE_RATE_UNSPECIFIED} means to use a route-dependent value
+ * which is usually the sample rate of the source.
+ */
+ public RecordConfig setSampleRate(int sampleRate) {
+ this.sampleRate = sampleRate;
+ return this;
+ }
+
+ public int getChannelConfig() {
+ return channelConfig;
+ }
+
+ /**
+ * @param channelConfig describes the configuration of the audio channels.
+ * See {@link AudioFormat#CHANNEL_IN_MONO} and
+ * {@link AudioFormat#CHANNEL_IN_STEREO}. {@link AudioFormat#CHANNEL_IN_MONO} is guaranteed
+ * to work on all devices.
+ */
+ public RecordConfig setChannelConfig(int channelConfig) {
+ this.channelConfig = channelConfig;
+ return this;
+ }
+
+ public int getAudioFormat() {
+ return audioFormat;
+ }
+
+ /**
+ * @param audioFormat the format in which the audio data is to be returned.
+ * See {@link AudioFormat#ENCODING_PCM_8BIT}, {@link AudioFormat#ENCODING_PCM_16BIT},
+ * and {@link AudioFormat#ENCODING_PCM_FLOAT}.
+ */
+ public RecordConfig setAudioFormat(int audioFormat) {
+ this.audioFormat = audioFormat;
+ return this;
+ }
+
+
+ }
+
+
+ /**
+ * idealRecorder的holder 用来初始化
+ */
+ private static class IdealRecorderHolder {
+ private final static IdealRecorder instance = new IdealRecorder();
+ }
+}
diff --git a/library/src/main/java/tech/oom/idealrecorder/StatusListener.java b/library/src/main/java/tech/oom/idealrecorder/StatusListener.java
new file mode 100644
index 0000000..3f99878
--- /dev/null
+++ b/library/src/main/java/tech/oom/idealrecorder/StatusListener.java
@@ -0,0 +1,72 @@
+package tech.oom.idealrecorder;
+
+/**
+ * 录音各种状态的回调类
+ */
+
+public class StatusListener {
+
+
+ /**
+ * 开始录音的回调
+ */
+ public void onStartRecording() {
+ }
+
+ /**
+ * 录音时的buffer
+ *
+ * @param data PCM Data
+ * @param length 长度
+ */
+ public void onRecordData(short[] data, int length) {
+ }
+
+
+ /**
+ * 录音时的音量
+ *
+ * @param volume 音量
+ */
+ public void onVoiceVolume(int volume) {
+ }
+
+
+
+ /**
+ * 录音失败
+ *
+ * @param code 错误码
+ * @param errorMsg 错误信息描述
+ */
+ public void onRecordError(int code, String errorMsg) {
+
+ }
+
+
+ /**
+ * 保存文件失败
+ *
+ * @param error
+ */
+ public void onFileSaveFailed(String error) {
+
+ }
+
+ /**
+ * 保存录音文件成功
+ *
+ * @param fileUri 保存文件的路径
+ */
+ public void onFileSaveSuccess(String fileUri) {
+
+ }
+
+
+ /**
+ * 停止录音的回调
+ */
+ public void onStopRecording() {
+ }
+
+}
diff --git a/library/src/main/java/tech/oom/idealrecorder/file/AudioFileHelper.java b/library/src/main/java/tech/oom/idealrecorder/file/AudioFileHelper.java
new file mode 100644
index 0000000..e55d20a
--- /dev/null
+++ b/library/src/main/java/tech/oom/idealrecorder/file/AudioFileHelper.java
@@ -0,0 +1,192 @@
+package tech.oom.idealrecorder.file;
+
+import android.media.AudioFormat;
+import android.text.TextUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+import tech.oom.idealrecorder.IdealRecorder;
+import tech.oom.idealrecorder.utils.Log;
+
+
+/**
+ * 语音文件帮助类 用户保存相关文件
+ */
+
+public class AudioFileHelper {
+
+ public static final String TAG = "AudioFileHelper";
+ private AudioFileListener listener;
+ private String savePath;
+ private RandomAccessFile randomAccessFile;
+ private File targetFile;
+ private IdealRecorder.RecordConfig config;
+
+ public AudioFileHelper(AudioFileListener listener) {
+ this.listener = listener;
+ }
+
+ public void setSavePath(String savePath) {
+ this.savePath = savePath;
+ }
+
+ public void setRecorderConfig(IdealRecorder.RecordConfig config) {
+ this.config = config;
+ }
+
+
+ public void start() {
+ try {
+
+ open(savePath);
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ if (listener != null) {
+ listener.onFailure(e.toString());
+ }
+ }
+ }
+
+ public void save(byte[] data, int offset, int size) {
+ if (randomAccessFile == null) {
+ return;
+ }
+ try {
+ write(randomAccessFile, data, offset, size);
+ } catch (IOException e) {
+ e.printStackTrace();
+ if (listener != null) {
+ listener.onFailure(e.toString());
+ }
+
+ }
+ }
+
+ public void finish() {
+ try {
+ close();
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ if (listener != null) {
+ listener.onFailure(e.toString());
+ }
+
+ }
+ }
+
+
+ private void open(String path) throws IOException {
+ if (TextUtils.isEmpty(path)) {
+ Log.d(TAG,"Path not set , data will not save");
+ return;
+ }
+ if(this.config == null){
+ Log.d(TAG,"RecordConfig not set , data will not save");
+ return;
+ }
+ targetFile = new File(path);
+
+ if (targetFile.exists()) {
+ targetFile.delete();
+ } else {
+ File parentDir = targetFile.getParentFile();
+ if (!parentDir.exists()) {
+ parentDir.mkdirs();
+ }
+ }
+ short bSamples;
+ short nChannels;
+ int sRate;
+ if (config.getAudioFormat() == AudioFormat.ENCODING_PCM_16BIT) {
+ bSamples = 16;
+ } else {
+ bSamples = 8;
+ }
+
+ if (config.getChannelConfig() == AudioFormat.CHANNEL_IN_MONO) {
+ nChannels = 1;
+ } else {
+ nChannels = 2;
+ }
+ sRate = config.getSampleRate();
+ randomAccessFile = new RandomAccessFile(targetFile, "rw");
+ randomAccessFile.setLength(0);
+ // Set file length to
+ // 0, to prevent unexpected behavior in case the file already existed
+ // 16K、16bit、单声道
+ /* RIFF header */
+ randomAccessFile.writeBytes("RIFF"); // riff id
+ randomAccessFile.writeInt(0); // riff chunk size *PLACEHOLDER*
+ randomAccessFile.writeBytes("WAVE"); // wave type
+
+ /* fmt chunk */
+ randomAccessFile.writeBytes("fmt "); // fmt id
+ randomAccessFile.writeInt(Integer.reverseBytes(16)); // fmt chunk size
+ randomAccessFile.writeShort(Short.reverseBytes((short) 1)); // AudioFormat,1 for PCM
+ randomAccessFile.writeShort(Short.reverseBytes(nChannels));// Number of channels, 1 for mono, 2 for stereo
+ randomAccessFile.writeInt(Integer.reverseBytes(sRate)); // Sample rate
+ randomAccessFile.writeInt(Integer.reverseBytes(sRate * bSamples * nChannels / 8)); // Byte rate,SampleRate*NumberOfChannels*BitsPerSample/8
+ randomAccessFile.writeShort(Short.reverseBytes((short) (nChannels * bSamples / 8))); // Block align, NumberOfChannels*BitsPerSample/8
+ randomAccessFile.writeShort(Short.reverseBytes(bSamples)); // Bits per sample
+
+ /* data chunk */
+ randomAccessFile.writeBytes("data"); // data id
+ randomAccessFile.writeInt(0); // data chunk size *PLACEHOLDER*
+
+ Log.d(TAG, "wav path: " + path);
+
+ }
+
+ private void write(RandomAccessFile file, byte[] data, int offset, int size) throws IOException {
+ file.write(data, offset, size);
+// Log.d(TAG, "fwrite: " + size);
+ }
+
+ private void close() throws IOException {
+ try {
+ if (randomAccessFile == null) {
+ if (listener != null) {
+ listener.onFailure("File save error exception occurs");
+ }
+ return;
+ }
+ randomAccessFile.seek(4); // riff chunk size
+ randomAccessFile.writeInt(Integer.reverseBytes((int) (randomAccessFile.length() - 8)));
+ randomAccessFile.seek(40); // data chunk size
+ randomAccessFile.writeInt(Integer.reverseBytes((int) (randomAccessFile.length() - 44)));
+
+ Log.d(TAG, "wav size: " + randomAccessFile.length());
+ if (listener != null) {
+ listener.onSuccess(savePath);
+ }
+
+ } finally {
+ if (randomAccessFile != null) {
+ randomAccessFile.close();
+ randomAccessFile = null;
+ }
+
+ }
+ }
+
+ public void cancel() {
+ if (randomAccessFile == null) {
+ return;
+ }
+ if (targetFile == null) {
+ return;
+ }
+ if (targetFile.exists()) {
+ targetFile.delete();
+ }
+ randomAccessFile = null;
+ targetFile = null;
+
+ }
+
+
+}
diff --git a/library/src/main/java/tech/oom/idealrecorder/file/AudioFileListener.java b/library/src/main/java/tech/oom/idealrecorder/file/AudioFileListener.java
new file mode 100644
index 0000000..5207bf1
--- /dev/null
+++ b/library/src/main/java/tech/oom/idealrecorder/file/AudioFileListener.java
@@ -0,0 +1,19 @@
+package tech.oom.idealrecorder.file;
+
+/**
+ * Created by issuser on 2017/6/8 0008.
+ */
+
+public interface AudioFileListener {
+ /**
+ * 文件保存失败
+ * @param reason 失败的原因
+ */
+ void onFailure(String reason);
+
+ /**
+ * 文件保存成功
+ * @param savePath 保存文件的路径
+ */
+ void onSuccess(String savePath);
+}
diff --git a/library/src/main/java/tech/oom/idealrecorder/record/Recorder.java b/library/src/main/java/tech/oom/idealrecorder/record/Recorder.java
new file mode 100644
index 0000000..d305cf9
--- /dev/null
+++ b/library/src/main/java/tech/oom/idealrecorder/record/Recorder.java
@@ -0,0 +1,230 @@
+package tech.oom.idealrecorder.record;
+
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+
+import tech.oom.idealrecorder.IdealConst;
+import tech.oom.idealrecorder.IdealRecorder;
+import tech.oom.idealrecorder.utils.Log;
+
+
+public class Recorder {
+ public static final int TIMER_INTERVAL = 100;
+ private static final String TAG = "Recorder";
+ private IdealRecorder.RecordConfig recordConfig;
+ private AudioRecord mAudioRecorder = null;
+ private RecorderCallback mCallback;
+ private int bufferSize;
+ private boolean isRecord = false;
+ private Thread mThread = null;
+ private short[] wave;
+ private Runnable RecordRun = new Runnable() {
+
+ public void run() {
+ if ((mAudioRecorder != null) && (mAudioRecorder.getState() == 1)) {
+
+ try {
+ mAudioRecorder.stop();
+ mAudioRecorder.startRecording();
+ } catch (Exception e) {
+ e.printStackTrace();
+ recordFailed(IdealConst.RecorderErrorCode.RECORDER_EXCEPTION_OCCUR);
+ mAudioRecorder = null;
+ }
+ }
+ if ((mAudioRecorder != null) &&
+ (mAudioRecorder.getState() == 1) && (mAudioRecorder.getRecordingState() == 1)) {
+ Log.e(TAG, "no recorder permission or recorder is not available right now");
+ recordFailed(IdealConst.RecorderErrorCode.RECORDER_PERMISSION_ERROR);
+ mAudioRecorder = null;
+ }
+ for (int i = 0; i < 2; i++) {
+ if (mAudioRecorder == null) {
+ isRecord = false;
+ break;
+ }
+ mAudioRecorder.read(wave, 0, wave.length);
+ }
+ while (isRecord) {
+ int nLen = 0;
+ try {
+ nLen = mAudioRecorder.read(wave, 0, wave.length);
+ } catch (Exception e) {
+ isRecord = false;
+ recordFailed(IdealConst.RecorderErrorCode.RECORDER_EXCEPTION_OCCUR);
+ }
+ if (nLen == wave.length) {
+ mCallback.onRecorded(wave);
+ } else {
+ recordFailed(IdealConst.RecorderErrorCode.RECORDER_READ_ERROR);
+ isRecord = false;
+ }
+ }
+ Log.i(TAG, "out of the reading while loop,i'm going to stop");
+ unInitializeRecord();
+ doRecordStop();
+ }
+ };
+
+
+ public Recorder(IdealRecorder.RecordConfig config, RecorderCallback callback) {
+ this.mCallback = callback;
+ this.recordConfig = config;
+ }
+
+ public void setRecordConfig(IdealRecorder.RecordConfig config) {
+ this.recordConfig = config;
+ }
+
+
+ public boolean start() {
+ isRecord = true;
+ synchronized (this) {
+ if (doRecordReady()) {
+ Log.d(TAG, "doRecordReady");
+ if (initializeRecord()) {
+ Log.d(TAG, "initializeRecord");
+ if (doRecordStart()) {
+ Log.d(TAG, "doRecordStart");
+
+ mThread = new Thread(RecordRun);
+ mThread.start();
+ return true;
+ }
+ }
+ }
+ }
+ isRecord = false;
+ return false;
+ }
+
+
+ public void stop() {
+ synchronized (this) {
+ mThread = null;
+ isRecord = false;
+ }
+ }
+
+ public void immediateStop() {
+ isRecord = false;
+ if (mThread != null) {
+ try {
+ mThread.join();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ mThread = null;
+ }
+
+ public boolean isStarted() {
+ return isRecord;
+ }
+
+ private boolean initializeRecord() {
+ synchronized (this) {
+ try {
+ if (mCallback == null) {
+ Log.e(TAG, "Error VoiceRecorderCallback is null");
+ return false;
+ }
+ if (recordConfig == null) {
+ Log.e(TAG, "Error recordConfig is null");
+ }
+ short nChannels;
+ int sampleRate;
+ short bSamples;
+ int audioSource;
+ int audioFormat;
+ int channelConfig;
+ if (recordConfig.getAudioFormat() == AudioFormat.ENCODING_PCM_16BIT) {
+ bSamples = 16;
+ } else {
+ bSamples = 8;
+ }
+
+ if ((channelConfig = recordConfig.getChannelConfig()) == AudioFormat.CHANNEL_IN_MONO) {
+ nChannels = 1;
+ } else {
+ nChannels = 2;
+ }
+ audioSource = recordConfig.getAudioSource();
+ sampleRate = recordConfig.getSampleRate();
+ audioFormat = recordConfig.getAudioFormat();
+ int framePeriod = sampleRate * TIMER_INTERVAL / 1000;
+ bufferSize = framePeriod * 2 * bSamples * nChannels / 8;
+
+ wave = new short[framePeriod * bSamples / 8 * nChannels / 2];
+ Log.d(TAG, "buffersize = " + bufferSize);
+ int nMinSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
+ if (bufferSize < nMinSize) {
+ bufferSize = nMinSize;
+
+ Log.d(TAG, "Increasing buffer size to " + Integer.toString(bufferSize));
+ }
+ if (mAudioRecorder != null) {
+ unInitializeRecord();
+ }
+ mAudioRecorder = new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat, bufferSize);
+ if (mAudioRecorder.getState() != 1) {
+ mAudioRecorder = null;
+ recordFailed(IdealConst.RecorderErrorCode.RECORDER_PERMISSION_ERROR);
+ Log.e(TAG, "AudioRecord initialization failed,because of no RECORD permission or unavailable AudioRecord ");
+ throw new Exception("AudioRecord initialization failed");
+ }
+ Log.i(TAG, "initialize Record");
+ return true;
+ } catch (Throwable e) {
+ if (e.getMessage() != null) {
+ Log.e(TAG, getClass().getName() + e.getMessage());
+ } else {
+ Log.e(TAG, getClass().getName() + "Unknown error occured while initializing recording");
+ }
+ return false;
+ }
+ }
+ }
+
+ private void unInitializeRecord() {
+ Log.i(TAG, "unInitializeRecord");
+ synchronized (this) {
+ if (mAudioRecorder != null) {
+ try {
+ mAudioRecorder.stop();
+ mAudioRecorder.release();
+ } catch (Exception e) {
+ e.printStackTrace();
+ Log.e(TAG, "mAudioRecorder release error!");
+ }
+ mAudioRecorder = null;
+ }
+ }
+ }
+
+ private boolean doRecordStart() {
+ if (mCallback != null) {
+ return mCallback.onRecorderStart();
+ }
+ return true;
+ }
+
+ private boolean doRecordReady() {
+ if (mCallback != null) {
+ return mCallback.onRecorderReady();
+ }
+ return true;
+ }
+
+ private void doRecordStop() {
+ if (mCallback != null) {
+ mCallback.onRecorderStop();
+ }
+ }
+
+ private void recordFailed(int errorCode) {
+ if (mCallback != null) {
+ mCallback.onRecordedFail(errorCode);
+ }
+ }
+}
diff --git a/library/src/main/java/tech/oom/idealrecorder/record/RecorderCallback.java b/library/src/main/java/tech/oom/idealrecorder/record/RecorderCallback.java
new file mode 100644
index 0000000..9fb7ce0
--- /dev/null
+++ b/library/src/main/java/tech/oom/idealrecorder/record/RecorderCallback.java
@@ -0,0 +1,33 @@
+package tech.oom.idealrecorder.record;
+
+public abstract interface RecorderCallback {
+ /**
+ * 录音开始
+ */
+ public abstract boolean onRecorderStart();
+
+ /**
+ * 录音是否就绪
+ */
+ public abstract boolean onRecorderReady();
+
+ /**
+ * 录音停止
+ */
+ public abstract void onRecorderStop();
+
+ /**
+ * 正在录音
+ *
+ * @param wave 录制的数据data
+ */
+ public abstract void onRecorded(short[] wave);
+
+
+ /**
+ * 录制失败
+ *
+ * @param paramInt 失败的code
+ */
+ public abstract void onRecordedFail(int paramInt);
+}
diff --git a/library/src/main/java/tech/oom/idealrecorder/utils/BytesTransUtil.java b/library/src/main/java/tech/oom/idealrecorder/utils/BytesTransUtil.java
new file mode 100644
index 0000000..3ad3624
--- /dev/null
+++ b/library/src/main/java/tech/oom/idealrecorder/utils/BytesTransUtil.java
@@ -0,0 +1,235 @@
+package tech.oom.idealrecorder.utils;
+
+import java.nio.ByteOrder;
+
+public class BytesTransUtil {
+ private static BytesTransUtil instance = null;
+ private String TAG = "BytesTransUtil";
+
+ public static BytesTransUtil getInstance() {
+ if (instance == null) {
+ instance = new BytesTransUtil();
+ }
+ return instance;
+ }
+
+ public boolean testCPU() {
+ if (ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN) {
+ return true;
+ }
+ return false;
+ }
+
+ public byte[] getBytes(short s, boolean bBigEnding) {
+ byte[] buf = new byte[2];
+ if (bBigEnding) {
+ for (int i = buf.length - 1; i >= 0; i--) {
+ buf[i] = ((byte) (s & 0xFF));
+ s = (short) (s >> 8);
+ }
+ } else {
+ for (int i = 0; i < buf.length; i++) {
+ buf[i] = ((byte) (s & 0xFF));
+ s = (short) (s >> 8);
+ }
+ }
+ return buf;
+ }
+
+ public byte[] getBytes(int s, boolean bBigEnding) {
+ byte[] buf = new byte[4];
+ if (bBigEnding) {
+ for (int i = buf.length - 1; i >= 0; i--) {
+ buf[i] = ((byte) (s & 0xFF));
+ s >>= 8;
+ }
+ } else {
+ for (int i = 0; i < buf.length; i++) {
+ buf[i] = ((byte) (s & 0xFF));
+ s >>= 8;
+ }
+ }
+ return buf;
+ }
+
+ public byte[] getBytes(long s, boolean bBigEnding) {
+ byte[] buf = new byte[8];
+ if (bBigEnding) {
+ for (int i = buf.length - 1; i >= 0; i--) {
+ buf[i] = ((byte) (int) (s & 0xFF));
+ s >>= 8;
+ }
+ } else {
+ for (int i = 0; i < buf.length; i++) {
+ buf[i] = ((byte) (int) (s & 0xFF));
+ s >>= 8;
+ }
+ }
+ return buf;
+ }
+
+ public short getShort(byte[] buf, boolean bBigEnding) {
+ if (buf == null) {
+ throw new IllegalArgumentException("byte array is null!");
+ }
+ if (buf.length > 2) {
+ throw new IllegalArgumentException("byte array size > 2 !");
+ }
+ short r = 0;
+ if (bBigEnding) {
+ for (int i = 0; i < buf.length; i++) {
+ r = (short) (r << 8);
+ r = (short) (r | buf[i] & 0xFF);
+ }
+ } else {
+ for (int i = buf.length - 1; i >= 0; i--) {
+ r = (short) (r << 8);
+ r = (short) (r | buf[i] & 0xFF);
+ }
+ }
+ return r;
+ }
+
+ public int getInt(byte[] buf, boolean bBigEnding) {
+ if (buf == null) {
+ throw new IllegalArgumentException("byte array is null!");
+ }
+ if (buf.length > 4) {
+ throw new IllegalArgumentException("byte array size > 4 !");
+ }
+ int r = 0;
+ if (bBigEnding) {
+ for (int i = 0; i < buf.length; i++) {
+ r <<= 8;
+ r |= buf[i] & 0xFF;
+ }
+ } else {
+ for (int i = buf.length - 1; i >= 0; i--) {
+ r <<= 8;
+ r |= buf[i] & 0xFF;
+ }
+ }
+ return r;
+ }
+
+ public long getLong(byte[] buf, boolean bBigEnding) {
+ if (buf == null) {
+ throw new IllegalArgumentException("byte array is null!");
+ }
+ if (buf.length > 8) {
+ throw new IllegalArgumentException("byte array size > 8 !");
+ }
+ long r = 0L;
+ if (bBigEnding) {
+ for (int i = 0; i < buf.length; i++) {
+ r <<= 8;
+ r |= buf[i] & 0xFF;
+ }
+ } else {
+ for (int i = buf.length - 1; i >= 0; i--) {
+ r <<= 8;
+ r |= buf[i] & 0xFF;
+ }
+ }
+ return r;
+ }
+
+ public byte[] getBytes(int i) {
+ return getBytes(i, testCPU());
+ }
+
+ public byte[] getBytes(short s) {
+ return getBytes(s, testCPU());
+ }
+
+ public byte[] getBytes(long l) {
+ return getBytes(l, testCPU());
+ }
+
+ public int getInt(byte[] buf) {
+ return getInt(buf, testCPU());
+ }
+
+ public short getShort(byte[] buf) {
+ return getShort(buf, testCPU());
+ }
+
+ public long getLong(byte[] buf) {
+ return getLong(buf, testCPU());
+ }
+
+ public short[] Bytes2Shorts(byte[] buf) {
+ byte bLength = 2;
+ short[] s = new short[buf.length / bLength];
+ for (int iLoop = 0; iLoop < s.length; iLoop++) {
+ byte[] temp = new byte[bLength];
+ for (int jLoop = 0; jLoop < bLength; jLoop++) {
+ temp[jLoop] = buf[(iLoop * bLength + jLoop)];
+ }
+ s[iLoop] = getShort(temp);
+ }
+ return s;
+ }
+
+ public byte[] Shorts2Bytes(short[] s) {
+ byte bLength = 2;
+ byte[] buf = new byte[s.length * bLength];
+ for (int iLoop = 0; iLoop < s.length; iLoop++) {
+ byte[] temp = getBytes(s[iLoop]);
+ for (int jLoop = 0; jLoop < bLength; jLoop++) {
+ buf[(iLoop * bLength + jLoop)] = temp[jLoop];
+ }
+ }
+ return buf;
+ }
+
+ public int[] Bytes2Ints(byte[] buf) {
+ byte bLength = 4;
+ int[] s = new int[buf.length / bLength];
+ for (int iLoop = 0; iLoop < s.length; iLoop++) {
+ byte[] temp = new byte[bLength];
+ for (int jLoop = 0; jLoop < bLength; jLoop++) {
+ temp[jLoop] = buf[(iLoop * bLength + jLoop)];
+ }
+ s[iLoop] = getInt(temp);
+ }
+ return s;
+ }
+
+ public byte[] Ints2Bytes(int[] s) {
+ byte bLength = 4;
+ byte[] buf = new byte[s.length * bLength];
+ for (int iLoop = 0; iLoop < s.length; iLoop++) {
+ byte[] temp = getBytes(s[iLoop]);
+ for (int jLoop = 0; jLoop < bLength; jLoop++) {
+ buf[(iLoop * bLength + jLoop)] = temp[jLoop];
+ }
+ }
+ return buf;
+ }
+
+ public long[] Bytes2Longs(byte[] buf) {
+ byte bLength = 8;
+ long[] s = new long[buf.length / bLength];
+ for (int iLoop = 0; iLoop < s.length; iLoop++) {
+ byte[] temp = new byte[bLength];
+ for (int jLoop = 0; jLoop < bLength; jLoop++) {
+ temp[jLoop] = buf[(iLoop * bLength + jLoop)];
+ }
+ s[iLoop] = getLong(temp);
+ }
+ return s;
+ }
+
+ public byte[] Longs2Bytes(long[] s) {
+ byte bLength = 8;
+ byte[] buf = new byte[s.length * bLength];
+ for (int iLoop = 0; iLoop < s.length; iLoop++) {
+ byte[] temp = getBytes(s[iLoop]);
+ for (int jLoop = 0; jLoop < bLength; jLoop++) {
+ buf[(iLoop * bLength + jLoop)] = temp[jLoop];
+ }
+ }
+ return buf;
+ }
+}
diff --git a/library/src/main/java/tech/oom/idealrecorder/utils/Log.java b/library/src/main/java/tech/oom/idealrecorder/utils/Log.java
new file mode 100644
index 0000000..f6ee8fa
--- /dev/null
+++ b/library/src/main/java/tech/oom/idealrecorder/utils/Log.java
@@ -0,0 +1,59 @@
+package tech.oom.idealrecorder.utils;
+
+/**
+ *
+ * 日志管理类
+ */
+public class Log {
+
+ /** 是否打印日志 */
+ public static boolean DEBUG = true;
+
+ public static void v(String tag, String msg) {
+ if (DEBUG) {
+ android.util.Log.v(tag, msg);
+ }
+ }
+
+ public static void d(String tag, String msg) {
+ if (DEBUG) {
+ android.util.Log.d(tag, msg);
+ }
+ }
+
+ public static void d(String tag, String msg, Throwable tr) {
+ if (DEBUG) {
+ android.util.Log.d(tag, msg, tr);
+ }
+ }
+
+ public static void i(String tag, String msg) {
+ if (DEBUG) {
+ android.util.Log.i(tag, msg);
+ }
+ }
+
+ public static void w(String tag, String msg) {
+ if (DEBUG) {
+ android.util.Log.w(tag, msg);
+ }
+ }
+
+ public static void w(String tag, String msg, Throwable tr) {
+ if (DEBUG) {
+ android.util.Log.w(tag, msg, tr);
+ }
+ }
+
+ public static void e(String tag, String msg) {
+ if (DEBUG) {
+ android.util.Log.e(tag, msg);
+ }
+ }
+
+ public static void e(String tag, String msg, Throwable tr) {
+ if (DEBUG) {
+ android.util.Log.e(tag, msg, tr);
+ }
+ }
+}
diff --git a/library/src/main/res/values/strings.xml b/library/src/main/res/values/strings.xml
new file mode 100644
index 0000000..d593dff
--- /dev/null
+++ b/library/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ idealRecorder
+
diff --git a/library/src/test/java/tech/oom/idealrecorder/ExampleUnitTest.java b/library/src/test/java/tech/oom/idealrecorder/ExampleUnitTest.java
new file mode 100644
index 0000000..4a6c4ba
--- /dev/null
+++ b/library/src/test/java/tech/oom/idealrecorder/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package tech.oom.idealrecorder;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..3306997
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app', ':library'