├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── app-release.apk ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── github │ │ └── junyuecao │ │ └── androidsoundeffect │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── junyuecao │ │ │ └── androidsoundeffect │ │ │ ├── MainActivity.kt │ │ │ └── VoiceRecorder.java │ └── res │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── io │ └── github │ └── junyuecao │ └── androidsoundeffect │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── soundtouch ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src ├── androidTest └── java │ └── io │ └── github │ └── junyuecao │ └── soundtouch │ └── ExampleInstrumentedTest.java ├── main ├── AndroidManifest.xml ├── cpp │ ├── Android.mk │ ├── Application.mk │ ├── SoundTouch │ │ ├── AAFilter.cpp │ │ ├── AAFilter.h │ │ ├── BPMDetect.cpp │ │ ├── FIFOSampleBuffer.cpp │ │ ├── FIRFilter.cpp │ │ ├── FIRFilter.h │ │ ├── InterpolateCubic.cpp │ │ ├── InterpolateCubic.h │ │ ├── InterpolateLinear.cpp │ │ ├── InterpolateLinear.h │ │ ├── InterpolateShannon.cpp │ │ ├── InterpolateShannon.h │ │ ├── Makefile.am │ │ ├── PeakFinder.cpp │ │ ├── PeakFinder.h │ │ ├── RateTransposer.cpp │ │ ├── RateTransposer.h │ │ ├── SoundTouch.cpp │ │ ├── SoundTouch.dsp │ │ ├── SoundTouch.dsw │ │ ├── SoundTouch.sln │ │ ├── SoundTouch.vcproj │ │ ├── TDStretch.cpp │ │ ├── TDStretch.h │ │ ├── cpu_detect.h │ │ ├── cpu_detect_x86.cpp │ │ ├── include │ │ │ ├── BPMDetect.h │ │ │ ├── FIFOSampleBuffer.h │ │ │ ├── FIFOSamplePipe.h │ │ │ ├── Makefile.am │ │ │ ├── STTypes.h │ │ │ ├── SoundTouch.h │ │ │ └── soundtouch_config.h.in │ │ ├── mmx_optimized.cpp │ │ └── sse_optimized.cpp │ ├── SoundTouchJNI.cpp │ └── common.h ├── java │ └── io │ │ └── github │ │ └── junyuecao │ │ └── soundtouch │ │ └── SoundTouch.java └── res │ └── values │ └── strings.xml └── test └── java └── io └── github └── junyuecao └── soundtouch └── ExampleUnitTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | android: 3 | components: 4 | - build-tools-26.0.1 5 | - android-26 6 | - extra 7 | before_script: 8 | - export TERM=dumb 9 | # Approx. 3 seconds 10 | - curl -L https://dl.google.com/android/repository/android-ndk-r14b-linux-x86_64.zip -O 11 | # Usually around 1 minute 12 | - unzip android-ndk-r14b-linux-x86_64.zip > /dev/null 13 | - rm android-ndk-r14b-linux-x86_64.zip 14 | - export ANDROID_NDK_HOME=`pwd`/android-ndk-r14b 15 | - export LOCAL_ANDROID_NDK_HOME="$ANDROID_NDK_HOME" 16 | - export LOCAL_ANDROID_NDK_HOST_PLATFORM="linux-x86_64" 17 | - export PATH=$PATH:${ANDROID_NDK_HOME} 18 | - env 19 | script: 20 | - ./gradlew build -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidSoundTouch 2 | [![Build Status](https://travis-ci.org/junyuecao/AndroidSoundTouch.svg?branch=master)](https://travis-ci.org/junyuecao/AndroidSoundTouch) 3 | 4 | A handy SoundTouch java wrapper library for Android 5 | 6 | This repo is a java wrapper of [SoundTouch](https://www.surina.net/soundtouch/) for Android. 7 | 8 | - Keep APIs of soundtouch and provide a Java class to process audio streams so you can do it without any C/C++ code. 9 | - Support ENCODING_PCM_16BIT samples. 10 | - Support mono(1)/stereo(2) channels. 11 | - Support short/byte array input/output 12 | 13 | ## Demo 14 | 15 | [Demo apk download link](https://github.com/junyuecao/AndroidSoundTouch/blob/master/app-release.apk?raw=true) 16 | 17 | Note: You should grant audio record permission manually before using. 18 | 19 | ## Usage 20 | Gradle: 21 | ```groovy 22 | repositories { 23 | jcenter() 24 | } 25 | 26 | // For gradle plugin 2.x 27 | dependencies { 28 | compile 'io.github.junyuecao:soundtouch:1.0.1' 29 | } 30 | 31 | // For gradle plugin 3.x 32 | dependencies { 33 | implementation 'io.github.junyuecao:soundtouch:1.0.1' 34 | } 35 | ``` 36 | 37 | The APIs are almost the same with the C/C++ version. 38 | - in Java 39 | ```Java 40 | public void onVoiceStart() { 41 | mSoundTouch = new SoundTouch(); 42 | mSoundTouch.setChannels(1); 43 | mSoundTouch.setSampleRate(VoiceRecorder.SAMPLE_RATE); 44 | } 45 | 46 | public void onVoice(byte[] data, int size) { 47 | mSoundTouch.setRate(mRate); 48 | mSoundTouch.setPitch(mPitch); 49 | mSoundTouch.putSamples(data, size); 50 | int bufferSize = 0 51 | do { 52 | bufferSize = mSoundTouch.receiveSamples(mTempBuffer, BUFFER_SIZE); 53 | if (bufferSize > 0) { 54 | mTestWavOutput.write(mTempBuffer, 0, bufferSize); 55 | } 56 | } while (bufferSize != 0); 57 | 58 | } 59 | 60 | public void onVoiceEnd() { 61 | mSoundTouch.release(); 62 | } 63 | ``` 64 | 65 | -------------------------------------------------------------------------------- /app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junyuecao/AndroidSoundTouch/18132e55bf48204d35cd1cc57ca1f4bd7a8a764e/app-release.apk -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | def sign = project.hasProperty("RELEASE_STORE_FILE"); 9 | signingConfigs { 10 | if (sign) { 11 | myConfig { 12 | //需要在build.gradle同目录下的gradle.properties中配置以下信息 13 | //RELEASE_STORE_FILE=/path/to/key/ 14 | //RELEASE_STORE_PASSWORD=*********** 15 | //RELEASE_KEY_ALIAS=***** 16 | //RELEASE_KEY_PASSWORD=******* 17 | storeFile file(RELEASE_STORE_FILE) 18 | storePassword RELEASE_STORE_PASSWORD 19 | keyAlias RELEASE_KEY_ALIAS 20 | keyPassword RELEASE_KEY_PASSWORD 21 | } 22 | } 23 | 24 | } 25 | compileSdkVersion 26 26 | buildToolsVersion "26.0.1" 27 | defaultConfig { 28 | applicationId "io.github.junyuecao.androidsoundeffect" 29 | minSdkVersion 21 30 | targetSdkVersion 26 31 | versionCode 1 32 | versionName "1.0" 33 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 34 | } 35 | buildTypes { 36 | release { 37 | signingConfig sign ? signingConfigs.myConfig : null 38 | minifyEnabled false 39 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 40 | } 41 | } 42 | } 43 | 44 | dependencies { 45 | implementation fileTree(dir: 'libs', include: ['*.jar']) 46 | implementation 'com.android.support:appcompat-v7:26.1.0' 47 | implementation 'com.android.support.constraint:constraint-layout:1.0.2' 48 | testImplementation 'junit:junit:4.12' 49 | androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.1', { 50 | exclude group: 'com.android.support', module: 'support-annotations' 51 | }) 52 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" 53 | compile project(path: ':soundtouch') 54 | } 55 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/github/junyuecao/androidsoundeffect/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.junyuecao.androidsoundeffect 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("io.github.junyuecao.androidsoundeffect", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/junyuecao/androidsoundeffect/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.junyuecao.androidsoundeffect 2 | 3 | import android.media.AudioFormat 4 | import android.media.MediaPlayer 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import android.support.v7.app.AppCompatActivity 8 | import android.util.Log 9 | import android.widget.SeekBar 10 | import io.github.junyuecao.soundtouch.SoundTouch 11 | import kotlinx.android.synthetic.main.activity_main.* 12 | import java.io.* 13 | import java.nio.ByteBuffer 14 | import java.nio.ByteOrder 15 | 16 | 17 | class MainActivity : AppCompatActivity(), VoiceRecorder.Callback { 18 | val TAG = "MainActivity" 19 | private var mRecorder : VoiceRecorder? = null 20 | private var mSoundTouch : SoundTouch? = null 21 | private var mIsRecording = false 22 | private var mTestWavOutput: FileOutputStream? = null 23 | private val BUFFER_SIZE: Int = 4096 24 | private var mTempBuffer : ByteArray = ByteArray(BUFFER_SIZE) 25 | 26 | private var mPitch: Double = 1.0; 27 | private var mRate: Double = 1.0; 28 | 29 | override fun onVoiceStart() { 30 | mSoundTouch = SoundTouch() 31 | mSoundTouch?.setChannels(1) 32 | mSoundTouch?.setSampleRate(VoiceRecorder.SAMPLE_RATE) 33 | mTestWavOutput = getTestWavOutput() 34 | writeWavHeader(mTestWavOutput!!, 35 | AudioFormat.CHANNEL_IN_MONO, 36 | VoiceRecorder.SAMPLE_RATE, 37 | AudioFormat.ENCODING_PCM_16BIT); 38 | } 39 | 40 | override fun onVoice(data: ByteArray?, size: Int) { 41 | Log.d(TAG, "onVoice: $data, Size: $size") 42 | mSoundTouch?.setRate(mRate) 43 | mSoundTouch?.setPitch(mPitch) 44 | mSoundTouch?.putSamples(data, size) 45 | var bufferSize = 0 46 | do { 47 | bufferSize = mSoundTouch!!.receiveSamples(mTempBuffer, BUFFER_SIZE) 48 | if (bufferSize > 0) { 49 | mTestWavOutput?.write(mTempBuffer, 0, bufferSize) 50 | } 51 | } while (bufferSize != 0) 52 | 53 | } 54 | 55 | override fun onVoiceEnd() { 56 | mSoundTouch?.release() 57 | try { 58 | mTestWavOutput?.close() 59 | mTestWavOutput = null 60 | } catch (e: IOException) { 61 | e.printStackTrace() 62 | } 63 | updateWavHeader(getTempFile()) 64 | } 65 | 66 | override fun onCreate(savedInstanceState: Bundle?) { 67 | super.onCreate(savedInstanceState) 68 | setContentView(R.layout.activity_main) 69 | 70 | // Example of a call to a native method 71 | mRecorder = VoiceRecorder(this) 72 | 73 | start.setOnClickListener { 74 | mIsRecording = !mIsRecording 75 | if (mIsRecording) { 76 | start.text = "Stop" 77 | mRecorder?.start() 78 | } else { 79 | start.text = "Start" 80 | mRecorder?.stop() 81 | } 82 | 83 | } 84 | 85 | play.setOnClickListener { 86 | val tempFile = getTempFile() 87 | if (tempFile.exists()) { 88 | val player = MediaPlayer.create(this, Uri.fromFile(tempFile)) 89 | player.start() 90 | } 91 | } 92 | pitchText.text = "Pitch: $mPitch" 93 | pitch.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { 94 | override fun onStartTrackingTouch(p0: SeekBar?) { 95 | 96 | } 97 | 98 | override fun onStopTrackingTouch(p0: SeekBar?) { 99 | 100 | } 101 | 102 | override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) { 103 | if (pitch.progress > 500) { 104 | mPitch = (3.0 / 500.0) * pitch.progress - 2 105 | } else{ 106 | mPitch = 0.75 / 500 * pitch.progress+ 0.25 107 | } 108 | pitchText.text = "Pitch: $mPitch" 109 | } 110 | 111 | }) 112 | rateText.text = "Rate: $mRate" 113 | rate.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { 114 | override fun onStartTrackingTouch(p0: SeekBar?) { 115 | 116 | } 117 | 118 | override fun onStopTrackingTouch(p0: SeekBar?) { 119 | 120 | } 121 | 122 | override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) { 123 | if (pitch.progress > 500) { 124 | mRate = (3.0 / 500.0) * rate.progress - 2 125 | } else{ 126 | mRate = 0.75 / 500 * rate.progress+ 0.25 127 | } 128 | rateText.text = "Rate: $mRate" 129 | } 130 | }) 131 | } 132 | 133 | private fun getTestWavOutput(): FileOutputStream? { 134 | val s = getTempFile() 135 | 136 | try { 137 | val os = FileOutputStream(s) 138 | return os 139 | } catch (e: FileNotFoundException) { 140 | e.printStackTrace() 141 | return null 142 | } 143 | 144 | } 145 | 146 | override fun onStop() { 147 | super.onStop() 148 | mIsRecording = false 149 | start.text = "Start" 150 | mRecorder?.stop() 151 | } 152 | 153 | private fun getTempFile() = File(getExternalFilesDir(null), "record_temp.wav") 154 | 155 | /** 156 | * Writes the proper 44-byte RIFF/WAVE header to/for the given stream 157 | * Two size fields are left empty/null since we do not yet know the final stream size 158 | 159 | * @param out The stream to write the header to 160 | * * 161 | * @param channelMask An AudioFormat.CHANNEL_* mask 162 | * * 163 | * @param sampleRate The sample rate in hertz 164 | * * 165 | * @param encoding An AudioFormat.ENCODING_PCM_* value 166 | * * 167 | * @throws IOException 168 | */ 169 | @Throws(IOException::class) 170 | private fun writeWavHeader(out: OutputStream, channelMask: Int, sampleRate: Int, encoding: Int) { 171 | val channels: Short 172 | when (channelMask) { 173 | AudioFormat.CHANNEL_IN_MONO -> channels = 1 174 | AudioFormat.CHANNEL_IN_STEREO -> channels = 2 175 | else -> throw IllegalArgumentException("Unacceptable channel mask") 176 | } 177 | 178 | val bitDepth: Short 179 | when (encoding) { 180 | AudioFormat.ENCODING_PCM_8BIT -> bitDepth = 8 181 | AudioFormat.ENCODING_PCM_16BIT -> bitDepth = 16 182 | AudioFormat.ENCODING_PCM_FLOAT -> bitDepth = 32 183 | else -> throw IllegalArgumentException("Unacceptable encoding") 184 | } 185 | 186 | writeWavHeader(out, channels, sampleRate, bitDepth) 187 | } 188 | 189 | /** 190 | * Writes the proper 44-byte RIFF/WAVE header to/for the given stream 191 | * Two size fields are left empty/null since we do not yet know the final stream size 192 | 193 | * @param out The stream to write the header to 194 | * * 195 | * @param channels The number of channels 196 | * * 197 | * @param sampleRate The sample rate in hertz 198 | * * 199 | * @param bitDepth The bit depth 200 | * * 201 | * @throws IOException 202 | */ 203 | @Throws(IOException::class) 204 | private fun writeWavHeader(out: OutputStream, channels: Short, sampleRate: Int, bitDepth: Short) { 205 | // Convert the multi-byte integers to raw bytes in little endian format as required by the spec 206 | val littleBytes = ByteBuffer 207 | .allocate(14) 208 | .order(ByteOrder.LITTLE_ENDIAN) 209 | .putShort(channels) 210 | .putInt(sampleRate) 211 | .putInt(sampleRate * channels.toInt() * (bitDepth / 8)) 212 | .putShort((channels * (bitDepth / 8)).toShort()) 213 | .putShort(bitDepth) 214 | .array() 215 | 216 | // Not necessarily the best, but it's very easy to visualize this way 217 | out.write(byteArrayOf( 218 | // RIFF header 219 | 'R'.toByte(), 'I'.toByte(), 'F'.toByte(), 'F'.toByte(), // ChunkID 220 | 0, 0, 0, 0, // ChunkSize (must be updated later) 221 | 'W'.toByte(), 'A'.toByte(), 'V'.toByte(), 'E'.toByte(), // Format 222 | // fmt subchunk 223 | 'f'.toByte(), 'm'.toByte(), 't'.toByte(), ' '.toByte(), // Subchunk1ID 224 | 16, 0, 0, 0, // Subchunk1Size 225 | 1, 0, // AudioFormat 226 | littleBytes[0], littleBytes[1], // NumChannels 227 | littleBytes[2], littleBytes[3], littleBytes[4], littleBytes[5], // SampleRate 228 | littleBytes[6], littleBytes[7], littleBytes[8], littleBytes[9], // ByteRate 229 | littleBytes[10], littleBytes[11], // BlockAlign 230 | littleBytes[12], littleBytes[13], // BitsPerSample 231 | // data subchunk 232 | 'd'.toByte(), 'a'.toByte(), 't'.toByte(), 'a'.toByte(), // Subchunk2ID 233 | 0, 0, 0, 0)// Subchunk2Size (must be updated later) 234 | ) 235 | } 236 | 237 | /** 238 | * Updates the given wav file's header to include the final chunk sizes 239 | 240 | * @param wav The wav file to update 241 | * * 242 | * @throws IOException 243 | */ 244 | @Throws(IOException::class) 245 | private fun updateWavHeader(wav: File) { 246 | val sizes = ByteBuffer 247 | .allocate(8) 248 | .order(ByteOrder.LITTLE_ENDIAN) 249 | // There are probably a bunch of different/better ways to calculate 250 | // these two given your circumstances. Cast should be safe since if the WAV is 251 | // > 4 GB we've already made a terrible mistake. 252 | .putInt((wav.length() - 8).toInt()) // ChunkSize 253 | .putInt((wav.length() - 44).toInt()) // Subchunk2Size 254 | .array() 255 | 256 | var accessWave: RandomAccessFile? = null 257 | 258 | try { 259 | accessWave = RandomAccessFile(wav, "rw") 260 | // ChunkSize 261 | accessWave.seek(4) 262 | accessWave.write(sizes, 0, 4) 263 | 264 | // Subchunk2Size 265 | accessWave.seek(40) 266 | accessWave.write(sizes, 4, 4) 267 | } catch (ex: IOException) { 268 | // Rethrow but we still close accessWave in our finally 269 | throw ex 270 | } finally { 271 | if (accessWave != null) { 272 | try { 273 | accessWave.close() 274 | } catch (ex: IOException) { 275 | // 276 | } 277 | 278 | } 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/junyuecao/androidsoundeffect/VoiceRecorder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.junyuecao.androidsoundeffect; 18 | 19 | import android.media.AudioFormat; 20 | import android.media.AudioRecord; 21 | import android.media.MediaRecorder; 22 | import android.support.annotation.NonNull; 23 | import android.support.annotation.RequiresApi; 24 | 25 | /** 26 | * 麦克风采集 27 | * Continuously records audio and notifies the {@link VoiceRecorder.Callback} when voice (or any 28 | * sound) is heard. 29 | * 30 | *

