Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / content / browser / speech / endpointer / energy_endpointer.cc
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 //
5 // To know more about the algorithm used and the original code which this is
6 // based of, see
7 // https://wiki.corp.google.com/twiki/bin/view/Main/ChromeGoogleCodeXRef
8
9 #include "content/browser/speech/endpointer/energy_endpointer.h"
10
11 #include <math.h>
12
13 #include "base/logging.h"
14
15 namespace {
16
17 // Returns the RMS (quadratic mean) of the input signal.
18 float RMS(const int16* samples, int num_samples) {
19   int64 ssq_int64 = 0;
20   int64 sum_int64 = 0;
21   for (int i = 0; i < num_samples; ++i) {
22     sum_int64 += samples[i];
23     ssq_int64 += samples[i] * samples[i];
24   }
25   // now convert to floats.
26   double sum = static_cast<double>(sum_int64);
27   sum /= num_samples;
28   double ssq = static_cast<double>(ssq_int64);
29   return static_cast<float>(sqrt((ssq / num_samples) - (sum * sum)));
30 }
31
32 int64 Secs2Usecs(float seconds) {
33   return static_cast<int64>(0.5 + (1.0e6 * seconds));
34 }
35
36 float GetDecibel(float value) {
37   if (value > 1.0e-100)
38     return 20 * log10(value);
39   return -2000.0;
40 }
41
42 }  // namespace
43
44 namespace content {
45
46 // Stores threshold-crossing histories for making decisions about the speech
47 // state.
48 class EnergyEndpointer::HistoryRing {
49  public:
50   HistoryRing() : insertion_index_(0) {}
51
52   // Resets the ring to |size| elements each with state |initial_state|
53   void SetRing(int size, bool initial_state);
54
55   // Inserts a new entry into the ring and drops the oldest entry.
56   void Insert(int64 time_us, bool decision);
57
58   // Returns the time in microseconds of the most recently added entry.
59   int64 EndTime() const;
60
61   // Returns the sum of all intervals during which 'decision' is true within
62   // the time in seconds specified by 'duration'. The returned interval is
63   // in seconds.
64   float RingSum(float duration_sec);
65
66  private:
67   struct DecisionPoint {
68     int64 time_us;
69     bool decision;
70   };
71
72   std::vector<DecisionPoint> decision_points_;
73   int insertion_index_;  // Index at which the next item gets added/inserted.
74
75   DISALLOW_COPY_AND_ASSIGN(HistoryRing);
76 };
77
78 void EnergyEndpointer::HistoryRing::SetRing(int size, bool initial_state) {
79   insertion_index_ = 0;
80   decision_points_.clear();
81   DecisionPoint init = { -1, initial_state };
82   decision_points_.resize(size, init);
83 }
84
85 void EnergyEndpointer::HistoryRing::Insert(int64 time_us, bool decision) {
86   decision_points_[insertion_index_].time_us = time_us;
87   decision_points_[insertion_index_].decision = decision;
88   insertion_index_ = (insertion_index_ + 1) % decision_points_.size();
89 }
90
91 int64 EnergyEndpointer::HistoryRing::EndTime() const {
92   int ind = insertion_index_ - 1;
93   if (ind < 0)
94     ind = decision_points_.size() - 1;
95   return decision_points_[ind].time_us;
96 }
97
98 float EnergyEndpointer::HistoryRing::RingSum(float duration_sec) {
99   if (!decision_points_.size())
100     return 0.0;
101
102   int64 sum_us = 0;
103   int ind = insertion_index_ - 1;
104   if (ind < 0)
105     ind = decision_points_.size() - 1;
106   int64 end_us = decision_points_[ind].time_us;
107   bool is_on = decision_points_[ind].decision;
108   int64 start_us = end_us - static_cast<int64>(0.5 + (1.0e6 * duration_sec));
109   if (start_us < 0)
110     start_us = 0;
111   size_t n_summed = 1;  // n points ==> (n-1) intervals
112   while ((decision_points_[ind].time_us > start_us) &&
113          (n_summed < decision_points_.size())) {
114     --ind;
115     if (ind < 0)
116       ind = decision_points_.size() - 1;
117     if (is_on)
118       sum_us += end_us - decision_points_[ind].time_us;
119     is_on = decision_points_[ind].decision;
120     end_us = decision_points_[ind].time_us;
121     n_summed++;
122   }
123
124   return 1.0e-6f * sum_us;  //  Returns total time that was super threshold.
125 }
126
127 EnergyEndpointer::EnergyEndpointer()
128     : status_(EP_PRE_SPEECH),
129       offset_confirm_dur_sec_(0),
130       endpointer_time_us_(0),
131       fast_update_frames_(0),
132       frame_counter_(0),
133       max_window_dur_(4.0),
134       sample_rate_(0),
135       history_(new HistoryRing()),
136       decision_threshold_(0),
137       estimating_environment_(false),
138       noise_level_(0),
139       rms_adapt_(0),
140       start_lag_(0),
141       end_lag_(0),
142       user_input_start_time_us_(0) {
143 }
144
145 EnergyEndpointer::~EnergyEndpointer() {
146 }
147
148 int EnergyEndpointer::TimeToFrame(float time) const {
149   return static_cast<int32>(0.5 + (time / params_.frame_period()));
150 }
151
152 void EnergyEndpointer::Restart(bool reset_threshold) {
153   status_ = EP_PRE_SPEECH;
154   user_input_start_time_us_ = 0;
155
156   if (reset_threshold) {
157     decision_threshold_ = params_.decision_threshold();
158     rms_adapt_ = decision_threshold_;
159     noise_level_ = params_.decision_threshold() / 2.0f;
160     frame_counter_ = 0;  // Used for rapid initial update of levels.
161   }
162
163   // Set up the memories to hold the history windows.
164   history_->SetRing(TimeToFrame(max_window_dur_), false);
165
166   // Flag that indicates that current input should be used for
167   // estimating the environment. The user has not yet started input
168   // by e.g. pressed the push-to-talk button. By default, this is
169   // false for backward compatibility.
170   estimating_environment_ = false;
171 }
172
173 void EnergyEndpointer::Init(const EnergyEndpointerParams& params) {
174   params_ = params;
175
176   // Find the longest history interval to be used, and make the ring
177   // large enough to accommodate that number of frames.  NOTE: This
178   // depends upon ep_frame_period being set correctly in the factory
179   // that did this instantiation.
180   max_window_dur_ = params_.onset_window();
181   if (params_.speech_on_window() > max_window_dur_)
182     max_window_dur_ = params_.speech_on_window();
183   if (params_.offset_window() > max_window_dur_)
184     max_window_dur_ = params_.offset_window();
185   Restart(true);
186
187   offset_confirm_dur_sec_ = params_.offset_window() -
188                             params_.offset_confirm_dur();
189   if (offset_confirm_dur_sec_ < 0.0)
190     offset_confirm_dur_sec_ = 0.0;
191
192   user_input_start_time_us_ = 0;
193
194   // Flag that indicates that  current input should be used for
195   // estimating the environment. The user has not yet started input
196   // by e.g. pressed the push-to-talk button. By default, this is
197   // false for backward compatibility.
198   estimating_environment_ = false;
199   // The initial value of the noise and speech levels is inconsequential.
200   // The level of the first frame will overwrite these values.
201   noise_level_ = params_.decision_threshold() / 2.0f;
202   fast_update_frames_ =
203       static_cast<int64>(params_.fast_update_dur() / params_.frame_period());
204
205   frame_counter_ = 0;  // Used for rapid initial update of levels.
206
207   sample_rate_ = params_.sample_rate();
208   start_lag_ = static_cast<int>(sample_rate_ /
209                                 params_.max_fundamental_frequency());
210   end_lag_ = static_cast<int>(sample_rate_ /
211                               params_.min_fundamental_frequency());
212 }
213
214 void EnergyEndpointer::StartSession() {
215   Restart(true);
216 }
217
218 void EnergyEndpointer::EndSession() {
219   status_ = EP_POST_SPEECH;
220 }
221
222 void EnergyEndpointer::SetEnvironmentEstimationMode() {
223   Restart(true);
224   estimating_environment_ = true;
225 }
226
227 void EnergyEndpointer::SetUserInputMode() {
228   estimating_environment_ = false;
229   user_input_start_time_us_ = endpointer_time_us_;
230 }
231
232 void EnergyEndpointer::ProcessAudioFrame(int64 time_us,
233                                          const int16* samples,
234                                          int num_samples,
235                                          float* rms_out) {
236   endpointer_time_us_ = time_us;
237   float rms = RMS(samples, num_samples);
238
239   // Check that this is user input audio vs. pre-input adaptation audio.
240   // Input audio starts when the user indicates start of input, by e.g.
241   // pressing push-to-talk. Audio received prior to that is used to update
242   // noise and speech level estimates.
243   if (!estimating_environment_) {
244     bool decision = false;
245     if ((endpointer_time_us_ - user_input_start_time_us_) <
246         Secs2Usecs(params_.contamination_rejection_period())) {
247       decision = false;
248       DVLOG(1) << "decision: forced to false, time: " << endpointer_time_us_;
249     } else {
250       decision = (rms > decision_threshold_);
251     }
252
253     history_->Insert(endpointer_time_us_, decision);
254
255     switch (status_) {
256       case EP_PRE_SPEECH:
257         if (history_->RingSum(params_.onset_window()) >
258             params_.onset_detect_dur()) {
259           status_ = EP_POSSIBLE_ONSET;
260         }
261         break;
262
263       case EP_POSSIBLE_ONSET: {
264         float tsum = history_->RingSum(params_.onset_window());
265         if (tsum > params_.onset_confirm_dur()) {
266           status_ = EP_SPEECH_PRESENT;
267         } else {  // If signal is not maintained, drop back to pre-speech.
268           if (tsum <= params_.onset_detect_dur())
269             status_ = EP_PRE_SPEECH;
270         }
271         break;
272       }
273
274       case EP_SPEECH_PRESENT: {
275         // To induce hysteresis in the state residency, we allow a
276         // smaller residency time in the on_ring, than was required to
277         // enter the SPEECH_PERSENT state.
278         float on_time = history_->RingSum(params_.speech_on_window());
279         if (on_time < params_.on_maintain_dur())
280           status_ = EP_POSSIBLE_OFFSET;
281         break;
282       }
283
284       case EP_POSSIBLE_OFFSET:
285         if (history_->RingSum(params_.offset_window()) <=
286             offset_confirm_dur_sec_) {
287           // Note that this offset time may be beyond the end
288           // of the input buffer in a real-time system.  It will be up
289           // to the RecognizerSession to decide what to do.
290           status_ = EP_PRE_SPEECH;  // Automatically reset for next utterance.
291         } else {  // If speech picks up again we allow return to SPEECH_PRESENT.
292           if (history_->RingSum(params_.speech_on_window()) >=
293               params_.on_maintain_dur())
294             status_ = EP_SPEECH_PRESENT;
295         }
296         break;
297
298       default:
299         LOG(WARNING) << "Invalid case in switch: " << status_;
300         break;
301     }
302
303     // If this is a quiet, non-speech region, slowly adapt the detection
304     // threshold to be about 6dB above the average RMS.
305     if ((!decision) && (status_ == EP_PRE_SPEECH)) {
306       decision_threshold_ = (0.98f * decision_threshold_) + (0.02f * 2 * rms);
307       rms_adapt_ = decision_threshold_;
308     } else {
309       // If this is in a speech region, adapt the decision threshold to
310       // be about 10dB below the average RMS. If the noise level is high,
311       // the threshold is pushed up.
312       // Adaptation up to a higher level is 5 times faster than decay to
313       // a lower level.
314       if ((status_ == EP_SPEECH_PRESENT) && decision) {
315         if (rms_adapt_ > rms) {
316           rms_adapt_ = (0.99f * rms_adapt_) + (0.01f * rms);
317         } else {
318           rms_adapt_ = (0.95f * rms_adapt_) + (0.05f * rms);
319         }
320         float target_threshold = 0.3f * rms_adapt_ +  noise_level_;
321         decision_threshold_ = (.90f * decision_threshold_) +
322                               (0.10f * target_threshold);
323       }
324     }
325
326     // Set a floor
327     if (decision_threshold_ < params_.min_decision_threshold())
328       decision_threshold_ = params_.min_decision_threshold();
329   }
330
331   // Update speech and noise levels.
332   UpdateLevels(rms);
333   ++frame_counter_;
334
335   if (rms_out)
336     *rms_out = GetDecibel(rms);
337 }
338
339 float EnergyEndpointer::GetNoiseLevelDb() const {
340   return GetDecibel(noise_level_);
341 }
342
343 void EnergyEndpointer::UpdateLevels(float rms) {
344   // Update quickly initially. We assume this is noise and that
345   // speech is 6dB above the noise.
346   if (frame_counter_ < fast_update_frames_) {
347     // Alpha increases from 0 to (k-1)/k where k is the number of time
348     // steps in the initial adaptation period.
349     float alpha = static_cast<float>(frame_counter_) /
350         static_cast<float>(fast_update_frames_);
351     noise_level_ = (alpha * noise_level_) + ((1 - alpha) * rms);
352     DVLOG(1) << "FAST UPDATE, frame_counter_ " << frame_counter_
353              << ", fast_update_frames_ " << fast_update_frames_;
354   } else {
355     // Update Noise level. The noise level adapts quickly downward, but
356     // slowly upward. The noise_level_ parameter is not currently used
357     // for threshold adaptation. It is used for UI feedback.
358     if (noise_level_ < rms)
359       noise_level_ = (0.999f * noise_level_) + (0.001f * rms);
360     else
361       noise_level_ = (0.95f * noise_level_) + (0.05f * rms);
362   }
363   if (estimating_environment_ || (frame_counter_ < fast_update_frames_)) {
364     decision_threshold_ = noise_level_ * 2; // 6dB above noise level.
365     // Set a floor
366     if (decision_threshold_ < params_.min_decision_threshold())
367       decision_threshold_ = params_.min_decision_threshold();
368   }
369 }
370
371 EpStatus EnergyEndpointer::Status(int64* status_time)  const {
372   *status_time = history_->EndTime();
373   return status_;
374 }
375
376 }  // namespace content