关于声音采集和声音处理的一些建议

2022-05-25 14:52:08 浏览数 (1)

上篇文章介绍了VideoEditor开发中需要用到的三方库,本文我们继续回到相机录制的主题上。相机录制的过程除了采集画面,还有采集音频数据的过程,我们今天就主要介绍一下声音采集的过程以及采集的声音是怎么处理的。

相机预览的上面可以“选择音乐”,如果选择音乐了,在真正进行录制的时候就会有两种音频源,一路来自AudioRecord采集到的环境声,另一路是播放的音乐文件,最终你要将两种音频进行混音处理,变成一种声音,添加到最终录制而成的视频文件中。

声音采集

声音采集是系统提供的接口采集环境声,AudioRecord就是Android平台上提供的采集声音的系统API。在采集声音之前,需要设置声音的采样率和声道数,通常情况下采样率是44100Hz,声道数是2。

代码语言:javascript复制
private final static int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
private final static int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
mBufferSize = android.media.AudioRecord.getMinBufferSize(
    sampleRate, channelCount == 1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO,
    AUDIO_FORMAT
);
mAudioRecord = new android.media.AudioRecord(
    AUDIO_SOURCE, sampleRate,
    channelCount == 1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO,
    AUDIO_FORMAT, mBufferSize
);

然后调用一个线程,线程内部用AudioRecord收集声音。采用回来的声音是short数组。这是原始的数据,会送到native层进行重采样(如果需要的话),和音乐解码出来的原始数据进行混音处理,混音之后,开始编码成AVPacket放入Audio Packet Queue,和视频统一封装的时候会用到。

代码语言:javascript复制
short[] buffer = new short[mBufferSize / 2];
while (mRecording) {
    int sampleSize = mAudioRecord.read(buffer, 0, buffer.length);
    if (mOnAudioRecordListener != null && sampleSize > 0) {
        mOnAudioRecordListener.onAudioRecord(buffer, sampleSize);
    }
}

音乐文件处理

选中的音乐文件,首先要解封装,解码成原始的数据,查看其原始的采样率和声道,看看是否需要重采样,录制的过程中还需要将音乐文件播放出来。ffmpeg音视频框架基本支持这一套流程。最后的播放渲染使用的是OpenSL ES框架,也可以使用AudioTrack,不过核心代码都在native层,OpenSL ES方便一点。

