Course 3 / Lecture 4

How to build an IVR with Ozeki VoIP SIP SDK

How to develop an IVR system written in C# by using XML code

In the previous example we created a Multi-level IVR code example written in C#, which is able to read a greeting message to the caller, and give him/her some menu options to choose. The caller could listen more information about Ozeki Ltd., or a sample mp3 song by pressing button one or two, and he/she could enter a sub menu by pressing button three. In this lower menu level the caller was able to use the blind transfer functionality of the IVR system by pressing button one, or he/she could return to the main menu level by pressing button two. If you click on the following link, you can reach the full description of the Multi-level IVR example code.


In this tutorial example we will learn the followings:

  • How to create basic and multi-level IVR systems by using XML codes

This example program allows you to listen welcome messages, important information, sample mp3 songs in your IVR system.

For the source code of the basic IVR using XML example, please click here!

ICommand.cs


Program.cs


Menu.cs


MultipleCommandHandler.cs


Softphone.cs


SpeakCommand.cs


PlayCommand.cs


For the source code of the Multi-level IVR using XML example, please click here!

ICommand.cs


Program.cs


Menu.cs


MultipleCommandHandler.cs


Softphone.cs


SpeakCommand.cs


PlayCommand.cs


We are using seven classes in these examples:

  • Program.cs
    Our class with the Main() method, this class controls the console window, interacts with the user, by using softphone and callhandler objects.
  • Softphone.cs
    The softphone's implementation goes here, all of it's events, methods, functions, variables.
  • Menu.cs
    This class serves to managing and handling the menu sections of the IVR system, no matter it is the main menu or a sub-menu.
  • MultipleCommandHandler.cs
    If more than one commands belong to the key what the user pressed, the IVR system needs to start all of the commands with the same key.
  • SpeakCommand.cs
    With the speak command class the IVR system is able to read an optional text message to the caller.
  • PlayCommand.cs
    The play command class is useful when we would like to play an mp3 file to the caller. It could be a song or a pre recorded speech.
  • ICommand.cs
    This interface is intended to execute any subsequent command (play, speak, new menu level).

The implementation of the IVR using XML

We are going to create a program, which is intended to create a functional one or multi-level IVR system by using XML code to add the structure of the system. Start the coding with the creation of the Softphone class.

Softphone.cs

This class is used to introduce how to declare, define and initialize a softphone, how to handle some of the Ozeki VoIP SIP SDK's events and how to use some of that's functions. In other words, we would like to create a "telephone software", which has the same functions (or much more), as an ordinary mobile (or any other) phone.

If you would like to know more about the specification of the softphone (and the creation of it), then visit our How to build a softphone page, where you can find discriptions about the most important functions, methods and events of this topic.

ICommand.cs

We use the ICommand interface to give a general form to the commands belong to the current menu level. So if the command is a text to speech, an mp3 player or perhaps a new menu level command, the system will be able to handle all of them.
This interface implements two methods (Start() and Cancel()) and one event (Completed). So if we implement all of these components in a class, we can start or cancel the commands and we can check whether the commands are completed or not.

interface ICommand
{
    void Start(ICall call);
    void Cancel();
    event EventHandler Completed;
}

Program.cs

The Program class is for getting the SIP account values from the user to create a softphone, managing the incoming calls and processing all the components of the XML code. If we want to modify the structure of the IVR system, we don't need to change the source code, we only need to modify the XML part.

The Main() method

In the Main method of the Program class first we need to call the ShowHelp() method to give some information to the user about the behavior of the program. After this, create a softphone object from the Softphone class and call the sipAccountInitialization() method with softphone parameter. With this, the program gets the necessary SIP account informations from the user to create the softphone and the phone line. Finally, we need to subscribe on the IncomingCall event of the softphone to check if the system has an incoming call.

static void Main(string[] args)
{
     ShowHelp();

     Softphone softphone = new Softphone();

     sipAccountInitialization(softphone);

     softphone.IncomigCall += softphone_IncomigCall;

     Console.ReadLine();
}

The softphone_IncomingCall() method

This method should be called when the IncomingCall event occurs. In this method create a menu variable which will be equals with return value of the ReadXML() method. If it is not null, then we call the Start() method of this menu, with the call parameter. If the menu object is null, then we need to reject the incoming call.

