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);}

其实和播放视频比较相似,流程:

  1. 注册组件
  2. 打开视频文件
  3. 查找视频文件的流信息
  4. 查找音频编码器并打开
  5. 播放音频准备
  6. 循环读取帧
  7. 解码
  8. 重采样
  9. 播放重采样音频数据
  10. 释放

在代码中,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层的数组传递

更多相关文章

  1. Android麦克风录音的实现
  2. Android多媒体开发高级编程——为智能手机和平板电脑开发图形、
  3. android 高仿多米音乐播放器 (有图有码有真相)
  4. Android(安卓)实现左右声道播放不同音乐
  5. Android(安卓)2.3.4 RTSP的实现不在StageFright中,在opencore中
  6. Android音频系统之AudioFlinger(四)
  7. Android(安卓)VideoView本地视频播放
  8. android传感器学习之采样率和属性
  9. android webview自动播放Video

随机推荐

  1. Android(安卓)聊天软件实现
  2. Android动画效果生动有趣的通知NiftyNoti
  3. Android深入四大组件(一)应用程序启动过程
  4. Android(安卓)Studio安装教程(超详细教程)
  5. Android动画效果生动有趣的通知NiftyNoti
  6. android屏幕监视工具 android screen mon
  7. Android内核开发:开发板选购
  8. [Android] 基于 Linux 命令行构建 Androi
  9. Android(安卓)SDK版本和ADT版本
  10. Android(安卓)4层框架