【问题标题】:Why doesn't onMarkerReached get called in the next play unless I leave enough time after stopping?除非我在停止后留出足够的时间,否则为什么不会在下一场比赛中调用 onMarkerReached?
【发布时间】:2023-03-22 23:10:01
【问题描述】:

(抱歉一遍又一遍地修复它。)

我想在停止 AudioTrack 后立即播放新的流。

但是,如果您在使用 stop () 停止播放后几毫秒后未开始播放,则可能不会调用 OnMarkerReached 回调。

我写了示例代码。

按下按钮播放声音。

按下按钮时,0.5 秒的数据写入 AudioTrack,0.25 秒后,onMarkerReached 将接下来 0.5 秒的波形写入 AudioTrack。播放的声音是 2 秒的声音。

如果您在播放声音的同时按下按钮 2 秒钟,它将stop()flush()并开始下一次播放。此时,可能不会调用 OnMarkerReached,除非在播放前插入额外的时间。

在两台安卓设备上进行了验证。

OnMarkerReached 被调用,即使设备 A 有 0 秒的额外时间。 但是,即使设备 B 有 10 毫秒的额外时间,也不会调用 OnMarkerReached

为什么有时不调用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'
}

【问题讨论】:

  • 我们决定模仿ExoPlayer,每次都重新创建AudioTrack。感谢您看到问题并回答。

标签: android multithreading audio callback audiotrack


【解决方案1】:

onMarkerReached 是一个非常糟糕的 API;从 OEM 到 OEM 的行为似乎有所不同,据我所见,更新间隔可以大到一秒(就像来自AudioTrack.getNotificationMarkerPosition() 的返回值一样粗略)。恕我直言,onMarkerReached() 唯一可接受的用例是媒体播放器类型控件的定期搜索栏更新。

最好将您的新内容流式传输到一个预先存在的 AudioTrack 中。换句话说,您只需启动一个AudioTrack,让它继续播放,并确保它永远不会饿死(您可以在轨道中插入静音以填补任何空白)。这样,您就可以决定“播放 1”如何过渡到“播放 2”,并且可以精确控制时间。

【讨论】:

  • 感谢您提供宝贵的反馈和解决方案。我从来没有想出一个方法来继续玩。顺便说一句,你在哪里发现它是一个指定相当糟糕的 API?
  • “从未出现”?我在 48 分钟前回答了这个问题! :) 我说它指定不好 b/c 文档没有指定更新间隔,我自己的测试表明它因设备而异。
  • 没关系,因为我理解继续玩的想法。我知道更新很粗糙。调用 OnMarkerReached 时是“更新”吗?但是,这一次,即使在第二次播放之前设置了标记,也不会调用 OnMarkerReached。时机很粗糙,但不就是一个不叫 OnMarkerReached 的 bug 吗?我英语不好,所以你可能不明白你已经提供了答案。
  • 对,“更新间隔”是指调用onMarkerReached() 的速率。
  • 我不确定为什么您的延迟选择会对onMarkerReached() 产生影响。在我看来,您的 0/10/100 延迟只是改变了按下按钮和开始播放之间的延迟。我很惊讶这会有所作为。我也很惊讶你第一次打电话给AudioTrack.stop() 时没有得到IllegalStateException...
猜你喜欢
  • 1970-01-01
  • 2016-11-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-23
相关资源
最近更新 更多