Android AudioTrack 缓冲问题
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/3305598/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Android AudioTrack buffering problems
提问by JCL
Ok so I have a frequency generator which uses AudioTrack to send PCM data to the hardware. Here's the code I'm using for that:
好的,所以我有一个频率发生器,它使用 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;
}
}
The frequency is tied to a slide bar, and the correct value is being reported in the sample generation loop. Everything is fine and dandy when I start the app. When you drag your finger along the slide bar you get a nice sweeping sound. But after around 10 seconds of playing around with it, the audio starts to get jumpy. Instead of a smooth sweep, it's staggered, and only changes tone around every 1000 Hz or so. Any ideas on what could be causing this?
频率与滑动条相关联,并且在样本生成循环中报告正确的值。当我启动应用程序时,一切都很好。当您沿着滑动条拖动手指时,您会听到清脆的声音。但是在玩了大约 10 秒后,音频开始变得跳跃。它不是平滑的扫描,而是交错的,并且每 1000 Hz 左右才会改变音调。关于可能导致这种情况的任何想法?
Here's all the code in case the problem lies somewhere else:
这是所有代码,以防问题出在其他地方:
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
}
}
回答by Martin Konicek
I found the cause!
我找到原因了!
The problem is the size of the AudioTrack's buffer. If you cannot generate samples fast enough, the samples run out, the playback pauses, and continues when there are enough samples again.
问题是 AudioTrack 缓冲区的大小。如果您不能足够快地生成样本,则样本会用完,播放暂停,并在再次有足够样本时继续播放。
The only solution is to make sure that you can generate samples fast enough (44100/s). As a fast fix, try lowering the sampling rate to 22000 (or increasing the size of the buffer).
唯一的解决方案是确保您可以足够快地生成样本 (44100/s)。作为快速修复,尝试将采样率降低到 22000(或增加缓冲区的大小)。
At least this is how it behaves for me - when I optimized my sample generation routine, the jumping went away.
至少它对我来说是这样的——当我优化我的样本生成程序时,跳跃消失了。
(Increasing the size of the buffer makes the sound start playing later, as there is some reserve being waited for - until the buffer fills. But still, if you are not generating the samples fast enough, eventually the samples run out).
(增加缓冲区的大小会使声音稍后开始播放,因为有一些保留正在等待 - 直到缓冲区填满。但是,如果您没有足够快地生成样本,最终样本会用完)。
Oh, and you should put invariant variables outside of the loop! And maybe make the samples array larger, so that the loop runs longer.
哦,你应该把不变变量放在循环之外!并且可能使样本数组更大,以便循环运行更长的时间。
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;
}
You could also precompute the values of all sines for frequencies 200Hz-8000Hz, with a step of 10Hz. Or do this on demand: when the user select a frequency, generate samples using your method for a while and also save them to an array. When you generate enough samples to have a one full sine wave, you can just keep looping over the array (since sin is a periodic function). Actually there might be some small inconsistencies in the sound as you never hit a period exactly (because you are increasing the angle by 1 and the lenght of one full sine wave is sampleRate / (double)frequency
samples). But taking a right multiple of the period makes the inconsistencies unnoticeable.
您还可以预先计算 200Hz-8000Hz 频率的所有正弦值,步长为 10Hz。或者根据需要执行此操作:当用户选择频率时,使用您的方法生成样本一段时间并将它们保存到数组中。当您生成足够的样本以获得一个完整的正弦波时,您可以继续循环遍历数组(因为 sin 是一个周期函数)。实际上,声音中可能存在一些小的不一致,因为您从未完全达到一个周期(因为您将角度增加 1 并且一个完整的正弦波的长度是sampleRate / (double)frequency
样本)。但是采用正确的周期倍数会使不一致之处变得不明显。
Also, see http://developer.android.com/guide/practices/design/performance.html
另请参阅http://developer.android.com/guide/practices/design/performance.html
回答by Tsherr
In case anyone stumbles across this with the same problem (as I did), the solution actually has nothing to do with buffers, it has to do with the sine function. Try replacing angle += increment
with angle += (increment % (2.0f * (float) Math.PI));
. Also, for efficiency, try using FloatMath.sin() instead of Math.sin().
如果有人遇到同样的问题(就像我一样),解决方案实际上与缓冲区无关,它与正弦函数有关。尝试替换angle += increment
为angle += (increment % (2.0f * (float) Math.PI));
. 此外,为了提高效率,请尝试使用 FloatMath.sin() 而不是 Math.sin()。
回答by Kartal
I think I've managed to solve the problem.
我想我已经设法解决了这个问题。
...
samples[i] = (short)((float)Math.sin( angle )*Short.MAX_VALUE);
angle += increment;
angle = angle % (2.0f * (float) Math.PI); //added statement
...
As stated before, it's not about buffers. It is about the angle variable, it increases continuously. After a while, the variable gets too large and does not support little steps.
如前所述,这与缓冲区无关。它是关于角度变量,它不断增加。一段时间后,变量变得太大,不支持小步。
As the sine repeats after 2*PI, we need to take the modulus of angle.
由于正弦在 2*PI 后重复,我们需要取角度模数。
Hope this helps.
希望这可以帮助。
edited: angle += increment is enough for the job.
编辑:角度 += 增量足以完成这项工作。