static void softphone_IncomigCall(object sender, Ozeki.VoIP.VoIPEventArgs<Ozeki.VoIP.IPhoneCall> e)
{
    var menu = ReadXML();

    if (menu != null)
         menu.Start(e.Item);
    else
         e.Item.Reject();
}

The ReadXML() method

In my example, the Menu return type ReadXML() method will contain the XML code of the IVR, but reading it from a file is also a good option, because if we would like to modify something in the xml code we can do it without shutting down the running of the program. It starts with a menu tag so it will be the main menu. If the system answeres the call, then the caller will hear a greeting message. This is what the init section is for. We will use two types of commands in this example: the speak command and the play command. With the speak command the IVR system is able to read an optional text message to the caller. The play command is useful when we would like to play an mp3 file to the caller. It could be a song or a pre recorded speech. If the caller press button one then he or she can hear more information about ozeki company, and a sample mp3 song. Button two has no specific function.

After the creation of the XML code, make a new Menu object, and call the MenuLevel() method with the ivrXML parameter which contains the xml code in string format, then return with the menu.

private static Menu ReadXML()
{

            string ivrXml = @"<ivr>
	                            <menu>
                                    <init>
                                        <speak>
                                               Introduce you the Interactive Voice Mail aka IVR example code written with Ozeki VoIP SDK.
                                               To get more information about Ozeki Ltd. and hear a sample mp3 song, please press button one.
                                               By pressing button two, you can listen an inform message
                                        </speak>
                                        <play>../../test.mp3</play>
                                    </init>
		                            <keys>
			                            <key pressed='1'>
                                               <speak>
                                                      Ozeki Informatics Ltd. is a leading mobile messaging software vendor and Ozeki VoIP SIP SDK is 
                                                      an excellent software development kit that allows you to establish VoIP calls from your application 
                                                      easily and quickly. You do not need to have expert programming skills, with the most basic programming 
                                                      knowledge you will be able to create extraordinary VoIP solutions with this tool.
                                               </speak>
				                               <play>../../test.mp3</play>
			                            </key>
			                            <key pressed='2'>
				                               <speak>
                                                      You pressed button two. You did nothing.
                                               </speak>
			                            </key>
		                            </keys>
	                            </menu>
                           </ivr>";

   var menu = new Menu();
   MenuLevel(ivrXml, menu);
   return menu;
}

The MenuLevel() method

The MenuLevel() method has two parameters: a string type ivrXml which contains the current menu section of the XML code, and a Menu type menu object. The whole process is in a try-catch block to get the unexpected exceptions. If the system catches one, then we need to write the error message on the screen and stop running.

First use the Parse() method with the ivrXml parameter to load an XElement from our XML string. Then select the "menu" element of the code. If the menuElement variable is null (so there is no menu tag in the XML code), then the XML code is inadequate, stop the running. If there was no problem, then select the init element of the menuElement. We need to get all of the commands in the init, so iterate in it with foreach statement. In my example a command can be "play" or "speak". If the current command is play, then call the AddInitCommand() method of the menu class with PlayCommand() parameter to add this command to the command list, and do the same if command is speak, but use the SpeakCommand() parameter.

If we got all the commands from the init section, select the "keys" element of the menu. This will contain the keys, which can be pressed by the caller. If there is no pressedKeyAttribute the XML code is invalid. Then get the pressed key, store it in the pressedKey integer variable, and add all of the commands which belong to the current key.

private static void MenuLevel(string ivrXml, Menu menu)
{
        try
        {
                var xelement = XElement.Parse(ivrXml);

                var menuElement = xelement.Element("menu");

                if (menuElement == null)
                {
                    Console.WriteLine("Wrong XML code!");
                    return;
                }

                var menuInit = menuElement.Element("init");

                foreach (var initElements in menuInit.Elements())
                {
                    switch (initElements.Name.ToString())
                    {
                        case "play":
                            menu.AddInitCommand(new PlayCommand(initElements.Value.ToString()));
                            break;
                        case "speak":
                            menu.AddInitCommand(new SpeakCommand(initElements.Value.ToString()));
                            break;
                    }
                }

                var menuKeys = menuElement.Element("keys");

                foreach (var key in menuKeys.Elements("key"))
                {
                    var pressedKeyAttribute = key.Attribute("pressed");

                    if (pressedKeyAttribute == null)
                    {
                        Console.WriteLine("Invalid ivr xml, keypress has no value!");
                        return;
                    }

                    int pressedKey;

                    if (!Int32.TryParse(pressedKeyAttribute.Value, out pressedKey))
                    {
                        Console.WriteLine("You did not add any number!");
                    }

                    foreach (var element in key.Elements())
                    {
                        switch (element.Name.ToString())
                        {
                            case "play":
                                menu.AddKeypressCommand(pressedKey, new PlayCommand(element.Value.ToString()));
                                break;
                            case "speak":
                                menu.AddKeypressCommand(pressedKey, new SpeakCommand(element.Value.ToString()));
                                break;
                            default:
                                return;
                        }
                    }

                }
            }
        catch (Exception ex)
        {
                Console.WriteLine(ex.Message);
                Console.WriteLine("Invalid ivr xml");
        }
}