代码语言:javascript复制
/// 1.申请AVFormatContext
format_context_ = avformat_alloc_context();
if (format_context_ == nullptr) {
  /// Error
  return;
}
/// 2.打开音频文件
int ret = avformat_open_input(&format_context_, path, nullptr, nullptr);
if (ret != 0) {
  /// Error, 打开文件失败
  return;
}
/// 3.解析音频中stream信息
ret = avformat_find_stream_info(format_context_, nullptr);
if (ret < 0) {
  /// Error, 解析留信息失败
  return;
}
/// 4.获取音频轨道的索引值
stream_index_ = av_find_best_stream(format_context_, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
if (stream_index_ == AVERROR_STREAM_NOT_FOUND) {
  /// Error, 不存在音频轨道
  return;
}
/// 5.获取音频流
AVStream *stream = format_context_->streams[stream_index_];
/// 6.查询是否存在对应的音频解码器
AVCodec *codec = avcodec_find_decoder(stream->codecpar->codec_id);
if (codec == nullptr) {
  /// Error, 找不到对应的音频解码器
  return;
}
/// 7.分配AVCodecContext, 后续解码用到
codec_context_ = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(codec_context_, stream->codecpar);
/// 8.打开解码器
ret = avcodec_open2(codec_context_, codec, nullptr);

上面是从一个文件路径到打开解码器的一系列过程,解码器上下文CodecContext已经打开了,接下来我们开始解码。这儿要说明一点的是,解码的过程实际上是生产原始数据的过程,有生产必定有消费,消费要么是编码,要么是播放,一进一出、按序输出的过程,用到了队列来处理整个流程。这个队列是有大小限制的,存放的是解码(重采样)之后的音频数据。启动一个线程开始解码,解码重采样之后将数据放入Audio Frame Queue中,直到队列满了,条件锁开始wait,另外一个消费线程在解码线程启动的时候也会启动,开始从Audio Frame Queue中取Frame数据开始播放或者编码,当从队列中取出数据时,队列就不满了,会放开条件锁,就这样下去,直到音频文件完整读取解码完成。下面只是伪代码,仅供参考

代码语言:javascript复制
while (true) {
  /// 如果读到结束了, 先等等, codec_context中还有残余的packet要清空
  if (end_of_stream_) {
    pthread_mutex_lock(&end_of_stream_mutex_);
    pthread_cond_wait(&end_of_stream_cond_, &end_of_stream_mutex_);
    pthread_mutex_unlock(&end_of_stream_mutex_);
  }
  int ret = av_read_frame(format_context_, &packet);
  if (ret >= 0) {
    /// 判断当前是否是音频
    if (packet.stream_index == stream_index_) {
      /// 送入到codec_context 中排队开始解码
      ret = avcodec_send_packet(codec_context_, &packet);
      if (ret == 0) {
        /// 送入解码器成功
        /// 如果队列已满, wait锁住
        if (buffer_queue_->Size() >= MUSIC_BUFFER_MAX_SIZE) {
          pthread_mutex_lock(&mutex_);
          pthread_cond_wait(&cond_, &mutex_);
          pthread_mutex_unlock(&mutex_);
        }
        /// 从codec_context 获取解码后的AVFrame
        ret = avcodec_receive_frame(codec_context_, frame);
        /// 下面开始重采样
        }
      }
    }
  }
}

ffmpeg中有两个重要的变量:AVPacket表示未解码之后的数据,AVFrame表示解码之后的数据,可以存储音频或者视频。avcodec_send_packet函数将AVPacket送入codec_context中排队等着解码,avcodec_receive_frame从codec_context依次取出解码好的AVFrame,解码出来的数据是原始数据,但是还没有结束,需要重采样。为什么要重采样?我们知道声音有两个重要的属性:sample_rate(采样率)和channel(采样频道),声音的标准化就通过这两个决定,当我们编码和播放解码出来的音频数据时,就需要将声音的两个属性标准化一下,使得处理之后的音频能够正常的编码或者播放,重采样就是重塑sample_rate和channel的过程。

代码语言:javascript复制
/// 1. 申请重采样上下文
swr_context_ = swr_alloc();

auto src_channel_layout = av_get_default_channel_layout(codec_context_->channels);
AVSampleFormat dec_sample_format = AV_SAMPLE_FMT_S16;
auto dec_channel_layout = dest_channel_ == 1 ? AV_CH_LAYOUT_MONO : AV_CH_LAYOUT_STEREO;
swr_context_ = swr_alloc_set_opts(nullptr, dec_channel_layout, dec_sample_format, dest_sample_rate_,
                                  src_channel_layout, (AVSampleFormat) format,
                                  codec_context_->sample_rate, 0, nullptr);
/// 2.初始化重采样上下文, 设置好对应的属性
int ret = swr_init(swr_context_);
/// 3.重采样
memset(resample_audio_buffer_, 0, AUDIO_BUFFER_LENGTH);
int out_nb_samples = swr_convert(swr_context_, &resample_audio_buffer_, sample_size,
                                 (const uint8_t **) frame->data, frame->nb_samples);
/// 4.获取重采样之后的buffer_size
int line_size = 0;
auto buffer_size = av_samples_get_buffer_size(&line_size, dest_channel_, out_nb_samples,
                                              AV_SAMPLE_FMT_S16, 1);

假如原始的音乐采样率是48000Hz(channel == 2),重采样之后它变成44100Hz(channel == 2)。上面完整介绍了解封、解码、重采样的流程。接下来必须有地方消费它。

