I have noticed that AudioTrack.setPlaybackPositionUpdateListener() sometimes does not seem to work as intended (at least in Android Studio emulators from API 19-22).
I've made a little test program that has a button that when pressed, starts feeding an AudioTrack with one-second-long buffers of audio.
The AudioTrack is supposed to call back using onPeriodicNotification() which in turn flips the background color of the Activity and makes a Log.
Expected behavior:
The notification is sent and received more-or-less exactly every one second after having pressed the start button.
This does happen most of the time, but sometimes (seems to be mostly on API 19-22):
The first (after one-second) notification is missed/postponed, and instead we get two simultaneous notifications happening at two seconds.
Why is this happening? Is there a better way to get a callback based on the playback head position?
The only reason I can think of is possibly a CPU frequency scaling issue as described around the 19:30 mark of this video.
MainActivity:
public class MainActivity extends Activity {
View rootView;
Button startButton;
int initialBackgroundColor;
boolean colorFlipper = false;
boolean isPlaying = false;
// AUDIO -------------
AudioTrack audioTrack;
int sampleRateInHz = 44100;
int bufferSizeInBytes = 44100;
byte[] silenceArray;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rootView = findViewById(R.id.rootView);
startButton = findViewById(R.id.startButton);
initialBackgroundColor = rootView.getDrawingCacheBackgroundColor();
// AUDIO -------------
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes, AudioTrack.MODE_STREAM);
audioTrack.setPositionNotificationPeriod(44100); // this amounts to one second
// create "dummy" (silent) sound... one second long
silenceArray = new byte[bufferSizeInBytes];
for (int i = 0; i < (bufferSizeInBytes-1); i++)
silenceArray[i] = 0;
}
public void startPressed(View v) {
boolean wasPlayingWhenPressed = startButton.isSelected();
startButton.setSelected(!startButton.isSelected());
if (wasPlayingWhenPressed) {
stop();
startButton.setText("START");
} else {
start();
startButton.setText("STOP");
}
}
private void start() {
isPlaying = true;
audioTrack.reloadStaticData();
audioTrack.play();
Runnable r = new Runnable() {
public void run() {
audioTrack.setPlaybackPositionUpdateListener(new AudioTrack.OnPlaybackPositionUpdateListener(){
@Override
public void onMarkerReached(AudioTrack arg0) {
}
@Override
public void onPeriodicNotification(AudioTrack arg0) {
// this *should* be called every second after play is pressed,
// but sometimes the first call is postponed and comes out
// simultaneously with the second call
Log.i("XXX","onPeriodicNotification() was called");
flipBackgroundColor();
}
});
while(isPlaying) {
audioTrack.write(silenceArray,0,silenceArray.length);
}
}
};
Thread backround_thread = new Thread(r);
backround_thread.start();
}
private void stop() {
isPlaying = false;
audioTrack.stop();
}
private void flipBackgroundColor(){
colorFlipper = !colorFlipper;
if (colorFlipper) {
rootView.setBackgroundColor(Color.RED);
} else {
rootView.setBackgroundColor(initialBackgroundColor);
}
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rootView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/startButton"
android:onClick="startPressed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="start"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
build.gradle:
apply plugin: 'com.android.application'
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.example.boober.stackqaudiotrackcallback"
minSdkVersion 19
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support.constraint:constraint-layout:1.1.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}