Menu.cs

This class serves to managing and handling the menu sections of the IVR system, no matter it is the main menu or a sub-menu. So this class will manage the init and keys parts of the code which belong to the same menu level. We need to implemet all the methods and the event of the ICommand interface and create some objects at the beginning of the class.

Objects

Create the following objects:

  • Dictionary<int, MultipleCommandHandler> keys to store the commands with the keys
  • MultipleCommandHandler init to handle the process of the init section
  • ICall call for managing the call
  • Timer greetingMessageTimer for repeating the greeting message
  • MultipleCommandHandler handler to handle the actual commands

Dictionary<int, MultipleCommandHandler> keys;
MultipleCommandHandler init;
ICall call;
Timer greetingMessageTimer;
MultipleCommandHandler handler;

The constructor

If we create a Menu object we need to set the keys, init and greetingMessageTimer instances, and set the greetingMessageTimer to auto reset. So the greeting message will repeat automatically.

public Menu()
{
      keys = new Dictionary<int, MultipleCommandHandler>();
      init = new MultipleCommandHandler();
      greetingMessageTimer = new Timer();
      greetingMessageTimer.AutoReset = true;
}

The Start() method

If we start the menu, then we need to set the local call object with the parameter, subscribe on the necessary events, like the CallStateChanged and DtmfReceived events of the call and the Elapsed event of the greetingMessageTimer. Set the Interval of the greetingMessageTimer, accept the call, then start the greetingMessageTimer and call the start method of the init to start the included commands.

public void Start(ICall call)
{
      this.call = call;
      Onsubscribe();
      greetingMessageTimer.Interval = 20000;
      call.Answer();
      greetingMessageTimer.Start();
      init.Start(call);
}

The call_DtmfReceived() method

We need to listen the DTMF signals to know the caller pressed a button and he/she would like start a menu option. This is what the call_DtmfReceived() method is for. If the caller pressed a button, it should be checked whether there is a command list with the key or not. If it is true, then we need to unsubscribe from all the events, subscribe on the the completed event of the handler and call the start method of it with the call parameter.

void call_DtmfReceived(object sender, VoIPEventArgs<DtmfInfo> e)
{
      if (keys.TryGetValue(e.Item.Signal.Signal, out handler))
      {
            Unsubscribe();
            handler.Completed += handler_Completed;
            handler.Start(call);
	  }
}

The Onsubscribe(), Unsubscribe() and handler_Completed() methods

In the Unsubscribe() method we need to cancel the init and stop the greetingMessageTimer, because there are commands for the pressed key. If the handler is not null, then call the Cancel() method of it and we need to unsubscribe from all of the events.

The Onsubscribe() method is for subscribing back on all of the events and start the greetingMessageTimer.

When the current handler is completed, then call the Onsubscribe method and start again the greeting message of the menu.

public void Unsubscribe()
{
            init.Cancel();
            greetingMessageTimer.Stop();

            if (handler != null)
                handler.Cancel();

            call.CallStateChanged -= call_CallStateChanged;
            call.DtmfReceived -= call_DtmfReceived;
            greetingMessageTimer.Elapsed -= greetingMessageTimer_Elapsed;
}

private void Onsubscribe()
{
			Unsubscribe();

            call.CallStateChanged += call_CallStateChanged;
            call.DtmfReceived += call_DtmfReceived;
            greetingMessageTimer.Elapsed += greetingMessageTimer_Elapsed;
            greetingMessageTimer.Start();
}

void handler_Completed(object sender, EventArgs e)
{
            Onsubscribe();

            init.Start(call);
}

The AddInitCommand and AddKeyPressCommand