OpenSL ES播放音频

解码之后的原始音频数据,需要播放渲染出来,Android上可以选择AudioTrack和OpenSL ES,我们这里使用的是OpenSL ES,Android引入OpenSL ES需要在CMakeLists.txt中引入特定的库——OpenSLES。在native层使用的还需要引入头文件。

代码语言:javascript复制
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>


SLEngineItf engine_;
SLObjectItf engine_object_;
/// 输出到喇叭
SLObjectItf output_mix_object_;
SLObjectItf player_object_;
SLAndroidSimpleBufferQueueItf buffer_queue_;
SLPlayItf play_;
代码语言:javascript复制
SLEngineOption engineOptions[] = {{(SLuint32) SL_ENGINEOPTION_THREADSAFE, (SLuint32) SL_BOOLEAN_TRUE}};

/// 1.创建 OpenSL ES engine 对象
SLresult result = slCreateEngine(&engine_object_, ARRAY_LEN(engineOptions), engineOptions, 0,
                                 nullptr,
                                 nullptr);
RESULT_CHECK(result)
/// 2.关掉异步
result = (*engine_object_)->Realize(engine_object_, SL_BOOLEAN_FALSE);
RESULT_CHECK(result)
/// 3.获取OpenSL ES engine
result = (*engine_object_)->GetInterface(engine_object_, SL_IID_ENGINE, &engine_);
RESULT_CHECK(result)
/// 4.创建声音混合器,
result = (*engine_)->CreateOutputMix(engine_, &output_mix_object_, 0, nullptr, nullptr);
RESULT_CHECK(result)
result = (*output_mix_object_)->Realize(output_mix_object_, SL_BOOLEAN_FALSE);
RESULT_CHECK(result)
SLDataLocator_AndroidSimpleBufferQueue data_source_locator = {
  SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
  1
};
SLDataFormat_PCM data_source_format = {
  SL_DATAFORMAT_PCM,
  (SLuint32) channels,
  (SLuint32) OpenSLSampleRate(sample_rate),
  SL_PCMSAMPLEFORMAT_FIXED_16,
  SL_PCMSAMPLEFORMAT_FIXED_16,
  (SLuint32) GetChannelMask(channels),
  SL_BYTEORDER_LITTLEENDIAN
};
SLDataSource data_source = {
  &data_source_locator, &data_source_format
};
SLDataLocator_OutputMix data_sink_locator = {
  SL_DATALOCATOR_OUTPUTMIX,
  output_mix_object_
};
SLDataSink data_sink = {
  &data_sink_locator, nullptr
};
SLInterfaceID interface_ids[] = {
  SL_IID_BUFFERQUEUE, SL_IID_MUTESOLO, SL_IID_VOLUME
};
SLboolean requiredInterfaces[] = {
  SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE
};
/// 5.创建音频播放器
result = (*engine_)->CreateAudioPlayer(engine_, &player_object_, &data_source,
                                       &data_sink, ARRAY_LEN(interface_ids), interface_ids, requiredInterfaces);
RESULT_CHECK(result)
result = (*player_object_)->Realize(player_object_, SL_BOOLEAN_FALSE);
RESULT_CHECK(result)
/// 6.设置buffer queue
result = (*player_object_)->GetInterface(player_object_, SL_IID_BUFFERQUEUE, &buffer_queue_);
RESULT_CHECK(result)
/// 7.监听buffer queue回调
result = (*buffer_queue_)->RegisterCallback(buffer_queue_, PlayerCallback, this);
RESULT_CHECK(result)
result = (*player_object_)->GetInterface(player_object_, SL_IID_PLAY, &play_);
RESULT_CHECK(result)
代码语言:javascript复制
/// 1.开始播放
auto result = (*play_)->SetPlayState(play_, SL_PLAYSTATE_PLAYING);
if (play_status_ == SL_PLAYSTATE_PLAYING) {
  uint8_t *buffer = nullptr;
  int buffer_size = 0;
  int ret = callback_(&buffer, &buffer_size, context_);
  if (ret == 0 && buffer_size > 0 && buffer != nullptr) {
    (*buffer_queue_)->Enqueue(buffer_queue_, buffer, buffer_size);
    (*play_)->GetPosition(play_, &position_);
  } else {
    LOGE("%s ret: %d buffer_size: %d", __func__, ret, buffer_size);
  }
}

