Programmable Voice

  1. Home
  2. Docs
  3. Programmable Voice
  4. Voice Elements Demo
  5. Sample Solution Tutorials
  6. Dialer

Dialer

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

Sections of this tutorial you can jump directly to:

Dialer Client – If you are familiar with how to download our sample solutions, skip ahead to the demo.
Understanding the Source Code – To learn more about the source code in this solution, click here to skip to that section.

How to 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.

Voice Elements Demo Dashboard

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.

Demo Dashboard Sample Solutions noting the three download formats available.

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

Dialer Sample Solution

Enter the numbers to you would like to call in the test and then 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

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();
    }
}
Was this article helpful to you? Yes 15 No