Introduction
In today’s business world call centers are conquering even more territory than ever. Their main task is serving the customers that is usually done by the sales and technical support departments as product support, telemarketing or market research.
It is essential for good call centers to be able to handle large amount of simultaneous calls, because among large companies hundreds of phone calls might come in simultaneously. Due to this fact, applying effective call managing options (call queuing, call forwarding, call holding, etc.) has become inevitable.
Each well-functioning call center has an Interactive Voice Response (IVR) menu that takes a lot of toll from the agents, because it helps the customers to access to basic services in connection with usually their account details or the products/services available. Usually your callers can access some simple actions through the IVR or you can ask to be forwarded to a live agent. For better understanding, IVRs are voice menu systems that can direct the customers to the menu points they need. It receives the customer’s responses given by their touch-tone telephone keypad entry (DTMF signal). Instead of DTMF signalling, IVR systems can be also controlled with human voice commands (voice control). In this project I will explain the implementation of both of them.
Prerequisites
- I have built my IVR application in C#, so you need an IDE (Integrated Development Environment) supporting this programming language, such as Microsoft Visual Studio.
- .NET Framework installed on your PC is also needed.
- As my IVR system is based on VoIP technology, you need to add some VoIP components to the references in your IDE in order to define the default behaviour of the IVR in the simplest way. Since I have already used Ozeki VoIP SIP SDK for another VoIP development (I have created an IP PBX), I used the prewritten VoIP components of this SDK.
Settings in your IDE (Integrated Development Environment)
Create a new project:
- Open Visual Studio, click on File then New project
- Select the Visual C# Console Application option
- Specify a name for your new project
- Click on OK
Add VoIP components to your references to achieve and use the IVR components of the SDK:
- Right-click on References
- Select the Add references option
- Browse the VoIPSDK.dll file that can be found where the SDK has been installed to
- Select the .dll and click on the OK button
Writing the code
In my project 3 classes have been used: Softphone.cs, CallHandler.cs; Program.cs. Let’s see the implementation of the 3 classes step-by-step.
Implementing Softphone.cs class
First, you need to create a simple softphone that has the same functions as an ordinary telephone. (It is necessary, because the IVR need to be able to receive calls coming from other telephones. So you will manage the incoming calls with the help of this softphone.) Softphone.cs
is the class that is used to introduce how to declare, define and initialize a softphone, how to handle some of the events of the VoIP SDK and how to use its functions. This class will be used in the Program.cs for creating a new softphone.
As a first step, add some extra using lines (Code example 1):
using Ozeki.Network.Nat;
using Ozeki.VoIP;
using Ozeki.VoIP.SDK;
Code example 1: Add some new using lines
Now you need to create a softphone and a phone line from the ISoftPhone
and IPhoneLine
interfaces (Code example 2):
ISoftPhone softphone; IPhoneLine phoneLine;
Code example 2: IsoftPhone and IPhoneLine objects
In a constructor, you also need to initialize this softphone with default parameters (Code example 3). You need to set the port range, indicated by the first paramerter as the minPortRange
and the maximum parameter maxPortRange
. This is the port's interval. The third parameter is the listening port (5060 is the port of the SIP). By subscribing to the IncomingCall
event, incoming calls will be monitored continuously.
softphone = SoftPhoneFactory.CreateSoftPhone(5000, 10000, 5060);
softphone.IncomingCall += softphone_IncomingCall;
Code example 3: Initialization of the softphone
Now you need to register your SIP Account to the server by using the Register method (Code example 4). This method gets all of the values required for the registration as parameters: registrationRequired
, displayName
, userName
, authenticationId
, registerPassword
, domainHost
and domainPort
. These parameters will be used to create a SIP account with the constructor of the SIPAccount class. After creating the account, the method configures the Network Address Translation (NAT) with the NatTraversalMethod
in order to ensure incoming calls will get through the firewall. Having done these steps, the system can create the phone line (CreatePhoneLine
method) with the help of the account and the NatConfiguration
allowing the IVR can be called. Finally, you need to register this phone line.
public void Register(bool registrationRequired, string displayName, string userName, string authenticationId, string registerPassword, string domainHost, int domainPort)
{
try
{
var account = new SIPAccount(registrationRequired, displayName, userName, authenticationId, registerPassword, domainHost, domainPort);
Console.WriteLine("\n Creating SIP account {0}", account);
var natConfiguration = new NatConfiguration(NatTraversalMethod.None);
phoneLine = softphone.CreatePhoneLine(account, natConfiguration);
Console.WriteLine("Phoneline created.");
phoneLine.PhoneLineStateChanged += phoneLine_PhoneLineStateChanged;
softphone.RegisterPhoneLine(phoneLine);
}
catch(Exception ex)
{
Console.WriteLine("Error during SIP registration" + ex.ToString());
}
}
Code example 4: SIP account registration
Implementing CallHandler.cs class
Now, let’s see the second class. CallHandler.cs
is used to manage the incoming calls. In case of an inbound call, the caller will hear a greeting message through the speaker and after listening the selectable menu items, the caller can choose one of the menu items by pressing a button on his/her keypad.
In order to manage the incoming calls you need to create some objects: mediaConnector
, audioHandler
, phoneCallAudioSender
and greetingMessageTimer
from the ICall
interface and the MediaConnector
, AudioHandler
, PhoneCallAudioSender
and Timer
classes (Code example 5).
ICall call;
MediaConnector mediaConnector;
AudioHandler audioHandler;
PhoneCallAudioSender phoneCallAudioSender;
Timer greetingMessageTimer;
Code example 5: Add some new objects
The constructor of the class gets an ICall
type parameter and it makes the basic setups for the class. It sets the interval of the greetingMessageTimer
(30 seconds) that is for repeating the greeting message on the main menu level and it attaches the phoneCallAudioSender
to the call (Code example 6).
public CallHandler(ICall call)
{
greetingMessageTimer = new Timer();
greetingMessageTimer.Interval = 30000;
greetingMessageTimer.Elapsed += greetingMessageTimer_Elapsed;
this.call = call;
phoneCallAudioSender = new PhoneCallAudioSender();
phoneCallAudioSender.AttachToCall(call);
mediaConnector = new MediaConnector();
}
Code example 6: Add some new objects
Now take a look at the methods that are code blocks containing a series of statements.
One of the most important methods of this class is the Start()
method. It will be called by the main method. In this method you can subscribe to the CallStateChanged
and DtmfReceived
events then accept the call (Code example 7).
- The call state change event is one of the most important events. It informs both the server and the client about the changes of the call state.
- The DTMF received event informs the server about the button pressed by the client. It indicates that he/she want to step to another menu level.
public void Start()
{
call.CallStateChanged += call_CallStateChanged;
call.DtmfReceived += call_DtmfReceived;
call.Accept();
}
Code example 7: Creating the Start() method
In the Start method you are subscribed to the DtmfReceived
event using the call_DtmfReceived()
method and you can set what happens when a caller press a DTMF button. It can be seen in the Code example 8 that - in this example - if the caller presses 1, he/she can hear some product information with the help of the constructor of TextToSpeech
class. By pressing 2, the caller can hear a sample mp3 song by calling the Mp3ToSpeaker
method.
void call_DtmfReceived(object sender, VoIPEventArgs<DtmfInfo> e)
{
DisposeCurrentHandler();
switch (e.Item.Signal.Signal)
{
case 0: break;
case 1: TextToSpeech("Product XY has been designed for those software developers who especially interested in VoIP developments. If you prefer .NET programming languages, you might be interested in Product XY."); break;
case 2: MP3ToSpeaker(); break;
}
}
Code example 8: What happens if the caller presses the DTMF buttons
1.) TextToSpeech method
Using the TextToSpeech
method you can add a text message what you want to play to your caller. It will be read by the TextToSpeech
engine. Just make a TextToSpeech
object, connect it to the phoneCallAudioSender
by using the Media Connector and call the AddAndStartText
method (Code example 9).
private void TextToSpeech(string text)
{
var tts = new TextToSpeech();
audioHandler = tts;
mediaConnector.Connect(audioHandler, phoneCallAudioSender);
tts.AddAndStartText(text);
}
Code example 9: The TextToSpeech method
2.) MP3ToSpeaker method
Using the functions of the MP3ToSpeaker
method, the IVR can easily play an MP3 file to the caller (e.g. a greeting message). You only need to create an MP3StreamPlayback
object with the file path parameter, connect it to the PhoneCallAudioSender
via the mediaConnector
then start the streaming (StartStreaming()
method) (Code example 10).
private void MP3ToSpeaker()
{
var mp3Player = new MP3StreamPlayback("../../test.mp3");
audioHandler = mp3Player;
mediaConnector.Connect(mp3Player, phoneCallAudioSender);
mp3Player.StartStreaming();
}
Code example 10: The MP3ToSpeaker method
Implementing Program.cs class
You arrived to the last class that needs to be implemented. Program.cs introduces the usage of the softphone and the callHandler objects, and handles the console and DTMF events coming from the caller.
First, in the Main
section, you need to create the softphone object in order to reach the methods created in the Softphone class. (Then there will be some instructions about the code and the call of the sipAccountInitialization
method.) You can add your SIP account values in this section. Now you need to subscribe to the IncomingCall event in order to manage the incoming calls (Code example 11).
static void Main(string[] args)
{
callHandlers = new List<CallHandler>();
var softphone = new Softphone();
Console.WriteLine("/* Program usage description */");
sipAccountInitialization(softphone);
softphone.IncomigCall += softphone_IncomigCall;
Console.ReadLine();
}
Code example 11: The Main() method
Due to the CallHandler
list, this IVR is able to manage multiple calls. When a call comes in, the system accepts that automatically and then the call will be added to the list. Each further incoming call will be added to the list. The list contains the calls as long as they are active. When a call is ended, it is removed from the CallHandler
list.
The sipAccountInitialization()
method is responsible for getting the SIP account components from the user. As you could see above, the SIPAccount
constructor needs the following values:
- Authentication ID
- User name (default is the authentication ID)
- Display name (default is the authentication ID)
- Password
- Domain Host (default is the local host)
- Domain Port (default is 5060)
So you need to add these values for the program and it will call the Register method of the Softphone class by using them (Code example 12).
private static void sipAccountInitialization(Softphone softphone)
{
Console.WriteLine("Please setup your SIP account!\n");
Console.WriteLine("Please set your authentication ID: ");
var authenticationId = Read("authenticationId", true);
Console.WriteLine("Please set your user name (default:" +authenticationId +"): ");
var userName = Read("userName", false);
if (string.IsNullOrEmpty(userName))
userName = authenticationId;
Console.WriteLine("Please set your name to be displayed (default: " +authenticationId +"): ");
var displayName = Read("displayName", false);
if (string.IsNullOrEmpty(displayName))
displayName = authenticationId;
Console.WriteLine("Please set your registration password: ");
var registrationPassword = Read("registrationPassword", true);
Console.WriteLine("Please set the domain name (default: your local host): ");
var domainHost = Read("domainHost", false);
if (string.IsNullOrEmpty(domainHost))
domainHost = NetworkAddressHelper.GetLocalIP().ToString();
Console.WriteLine(domainHost);
Console.WriteLine("Please set the port number (default: 5060): ");
int domainPort;
string port = Read("domainPort", false);
if (string.IsNullOrEmpty(port))
{
domainPort = 5060;
}
else
{
domainPort = Int32.Parse(port);
}
Console.WriteLine("\nCreating SIP account and trying to register...\n");
softphone.Register(true, displayName, userName, authenticationId, registrationPassword, domainHost, domainPort);
}
Code example 12: The sipAccountInitialization() method
The Read()
function has 2 parameters: inputname
and readWhileEmpty
. If the readWhileEmpty
is false, there is a default value of this component (for example 5060 for the Domain Port). In this case the input can be empty or null. Else you need to add an input for the system and the function will return with the input to the Main
method (Code example 13).
private static string Read(string inputName, bool readWhileEmpty)
{
while (true)
{
string input = Console.ReadLine();
if (!readWhileEmpty)
{
return input;
}
if (!string.IsNullOrEmpty(input))
{
return input;
}
Console.WriteLine(inputName +" cannot be empty!");
Console.WriteLine(inputName +": ");
}
}
Code example 13: The Read() function
In order to be notified in case of an incoming call, you need to set an event specifically for this purpose. This is the IncomingCall
event. If there is a notification about a call awaiting to be accepted, the call has to be accepted by the IVR system. In softphone_IncomingCall()
method there is a callHandler
object for managing the methods of the CallHandler
class. If there is an incoming call, call Start()
method of the CallHandler
(Code example 14).
static void softphone_IncomigCall(object sender, Ozeki.VoIP.VoIPEventArgs<Ozeki.VoIP.IPhoneCall> e)
{
Console.WriteLine("Incoming call!");
var callHandler = new CallHandler(e.Item);
callHandler.Completed += callHandler_Completed;
lock (callHandlers)
callHandlers.Add(callHandler);
callHandler.Start();
}
Code example 14: The softphone_IncomingCall() method
The implementation of the blind transfer
Now it is time to see how to optimize your IVR with blind transfer functionality.
The call transfer can be done automatically by a call center server application or it can be coordinated by a human operator. In case of a blind transfer the first option is the most usual. Blind transfer means that the call will be transferred to a randomly chosen end-point, basically the first available agent.
If the user selects this option, the system transfers the call to another phone number automatically. For this, you need to add some modifications to this basic IVR code.
At the beginning of the Program class make static string variable for storing the blind transfer value, then the user need to enter a phone number if he/she would like to use the blind transferring function. If he/she do not want to use this option, then press '0' (Code example 15).
Console.WriteLine("Please set the number for blind transferring! If you don't want to use this function of the IVR, press 0!");
blindTransfer = Read("blindTransfer", true);
Code example 15: Making static string variable
After that, you need to pass this value to the CallHandler
class. You can do it by calling a new (for example blindTransferNumber()
) method in the softphone_IncomingCall
section of your code. By using this you can give the blindTransfer
value to the CallHandler
class (Code example 16).
public void BlindTransferNumber(string blindTransfer)
{
blindTransferNumber = blindTransfer;
}
Code example 16: The blindTransferNumber() method
The only thing you left to do is adding a new case to the switch statement in the call_DtmfReceived()
method. If the user press 3 and if the number that he/she provided was zero in the program class, then write on the console that he/she can not use this function of the IVR. Else call the blindtransfer
method and give the blindtransfer number value to it as a parameter (Code example 17).
case 3:
{
if (blindTransferNumber == "0")
{
TextToSpeech("You did not add any number for blind transferring!");
break;
}
else
{
call.BlindTransfer(blindTransferNumber);
break;
}
}
Code example 17: Adding a new case to the switch statement in the call_DtmfReceived() method
The implementation of the voice-controlled IVR
To make the IVR menu system more effective you can facilitate your callers’ job with voice control. It means that they can navigate between the menu items using human voice commands instead of pressing any DTMF buttons.
In order to implement the human speech based control, you need to modify the CallHandler
class and create the events and methods of the SpeechToText
abstract class.
As you can see above, the CallHandler.cs
is used to manage the incoming calls. To implement the voice control functionality, first you need to create some new objects (Code example 18):
PhoneCallAudioReceiver
: to receive audio from the caller
IEnumerable
: to give word options to the system to be able to recognize them
SpeechToText
: to convert the human speech into text format
PhoneCallAudioReceiver phoneCallAudioReceiver;
IEnumerable<string> choices;
SpeechToText stt;
Code example 18: Add some new objects in the CallHandler.cs class
Code example 19 shows the CallHandler()
constructor, where you need to set the created objects and make instances for them. The PhoneCallAudioReceiver
object should be atttached to the call. Furthermore, you need to create an instance for the SpeechToText
object by using the CreateInstance
method of the SpeechToText
class. In order to allow the system to recognize the voice commands pronounced by the caller, you need to add some words to the choices list.
public CallHandler(ICall call)
{
greetingMessageTimer = new Timer();
greetingMessageTimer.Interval = 30000;
greetingMessageTimer.Elapsed += greetingMessageTimer_Elapsed;
this.call = call;
phoneCallAudioSender = new PhoneCallAudioSender();
phoneCallAudioSender.AttachToCall(call);
mediaConnector = new MediaConnector();
phoneCallAudioReceiver = new PhoneCallAudioReceiver();
phoneCallAudioReceiver.AttachToCall(call);
choices = new List<string>() { "first", "second"};
stt = SpeechToText.CreateInstance(choices);
}
Code example 19: The CallHandler() constructor
The Start()
method can be seen in Code example 20. In this section you need to connect the PhoneCallAudioReceiver
to the stt by using the mediaConnector
. As a result, the system will be able to start the recognition of the human speech. You also need to subscribe to the WordHypothesized
event of the SpeechToText
class.
public void Start()
{
mediaConnector.Connect(phoneCallAudioReceiver, stt);
call.CallStateChanged += call_CallStateChanged;
stt.WordHypothesized += CallHandler_WordHypothesized;
call.Accept();
}
Code example 20: The Start() method
When the system detects the human speech and the recognized word is equal to one word of the choices list, the CallHandler_WordHypothesized()
method will be started. This method manages the switch statement. As you can see below, if the caller says ’first’, a brief product information can be heard with the help of the constructor of TextToSpeech
class. If he/she says ’second’, the caller can hear a sample mp3 song by calling the Mp3ToSpeaker
method.
void CallHandler_WordHypothesized(object sender, SpeechDetectionEventArgs e)
{
DisposeCurrentHandler();
Console.WriteLine(e.Word.ToString());
switch (e.Word.ToString())
{
case "first": TextToSpeech("Product XY has been designed for those software developers who especially interested in VoIP developments. If you prefer .NET programming languages, you might be interested in Product XY."); break;
case "second": MP3ToSpeaker(); break;
}
}
Code example 21: The CallHandler_WordHypothesized() method
How to create a multi-level IVR
Concerning to the fact, that in today’s business world the more advanced multi-level IVRs are commonly used, I improved my IVR solution. I created an advanced menu system that can navigate the caller through more than one menu level.
As this article demonstrates how to build a basic IVR, I though it would be better to present my improvement as tip. If you are interested in developing a multi-level IVR menyu sysem, please study my tip that explains its implementation step-by step:
How to create a multi-level IVR (Interactive Voice Response) menu system in C#: http://www.codeproject.com/Tips/752443/How-to-create-a-multi-level-IVR-Interactive-Voice
Summary
To sum it up, a call center can be really effective if it is able to handle a large amount of simultaneous calls as well as it has advanced call management features. In my project I have developed a basic IVR that can receive and manage the incoming calls without human intervention. By building blind transfer functionality, my IVR can be used to transfer the caller to a live operator – automatically by using DTMF signaling. You can extend your IVR – and therefore improve your call center – with further more professional features of course, such as call queue, voicemail and call recording, etc.
References
Theoretical background:
Download the necessary software:
Supplementary information: