实时音频捕获bug紧急修复


文章目录

  • 实时音频捕获bug紧急修复
  • 背景
  • 问题描述
  • 原因分析
  • 解决方案
  • 结论


背景

在进行集成测试的时候,我们团队成员间进行了多方面的交流,确认了自己负责模块与对方负责模块之间数据交换协议以及对方对其他人模块功能的期望。在了解到对方的语音处理接口对采样率有要求后,我调整了音频捕获类使用的采样率(使用的stk框架中支持),但是在后续的测试中出现了严重的问题。

问题描述

在修改为较低采样率后,录制的音频文件效果十分诡异,频率严重降低,根本无法入耳,更不用说识别。

原因分析

在检查了自己的代码后,我认为不存在相关的失误,于是我转而看了看框架的代码,发现了十分有意思的事情

RtWvIn :: RtWvIn( unsigned int nChannels, StkFloat sampleRate, int device, int bufferFrames, int nBuffers )
  : stopped_( true ), readIndex_( 0 ), writeIndex_( 0 ), framesFilled_( 0 )
{
  // We'll let RtAudio deal with channel and sample rate limitations.
  RtAudio::StreamParameters parameters;
  if ( device == 0 )
    parameters.deviceId = adc_.getDefaultInputDevice();
  else
    parameters.deviceId = device - 1;
  parameters.nChannels = nChannels;
  unsigned int size = bufferFrames;
  RtAudioFormat format = ( sizeof(StkFloat) == 8 ) ? RTAUDIO_FLOAT64 : RTAUDIO_FLOAT32;

  try {
    adc_.openStream( NULL, ¶meters, format, (unsigned int)Stk::sampleRate(), &size, &read, (void *)this );
  }
  catch ( RtAudioError &error ) {
    handleError( error.what(), StkError::AUDIO_SYSTEM );
  }

  data_.resize( size * nBuffers, nChannels );
  lastFrame_.resize( 1, nChannels );
}

上面的代码是stk框架实时音频输入的类的构造函数。这里可能看不出问题,但是在我的IDE中,sampleRate下面有一条明显的警告,内容是unused variable。即,即使在构造对象的时候提供了希望的采样率,这个类的对象也不会去理会,而是照着自己的采样率采样。

那么我们遇到的问题就很明确了——框架本身的缺陷导致实时音频处理的相关对象不能根据需求改变采样率,而文件音频处理的类可以,导致输入音频的采样率(44100)和输出音频的采样率(16000)不一致,反应在信号特性上就是频率降低。

解决方案

幸运的是,框架中有一个信号缓冲区类StkFrames,这个类定义了一个非常好的方法interpolation,可以输入非整数index通过插值的方式计算出信号的频率,既然框架中原有的类没有完成这项简单的工作,那么我就来亲自完成。

当然,我对可行性进行了技术测试,借用了框架的示例代码

int main( int argc, char *argv[] )
{
  // minimal command-line checking
  if ( argc != 5 ) usage();

  Stk::showWarnings( true );

  unsigned int channels = (unsigned int) atoi( argv[1] );
  double sampleRate = atof( argv[4] );
  double time = atof( argv[3] );
  long samples, i;
  StkFrames in_frame( 44100, channels );
  StkFrames out_frame((int)sampleRate, channels);

  // Set the global sample rate.
  Stk::setSampleRate( sampleRate );

  // Initialize our WvIn/WvOut pointers.
  RtWvIn *input = 0;
  FileWvOut *output = 0;

  // Open the realtime input device
  try {
	input = new RtWvIn( channels , sampleRate, 6);
  }
  catch ( StkError & ) {
    exit( 1 );
  }
  
  // Open the soundfile for output.
  try {
    output = new FileWvOut( argv[2], channels, FileWrite::FILE_WAV );
  }
  catch ( StkError & ) {
    goto cleanup;
  }

  // Here's the runtime loop
  samples = (long) ( time * Stk::sampleRate() );
  for ( i=0; i<time; i++ ) {
	input->tick( in_frame );
	for(int j = 0; j < out_frame.frames(); j++)
	{
		out_frame[j] = in_frame.interpolate(j * in_frame.frames() / out_frame.frames());
		printf("sample :%f\n", out_frame[j]);
	}
	output->tick(out_frame);
  }

 cleanup:
  delete input;
  delete output;
  return 0;
}

我一次性取出一个窗口,然后使用文件和设备采样率的比值和输出文件需要的样本点索引完成了采样,效果符合预期,声音的频率恢复正常。

然而有趣的是,RtWvIn这个类的定义非常的幽默——虽然继承了抽象父类,但是这个类把所有的函数都写成了实函数,也就是说不能继承。考虑了框架中类的继承关系,我依然认为派生一个该类的子类依旧是最好的解决方案,所以我将该类的tick(int)方法前面加上了virtual,重新编译,安装了框架。

然后,在音频捕获模块的相关文件中,我添加了如下的类定义

/**
 * @brief The MyRtWvIn class
 * 2018-06-12 原始的RtWvIn存在这非常搞笑的问题,这里我们必须通过继承一个新的类来解决这个问题
 * 这个问题是这样的:这个类的构造函数中虽然有SampleRate这个参数,但实际上根本没有使用(甚至没有把它传给父类),
 * 导致实时录音的这个对象只能使用录音设备原始的采样率进行录音,而输出对象并不知道这一点,会按照新的采样率输出,这就将导致输出文件中音频的频率发生错乱
 */
class MyRtWvIn : public RtWvIn
{
public:
	static void getDeviceSampleRate(){cout << Stk::sampleRate() << endl; devRate = Stk::sampleRate();} //这个函数必须在设置采样率之前调用,不然这个类也不能正常工作
	MyRtWvIn( unsigned int nChannels = 1,
			  StkFloat sampleRate = Stk::sampleRate(),
			  int device = 0,
			  int bufferFrames = RT_BUFFER_SIZE,
			  int nBuffers = 20);
	StkFloat tick(unsigned int channel=0);	//我们只用得着这一种tick,不需要过多定义
private:
	static StkFloat devRate;
	StkFloat targetRate;
	StkFrames in_frame;
	const uint32_t MAX_index;
	uint32_t index;
};

相关定义如下

StkFloat MyRtWvIn::devRate = 0;
/**
 * @brief MyRtWvIn::MyRtWvIn
 * 这个构造函数构造出一个父类对象和两个frame就够了
 * @param nChannels
 * @param sampleRate
 * @param device
 * @param bufferFrames
 * @param nBuffers
 */
MyRtWvIn::MyRtWvIn(unsigned int nChannels, StkFloat sampleRate, int device, int bufferFrames, int nBuffers) :
	RtWvIn(nChannels, sampleRate, device, bufferFrames, nBuffers),
	in_frame((int)devRate / 100, nChannels), MAX_index((int)sampleRate / 100), index(0){}

StkFloat MyRtWvIn::tick(unsigned int channel)
{
	if(index > MAX_index)
	{
		RtWvIn::tick(in_frame, channel);
		index = 0;
	}
	StkFloat findex = (StkFloat) index++ * ( in_frame.frames() ) / ( MAX_index );
	StkFloat retval = in_frame.interpolate(findex);
	return retval;
}

这样,我们实现了真正有效的实时音频捕获类,经过过去的技术测试代码测试后,没有发现问题

结论

这是我们在测试过程中出现的最严重的,由客观因素导致的问题,及时的解决也是一次可观的成就!