Programmable Voice

  1. Home
  2. Docs
  3. Programmable Voice
  4. Tutorials
  5. Dialer

Dialer

Download

This tutorial is a walkthrough explanation of the Dialer sample solution that can be downloaded from your demo dashboard.  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.

This tutorial covers how to write a simple dialer Voice Elements application that will send voice messages en masse. It is important to note that this project is set up to run either as a windows service or as a Windows form.  While you are debugging, you will want to simply start the application using the default Windows form.  Once you are ready to deploy for production, we recommend that you install as a Windows Service. See the documentation for .net windows services here. Once you have the project downloaded, unzip it and open Dialer.sln with Visual Studio.

This application is already complete and ready to go, so you can test it out to see what it does before you start to read through the code to see how it works.  When you first compile the solution, it will automatically get the Voice Elements Client from NuGet.

Voice Elements MainCode

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().

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() 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. 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() 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() 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

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. 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() 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 9 No

How can we help?