package com.voxeo.moho.cpa;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.log4j.Logger;
import com.voxeo.moho.Call;
import com.voxeo.moho.State;
import com.voxeo.moho.common.event.MohoCPAEvent;
import com.voxeo.moho.event.CPAEvent.Type;
import com.voxeo.moho.event.InputDetectedEvent;
import com.voxeo.moho.event.Observer;
import com.voxeo.moho.media.Input;
import com.voxeo.moho.media.input.EnergyGrammar;
import com.voxeo.moho.media.input.Grammar;
import com.voxeo.moho.media.input.InputCommand;
import com.voxeo.moho.media.input.SignalGrammar;
import com.voxeo.moho.media.input.SignalGrammar.Signal;
public class CallProgressAnalyzer implements Observer {
private Logger log = Logger.getLogger(CallProgressAnalyzer.class);
/**
* <p>
* The 'cpa-maxtime' parameter is the "measuring stick" used to determine
* 'human' or 'machine' events. If the duration of voice activity is less than
* the value of 'cpa-maxtime', the called party is considered to be 'human.'
* If voice activity exceeds the 'cpa-maxtime' value, your application has
* likely called a 'machine'.
* <p>
* The recommended value for this parameter is between 4000 and 6000ms.
*/
protected long voxeo_cpa_max_time = 4000;
/**
* <p>
* The 'cpa-maxsilence' parameter is used to identify the end of voice
* activity. When activity begins, CPA will measure the duration until a
* period of silence greater than the value of 'cpa-maxsilence' is detected.
* Armed with start and end timestamps, CPA can then calculate the total
* duration of voice activity.
* <p>
* A value of 800 to 1200ms is suggested for this parameter.
*/
protected long voxeo_cpa_final_silence = 1000;
/**
* <p>
* The 'cpa-min-speech-duration' parameter is used to identify the minimum
* duration of energy.
* <p>
* A value of (x)ms to (y)ms is suggested for this parameter.
*/
protected long voxeo_cpa_min_speech_duration = 80;
/**
* <p>
* The 'cpa-min-volume' parameter is used to identify the threshold of what is
* considered to be energy vs silence.
* <p>
* A value of (x)db to (y)db is suggested for this parameter.
*/
protected int voxeo_cpa_min_volume = -24;
protected Map<Call, ProgressStatus> _status = new ConcurrentHashMap<Call, ProgressStatus>();
public CallProgressAnalyzer() {
}
public void setMaxTime(final long voxeo_cpa_max_time) {
this.voxeo_cpa_max_time = voxeo_cpa_max_time;
}
public long getMaxTime() {
return this.voxeo_cpa_max_time;
}
public void setFinalSilence(final long voxeo_cpa_final_silence) {
this.voxeo_cpa_final_silence = voxeo_cpa_final_silence;
}
public long getFinalSilence() {
return this.voxeo_cpa_final_silence;
}
public void setMinSpeechDuration(final long voxeo_cpa_min_speech_duration) {
this.voxeo_cpa_min_speech_duration = voxeo_cpa_min_speech_duration;
}
public long getMinSpeechDuration() {
return this.voxeo_cpa_min_speech_duration;
}
public void setMinVolume(final int voxeo_cpa_min_volume) {
this.voxeo_cpa_min_volume = voxeo_cpa_min_volume;
}
public int getMinVolume() {
return this.voxeo_cpa_min_volume;
}
public void start(final Call call, Signal... signals) {
start(call, -1, -1, false, signals);
}
/**
* @param call
* @param runtime
* Maximum time duration for detection.
* @param timeout
* Maximum time limit for first media.
* @param autoreset
* Indicating whether detection will contine on receiving
* end-of-speech event
* @param signals
*/
public void start(final Call call, final long runtime, final long timeout, final boolean autoreset, Signal... signals) {
call.addObserver(this);
final Grammar[] grammars = new Grammar[(signals == null || signals.length == 0) ? 2 : signals.length + 2];
grammars[0] = new EnergyGrammar(true, false, false);
grammars[1] = new EnergyGrammar(false, true, false);
if (signals != null && signals.length > 0) {
for (int i = 0; i < signals.length; i++) {
grammars[i + 2] = new SignalGrammar(signals[i], false);
}
}
final InputCommand cmd = new InputCommand(grammars);
if (runtime > 0) {
cmd.setMaxTimeout(runtime);
}
if (timeout > 0) {
cmd.setInitialTimeout(timeout);
}
cmd.setAutoRest(autoreset);
cmd.setEnergyParameters(voxeo_cpa_final_silence, null, null, voxeo_cpa_min_speech_duration, voxeo_cpa_min_volume);
log.info("Starting " + this + "[max_time:" + voxeo_cpa_max_time + ", final_silence:" + voxeo_cpa_final_silence
+ ", min_speech:" + voxeo_cpa_min_speech_duration + ", min_volume:" + voxeo_cpa_min_volume + ", runtime:"
+ runtime + ", timeout:" + timeout + ", autoreset:" + autoreset + ", signals=" + toString(signals) + "] on "
+ call);
final Input<Call> input = call.input(cmd);
_status.put(call, new ProgressStatus(call, input));
}
public void stop(final Call call) {
ProgressStatus status = _status.remove(call);
if (status != null) {
log.info("Stopping " + this + " on " + call);
status._input.stop();
call.removeObserver(this);
}
}
@State
public void onInputDetected(final InputDetectedEvent<Call> event) {
final Call call = event.getSource();
final ProgressStatus status = _status.get(call);
if (status == null) {
return;
}
if (!event.isStartOfSpeech() && !event.isEndOfSpeech() && event.getSignal() == null) {
return;
}
log.info(event);
InputDetectedEvent<Call> lastEvent = null;
if (status._events.size() > 0) {
lastEvent = status._events.get(status._events.size() - 1);
}
status._events.add(event);
if (event.isStartOfSpeech()) {
status._lastStartOfSpeech = System.currentTimeMillis();
}
else if (event.isEndOfSpeech()) {
if (lastEvent != null && lastEvent.getSignal() != null) {
status.reset();
return;
}
if (status._lastStartOfSpeech == 0) {
log.warn("Not received START-OF-SPEECH event yet.");
status.reset();
return;
}
status._lastEndOfSpeech = System.currentTimeMillis();
status._retries += 1;
long duration = status._lastEndOfSpeech - status._lastStartOfSpeech - voxeo_cpa_final_silence;
if (duration < voxeo_cpa_max_time) {
call.dispatch(new MohoCPAEvent<Call>(event.getSource(), Type.HUMAN_DETECTED, duration, status._retries));
}
else {
call.dispatch(new MohoCPAEvent<Call>(event.getSource(), Type.MACHINE_DETECTED, duration, status._retries));
}
status.reset();
}
else if (event.getSignal() != null) {
call.dispatch(new MohoCPAEvent<Call>(event.getSource(), Type.MACHINE_DETECTED, event.getSignal()));
}
}
private String toString(final Signal[] signals) {
if (signals == null || signals.length == 0) {
return null;
}
final StringBuilder sbuf = new StringBuilder();
sbuf.append("[");
for (final Signal s : signals) {
sbuf.append(s);
sbuf.append(",");
}
sbuf.replace(sbuf.length() - 1, sbuf.length(), "]");
return sbuf.toString();
}
protected class ProgressStatus {
protected int _retries = 0;
protected long _lastStartOfSpeech = 0;
protected long _lastEndOfSpeech = 0;
protected final Input<Call> _input;
protected final Call _call;
protected final List<InputDetectedEvent<Call>> _events = new ArrayList<InputDetectedEvent<Call>>();
public ProgressStatus(final Call call, final Input<Call> input) {
_input = input;
_call = call;
}
public void reset() {
this._lastStartOfSpeech = 0;
this._lastEndOfSpeech = 0;
}
}
}