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

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

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

在代码中,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类型符号
BooleanZ
ByteB
CharC
ShortS
IntI
LongL
FloatF
DoubleD
VoidV
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层的数组传递

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值