6

I am using TarsosDSP to calculate pitch frequencies in real time. It uses an AudioDispatcher which implements Runnable and post the results via handlePitch method to make use of in the main thread.

I am using SurfaceView to draw this value as it updates. SurfaceView also requires another thread to draw on canvas. So I have 2 runnable objects. I couldnt manage how to update surface view via one thread while getting the pitch values from another thread (audiodispatcher).

I just want to use cent value which I get in the handlePitch() method to update my drawing over surfaceview. But my app freezes. Any idea?

In 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();

In SurfaceView.java (below code is triggered from the constructor)

    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();
            }

        }
    }
ugur
  • 3,604
  • 3
  • 26
  • 57
  • 1
    one thing: in `setCent` the `if` should go after the assingment. Other thing: Is your `MyThread` calling `drawSomething`? – eduyayo Mar 09 '17 at 09:47
  • Thanks for setCent. And i added MyThread.java file. It s calling drawSomething. The problem is how to use cent value which I get via handlePitch event. – ugur Mar 09 '17 at 21:09
  • The value should be volatile or accesed with a getter so you get a propper copy of the value – eduyayo Mar 09 '17 at 22:38

1 Answers1

2

If I understand your problem correctly, you have an independent source of events working on its own thread (PitchDetectionHandler) and a SurfaceView that you want to re-paint on its own thread when the event from the source comes. If this is the case, then I think the whole idea with sleep(1000) is wrong. You should track actual events and react to them rather than sleep waiting for them. And it seems that on Android the easiest solution is to use HandlerThread/Looper/Handler infrastructure like so:

Beware of the bugs in the following code; not only haven't I tried it but I even haven't compiled it.

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;
    }
}

And then in your activity class you do something like

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();

    ...

}

Note that SurfacePitchDrawingHelper encapsulates most of the logic related to drawing and there is no need in your subclass MySurfaceView (which I think is a bad idea anyway).

The main idea is that SurfacePitchDrawingHelper creates are dedicated HandlerThread when new Surface is created. HandlerThread + Looper + Handler provide a useful infrastructure of running (in an efficient way) an infinite loop on a separate thread that waits for incoming messages and handles them one by one. So its effective public API besides SurfaceHolder.Callback2 consists of single postRedraw method that might be used to ask the drawing thread to do another redraw and this is exactly what is used by custom PitchDetectionHandler. "Asking" is done by putting a message in the queue to be processed by the drawing thread (more specifically our custom Handler on that thread). I didn't bother with reducing real public API to "effective" one because it makes code a bit more complicated and I'm too lazy. But of course both "implements" can be moved to inner classes.

There is one important decision to be made by you: whether drawing thread should produce each inbound message (all cent values) in order the came or just the latest at the moment drawing occurs. This might become especially important in case PitchDetectionHandler produces events much faster then the "drawing thread" can update Surface. I believe that for most of the cases it is OK to handle just the last value from the PitchDetectionHandler but I left both version in the code for illustration. This distinction is currently implemented in the code by _processOnlyLast field. Most probably you should make this decision ones and just get rid of this almost-constant-field and the code in the irrelevant branches.

And of course don't forget to put your actual drawing logic inside doRedraw


Update (why Back button doesn't work)

TLDR version

The offending line is

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

Just comment it out!

Longer version

Looking at your example we can see that d is AudioDispatcher which implements Runnable and thus run method is a method to be called on a new thread. You may notice that this is important because inside this method does some IO and blocks the thread it runs on. So in your case it blocked the main UI thread. A few lines down you do

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

and this seems to be a correct way to use AudioDispatcher

This can easily be seen from the stack traces that I asked in the comments for.

Stack trace of the main thread

SergGr
  • 23,570
  • 2
  • 30
  • 51
  • It still blocks the main thread. Not working :( Thanks for your detailed answer anyway. – ugur Mar 11 '17 at 17:39
  • @uguboz, could you provide some details please? How do you see that it blocks UI thread? Have you tried to debug it and stop all threads ("break all")? What is the stack trace of the main/UI thread? – SergGr Mar 11 '17 at 18:40
  • I tried some simple drawing in doRedraw method but i get white screen and when i pressed back button on my phone it didnt respond. I will try debugging and get back to u. – ugur Mar 11 '17 at 19:29
  • Hey i debugged it and see that surfaceCreated is never called – ugur Mar 11 '17 at 19:56
  • @uguboz, "surfaceCreated is never called" is a big surprise. Are you sure you didn't forget to do `surfaceView.getHolder().addCallback(surfacePitchDrawingHelper);`? Is it done in `Activity`'s `onCreate`? – SergGr Mar 11 '17 at 23:51
  • Exactly i called it in onCreate – ugur Mar 12 '17 at 14:45
  • @uguboz, I'm not sure how you get `surfaceCreated` to be never called but I found and fixed a critical bug in line `_handler = new Handler(_drawingThread.getLooper(), this);`. Originally `this` was not passed as a callback to the `Handler` constructor and thus no work was done on the `HandlerThread`. Please, let me know if this still doesn't fix your issue. – SergGr Mar 12 '17 at 20:42
  • Still same. I got no drawing and doesnt respond to my back button action – ugur Mar 12 '17 at 23:12
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/137906/discussion-between-serggr-and-uguboz). – SergGr Mar 12 '17 at 23:21
  • @uguboz Well, you seem to be unavailable for a chat. I strongly suspect that you have some **other issue** in the code that block UI thread and it is almost impossible to dig it out without more details for you (this is why [MCVE](http://stackoverflow.com/help/mcve) is so important. As for me I can say that the code in my answer works for me at least in a simple example as you can see in the test project I uploaded to http://dropmefiles.com/BXQiH I still suggest running a debugger and using "Pause Program" aka "Break All" to find out actual stack trace and particularly what UI thread is doing – SergGr Mar 13 '17 at 00:15
  • Your code is working but it redraws when button onClick. What i need is an another thread to update the variable instead of button pressed. The problem appears here. – ugur Mar 13 '17 at 18:58
  • @uguboz, I can easily modify my example to start a new `Thread` and change color each second and it works perfectly well. What I'm trying to say is that your current issues are most probably related to **some other code** than `SurfaceView` and I'm trying to get more info about it but you seem very reluctant to provide any additional information. Without such information I don't know how I can help you. [Minimal, Complete, and Verifiable example](http://stackoverflow.com/help/mcve) would be very nice but is not required. Still I need at least some additional information to help you! – SergGr Mar 13 '17 at 20:51
  • @uguboz, There is one simple test you can do if you are using my code. Just comment out all the code that mentions `SurfacePitchDrawingHelper` class and `surfacePitchDrawingHelper` variable from your `Activity`. There should be around 5 lines of such code (and yes, comment out very minimal amount of code not whole methods). Obviously `SurfaceView` will not be updated but the important quesetion is: does the "Back" button work now? I suspect that it still don't, and if so, the issue is somewhere else. – SergGr Mar 13 '17 at 20:59
  • Here is my full code https://yadi.sk/d/LGhOL-Lo3Fj4BL. Both works fine when i seperate the surfaceview and pitch thread. – ugur Mar 13 '17 at 21:46
  • @uguboz, see my update with description of the your issue with blocking. P.S. If you were a bit more cooperative I'd still got a change to win your bounty ;) – SergGr Mar 13 '17 at 23:01
  • I ve been very busy, sorry I will award more bounty if it works. I will try asap. – ugur Mar 14 '17 at 06:54
  • It works great. Thanks man! i will be able to award you within 23hours. – ugur Mar 14 '17 at 17:37