Search code examples
androidxamarinmvvmcross

Java.Lang.JavaSystem.LoadLibrary(...) fails after moving code to async/await


I am using pocketsphinx in a Xamarin Android app (using MVVMCross) for speech recognition. It works great when I use it in the View, and it works great when I moved it from the View into a plugin so that I could use it in the ViewModel (using callback event handlers).

Now I am refactoring (again!) to use async/await, and the app fails to load the pocketsphinx library. My code is below. When I reach the call

Java.Lang.JavaSystem.LoadLibrary("pocketsphinx_jni");

I get an exception:

Java.Lang.UnsatisfiedLinkError: Library pocketsphinx_jni not found; tried [/vendor/lib/libpocketsphinx_jni.so, /system/lib/libpocket...

I've tried a combination of things using Application.Context.ApplicationInfo.NativeLibraryDir and Java.Lang.JavaSystem.Load but no joy.

Any ideas on why this would start to happen now? Presumably something to do with threading, but I am stumped as to how to solve the issue. All of this code worked fine before I started refactoring to use async/await.

The call in my ViewModel is:

var result = await _speechRecognitionService.ListenAsync(delay);

And here is my SpeechRecognitionService class:

public class SpeechRecognitionService : Java.Lang.Object, 
    ISpeechRecognitionService, IRecognitionListener
{
    private SpeechRecognizer _recognizer;
    private Java.IO.File _assetDir;
    private bool _isHandlingResponse;
    private bool _isInitialised;
    private static EventWaitHandle _waitHandle = new AutoResetEvent(false);
    private string _response;

    public event EventHandler HeardSpeechEventHandler;

    public bool IsInitialised()
    {
        return _isInitialised;
    }

    /// <summary>
    /// Hopefully fire off the listen asynchronously
    /// </summary>
    /// <param name="delay"></param>
    /// <returns></returns>
    public async Task<string> ListenAsync(int delay)
    {
        return await Task.Run(() => DoListen(delay));
    }

    /// <summary>
    /// Initialise if required then fire up the listener
    /// </summary>
    /// <param name="delay"></param>
    /// <returns></returns>
    private string DoListen(int delay)
    {
        if (!IsInitialised())
        {
            Init();
        }
        Listen(delay);
        return "";
    }

    /// <summary>
    /// Set up the config for the listener
    /// </summary>
    public void Init()
    {
        Console.WriteLine("SpeechRecognitionIntentService ====== Init");

        try
        {
            // THIS CALL FAILS!
            Java.Lang.JavaSystem.LoadLibrary("pocketsphinx_jni");
            _assetDir = Edu.Cmu.Pocketsphinx.Assets.SyncAssets(Application.Context);
        }
        catch (IOException e)
        {
            throw new Exception("Failed to synchronize assets when attempting to load voice recognition software", e);
        }

        var config = Decoder.DefaultConfig();
        config.SetString("-dict", JoinPath(_assetDir, "models/lm/cmu07a.dic"));
        config.SetString("-hmm", JoinPath(_assetDir, "models/hmm/en-us-semi"));
        config.SetString("-rawlogdir", _assetDir.AbsolutePath);
        config.SetInt("-maxhmmpf", 10000);
        config.SetBoolean("-fwdflat", false);
        config.SetBoolean("-bestpath", false);
        config.SetFloat("-kws_threshold", 1e-5);
        //config.SetFloat("-samprate", 8000);
        //config.SetBoolean("-remove_noise", false);

        _recognizer = new SpeechRecognizer(config);
        _recognizer.AddListener(this);

        // Create keyword-activation search.
        //_recognizer.SetKws(KwsSearchName, Keyphrase);

        // Create grammar-based search for guide.
        var lw = config.GetInt("-lw");
        AddSearch("guide", "guide.gram", "<guide.command>", lw);

        // Create language model search.
        //var path = JoinPath(_assetDir, "models/lm/weather.dmp");
        //var lm = new NGramModel(config, _recognizer.Logmath, path);
        //_recognizer.SetLm("forecast", lm);

        _isInitialised = true;

        Console.WriteLine("InitVoiceRecognition done.");
    }

    /// <summary>
    /// Listen for "delay" seconds. Use a timer. The timer and the speech recognition callbacks both use the _isHandlingResponse flag to decide
    /// who is handling the response. If the user speaks before the timer elapses then the speech recognizer callbacks set the flag and handle
    /// it by returning the spoken string to the HeardSpeechEventHandler. If the timer elapses first, then it sets the flag and sends an empty string
    /// back to the HeardSpeechEventHandler. 
    /// </summary>
    /// <param name="delay"></param>
    public string Listen(int delay)
    {
        Console.WriteLine("Start listening for {0} seconds.", delay);
        //_recognizer.StopListening();

        try
        {
            _isHandlingResponse = false;
            _recognizer.SetSearch("guide");

            var timer = new Timer(_ =>
            {
                // If the speech recognizer is not handling the response already (i.e. the user hasn't spoken) then
                // set the flag and return an empty response
                if (!_isHandlingResponse)
                {
                    _isHandlingResponse = true;
                    _recognizer.StopListening();
                    //Debug.WriteLine("The timer elapsed for the delay of {0} seconds with NO RESPONSE ({0})", delay, CurrentObject.Label);
                    //if (HeardSpeechEventHandler != null)
                    //{
                    //  HeardSpeechEventHandler(this, new SpeechResult() {Result = string.Empty});
                    //}
                    Console.Write("Timer elapsed so setting response to '' and setting wait handle");
                    _response = string.Empty;
                    _waitHandle.Set();
                }
            }, null, delay * 1000, Timeout.Infinite);

            _recognizer.StartListening();
            Console.WriteLine("Started listening, now waiting for wait handle...");
            _waitHandle.WaitOne();
            return _response;
        }
        catch (Exception ex)
        {
            Console.WriteLine("An exception occurred attempting to listen: {0}", ex.Message);
            return "";
        }
    }

    //public IntPtr Handle { get; private set; }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="p0"></param>
    public void OnPartialResult(Hypothesis p0)
    {
        Console.WriteLine("Partial result: " + p0.Hypstr);
        // If the timer is not handling the response already (i.e. timer hasn't timed out yet) then
        // set the flag and stop listening. This will kick off the OnResult function.
        if (!_isHandlingResponse)
        {
            _isHandlingResponse = true;
            _recognizer.StopListening();

            Console.WriteLine("Partial result: " + p0.Hypstr);
        }
    }

    //http://www.albahari.com/
    /// <summary>
    /// 
    /// </summary>
    /// <param name="p0"></param>
    public void OnResult(Hypothesis p0)
    {
        //if (!_isHandlingResponse)
        //{
        //  _isHandlingResponse = true;
        Console.WriteLine("Full result, setting response to '{0}' and setting wait handle", p0.Hypstr);

        //if (HeardSpeechEventHandler != null)
        //{
        //  HeardSpeechEventHandler(this, new SpeechResult() {Result = p0.Hypstr});
        //}
        _response = p0.Hypstr;
        _waitHandle.Set();
        //}
    }

    public void OnVadStateChanged(bool p0)
    {
        //throw new NotImplementedException();
        //Console.WriteLine("OnVadStateChanged: {0}", p0);
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="name"></param>
    /// <param name="path"></param>
    /// <param name="ruleName"></param>
    /// <param name="lw"></param>
    private void AddSearch(String name, String path, String ruleName, int lw)
    {
        //File grammarParent = new File(Path.Combine(_assetDir.AbsolutePath, "grammar"));
        var grammarParent = JoinPath(_assetDir, "grammar");

        var jsgf = new Jsgf(Path.Combine(grammarParent, path));
        var rule = jsgf.GetRule(ruleName);
        var fsg = jsgf.BuildFsg(rule, _recognizer.Logmath, lw);
        _recognizer.SetFsg(name, fsg);
        Console.WriteLine("AddSearch done.");
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="parent"></param>
    /// <param name="path"></param>
    /// <returns></returns>
    private static String JoinPath(Java.IO.File parent, String path)
    {
        return new Java.IO.File(parent, path).AbsolutePath;
    }
}

Solution

  • I got this to work by getting the UI thread of the current activity to load the Pocket Sphinx library, like so:

    Mvx.Resolve<IMvxAndroidCurrentTopActivity>().Activity.RunOnUiThread(() => 
                    Java.Lang.JavaSystem.LoadLibrary("pocketsphinx_jni"));
    

    It feels a little wrong to do it like this (why should my plugin need to know anything about the current UI thread?) - but it works.