为什么这个节拍器应用程序会崩溃? (Android)
Why does this metronome app crash? (Android)
我正在开发一个非常小的 Android 项目,该项目使用 this exact code from github。
但是,当我(或您)间歇性地按下 start/stop 按钮时...应用程序最终会崩溃。不幸的是,这可能需要一些时间才能重现......但它会发生!
哦,我忘记想要的结果了!!
The desired result is that this crash does not occur. :)
有谁知道为什么会发生这种崩溃?自 2013 年 3 月以来,此代码的作者已在 Github 上公开 bug/issue...所以我很确定这不是一个特别愚蠢的问题...如果您知道回答这个问题,你无疑会被誉为大佬。
几天来,我一直在剖析代码、打印调试和研究 ASyncTask、Handlers 和 AudioTrack,但我想不通……但如果没有人比我更厉害的话,我会的.
这是堆栈跟踪:
E/AndroidRuntime: FATAL EXCEPTION: AsyncTask #4
Process: com.example.boober.beatkeeper, PID: 15664
java.lang.RuntimeException: An error occurred while executing doInBackground()
at android.os.AsyncTask.done(AsyncTask.java:309)
at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:354)
at java.util.concurrent.FutureTask.setException(FutureTask.java:223)
at java.util.concurrent.FutureTask.run(FutureTask.java:242)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
Caused by: java.lang.IllegalStateException: Unable to retrieve AudioTrack pointer for write()
at android.media.AudioTrack.native_write_byte(Native Method)
at android.media.AudioTrack.write(AudioTrack.java:1761)
at android.media.AudioTrack.write(AudioTrack.java:1704)
at com.example.boober.beatkeeper.AudioGenerator.writeSound(AudioGenerator.java:55)
at com.example.boober.beatkeeper.Metronome.play(Metronome.java:60)
at com.example.boober.beatkeeper.MainActivity$MetronomeAsyncTask.doInBackground(MainActivity.java:298)
at com.example.boober.beatkeeper.MainActivity$MetronomeAsyncTask.doInBackground(MainActivity.java:283)
at android.os.AsyncTask.call(AsyncTask.java:295)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
你可以去github下载原代码,但是为了满足Whosebug的要求,我还提供了更简洁的"minimal working example",你可以单独切割和如果愿意,可以粘贴到您的 Android 工作室。
主要活动:
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
String TAG = "AAA";
Button playStopButton;
TextView currentBeat;
// important objects
MetronomeAsyncTask aSync;
Handler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
currentBeat = findViewById(R.id.currentBeatTextView);
playStopButton = findViewById(R.id.playStopButton);
// important objcts
aSync = new MetronomeAsyncTask();
}
// only called from within playStopPressed()
private void stopPressed() {
aSync.stop();
aSync = new MetronomeAsyncTask();
}
// only called from within playStopPressed()
private void playPressed() {
//aSync.execute();
aSync.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null);
}
public synchronized void playStopButtonPressed(View v) {
boolean wasPlayingWhenPressed = playStopButton.isSelected();
playStopButton.setSelected(!playStopButton.isSelected());
if (wasPlayingWhenPressed) {
stopPressed();
} else {
playPressed();
}
}
// METRONOME BRAIN STUFF ------------------------------------------
private Handler getHandler() {
return new Handler() {
@Override
public void handleMessage(Message msg) {
String message = (String) msg.obj;
if (message.equals("1")) {
currentBeat.setTextColor(Color.GREEN);
}
else {
currentBeat.setTextColor(Color.BLUE);
}
currentBeat.setText(message);
}
};
}
private class MetronomeAsyncTask extends AsyncTask<Void, Void, String> {
MetronomeBrain metronome;
MetronomeAsyncTask() {
mHandler = getHandler();
metronome = new MetronomeBrain(mHandler);
Runtime.getRuntime().gc(); // <---- don't know if this line is necessary or not.
}
protected String doInBackground(Void... params) {
metronome.setBeat(4);
metronome.setNoteValue(4);
metronome.setBpm(100);
metronome.setBeatSound(2440);
metronome.setSound(6440);
metronome.play();
return null;
}
public void stop() {
metronome.stop();
metronome = null;
}
public void setBpm(short bpm) {
metronome.setBpm(bpm);
metronome.calcSilence();
}
public void setBeat(short beat) {
if (metronome != null)
metronome.setBeat(beat);
}
}
}
MetronomeBrain:
import android.os.Handler;
import android.os.Message;
public class MetronomeBrain {
private double bpm;
private int beat;
private int noteValue;
private int silence;
private double beatSound;
private double sound;
private final int tick = 1000; // samples of tick
private boolean play = true;
private AudioGenerator audioGenerator = new AudioGenerator(8000);
private Handler mHandler;
private double[] soundTickArray;
private double[] soundTockArray;
private double[] silenceSoundArray;
private Message msg;
private int currentBeat = 1;
public MetronomeBrain(Handler handler) {
audioGenerator.createPlayer();
this.mHandler = handler;
}
public void calcSilence() {
silence = (int) (((60 / bpm) * 8000) - tick);
soundTickArray = new double[this.tick];
soundTockArray = new double[this.tick];
silenceSoundArray = new double[this.silence];
msg = new Message();
msg.obj = "" + currentBeat;
double[] tick = audioGenerator.getSineWave(this.tick, 8000, beatSound);
double[] tock = audioGenerator.getSineWave(this.tick, 8000, sound);
for (int i = 0; i < this.tick; i++) {
soundTickArray[i] = tick[i];
soundTockArray[i] = tock[i];
}
for (int i = 0; i < silence; i++)
silenceSoundArray[i] = 0;
}
public void play() {
calcSilence();
do {
msg = new Message();
msg.obj = "" + currentBeat;
if (currentBeat == 1)
audioGenerator.writeSound(soundTockArray);
else
audioGenerator.writeSound(soundTickArray);
if (bpm <= 120)
mHandler.sendMessage(msg);
audioGenerator.writeSound(silenceSoundArray);
if (bpm > 120)
mHandler.sendMessage(msg);
currentBeat++;
if (currentBeat > beat)
currentBeat = 1;
} while (play);
}
public void stop() {
play = false;
audioGenerator.destroyAudioTrack();
}
public double getBpm() {
return bpm;
}
public void setBpm(int bpm) {
this.bpm = bpm;
}
public int getNoteValue() {
return noteValue;
}
public void setNoteValue(int bpmetre) {
this.noteValue = bpmetre;
}
public int getBeat() {
return beat;
}
public void setBeat(int beat) {
this.beat = beat;
}
public double getBeatSound() {
return beatSound;
}
public void setBeatSound(double sound1) {
this.beatSound = sound1;
}
public double getSound() {
return sound;
}
public void setSound(double sound2) {
this.sound = sound2;
}
}
音频发生器:
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
public class AudioGenerator {
private int sampleRate;
private AudioTrack audioTrack;
public AudioGenerator(int sampleRate) {
this.sampleRate = sampleRate;
}
public double[] getSineWave(int samples,int sampleRate,double frequencyOfTone){
double[] sample = new double[samples];
for (int i = 0; i < samples; i++) {
sample[i] = Math.sin(2 * Math.PI * i / (sampleRate/frequencyOfTone));
}
return sample;
}
public byte[] get16BitPcm(double[] samples) {
byte[] generatedSound = new byte[2 * samples.length];
int index = 0;
for (double sample : samples) {
// scale to maximum amplitude
short maxSample = (short) ((sample * Short.MAX_VALUE));
// in 16 bit wav PCM, first byte is the low order byte
generatedSound[index++] = (byte) (maxSample & 0x00ff);
generatedSound[index++] = (byte) ((maxSample & 0xff00) >>> 8);
}
return generatedSound;
}
public void createPlayer(){
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate, AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT, sampleRate,
AudioTrack.MODE_STREAM);
audioTrack.play();
}
public void writeSound(double[] samples) {
byte[] generatedSnd = get16BitPcm(samples);
audioTrack.write(generatedSnd, 0, generatedSnd.length);
}
public void destroyAudioTrack() {
audioTrack.stop();
// This line seems to be a most likely culprit of the start/stop crash.
// Is this line even necessary?
audioTrack.release();
}
}
布局:
<?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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.boober.android_metronome.MainActivity">
<Button
android:id="@+id/playStopButton"
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:onClick="playStopButtonPressed"
android:text="Play"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/currentBeatTextView"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="32dp"
android:text="TextView"
android:gravity="center_vertical"
android:textAlignment="center"
android:textSize="30sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playStopButton" />
</android.support.constraint.ConstraintLayout>
经过思考dmarin's comment and reading the code, I arrive to the conclusion that dmarin回答了你的问题。这是一种竞争条件,也是对未初始化对象的访问。所以 short 解决方案是:代码需要检查访问的数据是否已初始化。如果 null
或 getState()
等于 "initialized",则可以检查 AudioTrack
个对象。不幸的是,问题并没有随着我的设置而消失(Android Studio 3.1.2,Android SDK Build-Tools 28-rc2)。
private boolean isInitialized() {
return audioTrack.getState() == AudioTrack.STATE_INITIALIZED;
}
经过代码分析后,您可能会注意到 AsyncTasks 和 AudioTracks 的创建。因此,为了最大限度地减少这些,只在 onCreate
函数中创建一次 AsyncTask,并将 AudioTrack
对象设置为 static
。
主要Activity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
currentBeat = findViewById(R.id.currentBeatTextView);
playStopButton = findViewById(R.id.playStopButton);
// important objcts
aSync = new MetronomeAsyncTask();
aSync.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null);
}
音频发生器
public class AudioGenerator {
/*changed to static*/
private static AudioTrack audioTrack;
...
}
我承认仅仅将其更改为静态并不是一个完美的解决方案。但由于我只需要一根管道连接到 AudioService,这样就可以了。
创建音频管道、停止播放音频并释放资源将如下所示:
public void createPlayer(){
if (audioTrack == null || ! isInitialized())
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate, AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT, sampleRate,
AudioTrack.MODE_STREAM);
if (isInitialized()){
audioTrack.play();
}
}
public void destroyAudioTrack() {
if (isInitialized()) {
audioTrack.stop();
}
}
public void stopRelease() {
if (isInitialized()) {
audioTrack.stop();
audioTrack.release();
}
}
布尔值 play
由我重新调整用途。此外,当按下播放按钮时,名为 currentBeat
的节拍计数器将被重置。对于从 MainActivity
访问:将这些变量从 private
更改为 public
不是最佳解决方案。
// only called from within playStopPressed()
private void stopPressed() {
aSync.metronome.play = false;
}
// only called from within playStopPressed()
private void playPressed() {
aSync.metronome.play = true;
aSync.metronome.currentBeat = 1;
}
在 MetronomeBrain
的 play()
中,循环变成了无限循环。这个问题很快就会得到解决。这就是 play
布尔值可能被重新利用的原因。音调的播放需要设置不同的条件,这取决于play
.
public void play() {
calcSilence();
/*a change for the do-while loop: It runs forever and needs
to be killed externally of the loop.
Also the play decides, if audio is being played.*/
do {
msg = new Message();
msg.obj = "" + currentBeat;
if (currentBeat == 1 && play)
audioGenerator.writeSound(soundTockArray);
else if (play)
audioGenerator.writeSound(soundTickArray);
if (bpm <= 120)
mHandler.sendMessage(msg);
audioGenerator.writeSound(silenceSoundArray);
if (bpm > 120)
mHandler.sendMessage(msg);
currentBeat++;
if (currentBeat > beat)
currentBeat = 1;
} while (true);
}
现在循环永远运行,但它可能只播放,如果 play
设置为 true
。如果有必要进行清理,可以在 Activity
生命周期结束时完成,就像 MainActivity
:
中这样
@Override
protected void onDestroy() {
aSync.metronome.stopReleaseAudio(); //calls the stopRelease()
aSync.cancel(true);
super.onDestroy();
}
正如我所说,代码可以进一步改进,但它提供了一个公平的提示和足够的 material 到 think/learn 关于 AsyncTasks,音频服务和 Activity - 生命周期.
参考资料
- https://developer.android.com/reference/android/os/AsyncTask
- https://developer.android.com/reference/android/media/AudioManager
- https://developer.android.com/reference/android/media/AudioTrack
- https://developer.android.com/reference/android/app/Activity#activity-lifecycle
TL;DR: 确保对象在访问它们之前已经初始化,只需创建一次所有内容,并在不需要时销毁它们,例如在 activity.
的末尾
我正在开发一个非常小的 Android 项目,该项目使用 this exact code from github。
但是,当我(或您)间歇性地按下 start/stop 按钮时...应用程序最终会崩溃。不幸的是,这可能需要一些时间才能重现......但它会发生!
哦,我忘记想要的结果了!!
The desired result is that this crash does not occur. :)
有谁知道为什么会发生这种崩溃?自 2013 年 3 月以来,此代码的作者已在 Github 上公开 bug/issue...所以我很确定这不是一个特别愚蠢的问题...如果您知道回答这个问题,你无疑会被誉为大佬。
几天来,我一直在剖析代码、打印调试和研究 ASyncTask、Handlers 和 AudioTrack,但我想不通……但如果没有人比我更厉害的话,我会的.
这是堆栈跟踪:
E/AndroidRuntime: FATAL EXCEPTION: AsyncTask #4
Process: com.example.boober.beatkeeper, PID: 15664
java.lang.RuntimeException: An error occurred while executing doInBackground()
at android.os.AsyncTask.done(AsyncTask.java:309)
at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:354)
at java.util.concurrent.FutureTask.setException(FutureTask.java:223)
at java.util.concurrent.FutureTask.run(FutureTask.java:242)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
Caused by: java.lang.IllegalStateException: Unable to retrieve AudioTrack pointer for write()
at android.media.AudioTrack.native_write_byte(Native Method)
at android.media.AudioTrack.write(AudioTrack.java:1761)
at android.media.AudioTrack.write(AudioTrack.java:1704)
at com.example.boober.beatkeeper.AudioGenerator.writeSound(AudioGenerator.java:55)
at com.example.boober.beatkeeper.Metronome.play(Metronome.java:60)
at com.example.boober.beatkeeper.MainActivity$MetronomeAsyncTask.doInBackground(MainActivity.java:298)
at com.example.boober.beatkeeper.MainActivity$MetronomeAsyncTask.doInBackground(MainActivity.java:283)
at android.os.AsyncTask.call(AsyncTask.java:295)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
你可以去github下载原代码,但是为了满足Whosebug的要求,我还提供了更简洁的"minimal working example",你可以单独切割和如果愿意,可以粘贴到您的 Android 工作室。
主要活动:
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
String TAG = "AAA";
Button playStopButton;
TextView currentBeat;
// important objects
MetronomeAsyncTask aSync;
Handler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
currentBeat = findViewById(R.id.currentBeatTextView);
playStopButton = findViewById(R.id.playStopButton);
// important objcts
aSync = new MetronomeAsyncTask();
}
// only called from within playStopPressed()
private void stopPressed() {
aSync.stop();
aSync = new MetronomeAsyncTask();
}
// only called from within playStopPressed()
private void playPressed() {
//aSync.execute();
aSync.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null);
}
public synchronized void playStopButtonPressed(View v) {
boolean wasPlayingWhenPressed = playStopButton.isSelected();
playStopButton.setSelected(!playStopButton.isSelected());
if (wasPlayingWhenPressed) {
stopPressed();
} else {
playPressed();
}
}
// METRONOME BRAIN STUFF ------------------------------------------
private Handler getHandler() {
return new Handler() {
@Override
public void handleMessage(Message msg) {
String message = (String) msg.obj;
if (message.equals("1")) {
currentBeat.setTextColor(Color.GREEN);
}
else {
currentBeat.setTextColor(Color.BLUE);
}
currentBeat.setText(message);
}
};
}
private class MetronomeAsyncTask extends AsyncTask<Void, Void, String> {
MetronomeBrain metronome;
MetronomeAsyncTask() {
mHandler = getHandler();
metronome = new MetronomeBrain(mHandler);
Runtime.getRuntime().gc(); // <---- don't know if this line is necessary or not.
}
protected String doInBackground(Void... params) {
metronome.setBeat(4);
metronome.setNoteValue(4);
metronome.setBpm(100);
metronome.setBeatSound(2440);
metronome.setSound(6440);
metronome.play();
return null;
}
public void stop() {
metronome.stop();
metronome = null;
}
public void setBpm(short bpm) {
metronome.setBpm(bpm);
metronome.calcSilence();
}
public void setBeat(short beat) {
if (metronome != null)
metronome.setBeat(beat);
}
}
}
MetronomeBrain:
import android.os.Handler;
import android.os.Message;
public class MetronomeBrain {
private double bpm;
private int beat;
private int noteValue;
private int silence;
private double beatSound;
private double sound;
private final int tick = 1000; // samples of tick
private boolean play = true;
private AudioGenerator audioGenerator = new AudioGenerator(8000);
private Handler mHandler;
private double[] soundTickArray;
private double[] soundTockArray;
private double[] silenceSoundArray;
private Message msg;
private int currentBeat = 1;
public MetronomeBrain(Handler handler) {
audioGenerator.createPlayer();
this.mHandler = handler;
}
public void calcSilence() {
silence = (int) (((60 / bpm) * 8000) - tick);
soundTickArray = new double[this.tick];
soundTockArray = new double[this.tick];
silenceSoundArray = new double[this.silence];
msg = new Message();
msg.obj = "" + currentBeat;
double[] tick = audioGenerator.getSineWave(this.tick, 8000, beatSound);
double[] tock = audioGenerator.getSineWave(this.tick, 8000, sound);
for (int i = 0; i < this.tick; i++) {
soundTickArray[i] = tick[i];
soundTockArray[i] = tock[i];
}
for (int i = 0; i < silence; i++)
silenceSoundArray[i] = 0;
}
public void play() {
calcSilence();
do {
msg = new Message();
msg.obj = "" + currentBeat;
if (currentBeat == 1)
audioGenerator.writeSound(soundTockArray);
else
audioGenerator.writeSound(soundTickArray);
if (bpm <= 120)
mHandler.sendMessage(msg);
audioGenerator.writeSound(silenceSoundArray);
if (bpm > 120)
mHandler.sendMessage(msg);
currentBeat++;
if (currentBeat > beat)
currentBeat = 1;
} while (play);
}
public void stop() {
play = false;
audioGenerator.destroyAudioTrack();
}
public double getBpm() {
return bpm;
}
public void setBpm(int bpm) {
this.bpm = bpm;
}
public int getNoteValue() {
return noteValue;
}
public void setNoteValue(int bpmetre) {
this.noteValue = bpmetre;
}
public int getBeat() {
return beat;
}
public void setBeat(int beat) {
this.beat = beat;
}
public double getBeatSound() {
return beatSound;
}
public void setBeatSound(double sound1) {
this.beatSound = sound1;
}
public double getSound() {
return sound;
}
public void setSound(double sound2) {
this.sound = sound2;
}
}
音频发生器:
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
public class AudioGenerator {
private int sampleRate;
private AudioTrack audioTrack;
public AudioGenerator(int sampleRate) {
this.sampleRate = sampleRate;
}
public double[] getSineWave(int samples,int sampleRate,double frequencyOfTone){
double[] sample = new double[samples];
for (int i = 0; i < samples; i++) {
sample[i] = Math.sin(2 * Math.PI * i / (sampleRate/frequencyOfTone));
}
return sample;
}
public byte[] get16BitPcm(double[] samples) {
byte[] generatedSound = new byte[2 * samples.length];
int index = 0;
for (double sample : samples) {
// scale to maximum amplitude
short maxSample = (short) ((sample * Short.MAX_VALUE));
// in 16 bit wav PCM, first byte is the low order byte
generatedSound[index++] = (byte) (maxSample & 0x00ff);
generatedSound[index++] = (byte) ((maxSample & 0xff00) >>> 8);
}
return generatedSound;
}
public void createPlayer(){
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate, AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT, sampleRate,
AudioTrack.MODE_STREAM);
audioTrack.play();
}
public void writeSound(double[] samples) {
byte[] generatedSnd = get16BitPcm(samples);
audioTrack.write(generatedSnd, 0, generatedSnd.length);
}
public void destroyAudioTrack() {
audioTrack.stop();
// This line seems to be a most likely culprit of the start/stop crash.
// Is this line even necessary?
audioTrack.release();
}
}
布局:
<?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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.boober.android_metronome.MainActivity">
<Button
android:id="@+id/playStopButton"
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:onClick="playStopButtonPressed"
android:text="Play"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/currentBeatTextView"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="32dp"
android:text="TextView"
android:gravity="center_vertical"
android:textAlignment="center"
android:textSize="30sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playStopButton" />
</android.support.constraint.ConstraintLayout>
经过思考dmarin's comment and reading the code, I arrive to the conclusion that dmarin回答了你的问题。这是一种竞争条件,也是对未初始化对象的访问。所以 short 解决方案是:代码需要检查访问的数据是否已初始化。如果 null
或 getState()
等于 "initialized",则可以检查 AudioTrack
个对象。不幸的是,问题并没有随着我的设置而消失(Android Studio 3.1.2,Android SDK Build-Tools 28-rc2)。
private boolean isInitialized() {
return audioTrack.getState() == AudioTrack.STATE_INITIALIZED;
}
经过代码分析后,您可能会注意到 AsyncTasks 和 AudioTracks 的创建。因此,为了最大限度地减少这些,只在 onCreate
函数中创建一次 AsyncTask,并将 AudioTrack
对象设置为 static
。
主要Activity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
currentBeat = findViewById(R.id.currentBeatTextView);
playStopButton = findViewById(R.id.playStopButton);
// important objcts
aSync = new MetronomeAsyncTask();
aSync.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null);
}
音频发生器
public class AudioGenerator {
/*changed to static*/
private static AudioTrack audioTrack;
...
}
我承认仅仅将其更改为静态并不是一个完美的解决方案。但由于我只需要一根管道连接到 AudioService,这样就可以了。
创建音频管道、停止播放音频并释放资源将如下所示:
public void createPlayer(){
if (audioTrack == null || ! isInitialized())
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate, AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT, sampleRate,
AudioTrack.MODE_STREAM);
if (isInitialized()){
audioTrack.play();
}
}
public void destroyAudioTrack() {
if (isInitialized()) {
audioTrack.stop();
}
}
public void stopRelease() {
if (isInitialized()) {
audioTrack.stop();
audioTrack.release();
}
}
布尔值 play
由我重新调整用途。此外,当按下播放按钮时,名为 currentBeat
的节拍计数器将被重置。对于从 MainActivity
访问:将这些变量从 private
更改为 public
不是最佳解决方案。
// only called from within playStopPressed()
private void stopPressed() {
aSync.metronome.play = false;
}
// only called from within playStopPressed()
private void playPressed() {
aSync.metronome.play = true;
aSync.metronome.currentBeat = 1;
}
在 MetronomeBrain
的 play()
中,循环变成了无限循环。这个问题很快就会得到解决。这就是 play
布尔值可能被重新利用的原因。音调的播放需要设置不同的条件,这取决于play
.
public void play() {
calcSilence();
/*a change for the do-while loop: It runs forever and needs
to be killed externally of the loop.
Also the play decides, if audio is being played.*/
do {
msg = new Message();
msg.obj = "" + currentBeat;
if (currentBeat == 1 && play)
audioGenerator.writeSound(soundTockArray);
else if (play)
audioGenerator.writeSound(soundTickArray);
if (bpm <= 120)
mHandler.sendMessage(msg);
audioGenerator.writeSound(silenceSoundArray);
if (bpm > 120)
mHandler.sendMessage(msg);
currentBeat++;
if (currentBeat > beat)
currentBeat = 1;
} while (true);
}
现在循环永远运行,但它可能只播放,如果 play
设置为 true
。如果有必要进行清理,可以在 Activity
生命周期结束时完成,就像 MainActivity
:
@Override
protected void onDestroy() {
aSync.metronome.stopReleaseAudio(); //calls the stopRelease()
aSync.cancel(true);
super.onDestroy();
}
正如我所说,代码可以进一步改进,但它提供了一个公平的提示和足够的 material 到 think/learn 关于 AsyncTasks,音频服务和 Activity - 生命周期.
参考资料
- https://developer.android.com/reference/android/os/AsyncTask
- https://developer.android.com/reference/android/media/AudioManager
- https://developer.android.com/reference/android/media/AudioTrack
- https://developer.android.com/reference/android/app/Activity#activity-lifecycle
TL;DR: 确保对象在访问它们之前已经初始化,只需创建一次所有内容,并在不需要时销毁它们,例如在 activity.
的末尾