Android FFmpeg系列——0 编译.so库
Android FFmpeg系列——1 播放视频
Android FFmpeg系列——2 播放音频
Android FFmpeg系列——3 C多线程使用
Android FFmpeg系列——4 子线程播放音视频
Android FFmpeg系列——5 音视频同步播放
Android FFmpeg系列——6 Java 获取播放进度
Android FFmpeg系列——7 实现快进/快退功能
虽然听了很多音频,但其实对音频知之甚少,所以很有必要了解一下音频。
Audio,指人耳可以听到的声音频率在20Hz~20kHz之间的声波,称为音频。
播放音频之前,我们得先了解音频是怎么保存的。保存音频,其实也就是录音和制作。
模拟时代是把原始信号以物理方式录制到磁带上(当然在录音棚里完成了),然后加工、剪接、修改,最后录制到磁带、LP等广大听众可以欣赏的载体上。这一系列过程全是模拟的,每一步都会损失一些信号,到了听众手里自然是差了好远,更不用说什么HI-FI(高保真)了。
数码时代是第一步就把原始信号录成数码音频资料,然后用硬件设备或各种软件进行加工处理,这个过程与模拟方法相比有无比的优越性,因为它几乎不会有任何损耗。对于机器来说这个过程只是处理一下数字而已,当然丢码的可能性也有,但只要操作合理就不会发生。最后把这堆数字信号传输给数字记录设备如CD等,损耗自然小很多了。
数码音频是我们保存声音信号,传输声音信号的一种方式,它的特点是信号不容易损失。而模拟信号是我们最后可以听到的东西。
接下来,我们要了解2个概念:采样率和比特率。
我们知道所有的声音都有其波形,在原有的模拟信号波形上每隔一段时间进行一次“取点”,赋予每一个点以一个数值,这就是“采样”,然后把所有的“点”连起来就可以描述模拟信号了。很明显,在一定时间内取的点越多,描述出来的波形就越精确,这个尺度我们就称为“采样率”。
我们最常用的采样频率是44.1kHz,它的意思是每秒取样44100次。
我们知道声音有轻有响,影响声音响度的物理要素是振幅,作为数码录音,必须也要能精确表示乐曲的轻响,所以一定要对波形的振幅有一个精确的描述。“比特(bit)”就是这样一个单位,16比特就是指把波形的振幅划为2^16即65536个等级,根据模拟信号的轻响把它划分到某个等级中去,就可以用数字来表示了。和采样频率一样,比特率越高,越能细致地反映乐曲的轻响变化。
以上简介均来自 音频_百度百科
我们使用ffmpeg解码音频的时候,往往需要改变原音频的采样率,即需要重采样。
比如一音乐文件的采样率22050,而播放端往往是固定的采样率,比如44100。在这种情况下,如果把解码出来的数据直接播放,会产生快进的效果。这个时候就需要对解码出来的数据作一次重采样,将数据转化为44100采样率下的数据,才能正确播放。
/**
1. 播放音频流
2. R# 代表申请内存 需要释放或关闭
*/
extern "C"
JNIEXPORT void JNICALL
Java_com_johan_player_Player_playAudio(JNIEnv *env, jobject instance, jstring path_) {
// 记录结果
int result;
// R1 Java String -> C String
const char *path = env->GetStringUTFChars(path_, 0);
// 注册组件
av_register_all();
// R2 创建 AVFormatContext 上下文
AVFormatContext *format_context = avformat_alloc_context();
// R3 打开视频文件
avformat_open_input(&format_context, path, NULL, NULL);
// 查找视频文件的流信息
result = avformat_find_stream_info(format_context, NULL);
if (result < 0) {
LOGE("Player Error : Can not find video file stream info");
return;
}
// 查找音频编码器
int audio_stream_index = -1;
for (int i = 0; i < format_context->nb_streams; i++) {
// 匹配音频流
if (format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
audio_stream_index = i;
}
}
// 没找到音频流
if (audio_stream_index == -1) {
LOGE("Player Error : Can not find audio stream");
return;
}
// 初始化音频编码器上下文
AVCodecContext *audio_codec_context = avcodec_alloc_context3(NULL);
avcodec_parameters_to_context(audio_codec_context, format_context->streams[audio_stream_index]->codecpar);
// 初始化音频编码器
AVCodec *audio_codec = avcodec_find_decoder(audio_codec_context->codec_id);
if (audio_codec == NULL) {
LOGE("Player Error : Can not find audio codec");
return;
}
// R4 打开视频解码器
result = avcodec_open2(audio_codec_context, audio_codec, NULL);
if (result < 0) {
LOGE("Player Error : Can not open audio codec");
return;
}
// 音频重采样准备
// R5 重采样上下文
struct SwrContext *swr_context = swr_alloc();
// 缓冲区
uint8_t *out_buffer = (uint8_t *) av_malloc(44100 * 2);
// 输出的声道布局 (双通道 立体音)
uint64_t out_channel_layout = AV_CH_LAYOUT_STEREO;
// 输出采样位数 16位
enum AVSampleFormat out_format = AV_SAMPLE_FMT_S16;
// 输出的采样率必须与输入相同
int out_sample_rate = audio_codec_context->sample_rate;
//swr_alloc_set_opts 将PCM源文件的采样格式转换为自己希望的采样格式
swr_alloc_set_opts(swr_context,
out_channel_layout, out_format, out_sample_rate,
audio_codec_context->channel_layout, audio_codec_context->sample_fmt, audio_codec_context->sample_rate,
0, NULL);
swr_init(swr_context);
// 调用 Java 层创建 AudioTrack
int out_channels = av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO);
jclass player_class = env->GetObjectClass(instance);
jmethodID create_audio_track_method_id = env->GetMethodID(player_class, "createAudioTrack", "(II)V");
env->CallVoidMethod(instance, create_audio_track_method_id, 44100, out_channels);
// 播放音频准备
jmethodID play_audio_track_method_id = env->GetMethodID(player_class, "playAudioTrack", "([BI)V");
// 声明数据容器 有2个
// R6 解码前数据容器 Packet 编码数据
AVPacket *packet = av_packet_alloc();
// R7 解码后数据容器 Frame MPC数据 还不能直接播放 还要进行重采样
AVFrame *frame = av_frame_alloc();
// 开始读取帧
while (av_read_frame(format_context, packet) >= 0) {
// 匹配音频流
if (packet->stream_index == audio_stream_index) {
// 解码
result = avcodec_send_packet(audio_codec_context, packet);
if (result < 0 && result != AVERROR(EAGAIN) && result != AVERROR_EOF) {
LOGE("Player Error : codec step 1 fail");
return;
}
result = avcodec_receive_frame(audio_codec_context, frame);
if (result < 0 && result != AVERROR_EOF) {
LOGE("Player Error : codec step 2 fail");
return;
}
// 重采样
swr_convert(swr_context, &out_buffer, 44100 * 2, (const uint8_t **) frame->data, frame->nb_samples);
// 播放音频
// 调用 Java 层播放 AudioTrack
int size = av_samples_get_buffer_size(NULL, out_channels, frame->nb_samples, AV_SAMPLE_FMT_S16, 1);
jbyteArray audio_sample_array = env->NewByteArray(size);
env->SetByteArrayRegion(audio_sample_array, 0, size, (const jbyte *) out_buffer);
env->CallVoidMethod(instance, play_audio_track_method_id, audio_sample_array, size);
env->DeleteLocalRef(audio_sample_array);
}
// 释放 packet 引用
av_packet_unref(packet);
}
// 调用 Java 层释放 AudioTrack
jmethodID release_audio_track_method_id = env->GetMethodID(player_class, "releaseAudioTrack", "()V");
env->CallVoidMethod(instance, release_audio_track_method_id);
// 释放 R7
av_frame_free(&frame);
// 释放 R6
av_packet_free(&packet);
// 释放 R5
swr_free(&swr_context);
// 关闭 R4
avcodec_close(audio_codec_context);
// 关闭 R3
avformat_close_input(&format_context);
// 释放 R2
avformat_free_context(format_context);
// 释放 R1
env->ReleaseStringUTFChars(path_, path);
}
其实和播放视频比较相似,流程:
在代码中,C层会反射调用Java层代码,这里稍微做一下笔记:
// 获取 instant 实例的 Class
jclass player_class = env->GetObjectClass(instance);
// 获取 Java 方法 ID
// 参数1:class,也就是实例的 Class
// 参数2:Java 方法名名称
// 参数3:Java 方法签名 格式是(参数类型)返回类型
jmethodID create_audio_track_method_id = env->GetMethodID(player_class, "createAudioTrack", "(II)V");
// 调用 Java方法 我这里调用的是Void返回值(也就是没有返回值)的方法
// 参数1:实例
// 参数2:Java 方法 ID
// 参数3:不定参数,也就是方法的参数
env->CallVoidMethod(instance, create_audio_track_method_id, 44100, out_channels);
对于参数类型/返回类型做一个记录:
Java类型 | 符号 |
---|---|
Boolean | Z |
Byte | B |
Char | C |
Short | S |
Int | I |
Long | L |
Float | F |
Double | D |
Void | V |
Object对象 | 以 “L” 开头,以 “;” 为结尾,中间是用 “/” 隔开的包及类名,如 Ljava/lang/String; 嵌套类用$隔开,和Java一样 |
数组 | 前面加 “[”,如 [I 表示 int [] |
代码中还涉及到 Native 数组转 Java 数组:
jbyteArray audio_sample_array = env->NewByteArray(size);
env->SetByteArrayRegion(audio_sample_array, 0, size, (const jbyte *) out_buffer);
看不懂的可以参考这篇博文 Android开发实践:Java层与Jni层的数组传递
布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Play Audio"
android:onClick="playAudio"
/>
LinearLayout>
Player Native 代码:
/**
* Created by johan on 2018/10/16.
*/
public class Player {
private AudioTrack audioTrack;
static {
System.loadLibrary("player");
}
public native void playAudio(String path);
/**
* 创建 AudioTrack
* 由 C 反射调用
* @param sampleRate 采样率
* @param channels 通道数
*/
public void createAudioTrack(int sampleRate, int channels) {
int channelConfig;
if (channels == 1) {
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
} else if (channels == 2) {
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
}else {
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
}
int bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT);
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig,
AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM);
audioTrack.play();
}
/**
* 播放 AudioTrack
* 由 C 反射调用
* @param data
* @param length
*/
public void playAudioTrack(byte[] data, int length) {
if (audioTrack != null && audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
audioTrack.write(data, 0, length);
}
}
/**
* 释放 AudioTrack
* 由 C 反射调用
*/
public void releaseAudioTrack() {
if (audioTrack != null) {
if (audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
audioTrack.stop();
}
audioTrack.release();
audioTrack = null;
}
}
}
Activity 代码:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void playAudio(View view) {
String videoPath = Environment.getExternalStorageDirectory() + "/mv.mp4";
Player player = new Player();
player.playAudio(videoPath);
}
}
能正常听到视频的播放的声音,但是程序会出现 ANR,大家大概都能猜到为什么了吧!!没错,就是在主线程进行耗时操作,这里耗时操作就是播放音频。
下一节我将会学习怎么在子线程(C子线程)播放音频。
Android使用FFmpeg(四)–ffmpeg实现音频播放(使用AudioTrack进行播放)
ffmpeg解码音频数据时,进行重采样(即改变文件原有的采样率)
ffmepg音频重采样
Android开发实践:Java层与Jni层的数组传递