Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Announce values while connected to Android auto #1071

Closed
wkochFPV opened this issue Oct 27, 2019 · 5 comments
Closed

Announce values while connected to Android auto #1071

wkochFPV opened this issue Oct 27, 2019 · 5 comments

Comments

@wkochFPV
Copy link

Announcements of values using text synthesis are not played while connected with Android auto. Audio can only be heard when accidently colliding with navigation announcements (these audio duck the fm radio while playing). Could xdrip also audio duck the radio to be able to hear the current value? I think, this feature is referred to as "audio focus" in android development.

Thank you for your efforts.

@wkochFPV
Copy link
Author

I am new to android studio and java programming, but modified the file speechutil.java to gain the audio focus before speaking. This solves the issue. Text to speech synthesis can now be heared while android auto is active. This is no clean programming. The audio focus is simply released 3 seconds after starting the TTS. A better way would be to handle the complete event as done in this TTS class example: https://gist.github.com/LukasKnuth/0c0d17b343483d25aca2

I have no clue how to suggest changes using git, so I am posting my modifications here in case one of the developers would like to adapt the changes.


package com.eveningoutpost.dexdrip.UtilityModels;

import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.os.Build;
import android.os.PowerManager;
import android.speech.tts.TextToSpeech;

import androidx.annotation.RequiresApi;

import com.eveningoutpost.dexdrip.Models.JoH;
import com.eveningoutpost.dexdrip.Models.UserError;
import com.eveningoutpost.dexdrip.xdrip;

import java.util.Locale;

/**

  • Created by jamorham on 20/12/2017.
  • Designed to be an easy use-anywhere speech output system, simply call the static "say" method
  • Can be called from any thread, self initializes and statically retains instance and mitigates the binding delays present with Android TTS
  • Uses xDrip preferences to define speech parameters, language etc and will baulk during calls and delay for music.
  • TODO: is there a way we can delay till the end of any sound playing - eg notification noise? Does Android expose this in the framework?
    */

public class SpeechUtil {

public static final String TAG = "SpeechUtil";
public static final String TWICE_DELIMITER = " ... ... ... "; // creates a pause hopefully works on all locales
private static volatile TextToSpeech tts = null; // maintained instance



// delay parameter allows you to force a millis delay before playing to avoid clash with notification sounds triggered at the same time
@SuppressWarnings("WeakerAccess")
public static void say(final String text) {
    say(text, 0);
}

@SuppressWarnings("WeakerAccess")
public static void say(final String text, long delay) {
    say(text, delay, 0);
}

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@SuppressWarnings("WeakerAccess")
public static void say(final String text, long delay, int retry) {

    new Thread(() -> {
        final PowerManager.WakeLock wl = JoH.getWakeLock("SpeechUtil", (int) Constants.SECOND_IN_MS * 60);
        try {
            if (tts == null) {
                initialize();
            }
            try {
                Thread.sleep(delay);
            } catch (InterruptedException ee) {
                //
            }

            if (isOngoingCall()) {
                UserError.Log.e(TAG, "Cannot speak due to ongoing call: " + text);
                return;
            }

            // if sound is playing, wait up to 40 seconds to deliver the speech
            final long max_wait = JoH.tsl() + Constants.SECOND_IN_MS * 40;
            while (isMusicPlaying() && JoH.tsl() < max_wait) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ee) {
                    //
                }
            }

            // pull in preferences for speech but set some lower bounds to avoid bad sounding speech
            final float speed = Math.max(0.2f, Pref.getInt("speech_speed", 10) / 10f);
            final float pitch = Math.max(0.4f, Pref.getInt("speech_pitch", 10) / 10f);

            // set the rates
            try {
                tts.setSpeechRate(speed);
                tts.setPitch(pitch);
            } catch (Exception e) {
                UserError.Log.e(TAG, "Deep TTS problem setting speech rates: " + e);
            }
            // handle repeat everything twice feature. We could queue it twice instead of expanding the string but then each block of speech is not a single transaction
            final boolean double_up_text_flag = (!text.contains(TWICE_DELIMITER)) && Pref.getBooleanDefaultFalse("speak_twice");
            final String final_text_to_speak = double_up_text_flag ? (text + TWICE_DELIMITER + text) : text;

            int result;
            try {
                // WK start - request audio focus
                final AudioManager manager = (AudioManager) xdrip.getAppContext().getSystemService(Context.AUDIO_SERVICE);
                final AudioManager.OnAudioFocusChangeListener afl = new AudioManager.OnAudioFocusChangeListener() {
                    @Override
                    public void onAudioFocusChange(int focusChange) {
                        // TODO React to audio-focus changes here!
                    }
                };
                int focus_res = manager.requestAudioFocus(
                        afl, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
                );
                // WK end
                result = tts.speak(final_text_to_speak, TextToSpeech.QUEUE_ADD, null);
                // WK start - wait for tts to play
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException ee) {
                    //
                }
                manager.abandonAudioFocus(afl);
                // WK end
            } catch (NullPointerException e) {
                result = TextToSpeech.ERROR;
                UserError.Log.e(TAG, "Got null pointer trying to speak! concurrency issue");
            }
            UserError.Log.d(TAG, "Speak result: " + result);

            // speech randomly fails, usually due to the service not being bound so quick after being initialized, so we wait and retry recursively
            if ((result != TextToSpeech.SUCCESS) && (retry < 5)) {
                UserError.Log.d(TAG, "Failed to speak: retrying in 2s: " + retry);
                say(text, delay + 2000, retry + 1);
                return;
            }
            // only get here if retries exceeded
            if (result != TextToSpeech.SUCCESS) {
                UserError.Log.wtf(TAG, "Failed to speak after: " + retry + " retries!!!");

            } else {
                UserError.Log.d(TAG, "Successfully spoke: " + text);
            }

        } finally {
            JoH.releaseWakeLock(wl);
        }
    }).start();

}

// get the locale that text to speech should be using
public static Locale getLocale() {
    try {
        if (tts != null) {
            // if we have an instance return what we are actually using
            return tts.getLanguage();
        } else {
            // if we don't have an instance return what we would like to be using
            return chosenLocale();
        }
    } catch (Exception e) {
        // always try to return something
        return chosenLocale();
    }
}

// evaluate locale from system and user settings
private static Locale chosenLocale() {
    // first get the default language
    Locale speech_locale = Locale.getDefault();
    try {
        final String tts_language = Pref.getStringDefaultBlank("speak_readings_custom_language").trim();
        // did the user specify another language for speech?
        if (tts_language.length() > 1) {
            final String[] lang_components = tts_language.split("_");
            final String country = (lang_components.length > 1) ? lang_components[1] : "";
            speech_locale = new Locale(lang_components[0], country, "");
        }
    } catch (Exception e) {
        UserError.Log.e(TAG, "Exception trying to use custom language: " + e);
    }
    return speech_locale;
}

// set up an instance to Android TTS with our desired language and settings
private synchronized static void initialize() {
    if (tts != null) return;
    tts = new TextToSpeech(xdrip.getAppContext(), status -> {

        if (status == TextToSpeech.SUCCESS && tts != null) {
            UserError.Log.d(TAG, "Initializing, successful result code: " + status);

            final Locale speech_locale = chosenLocale();

            UserError.Log.d(TAG, "Chosen locale: " + speech_locale);

            int set_language_result;
            // try setting the language we want
            try {
                set_language_result = tts.setLanguage(speech_locale);
            } catch (IllegalArgumentException e) {
                // can end up here with Locales like "OS"
                UserError.Log.e(TAG, "Got TTS set language error: " + e.toString());
                set_language_result = TextToSpeech.LANG_MISSING_DATA;
            } catch (Exception e) {
                // can end up here with deep errors from tts system
                UserError.Log.e(TAG, "Got TTS set language deep error: " + e.toString());
                set_language_result = TextToSpeech.LANG_MISSING_DATA;
            }

            // try various fallbacks
            if (set_language_result == TextToSpeech.LANG_MISSING_DATA
                    || set_language_result == TextToSpeech.LANG_NOT_SUPPORTED) {
                UserError.Log.e(TAG, "Default system language is not supported");
                try {
                    set_language_result = tts.setLanguage(Locale.ENGLISH);
                } catch (IllegalArgumentException e) {
                    // can end up here with parcel Locales like "OS"
                    UserError.Log.e(TAG, "Got TTS set default language error: " + e.toString());
                    set_language_result = TextToSpeech.LANG_MISSING_DATA;
                } catch (Exception e) {
                    // can end up here with deep errors from tts system
                    UserError.Log.e(TAG, "Got TTS set default language deep error: " + e.toString());
                    set_language_result = TextToSpeech.LANG_MISSING_DATA;
                }
            }
            //try any english as last resort
            if (set_language_result == TextToSpeech.LANG_MISSING_DATA
                    || set_language_result == TextToSpeech.LANG_NOT_SUPPORTED) {
                UserError.Log.e(TAG, "English is not supported! total failure");
                tts = null;
            }
        } else {
            UserError.Log.e(TAG, "Initialize status code indicates failure, code: " + status);
            tts = null;
        }
    });
}

// shutdown existing instance - most useful when changing language or parameters
public static synchronized void shutdown() {
    if (tts != null) {
        try {
            tts.shutdown();
        } catch (IllegalArgumentException e) {
            UserError.Log.e(TAG, "Got exception shutting down service: " + e);
        }
        tts = null;
    }
}


// this is a duplicate of similar code in JoH utility class but kept here as these methods will be specific for the speech util
@SuppressWarnings("ConstantConditions")
private static boolean isOngoingCall() {
    final AudioManager manager = (AudioManager) xdrip.getAppContext().getSystemService(Context.AUDIO_SERVICE);
    try {
        return (manager.getMode() == AudioManager.MODE_IN_CALL);
    } catch (NullPointerException e) {
        return false;
    }
}

// this isn't as complete as I would like - does the framework expose anything more generic we can use to detect any sound playing?
@SuppressWarnings("ConstantConditions")
private static boolean isMusicPlaying() {
    final AudioManager manager = (AudioManager) xdrip.getAppContext().getSystemService(Context.AUDIO_SERVICE);
    try {
        return manager.isMusicActive();
    } catch (NullPointerException e) {
        return false;
    }
}

// redirect user to android tts data file installation activity
public static void installTTSData(Context context) {
    try {
        final Intent intent = new Intent();
        intent.setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    } catch (ActivityNotFoundException e) {
        UserError.Log.e(TAG, "Could not install TTS data: " + e.toString());
    }
}

}

@steve8x8
Copy link

Are you familiar with diff? That would save interested folks from reading your, and the original, source in parallel trying to spot every single change.

@steve8x8
Copy link

As a courtesy to the developers, here's the diff:

--- a/SpeechUtil.java	2019-11-03 15:34:46.000000000 +0100
+++ b/SpeechUtil.java	2019-11-17 10:54:00.000000000 +0100
@@ -90,7 +90,27 @@
 
                 int result;
                 try {
+                    // WK start - request audio focus
+                    final AudioManager manager = (AudioManager) xdrip.getAppContext().getSystemService(Context.AUDIO_SERVICE);
+                    final AudioManager.OnAudioFocusChangeListener afl = new AudioManager.OnAudioFocusChangeListener() {
+                        @Override
+                        public void onAudioFocusChange(int focusChange) {
+                            // TODO React to audio-focus changes here!
+                        }
+                    };
+                    int focus_res = manager.requestAudioFocus(
+                            afl, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+                    );
+                    // WK end
                     result = tts.speak(final_text_to_speak, TextToSpeech.QUEUE_ADD, null);
+                    // WK start - wait for tts to play
+                    try {
+                        Thread.sleep(3000);
+                    } catch (InterruptedException ee) {
+                        //
+                    }
+                    manager.abandonAudioFocus(afl);
+                    // WK end
                 } catch (NullPointerException e) {
                     result = TextToSpeech.ERROR;
                     UserError.Log.e(TAG, "Got null pointer trying to speak! concurrency issue");

I hope I caught all the changes, and didn't break any formatting...

@tolot27
Copy link
Collaborator

tolot27 commented Jan 12, 2021

Can this issue be closed in favor of #1423?

@Navid200
Copy link
Collaborator

Closing due to inactivity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants