TarsosDSP и SurfaceView проблема многопоточности
Я использую TarsosDSP для расчета частоты основного тона в режиме реального времени. Он использует AudioDispatcher, который реализует Runnable и публикует результаты с помощью метода handlePitch для использования в основном потоке.
Я использую SurfaceView, чтобы нарисовать это значение по мере его обновления. SurfaceView также требует другого потока для рисования на холсте. Итак, у меня есть 2 работоспособных объекта. Я не мог управлять, как обновить вид поверхности через один поток, получая значения основного тона из другого потока (audiodispatcher).
Я просто хочу использовать значение цента, которое я получаю в методе handlePitch(), чтобы обновить мой рисунок поверх вида поверхности. Но мое приложение зависает. Любая идея?
В MainAcitivity.java (onCreate(...))
myView = (MySurfaceView) findViewById(R.id.myview);
int sr = 44100;//The sample rate
int bs = 2048;
AudioDispatcher d = AudioDispatcherFactory.fromDefaultMicrophone(sr,bs,0);
PitchDetectionHandler printPitch = new PitchDetectionHandler() {
@Override
public void handlePitch(final PitchDetectionResult pitchDetectionResult, AudioEvent audioEvent) {
final float p = pitchDetectionResult.getPitch();
runOnUiThread(new Runnable() {
@Override
public void run() {
if (p != -1){
float cent = (float) (1200*Math.log(p/8.176)/Math.log(2)) % 12;
System.out.println(cent);
myView.setCent(cent);
}
}
});
}
};
PitchProcessor.PitchEstimationAlgorithm algo = PitchProcessor.PitchEstimationAlgorithm.YIN; //use YIN
AudioProcessor pitchEstimator = new PitchProcessor(algo, sr,bs,printPitch);
d.addAudioProcessor(pitchEstimator);
d.run();//starts the dispatching process
AudioProcessor p = new PitchProcessor(algo, sr, bs, printPitch);
d.addAudioProcessor(p);
new Thread(d,"Audio Dispatcher").start();
В SurfaceView.java (ниже код запускается из конструктора)
myThread = new MyThread(this);
surfaceHolder = getHolder();
bmpIcon = BitmapFactory.decodeResource(getResources(),
R.mipmap.ic_launcher);
iconWidth = bmpIcon.getWidth();
iconHeight = bmpIcon.getHeight();
density = getResources().getDisplayMetrics().scaledDensity;
setLabelTextSize(Math.round(DEFAULT_LABEL_TEXT_SIZE_DP * density));
surfaceHolder.addCallback(new SurfaceHolder.Callback(){
@Override
public void surfaceCreated(SurfaceHolder holder) {
myThread.setRunning(true);
myThread.start();
}
@Override
public void surfaceChanged(SurfaceHolder holder,
int format, int width, int height) {
// TODO Auto-generated method stub
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
boolean retry = true;
myThread.setRunning(false);
while (retry) {
try {
myThread.join();
retry = false;
} catch (InterruptedException e) {
}
}
}});
protected void drawSomething(Canvas canvas) {
updateCanvas(canvas, this.cent); //draws some lines depending on the cent value
}
public void setCent(double cent) {
if (this.cent > maxCent)
this.cent = maxCent;
this.cent = cent;
}
ОБНОВИТЬ:
MyThread.java
public class MyThread extends Thread {
MySurfaceView myView;
private boolean running = false;
public MyThread(MySurfaceView view) {
myView = view;
}
public void setRunning(boolean run) {
running = run;
}
@Override
public void run() {
while(running){
Canvas canvas = myView.getHolder().lockCanvas();
if(canvas != null){
synchronized (myView.getHolder()) {
myView.drawSomething(canvas);
}
myView.getHolder().unlockCanvasAndPost(canvas);
}
try {
sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
1 ответ
Если я правильно понимаю вашу проблему, у вас есть независимый источник событий, работающий в своем собственном потоке (PitchDetectionHandler
) и SurfaceView
что вы хотите перерисовать в своем собственном потоке, когда приходит событие из источника. Если это так, то я думаю, что вся идея с sleep(1000)
неправильно. Вы должны отслеживать реальные события и реагировать на них, а не спать, ожидая их. И кажется, что на Android самое простое решение - использовать HandlerThread
/ Looper
/ Handler
инфраструктура вроде так:
Остерегайтесь ошибок в следующем коде; я не только не пробовал, но даже не компилировал.
import android.graphics.Canvas;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.view.SurfaceHolder;
public class SurfacePitchDrawingHelper implements Handler.Callback, SurfaceHolder.Callback2 {
private static final int MSG_DRAW = 100;
private static final int MSG_FORCE_REDRAW = 101;
private final Object _lock = new Object();
private SurfaceHolder _surfaceHolder;
private HandlerThread _drawingThread;
private Handler _handler;
private float _lastDrawnCent;
private volatile float _lastCent;
private final boolean _processOnlyLast = true;
@Override
public void surfaceCreated(SurfaceHolder holder) {
synchronized (_lock) {
_surfaceHolder = holder;
_drawingThread = new HandlerThread("SurfaceDrawingThread") {
@Override
protected void onLooperPrepared() {
super.onLooperPrepared();
}
};
_drawingThread.start();
_handler = new Handler(_drawingThread.getLooper(), this); // <-- this is where bug was
_lastDrawnCent = Float.NaN;
//postForceRedraw(); // if needed
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
synchronized (_lock) {
// clean queue and kill looper
_handler.removeCallbacksAndMessages(null);
_drawingThread.getLooper().quit();
while (true) {
try {
_drawingThread.join();
break;
} catch (InterruptedException e) {
}
}
_handler = null;
_drawingThread = null;
_surfaceHolder = null;
}
}
@Override
public void surfaceRedrawNeeded(SurfaceHolder holder) {
postForceRedraw();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
synchronized (_lock) {
_surfaceHolder = holder;
}
postForceRedraw();
}
private void postForceRedraw() {
_handler.sendEmptyMessage(MSG_FORCE_REDRAW);
}
public void postRedraw(float cent) {
if (_processOnlyLast) {
_lastCent = cent;
_handler.sendEmptyMessage(MSG_DRAW);
} else {
Message message = _handler.obtainMessage(MSG_DRAW);
message.obj = Float.valueOf(cent);
_handler.sendMessage(message);
}
}
private void doRedraw(Canvas canvas, float cent) {
// put actual painting logic here
}
@Override
public boolean handleMessage(Message msg) {
float lastCent = _processOnlyLast ? _lastCent : ((Float) msg.obj).floatValue();
boolean shouldRedraw = (MSG_FORCE_REDRAW == msg.what)
|| ((MSG_DRAW == msg.what) && (_lastDrawnCent != lastCent));
if (shouldRedraw) {
Canvas canvas = null;
synchronized (_lock) {
if (_surfaceHolder != null)
canvas =_surfaceHolder.lockCanvas();
}
if (canvas != null) {
doRedraw(canvas, lastCent);
_surfaceHolder.unlockCanvasAndPost(canvas);
_lastDrawnCent = lastCent;
}
return true;
}
return false;
}
}
А потом в вашем классе деятельности вы делаете что-то вроде
private SurfaceView surfaceView;
private SurfacePitchDrawingHelper surfacePitchDrawingHelper = new SurfacePitchDrawingHelper();
...
@Override
protected void onCreate(Bundle savedInstanceState) {
...
surfaceView = (SurfaceView) findViewById(R.id.surfaceView);
surfaceView.getHolder().addCallback(surfacePitchDrawingHelper);
int sr = 44100;//The sample rate
int bs = 2048;
AudioDispatcher d = AudioDispatcherFactory.fromDefaultMicrophone(sr, bs, 0);
PitchDetectionHandler printPitch = new PitchDetectionHandler() {
@Override
public void handlePitch(final PitchDetectionResult pitchDetectionResult, AudioEvent audioEvent) {
final float p = pitchDetectionResult.getPitch();
float cent = (float) (1200 * Math.log(p / 8.176) / Math.log(2)) % 12;
System.out.println(cent);
surfacePitchDrawingHelper.postRedraw(cent);
}
};
PitchProcessor.PitchEstimationAlgorithm algo = PitchProcessor.PitchEstimationAlgorithm.YIN; //use YIN
AudioProcessor pitchEstimator = new PitchProcessor(algo, sr, bs, printPitch);
d.addAudioProcessor(pitchEstimator);
// d.run();//starts the dispatching process <-- this was another bug in the original code (see update)!
AudioProcessor p = new PitchProcessor(algo, sr, bs, printPitch);
d.addAudioProcessor(p);
new Thread(d, "Audio Dispatcher").start();
...
}
Обратите внимание, что SurfacePitchDrawingHelper
инкапсулирует большую часть логики, связанной с рисованием, и нет необходимости в вашем подклассе MySurfaceView
(что я считаю плохой идеей в любом случае).
Основная идея заключается в том, что SurfacePitchDrawingHelper
творения посвящены HandlerThread
когда новый Surface
создано. HandlerThread
+ Looper
+ Handler
обеспечить полезную инфраструктуру для выполнения (эффективным способом) бесконечного цикла в отдельном потоке, который ожидает входящие сообщения и обрабатывает их одно за другим. Так что его эффективный публичный API помимо SurfaceHolder.Callback2
состоит из одного postRedraw
метод, который может быть использован, чтобы попросить поток рисования сделать другую перерисовку, и это именно то, что используется обычным PitchDetectionHandler
, "Задание" выполняется путем помещения сообщения в очередь для обработки потоком рисования (точнее, нашим Handler
в этой теме). Я не стал сокращать реальный публичный API до "эффективного", потому что он делает код немного сложнее, и я слишком ленив. Но, конечно, оба "инструмента" могут быть перемещены во внутренние классы.
Вы должны принять одно важное решение: должен ли поток рисунков создавать каждое входящее сообщение (все cent
значения) в порядке появления или только самого последнего на данный момент рисования. Это может стать особенно важным в случае PitchDetectionHandler
генерирует события намного быстрее, чем "поток рисунков" может обновляться Surface
, Я считаю, что для большинства случаев вполне нормально обрабатывать только последнее значение из PitchDetectionHandler
но я оставил обе версии в коде для иллюстрации. Это различие в настоящее время реализовано в коде _processOnlyLast
поле. Скорее всего, вы должны принять это решение и просто избавиться от этого почти постоянного поля и кода в нерелевантных ветвях.
И, конечно же, не забудьте поместить свою фактическую логику рисования внутрь doRedraw
Обновление (почему кнопка "Назад" не работает)
Версия TLDR
Оскорбительная строка
d.run();//starts the dispatching process
Просто закомментируйте это!
Более длинная версия
Глядя на ваш пример, мы видим, что d
является AudioDispatcher
который implements Runnable
и поэтому run
Метод - это метод, который вызывается в новом потоке. Вы можете заметить, что это важно, потому что внутри этого метода выполняется некоторый ввод-вывод и блокируется поток, на котором он работает. Так что в вашем случае он заблокировал основной поток пользовательского интерфейса. Несколько строк вниз вы делаете
new Thread(d, "Audio Dispatcher").start();
и это, кажется, правильный способ использования AudioDispatcher
Это легко увидеть по следам стека, которые я просил в комментариях.