安卓AudioTrack缓冲问题

15

好的,我有一个频率发生器,它使用AudioTrack将PCM数据发送到硬件。这是我使用的代码:

private class playSoundTask extends AsyncTask<Void, Void, Void> {
  float frequency;
  float increment;
  float angle = 0;
  short samples[] = new short[1024];

  @Override
  protected void onPreExecute() {
   int minSize = AudioTrack.getMinBufferSize( 44100, AudioFormat.CHANNEL_CONFIGURATION_MONO, AudioFormat.ENCODING_PCM_16BIT );        
   track = new AudioTrack( AudioManager.STREAM_MUSIC, 44100, 
     AudioFormat.CHANNEL_CONFIGURATION_MONO, AudioFormat.ENCODING_PCM_16BIT, 
     minSize, AudioTrack.MODE_STREAM);
   track.play();
  }

  @Override
  protected Void doInBackground(Void... params) {
   while( Main.this.isPlaying)
   {
    for( int i = 0; i < samples.length; i++ )
    {
     frequency = (float)Main.this.slider.getProgress();
     increment = (float)(2*Math.PI) * frequency / 44100;
     samples[i] = (short)((float)Math.sin( angle )*Short.MAX_VALUE);
     angle += increment;
    }

    track.write(samples, 0, samples.length);
   }
   return null;
  }
 }

频率与滑动条相关联,在样本生成循环中报告了正确的值。当我启动应用程序时,一切都很好。当你沿着滑动条拖动手指时,你会听到一个很好的扫描声。但在玩耍约10秒钟后,音频开始变得不稳定。它不再是平滑的扫过声,而是卡顿的,并且每1000 Hz左右才会更改音调。有什么想法是什么原因引起这种情况?

如果问题不在此处,请看以下所有代码:

    public class Main extends Activity implements OnClickListener, OnSeekBarChangeListener {
 AudioTrack track;
 SeekBar slider;
 ImageButton playButton;
 TextView display; 

 boolean isPlaying=false;

 /** Called when the activity is first created. */
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  display = (TextView) findViewById(R.id.display);
  display.setText("5000 Hz");

  slider = (SeekBar) findViewById(R.id.slider);
  slider.setMax(20000);
  slider.setProgress(5000);
  slider.setOnSeekBarChangeListener(this);


  playButton = (ImageButton) findViewById(R.id.play);
  playButton.setOnClickListener(this);

 }

 private class playSoundTask extends AsyncTask<Void, Void, Void> {
  float frequency;
  float increment;
  float angle = 0;
  short samples[] = new short[1024];

  @Override
  protected void onPreExecute() {
   int minSize = AudioTrack.getMinBufferSize( 44100, AudioFormat.CHANNEL_CONFIGURATION_MONO, AudioFormat.ENCODING_PCM_16BIT );        
   track = new AudioTrack( AudioManager.STREAM_MUSIC, 44100, 
     AudioFormat.CHANNEL_CONFIGURATION_MONO, AudioFormat.ENCODING_PCM_16BIT, 
     minSize, AudioTrack.MODE_STREAM);
   track.play();
  }

  @Override
  protected Void doInBackground(Void... params) {
   while( Main.this.isPlaying)
   {
    for( int i = 0; i < samples.length; i++ )
    {
     frequency = (float)Main.this.slider.getProgress();
     increment = (float)(2*Math.PI) * frequency / 44100;
     samples[i] = (short)((float)Math.sin( angle )*Short.MAX_VALUE);
     angle += increment;
    }

    track.write(samples, 0, samples.length);
   }
   return null;
  }
 }



 @Override
 public void onProgressChanged(SeekBar seekBar, int progress,
   boolean fromUser) {
  display.setText(""+progress+" Hz");
 }

 public void onClick(View v) {
  if (isPlaying) {
   stop();
  } else {
   start();
  }
 }

 public void stop() {
  isPlaying=false;
  playButton.setImageResource(R.drawable.play);
 }

 public void start() {
  isPlaying=true;
  playButton.setImageResource(R.drawable.stop);
  new playSoundTask().execute();
 }

 @Override
 protected void onResume() {
  super.onResume();

 }

 @Override
 protected void onStop() {
  super.onStop();
  //Store state
  stop();

 }


 @Override
 public void onStartTrackingTouch(SeekBar seekBar) {
  // TODO Auto-generated method stub

 }


 @Override
 public void onStopTrackingTouch(SeekBar seekBar) {
  // TODO Auto-generated method stub

 }
}

