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:
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?
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
}
}
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
withangle += (increment % (2.0f * (float) Math.PI));
. Also, for efficiency, try using FloatMath.sin() instead of Math.sin().I think I've managed to solve the problem.
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.
Hope this helps.
edited: angle += increment is enough for the job.
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.
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).
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.
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.Also, see http://developer.android.com/guide/practices/design/performance.html