Programmable Voice

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

Conferencing

This tutorial is a walk-through explanation of the Conferencing 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.)

Conferencing Tutorial

The Conferencing Solution is designed to demonstrate how easily Voice Elements can:

    • Listen In/Coaching
    • Recording Conference

Table of Contents

Sections of this tutorial you can jump directly to:

Conferencing 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.

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.

Conferencing Client

Screenshot Conferencing Solution Demo

Demo Notes:

This demo is designed to demonstrate a call scenario between a salesperson in training (Pupil), a prospective customer (Normal Participant), and the whisper coach (Coach) who guides the new salesperson through the call.  The Normal Participants cannot hear what the Coach says but hear everyone else.  The Pupil interacts with the Normal Participants.  Only the Pupil can hear the Coach.  The Coach can hear everyone.

There can only be one Pupil and one Coach in any conference, however there can be a large number of Normal Participants. *

The participant role is determined by the Passcode that is entered.

Participant Roles and Interactions

Passcode for our Demo Role Interaction Number allowed per conference
123 Normal Participant Interacts with all Normal Participants and Pupil 100+ *
456 Pupil Interacts only with Normal Participants. Pupil hears the Coach but cannot speak privately to the Coach One
789 Coach Is heard only by the Pupil One
n/a Monitor Can listen to the conference but cannot interact with anyone 100+ *

 

* Our Server Bank supports 100+ conference ports for this demo.

Monitors:  Although we wanted you to be aware of the Monitor role in the table above, they are not part of our demo scenario.

Number Allowed Per Conference: Your number of Normal Participants and Monitors on a call will only be limited by the number of conference ports you purchase with your Voice Elements License.

This demo is designed to give you a taste of what is possible; not a full exploration of all our conferencing features.  For more in-depth information about Voice Elements Conferencing, please see our article Conferencing Overview.

Explore the source code below to see all more features and functionality of Voice Elements Conferencing.

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.

Log.Write()

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.

MainCode() then sets everything on the TelephonyServer needed for SMS. Private and public keypairs have already been generated for your account, located in Settings.settings. The API Keys can be found and changed in the customer portal.  The TelephonyServer is then subscribed to the TelephonyServer_SmsMessage() method for handling inbound texts and the TelephonyServer_SmsDeliveryReport() method for logging message delivery status.

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

    s_telephonyServer.SmsMyPrivateKeyXml = Properties.Settings.Default.CustomerKeyPairXml;
    s_telephonyServer.SmsBorderElementsPublicKeyXml = Properties.Settings.Default.BorderKeyPairXml;
    s_telephonyServer.SmsMessage += TelephonyServer_SmsMessage;
    s_telephonyServer.SmsDeliveryReport += TelephonyServer_SmsDeliveryReport;

    // 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);
}

Inbound Call

Let’s take a look at the logic for taking care of an inbound call.

TelephonyServer_NewCall()

In the MainCode() of IvrApplication we set TelephonyServer_NewCall() to be called when a new phone call is received. This generates the ChannelResource which you can basically think of as the object that is the phone line. The ChannelResource class contains all of the methods and properties that you would expect to be able to perform with a phone line. The InboundCall class is used for handling all of the logic for inbound phone calls.

TelephonyServer_NewCall() constructs a new InboundConference object for which the TelephonyServer object and the ChannelResource object are provided as parameters.

RunScript()

The RunScript() method is then called on the new InboundConference object. This method contains all of the logic for setting up and adding new callers to the conference call.

static void TelephonyServer_NewCall(object sender, VoiceElements.Client.NewCallEventArgs e)
{
    try
    {
        Log.Write("NewCall Arrival! DNIS: {0}  ANI: {1}  Caller ID Name: {2}", e.ChannelResource.Dnis, e.ChannelResource.Ani, e.ChannelResource.CallerIdName);

        // Handle The New Call Here

        InboundConference inboundConference = new InboundConference(s_telephonyServer, e.ChannelResource);
        inboundConference.RunScript();

    }
    catch (Exception ex)
    {
        Log.WriteException(ex, "IvrApplication::NewCall");
        e.ChannelResource.Disconnect();
        e.ChannelResource.Dispose();
    }
}

Outbound Call

Let’s look at how making an outbound call with Voice Elements works.

MakeOutboundCall()

The IvrApplication class contains the method MakeOutboundCall() which is called when the button is clicked on the GUI. This project uses the OutboundCall class to handle the logic for outbound calls. This method first constructs a new OutboundCall object passing in the TelephonyServer object and the number that is to be called. The constructor creates a new ChannelResource to the TelephonyServer.

RunScript()

A new thread is then started to call the RunScript() method on the new OutboundCall object.

