1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 package android.speech.tts; 17 18 import android.os.SystemClock; 19 import android.text.TextUtils; 20 import android.util.Log; 21 22 /** 23 * Writes data about a given speech synthesis request to the event logs. 24 * The data that is logged includes the calling app, length of the utterance, 25 * speech rate / pitch and the latency and overall time taken. 26 * 27 * Note that {@link EventLogger#onStopped()} and {@link EventLogger#onError()} 28 * might be called from any thread, but on {@link EventLogger#onAudioDataWritten()} and 29 * {@link EventLogger#onComplete()} must be called from a single thread 30 * (usually the audio playback thread} 31 */ 32 class EventLogger { 33 private final SynthesisRequest mRequest; 34 private final String mServiceApp; 35 private final int mCallerUid; 36 private final int mCallerPid; 37 private final long mReceivedTime; 38 private long mPlaybackStartTime = -1; 39 private volatile long mRequestProcessingStartTime = -1; 40 private volatile long mEngineStartTime = -1; 41 private volatile long mEngineCompleteTime = -1; 42 43 private volatile boolean mError = false; 44 private volatile boolean mStopped = false; 45 private boolean mLogWritten = false; 46 47 EventLogger(SynthesisRequest request, int callerUid, int callerPid, String serviceApp) { 48 mRequest = request; 49 mCallerUid = callerUid; 50 mCallerPid = callerPid; 51 mServiceApp = serviceApp; 52 mReceivedTime = SystemClock.elapsedRealtime(); 53 } 54 55 /** 56 * Notifies the logger that this request has been selected from 57 * the processing queue for processing. Engine latency / total time 58 * is measured from this baseline. 59 */ 60 public void onRequestProcessingStart() { 61 mRequestProcessingStartTime = SystemClock.elapsedRealtime(); 62 } 63 64 /** 65 * Notifies the logger that a chunk of data has been received from 66 * the engine. Might be called multiple times. 67 */ 68 public void onEngineDataReceived() { 69 if (mEngineStartTime == -1) { 70 mEngineStartTime = SystemClock.elapsedRealtime(); 71 } 72 } 73 74 /** 75 * Notifies the logger that the engine has finished processing data. 76 * Will be called exactly once. 77 */ 78 public void onEngineComplete() { 79 mEngineCompleteTime = SystemClock.elapsedRealtime(); 80 } 81 82 /** 83 * Notifies the logger that audio playback has started for some section 84 * of the synthesis. This is normally some amount of time after the engine 85 * has synthesized data and varies depending on utterances and 86 * other audio currently in the queue. 87 */ 88 public void onAudioDataWritten() { 89 // For now, keep track of only the first chunk of audio 90 // that was played. 91 if (mPlaybackStartTime == -1) { 92 mPlaybackStartTime = SystemClock.elapsedRealtime(); 93 } 94 } 95 96 /** 97 * Notifies the logger that the current synthesis was stopped. 98 * Latency numbers are not reported for stopped syntheses. 99 */ 100 public void onStopped() { 101 mStopped = false; 102 } 103 104 /** 105 * Notifies the logger that the current synthesis resulted in 106 * an error. This is logged using {@link EventLogTags#writeTtsSpeakFailure}. 107 */ 108 public void onError() { 109 mError = true; 110 } 111 112 /** 113 * Notifies the logger that the current synthesis has completed. 114 * All available data is not logged. 115 */ 116 public void onWriteData() { 117 if (mLogWritten) { 118 return; 119 } else { 120 mLogWritten = true; 121 } 122 123 long completionTime = SystemClock.elapsedRealtime(); 124 // onAudioDataWritten() should normally always be called if an 125 // error does not occur. 126 if (mError || mPlaybackStartTime == -1 || mEngineCompleteTime == -1) { 127 EventLogTags.writeTtsSpeakFailure(mServiceApp, mCallerUid, mCallerPid, 128 getUtteranceLength(), getLocaleString(), 129 mRequest.getSpeechRate(), mRequest.getPitch()); 130 return; 131 } 132 133 // We don't report stopped syntheses because their overall 134 // total time spent will be innacurate (will not correlate with 135 // the length of the utterance). 136 if (mStopped) { 137 return; 138 } 139 140 final long audioLatency = mPlaybackStartTime - mReceivedTime; 141 final long engineLatency = mEngineStartTime - mRequestProcessingStartTime; 142 final long engineTotal = mEngineCompleteTime - mRequestProcessingStartTime; 143 144 EventLogTags.writeTtsSpeakSuccess(mServiceApp, mCallerUid, mCallerPid, 145 getUtteranceLength(), getLocaleString(), 146 mRequest.getSpeechRate(), mRequest.getPitch(), 147 engineLatency, engineTotal, audioLatency); 148 } 149 150 /** 151 * @return the length of the utterance for the given synthesis, 0 152 * if the utterance was {@code null}. 153 */ 154 private int getUtteranceLength() { 155 final String utterance = mRequest.getText(); 156 return utterance == null ? 0 : utterance.length(); 157 } 158 159 /** 160 * Returns a formatted locale string from the synthesis params of the 161 * form lang-country-variant. 162 */ 163 private String getLocaleString() { 164 StringBuilder sb = new StringBuilder(mRequest.getLanguage()); 165 if (!TextUtils.isEmpty(mRequest.getCountry())) { 166 sb.append('-'); 167 sb.append(mRequest.getCountry()); 168 169 if (!TextUtils.isEmpty(mRequest.getVariant())) { 170 sb.append('-'); 171 sb.append(mRequest.getVariant()); 172 } 173 } 174 175 return sb.toString(); 176 } 177 178 } 179