Thursday, 2 June 2022

Android AudioTrack.onPlaybackPositionUpdateListener not always firing on time

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'
}


from Android AudioTrack.onPlaybackPositionUpdateListener not always firing on time

No comments:

Post a Comment