Audio Output over Bluetooth HFP

Started by Matthias1912, July 27, 2018, 10:04:35

0 Members and 2 Guests are viewing this topic.

Matthias1912

Hi developer(s),

I do have a problem with the bluetooth audio mode. Since garmin kicked the navigon app for car navigation I like to use locus as a replacement. But my problem is that the audio output need to route over BT HFP so that my car radio can interrupt the radio for the navigation speech output. For now the only working mode is A2DP. But there is now interrupt mode, my car radio need to stay in Bluetooth music mode. In this mode it is impossible to listen music with the car radio when there is now navigation instruction. For that I wrote a little class to show you what I mean or how to solve this problem.

Can you please include the functionality for Bluetooth HFP, so that the navigation route instructions are played by speech output over Bluetooth HFP?

Best regards from Germany
Matthias


import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.speech.tts.TextToSpeech;
import java.util.Calendar;
import java.util.Locale;
import static android.content.Context.AUDIO_SERVICE;

public class mySay
{
/* Please define the following in AndroidManifest.xml
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
*/

/* Using this class

Global define: private mySay _say;
in onCreate: _say = new mySay(this);
in functions: _say.say("Das ist eine Testausgabe", Locale.GERMANY, true, 2000, 2500);
*/

private Context context;

private TextToSpeech speech = null;
private boolean speech_init_finished = false;
private boolean speech_busy = false;
private boolean speech_SCO_connected = false;

public mySay(Context c)
{
context = c;

speech = null;
speech_init_finished = false;
speech_busy = false;
speech_SCO_connected = false;

speech = new TextToSpeech(context, new TextToSpeech.OnInitListener()
{
@Override
public void onInit(int status)
{
if (status == TextToSpeech.SUCCESS) speech_init_finished = true;
}
});
}

public boolean isInitialized()
{
return speech_init_finished;
}

public boolean isBusy()
{
return speech_busy;
}

public void close()
{
try { speech.stop(); }
catch (Exception ex) { }

try { speech.shutdown(); }
catch (Exception ex) { }

speech_init_finished = false;
speech = null;
}

private long get_timestamp_ms()
{
Calendar cal_today = Calendar.getInstance();
java.util.Date date_today = cal_today.getTime();
return date_today.getTime();
}

public boolean say(final String text, final Locale locale, final boolean use_HFP, final int audio_delay_HFP_ms, final int SCO_timeout_ms)
{
if (speech_busy) return false;
if (speech == null) return false;
speech_busy = true;

new Thread(new Runnable()
{
public void run()
{
BroadcastReceiver receiver = null;

try
{
if (speech_init_finished != true) throw new Exception();

speech.setSpeechRate(1.0f);
speech.setPitch(1.0f);
int result = speech.setLanguage(locale);
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) throw new Exception();

AudioManager am = (AudioManager)context.getSystemService(AUDIO_SERVICE);
int OLD_AUDIO_MODE = am.getMode();

// Check if HFP (SCO) is available
boolean SCO_available = false;
AudioDeviceInfo[] devices = am.getDevices (AudioManager.GET_DEVICES_OUTPUTS);
for (int i=0; i<devices.length; i++)
{
if (devices[i].getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) SCO_available = true;
}

if (use_HFP && SCO_available)
{
speech_SCO_connected = false;

receiver = new BroadcastReceiver()
{
@Override
public void onReceive(Context context, Intent intent)
{
if (intent.getAction() == AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)
{
int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1);
if (state == AudioManager.SCO_AUDIO_STATE_CONNECTED) speech_SCO_connected = true;
else speech_SCO_connected = false;
}
}
};
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
context.registerReceiver(receiver, intentFilter);

am.setMode(AudioManager.MODE_IN_COMMUNICATION);        // MODE_IN_CALL, MODE_IN_COMMUNICATION
am.setBluetoothScoOn(true);
am.startBluetoothSco();
am.setSpeakerphoneOn(false);

am.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);

// wait for SCO Connected with a max. timeout of 2,5s
long timestamp_ms_Start = get_timestamp_ms();
while (speech_SCO_connected == false && (get_timestamp_ms() - timestamp_ms_Start) < SCO_timeout_ms)
{
// wait
try { Thread.sleep(100); }
catch (Exception ex) { }
}

if (speech_SCO_connected == true)
{
// additional delay to prevent missing the initial words
try { Thread.sleep(audio_delay_HFP_ms); }
catch (Exception ex) { }

speech.speak(text, TextToSpeech.QUEUE_FLUSH, null, "UtteranceIdTextToSpeechOutput");
while (speech.isSpeaking()) { } // warten bis das sprechen fertig ist
}

am.stopBluetoothSco();
am.setMode(OLD_AUDIO_MODE);
am.setSpeakerphoneOn(true);
}
else
{
am.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
speech.speak(text, TextToSpeech.QUEUE_FLUSH, null, "UtteranceIdTextToSpeechOutput");
while (speech.isSpeaking()) { } // warten bis das sprechen fertig ist
}
}
catch (Exception ex) { }

try
{
if (receiver != null) context.unregisterReceiver(receiver);
}
catch (Exception ex) { }

speech_busy = false;
}
}).start();

return true;
}
}
  •  
    The following users thanked this post: undrwater

Menion

Good day Matthias,
thank you for a very complex part of TTS system source code. I think I got a point of your solution and most probably understand how it works. Anyway, I also have to say that Locus Map is made mainly for hike & bike usage where your solution has no benefit. For use in car, I see the main problem with additional permission, additional settings (something like "Enable support for BT HFP) that will be in app, and also inability to test such feature (because I do not have a car with BT support).
Usually, new ideas are placed on our help.locusmap.eu web site, but such a complicated idea will be lost there between around 1000 interesting, not yet implemented, ideas.
So sorry, I think that such improvement does not belong directly to scope & plans of Locus Map app.
Thanks for understanding.
- Official help (ideas, questions, problems): help.locusmap.eu
- Advanced topics, sharing of knowledges: you're here!
- LM 4 Beta download, LM 4 Release download
  •  

undrwater

I came here to request a similar feature (now "me too!"). I realize this post is old, but I want to make a case for increased navigation features.


  • People are dropping Google for various reasons
  • It is possible someone may make (or already has done so) a "smart bike / smart backpack" that could take advantage of this feature
  • Choice is generally a "good thing"
  • You've got the sample code for integration! - How often do you see that?

I love Locus Map.  I'd like to use it for everything.
  •  

Menion

Hi,
problem with such specific idea is as I wrote before: main app usage & inability to test such feature.

Anyway, I just made basic simple implementation, but I'll need from you to test it.

How to do it:

  • wait for next Beta version. It may take a week or two though
  • enable expert settings
  • in expert settings enable "Enable TTS over BT SCO
  • test and give me feedback ;)
- Official help (ideas, questions, problems): help.locusmap.eu
- Advanced topics, sharing of knowledges: you're here!
- LM 4 Beta download, LM 4 Release download
  •  

Matthias1912

Hi Menion,
sorry for my late answer, I got no email notification from your second post. Last week I ended up with the same problem (BT SCO) again and I found my own post 😉
So short answer, I tested your implementation and TTS over BT SCO works perfect. But you have to release the BT SCO channel after finishing the TTS, otherwise the channel stays open and it does not switch back to car music.
If you modify your code, will this modification be only available in Locus 4 or also to Locus Pro?
  •  

Menion

Hello Matthias,
3 years waiting on the feedback, funny :).
I'm checking the code I made and for me, it looks all is done correctly. After every call "speak", code almost identical to your 'close()' method is called. My implementation is not identical because it is based on events from registered 'UtteranceProgressListener'. So, unfortunately, I do not know what should be done better, sorry.
- Official help (ideas, questions, problems): help.locusmap.eu
- Advanced topics, sharing of knowledges: you're here!
- LM 4 Beta download, LM 4 Release download
  •  

Viajero Perdido

As an aside, I appreciate how (Locus? Android?) turns down the music audio gently, lets the TTS speak, then turns the music back up, again gently (over maybe half a second).  It's much less intrusive that way.  :)
  •  

Menion

@Viajero Perdido
thanks. It is always cooperation. Locus Map always informs the system that now wants to say something and also informs about speed being done. And it is on the currently running audio player, to use this information.

In the case of this task we discuss with Matthias, I do not exactly know what am I doing to be trie  :). Anyway, I was heavily inspired by the code directly from Matthias. I make sense, it should work I believe and I have no option to test it ... and it's really a minor task where I do not want to spend too much time. So I'm probably done here ...
- Official help (ideas, questions, problems): help.locusmap.eu
- Advanced topics, sharing of knowledges: you're here!
- LM 4 Beta download, LM 4 Release download
  •  

Matthias1912

Hi Menion,
Yes, 3 years, shame on me. I checked my code and you are right, there is no SCO release.
I will test my code with my car radio this evening.
I think I know what is missing I will inform you when I finished my test.
  •  

Matthias1912

Hi Menion,
so I tested my code. I do not know how much you are using from my sample. I got it working with the following changes.

100ms delay after each "speak" command
and after finishing speaking, I added a 100ms delay too and then I called "am.abandonAudioFocus(null);" to release the Audio Focus.


public boolean say(final String text, final Locale locale, final boolean use_HFP, final int audio_delay_HFP_ms, final int SCO_timeout_ms)
{
    if (speech_busy) return false;
    if (speech == null) return false;
    speech_busy = true;

    Toast.makeText(context, "active", Toast.LENGTH_SHORT).show();

    new Thread(new Runnable()
    {
        public void run()
        {
            BroadcastReceiver receiver = null;

            try
            {
                if (speech_init_finished != true) throw new Exception();

                speech.setSpeechRate(1.0f);
                speech.setPitch(1.0f);
                int result = speech.setLanguage(locale);
                if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) throw new Exception();

                AudioManager am = (AudioManager)context.getSystemService(AUDIO_SERVICE);
                int OLD_AUDIO_MODE = am.getMode();

                // Check if HFP (SCO) is available
                boolean SCO_available = false;
                AudioDeviceInfo[] devices = am.getDevices (AudioManager.GET_DEVICES_OUTPUTS);
                for (int i=0; i<devices.length; i++)
                {
                    if (devices[i].getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) SCO_available = true;
                }

                if (use_HFP && SCO_available)
                {
                    speech_SCO_connected = false;

                    receiver = new BroadcastReceiver()
                    {
                        @Override
                        public void onReceive(Context context, Intent intent)
                        {
                            if (intent.getAction() == AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)
                            {
                                int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1);
                                if (state == AudioManager.SCO_AUDIO_STATE_CONNECTED) speech_SCO_connected = true;
                                else speech_SCO_connected = false;
                            }
                        }
                    };
                    IntentFilter intentFilter = new IntentFilter();
                    intentFilter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
                    context.registerReceiver(receiver, intentFilter);

                    am.setMode(AudioManager.MODE_IN_COMMUNICATION);        // MODE_IN_CALL, MODE_IN_COMMUNICATION
                    am.setBluetoothScoOn(true);
                    am.startBluetoothSco();
                    am.setSpeakerphoneOn(false);

                    am.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);

                    // wait for SCO Connected with a max. timeout of 2,5s
                    long timestamp_ms_Start = get_timestamp_ms();
                    while (speech_SCO_connected == false && (get_timestamp_ms() - timestamp_ms_Start) < SCO_timeout_ms)
                    {
                        // wait
                        try { Thread.sleep(100); }
                        catch (Exception ex) { }
                    }

                    if (speech_SCO_connected == true)
                    {
                        // additional delay to prevent missing the initial words
                        try { Thread.sleep(audio_delay_HFP_ms); }
                        catch (Exception ex) { }

                        speech.speak(text, TextToSpeech.QUEUE_FLUSH, null, "UtteranceIdTextToSpeechOutput");
                        try { Thread.sleep(100); } // delay ms
                        catch (Exception ex) { }
                        while (speech.isSpeaking()) { } // warten bis das sprechen fertig ist
                    }

                    am.stopBluetoothSco();
                    //am.setBluetoothScoOn(false);
                    am.setMode(OLD_AUDIO_MODE);
                    am.setSpeakerphoneOn(true);
                }
                else
                {
                    am.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
                    speech.speak(text, TextToSpeech.QUEUE_FLUSH, null, "UtteranceIdTextToSpeechOutput");
                    try { Thread.sleep(100); } // delay ms
                    catch (Exception ex) { }
                    while (speech.isSpeaking()) { } // warten bis das sprechen fertig ist
                }

                try { Thread.sleep(100); } // delay ms
                catch (Exception ex) { }
                am.abandonAudioFocus(null);
            }
            catch (Exception ex) { }

            try
            {
                if (receiver != null) context.unregisterReceiver(receiver);
            }
            catch (Exception ex) { }

            speech_busy = false;

        }
    }).start();

    return true;
}


Can you please change this in locus pro code, so I can test it again? I will not wait another 3 years again, I promise.  ;)
  •  

Menion

Hello Matthias,

hehe :). Thanks for the updated code. Your Thread.sleep(100); calls are a really little bit ... crazy. As I wrote before, I use a different approach for handling the TTS speech lifecycle ...


                tts.setOnUtteranceProgressListener(object : UtteranceProgressListener() {

                    override fun onStart(utteranceId: String) {
                        onPlaybackStarted(utteranceId)
                    }

                    override fun onDone(utteranceId: String) {
                        onPlaybackDone(utteranceId)
                    }

                    override fun onError(utteranceId: String) {
                        Logger.e("onError($utteranceId)")

                        // disable focus in all cases
                        AudioUtils.disableAudioFocus()
                    }
                })


where 'onPlaybackDone' firstly disable audioFocus and then clear BT SCO references. So all is already there and set up.

I do not know where is the problem, sorry. I'm definitely not a BT expert, but from what I read now, maybe using STREAM_VOICE_CALL as an output stream should help here? Android Javadoc is quite precise here.
- Official help (ideas, questions, problems): help.locusmap.eu
- Advanced topics, sharing of knowledges: you're here!
- LM 4 Beta download, LM 4 Release download
  •  

Matthias1912

Hi Menion,
thanks for your answer. Yes of course, the Thread.sleep method is quick and dirty, but it was only for demonstration. Can you please share some more code to see how you do it and to find the differences. Or maybe you are willing to create a short app?

I want to understand your implementation so that I can help because I need this functionality. I think the setOnUtteranceProgressListener is defined in the onInit part where my speech_init_finished = true is set. But what is behind your onPlaybackStarted and onPlaybackDone functions? How do you start the tts? Where do you set the AudioManager / SCO parameter? Are you using the same tts implementation for the SCO and "normal music channel" parts (switching by a variable like mine: "use_HFP")?
  •  

Menion

These are good questions. Anyway, I'm sorry, but this task is really far beyond what Locus Map should do & take care of. Output to BT devices is handled by the system and except in special cases, the app should not need to worry about it. You are the first who need something like this and I have so many other important tasks to do .. sorry.

If this functionality does not work correctly, I!ll rather remove it not to confuse other users.

Thanks for understanding Matthias!
- Official help (ideas, questions, problems): help.locusmap.eu
- Advanced topics, sharing of knowledges: you're here!
- LM 4 Beta download, LM 4 Release download
  •  

Matthias1912

Hi Menion,
sorry but I cannot agree with you. Your SCO code is almost ready, it is now fine tuning.

I think I am not the only one want to have this feature. The most people don't talk to the programmers they just move to another app. Osmand for example has this feature but I'm a locus user since the first hour and I like to see that locus has this feature too.

By the way, I changed my code to an event version. I tried to assume what is in your functions. There is only one sleep command left, and this is only for a timeout condition.



package com.example.mysay;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
import java.util.Calendar;
import java.util.Locale;
import static android.content.Context.AUDIO_SERVICE;

public class mySay
{
/* Please define the following in AndroidManifest.xml
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
*/

/* Using this class

Global define: private mySay _say;
in onCreate: _say = new mySay(this);
in onDestroy: _say.TTS_Close();
in functions: _say.say("Das ist eine Testausgabe", Locale.GERMANY, true);
*/

private Context context;

public class TTS_Params_STRUCT
{
public TextToSpeech speech;
public AudioManager am;
public BroadcastReceiver receiver;
public boolean use_SCO;
public boolean SCO_available;
public boolean SCO_connected;
public Locale locale;
public int OLD_AUDIO_MODE;
}
public TTS_Params_STRUCT TTS_Params = new TTS_Params_STRUCT();

private long get_timestamp_ms()
{
Calendar cal_today = Calendar.getInstance();
java.util.Date date_today = cal_today.getTime();
return date_today.getTime();
}

public mySay(Context c)
{
context = c;

onPlaybackInit();
}

private void onPlaybackInit()
{
try
{
TTS_Params.speech = new TextToSpeech(context, new TextToSpeech.OnInitListener()
{
@Override
public void onInit(int status)
{
if (status == TextToSpeech.SUCCESS)
{
TTS_Params.speech.setOnUtteranceProgressListener(new UtteranceProgressListener()
{
@Override
public void onStart(String utteranceId) {
onPlaybackStarted(utteranceId);
}

@Override
public void onDone(String utteranceId) {
onPlaybackDoneOrError(utteranceId);
}

@Override
public void onError(String utteranceId) {
onPlaybackDoneOrError(utteranceId);
}
});

TTS_Params.receiver = new BroadcastReceiver()
{
@Override
public void onReceive(Context context, Intent intent)
{
if (intent.getAction() == AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)
{
int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1);
switch (state)
{
case AudioManager.SCO_AUDIO_STATE_CONNECTING:
break;
case AudioManager.SCO_AUDIO_STATE_CONNECTED:
TTS_Params.SCO_connected = true;
break;
case AudioManager.SCO_AUDIO_STATE_DISCONNECTED:
case AudioManager.SCO_AUDIO_STATE_ERROR:
TTS_Params.SCO_connected = false;
break;
default:
break;
}
}
}
};

IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
context.registerReceiver(TTS_Params.receiver, intentFilter);
}
}
});

TTS_Params.am = (AudioManager)context.getSystemService(AUDIO_SERVICE);

TTS_Params.use_SCO = false;
TTS_Params.SCO_connected = false;
TTS_Params.locale = Locale.getDefault();
TTS_Params.OLD_AUDIO_MODE = -1;

// Check if HFP (SCO) is available
TTS_Params.SCO_available = false;
AudioDeviceInfo[] devices =  TTS_Params.am.getDevices (AudioManager.GET_DEVICES_OUTPUTS);
for (int i=0; i<devices.length; i++)
{
if (devices[i].getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) TTS_Params.SCO_available = true;
}
}
catch (Exception ex) { }
}

