Android AudioTrack.onPlaybackPositionUpdateListener not always firing on time
Asked Answered
T

0

8

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'
}
Thais answered 16/5, 2018 at 23:30 Comment(5)
Im a little late to the party. I'm having the exact same issue. 1 sec audioTrack buffer, 48k sample rate, total record time 5 seconds, audio format 16 bit PCM, recorded and played in 1 channel (MONO). This means frame size is 16bits and total recorded frames is 240000 (over the 5 seconds). However, when playing back, my listener only gets notified of 194880 frames (just like you, Im also logging it). The strange part is, after my last log, android itself says: "D/AudioTrack: stop(17435): called with 240000 frames delivered"Maccabees
Tried to create my own HandlerThread and use its Looper, but still get the exact same result.Maccabees
My audio is not getting cut short. Its plays 100%. It is only the notification listener that does not get notified of all the frames. Strange behaviourMaccabees
A bit strange to ask this 4 years later, but did you manage to fix the issue? ThanksMaccabees
I was only able to work around it by manually firing the callback myself at the momemt the first second expired. If I recall, the problem affected API 19-22 whether it was an emulator OR real device but I’ll have to revisit it.Thais

© 2022 - 2024 — McMap. All rights reserved.