TarsosDSP和SurfaceView多线程问题

6
我正在使用TarsosDSP进行实时音高频率的计算。它使用一个AudioDispatcher实现Runnable并通过handlePitch方法发布结果以便在主线程中使用。
我正在使用SurfaceView绘制这个值,因为它会更新。SurfaceView还需要另一个线程来在画布上绘制。所以我有两个可运行对象。我无法控制如何在一个线程中更新surfaceview,同时从另一个线程(audiodispatcher)获取唱片值。
我只想使用handlePitch()方法中获得的cent值来更新我的绘图。但是我的应用会冻结。有什么想法吗?
在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;
    }

UPDATE:

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
一件事:在 setCent 中,if 应该在赋值之后。另一件事:你的 MyThread 是否在调用 drawSomething - eduyayo
感谢setCent。我已经添加了MyThread.java文件。它正在调用drawSomething函数。问题是如何使用通过handlePitch事件获得的cent值。 - ugur
该值应为volatile,或通过getter访问,以获取该值的正确副本。 - eduyayo
1个回答

2
如果我正确理解您的问题,您有一个独立的事件源在其自己的线程上工作(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(我认为这是一个不好的想法)没有必要重复实现。

主要思路是,在创建新的 Surface 时,SurfacePitchDrawingHelper 创建了一个专用的 HandlerThreadHandlerThread + Looper + Handler 提供了一个有用的基础设施,可以在单独的线程上以高效的方式运行无限循环,等待传入的消息并逐个处理它们。 因此,除了 SurfaceHolder.Callback2 之外,其有效的公共 API 包括一个单一的 postRedraw 方法,可用于请求绘图线程进行另一次重绘,这正是自定义 PitchDetectionHandler 使用的方法。 "询问" 是通过将消息放入队列中以由绘图线程处理(更具体地说是在该线程上的自定义 Handler 处理)来完成的。 我没有费心将真正的公共 API 减少到 "有效" 的 API,因为这会使代码变得有点复杂,而我太懒了。 但是当然,两个 "implements" 都可以移动到内部类中。

您需要做出一个重要的决定:绘制线程是否应按照它们到达的顺序生成每个入站消息(所有 cent 值),还是仅在绘制发生时生成最新的消息。如果 PitchDetectionHandler 产生的事件比 "绘图线程" 可以更新 Surface 的速度快得多,则这可能变得特别重要。 我认为,对于大多数情况,仅处理来自 PitchDetectionHandler 的最后一个值就可以了,但我在代码中保留了两个版本以进行说明。 这种区别当前通过 _processOnlyLast 字段在代码中实现。 您很可能应该做出这个决定并摆脱几乎恒定的字段以及无关分支中的代码。

当然,不要忘记将您的实际绘图逻辑放在 doRedraw


更新(为什么返回按钮不起作用)

TLDR 版本

有问题的行是

 d.run();//starts the dispatching process

只需要将其注释掉!

更详细的版本

从您的示例中可以看出,dAudioDispatcher,它实现了Runnable接口,因此run方法是在新线程上调用的方法。您可能会注意到这很重要,因为该方法内部执行一些IO操作并阻塞运行的线程。因此,在您的情况下,它会阻塞主UI线程。在几行代码之后,您可以执行以下操作:

new Thread(d, "Audio Dispatcher").start();

这似乎是使用AudioDispatcher的正确方法。

这可以从我在评论中询问的堆栈跟踪中轻松看出。

主线程的堆栈跟踪


我在doRedraw方法中尝试了一些简单的绘图,但是屏幕变成了白色,当我按下手机上的返回按钮时它没有响应。我将尝试进行调试并回复您。 - ugur
@uguboz,“surfaceCreated没有被调用”是个大惊喜。您确定没有忘记执行surfaceView.getHolder().addCallback(surfacePitchDrawingHelper);吗?这在ActivityonCreate方法中完成了吗? - SergGr
@uguboz,我不确定您是如何使surfaceCreated永远不被调用的,但我在_handler = new Handler(_drawingThread.getLooper(), this);这一行中发现并修复了一个关键性错误。最初没有将this作为回调传递给Handler构造函数,因此在HandlerThread上没有执行任何工作。如果问题仍未解决,请告诉我。 - SergGr
@uguboz,我可以轻松地修改我的示例以启动一个新的“线程”,并在每秒钟更改颜色,它完美地工作。我的意思是说,你目前遇到的问题很可能与SurfaceView无关,而是与其他代码有关。我正在尝试获取更多信息,但你似乎不愿提供任何额外的信息。没有这样的信息,我不知道如何帮助你。最小化、完整化和可验证化的示例会非常好,但不是必需的。仍然需要一些额外的信息来帮助你! - SergGr
这是我的完整代码:https://yadi.sk/d/LGhOL-Lo3Fj4BL。当我将SurfaceView和Pitch线程分开时,两者都可以正常工作。 - ugur
显示剩余12条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接