Creating Automated Attendants in UCMA 3.0 and Lync 2010: The UCMA Attendant (Part 4 of 5)

Summary:   Learn how a Microsoft Unified Communications Managed API (UCMA) 3.0 application can process dual-tone multifrequency (DTMF) tones that are sent from a Microsoft Lync 2010 application. Part 4 discusses the actions of the Microsoft Unified Communications Managed API (UCMA) 3.0 application.

Applies to:   Microsoft Unified Communications Managed API (UCMA) 3.0 Core SDK | Microsoft Lync 2010 SDK | Microsoft Speech Platform SDK

Published:   December 2011 | Provided by:   John Austin and Mark Parker, Microsoft | About the Authors

Contents

This is the fourth in a five-part series of articles about how to build client and middle-tier applications that interact by using DTMF tones in a Microsoft Lync 2010 API and using speech synthesis in a Microsoft Unified Communications Managed API (UCMA) 3.0 application.

The UCMA 3.0 application performs the following actions.

  1. Creates the platform and an endpoint.

  2. Creates a delegate to be called when an incoming call arrives.

  3. Creates and sets up a ToneController instance.

  4. Creates and sets up the speech synthesis infrastructure.

  5. Prepares and speaks a speech synthesizer prompt.

  6. Processes user key clicks.

  7. Shuts down the application.

The following sections provide detailed explanations of the steps in the previous list.

The following variable declarations are used in the application.

private UCMASampleHelper _helper;
private UserEndpoint _userEndpoint;

private AudioVideoCall _audioVideoCall;
private AudioVideoFlow _audioVideoFlow;

public const int NO_TONE = -1;
private int _cachedTone = NO_TONE;
private Object _thisLock = new Object();

private SpeechSynthesizer _speechSynthesizer;
private PromptBuilder _pb;

// Wait handles are used to keep the main thread and worker threads synchronized.
private AutoResetEvent _waitForCallToBeAccepted = new AutoResetEvent(false);
private AutoResetEvent _waitForConversationToBeTerminated = new AutoResetEvent(false);
private AutoResetEvent _waitForTransferStarted = new AutoResetEvent(false);
NoteNote

The UCMASampleHelper class is defined in UCMASampleHelper.cs.

The helper method, CreateAndEstablishUserEndpoint method, which is defined in UCMASampleHelper.cs, creates and starts a CollaborationPlatform instance, and then creates and establishes the UserEndpoint instance for the UCMA 3.0 application.

_helper = new UCMASampleHelper();
_userEndpoint = _helper.CreateEstablishedUserEndpoint("DepartmentStoreDTMF Sample User");

The following example uses the RegisterForIncomingCall<TCall> method to register a delegate that is called when an incoming audio/video call arrives.

_userEndpoint.RegisterForIncomingCall<AudioVideoCall>(AudioVideoCall_Received);

The following example shows the definition of the AudioVideoCall_Received delegate.

void AudioVideoCall_Received(object sender, CallReceivedEventArgs<AudioVideoCall> e)
{
  _audioVideoCall = e.Call;
  _audioVideoCall.AudioVideoFlowConfigurationRequested += this.AudioVideoCall_FlowConfigurationRequested;
    
  // For logging purposes, register for notification of the StateChanged event on the call.
  _audioVideoCall.StateChanged +=
            new EventHandler<CallStateChangedEventArgs>(AudioVideoCall_StateChanged);
        
  // Remote Participant URI represents the far end (caller) in this conversation. 
  Console.WriteLine("Call received from: " + e.RemoteParticipant.Uri);
        
  // Now, accept the call. 
  _audioVideoCall.BeginAccept(CallAcceptCB, _audioVideoCall);
}

Three important tasks that the delegate performs:

The following example shows the handler for the AudioVideoFlowConfigurationRequested event. Its most important task is to retrieve the Flow property from the AudioVideoFlowConfigurationRequestedEventArgs parameter, and cache it in a global variable for later use.

public void AudioVideoCall_FlowConfigurationRequested(object sender, AudioVideoFlowConfigurationRequestedEventArgs e)
{
  Console.WriteLine("Flow Created.");
  _audioVideoFlow = e.Flow;

  // Now that the flow is non-null, bind the event handler for State Changed.
  // When the flow goes active, (as indicated by the state changed event) the application can take media-related actions on the flow.
  _audioVideoFlow.StateChanged += new EventHandler<MediaFlowStateChangedEventArgs>(AudioVideoFlow_StateChanged);
}

The application uses a ToneController instance to detect DTMF tones that are sent to it by an external caller. After the ToneController instance is created, the call flow is attached by using the AttachFlow method. The flow that was obtained in the AudioVideoCall_FlowConfigurationRequested method is used as the parameter to the AttachFlow method.

Next, a handler for the ToneReceived event on the tone controller is registered. The toneController_ToneReceived method is described later in part 4.

ToneController toneController = new ToneController();
toneController.AttachFlow(_audioVideoFlow);

// Subscribe for notification when the ToneReceived event is raised. 
toneController.ToneReceived += new EventHandler<ToneControllerEventArgs>(toneController_ToneReceived);

Getting a SpeechSynthesizer instance ready to begin speaking prompts requires several preliminary steps, starting with creating a SpeechSynthesisConnector instance. After the SpeechSynthesisConnector instance is created, the call flow is attached by using the AttachFlow method.

To create and set up the speech synthesis infrastructure

  1. Create a SpeechSynthesisConnector instance.

  2. Attach the call flow to the SpeechSynthesisConnector, by using the AttachFlow method. The flow that was obtained in the AudioVideoCall_FlowConfigurationRequested method is used as the parameter to the AttachFlow method.

  3. Create a SpeechSynthesizer instance.

  4. Create a SpeechAudioFormatInfo instance, initializing it to appropriate values.

  5. Set the output of the SpeechSynthesizer by using the SetOutputToAudioStream method. The parameters to this method are the SpeechSynthesisConnector instance and the SpeechAudioFormatInfo instance that were created earlier.

  6. Register for notification of events of interest.

  7. Start the SpeechSynthesisConnector by using the Start method.

The following example shows how to set up the infrastructure for speech synthesis.

 // Create a speech synthesis connector and attach it to the AudioVideoFlow instance.
SpeechSynthesisConnector speechSynthesisConnector = new SpeechSynthesisConnector();
speechSynthesisConnector.AttachFlow(_audioVideoFlow);

// Create a speech synthesizer and set its output to the speech synthesis connector.
_speechSynthesizer = new SpeechSynthesizer();
SpeechAudioFormatInfo audioformat = new SpeechAudioFormatInfo(16000, AudioBitsPerSample.Sixteen, Microsoft.Speech.AudioFormat.AudioChannel.Mono);
_speechSynthesizer.SetOutputToAudioStream(speechSynthesisConnector, audioformat);

// Register for notification of the SpeakCompleted and SpeakStarted events on the speech synthesizer.
_speechSynthesizer.SpeakStarted += new EventHandler<SpeakStartedEventArgs>(SpeechSynthesizer_SpeakStarted);
_speechSynthesizer.SpeakCompleted += new EventHandler<SpeakCompletedEventArgs>(SpeechSynthesizer_SpeakCompleted);

// Start the speech synthesis connector.
speechSynthesisConnector.Start();

At this point, the speech synthesizer is ready to begin speaking prompts.

The following example creates a PromptBuilder instance to hold the first prompt that is spoken to the user. After welcoming the user to Northwind Traders, the user is asked to click 1, 2, or 3 to be connected with one of the three departments. The user will be asked later for a specific subdepartment within the chosen department.

string[] departments = new string[] { "", "sporting goods", "kitchen goods", "automotive parts and services" };
_pb = new PromptBuilder();
_pb.AppendText("Welcome to Northwind Traders.");
_pb.AppendBreak(new TimeSpan(0, 0, 0, 0, 500));
_pb.AppendText("Press 1 for " + departments[1] + ".");
_pb.AppendBreak(new TimeSpan(0, 0, 0, 0, 250));
_pb.AppendText("Press 2 for " + departments[2] + ".");
_pb.AppendBreak(new TimeSpan(0, 0, 0, 0, 250));
_pb.AppendText("Press 3 for " + departments[3] + ".");
_speechSynthesizer.SpeakAsync(_pb);

When the user clicks a key on the keypad, a DTMF tone is emitted, which causes the ToneReceived event to be raised. The handler for this event contains all of the logic to process user DTMF tones.

Because the user has to click two keys to navigate through the two-tier menu hierarchy, the handler must differentiate between the first key click and the second. The handler stores the first key press in a global variable, _cachedTone.

The following example shows pseudocode for the logic that is used in this event handler.

Cancel the prompt that is currently being spoken
if (the current key press is the first one)
   Store the key press
   Prompt the user to press a key for the appropriate subdepartment
else
   Cancel the prompt that is currently being spoken
   Play a prompt that informs the user of the department he/she is being transferred to
endif

Important noteImportant

The application does not actually transfer the user. To add this functionality, use the BeginTransfer method on the call.

If the user clicks a key before the prompt is played to its end, the SpeakAsyncCancelAll method causes the prompt to stop playing. The reasoning here is that the user has made a choice, so no longer needs the prompt.

The following example shows the handler for the ToneReceived event.

void toneController_ToneReceived(object sender, ToneControllerEventArgs e)
{
  int newTone = NO_TONE;
  string[,] subDepartments = new string[,] 
  {
     { "", "", "", "" },
     { "", "team sports", "fishing and camping", "individual sports" },
     { "", "kitchen appliances", "tableware", "cookware" },
     { "", "tires and brakes", "automotive services", "auto accessories" }
  };
  // The user already pressed a number key, so there's no need to continue with prompts.
  _speechSynthesizer.SpeakAsyncCancelAll();

  // Has a tone already been received? If not, cache the tone.
  if (_cachedTone == NO_TONE)
  {
    Console.WriteLine("Tone Received: " + (ToneId)e.Tone + " (" + e.Tone + ")");
    
    lock (this)
    {
      _cachedTone = e.Tone;
      newTone = e.Tone;
    }
    _pb.ClearContent();
    // Prepare the second-level prompt.
    _pb.AppendText("Press 1 for " + subDepartments[_cachedTone, 1] + ". ");
    _pb.AppendBreak(new TimeSpan(0,0,0,0,250));
    _pb.AppendText("Press 2 for " + subDepartments[_cachedTone, 2] + ". ");
    _pb.AppendBreak(new TimeSpan(0, 0, 0, 0, 250));
    _pb.AppendText("Press 3 for " + subDepartments[_cachedTone, 3] + ". ");
    _pb.AppendBreak(new TimeSpan(0, 0, 0, 0, 250));

    _speechSynthesizer.SpeakAsync(_pb);
  }

  // We have already received the first tone. The second tone indicates the subdepartment
  // to be transferred to.
  else
  {
    // The user already pressed a number key, so there's no need to continue with prompts.
    _speechSynthesizer.SpeakAsyncCancelAll();
    Console.WriteLine("Tones Received: " + (ToneId)_cachedTone + " (" + _cachedTone + ")" + (ToneId)e.Tone + " (" + e.Tone + ")");
    newTone = e.Tone;
    _speechSynthesizer.Speak("Transferring to " + subDepartments[_cachedTone, newTone] + ".");
    _waitForTransferStarted.Set();
    _cachedTone = NO_TONE;
  }
}

The application’s final prompt is, “Transferring to …” After this prompt, the application begins its shutdown process.

To shut down the application

  1. Stop the SpeechSynthesisConnector, by using the Stop method.

  2. Detach the flow from the SpeechSynthesisConnector, by using the DetachFlow method.

  3. Terminate the call and the conversation, and unregister the AudioVideoCall_Received delegate (the delegate that is invoked when a call arrives).

    The call is ended by using the BeginTerminate method on the call. The conversation is ended by using the BeginTerminate method on the conversation instance. The delegate is unregistered by using the UnregisterForIncomingCall<TCall> method on the endpoint.

    Note Note

    The following sample does not show all of these method calls. For more information, see the CallTerminateCB and ConversationTerminateCB callback methods in Creating Automated Attendants in UCMA 3.0 and Lync 2010: Code Listing and Conclusion (Part 5 of 5).

  4. Shut down the CollaborationPlatform instance, by using the ShutdownPlatform helper method, which is defined in UCMASampleHelp.cs.

// Stop the speech synthesis connector.
speechSynthesisConnector.Stop();
Console.WriteLine("Stopping the speech synthesis connector.");

speechSynthesisConnector.DetachFlow();

UCMASampleHelper.PauseBeforeContinuing("Press ENTER to shut down and exit.");

// Terminate the call, the conversation, and then unregister the 
// endpoint from receiving an incoming call. 
_audioVideoCall.BeginTerminate(CallTerminateCB, _audioVideoCall);
_waitForConversationToBeTerminated.WaitOne();
 
// Clean up by shutting down the platform.
_helper.ShutdownPlatform();

John Austin, Microsoft, is a programmer/writer in the Lync client SDK documentation team. He has been writing Microsoft technical documentation for four years. Prior to working for Microsoft, John spent two decades as a software developer. Mark Parker is a programming writer at Microsoft whose current responsibility is the UCMA SDK documentation. Mark previously worked on the Microsoft Speech Server 2007 documentation.

Show: