Почему onMarkerReached не вызывается при следующем воспроизведении, если я не оставлю достаточно времени после остановки?

(Извините, что исправляю это снова и снова.)

Я хочу воспроизвести новый поток сразу после остановки AudioTrack.

Однако если вы не начнете воспроизведение через несколько миллисекунд после остановки воспроизведения с помощью stop(), обратный вызов OnMarkerReached может не вызваться.

Я написал пример кода.

Нажмите кнопку, чтобы воспроизвести звук.

При нажатии кнопки данные за 0,5 секунды записываются в AudioTrack, а через 0,25 секунды в AudioTrack записывается осциллограмма за следующие 0,5 секунды с помощью onMarkerReached. Воспроизведение звука длится 2 секунды.

Если вы нажмете кнопку во время воспроизведения звука в течение 2 секунд, он остановится (), сбросится () и начнется следующее воспроизведение. В настоящее время OnMarkerReached нельзя вызывать, если перед воспроизведением не будет вставлено дополнительное время.

Проверка проводилась на двух устройствах Android.

OnMarkerReached вызывался, даже если на устройстве А было 0 секунд дополнительного времени. Однако OnMarkerReached не вызывался, даже если у устройства Б было 10 мс дополнительного времени.

Почему иногда не вызывается onMarkerReached?

Вот пример кода.

MainActivity.java

package com.example.audiotrackexample;

import androidx.appcompat.app.AppCompatActivity;

import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity implements AudioTrack.OnPlaybackPositionUpdateListener {

    final String TAG = MainActivity.class.getSimpleName();

    Button playBtn;
    Button queryBtn;
    TextView curPosTv;
    TextView curPosTv2;

    public class SinGenerator {

        float freq;
        int samplingRate;
        int sampleCount;

        public SinGenerator(float freq, int samplingRate){
            this.freq = freq;
            this.samplingRate = samplingRate;
            this.sampleCount = 0;
        }

        public short generate(){
            this.sampleCount++;
            double t = (double)freq * sampleCount / samplingRate;
            double sin = Math.sin(2.0 * Math.PI * t);
            return (short) (sin*Short.MAX_VALUE);
        }
    }

    static final int SAMPLING_RATE = 16000;
    static final int AUDIO_DATA_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
    static final int CHANNEL = AudioFormat.CHANNEL_OUT_MONO;
    static final int READ_WAVE_BUFFER_SIZE = SAMPLING_RATE / 2;    // 0.5s
    static final int AUDIO_TRACK_MIN_BUFFER_SIZE = SAMPLING_RATE;
    int audioTrackBufferSize;
    AudioTrack audioTrack;
    SinGenerator sinGenerator = new SinGenerator(300, SAMPLING_RATE);
    short[] wave;
    short[] readWaveBuff = new short[READ_WAVE_BUFFER_SIZE];
    int waveReadLen = 0;
    Handler handler = new Handler(Looper.myLooper());

    void setNextWave(){
        System.arraycopy( wave, waveReadLen, readWaveBuff, 0, READ_WAVE_BUFFER_SIZE );
        waveReadLen += READ_WAVE_BUFFER_SIZE;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        playBtn = findViewById(R.id.playBtn);
        queryBtn = findViewById(R.id.queryBtn);
        curPosTv = findViewById(R.id.curPosTv);
        curPosTv2 = findViewById(R.id.curPosTv2);

        queryBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d(TAG, "curPlaybackPos : " + audioTrack.getPlaybackHeadPosition());
                curPosTv2.setText("curPlaybackPos : " + audioTrack.getPlaybackHeadPosition());
            }
        });

        audioTrackBufferSize = AudioTrack.getMinBufferSize(
                SAMPLING_RATE,
                CHANNEL,
                AUDIO_DATA_FORMAT);

        if (audioTrackBufferSize < AUDIO_TRACK_MIN_BUFFER_SIZE) {
            audioTrackBufferSize = AUDIO_TRACK_MIN_BUFFER_SIZE;
        }

        Log.d(TAG, "audioTrackBufferSize : " + audioTrackBufferSize);

        audioTrack = new AudioTrack(
              AudioManager.STREAM_MUSIC,
              SAMPLING_RATE,
              CHANNEL,
              AUDIO_DATA_FORMAT,
              audioTrackBufferSize,
              AudioTrack.MODE_STREAM);
        audioTrack.setPlaybackPositionUpdateListener(MainActivity.this);

        wave = new short[READ_WAVE_BUFFER_SIZE * 4];
        for (int i = 0; i < 4; i++) {
            sinGenerator = new SinGenerator(300 + i * 100, SAMPLING_RATE);
            for (int j = 0; j < READ_WAVE_BUFFER_SIZE; j++) {
                wave[i * READ_WAVE_BUFFER_SIZE + j] = sinGenerator.generate();
            }
        }

        playBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                audioTrack.stop();
                audioTrack.flush();

                handler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        waveReadLen = 0;
                        setNextWave();

                        audioTrack.setNotificationMarkerPosition(READ_WAVE_BUFFER_SIZE / 2);
                        audioTrack.write(readWaveBuff, 0, READ_WAVE_BUFFER_SIZE);
                        audioTrack.play();
                        curPosTv.setText("curPlaybackPos : " + audioTrack.getPlaybackHeadPosition());
                    }
                },0);        // device A is onMarkerReached called. but device B is not.
//                }, 10);        // device A is onMarkerReached called. but device B is not.
//                }, 100);     // Devices A and B call onMarkerReached.

            }
        });
    }

    @Override
    public void onMarkerReached(AudioTrack track) {

        curPosTv.setText("curPlaybackPos : " + audioTrack.getPlaybackHeadPosition());
        if(waveReadLen == wave.length) {
            audioTrack.stop();
            audioTrack.flush();
            Log.d(TAG, "finish playing");
        } else {

            setNextWave();
            audioTrack.write(readWaveBuff, 0, READ_WAVE_BUFFER_SIZE);
            int newMarkerPosition = audioTrack.getNotificationMarkerPosition();
            if (waveReadLen == wave.length) {
                newMarkerPosition += (READ_WAVE_BUFFER_SIZE / 2) + READ_WAVE_BUFFER_SIZE;
            } else {
                newMarkerPosition += READ_WAVE_BUFFER_SIZE;
            }

            audioTrack.setNotificationMarkerPosition(newMarkerPosition);
            Log.d(TAG, "curPos : " + audioTrack.getPlaybackHeadPosition() + " markerPos : " + audioTrack.getNotificationMarkerPosition());

        }
    }

    @Override
    public void onPeriodicNotification(AudioTrack track) {
    }

}

Activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:gravity="center"
    android:orientation="vertical">

    <Button
        android:id="@+id/playBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Audio Play">
    </Button>

    <TextView
        android:id="@+id/curPosTv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

    </TextView>

    <Button
        android:id="@+id/queryBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Query marker position">
    </Button>

    <TextView
        android:id="@+id/curPosTv2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

    </TextView>

</LinearLayout>

build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"
    defaultConfig {
        applicationId "com.example.audiotrackexample"
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

person syuh31    schedule 15.02.2020    source источник
comment
Мы решили подражать ExoPlayer и каждый раз воссоздавать AudioTrack. Спасибо, что увидели вопрос и ответили на него.   -  person syuh31    schedule 20.02.2020


Ответы (1)


onMarkerReached — довольно плохо определенный API; поведение меняется от OEM к OEM, и из того, что я видел, интервалы обновления могут достигать секунды (так же грубо, как возвращаемые значения из AudioTrack.getNotificationMarkerPosition()). ИМХО, единственный приемлемый вариант использования onMarkerReached() — это периодические обновления панели поиска для элемента управления типа медиаплеера.

Гораздо лучше просто транслировать новый контент в один уже существующий файл AudioTrack. Другими словами, вы просто запускаете один AudioTrack, оставляете его играть и следите за тем, чтобы он никогда не зависал (вы можете вставить в трек тишину, чтобы заполнить пробелы). Таким образом, вы можете решить, как «воспроизведение 1» переходит в «воспроизведение 2», и у вас есть точный контроль над синхронизацией.

person greeble31    schedule 15.02.2020
comment
Спасибо за ваши ценные отзывы и решения. Я так и не придумал, как продолжать играть. Кстати, где вы нашли, что это довольно плохо определенный API? - person syuh31; 15.02.2020
comment
никогда не подходил? Я ответил на это 48 минут назад! :) Я сказал, что это было плохо указано, потому что в документации не указан интервал обновления, и мое собственное тестирование показало, что он сильно различается от устройства к устройству. - person greeble31; 15.02.2020
comment
Это нормально, потому что я понимаю идею продолжать играть. Я понимаю, что обновление грубое. Обновление при вызове OnMarkerReached? Однако на этот раз OnMarkerReached не вызывается, хотя маркер был установлен до второго воспроизведения. Выбор времени приблизительный, но разве это не ошибка, которая не называется OnMarkerReached? Я плохо говорю по-английски, поэтому вы можете не понять, что уже дали ответ. - person syuh31; 15.02.2020
comment
Правильно, под интервалом обновления я подразумеваю скорость, с которой вызывается onMarkerReached(). - person greeble31; 15.02.2020
comment
Я не уверен, почему ваш выбор задержки может повлиять на onMarkerReached(). Мне кажется, что ваша задержка 0/10/100 просто меняет задержку между нажатием кнопки и началом воспроизведения. Я удивлен, что есть разница. Я также удивлен, что вы не получаете IllegalStateException при первом звонке AudioTrack.stop()... - person greeble31; 15.02.2020
comment
Кстати, вы должны использовать postDelayed(); это делает ваш ScheduledExecutorService ненужным. - person greeble31; 15.02.2020
comment
Спасибо за совет. Код был изменен. Кроме того, я заметил, что когда onMarkerReached не вызывался во время второго воспроизведения, я проверил воспроизведениеPosition и обнаружил, что он остановился на 2000-4000. Другими словами, сама игра как бы останавливается. - person syuh31; 15.02.2020
comment
Обратите внимание, что stop()-ing и flush()-ing вернут положение головки воспроизведения в 0. Это не длится в течение времени жизни AudioTrack; только время жизни текущего воспроизведения (согласно документам указать). - person greeble31; 15.02.2020