This tutorial is a walk-through explanation of the Dialer sample solution that can be downloaded from your demo dashboard on the Voice Elements Customer Portal.
(If you haven’t already, sign up for a demo account and get 100 minutes of free call time for the next 30 days with the Voice Elements servers.)
Dialer Tutorial
The Dialer solution is designed to demonstrate how easily Voice Elements can:
-
- Detect a Human or a Machine
- Detect a Beep
- Use Call Progress to make smart decisions about how to handle dialed calls.
Table of Contents
Download the Sample Solution
Sign In to the Voice Elements Customer Portal.
If you are not brought directly to the Dashboard screen, click on Dashboard in the top navigation links.
Sample Solutions Available in Three Download Formats
We offer three download formats for running the Sample Solutions: Windows Executable Program, Windows Source Code Download, or .NET Core Cross Platform Code. You can download any option and run them as often as you like.
Choosing to Download the Windows Executable Program
You’ll enjoy this user friendly option if you are not a programmer, if Visual Studio is not installed on the device or machine you are currently using for the demo, or if you just want to jump straight to seeing Voice Elements in action.
Select the Windows Program button ‘Download exe’. The ZIP file will be downloaded to your machine’s Downloads folder. You might also find it in the tray at the bottom of your browser window. Unzip the folder and extract the files to your chosen location. Look for the application named VoiceApp and run it.
Choosing to Download the Windows Source Code
If you are a programmer and have Visual Studio loaded on the device or machine you are using, you may enjoy downloading the Windows Source Code and seeing it in action from that perspective. Not only will you see how easy it is to program the code, you will also see how simple it is to create your telephony application.
Select the Windows Source Code button ‘Download sln’. Unzip the folder and extract the files to your chosen location. Run the Microsoft Visual Studio Solution ‘Dialer’.
Choosing to Download the .NET Core Cross Platform Solution
This download format is designed for Linux or Windows You will need a .NET Core compatible compiler to run the solution.
Run Anyway and Allow Access
Your device might recognize the file as an unrecognized app with an unknown publisher and ask if you are sure you want to run it. Select the option that will confirm that you want to run the application anyway.
Your firewall might prompt you to confirm access to the app. Select the option that allows access.
Once you have removed the obstacles, the application will run.
Dialer Client Application
Enter the numbers to you would like to call in the box, entering one per line. Select your dialing option:
- Simulate DB: This option acts as if you had those numbers stored in a database and places the calls.
- Use Socket: This option transmits the numbers via a socket to the application to trigger the dialing. You will need to look at the code to see how this is done.
Testing Notes:
- Enter several phone numbers to call. Watch the log as Voice Elements dials the first number, reacts to the dial result, then calls the next number.
- Voice Elements can detect when a human has answered. Note that the recorded message begins when you say ‘Hello’.
- Now let a call go to voicemail. Call Progress interprets machine or human based on the outgoing message. If the outgoing message begins with ‘Hello’, it might detect human and play the recorded message. However, once it detects the beep, the recording starts over. Your call recipient’s voicemail captures the entire message. Never leave a partial message again!
Ready to try more Sample Solutions?
You can click on More Samples within the app, or go back to your browser and log in to the Voice Elements Customer Portal. We have a tutorial for each sample solution to guide you through running each sample.
You must close the current sample before running another one.
If you want to run the Sample Solution again, you might consider moving the folder out of downloads to your desktop or a location where you want to store all the Sample Solutions.
We hope you try all our Sample Solutions to really see how comprehensive and robust Voice Elements is.
Understanding the Source Code
For more detailed information about the Voice Elements Classes and Methods, explore our Class Library documentation at Voice Elements Developer Help. We’ve linked a few classes and methods in the sections below to encourage you to take advantage of this treasure-trove of knowledge from our developers.
IvrApplication
The core class of this project is IvrApplication. This class contains a lot of logic that sets up the application as a windows service so you can ignore a lot of the code in it for now. The most important method here is MainCode().
MainCode()
When the application is run, it starts a new thread which runs MainCode(). This connects to the Voice Elements servers in the cloud. Then loops indefinitely checking for new tasks to run, and inbound call events.
Note that Log.Write() is used frequently to log call progress and help with debugging. It is recommended that you continue to do this as you program your own Voice Elements applications.
The first thing MainCode() does is connect to the Voice Elements servers. This is done by constructing a new TelephonyServer object passing in server IP, username, and password as parameters. These values have already been generated for your account but you can change them in your Settings.settings file.
MainCode() also sets the CacheMode on the TelephonyServer object. ClientSession mode means that the server will stream and cache the files to and from your client machine. These files are flushed after you disconnect. Server mode means that the files reside on the server and will use the full path name to find them there. Note that Server mode can only be used on your own dedicated Voice Elements server.
After connecting to the server and setting its cache mode the new call event should be subscribed to. This sets a method to be called when an incoming call is received. In this example TelephonyServer_NewCall() is the method to be called on new incoming call events.
RegisterDNIS()
RegisterDNIS() is then called on the TelephonyServer to tell the server which phone numbers the application will handle. This method can be called with no parameters to instruct Voice Elements to handle calls from all phone numbers on your account. Otherwise you can specify numbers to handle as parameters.
try { Log.Write("Connecting to: {0}", Properties.Settings.Default.PhoneServer); s_telephonyServer = new TelephonyServer("gtcp://" + Properties.Settings.Default.PhoneServer, Properties.Settings.Default.UserName, Properties.Settings.Default.Password); // CHANGE YOUR CACHE MODE HERE s_telephonyServer.CacheMode = VoiceElements.Interface.CacheMode.ClientSession; // SUBSCRIBE to the new call event. s_telephonyServer.NewCall += new VoiceElements.Client.NewCall(TelephonyServer_NewCall); s_telephonyServer.RegisterDNIS(); // Subscribe to the connection events to allow you to reconnect if something happens to the internet connection. // If you are running your own VE server, this is less likely to happen except when you restart your VE server. s_telephonyServer.ConnectionLost += new ConnectionLost(TelephonyServer_ConnectionLost); s_telephonyServer.ConnectionRestored += new ConnectionRestored(TelephonyServer_ConnectionRestored); }
Dialer Methods
IvrApplication also contains the logic for making multiple calls at a time. The first step in this is the QueueNumbers() method.
QueueNumbers()
This method simply adds phone numbers to the c# queue that is used in this program to manage the numbers that are being dialed. This method is called from the GUI with a list of phone numbers as input. This method then loops through the list of phone numbers and enqueues them.
public static void QueueNumbers(List<KeyValuePair<int, string>> items) { // Lock the queue before adding the number lock (s_queue) { foreach (var item in items) { QueueItem queueItem = new QueueItem() { RecordID = item.Key, Number = item.Value }; s_queue.Enqueue(queueItem); } } }
CheckOutboundQueue() and ProcessQueueItem()
CheckOutboundQueue() is the method that goes through the queue and sends each phone number to get a channel resource and make a call.
Calling the ProcessQueueItem() method for each phone number.
private static void CheckOutboundQueue() { // Loop through our queue of calls while (s_queue.Count > 0) { QueueItem item = null; // Only lock the queue when we are ready to get the next item lock(s_queue) { // In case another thread removed a record, we will check the count again if (s_queue.Count > 0) item = s_queue.Dequeue(); else break; } if (item != null) { Log.Write("Assigning channel to dial " + item.Number); bool successful = ProcessQueueItem(item); // If we couldn't get a channel to dial, we will add this // item back to the queue and stop processing more if (!successful) { lock(s_queue) { s_queue.Enqueue(item); } break; } } } }
ProcessQueueItem()
ProcessQueueItem() takes a QueueItem as a parameter which is basically just a phone number. This method loops through the ChannelResources, if there is not a ChannelResource available the method returns false. Otherwise if a ChannelResource is available a new OutboundCall object is constructed and the RunScript() method is called on it using a new thread.
private static bool ProcessQueueItem(QueueItem item) { int i = 0; // Find a channel - if it is null or finished it is available for (i = 0; i < s_numChannels; i++) { if (s_outboundCalls[i] == null || s_outboundCalls[i].Status == OutboundCall.CallStatus.Finished) break; } // If all channels are in use, we'll return false if (i >= s_numChannels) { Log.Write("All {0} channels are busy", s_numChannels); return false; } // If we found a channel, we will use it! try { s_outboundCalls[i] = new OutboundCall(s_telephonyServer, item); } catch (Exception ex) { Log.Write("Server: Channels are busy - {0}", ex.Message); return false; } // We will start the new outbound call on its own thread ThreadStart ts = new ThreadStart(s_outboundCalls[i].RunScript); Thread t = new Thread(ts); t.Name = "Outbound" + i.ToString(); t.Start(); return true; }
Outbound Call
RunScript()
In the OutboundCall class, the RunScript() method contains the majority of the logic for handling outbound phone calls. The OriginatingPhoneNumber property of the ChannelResource can be set to display any outbound Caller ID phone number if needed. The MaximumTime property can also be set so that the call will automatically fail after a chosen amount of time. The CallProgress property can be set to CallProgress.AnalyzeCall so that the program can see if a human answers.
Dial()
The outbound call is actually placed when the Dial() method is called on the ChannelResource passing in the phone number to call as a parameter. This is set to a DialResult variable to keep track of call progress. The DialResult variable is then used in a switch statement to determine the result of the call. If a human answers, the call connects, or a machine is detected then the call was successful. Otherwise the call is unsuccessful and the method returns. In the case that the call was successful beep detection is started by calling StartBeepDetection(). The message then begins to play but will start again if a beep is detected, at that point the BeepDetectionStop() method is called on the VoiceResource.
try { // Use WriteWithId to differentiate between separate instances of the class Log.WriteWithId(m_channelResource.DeviceName, "OutboundCall Script Starting"); Log.WriteWithId(m_channelResource.DeviceName, "Dialing {0}", m_queueItem.Number); m_status = CallStatus.Running; // You can display any outbound Caller ID phone number if needed (this is disabled for testing) m_channelResource.OriginatingPhoneNumber = Properties.Settings.Default.TestPhoneNumber; // Instruct the server to wait no more then 30 seconds for a connection m_channelResource.MaximumTime = 30; // We will use call analysis to see if a human answers m_channelResource.CallProgress = CallProgress.AnalyzeCall; // Place the call DialResult dr = m_channelResource.Dial(m_queueItem.Number); Log.WriteWithId(m_channelResource.DeviceName, "The dial result for {0} was: {1}", m_queueItem.Number, dr); switch (dr) { case DialResult.HumanDetected: case DialResult.Connected: m_queueItem.Result = "Answered"; break; case DialResult.MachineDetected: case DialResult.PbxDetected: m_queueItem.Result = "Machine"; break; case DialResult.Busy: m_queueItem.Result = "Busy"; break; case DialResult.NoAnswer: m_queueItem.Result = "NoAnswer"; break; case DialResult.NoDialTone: case DialResult.Error: case DialResult.Failed: case DialResult.NoRingback: case DialResult.FastBusy: m_queueItem.Result = "Error"; break; case DialResult.OperatorIntercept: m_queueItem.Result = "Disconnected"; break; case DialResult.FaxToneDetected: m_queueItem.Result = "Fax"; break; default: break; } // Call was not successful if (m_queueItem.Result != "Answered" && m_queueItem.Result != "Machine") { Log.WriteWithId(m_channelResource.DeviceName, "Unexpected dial result, cancelling Call"); if (m_channelResource.GeneralCause == 402) Log.WriteWithId(m_channelResource.DeviceName, "You have ran out of minutes. Contact customer support to have more added"); return; } // We will start listening for a voicemail "beep" StartBeepDetection(); // Play the message Log.WriteWithId(m_channelResource.DeviceName, "Playing file..."); m_voiceResource.Play("../../WelcomeMessage.wav"); // If we got a fax tone while playing the message, we'll update the result if (m_voiceResource.TerminationCodeFlag(TerminationCode.Tone)) { m_queueItem.Result = "Fax"; return; } // If a beep was detected while playing the message, we will start the message over again else if (m_voiceResource.TerminationCode == TerminationCode.Beep) { // If we detected a beep, it must be a machine m_queueItem.Result = "Machine"; Log.WriteWithId(m_channelResource.DeviceName, "Beep Detected"); m_voiceResource.BeepDetectionStop(); // Play the message again Log.WriteWithId(m_channelResource.DeviceName, "Playing file again..."); m_voiceResource.Play("../../WelcomeMessage.wav"); } }
StartBeepDetection()
StartBeepDetection() is a method from the OutboundCall class this method uses the beep detector to determine if the call has been sent to voice mail. Beep detection is achieved by calling the BeepDetectionStart() method on the VoiceResource object. The TerminationDigits property can also be set, in this case to FG to terminate if a fax tone is detected. The ClearDigitBuffer property should be set to false so that if the user begins to press digits during the prompt for input, those digits will not be lost.
private void StartBeepDetection() { try { m_voiceResource.BeepDetectionStart(); m_voiceResource.TerminationDigits = "FG"; // We will also terminate if a fax tone is detected m_voiceResource.ClearDigitBuffer = false; } catch (Exception ex) { Log.WriteException(ex, "Could not start beep detection"); } }
Sockets
This sample solution also contains logic for getting phone numbers to dial from a separate application via a socket. The SocketClient class is an example of the code required by an application to send a message to this application. The SocketServer class implements a simple socket for listening and handling messages from a SocketClient.
SocketServer
ListenForClients()
private void ListenForClients() { // Start listening for new clients this.m_tcpListener.Start(); while (true) { try { // Blocks until a client has connected to the server m_log.Write("Waiting for connection . . ."); TcpClient client = this.m_tcpListener.AcceptTcpClient(); m_log.Write("Received Connection . . "); // When we receive a new client connection, handle its messages on a new thread Thread clientThread = new Thread(new ParameterizedThreadStart(HandleClientMessage)); clientThread.Start(client); } catch (Exception ex) { m_log.Write("AcceptTcpClient Exception - {0}", ex.Message); break; } } m_stopped = true; }
HandleClientMessage()
private void HandleClientMessage(object client) { TcpClient tcpClient = (TcpClient)client; NetworkStream clientStream = tcpClient.GetStream(); byte[] messageArray = new byte[4096]; int bytesRead; string workingBuffer = ""; int messageLength = 0; string messageType = ""; while (true) { bytesRead = 0; try { // Read the stream from the client // (blocks until a client sends a message) bytesRead = clientStream.Read(messageArray, 0, 4096); } catch (Exception ex) { // Socket error has occurred m_log.WriteException(ex, "Lost connection - socket error"); break; } // If nothing was read, then the client disconnected if (bytesRead == 0) { m_log.Write("The client has disconnected."); break; } // A message has been successfully received string messageSegment = new ASCIIEncoding().GetString(messageArray, 0, bytesRead); m_log.Write("Message segment received: {0}", messageSegment); // The first time through we will parse out the beginning "header" of the // message if (messageLength == 0) { // The first 5 characters are the length of the message messageLength = Convert.ToInt32(messageSegment.Substring(0, 5)); // The next character is the type of message messageType = messageSegment.Substring(5, 1); // The remaining text is the message content workingBuffer = messageSegment.Substring(6); } else { // Since we've already parsed out the "header", keep appending the remaining messages workingBuffer += messageSegment; } m_log.Write("Len:{0} WorkbufLen:{1} MessageType:{2}", messageLength, workingBuffer.Length, messageType); // Continue reading until we reach the end of the message if (workingBuffer.Length != messageLength - 6) continue; // Process the message once we have it all ProcessMessage(messageType, workingBuffer, clientStream); break; } // When we are done with the message, close the client's connection try { tcpClient.Close(); } catch { } } }
ProcessMessage()
private void ProcessMessage(string messageType, string jsonString, NetworkStream clientStream) { string response = "[OK]"; try { m_log.Write("Received: {0}", jsonString); // If this is a dialer message, parse the JSON and queue the numbers to dial if (messageType == "D") { // Try de-serializing the data List<KeyValuePair<int, string>> numbers = JsonConvert.DeserializeObject<List<KeyValuePair<int, string>>>(jsonString); // Queue the numbers to be processed IvrApplication.QueueNumbers(numbers); // Trigger the thread event so it starts processing the numbers IvrApplication.ThreadEvent.Set(); } } catch (Exception ex) { m_log.WriteException(ex, "Could not process message"); response = "[Error]"; } try { // Send the response to the client byte[] buffer = new ASCIIEncoding().GetBytes(response); clientStream.Write(buffer, 0, buffer.Length); clientStream.Flush(); } catch (Exception ex) { m_log.WriteException(ex, "Could not send response to client"); } }
SocketClient
Connect()
private bool Connect() { try { m_tcpClient = new TcpClient(); // Get address of host IPAddress[] ips1 = Dns.GetHostAddresses(m_serverAddress); // Gets first IP address associated with it string finalIP = ips1[0].ToString(); m_log.Write("Connecting to {0}:{1}", finalIP, m_port); // Create a network endpoint IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse(finalIP), m_port); // Connect to the endpoint and get a stream to send and receive data m_tcpClient.Connect(serverEndPoint); m_clientStream = m_tcpClient.GetStream(); m_log.Write("Connect successful"); return true; } catch (Exception ex) { m_log.WriteException(ex, "Connect Failed"); return false; } }
SendMessage()
public string SendMessage(string message, char messageType) { if (!Connect()) return "Error: Server Not Found or not listening"; try { ASCIIEncoding encoder = new ASCIIEncoding(); // Get the length of the message to send int messageLength = message.Length + 6; // (add 6 for the length and message type) string paddedLength = messageLength.ToString("00000"); // Create a message with the size, type and message content string completeMessage = String.Format("{0}{1}{2}", paddedLength, messageType, message); byte[] buffer = encoder.GetBytes(completeMessage); // Write the buffer to the stream m_clientStream.Write(buffer, 0, buffer.Length); m_clientStream.Flush(); m_log.Write("Packet sent: {0}", completeMessage); byte[] responseArray = new byte[4096]; m_clientStream.ReadTimeout = 10000; // Get the response from the server int bytesRead = m_clientStream.Read(responseArray, 0, 4096); string response = encoder.GetString(responseArray, 0, bytesRead); m_log.Write("Received: {0}", response); // If we don't receive '[OK]', then something went wrong if (response.Length < 4) return "Error: " + response; string responseStart = response.Substring(0, 4); if (responseStart == "[OK]" && response.Length == 4) return "Success"; else if (responseStart == "[OK]") return response.Substring(4); else return "Error: " + response; } catch (Exception ex) { m_log.WriteException(ex, "Could not send message"); return "Error: [Exception Thrown]"; } finally { Close(); } }
For a deeper dive into coding Voice Elements and our Class Library, explore: