1 /**
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.car.voicecontrol;
17 
18 import android.annotation.NonNull;
19 import android.annotation.Nullable;
20 import android.annotation.StringRes;
21 import android.content.Context;
22 import android.media.AudioAttributes;
23 import android.os.Handler;
24 import android.speech.tts.UtteranceProgressListener;
25 import android.speech.tts.Voice;
26 import android.text.TextUtils;
27 import android.util.Log;
28 
29 import java.io.File;
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.HashMap;
33 import java.util.List;
34 import java.util.Locale;
35 import java.util.Map;
36 import java.util.stream.Collectors;
37 
38 /**
39  * Sample implementation of {@link TextToSpeech} interface. This implementation uses the system
40  * default {@link android.speech.tts.TextToSpeech} service.
41  */
42 public class TextToSpeechImpl implements TextToSpeech {
43     private static final String TAG = "Mica.TextToSpeechImpl";
44 
45     // Strings used when warming up the TTS pipeline.
46     private static final String WARM_UP_PIPELINE_FILENAME = "warmUpPipeline";
47     private static final String WARM_UP_PIPELINE_TEXT = "Hello";
48     private static final String WARM_UP_PIPELINE_UTTERANCE_ID = "warm-up";
49 
50     private android.speech.tts.TextToSpeech mTTS;
51     private Context mContext;
52     private final Listener mListener;
53     private List<String> mPendingUtterance = new ArrayList<>();
54     private boolean mIsReady;
55     private String mSelectedVoice;
56     private Handler mHandler = new Handler();
57     private final UtteranceProgressListener mTTSListener = new UtteranceProgressListener() {
58         @Override
59         public void onStart(String id) {
60             Log.d(TAG, "TTS start");
61         }
62 
63         @Override
64         public void onDone(String id) {
65             Log.d(TAG, "TTS done");
66             if (WARM_UP_PIPELINE_UTTERANCE_ID.equals(id)) {
67                 // Ignore warm up utterance.
68                 return;
69             }
70             mHandler.post(() -> {
71                 if (isWaitingForAnswer()) {
72                     mListener.onWaitingForAnswer();
73                 } else {
74                     mListener.onUtteranceDone(true);
75                 }
76             });
77         }
78 
79         @Override
80         public void onError(String id) {
81             Log.d(TAG, "TTS error");
82             mHandler.post(() -> mListener.onUtteranceDone(false));
83         }
84     };
85     private QuestionCallback mQuestionCallback = null;
86     private final Map<String, AnswerType> mAnswerTypes = new HashMap<>();
87     private File mWarmUpPipelineFile;
88 
TextToSpeechImpl(Context context, Listener listener)89     public TextToSpeechImpl(Context context, Listener listener) {
90         Log.d(TAG, "TTS create");
91         mContext = context;
92         mListener = listener;
93         Arrays.stream(context.getResources()
94                 .getStringArray(R.array.speech_reply_affirmative_answers))
95                 .forEach(answer -> mAnswerTypes.put(answer, AnswerType.AFFIRMATIVE));
96         Arrays.stream(context.getResources()
97                 .getStringArray(R.array.speech_reply_negative_answers))
98                 .forEach(answer -> mAnswerTypes.put(answer, AnswerType.NEGATIVE));
99         mTTS = new android.speech.tts.TextToSpeech(context, status -> {
100             Log.d(TAG, "TTS started: " + status);
101             if (status == android.speech.tts.TextToSpeech.ERROR) {
102                 throw new IllegalStateException("Unable to setup TTS");
103             }
104             doSetVoice(mSelectedVoice);
105             mTTS.setOnUtteranceProgressListener(mTTSListener);
106             mTTS.setAudioAttributes(new AudioAttributes.Builder()
107                     .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
108                     .setUsage(AudioAttributes.USAGE_ASSISTANT)
109                     .build());
110             mIsReady = true;
111             for (String text : mPendingUtterance) {
112                 mTTS.speak(text, android.speech.tts.TextToSpeech.QUEUE_ADD, null, "");
113             }
114             mPendingUtterance.clear();
115             warmUpPipeline();
116             mListener.onReady(this);
117         });
118     }
119 
120     @Override
setSelectedVoice(String name)121     public void setSelectedVoice(String name) {
122         if (mIsReady) {
123             doSetVoice(name);
124         } else {
125             mSelectedVoice = name;
126         }
127     }
128 
doSetVoice(String name)129     private void doSetVoice(String name) {
130         Voice voice = getVoice(name);
131         if (voice != null) {
132             mTTS.setVoice(voice);
133         } else {
134             // Setting the language resets the voice to the default for that language
135             mTTS.setLanguage(Locale.US);
136         }
137     }
138 
warmUpPipeline()139     private void warmUpPipeline() {
140         if (mWarmUpPipelineFile != null) {
141             mWarmUpPipelineFile.delete();
142         }
143         // Start loading up the pipeline by synthesizing some throwaway text to a file. This will
144         // reduce latency for the first Text to Speech request.
145         mWarmUpPipelineFile = new File(mContext.getFilesDir(), WARM_UP_PIPELINE_FILENAME);
146         mTTS.synthesizeToFile(WARM_UP_PIPELINE_TEXT, null, mWarmUpPipelineFile,
147                 WARM_UP_PIPELINE_UTTERANCE_ID);
148     }
149 
150     @Override
destroy()151     public void destroy() {
152         if (mTTS != null) {
153             mTTS.shutdown();
154             mTTS = null;
155         }
156         if (mWarmUpPipelineFile != null) {
157             mWarmUpPipelineFile.delete();
158             mWarmUpPipelineFile = null;
159         }
160     }
161 
162     @Override
speak(@tringRes int resId, Object... args)163     public void speak(@StringRes int resId, Object... args) {
164         doSpeak(mContext.getString(resId, args));
165     }
166 
167     @Override
speak(String text, Object... args)168     public void speak(String text, Object... args) {
169         doSpeak(String.format(text, args));
170     }
171 
doSpeak(String text)172     private void doSpeak(String text) {
173         if (mIsReady) {
174             mTTS.speak(text, android.speech.tts.TextToSpeech.QUEUE_ADD, null, "");
175         } else {
176             mPendingUtterance.add(text);
177         }
178     }
179 
180     @Override
ask(QuestionCallback callback, String fmt, Object... args)181     public void ask(QuestionCallback callback, String fmt, Object... args) {
182         doAsk(String.format(fmt, args), callback);
183     }
184 
185     @Override
ask(QuestionCallback callback, @StringRes int resId, Object... args)186     public void ask(QuestionCallback callback, @StringRes int resId, Object... args) {
187         doAsk(mContext.getString(resId, args), callback);
188     }
189 
doAsk(String text, QuestionCallback callback)190     private <T> void doAsk(String text, QuestionCallback callback) {
191         mQuestionCallback = callback;
192         doSpeak(text);
193     }
194 
195     @Override
isWaitingForAnswer()196     public boolean isWaitingForAnswer() {
197         return mQuestionCallback != null;
198     }
199 
200     @Override
provideAnswer(@onNull List<String> strings)201     public void provideAnswer(@NonNull List<String> strings) {
202         QuestionCallback callback = mQuestionCallback;
203         mQuestionCallback = null;
204         callback.onResult(strings);
205     }
206 
207     @Override
getVoices()208     public List<String> getVoices() {
209         if (!mIsReady) {
210             return new ArrayList<>();
211         }
212         return mTTS.getVoices()
213                 .stream()
214                 .map(Voice::getName)
215                 .sorted()
216                 .collect(Collectors.toList());
217     }
218 
219     @Nullable
getVoice(String name)220     private Voice getVoice(String name) {
221         if (!mIsReady || TextUtils.isEmpty(name)) {
222             return null;
223         }
224         return mTTS.getVoices()
225                 .stream()
226                 .filter(v -> v.getName().equals(name))
227                 .findFirst()
228                 .orElse(null);
229     }
230 
231     @Override
getAnswerType(List<String> strings)232     public AnswerType getAnswerType(List<String> strings) {
233         return strings.stream()
234                 .map(s -> s.toLowerCase().trim())
235                 .map(s -> mAnswerTypes.get(s))
236                 .filter(type -> type != null)
237                 .findFirst()
238                 .orElse(null);
239     }
240 }
241