├── .gitattributes ├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml ├── gradle.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── zlw │ │ └── audio_recorder │ │ ├── MainActivity.java │ │ ├── TestHzActivity.java │ │ ├── base │ │ └── MyApp.java │ │ └── widget │ │ └── AudioView.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_hz.xml │ └── activity_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── doc └── demo.jpg ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── recorderlib ├── .gitignore ├── build.gradle ├── libs │ ├── arm64-v8a │ │ └── libmp3lame.so │ ├── armeabi-v7a │ │ └── libmp3lame.so │ ├── armeabi │ │ └── libmp3lame.so │ ├── mips │ │ └── libmp3lame.so │ ├── mips64 │ │ └── libmp3lame.so │ ├── x86 │ │ └── libmp3lame.so │ └── x86_64 │ │ └── libmp3lame.so ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ ├── com │ │ │ └── zlw │ │ │ │ └── main │ │ │ │ └── recorderlib │ │ │ │ ├── RecordManager.java │ │ │ │ ├── recorder │ │ │ │ ├── RecordConfig.java │ │ │ │ ├── RecordHelper.java │ │ │ │ ├── RecordService.java │ │ │ │ ├── listener │ │ │ │ │ ├── RecordDataListener.java │ │ │ │ │ ├── RecordFftDataListener.java │ │ │ │ │ ├── RecordResultListener.java │ │ │ │ │ ├── RecordSoundSizeListener.java │ │ │ │ │ └── RecordStateListener.java │ │ │ │ ├── mp3 │ │ │ │ │ ├── Mp3EncodeThread.java │ │ │ │ │ ├── Mp3Encoder.java │ │ │ │ │ └── Mp3Utils.java │ │ │ │ └── wav │ │ │ │ │ └── WavUtils.java │ │ │ │ └── utils │ │ │ │ ├── ByteUtils.java │ │ │ │ ├── FileUtils.java │ │ │ │ ├── Logger.java │ │ │ │ └── RecordUtils.java │ │ └── fftlib │ │ │ ├── ByteUtils.java │ │ │ ├── Complex.java │ │ │ ├── FFT.java │ │ │ └── FftFactory.java │ ├── jni │ │ ├── Android.mk │ │ ├── Application.mk │ │ ├── Mp3Encoder.c │ │ ├── Mp3Encoder.h │ │ └── lame-3.100_libmp3lame │ │ │ ├── VbrTag.c │ │ │ ├── VbrTag.h │ │ │ ├── bitstream.c │ │ │ ├── bitstream.h │ │ │ ├── encoder.c │ │ │ ├── encoder.h │ │ │ ├── fft.c │ │ │ ├── fft.h │ │ │ ├── gain_analysis.c │ │ │ ├── gain_analysis.h │ │ │ ├── id3tag.c │ │ │ ├── id3tag.h │ │ │ ├── l3side.h │ │ │ ├── lame-analysis.h │ │ │ ├── lame.c │ │ │ ├── lame.h │ │ │ ├── lame_global_flags.h │ │ │ ├── lameerror.h │ │ │ ├── machine.h │ │ │ ├── mpglib_interface.c │ │ │ ├── newmdct.c │ │ │ ├── newmdct.h │ │ │ ├── presets.c │ │ │ ├── psymodel.c │ │ │ ├── psymodel.h │ │ │ ├── quantize.c │ │ │ ├── quantize.h │ │ │ ├── quantize_pvt.c │ │ │ ├── quantize_pvt.h │ │ │ ├── reservoir.c │ │ │ ├── reservoir.h │ │ │ ├── set_get.c │ │ │ ├── set_get.h │ │ │ ├── tables.c │ │ │ ├── tables.h │ │ │ ├── takehiro.c │ │ │ ├── util.c │ │ │ ├── util.h │ │ │ ├── vbrquantize.c │ │ │ ├── vbrquantize.h │ │ │ ├── version.c │ │ │ └── version.h │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── zlw │ └── main │ └── recorderlib │ └── ExampleUnitTest.java └── settings.gradle /.gitattributes: -------------------------------------------------------------------------------- 1 | *.c linguist-language=Java 2 | *.h linguist-language=Java -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | /.idea/ 7 | /.idea/.* 8 | .DS_Store 9 | /build 10 | /captures 11 | .externalNativeBuild 12 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 25 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | 51 | 56 | 57 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZlwAudioRecorder 2 | 3 | ### 功能 4 | 1. 使用AudioRecord进行录音 5 | 2. 实现pcm、wav、mp3音频的录制 6 | 3. 实时获取录音的音量、及录音byte数据 7 | 4. 获取wav/mp3录音文件的时长 8 | 5. 可配置录音的采样率、位宽 (v1.04更新) 9 | 5. 录音可视化 (v1.05更新) 10 | 5. 音源支持内录(Android10及以上版本支持) (v1.09更新) 11 | 12 | ### 博客 13 | https://www.jianshu.com/p/c0222de2faed 14 | 15 | ### Gradle 16 | [![](https://jitpack.io/v/zhaolewei/ZlwAudioRecorder.svg)](https://jitpack.io/#zhaolewei/ZlwAudioRecorder) 17 | 18 | dependencies { 19 | implementation 'com.github.zhaolewei:ZlwAudioRecorder:v1.09' 20 | } 21 | 22 | allprojects { 23 | repositories { 24 | ... 25 | maven { url 'https://www.jitpack.io' } 26 | } 27 | } 28 | ### 如何使用 29 | 30 | 1. 初始化 31 | * init 32 | ```java 33 | /** 34 | * 参数1: Application 实例 35 | * 参数2: 是否打印日志 36 | */ 37 | RecordManager.getInstance().init(MyApp.getInstance(), false); 38 | ``` 39 | * 在清单文件中注册Services 40 | 41 | ```java 42 | 43 | ``` 44 | * 确保有录音权限 45 | 46 | 2. 配置录音参数 47 | 48 | * 修改录音格式(默认:WAV) 49 | ```java 50 | RecordManager.getInstance().changeFormat(RecordConfig.RecordFormat.WAV); 51 | ``` 52 | 53 | * 修改录音配置 54 | ```java 55 | RecordManager.getInstance().changeRecordConfig(recordManager.getRecordConfig().setSampleRate(16000)); 56 | RecordManager.getInstance().changeRecordConfig(recordManager.getRecordConfig().setEncodingConfig(AudioFormat.ENCODING_PCM_8BIT)); 57 | ``` 58 | * 修改录音音源 59 | ```java 60 | RecordManager.getInstance().setSource(RecordConfig.SOURCE_MIC); // 麦克风 61 | RecordManager.getInstance().setSource(RecordConfig.SOURCE_SYSTEM); //系统内录 62 | ``` 63 | * 修改录音文件存放位置(默认sdcard/Record) 64 | ```java 65 | RecordManager.getInstance().changeRecordDir(recordDir); 66 | ``` 67 | * 录音状态监听 68 | ```java 69 | RecordManager.getInstance().setRecordStateListener(new RecordStateListener() { 70 | @Override 71 | public void onStateChange(RecordHelper.RecordState state) { 72 | } 73 | } 74 | 75 | @Override 76 | public void onError(String error) { 77 | } 78 | }); 79 | ``` 80 | * 录音结果监听 81 | ```java 82 | RecordManager.getInstance().setRecordResultListener(new RecordResultListener() { 83 | @Override 84 | public void onResult(File result) { 85 | } 86 | }); 87 | ``` 88 | * 声音大小监听 89 | ```java 90 | RecordManager.getInstance().setRecordSoundSizeListener(new RecordSoundSizeListener() { 91 | @Override 92 | public void onSoundSize(int soundSize) { 93 | } 94 | }); 95 | ``` 96 | * 音频数据监听 97 | ```java 98 | recordManager.setRecordDataListener(new RecordDataListener() { 99 | @Override 100 | public void onData(byte[] data) { 101 | } 102 | }); 103 | ``` 104 | * 音频可视化数据监听 105 | ```java 106 | recordManager.setRecordFftDataListener(new RecordFftDataListener() { 107 | @Override 108 | public void onFftData(byte[] data) { 109 | audioView.setWaveData(data); 110 | } 111 | }); 112 | ``` 113 | 3. 录音控制 114 | * 开始录音 115 | ```java 116 | RecordManager.getInstance().start(); 117 | ``` 118 | * 暂停录音 119 | ```java 120 | RecordManager.getInstance().pasue(); 121 | ``` 122 | * 恢复录音 123 | ```java 124 | RecordManager.getInstance().resume(); 125 | ``` 126 | * 停止 127 | ```java 128 | RecordManager.getInstance().stop(); 129 | ``` 130 | 131 | ### Demo 132 | ![Demo.png](https://raw.githubusercontent.com/zhaolewei/ZlwAudioRecorder/master/doc/demo.jpg) 133 | * 演示视频>>> https://www.bilibili.com/video/av48748708?from=search&seid=7409882966117066343 134 | 135 | 136 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | publishing { 7 | publications { 8 | // 这个mavenJava可以随便填,只是一个任务名字而已 9 | // MavenPublication必须有,这个是调用的任务类 10 | mavenJava(MavenPublication) { 11 | // 这里头是artifacts的配置信息,不填会采用默认的 12 | groupId = 'org.gradle.sample' 13 | artifactId = 'library' 14 | version = '1.1' 15 | } 16 | } 17 | } 18 | 19 | android { 20 | namespace 'com.zlw.audio_recorder' 21 | compileSdk 33 22 | 23 | defaultConfig { 24 | applicationId "com.zlw.audio_recorder" 25 | minSdk 24 26 | versionCode 1 27 | versionName "1.0" 28 | 29 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 30 | vectorDrawables { 31 | useSupportLibrary true 32 | } 33 | } 34 | 35 | buildTypes { 36 | release { 37 | minifyEnabled false 38 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 39 | } 40 | } 41 | compileOptions { 42 | sourceCompatibility JavaVersion.VERSION_1_8 43 | targetCompatibility JavaVersion.VERSION_1_8 44 | } 45 | kotlinOptions { 46 | jvmTarget = '1.8' 47 | } 48 | buildFeatures { 49 | compose true 50 | } 51 | composeOptions { 52 | kotlinCompilerExtensionVersion '1.4.3' 53 | } 54 | packaging { 55 | resources { 56 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 57 | } 58 | } 59 | } 60 | 61 | dependencies { 62 | implementation 'androidx.core:core-ktx:1.9.0' 63 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' 64 | implementation 'androidx.activity:activity-compose:1.7.2' 65 | implementation platform('androidx.compose:compose-bom:2023.03.00') 66 | implementation 'androidx.compose.ui:ui' 67 | implementation 'androidx.compose.ui:ui-graphics' 68 | implementation 'androidx.compose.ui:ui-tooling-preview' 69 | implementation 'androidx.compose.material3:material3' 70 | 71 | implementation 'com.blankj:utilcodex:1.31.1' 72 | implementation 'com.yanzhenjie:permission:2.0.3' 73 | implementation 'com.github.zhaolewei:Logger:1.0.2' 74 | implementation project(path: ':recorderlib') 75 | } -------------------------------------------------------------------------------- /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/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/zlw/audio_recorder/TestHzActivity.java: -------------------------------------------------------------------------------- 1 | package com.zlw.audio_recorder; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.graphics.Color; 5 | import android.os.Bundle; 6 | import android.os.Environment; 7 | import android.view.View; 8 | import android.view.Window; 9 | import android.view.WindowManager; 10 | import android.widget.AdapterView; 11 | import android.widget.ArrayAdapter; 12 | import android.widget.Button; 13 | import android.widget.Spinner; 14 | import android.widget.TextView; 15 | import android.widget.Toast; 16 | 17 | import androidx.activity.ComponentActivity; 18 | 19 | import com.yanzhenjie.permission.AndPermission; 20 | import com.yanzhenjie.permission.runtime.Permission; 21 | import com.zlw.audio_recorder.base.MyApp; 22 | import com.zlw.audio_recorder.widget.AudioView; 23 | import com.zlw.loggerlib.Logger; 24 | import com.zlw.main.recorderlib.RecordManager; 25 | import com.zlw.main.recorderlib.recorder.RecordConfig; 26 | import com.zlw.main.recorderlib.recorder.RecordHelper; 27 | import com.zlw.main.recorderlib.recorder.listener.RecordFftDataListener; 28 | import com.zlw.main.recorderlib.recorder.listener.RecordResultListener; 29 | import com.zlw.main.recorderlib.recorder.listener.RecordStateListener; 30 | 31 | import java.io.File; 32 | import java.util.Locale; 33 | 34 | 35 | public class TestHzActivity extends ComponentActivity implements AdapterView.OnItemSelectedListener, View.OnClickListener { 36 | private static final String TAG = TestHzActivity.class.getSimpleName(); 37 | 38 | Button btRecord; 39 | Button btStop; 40 | TextView tvState; 41 | AudioView audioView; 42 | Spinner spUpStyle; 43 | Spinner spDownStyle; 44 | 45 | private boolean isStart = false; 46 | private boolean isPause = false; 47 | final RecordManager recordManager = RecordManager.getInstance(); 48 | private static final String[] STYLE_DATA = new String[]{"STYLE_ALL", "STYLE_NOTHING", "STYLE_WAVE", "STYLE_HOLLOW_LUMP"}; 49 | 50 | 51 | @Override 52 | protected void onCreate(Bundle savedInstanceState) { 53 | super.onCreate(savedInstanceState); 54 | requestWindowFeature(Window.FEATURE_NO_TITLE); 55 | this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); 56 | getWindow().setStatusBarColor(Color.TRANSPARENT); 57 | setContentView(R.layout.activity_hz); 58 | initView(); 59 | initPermission(); 60 | initAudioView(); 61 | } 62 | 63 | private void initView() { 64 | btRecord = findViewById(R.id.btRecord); 65 | btStop = findViewById(R.id.btStop); 66 | tvState = findViewById(R.id.tvState); 67 | audioView = findViewById(R.id.audioView); 68 | spUpStyle = findViewById(R.id.spUpStyle); 69 | spDownStyle = findViewById(R.id.spDownStyle); 70 | btRecord.setOnClickListener(this); 71 | btStop.setOnClickListener(this); 72 | } 73 | 74 | @Override 75 | protected void onResume() { 76 | super.onResume(); 77 | initRecord(); 78 | } 79 | 80 | @Override 81 | protected void onStop() { 82 | super.onStop(); 83 | recordManager.stop(); 84 | } 85 | 86 | private void initAudioView() { 87 | audioView.setStyle(AudioView.ShowStyle.STYLE_ALL, AudioView.ShowStyle.STYLE_ALL); 88 | tvState.setVisibility(View.GONE); 89 | 90 | ArrayAdapter adapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item, STYLE_DATA); 91 | adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 92 | spUpStyle.setAdapter(adapter); 93 | spDownStyle.setAdapter(adapter); 94 | spUpStyle.setOnItemSelectedListener(this); 95 | spDownStyle.setOnItemSelectedListener(this); 96 | } 97 | 98 | 99 | private void initPermission() { 100 | AndPermission.with(this) 101 | .runtime() 102 | .permission(new String[]{Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE, 103 | Permission.RECORD_AUDIO}) 104 | .start(); 105 | } 106 | 107 | private void initRecord() { 108 | recordManager.init(MyApp.getInstance(), true); 109 | recordManager.changeFormat(RecordConfig.RecordFormat.WAV); 110 | String recordDir = String.format(Locale.getDefault(), "%s/Record/com.zlw.main/", 111 | Environment.getExternalStorageDirectory().getAbsolutePath()); 112 | recordManager.changeRecordDir(recordDir); 113 | 114 | recordManager.setRecordStateListener(new RecordStateListener() { 115 | @Override 116 | public void onStateChange(RecordHelper.RecordState state) { 117 | Logger.i(TAG, "onStateChange %s", state.name()); 118 | 119 | switch (state) { 120 | case PAUSE: 121 | tvState.setText("暂停中"); 122 | break; 123 | case IDLE: 124 | tvState.setText("空闲中"); 125 | break; 126 | case RECORDING: 127 | tvState.setText("录音中"); 128 | break; 129 | case STOP: 130 | tvState.setText("停止"); 131 | break; 132 | case FINISH: 133 | tvState.setText("录音结束"); 134 | break; 135 | default: 136 | break; 137 | } 138 | } 139 | 140 | @Override 141 | public void onError(String error) { 142 | Logger.i(TAG, "onError %s", error); 143 | } 144 | }); 145 | recordManager.setRecordResultListener(new RecordResultListener() { 146 | @Override 147 | public void onResult(File result) { 148 | Toast.makeText(TestHzActivity.this, "录音文件: " + result.getAbsolutePath(), Toast.LENGTH_SHORT).show(); 149 | } 150 | }); 151 | recordManager.setRecordFftDataListener(new RecordFftDataListener() { 152 | @Override 153 | public void onFftData(byte[] data) { 154 | byte[] newdata = new byte[data.length - 36]; 155 | for (int i = 0; i < newdata.length; i++) { 156 | newdata[i] = data[i + 36]; 157 | } 158 | audioView.setWaveData(data); 159 | } 160 | }); 161 | } 162 | 163 | @Override 164 | public void onClick(View view) { 165 | if (view.getId() == R.id.btRecord) { 166 | if (isStart) { 167 | recordManager.pause(); 168 | btRecord.setText("开始"); 169 | isPause = true; 170 | isStart = false; 171 | } else { 172 | if (isPause) { 173 | recordManager.resume(); 174 | } else { 175 | recordManager.start(); 176 | } 177 | btRecord.setText("暂停"); 178 | isStart = true; 179 | } 180 | } else if (view.getId() == R.id.btStop) { 181 | recordManager.stop(); 182 | btRecord.setText("开始"); 183 | isPause = false; 184 | isStart = false; 185 | } 186 | } 187 | 188 | @SuppressLint("NonConstantResourceId") 189 | @Override 190 | public void onItemSelected(AdapterView parent, View view, int position, long id) { 191 | int parentId = parent.getId(); 192 | if (parentId == R.id.spUpStyle) { 193 | audioView.setStyle(AudioView.ShowStyle.getStyle(STYLE_DATA[position]), audioView.getDownStyle()); 194 | } else if (parentId == R.id.spDownStyle) { 195 | audioView.setStyle(audioView.getUpStyle(), AudioView.ShowStyle.getStyle(STYLE_DATA[position])); 196 | } 197 | } 198 | 199 | @Override 200 | public void onNothingSelected(AdapterView parent) { 201 | 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /app/src/main/java/com/zlw/audio_recorder/base/MyApp.java: -------------------------------------------------------------------------------- 1 | package com.zlw.audio_recorder.base; 2 | 3 | import android.app.Application; 4 | 5 | /** 6 | * @author zlw on 2018/7/4. 7 | */ 8 | public class MyApp extends Application { 9 | 10 | private static MyApp instance; 11 | 12 | @Override 13 | public void onCreate() { 14 | super.onCreate(); 15 | instance = this; 16 | } 17 | 18 | public static MyApp getInstance() { 19 | return instance; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/zlw/audio_recorder/widget/AudioView.java: -------------------------------------------------------------------------------- 1 | package com.zlw.audio_recorder.widget; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.Paint; 7 | import android.graphics.Path; 8 | import android.graphics.Point; 9 | import android.util.AttributeSet; 10 | import android.view.View; 11 | 12 | import androidx.annotation.Nullable; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | /** 18 | * @author zhaolewei on 2018/8/17. 19 | */ 20 | public class AudioView extends View { 21 | 22 | /** 23 | * 频谱数量 24 | */ 25 | private static final int LUMP_COUNT = 128 * 2; 26 | private static final int LUMP_WIDTH = 6; 27 | private static final int LUMP_SPACE = 2; 28 | private static final int LUMP_MIN_HEIGHT = LUMP_WIDTH; 29 | private static final int LUMP_MAX_HEIGHT = 200;//TODO: HEIGHT 30 | private static final int LUMP_SIZE = LUMP_WIDTH + LUMP_SPACE; 31 | private static final int LUMP_COLOR = Color.parseColor("#6de8fd"); 32 | 33 | private static final int WAVE_SAMPLING_INTERVAL = 5; 34 | 35 | private static final float SCALE = LUMP_MAX_HEIGHT / 128; 36 | 37 | private ShowStyle upShowStyle = ShowStyle.STYLE_HOLLOW_LUMP; 38 | private ShowStyle downShowStyle = ShowStyle.STYLE_WAVE; 39 | 40 | private byte[] waveData; 41 | List pointList; 42 | 43 | private Paint lumpUpPaint, lumpDownPaint; 44 | Path wavePathUp = new Path(); 45 | Path wavePathDown = new Path(); 46 | 47 | 48 | public AudioView(Context context) { 49 | super(context); 50 | init(); 51 | } 52 | 53 | public AudioView(Context context, @Nullable AttributeSet attrs) { 54 | super(context, attrs); 55 | init(); 56 | } 57 | 58 | public AudioView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 59 | super(context, attrs, defStyleAttr); 60 | init(); 61 | } 62 | 63 | private void init() { 64 | lumpUpPaint = new Paint(); 65 | lumpUpPaint.setAntiAlias(true); 66 | lumpUpPaint.setColor(LUMP_COLOR); 67 | lumpUpPaint.setStrokeWidth(3); 68 | lumpUpPaint.setStyle(Paint.Style.FILL); 69 | 70 | lumpDownPaint = new Paint(); 71 | lumpDownPaint.setAntiAlias(true); 72 | lumpDownPaint.setColor(LUMP_COLOR); 73 | lumpDownPaint.setStrokeWidth(3); 74 | lumpDownPaint.setStyle(Paint.Style.STROKE); 75 | } 76 | 77 | public void setWaveData(byte[] data) { 78 | this.waveData = readyData(data); 79 | genSamplingPoint(data); 80 | invalidate(); 81 | } 82 | 83 | public void setStyle(ShowStyle upShowStyle, ShowStyle downShowStyle) { 84 | this.upShowStyle = upShowStyle; 85 | this.downShowStyle = downShowStyle; 86 | if (upShowStyle == ShowStyle.STYLE_HOLLOW_LUMP || upShowStyle == ShowStyle.STYLE_ALL) { 87 | lumpUpPaint.setColor(Color.parseColor("#A4D3EE")); 88 | } 89 | if (downShowStyle == ShowStyle.STYLE_HOLLOW_LUMP || downShowStyle == ShowStyle.STYLE_ALL) { 90 | lumpDownPaint.setColor(Color.parseColor("#A4D3EE")); 91 | } 92 | } 93 | 94 | public ShowStyle getUpStyle() { 95 | return upShowStyle; 96 | } 97 | 98 | public ShowStyle getDownStyle() { 99 | return downShowStyle; 100 | } 101 | 102 | @Override 103 | protected void onDraw(Canvas canvas) { 104 | super.onDraw(canvas); 105 | wavePathUp.reset(); 106 | wavePathDown.reset(); 107 | 108 | for (int i = 0; i < LUMP_COUNT; i++) { 109 | if (waveData == null) { 110 | canvas.drawRect((LUMP_WIDTH + LUMP_SPACE) * i, 111 | LUMP_MAX_HEIGHT - LUMP_MIN_HEIGHT, 112 | (LUMP_WIDTH + LUMP_SPACE) * i + LUMP_WIDTH, 113 | LUMP_MAX_HEIGHT, 114 | lumpUpPaint); 115 | continue; 116 | } 117 | 118 | if (upShowStyle != null) { 119 | switch (upShowStyle) { 120 | case STYLE_HOLLOW_LUMP: 121 | drawLump(canvas, i, true); 122 | break; 123 | case STYLE_WAVE: 124 | drawWave(canvas, i, true); 125 | break; 126 | case STYLE_ALL: 127 | drawLump(canvas, i, true); 128 | drawWave(canvas, i, true); 129 | default: 130 | break; 131 | } 132 | } 133 | if (downShowStyle != null) { 134 | switch (downShowStyle) { 135 | case STYLE_HOLLOW_LUMP: 136 | drawLump(canvas, i, false); 137 | break; 138 | case STYLE_WAVE: 139 | drawWave(canvas, i, false); 140 | break; 141 | case STYLE_ALL: 142 | drawLump(canvas, i, false); 143 | drawWave(canvas, i, false); 144 | default: 145 | break; 146 | } 147 | } 148 | } 149 | } 150 | 151 | /** 152 | * 预处理数据 153 | * 154 | * @return 155 | */ 156 | private static byte[] readyData(byte[] fft) { 157 | byte[] newData = new byte[LUMP_COUNT]; 158 | for (int i = 0; i < Math.min(fft.length, LUMP_COUNT); i++) { 159 | newData[i] = (byte) Math.abs(fft[i]); 160 | } 161 | return newData; 162 | } 163 | 164 | /** 165 | * 绘制曲线 166 | * 167 | * @param canvas 168 | * @param i 169 | * @param reversal 170 | */ 171 | private void drawWave(Canvas canvas, int i, boolean reversal) { 172 | if (pointList == null || pointList.size() < 2) { 173 | return; 174 | } 175 | float ratio = SCALE * (reversal ? 1 : -1); 176 | if (i < pointList.size() - 2) { 177 | Point point = pointList.get(i); 178 | Point nextPoint = pointList.get(i + 1); 179 | int midX = (point.x + nextPoint.x) >> 1; 180 | if (reversal) { 181 | if (i == 0) { 182 | wavePathUp.moveTo(point.x, LUMP_MAX_HEIGHT - point.y * ratio); 183 | } 184 | wavePathUp.cubicTo(midX, LUMP_MAX_HEIGHT - point.y * ratio, 185 | midX, LUMP_MAX_HEIGHT - nextPoint.y * ratio, 186 | nextPoint.x, LUMP_MAX_HEIGHT - nextPoint.y * ratio); 187 | canvas.drawPath(wavePathUp, lumpDownPaint); 188 | } else { 189 | if (i == 0) { 190 | wavePathDown.moveTo(point.x, LUMP_MAX_HEIGHT - point.y * ratio); 191 | } 192 | wavePathDown.cubicTo(midX, LUMP_MAX_HEIGHT - point.y * ratio, 193 | midX, LUMP_MAX_HEIGHT - nextPoint.y * ratio, 194 | nextPoint.x, LUMP_MAX_HEIGHT - nextPoint.y * ratio); 195 | canvas.drawPath(wavePathDown, lumpDownPaint); 196 | } 197 | 198 | } 199 | } 200 | 201 | /** 202 | * 绘制矩形条 203 | * reversal: true: 上 204 | */ 205 | private void drawLump(Canvas canvas, int i, boolean reversal) { 206 | int minus = reversal ? 1 : -1; 207 | float top; 208 | 209 | if ((reversal && upShowStyle == ShowStyle.STYLE_ALL) || (!reversal && downShowStyle == ShowStyle.STYLE_ALL)) { 210 | top = (LUMP_MAX_HEIGHT - (LUMP_MIN_HEIGHT + waveData[i] / 4 * SCALE) * minus); 211 | } else { 212 | top = (LUMP_MAX_HEIGHT - (LUMP_MIN_HEIGHT + waveData[i] * SCALE) * minus); 213 | } 214 | canvas.drawRect(LUMP_SIZE * i, 215 | top, 216 | LUMP_SIZE * i + LUMP_WIDTH, 217 | LUMP_MAX_HEIGHT, 218 | lumpUpPaint); 219 | 220 | } 221 | 222 | /** 223 | * 生成波形图的采样数据,减少计算量 224 | * 225 | * @param data 226 | */ 227 | private void genSamplingPoint(byte[] data) { 228 | if (upShowStyle != ShowStyle.STYLE_WAVE && downShowStyle != ShowStyle.STYLE_WAVE && upShowStyle != ShowStyle.STYLE_ALL && downShowStyle != ShowStyle.STYLE_ALL) { 229 | return; 230 | } 231 | if (pointList == null) { 232 | pointList = new ArrayList<>(); 233 | } else { 234 | pointList.clear(); 235 | } 236 | pointList.add(new Point(0, 0)); 237 | for (int i = WAVE_SAMPLING_INTERVAL; i < LUMP_COUNT; i += WAVE_SAMPLING_INTERVAL) { 238 | pointList.add(new Point(LUMP_SIZE * i, waveData[i])); 239 | } 240 | pointList.add(new Point(LUMP_SIZE * LUMP_COUNT, 0)); 241 | } 242 | 243 | 244 | /** 245 | * 可视化样式 246 | */ 247 | public enum ShowStyle { 248 | /** 249 | * 空心的矩形小块 250 | */ 251 | STYLE_HOLLOW_LUMP, 252 | 253 | /** 254 | * 曲线 255 | */ 256 | STYLE_WAVE, 257 | 258 | /** 259 | * 不显示 260 | */ 261 | STYLE_NOTHING, 262 | /** 263 | * 都显示 264 | */ 265 | STYLE_ALL; 266 | 267 | public static ShowStyle getStyle(String name) { 268 | for (ShowStyle style : ShowStyle.values()) { 269 | if (style.name().equals(name)) { 270 | return style; 271 | } 272 | } 273 | 274 | return STYLE_NOTHING; 275 | } 276 | } 277 | 278 | } 279 | 280 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_hz.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 15 | 16 | 24 | 25 | 26 | 30 | 31 | 35 | 36 | 41 | 42 | 47 | 48 | 49 |