public static void MakeOutboundCall(string number)
{
    OutboundCall outbound = new OutboundCall(s_telephonyServer, number);

    // Always spawn calls on new threads
    ThreadStart ts = new ThreadStart(outbound.RunScript);
    Thread t = new Thread(ts);
    t.Name = "Outbound";

    t.Start();
}

The RunScript() method contains all of the logic for making an outbound call. It first sets the OriginatingPhoneNumber property of the ChannelResource, which sets the outbound Caller ID. It then sets the MaximumTime property on the ChannelResource so that the call will fail after 30 seconds if it does not connect.

Dial()

To actually place the call, the Dial() method is called on the ChannelResource passing in the number to be called as a parameter. This method returns a DialResult property which is used to determine if the call connects or not. RunScript() then has logic to determine what to do if the call is answered or not. If the call is answered then the channel resource is connected to the conference. The call is then disconnected and all of the resources are cleaned up.

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_numberToCall);

    // With this, the server will detect if a human or machine answers the phone
    // m_channelResource.CallProgress = CallProgress.AnalyzeCall;

    // 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;

    // Place the call
    DialResult dr = m_channelResource.Dial(m_numberToCall);

    Log.WriteWithId(m_channelResource.DeviceName, "The dial result for {0} was: {1}", m_numberToCall, dr);

    if (dr == DialResult.Connected)
    {
        InboundConference inboundConference = new InboundConference(m_telephonyServer, m_channelResource);
        inboundConference.RunScript();
    }
    else
    {
        Log.WriteWithId(m_channelResource.DeviceName, "Unexpected dial result, cancelling Call");

        if (dr == DialResult.OperatorIntercept && m_channelResource.GeneralCause == 402)
            Log.WriteWithId(m_channelResource.DeviceName, "You have ran out of minutes. Contact customer support to have more added");
    }
}

Inbound Conference

The InboundConference class is the important class to pay attention to for writing conferencing applications with Voice Elements. The RunScript() method contains most all of the logic for conferencing. This method first asks for a pass code that needs to be entered in order to join the conference. This is set up so that there can be 3 failed inputs but on the fourth fail the incoming call will be disconnected. Otherwise if the pass code is entered correctly then the program first checks to see if the conference as already been created or not. If it has yet to be created then the conference is created. The ChannelResource is then added to the Conference. Certain callers can be muted by calling Monitor() on the ConferenceResource instead of Add().

Coach and Pupil Feature

This demo contains the ‘Coach, Pupil’ conference call feature, where one of the phones in the conference can be a Pupil who can hear the one Coach, however a normal caller in the conference cannot hear the Coach. In this example, this is set up so that a specific pass code must be entered to join the conference as either a Pupil or a Coach. So the pass code 123 will put the caller in as Normal, 456 will put the caller in as a Pupil, and 789 will put the caller in as a Coach. This class also keeps track of whether or not there is already a Pupil or Coach in the call so that there can’t be more than one of each.