虽然这并不是直接回答你的问题,但需要注意的是,进度条和流媒体存在已知问题:http://code.google.com/p/android/issues/detail?id=4124尽管你的问题与进度条本身无关,而是与音频流有关。因此,这并不能解决你的问题,但由于你正在处理音频流,所以应该知道它存在一个或多个未解决的错误。 - Mathias Conradt
我有完全相同的问题。缓冲区越大,跳动开始得越晚。在日志中,我看到很多非常长的GC(GC在4200ms中清理了180个对象)。我没有进行任何分配。在DDMS分配跟踪器中,似乎AudioTrack本身正在进行分配。不确定GC是否与跳动有关。您解决了这个问题吗? - Martin Konicek
你有没有清理过你的“audiotrack”?这可能是与内存相关的问题。 - An-droid
请分享onPostExecute方法。 - Ahmad Arslan
3个回答

12

我找到了问题所在!

问题在于AudioTrack的缓冲区大小。如果您无法快速生成样本,则会出现样本不足,回放暂停,并在再次有足够的样本时继续。

唯一的解决方法是确保您可以快速生成样本(44100 /秒)。 作为快速修复,请尝试降低采样率至22000(或增加缓冲区的大小)。

至少对我而言,情况就是这样 - 当我优化了我的样本生成例程时,跳动就消失了。

(增加缓冲区的大小使声音开始播放更晚,因为需要等待一些储备 - 直到缓冲区填满。但是,如果您不能快速生成样本,最终样本将用完)。


哦,还有,您应将不变的变量放在循环外面!也许要使样本数组更大,以便循环运行更长时间。

short samples[] = new short[4*1024];
...

frequency = (float)Main.this.slider.getProgress();
increment = (float)(2 * Math.PI) * frequency / 44100;
for(int i = 0; i < samples.length; i++)
{
   samples[i] = (short)((float)Math.sin(angle) * Short.MAX_VALUE);
   angle += increment;
}
你也可以预先计算所有200Hz-8000Hz频率下的正弦值,步长为10Hz。或者按需进行此操作:当用户选择一个频率时,使用你的方法生成样本,并将它们保存到数组中。当你生成足够的样本来产生一个完整的正弦波时,你可以简单地循环遍历该数组(因为正弦是一个周期函数)。实际上,由于你每次增加的角度为1,而一整个正弦波的长度为sampleRate / (double)frequency个样本,因此在声音方面可能会有一些小的不一致性。但是选择正确的周期倍数会使这些不一致性变得不明显。
另外,请参见http://developer.android.com/guide/practices/design/performance.html

4

如果有人遇到与我相同的问题(如我所遇到的),解决方案实际上与缓冲区无关,而与正弦函数有关。尝试用 angle += (increment % (2.0f * (float) Math.PI)); 替换 angle += increment。此外,为了提高效率,请尝试使用 FloatMath.sin() 而不是 Math.sin()。


1

我想我已经成功解决了问题。

...
samples[i] = (short)((float)Math.sin( angle )*Short.MAX_VALUE);
angle += increment;
angle = angle % (2.0f * (float) Math.PI); //added statement
...

如前所述,这与缓冲区无关。它与角度变量有关,它会持续增加。过一段时间后,变量变得太大,不支持小步骤。

由于正弦函数在2 * PI后重复,我们需要取模角度。

希望这有所帮助。

编辑:angle += increment就足够了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接