Creating Automated Attendants in UCMA 3.0 and Lync 2010: Code Listing and Conclusion (Part 5 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 5 provides code listings for the UCMA 3.0 application and the Lync 2010 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

  • Lync Application Code

  • UCMA Application Code

  • Conclusion

  • Additional Resources

This is the last 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.

Lync Application Code

Namespace Using Declarations

This example shows the using statements that are added to the cash register and conversation window classes

using Microsoft.Lync.Model;
using Microsoft.Lync.Model.Conversation;
using Microsoft.Lync.Model.Conversation.AudioVideo;

CashRegister Class

The examples in this section are taken from the CashRegister form.

CashRegister Class Field Declarations

The following example declares class fields that support the audio call features of the cash register.

#region private Lync audio calling fields
// Holds the Lync client instance.
private LyncClient client;

// Controls created in this sample.
private SignInControl signInControl;
private PhonePadControl phonePadControl;
private System.Windows.Forms.Button Call_Button = new Button();
private string dialString = string.Empty;
private Boolean itemIsOdd = true;
private Contact contactToCall;



// Save a list of the conversation windows, so they can be closed
// when a conversation is removed.
private Dictionary<Conversation, ConversationWindow> conversationWindows = new Dictionary<Conversation, ConversationWindow>();

// Used to prevent Lync events from firing before the sample conversation window is loaded.
private AutoResetEvent conversationWindowLock = new AutoResetEvent(false);

#endregion

CashRegister Constructor

This example is the constructor for the CashRegister window.

/// <summary>
/// Form constructor.
/// </summary>
public CashRegister()
{
   InitializeComponent();

   try
   {
        // Obtains the lync _client instance.
        client = LyncClient.GetClient();

        // Register for state updates. Whenever the state of _client changes, this
        // event is fired. For example, during Sign-In it will move from:
        // SignedOut -> SigningIn -> SignedIn
        client.StateChanged += client_StateChanged;

        // Register for the _client disconnected event.
        // When the Lync process exits, it will fire this event.
        client.ClientDisconnected += client_ClientDisconnected;

        // Create the Sign-In user control.
        signInControl = new SignInControl(client);

        // Create the phonePad control for numeric dialing.
        phonePadControl = new PhonePadControl();
        phonePadControl.PlayAudioType = "DTMF";

   }
   // If the Lync process is not running and UISuppressionMode is false,
   // this exception will be thrown.
   catch (ClientNotFoundException)
   {
      // Explain to the user what happened.
      MessageBox.Show("Microsoft Lync does not appear to be running. Please start Lync.");

      // Exit (in a fully implemented application, a retry here would be recommended).
      Application.Exit();
   }
   catch (NotStartedByUserException)
   {
      // Explain to the user what happened.
      MessageBox.Show("Microsoft Lync does not appear to be running. Please start Lync.");

      // Exit (in a fully implemented application, a retry here would be recommended).
      Application.Exit();
   }
}

CashRegister.Load Event Handler

This example is the CashRegister form load event.

/// <summary>
/// Updates the initial UI state and registers for client events.
/// </summary>
private void CashRegister_Load(object sender, EventArgs e)
{
   // Show the current client state.
   toolStripStatusLabel.Text = client.State.ToString();

   // Register for state updates. Whenever the client changes its state, this
   // event will be fired. For example, during Sign-In it will likely move from:
   // SignedOut -> SigningIn -> SignedIn.
   client.StateChanged += client_StateChanged;

   // Register for the client exiting event.
   // When/if the Lync process exits, it will fire this event.
   client.ClientDisconnected += client_ClientDisconnected;

   //***********************************************************************************
   // This application works with UISuppressionMode set to either true or false.
   //
   // UISuppressionMode hides the Lync user interface.
   //
   // Registry key for enabling UISuppressionMode:
   //
   // 32bit OS:
   // [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Communicator]
   // "UISuppressionMode"=dword:00000001
   //
   // 64bit OS:
   // [HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Communicator]
   // "UISuppressionMode"=dword:00000001
   //
   // When running with UISuppressionMode = 1 and this application is the only one
   // using the client, it's necessary to Initialize the client. The following check
   // determines whether the client has already been initialized. If it hasn't, the code
   // calls BeginInitialize() providing a callback method, on which this application's
   // main UI will be presented (either Sign-In or contact input, if already signed in).
   //***********************************************************************************

   // If this client is in UISuppressionMode...
   if (client.InSuppressedMode && client.State == ClientState.Uninitialized)
   {
      //...need to initialize it.
      try
      {
         client.BeginInitialize(this.ClientInitialized, null);
      }
      catch (LyncClientException lyncClientException)
      {
         Console.WriteLine(lyncClientException);
      }
      catch (SystemException systemException)
      {
         if (LyncModelExceptionHelper.IsLyncException(systemException))
         {
            // Log exceptions thrown by the Lync Model API.
            Console.WriteLine("Error: " + systemException);
         }
         else
         {
            // Rethrow any SystemException that did not come from the Lync Model API.
            throw;
         }
      }
   }
   else // Not in UI Suppression, so the client was already initialized.
   {
      // Register for conversation-related events.
      // These events will occur when new conversations are created (incoming/outgoing) and removed.
      client.ConversationManager.ConversationAdded += ConversationManager_ConversationAdded;
      client.ConversationManager.ConversationRemoved += ConversationManager_ConversationRemoved;

      // Show sign-in or contact selection.
      ShowMainContent();
   }
   Admin_Button.Select();
}

CashRegister.FormClosing Event Handler

You must include the following code in your form closing event handler. When Lync 2010 is configured for UI Suppression, any application that initializes the Lync platform must sign the user out and shut down the platform before closing the application that initialized the platform. Failure to do so can prevent the platform from initializing again.

        /// <summary>
        /// Unregisters from _client related events.
        /// </summary>
        private void CashRegister_FormClosing (object sender, FormClosingEventArgs e)
        {
            try
            {
                client.StateChanged -= client_StateChanged;
                client.ClientDisconnected -= client_ClientDisconnected;
                client.ConversationManager.ConversationAdded -= ConversationManager_ConversationAdded;
                client.ConversationManager.ConversationRemoved -= ConversationManager_ConversationRemoved;

                // If the client is in UI suppression state, application must
                // sign the user out and shut the Lync process down.
                if (client.InSuppressedMode == true)
                {
                    if (client.State == ClientState.SignedIn)
                    {
                        client.EndSignOut(client.BeginSignOut(null, null));
                        if (client.State == ClientState.SignedOut)
                        {
                            // BeginShutdown does not return execution to calling thread
                            // if Lync Client is NOT UI suppressed!
                            client.EndShutdown(client.BeginShutdown(null, null));
                        }
                    }
                }
            }
            catch (LyncClientException ex)
            {
                Console.WriteLine(ex);
            }
            catch (SystemException systemException)
            {
                if (LyncModelExceptionHelper.IsLyncException(systemException))
                {
                    // Log the exception thrown by the Lync Model API.
                    Console.WriteLine("Error: " + systemException);
                }
                else
                {
                    // Rethrow the SystemException which did not come from the Lync Model API.
                    throw;
                }
            }
        }

LyncClient Event Handlers

The following example is a set of Microsoft.Lync.Model.LyncClient event handlers

#region LyncClient Event handlers
/// <summary>
/// Called when the client is finished being initialized.
/// </summary>
/// <param name="result"></param>
private void ClientInitialized(IAsyncResult result)
{
   // Register for conversation-related events.
   // These events will occur when new conversations are created (incoming/outgoing) and removed.
   client.ConversationManager.ConversationAdded += ConversationManager_ConversationAdded;
   client.ConversationManager.ConversationRemoved += ConversationManager_ConversationRemoved;
}

/// <summary>
/// Called when the state of the Lync client changes.
/// Will show the correct controls on the main window based on the client state.
/// </summary>
void client_StateChanged(object sender, ClientStateChangedEventArgs e)
{
   // Post the execution into the UI thread.
   this.BeginInvoke(new MethodInvoker(delegate()
   {
      // Show the current client state.
      toolStripStatusLabel.Text = e.NewState.ToString();

      // Show the main content based on the client state.
      ShowMainContent();
   }));
}

/// <summary>
/// Called when Lync is exiting.
/// </summary>
void client_ClientDisconnected(object sender, EventArgs e)
{
   // Post the execution into the UI thread.
   this.BeginInvoke(new MethodInvoker(delegate()
   {
      Console.Out.WriteLine("Lync has disconnected");
      Application.Exit();
   }));
}
#endregion

Connect button Click Event Handler

The following example gets a Microsoft.Lync.Model.Contact that represents the dialed phone number and then adds a new Microsoft.Lync.Model.Conversation.Conversation.

/// <summary>
/// Reads the text of a button and notifies that it was pressed with the proper DTMF text.
/// </summary>
private void Connectbutton_Click(object sender, EventArgs e)
{
   if (dialString.Trim().Length == 0)
   {
      return;
   }
   contactToCall = client.ContactManager.GetContactByUri(dialString);

   // Clear the dialpad display for a new call.
   phonePadControl.ClearDialedString();
   dialString = string.Empty;

   // Create a new conversation.
   Conversation conversation = null;
   try
   {
      conversation = client.ConversationManager.AddConversation();
   }
   catch (LyncClientException ex)
   {
      Console.WriteLine(ex);
   }
   catch (SystemException systemException)
   {
      if (LyncModelExceptionHelper.IsLyncException(systemException))
      {
         // Log exceptions thrown by the Lync Model API.
         Console.WriteLine("Error: " + systemException);
      }
      else
      {
         // Rethrow any SystemException that did not come from the Lync Model API.
         throw;
      }
   }
}

ConversationManager.ConversationAdded Event Handler

This event handler is where most of the action takes place. The Microsoft.Lync.Model.Contact that represents the dialed phone number is added to the new conversation, the conversation audio/video modality is connected, and then a ConversationWindow is created and opened.

//*****************************************************************************************
//                              ConversationManager Event Handling
// 
// ConversationAdded occurs when:
// 1) A new conversation was created by this application.
// 2) A new conversation was created by another third party application or Lync itself.
// 3) An invite was received at this endpoint (InstantMessaging / AudioVideo).
//
// ConversationRemoved occurs when a conversation is terminated.
//
//*****************************************************************************************

/// <summary>
/// Called when a new conversation is added (incoming or outgoing).
/// 
/// Creates a window for this new conversation and shows it.
/// </summary>
void ConversationManager_ConversationAdded(object sender, ConversationManagerEventArgs e)
{

   // Create a new window (which will register for Conversation and child object events).

   // Start an audio call or conference by connecting the AvModality.
   try
   {
      // Add a participant to the conversation.
      // The window created for this conversation will handle ParticipantAdded events.
      if (contactToCall != null && e.Conversation != null)
      {
         try
         {
            e.Conversation.AddParticipant(contactToCall);
         }
         catch (LyncClientException lyncClientException)
         {
            Console.WriteLine(lyncClientException);
         }
         catch (SystemException systemException)
         {
            if (LyncModelExceptionHelper.IsLyncException(systemException))
            {
               // Log exceptions thrown by the Lync Model API.
               Console.WriteLine("Error: " + systemException);
            }
            else
            {
               // Rethrow any SystemException that did not come from the Lync Model API.
               throw;
            }
         }
      }
   }
   catch (LyncClientException lyncClientException)
   {
      Console.WriteLine(lyncClientException);
   }
   catch (SystemException systemException)
   {
      if (LyncModelExceptionHelper.IsLyncException(systemException))
      {
         // Log exceptions thrown by the Lync Model API.
         Console.WriteLine("Error: " + systemException);
      }
      else
      {
         // Log SystemExceptions that did not come from the Lync Model API.
         Console.WriteLine("System Error: " + systemException);
      }
   }
   
   
   ConversationWindow window = new ConversationWindow(e.Conversation);

   // Post the execution into the UI thread.
   this.BeginInvoke(new MethodInvoker(delegate()
   {
      // Add the window to the dictionary.
      conversationWindows.Add(e.Conversation, window);

      // Register for window load events so that the lock of the Lync thread can be released.
      window.Load += window_Load;

      // Show the new window.
      window.Show(this);

   }));

   // Wait until the window is loaded to release the SDK thread.
   conversationWindowLock.WaitOne();
}

This example is a set of private utility methods that show or hide the phone pad control and the sign in control.

#region Phone methods

/// <summary>
/// Releases the lock from the Lync thread when the conversation window is finished being loaded.
/// </summary>
void window_Load(object sender, EventArgs e)
{
   // Release the lock used to hold the Lync SDK thread.
   conversationWindowLock.Set();
}
/// <summary>
/// Shows the phone pad control.
/// </summary>
private void ShowPhonePadControl()
{
   // Remove the sign-in control / pending sign-in/out message.
   tableLayoutPanel.Controls.Remove(signInControl);
   tableLayoutPanel.Controls.Remove(labelPendingSignInOut);

   // Add the contact selection control to the second row of the panel.
   tableLayoutPanel.Controls.Add(phonePadControl, 0, 1);
   this.Call_Button.Font = new System.Drawing.Font("Microsoft Sans Serif", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
   this.Call_Button.Name = "Call_Button";
   this.Call_Button.Size = new System.Drawing.Size(112, 33);
   this.Call_Button.TabIndex = 0;
   this.Call_Button.Text = "Connect";
   this.Call_Button.UseVisualStyleBackColor = true;
   this.Call_Button.Click += new System.EventHandler(this.Connectbutton_Click);
   phonePadControl.ButtonClickedEvent += new ButtonClicked(PhonePadControl_buttonClicked);
   tableLayoutPanel.Controls.Add(Call_Button, 0, 2);

}

/// <summary>
/// Shows the Sign-In control.
/// </summary>
private void ShowSignInControl()
{
   // Remove the contact selection control / pending sign-in/out message.
   tableLayoutPanel.Controls.Remove(labelPendingSignInOut);
   tableLayoutPanel.Controls.Remove(phonePadControl);

   // Add the sign-in control to the second row of the panel.
   tableLayoutPanel.Controls.Add(signInControl, 0, 1);
}

/// <summary>
/// Shows a pending sign-in/out message.
/// </summary>
private void ShowPendingSignInOut()
{
   // Remove the contact selection or sign-in control.
   tableLayoutPanel.Controls.Remove(signInControl);

   // Add the pending sign-in/out message to the second row of the panel.
   tableLayoutPanel.Controls.Add(labelPendingSignInOut, 0, 1);
}

/// <summary>
/// After the client is initialized, shows either sign-in or contact selection controls.
/// </summary>
private void ShowMainContent()
{
   // Depending on the client state, show sign-in panel or the contact selection.
   if (client.State == ClientState.SignedIn)
   {
      // No sign-in necessary, show the contact list selection.
      ShowPhonePadControl();
   }
   else if (client.State == ClientState.SignedOut)
   {
      // Sign-in is needed, so show the sign-in control.
      ShowSignInControl();
   }
   else
   {
      // The client here could also be in the SigningIn or SigningOut state.
      // The application should take the appropriate action.
      // SigningIn: the application could wait for the SignedIn state or call lyncClient.BeginSignOut().
      // SigningOut: the application should wait for the SignedOut state.

      // Show a pending sign-in/out message.
      ShowPendingSignInOut();

      // The client_StateChanged() below will take care of showing the proper
      // controls when the state changes to SignedIn or SignedOut.
   }
}
#endregion

ConversationWindow Class

The examples in this section are taken from the ConversationWindow class.

ConversationWindow Class Declaration

The following example declares the ConversationWindow class.

namespace NorthwindCashRegister
{
   /// <summary>
   /// Implements a conversation window for the AvModality.
   /// 
   /// Each button represents one possible action that may be taken on the Conversation, 
   /// AvModality, Audio or Video channels.
   /// 
   /// The button’s state (Enabled true/false) is controlled by the ActionAvailability event.
   /// All of the buttons start with Enabled set to false. During the conversation life-cycle
   /// the buttons will become enabled / disabled.
   /// 
   /// Most of the actions take a callback method as an argument. This is used to notify the caller 
   /// asynchronously that an operation has finished. This implementation does not use the callback
   /// option since this window only shows very simple status of the conversation state.
   /// 
   /// The state of the conversation life-cycle is obtained through StateChanged events. Those will
   /// be raised independently by the Conversation, AvModality, AudioChannel and VideoChannel objects.
   /// </summary>
   public partial class ConversationWindow : Form
   {
   #region Fields

      // A reference to the conversation associated with this window.
      private Conversation _conversation;

      // Self participant's AvModality.
      private AVModality avModality;

      // Self participant's channels.
      private AudioChannel audioChannel;

      // Phone dial pad user control.
      private PhonePadControl phonePadControl;

   #endregion
...
   }
...
}

ConversationWindow Constructor

The following example constructs a conversation window

/// <summary>
/// Initiates the window for the specified conversation.
/// </summary>
public ConversationWindow(Conversation conversation)
{
   InitializeComponent();


   //*****************************************************************************************
   //                              Registering for events
   //
   // It is very important that registering for an object's events happens within the handler
   // of that object's added event. In another words, the application should register for 
   // conversation events within the ConversationAdded event handler.
   //
   // This is required to avoid timing issues which would cause the application to miss events.
   // While this handler method is executing, the Lync client is unable to process events for  
   // this application (since its thread is running this method), so no events will be lost.
   //
   // By registering for events here, we guarantee that all conversation-related events will be 
   // caught the first time they occur.
   //
   // We want to show the availability of the buttons in the conversation window based
   // on the ActionAvailability events. The solution below uses a lock to allow the window
   // to load while holding the event queue. This prevents events from being raised even 
   // before the user interface controls get a chance to load.
   //
   //*****************************************************************************************

   // Save the conversation reference.
   _conversation = conversation;

   // Save AVModality and AudioChannel, for the sake of readability.
   avModality = (AVModality) _conversation.Modalities[ModalityTypes.AudioVideo];
   audioChannel = avModality.AudioChannel;

   // Show the current conversation and modality states in the UI.
   toolStripStatusLabelConvesation.Text = _conversation.State.ToString();
   toolStripStatusLabelModality.Text = avModality.State.ToString();

   // Register for conversation state updates.
   _conversation.StateChanged += conversation_StateChanged;

   // Register for participant events.
   _conversation.ParticipantAdded += conversation_ParticipantAdded;
   _conversation.ParticipantRemoved += conversation_ParticipantRemoved;

   // Subscribe to the conversation action availability events (for the ability to add/remove participants).
   _conversation.ActionAvailabilityChanged += conversation_ActionAvailabilityChanged;

   // Subscribe to modality action availability events (all audio buttons except DTMF).
   avModality.ActionAvailabilityChanged += avModality_ActionAvailabilityChanged;

   // Subscribe to the modality state changes so that the status bar gets updated with the new state.
   avModality.ModalityStateChanged += avModality_ModalityStateChanged;

   // Subscribe to the audio channel action availability events (DTMF only).
   audioChannel.ActionAvailabilityChanged += audioChannel_ActionAvailabilityChanged;

   // Subscribe to the audio channel state changes so that the status bar gets updated with the new state.
   audioChannel.StateChanged += audioChannel_StateChanged;

   avModality = (AVModality)_conversation.Modalities[ModalityTypes.AudioVideo];
   AsyncCallback callback = new AsyncOperationHandler(avModality.EndConnect).Callback;
   avModality.BeginConnect(callback, null);

   // Create the phonePad control for DTMF dialing.
   phonePadControl = new PhonePadControl();
   phonePadControl.ButtonClickedEvent += new ButtonClicked(Phonepad_ButtonClicked);
   phonePadControl.Anchor = AnchorStyles.None;
   phonePadControl.Enabled = false;
   phonePadControl.InConversation = false;
   phonePadControl.PlayAudioType = "DTMF";

   tableLayoutPanel1.Controls.Add(phonePadControl, 0, 1);

}

Conversation.ParticipantAdded Event Handler

This example adds a call participant from the conversation roster.

/// <summary>
/// Called when a participant is added to the conversation.
/// 
/// Adds the participant to the roster listbox.
/// </summary>
void conversation_ParticipantAdded(object sender, ParticipantCollectionChangedEventArgs e)
{
   // Error case, the participant wasn't actually added.
   if (e.StatusCode < 0)
   {
      Console.Out.WriteLine("Participant was not added: code=" + e.StatusCode);
      return;
   }

   // Post the execution into the UI thread.
   this.BeginInvoke(new MethodInvoker(delegate()
   {
      // Create a new item and add to the roster listbox.
      listBoxRosterContacts.Items.Add(new ParticipantItem(e.Participant));
   }));

}

/// <summary>
/// Called when a participant is removed from the conversation.
/// 
/// Removes the participant from the roster listbox.
/// </summary>
void conversation_ParticipantRemoved(object sender, ParticipantCollectionChangedEventArgs e)
{
   // Error case, the participant wasn't actually removed.
   if (e.StatusCode < 0)
   {
      Console.Out.WriteLine("Participant was not removed: code=" + e.StatusCode);
      return;
   }

   // Post the execution into the UI thread.
   this.BeginInvoke(new MethodInvoker(delegate()
   {
      // Find the position of the participant to be removed in the roster listbox.
      int removePosition = -1;
      for (int i = 0; i < listBoxRosterContacts.Items.Count; i++)
      {
         ParticipantItem item = listBoxRosterContacts.Items[i] as ParticipantItem;
         if (item != null && item.Participant.Equals(e.Participant))
         {
            removePosition = i;
            break;
         }
      }

      // Remove the participant from the roster listbox.
      if (removePosition > 0)
      {
         listBoxRosterContacts.Items.RemoveAt(removePosition);
      }
   }));
}

Conversation.StateChanged Event Handler

This example updates the text of the conversation status label when the state of the conversation changes.

/// <summary>
/// Called when the conversation state changes.
/// 
/// Updates the status bar.
/// </summary>
void conversation_StateChanged(object sender, ConversationStateChangedEventArgs e)
{
   // Post the execution into the UI thread.
   this.BeginInvoke(new MethodInvoker(delegate()
   {
      // Show the current conversation state.
      toolStripStatusLabelConvesation.Text = e.NewState.ToString();
   }));
}

Modality Event Handlers

This example updates the text of the modality state label and enables the phone pad if the modality is connected.

#region Modality event handling

//*****************************************************************************************
//                              Modality event handling
//*****************************************************************************************

void avModality_ModalityStateChanged(object sender, ModalityStateChangedEventArgs e)
{
   // Post the execution into the UI thread.
   this.BeginInvoke(new MethodInvoker(delegate()
   {
      // Update the status bar with the audio channel state.
      toolStripStatusLabelModality.Text = e.NewState.ToString();
   }));
   if (e.NewState == ModalityState.Connected)
   {
      // Post the execution into the UI thread.
      this.BeginInvoke(new MethodInvoker(delegate()
      {
         phonePadControl.Enabled = audioChannel.CanInvoke(ChannelAction.SendDtmf);
      }));
   }
}

/// <summary>
/// Called when the availability of an action changes.
/// 
/// Will Enable/Disable buttons based on the availability.
/// </summary>
void avModality_ActionAvailabilityChanged(object sender, ModalityActionAvailabilityChangedEventArgs e)
{
   // Post the execution into the UI thread.
   this.BeginInvoke(new MethodInvoker(delegate()
   {
      // Each action is mapped to a button in the UI.
      if (e.Action == ModalityAction.Connect)
      {
         // Whatever the availability of connect is, disconnect 
         // has the opposite availability.
         buttonDisconnectAudio.Enabled = !e.IsAvailable;
      }

   }));
}

Audio Channel Event Handlers

This example updates the text of the audio channel state label.

#region Audio channel event handling

//*****************************************************************************************
//                              AudioChannel event handling
//*****************************************************************************************

/// <summary>
/// Updates the status bar when the new Audio Channel state.
/// </summary>
void audioChannel_StateChanged(object sender, ChannelStateChangedEventArgs e)
{
   // Post the execution into the UI thread.
   this.BeginInvoke(new MethodInvoker(delegate()
   {
      // Update the status bar with the audio channel state.
      toolStripStatusLabelAudioChannel.Text = e.NewState.ToString();       
   }));
}

/// <summary>
/// Called when the action availability changes for the action channel.
/// This event can be fired prematurely. The
/// AVModality has not connected yet and this event fires. 
/// Instead of enabling phone control in this event, enable
/// the control in the modalitystatechanged event when the modality state
/// is connected and the AudioChannel.CanInvoke(ChannelAction.SendDTMF) returns true.
/// Will Enable/Disable buttons based on the availability.
/// </summary>
void audioChannel_ActionAvailabilityChanged(object sender, ChannelActionAvailabilityEventArgs e)
{
   // Post the execution into the UI thread.
   this.BeginInvoke(new MethodInvoker(delegate()
   {
      // Only SendDtmf is used here since the buttons are already mapped
      // to the action availability of the modality itself.
      if (e.Action == ChannelAction.SendDtmf && avModality.State == ModalityState.Connected)
      {
         phonePadControl.Enabled = e.IsAvailable;
      }

   }));

}

#endregion

Phonepad_ButtonClicked Event Handler

The following example is a conversation window method that handles the dial pad event that is raised when a user clicks a button on the dial pad. The example verifies that the send DTMF action is available, and then sends a tone represented by the method parameter.

/// <summary>
/// Called when a button is pressed in the dial pad dialog.
/// 
/// Sends the DTMF tone using AudioChannel.BeginSendDtmf()
/// </summary>
void Phonepad_ButtonClicked(string tone)
{
   if (audioChannel.CanInvoke(ChannelAction.SendDtmf))
   {
      // Send a set of characters (in this case one) as a dial tone.
      try
      {
         AsyncCallback callback = new AsyncOperationHandler(audioChannel.EndSendDtmf).Callback;
         audioChannel.BeginSendDtmf(tone, callback, null);
      }
      catch (LyncClientException lyncClientException)
      {
         Console.WriteLine(lyncClientException);
      }
      catch (SystemException systemException)
      {
         if (LyncModelExceptionHelper.IsLyncException(systemException))
         {
            // Log exceptions thrown by the Lync Model API.
            Console.WriteLine("Error: " + systemException);
         }
         else
         {
            // Log SystemExceptions that did not come from the Lync Model API.
            Console.WriteLine("System Error: " + systemException);
         }
      }
   }
}

DisconnectButton Click Event Handler

The following example handles the buttonDisconnectAudio.Click event.

/// <summary>
/// Disconnects the modality: AvModality.BeginDisconnect()
/// </summary>
private void buttonDisconnectAudio_Click(object sender, EventArgs e)
{
   // End an audio call or conference by disconnecting the AvModality.
   try
   {
      AsyncCallback callback = new AsyncOperationHandler(avModality.EndDisconnect).Callback;
      avModality.BeginDisconnect(ModalityDisconnectReason.None, callback, null);
   }
   catch (LyncClientException lyncClientException)
   {
      Console.WriteLine(lyncClientException);
   }
   catch (SystemException systemException)
   {
      if (LyncModelExceptionHelper.IsLyncException(systemException))
      {
         // Log exceptions thrown by the Lync Model API.
         Console.WriteLine("Lync Error: " + systemException);
      }
      else
      {
         // Log any SystemException that did not come from the Lync Model API.
         Console.WriteLine("System Error: " + systemException);
      }
   }
}

PhonePadControl Code

The primary purpose of the phonepad control is to present a phone keypad to a user and notify any listening code that a user has clicked a button and identifies the text of the clicked button. To more closely simulate telephone keypad, PhonePadControl calls DeviceManager.BeginPlayAudioFile and passes the name of a .wav file to be played. The Northwind Traders Cash Register sample application provides a set of sample .wav files representing the 12 DTMF tones that are used on the phone pad.

The following example constructs the PhonePadControl and responds to button click events on the control.

using System;
using System.Drawing;
using System.Windows.Forms;
using Microsoft.Lync.Model;
using Microsoft.Lync.Model.Device;

namespace NorthwindCashRegister
{
    /// <summary>
    /// Called when a button in the dial pad dialog is pressed.
    /// </summary>
    public delegate void ButtonClicked(string tone);



    public partial class PhonePadControl : UserControl
    {
        /// <summary>
        /// Occurs when a button in the dial pad dialog is pressed.
        /// </summary>
        public event ButtonClicked ButtonClickedEvent;

        #region private Lync API object Model fields

        /// <summary>
        /// The Lync client API platform. 
        /// </summary>
        private LyncClient _LyncClient = LyncClient.GetClient();
        #endregion

        #region private support fields

        /// <summary>
        /// Active conversation flag. If true then do not play DTMF tones.
        /// </summary>
        private Boolean _InConversation = false;

        /// <summary>
        /// Indicates which set of audio files are played when a user clicks a button. 
        /// Choices include DTMF or speech files.
        /// </summary>
        private string _PlayAudioType = "DTMF";

        /// <summary>
        /// The path to the speech audio files.
        /// </summary>
        private string _speechPath = "";

        /// <summary>
        /// The path to the local user's profile folders.
        /// </summary>
        string _userProfilePath = "";

        /// <summary>
        /// The path to the DTMF audio files.
        /// </summary>
        private string _DTMFPath = "";

        #endregion

        #region public properties
        /// <summary>
        /// Set this property value to true when PhonePad is opened while an audio conversation is active.
        /// </summary>
        public Boolean InConversation
        {
            set
            {
                _InConversation = value;
            }
            get
            {
                return _InConversation;
            }
        }

        /// <summary>
        /// Set to string values of either "Speech" or "DTMF". The
        /// set value dictates whether DTMF or speech wav files are played.
        /// </summary>
        public string PlayAudioType
        {
            set
            {
              
                _PlayAudioType = value;

                //If invalid value is supplied, set string to DTMF.
                if (_PlayAudioType != "DTMF" && _PlayAudioType != "Speech")
                {
                    _PlayAudioType = "DTMF";
                }
            }
            get
            {
                return _PlayAudioType; 
            }
        }

        /// <summary>
        /// The concatenated string of user button clicks
        /// </summary>
        public string DialString
        {
            get
            {
                return labelSentDTMFs.Text;
            }
        }
        #endregion

        #region constructors
        public PhonePadControl()
        {
            InitializeComponent();
            this.BackColor = SystemColors.Control;
            labelSentDTMFs.Text = string.Empty;

            // Initialize audio file paths to local user desktop folder + sample application path + audio file folders.
            _userProfilePath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory).ToString();
            _DTMFPath = _userProfilePath + @"\Lync_SDK_Samples\NorthwindCashRegister\DTMF_Files\";
            _speechPath = _userProfilePath + @"\Lync_SDK_Samples\NorthwindCashRegister\Voice_Files\";

        }
        #endregion

        #region public methods
        /// <summary>
        /// Clears the string of dialed characters.
        /// </summary>
        public void ClearDialedString()
        {
            labelSentDTMFs.Text = string.Empty;
        }

        /// <summary>
        /// Clears the last character from the dialing DTMF character send history label on the top of the phone pad.
        /// </summary>
        public void ClearLastCharacter()
        {
            if (labelSentDTMFs.Text.Length > 0)
            {
                labelSentDTMFs.Text = labelSentDTMFs.Text.Substring(0, labelSentDTMFs.Text.Length - 1);
            }
        }

        #endregion

        #region private form event handlers
        /// <summary>
        /// Reads the text of a button and notifies any event listener that user has clicked a phone keypad button.
        /// </summary>
        private void button_Click(object sender, EventArgs e)
        {
            // Reads the text of the button.
            Button button = sender as Button;
            if (button != null)
            {
                string tone = button.Text;

                // Updates the label with the new tone being sent.
                labelSentDTMFs.Text += tone;

                // If the ButtonClicked delegate is registered, invoke registered event callback method to notify 
                // that a phone pad button was clicked.
                if (ButtonClickedEvent != null)
                {
                    ButtonClickedEvent(tone);
                }
            }
        }
        /// <summary>
        /// Handles the Mouse-down event for each phonepad button on the form
        /// </summary>
        /// <param name="sender">object. The Button control that was clicked by the user</param>
        /// <param name="e">MouseEventArgs. The event state</param>
        private void Button_MouseDownEvent(object sender, MouseEventArgs e)
        {

            Button pressedButton = (Button)sender;

            // Playing an audio file using the DeviceManager.BeginPlayAudiofile
            // creates a conflict with the Lync client when a computer has 
            // only 1 playback device. The audio channel in the Lync conversation
            // preempts the playback device and prevents the audio file playback operation
            // from completing. 
            // The issue is avoided by counting audio devices and then playing back 
            // audio files only when there are two or more playback devices configured.
            if (_LyncClient.DeviceManager.AudioDevices.Count > 1 || _InConversation == false)
            {
                if (_PlayAudioType.ToLower() == "speech")
                {
                    // Speak the character pressed by playing a wav file.
                    SaySpeech(pressedButton.Text);
                }
                else
                {
                    // Play a .wav file representing a DTMF tone.
                    SayDTMFTone(pressedButton.Text);
                }
            }
        }
        #endregion


        #region private helper methods
        /// <summary>
        /// Plays a .wav file corresponding to the character on the button that was clicked.
        /// </summary>
        /// <param name="whatToSay">string. The character displayed on the clicked button</param>
        private void SaySpeech(string whatToSay)
        {
            string audioPath = _speechPath;
            try
            {
                switch (whatToSay)
                {
                    case "1":
                        audioPath += "ONE.WAV";
                        break;
                    case "2":
                        audioPath += "TWO.WAV";
                        break;
                    case "3":
                        audioPath += "THREE.WAV";
                        break;
                    case "4":
                        audioPath += "FOUR.WAV";
                        break;
                    case "5":
                        audioPath += "FIVE.WAV";
                        break;
                    case "6":
                        audioPath += "SIX.WAV";
                        break;
                    case "7":
                        audioPath += "SEVEN.WAV";
                        break;
                    case "8":
                        audioPath += "EIGHT.WAV";
                        break;
                    case "9":
                        audioPath += "NINE.WAV";
                        break;
                    case "0":
                        audioPath += "ZERO.WAV";
                        break;
                    case "#":
                        audioPath += "POUND.WAV";
                        break;
                    case "*":
                        audioPath += "STAR.WAV";
                        break;

                }
                _LyncClient.DeviceManager.EndPlayAudioFile(
                    _LyncClient.DeviceManager.BeginPlayAudioFile(audioPath,
                    AudioPlayBackModes.AlertAndCommunication,
                    false,
                    null,
                    null));

            }
            catch (Microsoft.Lync.Model.LyncClientException )
            {
                _LyncClient.DeviceManager.EndPlayAudioFile(
                    _LyncClient.DeviceManager.BeginPlayAudioFile(_speechPath + "FileNotFound.WAV",
                    AudioPlayBackModes.Alert,
                    false,
                    null,
                    null));
            }
            catch (System.Runtime.InteropServices.COMException )
            {
                _LyncClient.DeviceManager.EndPlayAudioFile(
                    _LyncClient.DeviceManager.BeginPlayAudioFile(_speechPath + "FileNotFound.WAV",
                    AudioPlayBackModes.Alert,
                    false,
                    null,
                    null));
            }

        }

        /// <summary>
        /// Plays a .wav file representing a DTMF tone. 
        /// </summary>
        /// <param name="whatToSay">string. The character displayed on the clicked phone pad button.</param>
        private void SayDTMFTone(string whatToSay)
        {
            string audioPath = _DTMFPath;

            try
            {
                switch (whatToSay)
                {
                    case "1":
                        audioPath += "DTMF_ONE.WAV";
                        break;
                    case "2":
                        audioPath += "DTMF_TWO.WAV";
                        break;
                    case "3":
                        audioPath += "DTMF_THREE.WAV";
                        break;
                    case "4":
                        audioPath += "DTMF_FOUR.WAV";
                        break;
                    case "5":
                        audioPath += "DTMF_FIVE.WAV";
                        break;
                    case "6":
                        audioPath += "DTMF_SIX.WAV";
                        break;
                    case "7":
                        audioPath += "DTMF_SEVEN.WAV";
                        break;
                    case "8":
                        audioPath += "DTMF_EIGHT.WAV";
                        break;
                    case "9":
                        audioPath += "DTMF_NINE.WAV";
                        break;
                    case "0":
                        audioPath += "DTMF_ZERO.WAV";
                        break;
                    case "#":
                        audioPath += "DTMF_POUND.WAV";
                        break;
                    case "*":
                        audioPath += "DTMF_STAR.WAV";
                        break;

                }
                _LyncClient.DeviceManager.EndPlayAudioFile(
                    _LyncClient.DeviceManager.BeginPlayAudioFile(audioPath,
                    AudioPlayBackModes.AlertAndCommunication,
                    false,
                    null,
                    null));
            }
            catch (Microsoft.Lync.Model.LyncClientException lce)
            {
                MessageBox.Show("Exception on playback: " + lce.Message);
            }
            catch (System.Runtime.InteropServices.COMException comExecpt)
            {
                MessageBox.Show("COM Exception on playback: " + comExecpt.Message);
            }
        }


        #endregion

    }
}

ConversationItem Class

This example encapsulates an active conversation.

using System;
using System.Diagnostics;
using System.Text;

using Microsoft.Lync.Model;
using Microsoft.Lync.Model.Conversation;

namespace NorthwindCashRegister
{

   /// <summary>
   /// Helper class for displaying a conversation's participant list as a line in a listbox.
   /// </summary>
   public class ConversationItem
   {
      public ConversationItem(Conversation conversation)
      {
         Debug.Assert(conversation != null); 
         Conversation = conversation;
      }

      /// <summary>
      /// Gets or sets the Lync Conversation.
      /// </summary>
      public Conversation Conversation { get; private set; }

      /// <summary>
      /// Returns the display name of the contact.
      /// </summary>
      public override string ToString()
      {
         StringBuilder participantNames = new StringBuilder(100);

         // Iterate through the participants in the conversation to list their display names.
         foreach (Participant participant in Conversation.Participants)
         {
            // Ignore the self participant.
            if (participant.IsSelf)
            {
               continue;
            }
            
            // Concatenate the display name into the list.
            string name = null;
            try
            {
               name = participant.Contact.GetContactInformation(ContactInformationType.DisplayName) as string;
            }
            catch (LyncClientException lyncClientException)
            {
               Console.WriteLine(lyncClientException);
            }
            catch (SystemException systemException)
            {
               if (LyncModelExceptionHelper.IsLyncException(systemException))
               {
                  // Log exceptions thrown by the Lync Model API.
                  Console.WriteLine("Error: " + systemException);
               }
               else
               {
                  // Rethrow any SystemException that did not come from the Lync Model API.
                  throw;
               }
            }

            participantNames.Append(name ?? "<Unknown>").Append(", ");
         }

         return participantNames.ToString();
      }
   }

}

ParticipantItem Class

This example encapsulates a conversation participant.

using System;
using System.Diagnostics;
using Microsoft.Lync.Model;
using Microsoft.Lync.Model.Conversation;

namespace NorthwindCashRegister
{
   /// <summary>
   /// Helper class for displaying a participant's name in a listbox.
   /// </summary>
   public class ParticipantItem
   {
      public ParticipantItem(Participant participant)
      {
         Debug.Assert(participant != null);
         Participant = participant;
      }

      /// <summary>
      /// Gets or sets the Lync Participant.
      /// </summary>
      public Participant Participant { get; private set; }

      /// <summary>
      /// Returns the display name of the contact.
      /// </summary>
      public override string ToString()
      {
         // In case ToString() is called before the participant is set.
         if (Participant == null)
         {
            return string.Empty;
         }

         // Obtain the display name of the participant.
         string displayName = null;
         try
         {
            displayName = Participant.Contact.GetContactInformation(ContactInformationType.DisplayName) as string;
         }
         catch (LyncClientException lyncClientException)
         {
           Console.WriteLine(lyncClientException);
         }
         catch (SystemException systemException)
         {
            if (LyncModelExceptionHelper.IsLyncException(systemException))
            {
               // Log exceptions thrown by the Lync Model API.
               Console.WriteLine("Error: " + systemException);
            }
            else
            {
               // Rethrow any SystemException that did not come from the Lync Model API.
               throw;
            }
         }


         // If the contact is self, then add a (self) suffix.
         displayName = displayName ?? "<Unknown>";
         return Participant.IsSelf ? displayName + " (self)" : displayName;
      }
   }

}

LyncModelExceptionHelper Class

This example returns true if the given system exception can be cast to a specific exception from a set of exceptions.

using System;
using System.Runtime.InteropServices;

namespace NorthwindCashRegister
{
   internal class LyncModelExceptionHelper
   {
      /// <summary>
      /// Identify if a particular SystemException is one of the exceptions that may be thrown
      /// by the Lync Model API.
      /// </summary>
      /// <param name="ex"></param>
      /// <returns></returns>
      static internal bool IsLyncException(SystemException ex)
      {
         return
           ex is NotImplementedException ||
           ex is ArgumentException ||
           ex is NullReferenceException ||
           ex is NotSupportedException ||
           ex is ArgumentOutOfRangeException ||
           ex is IndexOutOfRangeException ||
           ex is InvalidOperationException ||
           ex is TypeLoadException ||
           ex is TypeInitializationException ||
           ex is InvalidComObjectException ||
           ex is InvalidCastException;
      }
   }
}

UCMA Application Code

This section includes the code that is used in the UCMA 3.0 application.

Application Configuration

App.Config, the application configuration file, is used to configure settings for the computer that is hosting the application. If the appropriate parameters are entered in the add elements (and the XML comment delimiters are removed), they do not have to be entered from the keyboard when the application is running.

The following example shows the App.Config file.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <!-- Provide parameters necessary for the sample to run without prompting for user input. -->
    <!-- Provide the FQDN of the Microsoft Lync Server 2010 computer.-->
    <!-- <add key="ServerFQDN1" value=""/> -->

    <!-- The user sign-in name that is used to sign in to the application. -->
    <!-- To use credentials used by the currently signed-in user, do not add a value. -->
    <!-- <add key="UserName1" value=""/> -->

    <!-- The user domain name that is used to sign in to the application. -->
    <!-- To use credentials used by the currently signed-in user, do not add a value. -->
    <!-- <add key="UserDomain1" value=""/> -->

    <!-- The user URI that is used to sign in to the application, in the format user@host. -->
    <!-- <add key="UserURI1" value=""/> -->
  </appSettings>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="mscorlib" publicKeyToken="b77a5c561934e089" culture="neutral" />
        <bindingRedirect oldVersion="2.0.0.0" newVersion="4.0.0.0"/>
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

Application Code

The following example shows the code for the UCMA 3.0 application.

// .NET namespaces
using System;
using System.Threading;
using Microsoft.Speech.AudioFormat;
using Microsoft.Speech.Synthesis;

// UCMA namespaces
using Microsoft.Rtc.Collaboration;
using Microsoft.Rtc.Collaboration.AudioVideo;
using Microsoft.Rtc.Signaling;

// UCMA samples namespaces
using Microsoft.Rtc.Collaboration.Sample.Common;

namespace Microsoft.Rtc.Collaboration.Sample
{
  public class DepartmentStoreDTMF
  {
    #region Globals
    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 _waitForSynthesisToBeFinished = new AutoResetEvent(false);
    private AutoResetEvent _waitForTransferStarted = new AutoResetEvent(false);

    #endregion

    #region Methods
    public static void Main(string[] args)
    {
      DepartmentStoreDTMF sample = new DepartmentStoreDTMF();
      sample.Run();
    }
        
    private void Run()
    {
      // A helper class to take care of platform and endpoint setup and cleanup. 
      _helper = new UCMASampleHelper();

      // Create a user endpoint using the network credential object. 
      _userEndpoint = _helper.CreateEstablishedUserEndpoint("DepartmentStoreDTMF Sample User");

      // Register a delegate to be called when an incoming audio-video call arrives.
      _userEndpoint.RegisterForIncomingCall<AudioVideoCall>(AudioVideoCall_Received);

      // Wait for the incoming call to be accepted, then terminate the conversation.
      Console.WriteLine("Waiting for incoming call...");
      _waitForCallToBeAccepted.WaitOne();

      // Create a ToneController and attach the AudioVideoFlow.
      ToneController toneController = new ToneController();
      toneController.AttachFlow(_audioVideoFlow);

      // Subscribe for notification when the ToneReceived event is raised. 
      toneController.ToneReceived += new EventHandler<ToneControllerEventArgs>(toneController_ToneReceived);
      
      // 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();

      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);
      _waitForTransferStarted.WaitOne();
  
      // 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();
    }

      
    #endregion

    #region EVENT HANDLERS
    // Record the state transitions in the console.
    void AudioVideoCall_StateChanged(object sender, CallStateChangedEventArgs e)
    {
      Console.WriteLine("Previous call state: " + e.PreviousState + "\nCurrent state: " + e.State);
    }

    // Handler for the AudioVideoFlowConfigurationRequested event on the call.
    // This event is raised when there is a flow present to begin media operations with, and that it is no longer null.
    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);
    }

    // Handler for the StateChanged event on an AudioVideoFlow instance.
    private void AudioVideoFlow_StateChanged(object sender, MediaFlowStateChangedEventArgs e)
    {
      // When the flow is active, media operations can begin.
      Console.WriteLine("Previous flow state: " + e.PreviousState.ToString() + "\nNew flow state: " + e.State.ToString());
    }

    // Delegate that is called when an incoming AudioVideoCall arrives.
    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);
    }

    // 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;
      }
    }
    
    // Handler for the SpeakStarted event on the SpeechSynthesizer.
    void SpeechSynthesizer_SpeakStarted(object sender, SpeakStartedEventArgs e)
    {
      Console.WriteLine("SpeakStarted event raised.");
    }

    // Handler for the SpeakCompleted event on the SpeechSynthesizer.
    void SpeechSynthesizer_SpeakCompleted(object sender, SpeakCompletedEventArgs e)
    {
      Console.WriteLine("SpeakCompleted event raised.");
    }
    #endregion

    #region CALLBACKS
    private void CallAcceptCB(IAsyncResult ar)
    {
      AudioVideoCall audioVideoCall = ar.AsyncState as AudioVideoCall;
      try
      {
        // Determine whether the call was accepted successfully.
        audioVideoCall.EndAccept(ar);
      }
      catch (RealTimeException exception)
      {
        // RealTimeException may be thrown on media or link-layer failures. 
        // A production application should catch additional exceptions, such as OperationTimeoutException,
        // OperationTimeoutException, and CallOperationTimeoutException.

        Console.WriteLine(exception.ToString());
      }
      finally
      {
        // Synchronize with main thread.
        _waitForCallToBeAccepted.Set();
      }
    }

    private void CallTerminateCB(IAsyncResult ar)
    {
      AudioVideoCall audioVideoCall = ar.AsyncState as AudioVideoCall;

      // Finish terminating the incoming call.
      audioVideoCall.EndTerminate(ar);

      // Remove this event handler now that the call has been terminated.
      _audioVideoCall.StateChanged -= AudioVideoCall_StateChanged;

      // Terminate the conversation.
      _audioVideoCall.Conversation.BeginTerminate(ConversationTerminateCB, _audioVideoCall.Conversation);
    }

    private void ConversationTerminateCB(IAsyncResult ar)
    {
      Conversation conversation = ar.AsyncState as Conversation;

      // Finish terminating the conversation.
      conversation.EndTerminate(ar);

      // Synchronize with main thread.
      _userEndpoint.UnregisterForIncomingCall<AudioVideoCall>(AudioVideoCall_Received);
      _waitForConversationToBeTerminated.Set();
    }
    #endregion
  }
}

Helper Class

The helper class contains member methods that create and start a CollaborationPlatform instance, and then create and establish the UserEndpoint instance that is used in the sample. For more information about the helper class, see Using UCMA 3.0 BackToBackCall: Code Listing (Part 3 of 4).

Conclusion

Using the ideas presented in this article, you can develop a Lync 2010 application and a UCMA 3.0 application that communicate with each other, by using DTMF tones and speech synthesis.

Additional Resources

For more information, see the following resources:

About the Authors

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.