From 60d2cb7e30aacfe53f3335e5ca2ba256a38eca4f Mon Sep 17 00:00:00 2001 From: Mikhail Naganov Date: Tue, 10 Mar 2020 18:37:35 -0700 Subject: [PATCH] EffectsTest: Add multi-threaded client for Visualizer Adding a multi-thread client helps to unveil problems with concurrent use of VisualizerEffect. Bug: 149375271 Test: enable "Multithreaded Use" in Visualizer Test Change-Id: Id485790dc7e91fde5dc5844a287645e96f8ec253 --- .../EffectsTest/res/layout/visualizertest.xml | 31 ++++ .../tests/EffectsTest/res/values/strings.xml | 2 + .../effectstest/VisualizerInstance.java | 25 +++ .../effectstest/VisualizerInstanceMT.java | 113 ++++++++++++ .../effectstest/VisualizerInstanceSync.java | 170 ++++++++++++++++++ .../android/effectstest/VisualizerTest.java | 167 +++++------------ 6 files changed, 385 insertions(+), 123 deletions(-) create mode 100644 media/tests/EffectsTest/src/com/android/effectstest/VisualizerInstance.java create mode 100644 media/tests/EffectsTest/src/com/android/effectstest/VisualizerInstanceMT.java create mode 100644 media/tests/EffectsTest/src/com/android/effectstest/VisualizerInstanceSync.java diff --git a/media/tests/EffectsTest/res/layout/visualizertest.xml b/media/tests/EffectsTest/res/layout/visualizertest.xml index 50ac7bbd8cd05..18d7a3648fbfe 100644 --- a/media/tests/EffectsTest/res/layout/visualizertest.xml +++ b/media/tests/EffectsTest/res/layout/visualizertest.xml @@ -50,6 +50,37 @@ + + + + + + + + + + Attach Detach Send Level + + Multithreaded Use diff --git a/media/tests/EffectsTest/src/com/android/effectstest/VisualizerInstance.java b/media/tests/EffectsTest/src/com/android/effectstest/VisualizerInstance.java new file mode 100644 index 0000000000000..817bd3d7bf5ea --- /dev/null +++ b/media/tests/EffectsTest/src/com/android/effectstest/VisualizerInstance.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.effectstest; + +interface VisualizerInstance { + void enableDataCaptureListener(boolean enable); + boolean getEnabled(); + void release(); + void setEnabled(boolean enabled); + void startStopCapture(boolean start); +} diff --git a/media/tests/EffectsTest/src/com/android/effectstest/VisualizerInstanceMT.java b/media/tests/EffectsTest/src/com/android/effectstest/VisualizerInstanceMT.java new file mode 100644 index 0000000000000..89cfbebe17cef --- /dev/null +++ b/media/tests/EffectsTest/src/com/android/effectstest/VisualizerInstanceMT.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.effectstest; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; + +class VisualizerInstanceMT implements VisualizerInstance { + + private static final String TAG = "VisualizerInstanceMT"; + + private final Object mLock = new Object(); + private final int mThreadCount; + @GuardedBy("mLock") + private Handler mVisualizerHandler; + @GuardedBy("mLock") + private VisualizerInstanceSync mVisualizer; + + VisualizerInstanceMT(int session, Handler uiHandler, int extraThreadCount) { + Log.d(TAG, "Multi-threaded constructor"); + mThreadCount = 1 + extraThreadCount; + Thread t = new Thread() { + @Override public void run() { + Looper.prepare(); + VisualizerInstanceSync v = new VisualizerInstanceSync(session, uiHandler); + synchronized (mLock) { + mVisualizerHandler = new Handler(); + mVisualizer = v; + } + Looper.loop(); + } + }; + t.start(); + } + + private VisualizerInstance getVisualizer() { + synchronized (mLock) { + return mVisualizer != null ? new VisualizerInstanceSync(mVisualizer) : null; + } + } + + private interface VisualizerOperation { + void run(VisualizerInstance v); + } + + private void runOperationMt(VisualizerOperation op) { + final VisualizerInstance v = getVisualizer(); + if (v == null) return; + for (int i = 0; i < mThreadCount; ++i) { + Thread t = new Thread() { + @Override + public void run() { + op.run(v); + } + }; + t.start(); + } + } + + @Override + public void enableDataCaptureListener(boolean enable) { + runOperationMt(v -> v.enableDataCaptureListener(enable)); + } + + @Override + public boolean getEnabled() { + final VisualizerInstance v = getVisualizer(); + return v != null ? v.getEnabled() : false; + } + + @Override + public void release() { + runOperationMt(v -> v.release()); + synchronized (mLock) { + if (mVisualizerHandler == null) return; + mVisualizerHandler.post(() -> { + synchronized (mLock) { + mVisualizerHandler = null; + mVisualizer = null; + Looper.myLooper().quitSafely(); + } + Log.d(TAG, "Exiting looper"); + }); + } + } + + @Override + public void setEnabled(boolean enabled) { + runOperationMt(v -> v.setEnabled(enabled)); + } + + @Override + public void startStopCapture(boolean start) { + runOperationMt(v -> v.startStopCapture(start)); + } +} diff --git a/media/tests/EffectsTest/src/com/android/effectstest/VisualizerInstanceSync.java b/media/tests/EffectsTest/src/com/android/effectstest/VisualizerInstanceSync.java new file mode 100644 index 0000000000000..e64f4e5785c83 --- /dev/null +++ b/media/tests/EffectsTest/src/com/android/effectstest/VisualizerInstanceSync.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.effectstest; + +import android.media.audiofx.Visualizer; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +// This class only has `final' members, thus any thread-safety concerns +// can only come from the Visualizer effect class. +class VisualizerInstanceSync implements VisualizerInstance { + + private static final String TAG = "VisualizerInstance"; + + private final Handler mUiHandler; + private final Visualizer mVisualizer; + private final VisualizerTestHandler mVisualizerTestHandler; + private final VisualizerListener mVisualizerListener; + + VisualizerInstanceSync(int session, Handler uiHandler) { + mUiHandler = uiHandler; + try { + mVisualizer = new Visualizer(session); + } catch (UnsupportedOperationException e) { + Log.e(TAG, "Visualizer library not loaded"); + throw new RuntimeException("Cannot initialize effect"); + } catch (RuntimeException e) { + throw e; + } + mVisualizerTestHandler = new VisualizerTestHandler(); + mVisualizerListener = new VisualizerListener(); + } + + // Not a "deep" copy, only copies the references. + VisualizerInstanceSync(VisualizerInstanceSync other) { + mUiHandler = other.mUiHandler; + mVisualizer = other.mVisualizer; + mVisualizerTestHandler = other.mVisualizerTestHandler; + mVisualizerListener = other.mVisualizerListener; + } + + @Override + public void enableDataCaptureListener(boolean enable) { + mVisualizer.setDataCaptureListener(enable ? mVisualizerListener : null, + 10000, enable, enable); + } + + @Override + public boolean getEnabled() { + return mVisualizer.getEnabled(); + } + + @Override + public void release() { + mVisualizer.release(); + Log.d(TAG, "Visualizer released"); + } + + @Override + public void setEnabled(boolean enabled) { + mVisualizer.setEnabled(enabled); + } + + @Override + public void startStopCapture(boolean start) { + mVisualizerTestHandler.sendMessage(mVisualizerTestHandler.obtainMessage( + start ? MSG_START_CAPTURE : MSG_STOP_CAPTURE)); + } + + private static final int MSG_START_CAPTURE = 0; + private static final int MSG_STOP_CAPTURE = 1; + private static final int MSG_NEW_CAPTURE = 2; + private static final int CAPTURE_PERIOD_MS = 100; + + private static int[] dataToMinMaxCenter(byte[] data, int len) { + int[] minMaxCenter = new int[3]; + minMaxCenter[0] = data[0]; + minMaxCenter[1] = data[len - 1]; + minMaxCenter[2] = data[len / 2]; + return minMaxCenter; + } + + private class VisualizerTestHandler extends Handler { + private final int mCaptureSize; + private boolean mActive = false; + + VisualizerTestHandler() { + mCaptureSize = mVisualizer.getCaptureSize(); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_START_CAPTURE: + if (!mActive) { + Log.d(TAG, "Start capture"); + mActive = true; + sendMessageDelayed(obtainMessage(MSG_NEW_CAPTURE), CAPTURE_PERIOD_MS); + } + break; + case MSG_STOP_CAPTURE: + if (mActive) { + Log.d(TAG, "Stop capture"); + mActive = false; + } + break; + case MSG_NEW_CAPTURE: + if (mActive) { + if (mCaptureSize > 0) { + byte[] data = new byte[mCaptureSize]; + if (mVisualizer.getWaveForm(data) == Visualizer.SUCCESS) { + int len = data.length < mCaptureSize ? data.length : mCaptureSize; + mUiHandler.sendMessage( + mUiHandler.obtainMessage( + VisualizerTest.MSG_DISPLAY_WAVEFORM_VAL, + dataToMinMaxCenter(data, len))); + } + if (mVisualizer.getFft(data) == Visualizer.SUCCESS) { + int len = data.length < mCaptureSize ? data.length : mCaptureSize; + mUiHandler.sendMessage( + mUiHandler.obtainMessage(VisualizerTest.MSG_DISPLAY_FFT_VAL, + dataToMinMaxCenter(data, len))); + } + } + sendMessageDelayed(obtainMessage(MSG_NEW_CAPTURE), CAPTURE_PERIOD_MS); + } + break; + } + } + } + + private class VisualizerListener implements Visualizer.OnDataCaptureListener { + @Override + public void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, + int samplingRate) { + if (visualizer == mVisualizer && waveform.length > 0) { + Log.d(TAG, "onWaveFormDataCapture(): " + waveform[0] + + " smp rate: " + samplingRate / 1000); + mUiHandler.sendMessage( + mUiHandler.obtainMessage(VisualizerTest.MSG_DISPLAY_WAVEFORM_VAL, + dataToMinMaxCenter(waveform, waveform.length))); + } + } + + @Override + public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) { + if (visualizer == mVisualizer && fft.length > 0) { + Log.d(TAG, "onFftDataCapture(): " + fft[0]); + mUiHandler.sendMessage( + mUiHandler.obtainMessage(VisualizerTest.MSG_DISPLAY_FFT_VAL, + dataToMinMaxCenter(fft, fft.length))); + } + } + } +} diff --git a/media/tests/EffectsTest/src/com/android/effectstest/VisualizerTest.java b/media/tests/EffectsTest/src/com/android/effectstest/VisualizerTest.java index 7db1d8d8625e7..2e141c5ef7c8a 100644 --- a/media/tests/EffectsTest/src/com/android/effectstest/VisualizerTest.java +++ b/media/tests/EffectsTest/src/com/android/effectstest/VisualizerTest.java @@ -17,51 +17,42 @@ package com.android.effectstest; import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.media.audiofx.Visualizer; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Log; import android.view.KeyEvent; -import android.view.Menu; import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.Button; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.EditText; import android.widget.TextView; import android.widget.ToggleButton; -import android.widget.SeekBar; -import java.nio.ByteOrder; -import java.nio.ByteBuffer; import java.util.HashMap; -import java.util.Map; public class VisualizerTest extends Activity implements OnCheckedChangeListener { private final static String TAG = "Visualizer Test"; - private Visualizer mVisualizer; + private VisualizerInstance mVisualizer; + ToggleButton mMultithreadedButton; ToggleButton mOnOffButton; ToggleButton mReleaseButton; + boolean mUseMTInstance; boolean mEnabled; EditText mSessionText; static int sSession = 0; - int mCaptureSize; ToggleButton mCallbackButton; boolean mCallbackOn; - VisualizerListener mVisualizerListener; - private static HashMap sInstances = new HashMap(10); - private VisualizerTestHandler mVisualizerTestHandler = null; + private static HashMap sInstances = + new HashMap(10); + private Handler mUiHandler; public VisualizerTest() { Log.d(TAG, "contructor"); + mUiHandler = new UiHandler(Looper.getMainLooper()); } @Override @@ -76,109 +67,45 @@ public class VisualizerTest extends Activity implements OnCheckedChangeListener mSessionText.setOnKeyListener(mSessionKeyListener); mSessionText.setText(Integer.toString(sSession)); - mReleaseButton = (ToggleButton)findViewById(R.id.visuReleaseButton); - mOnOffButton = (ToggleButton)findViewById(R.id.visualizerOnOff); - mCallbackButton = (ToggleButton)findViewById(R.id.visuCallbackOnOff); + mMultithreadedButton = (ToggleButton) findViewById(R.id.visuMultithreadedOnOff); + mReleaseButton = (ToggleButton) findViewById(R.id.visuReleaseButton); + mOnOffButton = (ToggleButton) findViewById(R.id.visualizerOnOff); + mCallbackButton = (ToggleButton) findViewById(R.id.visuCallbackOnOff); mCallbackOn = false; mCallbackButton.setChecked(mCallbackOn); - mVisualizerTestHandler = new VisualizerTestHandler(); - mVisualizerListener = new VisualizerListener(); - - getEffect(sSession); - - if (mVisualizer != null) { + mMultithreadedButton.setOnCheckedChangeListener(this); + if (getEffect(sSession) != null) { mReleaseButton.setOnCheckedChangeListener(this); mOnOffButton.setOnCheckedChangeListener(this); mCallbackButton.setOnCheckedChangeListener(this); } } - private static final int MSG_START_CAPTURE = 0; - private static final int MSG_STOP_CAPTURE = 1; - private static final int MSG_NEW_CAPTURE = 2; - private static final int CAPTURE_PERIOD_MS = 100; + public static final int MSG_DISPLAY_WAVEFORM_VAL = 0; + public static final int MSG_DISPLAY_FFT_VAL = 1; + + private class UiHandler extends Handler { + UiHandler(Looper looper) { + super(looper); + } - private class VisualizerTestHandler extends Handler { - boolean mActive = false; @Override public void handleMessage(Message msg) { switch (msg.what) { - case MSG_START_CAPTURE: - if (!mActive) { - Log.d(TAG, "Start capture"); - mActive = true; - sendMessageDelayed(obtainMessage(MSG_NEW_CAPTURE, 0, 0, null), CAPTURE_PERIOD_MS); + case MSG_DISPLAY_WAVEFORM_VAL: + case MSG_DISPLAY_FFT_VAL: + int[] minMaxCenter = (int[]) msg.obj; + boolean waveform = msg.what == MSG_DISPLAY_WAVEFORM_VAL; + displayVal(waveform ? R.id.waveformMin : R.id.fftMin, minMaxCenter[0]); + displayVal(waveform ? R.id.waveformMax : R.id.fftMax, minMaxCenter[1]); + displayVal(waveform ? R.id.waveformCenter : R.id.fftCenter, minMaxCenter[2]); + break; } - break; - case MSG_STOP_CAPTURE: - if (mActive) { - Log.d(TAG, "Stop capture"); - mActive = false; - } - break; - case MSG_NEW_CAPTURE: - if (mActive && mVisualizer != null) { - if (mCaptureSize > 0) { - byte[] data = new byte[mCaptureSize]; - if (mVisualizer.getWaveForm(data) == Visualizer.SUCCESS) { - int len = data.length < mCaptureSize ? data.length : mCaptureSize; - displayVal(R.id.waveformMin, data[0]); - displayVal(R.id.waveformMax, data[len-1]); - displayVal(R.id.waveformCenter, data[len/2]); - }; - if (mVisualizer.getFft(data) == Visualizer.SUCCESS) { - int len = data.length < mCaptureSize ? data.length : mCaptureSize; - displayVal(R.id.fftMin, data[0]); - displayVal(R.id.fftMax, data[len-1]); - displayVal(R.id.fftCenter, data[len/2]); - }; - } - sendMessageDelayed(obtainMessage(MSG_NEW_CAPTURE, 0, 0, null), CAPTURE_PERIOD_MS); - } - break; - } } } - private class VisualizerListener implements Visualizer.OnDataCaptureListener { - - public VisualizerListener() { - } - public void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, int samplingRate) { - if (visualizer == mVisualizer) { - if (waveform.length > 0) { - Log.d(TAG, "onWaveFormDataCapture(): "+waveform[0]+" smp rate: "+samplingRate/1000); - displayVal(R.id.waveformMin, waveform[0]); - displayVal(R.id.waveformMax, waveform[waveform.length - 1]); - displayVal(R.id.waveformCenter, waveform[waveform.length/2]); - } - } - } - public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) { - if (visualizer == mVisualizer) { - if (fft.length > 0) { - Log.d(TAG, "onFftDataCapture(): "+fft[0]); - displayVal(R.id.fftMin, fft[0]); - displayVal(R.id.fftMax, fft[fft.length - 1]); - displayVal(R.id.fftCenter, fft[fft.length/2]); - } - } - } - } - - @Override - public void onResume() { - super.onResume(); - } - - @Override - public void onPause() { - super.onPause(); - } - - private View.OnKeyListener mSessionKeyListener - = new View.OnKeyListener() { + private View.OnKeyListener mSessionKeyListener = new View.OnKeyListener() { public boolean onKey(View v, int keyCode, KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { switch (keyCode) { @@ -199,29 +126,26 @@ public class VisualizerTest extends Activity implements OnCheckedChangeListener }; // OnCheckedChangeListener + @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (buttonView.getId() == R.id.visuMultithreadedOnOff) { + mUseMTInstance = isChecked; + Log.d(TAG, "Multi-threaded client: " + (isChecked ? "enabled" : "disabled")); + } if (buttonView.getId() == R.id.visualizerOnOff) { if (mVisualizer != null) { mEnabled = isChecked; mCallbackButton.setEnabled(!mEnabled); if (mCallbackOn && mEnabled) { - mVisualizer.setDataCaptureListener(mVisualizerListener, - 10000, - true, - true); + mVisualizer.enableDataCaptureListener(true); } mVisualizer.setEnabled(mEnabled); if (mCallbackOn) { if (!mEnabled) { - mVisualizer.setDataCaptureListener(null, - 10000, - false, - false); + mVisualizer.enableDataCaptureListener(false); } } else { - int msg = isChecked ? MSG_START_CAPTURE : MSG_STOP_CAPTURE; - mVisualizerTestHandler.sendMessage( - mVisualizerTestHandler.obtainMessage(msg, 0, 0, null)); + mVisualizer.startStopCapture(isChecked); } } } @@ -248,16 +172,15 @@ public class VisualizerTest extends Activity implements OnCheckedChangeListener } - private void getEffect(int session) { + private VisualizerInstance getEffect(int session) { synchronized (sInstances) { if (sInstances.containsKey(session)) { mVisualizer = sInstances.get(session); } else { - try{ - mVisualizer = new Visualizer(session); - } catch (UnsupportedOperationException e) { - Log.e(TAG,"Visualizer library not loaded"); - throw (new RuntimeException("Cannot initialize effect")); + try { + mVisualizer = mUseMTInstance + ? new VisualizerInstanceMT(session, mUiHandler, 0 /*extraThreadCount*/) + : new VisualizerInstanceSync(session, mUiHandler); } catch (RuntimeException e) { throw e; } @@ -267,8 +190,6 @@ public class VisualizerTest extends Activity implements OnCheckedChangeListener mReleaseButton.setEnabled(false); mOnOffButton.setEnabled(false); if (mVisualizer != null) { - mCaptureSize = mVisualizer.getCaptureSize(); - mReleaseButton.setChecked(true); mReleaseButton.setEnabled(true); @@ -278,6 +199,7 @@ public class VisualizerTest extends Activity implements OnCheckedChangeListener mCallbackButton.setEnabled(!mEnabled); } + return mVisualizer; } private void putEffect(int session) { @@ -286,9 +208,8 @@ public class VisualizerTest extends Activity implements OnCheckedChangeListener synchronized (sInstances) { if (mVisualizer != null) { mVisualizer.release(); - Log.d(TAG,"Visualizer released"); - mVisualizer = null; sInstances.remove(session); + mVisualizer = null; } } }