These methods are for passing the commands (from the Program class) to the MultipleCommandHandler class wheter be an init command or a keypress command.

public void AddInitCommand(ICommand command)
{
            init.AddCommand(command);
}

public void AddKeypressCommand(int digit, ICommand command)
{
            if (!keys.ContainsKey(digit))
                keys[digit] = new MultipleCommandHandler();

            keys[digit].AddCommand(command);
}

MultipleCommandHandler.cs

If more than one commands belong to the key what the user pressed, the IVR system needs to start all of the commands with the same key. We will reach this functionality by using the methods and events of the MultipleCommandHandler class.

Objects and the constructor

We need to create some important objects:

  • Queue<ICommand> commandQueue to get the commands in sequence
  • List<ICommand> commandList; to store the commands in it
  • ICall call managing the call
  • ICommand currentCommand the handle the current command in the list

In the constructor of the class we need to set the commandList instance.

Queue<ICommand> commandQueue;
List<ICommand> commandList;
ICall call;
ICommand currentCommand;

public MultipleCommandHandler()
{
       commandList = new List<ICommand>();
}

The Start() method

If we call the Start() method of this class we need the set the call with the call parameter, set the commandQueue object with ICommand elements, and we need to call the StartNextCommand() method.

public void Start(ICall call)
{
    this.call = call;
    commandQueue = new Queue<ICommand>(commandList);
    StartNextCommand();
}

The StartNextCommand() method

In these methods we managing the starting of all the commands in the command list. In the StartNextCommand() method check the commandQueue.Count is bigger than 0. If it is true, then get the first command from the commandQueue, subscribe on the completed event of the currentCommand, cancel it then call the Start() method of it with the call parameter.

In the OnCompleted() method invoke the Completed event, and in the currentCommand_Complete method start the next command if the current command finished.

void StartNextCommand()
{
       if (commandQueue.Count > 0)
       {
             currentCommand = commandQueue.Dequeue();
             currentCommand.Completed += currentCommand_Complete;
             currentCommand.Cancel();
             currentCommand.Start(call);
       }

       else
            OnCompleted();
}

private void OnCompleted()
{
      var Handler = Completed;
      if (Handler != null) 
            Handler(this, EventArgs.Empty);
}

void currentCommand_Complete(object sender, EventArgs e)
{
     StartNextCommand();
}

The Cancel() method

If we call the Cancel method of this class we need to unsubscribe from the completed event of the currentCommand and cancel it.

public void Cancel()
{
      if (currentCommand != null)
      {
           currentCommand.Completed -= currentCommand_Complete;
           currentCommand.Cancel();
      }
}

SpeakCommand.cs

I mentioned before that in my example two specific command will be implemented. With the speak command class the IVR system is able to read an optional text message to the caller. The play command class is useful when we would like to play an mp3 file to the caller. It could be a song or a pre recorded speech.

Objects and the constructor

Implement the following objects to manage the speak commands in the IVR system:

  • ICall call to manage the calls
  • PhoneCallAudioSender phoneCallAudioSender to send audio to the caller through the phone line
  • AudioHandler audioHandler to handle audio devices
  • string text this will be read by the system to the caller

If we create an instance from this class we need to set the local text variable with the text parameter. That is what you can see in the constructor.

ICall call;
PhoneCallAudioSender phoneCallAudioSender;
MediaConnector mediaConnector;
string text;
bool isStarted;

public SpeakCommand(string text)
{
    this.text = text;
}

The Start() method

In the Start() method of the SpeakCommand class need to set the objects and call the TextToSpeech() method with the text parameter to form the text into speech.

public void Start(Ozeki.VoIP.ICall call)
{
	 isStarted = true;
     this.call = call;
     phoneCallAudioSender = new PhoneCallAudioSender();
     phoneCallAudioSender.AttachToCall(call);
     mediaConnector = new MediaConnector();
     TextToSpeech(text);
}

The TextToSpeech() method

We would like to turn the added text into speech. We will make it happen in the TextToSpeech() method of this class. Create a TextToSpeech object, then subscribe on the Stopped event of the tts, connect to the PhoneCallAudioHandler and finally call the AddAndStartText of the tts with text parameter.

private void TextToSpeech(string speach)
{
	tts = new TextToSpeech();
	tts.Stopped += tts_Stopped;
	mediaConnector.Connect(tts, phoneCallAudioSender);
	tts.AddAndStartText(speach);
}

