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

Это легко увидеть по следам стека, которые я просил в комментариях.

След стека основной нити

Другие вопросы по тегам