Android(安卓)FFmpeg系列——2 播放音频
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采样率下的数据,才能正确播放。
C 代码
/** 1. 播放音频流 2. R# 代表申请内存 需要释放或关闭 */extern "C"JNIEXPORT void JNICALLJava_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 实例的 Classjclass 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层的数组传递
Java 代码
布局文件:
<?xml version="1.0" encoding="utf-8"?><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层的数组传递
更多相关文章
- Android麦克风录音的实现
- Android多媒体开发高级编程——为智能手机和平板电脑开发图形、
- android 高仿多米音乐播放器 (有图有码有真相)
- Android(安卓)实现左右声道播放不同音乐
- Android(安卓)2.3.4 RTSP的实现不在StageFright中,在opencore中
- Android音频系统之AudioFlinger(四)
- Android(安卓)VideoView本地视频播放
- android传感器学习之采样率和属性
- android webview自动播放Video