/// 2.暂停播放
auto result = (*play_)->SetPlayState(play_, SL_PLAYSTATE_PAUSED);

/// 3.停止播放, 需要OpenSLES相关实例
auto result = (*play_)->SetPlayState(play_, SL_PLAYSTATE_STOPPED);

播放器过程不断从buffer queue中取出解码和重采样之后的数据,声音的播放会在一个单独的线程中,取出一帧音频数据,会计算出其pts,和即将渲染的视频的pts对比,做好音视频同步机制。

编码

编码也是消费解码出来的音频数据的另一种方式,编码是解码的逆向过程,将AVFrame编码成AVPacket数据,然后和视频流合成一个新的视频。

代码语言:javascript复制
/// 1.初始化音频AVCodecContext
auto codec = avcodec_find_encoder_by_name(codec_name.c_str());
if (codec == nullptr) {
  LOGE("%s avcodec_find_encoder_by_name error: %s", __func__, codec_name.c_str());
  return Error::FIND_FFMPEG_AUDIO_ENCODE_ERROR;
}
codec_context_ = avcodec_alloc_context3(codec);
codec_context_->codec_type = AVMEDIA_TYPE_AUDIO;
codec_context_->sample_rate = audio_sample_rate_;
codec_context_->bit_rate = audio_bit_rate_;
codec_context_->sample_fmt = AV_SAMPLE_FMT_S16;
codec_context_->channel_layout = audio_channels_ == 1 ? AV_CH_LAYOUT_MONO : AV_CH_LAYOUT_STEREO;
codec_context_->channels = av_get_channel_layout_nb_channels(codec_context_->channel_layout);
codec_context_->profile = FF_PROFILE_AAC_LOW;
codec_context_->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
codec_context_->codec_id = codec->id;
int ret = avcodec_open2(codec_context_, codec, nullptr);
if (ret != 0) {
  LOGE("%s avcodec_open2 error: %s", __func__, av_err2str(ret));
  return Error::OPEN_FFMPEG_AUDIO_CODEC_ERROR;
}

/// 2.取出Frame Queue中的数据, 送入编码器
ret = avcodec_send_frame(codec_context_, encode_frame_);

/// 3.取出编码器中的AVPacket
while (ret >= 0) {
  AVPacket pkt;
  av_init_packet(&pkt);
  pkt.duration = AV_NOPTS_VALUE;
  pkt.pts = pkt.dts = 0;
  ret = avcodec_receive_packet(codec_context_, &pkt);
  if (ret >= 0) {
    AVPacket packet;
    av_new_packet(&packet, pkt.size);
    memcpy(packet.data, pkt.data, pkt.size);
    auto pts = av_rescale_q(encode_frame_->pts, codec_context_->time_base, time_base);
    packet.pts = pts * av_q2d(time_base) * 1000.f;
    packet_queue_put(packet_queue_, &packet);
  }
  av_packet_unref(&pkt);
}

将编码后的AVPacket放入Packet Queue中,封装的线程中会取出对应的AVPacket,比较其pts和视频的pts,相对应的写入视频文件中。

音频还有两个比较重要的环节:混音和倍速,混音我之前分享过一个混音问题的处理:短视频中解决音视频混音出现杂音的问题,混音这一块后续还是有值得讨论的地方的。音频倍速的内容放到后面和视频的倍速一起讨论会比较好一点。声音还有一个声效的内容,涉及到算法,等我们将VideoEditor所有内容都了结了可以讨论下。大家还有什么有关声音问题的讨论,欢迎私信。

0 人点赞