The tts_Stopped() and Cancel() methods

In the tts_Stopped() method we need to invoke the Completed event of the this class to know when the command stopped. If we would like to cancel the current Speak command we need to call the Cancel method of this class. Check whether the tts is null or not. If it is not null, then disconnect the tts from the phoneCallAudioSender, and dispose the mediaConnector with the Dispose() method.

void tts_Stopped(object sender, EventArgs e)
{
	if (!isStarted)
    	return;
    isStarted = false;
            
    var handler = Completed;
    if (handler != null)
    	handler(this, EventArgs.Empty);
}

public void Cancel()
{
	if (tts != null)
	{
		mediaConnector.Disconnect(tts, phoneCallAudioSender);
		mediaConnector.Dispose();
	}
}

PlayCommand.cs

The PlayCommand class is almost the same as the SpeakCommand.cs, the only difference is we need to create a MP3ToSpeaker() method instead of the TextToSpeech() method.

The MP3ToSpeaker() method

We need to create an MP3StreamPlayback object with the path parameter, which is the path of the file we want to play. Subscribe on the Stopped event of the mp3Player, connect the to the phoneCallAudioSender and finally call the Start() method for streaming.

private void MP3ToSpeaker(string path)
{
	DisposeMediaConnection();
	mediaConnector = new MediaConnector();
	mp3Player = new MP3StreamPlayback(path);
	mp3Player.Stopped += mp3Player_Stopped;
	
	mediaConnector.Connect(mp3Player, phoneCallAudioSender);
	mp3Player.Start();
}

Now we finished the creation our IVR system using by XML code. This is a one-level menu system, and it can happen we need to manage a multi-level IVR system. The good news is we can turn this system to a multi-level IVR very easily. Only need to make some modifications in the Program class.

Multi-Level IVR using XML

Right now we own a one-level IVR system with one-level XML code. First modify the XML code to be multi-level:


	
         
              
                     Introduce you the Interactive Voice Mail aka IVR example code written with Ozeki VoIP SDK.
                     To get more information about Ozeki Ltd. and hear a sample mp3 song, please press button one.
                     By pressing button two, you can listen an inform message
              
              ../../test.mp3
         
		 
			         
                                   
                                          Ozeki Informatics Ltd. is a leading mobile messaging software vendor and Ozeki VoIP SIP SDK is 
                                          an excellent software development kit that allows you to establish VoIP calls from your application 
                                          easily and quickly. You do not need to have expert programming skills, with the most basic programming 
                                          knowledge you will be able to create extraordinary VoIP solutions with this tool.
                                   
				     			   ../../test.mp3
			    	
			        
				                   
                          				 You pressed button two. You did nothing.
                     			   
			        
                    
				     			   
                          				 
                                               You reached the lower menu.
                                         
                    					 
                                               
				                                              
                                                                  	You pressed button one at the lower menu level.
                                                              
			                                   
                                         
                                  
			        
		  
	

The only thing we left to do is making some modifications in the MenuLevel() method. At the section where we check which commands belong to pressed key we need to add a new case to the switch statement, because the command can be a new menu as well. When it happens, create a new Menu object, add the menu command to the command list with the AddKeypressCommand (parameters are pressed key and the new Menu object), and recursively call this MenuLevel() method with the innerMenu object and that section of the XML code which belongs to the pressed key.

foreach (var element in key.Elements())
{
     switch (element.Name.ToString())
     {
           case "play":
                 menu.AddKeypressCommand(pressedKey, new PlayCommand(element.Value.ToString()));
                 break;
           case "speak":
                 menu.AddKeypressCommand(pressedKey, new SpeakCommand(element.Value.ToString()));
                 break;
           case "menu":
                 Menu innerMenu = new Menu();
                 menu.AddKeypressCommand(pressedKey, innerMenu);
                 MenuLevel(key.ToString(), innerMenu);
                 break;
           default:
                 return;
     }
}

After these modifications we have a multi-level IVR system, and if we would like to add more sub-menu levels, we do not need modify the source code, only need to write some new menu level into the XML code.

Conclusion

By using this example codes you are able to create and manage your own multi-level IVR system with the help of the components of the Ozeki VoIP SIP SDK, and you can specify more commands to customize and modernize your IVR.

If you have any questions or need assistance, please contact us at info@voip-sip-sdk.com

Related Pages

More information