The recorded audio format is always {@link AudioFormat#ENCODING_PCM_16BIT} and 31 | * {@link AudioFormat#CHANNEL_IN_MONO}. This class will automatically pick the right sample rate 32 | * for the device. Use {@link #getSampleRate()} to get the selected value.

33 | */ 34 | @RequiresApi(18) 35 | public class VoiceRecorder { 36 | 37 | public static final int SAMPLE_RATE = 44100; 38 | 39 | private static final int CHANNEL = AudioFormat.CHANNEL_IN_MONO; 40 | private static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; 41 | 42 | // private static final int AMPLITUDE_THRESHOLD = 1500; 43 | // private static final int SPEECH_TIMEOUT_MILLIS = 2000; 44 | private static final int MAX_SPEECH_LENGTH_MILLIS = 30 * 1000; 45 | private final Callback mCallback; 46 | private final Object mLock = new Object(); 47 | private AudioRecord mAudioRecord; 48 | 49 | private Thread mThread; 50 | 51 | private byte[] mBuffer; 52 | /** The timestamp of the last time that voice is heard. */ 53 | private long mLastVoiceHeardMillis = Long.MAX_VALUE; 54 | /** The timestamp when the current voice is started. */ 55 | private long mVoiceStartedMillis; 56 | 57 | public VoiceRecorder(@NonNull Callback callback) { 58 | mCallback = callback; 59 | } 60 | 61 | /** 62 | * Starts recording audio. 63 | * 64 | *

The caller is responsible for calling {@link #stop()} later.

65 | */ 66 | public void start() { 67 | // Stop recording if it is currently ongoing. 68 | stop(); 69 | // Try to create a new recording session. 70 | mAudioRecord = createAudioRecord(); 71 | if (mAudioRecord == null) { 72 | throw new RuntimeException("Cannot instantiate VoiceRecorder"); 73 | } 74 | // Start recording. 75 | mAudioRecord.startRecording(); 76 | // Start processing the captured audio. 77 | mThread = new Thread(new ProcessVoice()); 78 | mThread.start(); 79 | } 80 | 81 | /** 82 | * Stops recording audio. 83 | */ 84 | public void stop() { 85 | if (mThread != null) { 86 | mThread.interrupt(); 87 | mThread = null; 88 | } 89 | synchronized (mLock) { 90 | dismiss(); 91 | if (mAudioRecord != null) { 92 | mAudioRecord.stop(); 93 | mAudioRecord.release(); 94 | mAudioRecord = null; 95 | } 96 | mBuffer = null; 97 | } 98 | } 99 | 100 | /** 101 | * Dismisses the currently ongoing utterance. 102 | */ 103 | public void dismiss() { 104 | if (mLastVoiceHeardMillis != Long.MAX_VALUE) { 105 | mLastVoiceHeardMillis = Long.MAX_VALUE; 106 | mCallback.onVoiceEnd(); 107 | } 108 | } 109 | 110 | /** 111 | * Retrieves the sample rate currently used to record audio. 112 | * 113 | * @return The sample rate of recorded audio. 114 | */ 115 | public int getSampleRate() { 116 | if (mAudioRecord != null) { 117 | return mAudioRecord.getSampleRate(); 118 | } 119 | return 0; 120 | } 121 | 122 | /** 123 | * Creates a new {@link AudioRecord}. 124 | * 125 | * @return A newly created {@link AudioRecord}, or null if it cannot be created (missing 126 | * permissions?). 127 | */ 128 | private AudioRecord createAudioRecord() { 129 | // for (int sampleRate : SAMPLE_RATE_CANDIDATES) { 130 | final int sizeInBytes = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL, ENCODING); 131 | if (sizeInBytes == AudioRecord.ERROR_BAD_VALUE) { 132 | return null; 133 | } 134 | final AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, 135 | SAMPLE_RATE, CHANNEL, ENCODING, sizeInBytes); 136 | if (audioRecord.getState() == AudioRecord.STATE_INITIALIZED) { 137 | mBuffer = new byte[sizeInBytes]; 138 | return audioRecord; 139 | } else { 140 | audioRecord.release(); 141 | } 142 | // } 143 | return null; 144 | } 145 | 146 | public static abstract interface Callback { 147 | 148 | /** 149 | * Called when the recorder starts hearing voice. 150 | */ 151 | void onVoiceStart(); 152 | 153 | /** 154 | * Called when the recorder is hearing voice. 155 | * 156 | * @param data The audio data in {@link AudioFormat#ENCODING_PCM_16BIT}. 157 | * @param size The size of the actual data in {@code data}. 158 | */ 159 | void onVoice(byte[] data, int size); 160 | 161 | /** 162 | * Called when the recorder stops hearing voice. 163 | */ 164 | void onVoiceEnd(); 165 | } 166 | 167 | /** 168 | * Continuously processes the captured audio and notifies {@link #mCallback} of corresponding 169 | * events. 170 | */ 171 | private class ProcessVoice implements Runnable { 172 | 173 | @Override 174 | public void run() { 175 | while (true) { 176 | synchronized (mLock) { 177 | if (Thread.currentThread().isInterrupted()) { 178 | break; 179 | } 180 | if(mAudioRecord == null) 181 | continue; 182 | if(mBuffer == null) 183 | continue; 184 | final int size = mAudioRecord.read(mBuffer, 0, mBuffer.length); 185 | final long now = System.currentTimeMillis(); 186 | if (mLastVoiceHeardMillis == Long.MAX_VALUE) { 187 | mVoiceStartedMillis = now; 188 | mCallback.onVoiceStart(); 189 | } 190 | mCallback.onVoice(mBuffer, size); 191 | mLastVoiceHeardMillis = now; 192 | if (now - mVoiceStartedMillis > MAX_SPEECH_LENGTH_MILLIS) { 193 | end(); 194 | } 195 | 196 | } 197 | } 198 | } 199 | 200 | private void end() { 201 | mLastVoiceHeardMillis = Long.MAX_VALUE; 202 | mCallback.onVoiceEnd(); 203 | } 204 | 205 | } 206 | 207 | } 208 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 17 | 22 | 27 | 32 | 37 | 42 | 47 | 52 | 57 | 62 | 67 | 72 | 77 | 82 | 87 | 92 | 97 | 102 | 107 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 22 | 23 | 34 | 35 |