【音频采集】
你可以使用手机进行现场录音,实现步骤如下:
第一步:在功能清单文件AndroidManifest.xml中添加音频刻录权限:
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
第二步:编写音频刻录代码:
MediaRecorder recorder = new MediaRecorder();
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);//从麦克风采集声音
recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);//内容输出格式
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);//音频编码方式
recorder.setOutputFile("/mnt/sdcard/glsite.amr");
recorder.prepare();//预期准备
recorder.start(); //开始刻录
...
recorder.stop();//停止刻录
recorder.reset(); //重设
recorder.release(); //刻录完成一定要释放资源
在Android开发中我们经常使用MediaPlayer来播放音频文件,但是MediaPlayer存在一些不足,例如:资源占用量较高、延迟时间较长、不支持多个音频同时播放等。这些缺点决定了MediaPlayer在某些场合的使用情况不会很理想,例如在对时间精准度要求相对较高的游戏开发中。
在游戏开发中我们经常需要播放一些游戏音效(比如:子弹爆炸,物体撞击等),这些音效的共同特点是短促、密集、延迟程度小。在这样的场景下,我们可以使用SoundPool代替MediaPlayer来播放这些音效。
SoundPool(android.media.SoundPool),顾名思义是声音池的意思,主要用于播放一些较短的声音片段,支持从程序的资源或文件系统加载。与MediaPlayer相比,SoundPool的优势在于CPU资源占用量低和反应延迟小。另外,SoundPool还支持自行设置声音的品质、音量、播放比率等参数,支持通过ID对多个音频流进行管理。
就现在已知的资料来说,SoundPool有一些设计上的BUG,从固件版本1.0开始有些还没有修复,我们在使用中应该小心再小心。相信将来Google会修复这些问题,但我们最好还是列出来:
1. SoundPool最大只能申请1M的内存空间,这就意味着我们只能用一些很短的声音片段,而不是用它来播放歌曲或者做游戏背景音乐。
2. SoundPool提供了pause和stop方法,但这些方法建议最好不要轻易使用,因为有些时候它们可能会使你的程序莫名其妙的终止。建议使用这两个方法的时候尽可能多做测试工作,还有些朋友反映它们不会立即中止播放声音,而是把缓冲区里的数据播放完才会停下来,也许会多播放一秒钟。
3. SoundPool的效率问题。其实SoundPool的效率在这些播放类中算是很好的了,但是有的朋友在G1中测试它还是有100ms左右的延迟,这可能会影响用户体验。也许这不能怪SoundPool本身,因为到了性能比较好的Droid中这个延迟就可以让人接受了。
在现阶段SoundPool有这些缺陷,但也有着它不可替代的优点,基于这些我们建议大在如下情况中多使用SoundPool:1.应用程序中的声效(按键提示音,消息等)2.游戏中密集而短暂的声音(如多个飞船同时爆炸)
【SoundPool】
开发步骤:
1> 往项目的res/raw目录中放入音效文件。
2> 新建SoundPool对象,然后调用SoundPool.load()加载音效,调用SoundPool.play()方法播放指定音效文件。
public class AudioActivity extends Activity {
private SoundPool pool;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//指定声音池的最大音频流数目为10,声音品质为5
pool = new SoundPool(10, AudioManager.STREAM_SYSTEM, 5);
final int sourceid = pool.load(this, R.raw.pj, 0);//载入音频流,返回在池中的id
Button button = (Button)this.findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
//播放音频,第二个参数为左声道音量;第三个参数为右声道音量;第四个参数为优先级;第五个参数为循环次数,0不循环,-1循环;第六个参数为速率,速率最低0.5最高为2,1代表正常速度
pool.play(sourceid, 1, 1, 0, -1, 1);
}
});
}
}
1. AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.glsite.musicplayer">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<service
android:name=".MusicPlayerService"
android:enabled="true"
android:exported="true"></service>
<activity android:name=".SettingActivity" />
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
2. MusicPlayer\app\src\main\res\menu\activity_main.xml 菜单
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/item_setting"
android:title="设置界面"/>
<item android:id="@+id/item_exit"
android:title="关闭播放器"/>
</menu>
3.MusicPlayer\app\src\main\res\layout
3.1activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ListView
android:id="@+id/lv"
android:layout_width="match_parent"
android:layout_height="match_parent"></ListView>
</android.support.constraint.ConstraintLayout>
3.2 activity_setting.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SettingActivity"
android:orientation="vertical">
<RadioGroup
android:id="@+id/rg_mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<RadioButton
android:id="@+id/rb_cycle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="单曲循环" />
<RadioButton
android:id="@+id/rb_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="播放下一曲" />
<RadioButton
android:id="@+id/rb_stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="播放后停止" />
</RadioGroup>
</LinearLayout>
3.3 item_music.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_item_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginTop="5dp"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:text="歌曲名称"
android:textColor="#99ff0000"
android:textSize="20sp" />
<ImageView
android:id="@+id/imageView"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="5dp"
app:srcCompat="@mipmap/ic_launcher" />
</RelativeLayout>
4.MusicPlayer\app\src\main\java\com\glsite\musicplayer
4.1BaseActivity 运行时权限
package com.glsite.musicplayer;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.widget.Toast;
/**
* @author Admin
* @version $Rev$
* @des ${TODO}
* @updateAuthor $Author$
* @updateDes ${TODO}
*/
public class BaseActivity extends AppCompatActivity {
//**************** Android M Permission (Android 6.0权限控制代码封装)
private int permissionRequestCode = 88;
private PermissionCallback permissionRunnable;
public interface PermissionCallback {
void hasPermission();
void noPermission();
}
/**
* Android M运行时权限请求封装
*
* @param permissionDes 权限描述
* @param runnable 请求权限回调
* @param permissions 请求的权限(数组类型),直接从Manifest中读取相应的值,比如Manifest.permission.WRITE_CONTACTS
*/
public void performCodeWithPermission(@NonNull String permissionDes, PermissionCallback runnable, @NonNull String... permissions) {
if (permissions == null || permissions.length == 0)
return;
// this.permissionrequestCode = requestCode;
this.permissionRunnable = runnable;
if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.M) || checkPermissionGranted(permissions)) {
if (permissionRunnable != null) {
permissionRunnable.hasPermission();
permissionRunnable = null;
}
} else {
//permission has not been granted.
requestPermission(permissionDes, permissionRequestCode, permissions);
}
}
private boolean checkPermissionGranted(String[] permissions) {
boolean flag = true;
for (String p : permissions) {
if (ActivityCompat.checkSelfPermission(this, p) != PackageManager.PERMISSION_GRANTED) {
flag = false;
break;
}
}
return flag;
}
private void requestPermission(String permissionDes, final int requestCode, final String[] permissions) {
if (shouldShowRequestPermissionRationale(permissions)) {
/*1. 第一次请求权限时,用户拒绝了,下一次:shouldShowRequestPermissionRationale() 返回 true,应该显示一些为什么需要这个权限的说明
2.第二次请求权限时,用户拒绝了,并选择了“不在提醒”的选项时:shouldShowRequestPermissionRationale() 返回 false
3. 设备的策略禁止当前应用获取这个权限的授权:shouldShowRequestPermissionRationale() 返回 false*/
// Provide an additional rationale to the user if the permission was not granted
// and the user would benefit from additional context for the use of the permission.
// For example, if the request has been denied previously.
// Snackbar.make(getWindow().getDecorView(), requestName,
// Snackbar.LENGTH_INDEFINITE)
// .setAction(R.string.common_ok, new View.OnClickListener() {
// @Override
// public void onClick(View view) {
// ActivityCompat.requestPermissions(BaseAppCompatActivity.this,
// permissions,
// requestCode);
// }
// })
// .show();
//如果用户之前拒绝过此权限,再提示一次准备授权相关权限
new AlertDialog.Builder(this)
.setTitle("提示")
.setMessage(permissionDes)
.setPositiveButton("授权", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ActivityCompat.requestPermissions(BaseActivity.this, permissions, requestCode);
}
}).show();
} else {
// Contact permissions have not been granted yet. Request them directly.
ActivityCompat.requestPermissions(BaseActivity.this, permissions, requestCode);
}
}
private boolean shouldShowRequestPermissionRationale(String[] permissions) {
boolean flag = false;
for (String p : permissions) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, p)) {
flag = true;
break;
}
}
return flag;
}
/**
* Callback received when a permissions request has been completed.
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode == permissionRequestCode) {
if (verifyPermissions(grantResults)) {
if (permissionRunnable != null) {
permissionRunnable.hasPermission();
permissionRunnable = null;
}
} else {
Toast.makeText(this, "暂无权限执行相关操作!", Toast.LENGTH_SHORT).show();
if (permissionRunnable != null) {
permissionRunnable.noPermission();
permissionRunnable = null;
}
}
} else {
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;
}
//********************** END Android M Permission ****************************************
}
4.2MainActivity.java
package com.glsite.musicplayer;
import android.Manifest;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Environment;
import android.os.IBinder;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
import java.util.ArrayList;
public class MainActivity extends BaseActivity {
private ListView mLv;
private IMusicService mIMusicService;
public static final String MP3DIR = Environment.getExternalStorageDirectory() + "/Download/";
private ArrayList<String> mMp3List;
private MyConn mConn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mLv = findViewById(R.id.lv);
performCodeWithPermission("读取SD卡需要用到的权限", new PermissionCallback() {
@Override
public void hasPermission() {
initPlayList();
}
@Override
public void noPermission() {
}
}, Manifest.permission.READ_EXTERNAL_STORAGE);
Intent intent = new Intent(this, MusicPlayerService.class);
startService(intent);
mConn = new MyConn();
bindService(intent, mConn, BIND_AUTO_CREATE);
mLv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (mIMusicService != null) {
mIMusicService.callPlay(mMp3List, position);
} else {
Toast.makeText(MainActivity.this,"还没有绑定服务吧", Toast.LENGTH_SHORT).show();
}
}
});
}
private class MyConn implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mIMusicService = (IMusicService) service;
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
}
/**
* 初始化播放列表
*/
private void initPlayList() {
File file = new File(MP3DIR);
File[] files = file.listFiles();
mMp3List = new ArrayList<>();
for (File f : files) {
if (f.getName().endsWith(".mp3")) {
mMp3List.add(f.getAbsolutePath());
System.out.println(f.getAbsolutePath());
}
}
mLv.setAdapter(new MusicListAdapter());
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = new MenuInflater(this);
inflater.inflate(R.menu.activity_main, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.item_setting) {
// 启动一个设置界面
Intent intent = new Intent(this, SettingActivity.class);
startActivity(intent);
} else if (item.getItemId() == R.id.item_exit){
// 停止播放并且退出
playerExit();
}
return super.onOptionsItemSelected(item);
}
/**
* 停止播放并退出
*/
private void playerExit() {
if (mIMusicService != null) {
mIMusicService.callStop();
}
if (mConn != null) {
unbindService(mConn);
mConn = null;
}
Intent intent = new Intent(this, MusicPlayerService.class);
stopService(intent);
finish();
}
@Override
protected void onDestroy() {
playerExit();
super.onDestroy();
}
private class MusicListAdapter extends BaseAdapter {
@Override
public int getCount() {
return mMp3List.size();
}
@Override
public Object getItem(int position) {
return mMp3List.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = View.inflate(MainActivity.this, R.layout.item_music, null);
TextView tv_name = (TextView) view.findViewById(R.id.tv_item_name);
String path = mMp3List.get(position);
tv_name.setText(path.substring(path.lastIndexOf("/") + 1));
return view;
}
}
}
4.3IMusicService.java
package com.glsite.musicplayer;
import java.util.List;
/**
* @author glsite.com
* @version $Rev$
* @des ${TODO}
* @updateAuthor $Author$
* @updateDes ${TODO}
*/
public interface IMusicService {
/**
* 调用服务里面的播放逻辑
*
* @param playList
* 音乐资源的路径集合
* @param position
*/
public void callPlay(List<String> playList, int position);
/**
* 调用停止播放的方法
*/
public void callStop();
}
4.4MusicPlayerService 音乐服务
package com.glsite.musicplayer;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Binder;
import android.os.IBinder;
import java.io.IOException;
import java.util.List;
public class MusicPlayerService extends Service {
public static final int MUSIC_STOP = 0;
public static final int MUSIC_PLAYING = 1;
public static final int MUSIC_PAUSE = 2;
private SharedPreferences mSp;
private MediaPlayer mMediaPlayer;
public static int playingStatus = 0;
public MusicPlayerService() {
}
@Override
public IBinder onBind(Intent intent) {
return new MyBinder();
}
@Override
public void onCreate() {
System.out.println("音乐播放器服务开启了");
mSp = getSharedPreferences("config", MODE_PRIVATE);
mMediaPlayer = new MediaPlayer();
super.onCreate();
}
@Override
public void onDestroy() {
System.out.println("音乐播放服务关闭了");
super.onDestroy();
}
private class MyBinder extends Binder implements IMusicService {
@Override
public void callPlay(List<String> playList, int position) {
play(playList, position);
}
@Override
public void callStop() {
stopPlayer();
}
}
/**
* 停止播放
*/
private void stopPlayer() {
if (mMediaPlayer != null) {
mMediaPlayer.stop();
mMediaPlayer.release();
mMediaPlayer = null;
}
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.cancelAll();
}
/**
* 播放音乐
*
* @param playList
* 所有音频列表
* @param position
* 当前的位置
*/
private void play(final List<String> playList, final int position) {
try {
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.stop();
}
mMediaPlayer.reset();
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
System.out.println(playList.get(position));
mMediaPlayer.setDataSource(playList.get(position));
mMediaPlayer.prepare();
mMediaPlayer.start();
String path = playList.get(position);
System.out.println(path.substring(path.lastIndexOf("/") + 1));
showNotification(path.substring(path.lastIndexOf("/") + 1));
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
int mode = mSp.getInt("mode", 0);
if (mode == SettingActivity.CYCLE) {
play(playList, position);
} else if (mode == SettingActivity.NEXT){
// 播放下一曲
int newPosition = position + 1;
if (newPosition >= playList.size()) {
newPosition = 0;
}
play(playList, newPosition);
} else if (mode == SettingActivity.STOP) {
// 设置状态为停止
playingStatus = MUSIC_STOP;
}
}
});
} catch (Exception e) {
e.printStackTrace();
playingStatus = MUSIC_STOP;
}
}
/**
* 显示播放音乐的通知提醒
*
* @param filename
* 音乐名
*/
private void showNotification(String filename) {
// 判断如果当前的SDK版本>=8.0
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
String id = "channel_1";
int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel channel = new NotificationChannel(id, "123", importance);
Notification noti = new Notification.Builder(this, id).setContentTitle("酷狗音乐正在播放")
.setContentText(filename)
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.build();
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.createNotificationChannel(channel);
nm.notify(1, noti);// 添加channel
} else {// 如果当前的SDK版本<8.0
Notification noti = new Notification.Builder(this).setContentTitle("酷狗音乐正在播放")
.setContentText(filename)
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.build();
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.notify(1, noti);
}
}
}
4.5SettingActivity.java
package com.glsite.musicplayer;
import android.content.SharedPreferences;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.RadioButton;
import android.widget.RadioGroup;
public class SettingActivity extends AppCompatActivity {
public static final int CYCLE = 1;
public static final int NEXT = 2;
public static final int STOP = 3;
private RadioGroup mRgMode;
private SharedPreferences mSp;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_setting);
mRgMode = findViewById(R.id.rg_mode);
mSp = getSharedPreferences("config", MODE_PRIVATE);
int mode = mSp.getInt("mode", 0);
RadioButton rb;
switch (mode) {
case CYCLE:
rb = findViewById(R.id.rb_cycle);
rb.setChecked(true);
break;
case NEXT:
rb = findViewById(R.id.rb_next);
rb.setChecked(true);
break;
case STOP:
rb = findViewById(R.id.rb_stop);
rb.setChecked(true);
break;
default:
break;
}
mRgMode.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
SharedPreferences.Editor editor = mSp.edit();
switch (checkedId) {
case R.id.rb_cycle:
editor.putInt("mode", CYCLE);
break;
case R.id.rb_next:
editor.putInt("mode", NEXT);
break;
case R.id.rb_stop:
editor.putInt("mode", STOP);
break;
default:
break;
}
editor.commit();
}
});
}
}