音视频(一)AudioTrack播放PCM裸数据
PCM介绍: PCM是英文Pulse-code modulation的缩写,中文译名是脉冲编码调制。我们知道在现实生活中,人耳听到的声音是模拟信号,PCM就是要把声音从模拟转换成数字信号的一种技术,它的原理简单地说就是利用一个固定的频率对模拟信号进行采样,采样后的信号在波形上看就像一串连续的幅值不一的脉冲,把这些脉冲的幅值按一定的精度进行量化,这些量化后的数值被连续地输出、传输、处理或记录到存储介质中,所有这些组成了数字音频的产生过程。
1.首先准备好一段PCM音频
可通过链接下载一段pcm数据
2.AudioTrack实例
AudioTrack
为音频播放(PCM数据)提供可用服务
示例代码:
AudioTrack audioTrack = new AudioTrack.Builder()
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build())
.setAudioFormat(new AudioFormat.Builder().setSampleRate(8000).setChannelMask(AudioFormat.CHANNEL_IN_STEREO).setEncoding(AudioFormat.ENCODING_PCM_16BIT).build())
.setTransferMode(AudioTrack.MODE_STREAM)
.setBufferSizeInBytes(bufferSize)
.build();
参数介绍:
AudioAttribtes
->Usage
:设置描述音频信号预期用途的属性,如闹钟或铃声等(USAGE_MEDIA音乐或电影);AudioAttribtes
->ContentType
:设置描述音频信号内容类型的属性,例如语音或音乐。(CONTENT_TYPE_MUSIC音乐类型);AudioFormat
->SampleRate
:音频采样率,16000hz为16k等AudioFormat
->ChannelMask
:声道设置,例如 AudioFormat.CHANNEL_IN_STEREO双声道(注意:当使用双声道的时候,采样率应该/2,不让音频可能会倍速播放,不是正常播放速度)AudioFormat
->Encoding
:设置数据编码格式,ENCODING_PCM_16BIT(兼容所有手机,基本都用这个16位的)、ENCODING_PCM_8BIT(8位)AudioTrack
->BufferSizeInBytes
:设置缓存区子节大小,最好调用静态方法AudioTrack.getMinBufferSize(采样率,声道,位数) 获取buffersizeAudioTrack
->TransferMode
:设置缓冲区数据传输模式。- 值得注意的是有两种模式:
AudioTrack.MODE_STREAM
此模式设置完毕后AudioTrack的write()
方法前面必须先调用play()
方法。边读边播,不会将数据直接加载到内存AudioTrack.MODE_STATIC
设置完毕后可以先write
最后在play()
播放。预先将需要播放的音频数据读取到内存中,然后才开始播放
write
:write()方法将文件流byte读入至AudioTrack中传入native层处理
3.播放代码
以AudioTrack.MODE_STREAM
模式播放pcm因为是边读边播所以开线程
//pcm文件是16K的采样率 我设置双声道 16000/2 = 8000
int bufferSize = AudioTrack.getMinBufferSize(8000, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);
new Thread(new Runnable() {
@Override
public void run() {
try {
//获取文件输入流 我这里存放在assets中
InputStream dis = getAssets().open("16k16bit.pcm");
audioTrack.play();
int length = 0;
byte a[] = new byte[bufferSize];
while ((length = dis.read(a)) != -1) {
//read读取的数据放入a缓冲区,将a缓冲区数据写入到audiotrack中
audioTrack.write(a, 0, length);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
audioTrack.stop();
audioTrack.release();
}
}
}).start();
虽然可以播放了,如何拿到
时间
呢?比如当前进度
和播放总时长
?
4.计算播放进度以及总时长
AudioTrack
类中是没有任何时间
的方法的,但是有几个函数通过这几个函数是可以大致计算出当前播放进度和音频总时长的.
播放进度:
//返回以帧表示的播放头位置
int play = audioTrack.getPlaybackHeadPosition();
//返回当前采样率
int rate = audioTrack.getPlaybackRate();
float currentPlayTime = playFarme * 1.0f / rate * 1.0f;
⚠️ 帧中的声音文件的长度。(通过将“帧数”(Frame Count)除以“采样率”(Sample Rate)来计算。) 也就是
播放进度 = 当前帧位置 / 当前采样率
PCM音频时长
pcmTimeCount = (int) ((fileSize * 1.0f) / (16.0f / 8.0f) / (2.0f) / 8000);
文件大小计算公式:
文件大小Byte=采样率×(采样位数/8)×声道数×总时间
当前已知量:文件大小、设置的采样率、采样位数16bit、声道数(双声道也就是2),直接可以将总时间
求出来
监听实时进度
setPlaybackPositionUpdateListener
有两个回调函数onMarkerReached
和onPeriodicNotification
分别对应 在侦听器上调用以通知它已到达先前设置的标记 和 调用侦听器定期通知它播放头已到达
//先设置间隔时长 意味着每xxMS会调用一次onPeriodicNotification和onMarkerReached
audioTrack.setPositionNotificationPeriod(1000);
audioTrack.setPlaybackPositionUpdateListener(new AudioTrack.OnPlaybackPositionUpdateListener() {
@Override
public void onMarkerReached(AudioTrack track) {
}
@Override
public void onPeriodicNotification(AudioTrack track) {
int playFarme = audioTrack.getPlaybackHeadPosition();
int rate = audioTrack.getPlaybackRate();
float currentPlayTime = playFarme * 1.0f / rate * 1.0f;
}
}, mMainHandler);
整体MainActivity代码:
private ActivityMainBinding binding;
private Handler mMainHandler;
private ProgressBar progressbar;
private int pcmTimeCount;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
progressbar = binding.progressbar;
mMainHandler = new Handler(Looper.myLooper());
AudioTrack audioTrack;
int bufferSize = AudioTrack.getMinBufferSize(8000, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
audioTrack = new AudioTrack.Builder()
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
.build())
.setAudioFormat(new AudioFormat.Builder().setSampleRate(8000).setChannelMask(AudioFormat.CHANNEL_IN_STEREO).setEncoding(AudioFormat.ENCODING_PCM_16BIT).build())
.setTransferMode(AudioTrack.MODE_STREAM)
.setBufferSizeInBytes(bufferSize)
.build();
} else {
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, 8000, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT,
bufferSize, AudioTrack.MODE_STREAM);
}
audioTrack.setPlaybackPositionUpdateListener(new AudioTrack.OnPlaybackPositionUpdateListener() {
@Override
public void onMarkerReached(AudioTrack track) {
}
@Override
public void onPeriodicNotification(AudioTrack track) {
int playFarme = audioTrack.getPlaybackHeadPosition();
int rate = audioTrack.getPlaybackRate();
float currentPlayTime = playFarme * 1.0f / rate * 1.0f;
progressbar.setProgress((int) MathUtils.clamp(Math.ceil(((currentPlayTime / pcmTimeCount) * 100)),0,100));
}
}, mMainHandler);
audioTrack.setPositionNotificationPeriod(1000);
if (audioTrack.getState() != AudioTrack.STATE_UNINITIALIZED && audioTrack.getPlayState() != PLAYSTATE_PLAYING) {
new Thread(new Runnable() {
@Override
public void run() {
try {
InputStream dis = getAssets().open("16k16bit.pcm");
audioTrack.play();
int length = 0;
byte a[] = new byte[bufferSize];
if (dis.available() > 0) {
int fileSize = dis.available();
/*
根据计算公式:
数据量Byte=
44100Hz
×(16/8)×2
×10s=1764KByte然后转化为相应的单位
*/
mMainHandler.post(new Runnable() {
@Override
public void run() {
if (fileSize > 0) {
pcmTimeCount = (int) ((fileSize * 1.0f) / (16.0f / 8.0f) / (2.0f) / 8000);
}
}
});
}
while ((length = dis.read(a)) != -1) {
audioTrack.write(a, 0, length);
}
mMainHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "播放结束", Toast.LENGTH_SHORT).show();
}
});
} catch (Exception e) {
e.printStackTrace();
} finally {
audioTrack.stop();
audioTrack.release();
}
}
}).start();
}
}
有不对的地方,欢迎指正及时更改。