public void TTS_Close()
{
try { TTS_Params.speech.stop(); }
catch (Exception ex) { }

try { TTS_Params.speech.shutdown(); }
catch (Exception ex) { }

try { context.unregisterReceiver(TTS_Params.receiver); }
catch (Exception ex) { }

TTS_Params.speech = null;
}

private void onPlaybackStarted(String utteranceId)
{
// onPlaybackStarted is called simultaneously from android with the TTS.speak(...) command
// if you want to use SCO, this function isn't waiting for SCO connected
// all the SCO function calls must be started bevore speak command is called
// -> all this is done in the function say(..)
}

private void onPlaybackDoneOrError(String utteranceId)
{
try
{
TTS_Params.am.stopBluetoothSco();
TTS_Params.am.setBluetoothScoOn(false);
if (TTS_Params.OLD_AUDIO_MODE != -1) TTS_Params.am.setMode(TTS_Params.OLD_AUDIO_MODE);
//TTS_Params.am.setSpeakerphoneOn(true);

TTS_Params.am.abandonAudioFocus(null);
}
catch (Exception ex) { }
}

public void say(final String text, Locale locale, boolean use_SCO)
{
// use SCO = true (HFP Mode = CALL Mode)
// use SCO = false (A2DP Mode = Music Mode)
TTS_Params.use_SCO = use_SCO;

TTS_Params.locale = locale;

new Thread(new Runnable()
{
public void run()
{
try
{
TTS_Params.speech.setSpeechRate(1.0f);
TTS_Params.speech.setPitch(1.0f);
int result = TTS_Params.speech.setLanguage(TTS_Params.locale);
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED)
{
TTS_Params.speech.setLanguage(Locale.getDefault());
}

TTS_Params.OLD_AUDIO_MODE = TTS_Params.am.getMode();

if (TTS_Params.use_SCO && TTS_Params.SCO_available)
{
TTS_Params.am.setMode(AudioManager.MODE_IN_CALL);        // MODE_IN_CALL, MODE_IN_COMMUNICATION
TTS_Params.am.setBluetoothScoOn(true);
TTS_Params.am.startBluetoothSco();
//TTS_Params.am.setSpeakerphoneOn(false);

TTS_Params.am.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);

long timeout = get_timestamp_ms() + 2000; // Wait for SCO is connected (comes from Broadcast Event) but wait max 2s
while (!TTS_Params.SCO_connected && get_timestamp_ms() < timeout)
{
try { Thread.sleep(100); } // Sleep to save CPU power during waiting
catch (Exception ex) { }
}
}
else
{
TTS_Params.am.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
}

TTS_Params.speech.speak(text, TextToSpeech.QUEUE_FLUSH, null, "UtteranceIdTextToSpeechOutput1234");
}
catch (Exception ex) { }
}
}).start();
}
}

  •  

Menion

Hello Matthias,
most of the users (I as well) do not know what is SCO. I had to search and read about it. Why? Because nobody wants to deal with such low-level systems of BT communication. Android does it well ... it takes care automatically of most of the stuff regards BT communication.

I still do not perfectly understand, why the current system, where Android devices picks the optimal methods of communication with the receiver, is not enough.

Anyway, thanks for the update. I've quickly compared your previous, current and my own implementation, and what was missing was 'isBluetoothScoOn = false' call. I've also, based on your change, removed  'isSpeakerphoneOn = true/false' call. You may give it a try in the next version. Thanks.
- Official help (ideas, questions, problems): help.locusmap.eu
- Advanced topics, sharing of knowledges: you're here!
- LM 4 Beta download, LM 4 Release download
  •