try
{
    Log.Write("Starting Script");

    ChannelResource.Answer();
    VoiceResource.TerminationDigits = "ANY";

    int i = 0;

    string passcode = "";

    for (i = 0; i < 3; i++)
    {
        if (i == 0)
        {
            VoiceResource.PlayTTS("Please enter your passcode followed by the pound sign");
        }
        else
        {
            VoiceResource.PlayTTS("Invalid Conference ID. Please try again. Enter the passcode followed by the pound sign");
        }

        VoiceResource.GetDigits(5, 20, "#", 4, false);

        passcode = VoiceResource.DigitBuffer;
        VoiceResource.WipeDigitBuffer();

        if (passcode == "123" || passcode == "456" || passcode == "789")
        {
            Log.Write("User entered correct passcode");
            // You can add logic to do a database lookup instead!
            break;
        }
    }

    if (i >= 3)
    {
        // Disconnect the user because they exceeded the max attempts
        VoiceResource.PlayTTS("Goodbye!");
        ChannelResource.Disconnect();
        return;
    }

    lock (InboundConference.SyncVar)
    {
        // We'll create the conference if it doesn't exist. Otherwise we will add the user to the conference
        // If you would like to manage multiple conferences .NET Dictionaries come in handy, that way you can do a lookup to see if the conference they are trying to enter already exists.
        if (InboundConference.Conference == null)
        {
            // Get the Conference resource
            InboundConference.Conference = TelephonyServer.GetConference();

            // Subscribe to the conference changed event
            InboundConference.Conference.ConferenceChanged += new ConferenceChanged(Conference_ConferenceChanged);

            // Set the ConferenceNotifyMode to On so that we are notified when users enter or leave the conference
            InboundConference.Conference.ConferenceNotifyMode = ConferenceNotifyMode.On;
        }
    }

    // We will clamp DTMF tones so that way when we add a menu other users won't hear when they press digits
    ChannelResource.ConferenceAttributes.MemberToneClamp = true;

    // We will add echo cancellation to improve voice quality
    ChannelResource.ConferenceAttributes.EchoCancellation = true;

    switch (passcode)
    {
        case "123":
            ChannelResource.ConferenceAttributes.ConfereeType = ConfereeType.Normal;
            ChannelResource.VoiceResource.PlayTTS("You are connected normally");
            break;
        case "456":
            //check that there isn't already a pupil
            if (!hasPupil)
            {
                hasPupil = true;
                ChannelResource.ConferenceAttributes.ConfereeType = ConfereeType.Pupil;
                ChannelResource.VoiceResource.PlayTTS("You are connected as a pupil");
            }
            else
            {
                ChannelResource.VoiceResource.PlayTTS("There is already a pupil");
                ChannelResource.ConferenceAttributes.ConfereeType = ConfereeType.Normal;
                ChannelResource.VoiceResource.PlayTTS("You are connected normaly");
            }
            break;
        case "789":
            //check that there isn't already a coach
            if (!hasCoach)
            {
                hasCoach = true;
                ChannelResource.ConferenceAttributes.ConfereeType = ConfereeType.Coach;
                ChannelResource.VoiceResource.PlayTTS("You are connected as a coach");
            }
            else
            {
                ChannelResource.VoiceResource.PlayTTS("There is already a coach");
                ChannelResource.ConferenceAttributes.ConfereeType = ConfereeType.Normal;
                ChannelResource.VoiceResource.PlayTTS("You are connected normally");
            }
            break;
    }

    // Now we'll add the user to the conference
    InboundConference.Conference.Add(ChannelResource);

    // If you would like to add listen only (muted) participants you can user Monitors
    // Because the audio from monitors is not used for mixing in the conference you can create very large conferences if most of the participants are monitors
    //InboundConference.Conference.Monitor(ChannelResource);

    ConferenceStarted = true;

    // We will wait until the user disconnects before terminating the call
    m_TerminateCall.WaitOne();
    if (hasCoach)
    {
        hasCoach = false;
    }
    if (hasPupil)
    {
        hasPupil = false;
    }
}

Conference_ConferenceChanged()

It’s also important to take note of the ConferenceChanged event method. When the conference is created above the ConferenceChanged event is subscribed to the Conference_ConferenceChanged() method which logs the status of the conference and disposes the conference when everyone leaves.

void Conference_ConferenceChanged(ConferenceChangedEventArgs ccea)
{
    Log.WriteWithId(InboundConference.Conference.ConferenceName, "The conference changed. Participants: {0} Monitors: {1}", InboundConference.Conference.Participants.Count, InboundConference.Conference.Monitors.Count);

    // We want to check to see if there are participants. If there are not we will dispose of the conference.
    if (!ConferenceStarted)
    {
        return;
    }
    if (InboundConference.Conference.Participants.Count > 0)
    {
        return;
    }

    //Unsubscribe from the conference changed event
    InboundConference.Conference.ConferenceChanged -= new ConferenceChanged(Conference_ConferenceChanged);

    if (recorderResource != null)
    {
        StopRecording();
    }
    // Dispose the conference and set to null
    InboundConference.Conference.Dispose();
    InboundConference.Conference = null;
}

Recording the Voice Conference

This demo also contains the logic for recording the voice conference. This is done using helper functions in the InboundConference class.

RecordConference()

RecordConference() is called when the Record button is pressed on the GUI. This method gets a new VoiceResource from the TelephonyServer for recording the call. It then sets a dynamic filename, and the termination conditions. The new VoiceResource is then added to the conference as a Monitor and begins to Record.

CheckConference()

CheckConference() simply returns whether there is a conference or not so that you can’t click the Record button unless there is a conference to record.

StopRecording()

StopRecording() just stops the recording and disposes of the VoiceResource.

public static void RecordConference()
{
    if (InboundConference.Conference != null)
    {
        recorderResource = TelephonyServer.GetVoiceResource();

        string filename = "VM_" + DateTime.Now.ToString("yyMMddhhmmss") + ".wav";

        recorderResource.TerminationDigits = "";
        recorderResource.MaximumTime = 1200;
        recorderResource.MaximumSilence = 1200;

        InboundConference.Conference.Monitor(recorderResource);

        Log.Write("Call is recording to " + filename);

        recorderResource.Record(filename);
    }
}

public static bool CheckConference()
{
    if (InboundConference.Conference == null)
    {
        return false;
    }
    return true;
}

public static void StopRecording()
{
    recorderResource.Stop();
    recorderResource.Dispose();
    Log.Write("Recording has stopped");

    frmInteractive._mainForm.setButtonText("Record");
}

For more detailed information about our Conferencing Tools, Quality Control and Settings, please see our article Conferencing Overview.

Was this article helpful to you? Yes 13 No