Android 录音机功能之读取音档绘制波形,可自订裁剪区间,录音暂停继续 下载地址:源码
一.效果图:
二.实现:
1.简介:
使用多个github项目,实现了以下:
*读取手机上有的音档
*滑动自订 裁剪音档区间
*可录音暂停后继续录音
*录音同时绘制波型图
2.添加依赖:
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion '27.0.3'
defaultConfig {
applicationId "com.mkjihu.audioedit"
minSdkVersion 19
targetSdkVersion 25
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'])
compile 'com.android.support:appcompat-v7:25.+'
compile 'com.android.support:support-v4:25.+'
compile 'com.android.support:recyclerview-v7:25.+'
//--RxJava2
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
compile 'io.reactivex.rxjava2:rxjava:2.1.0'
compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
//--MP3轉檔
compile 'com.github.adrielcafe:AndroidAudioConverter:0.0.8'
compile 'org.florescu.android.rangeseekbar:rangeseekbar-library:0.3.0'
compile 'com.github.Jay-Goo:RangeSeekBar:v1.1.0'
}
设置权限:
<uses-sdk
android:minSdkVersion="19"
android:targetSdkVersion="25" />
<!-- 使用音场效果必要的权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<!-- 使用SD卡 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- 允许程序重新启动其他程序
<uses-permission android:name="android.permission.RESTART_PACKAGES" />
3.主函数:
import java.io.File;
import java.io.FilenameFilter;
import java.io.UnsupportedEncodingException;
import com.mkjihu.audioedit.Presenter.MainPresenter;
import com.mkjihu.audioedit.obj.DialogBox;
import android.Manifest;
import android.app.ProgressDialog;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.provider.MediaStore;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.util.Base64;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageView;
public class MainActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener {
public SwipeRefreshLayout mSwipeRefreshLayout;
public RecyclerView recycler_view;
public ProgressDialog progressDialog;
public MainPresenter presenter;
final private int REQUEST_CODE_ASK_PERMISSIONS = 123;
public ImageView imageView1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
fid();
presenter = new MainPresenter(this);
progressDialog = new ProgressDialog(this);
progressDialog.setCancelable(true);
progressDialog.setInverseBackgroundForced(false);
progressDialog.setCanceledOnTouchOutside(false);
progressDialog.setMessage("掃描中...");
presenter.GetAudio();
try {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
//申请WRITE_EXTERNAL_STORAGE权限
ActivityCompat.requestPermissions(this, new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE
,Manifest.permission.WRITE_EXTERNAL_STORAGE
, Manifest.permission.READ_PHONE_STATE
, Manifest.permission.RECORD_AUDIO
, Manifest.permission.MODIFY_AUDIO_SETTINGS
, Manifest.permission.WRITE_CONTACTS}, REQUEST_CODE_ASK_PERMISSIONS);
}
} catch (Exception e) {
// TODO: handle exception
}
}
//權限同意返回
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_ASK_PERMISSIONS:
if(verifyPermissions(grantResults)){
Log.i("!!!", "同意");
}
else {
Log.i("!!!", "不同意");
DialogBox.getAlertDialog2(this, "提示", "請至設定之應用程式,開啟權限!");
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
public boolean verifyPermissions(int[] grantResults) {
// At least one result must be checked.
if(grantResults.length < 1){
return false;
}
// Verify that each required permission has been granted, otherwise return false.
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
public void tosdg(String aas) {
Intent intent = new Intent(this, SoundFilePage.class);
intent.putExtra("va", aas);
startActivity(intent);
}
private void fid()
{
imageView1 = (ImageView)findViewById(R.id.imageView1);
recycler_view = (RecyclerView)findViewById(R.id.recycler_view);
recycler_view.setLayoutManager(new LinearLayoutManager(this));
//下面这行代码就是添加分隔线的方法
recycler_view.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
mSwipeRefreshLayout = (SwipeRefreshLayout)findViewById(R.id.mSwipeRefreshLayout);
//设置下拉出现小圆圈是否是缩放出现,出现的位置,最大的下拉位置
mSwipeRefreshLayout.setProgressViewOffset(true, 0, 200);
//设置下拉圆圈的大小,两个值 LARGE, DEFAULT
//mSwipeRefreshLayout.setSize(SwipeRefreshLayout.LARGE);
// 设置下拉圆圈上的颜色,蓝色、绿色、橙色、红色
mSwipeRefreshLayout.setColorSchemeResources(
android.R.color.holo_blue_bright,
android.R.color.holo_green_light,
android.R.color.holo_orange_light,
android.R.color.holo_red_light);
mSwipeRefreshLayout.setOnRefreshListener(this);
imageView1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, RecordingPage.class);
startActivity(intent);
}
});
}
public void adasp(RecyclerView.Adapter<ViewHolder> adapter) {
recycler_view.setAdapter(adapter);
}
public void offRefresh() {
mSwipeRefreshLayout.setRefreshing(false);
//iwillPaint.dissdig();
}
public void openRefresh() {
mSwipeRefreshLayout.setRefreshing(true);
}
@Override
public void onRefresh() {
//下拉更新執行
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
presenter.GetAudio();
}
}, 1000);
}
@Override
protected void onDestroy() {
presenter.disposable.dispose();
super.onDestroy();
}
}
4.布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/LinearLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.mkjihu.audioedit.MainActivity" >
<FrameLayout
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:background="@color/colorPrimary" >
<ImageView
android:id="@+id/imageView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|center"
android:layout_marginRight="8dp"
android:src="@drawable/uin" />
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="可讀音頻列表"
android:textColor="#FFFFFF"
android:textSize="16sp" />
</FrameLayout>
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/mSwipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical" >
</android.support.v7.widget.RecyclerView>
</android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout>
5.录音相关功能RecordingPage.java:
import java.nio.ByteBuffer;
import com.androidquery.AQuery;
import com.cokus.wavelibrary.draw.WaveCanvas;
import com.cokus.wavelibrary.view.WaveSurfaceView;
import com.mkjihu.audioedit.Presenter.RecordingPresenter;
import com.mkjihu.audioedit.view.Chronometer;
import android.app.ProgressDialog;
import android.app.AlertDialog.Builder;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.net.Uri;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.EditText;
import android.widget.Toast;
public class RecordingPage extends AppCompatActivity {
public AQuery aq;
public WaveSurfaceView waveSfv;
public WaveCanvas waveCanvas;
private ProgressDialog logdialogs;
public RecordingPresenter presenter;
public int RegType = 0;
private String fileName = "MergePCM";//完成合併後的文件名;
private EditText editText;
public Chronometer chronometer1;
public long escapeTime = 0;
//https://github.com/adrielcafe/AndroidAudioConverter
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recording_page);
aq = new AQuery(this);
fid();
presenter = new RecordingPresenter(this,waveSfv);
aq.id(R.id.bt1).clicked(ls);
aq.id(R.id.bt2).clicked(ls);
aq.id(R.id.bt3).clicked(ls);
aq.id(R.id.bt1).text("開始");
RegType = 0;
}
public OnClickListener ls = new OnClickListener() {
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.bt1:
waveSfv.setVisibility(View.VISIBLE);
if (RegType==0) {//按下开始
aq.id(R.id.bt1).text("暫停");
presenter.StartReg();
RegType = 1;
chronometer1.setBase(SystemClock.elapsedRealtime() + escapeTime);
chronometer1.start();
}
else {//按下暫停
aq.id(R.id.bt1).text("開始");//-呼叫暫停
presenter.Stop(0);//暫停
RegType = 0;
escapeTime = chronometer1.getBase() - SystemClock.elapsedRealtime();
chronometer1.stop();
}
break;
case R.id.bt2:
aq.id(R.id.bt1).text("開始");
presenter.Stop(0);//暫停
presenter.clearFiles();
presenter.clidview();
RegType = 0;
escapeTime = 0;
chronometer1.stop();
chronometer1.setBase(SystemClock.elapsedRealtime());
break;
case R.id.bt3:
if (!editText.getText().toString().equals("")) {
fileName = editText.getText().toString();
}
presenter.steTitle(fileName);
if (RegType==1) {//播放狀態
//presenter.Stop(1);
Toast.makeText(RecordingPage.this, "請先暫停錄音", Toast.LENGTH_SHORT).show();
}
else{//已暫停狀態
presenter.Stop(2);
}
break;
}
}
};
public void toSidie(final String Path)
{
Builder builder = new Builder(this);
builder.setTitle("訊號轉換完成");
builder.setCancelable(false);
builder.setMessage("檔案路徑:"+Path);
//設定Negative按鈕資料
builder.setNegativeButton("確定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which)
{
Intent intent = new Intent(RecordingPage.this, SoundFilePage.class);
intent.putExtra("va", Path);
startActivity(intent);
finish();
}
});
builder.create().show();
}
@Override
protected void onDestroy() {
try {
presenter.destroy();
} catch (Exception e) { }
super.onDestroy();
}
private void fid() {
waveSfv = (WaveSurfaceView)findViewById(R.id.wavesfv);
if(waveSfv != null) {
waveSfv.setLine_off(42);
//解决surfaceView黑色闪动效果
waveSfv.setZOrderOnTop(true);
waveSfv.getHolder().setFormat(PixelFormat.TRANSLUCENT);
}
logdialogs = new ProgressDialog(this);
logdialogs.setCancelable(false);
logdialogs.setInverseBackgroundForced(false);
logdialogs.setCanceledOnTouchOutside(false);
logdialogs.setMessage("訊號轉換中...");
chronometer1 = (Chronometer)findViewById(R.id.chronometer1);
editText = (EditText)findViewById(R.id.editText1);
}
//--顯示加載
public void showdia() {
if(logdialogs!=null && !logdialogs.isShowing()) {
logdialogs.show();
}
}
//此处关闭加載
public void disdia() {
if(logdialogs!=null && logdialogs.isShowing()) {
logdialogs.dismiss();
}
}
//==================================================================================================================================================
//轉換為短字節
private byte[] short2byte(short[] sData) {
int shortArrsize = sData.length;
byte[] bytes = new byte[shortArrsize * 2];
for (int i = 0; i < shortArrsize; i++) {
bytes[i * 2] = (byte) (sData[i] & 0x00FF);
bytes[(i * 2) + 1] = (byte) (sData[i] >> 8);
sData[i] = 0;
}
return bytes;
}
//byte[] 轉換 short[]
public static short[] shortMe(byte[] bytes) {
short[] out = new short[bytes.length / 2]; // will drop last byte if odd number
ByteBuffer bb = ByteBuffer.wrap(bytes);
for (int i = 0; i < out.length; i++) {
out[i] = bb.getShort();
}
return out;
}
}
6.录音布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/LinearLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.mkjihu.audioedit.RecordingPage" >
<FrameLayout
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:background="@color/colorPrimary" >
<TextView
android:id="@+id/gwg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="錄音"
android:textColor="#FFFFFF"
android:textSize="16sp" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="150dp" >
<com.cokus.wavelibrary.view.WaveSurfaceView
android:id="@+id/wavesfv"
android:layout_width="fill_parent"
android:layout_height="150dp" />
</FrameLayout>
<com.mkjihu.audioedit.view.Chronometer
android:id="@+id/chronometer1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="8dp"
android:text="Chronometer"
android:textSize="20sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<Button
android:id="@+id/bt1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="開始" />
<Button
android:id="@+id/bt2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="重錄" />
<Button
android:id="@+id/bt3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="完成" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp" >
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="檔名:"
android:textSize="20sp" />
<EditText
android:id="@+id/editText1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:hint="未输入将使用预设檔名" />
</LinearLayout>
</LinearLayout>
7.自定义实现音波类:
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
/**
* 该类只是一个初始化surfaceview的封装
*/
public class WaveSurfaceView extends SurfaceView implements SurfaceHolder.Callback{
private SurfaceHolder holder;
private int line_off;//上下边距距离
public int getLine_off() {
return line_off;
}
public void setLine_off(int line_off) {
this.line_off = line_off;
}
public WaveSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
this.holder = getHolder();
holder.addCallback(this);
}
/**
* @author cokus
* init surfaceview
*/
public void initSurfaceView( final SurfaceView sfv){
new Thread(){
public void run() {
Canvas canvas = sfv.getHolder().lockCanvas(
new Rect(0, 0, sfv.getWidth(), sfv.getHeight()));// 关键:获取画布
if(canvas==null){
return;
}
//canvas.drawColor(Color.rgb(241, 241, 241));// 清除背景
canvas.drawARGB(255, 239, 239, 239);
int height = sfv.getHeight()-line_off;
Paint paintLine =new Paint();
Paint centerLine =new Paint();
Paint circlePaint = new Paint();
circlePaint.setColor(Color.rgb(246, 131, 126));
circlePaint.setAntiAlias(true);
canvas.drawCircle(0, line_off/4, line_off/4, circlePaint);// 上面小圆
canvas.drawCircle(0, sfv.getHeight()-line_off/4, line_off/4, circlePaint);// 下面小圆
canvas.drawLine(0, 0, 0, sfv.getHeight(), circlePaint);//垂直的线
paintLine.setColor(Color.rgb(169, 169, 169));
centerLine.setColor(Color.rgb(85, 140, 208));//-音波繪製顏色39, 199, 175
canvas.drawLine(0, line_off/2, sfv.getWidth(), line_off/2, paintLine);//最上面的那根线
canvas.drawLine(0, sfv.getHeight()-line_off/2-1, sfv.getWidth(), sfv.getHeight()-line_off/2-1, paintLine);//最下面的那根线
// canvas.drawLine(0, height*0.25f+20, sfv.getWidth(),height*0.25f+20, paintLine);//第二根线
// canvas.drawLine(0, height*0.75f+20, sfv.getWidth(),height*0.75f+20, paintLine);//第3根线
canvas.drawLine(0, height*0.5f+line_off/2, sfv.getWidth() ,height*0.5f+line_off/2, centerLine);//中心线
sfv.getHolder().unlockCanvasAndPost(canvas);// 解锁画布,提交画好的图像
};
}.start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// TODO Auto-generated method stub
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
// TODO Auto-generated method stub
initSurfaceView(this);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
}
8.自定义时间显示控件:
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.widget.TextView;
import java.text.DecimalFormat;
public class Chronometer extends TextView {
@SuppressWarnings("unused")
private static final String TAG = "Chronometer";
public interface OnChronometerTickListener {
void onChronometerTick(Chronometer chronometer);
}
private long mBase;
private boolean mVisible;
private boolean mStarted;
private boolean mRunning;
private OnChronometerTickListener mOnChronometerTickListener;
private static final int TICK_WHAT = 2;
private long timeElapsed;
public Chronometer(Context context) {
this (context, null, 0);
}
public Chronometer(Context context, AttributeSet attrs) {
this (context, attrs, 0);
}
public Chronometer(Context context, AttributeSet attrs, int defStyle) {
super (context, attrs, defStyle);
init();
}
private void init() {
mBase = SystemClock.elapsedRealtime();
updateText(mBase);
}
public void setBase(long base) {
mBase = base;
dispatchChronometerTick();
updateText(SystemClock.elapsedRealtime());
}
public long getBase() {
return mBase;
}
public void setOnChronometerTickListener(
OnChronometerTickListener listener) {
mOnChronometerTickListener = listener;
}
public OnChronometerTickListener getOnChronometerTickListener() {
return mOnChronometerTickListener;
}
public void start() {
mStarted = true;
updateRunning();
}
public void stop() {
mStarted = false;
updateRunning();
}
public void setStarted(boolean started) {
mStarted = started;
updateRunning();
}
@Override
protected void onDetachedFromWindow() {
super .onDetachedFromWindow();
mVisible = false;
updateRunning();
}
@Override
protected void onWindowVisibilityChanged(int visibility) {
super .onWindowVisibilityChanged(visibility);
mVisible = visibility == VISIBLE;
updateRunning();
}
private synchronized void updateText(long now) {
timeElapsed = now - mBase;
DecimalFormat df = new DecimalFormat("00");
int hours = (int)(timeElapsed / (3600 * 1000));
int remaining = (int)(timeElapsed % (3600 * 1000));
int minutes = (int)(remaining / (60 * 1000));
remaining = (int)(remaining % (60 * 1000));
int seconds = (int)(remaining / 1000);
remaining = (int)(remaining % (1000));
int milliseconds = (int)(((int)timeElapsed % 1000) / 10);
String text = "";
if (hours > 0) {
text += df.format(hours) + ":";
}
text += df.format(minutes) + ":";
text += df.format(seconds) + ":";
//text += Integer.toString(milliseconds);
text += String.format("%02d", milliseconds);
setText(text);
}
private void updateRunning() {
boolean running = mVisible && mStarted;
if (running != mRunning) {
if (running) {
updateText(SystemClock.elapsedRealtime());
dispatchChronometerTick();
mHandler.sendMessageDelayed(Message.obtain(mHandler,TICK_WHAT), 10);
} else {
mHandler.removeMessages(TICK_WHAT);
}
mRunning = running;
}
}
private Handler mHandler = new Handler() {
public void handleMessage(Message m) {
if (mRunning) {
updateText(SystemClock.elapsedRealtime());
dispatchChronometerTick();
sendMessageDelayed(Message.obtain(this , TICK_WHAT),10);
}
}
};
void dispatchChronometerTick() {
if (mOnChronometerTickListener != null) {
mOnChronometerTickListener.onChronometerTick(this);
}
}
public long getTimeElapsed() {
return timeElapsed;
}
}
具体代码请看:https://github.com/mkjihu/AudioWaveViewEdit
参考:
https://github.com/mkjihu/AudioWaveViewEdit
https://github.com/cokuscz/audioWaveCanvas