Android AudioTrack.onPlaybackPositionUpdateListener并非总是按时触发

Boo*_*unz 5 audio multithreading android callback audiotrack

我注意到,AudioTrack.setPlaybackPositionUpdateListener()有时似乎无法按预期工作(至少在API 19-22中的Android Studio模拟器中)。

我制作了一个小测试程序,该程序带有一个按钮,当按下该按钮时,它会开始向AudioTrack提供一秒钟长的音频缓冲区。

音轨应该使用onPeriodicNotification()进行回调,这又会翻转Activity的背景颜色并生成Log。

预期行为:

按下开始按钮后,大约每隔一秒钟发送或接收一次通知。

多数时间确实会发生这种情况,但有时(似乎主要在API 19-22上):

第一个(一秒钟后)通知丢失/推迟了,相反,我们在两秒钟时收到了两个同时发生的通知。

为什么会这样呢?是否有更好的方法根据播放头的位置获取回调?

我能想到的唯一原因可能是该视频 19:30标记附近所述的CPU频率缩放问题。

主要活动:

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);
        }

    }

}
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)

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'
}
Run Code Online (Sandbox Code Playgroud)