Course 2 / Lecture 4

How to build a VoIP PBX in C#

How to create voicemail in your VoIP PBX

Ozeki VoIP SIP SDK provides professional technology to create a virtual PBX extension for voice mail purposes in your PBX system. Read this article and use the source codes to start implementing this solution efficiently.

Download: voice-mail.zip

The example program that is introduced in this article is an extended version of the PBX implementation shown in the "VoIP PBX Dial plans" article. In the following sections, you will learn what new programming tools you need to create for implementing a Voice Mail virtual extension for your PBX.

Introduction

Please be informed that PBX development support is available in Ozeki VoIP SIP SDK from version 10.0. You can download the latest Ozeki SIP SDK version from the download page.

Private Branch Exchange (PBX) systems are used for phone line establishment between communicating end points, regardless of wired, mobile or VoIP technology they use. The end points are registered to the PBX through a SIP account and are called extensions. The PBX can also have virtual extensions that are not attached to physical or software communication solutions but used for some special purposes.

The sample C# application that is introduced in this article is an example for a virtual PBX extension for voice mail purposes. As Ozeki VoIP SIP SDK provides the splendid PBX behavior, you only need to define or override those functionalities, you want to change. As for a virtual extension, you need to define the Voice Mail extension and set it in the PBX. After this you can set a dialing rule that in some cases the voice mail should answer the calls - for example when no person answered in a certain amount of time.

The following program code uses the unique background support of Ozeki VoIP SIP SDK, therefore you will need to download and install Ozeki SIP SDK on your computer before starting to use the program code. You will also need to have Visual Studio 2010 or compatible IDE and .NET Framework installed on your system, as the program code below is written in C# language.

The virtual extension definition needs some basic modification in the PBX class code. Code 1 shows the new constructor of the PBX class. The main change is the usage of the extension container. This tool is needed for the virtual extension handling. When you got the extension handler provided by the Ozeki SDK, you can add any virtual extensions to it with a specific SIP account.

protected override void OnStart()
{
    SetDialplanProvider();
	SetListenedPort();

	InitVoiceMail();

	Console.WriteLine("PBX started.");
	base.OnStart();
}

void SetDialplanProvider()
{
   	var callManager = GetService<ICallManager>();
	var extensionContainer = GetService<IExtensionContainer>();
	callManager.DialplanProvider = new MyDialplanProvider(userInfoContainer, extensionContainer);
}

private void SetListenedPort()
{
	SetListenPort(localAddress, 5060, TransportType.Udp);
	Console.WriteLine("Listened port: 5060(UDP)");
}

void InitVoiceMail()
{
	var callManager = GetService<ICallManager>();
	var extensionContainer = GetService<IExtensionContainer>();

	var rootPath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
	var voiceMailRootPath = Path.Combine(rootPath, "Records");

	var voiceMailExtension = new VoiceMailExtension(VoiceMailNumber, callManager, voiceMailRootPath);
	extensionContainer.TryAddExtension(voiceMailExtension);
}
Code 1 - Extended constructor code

The dial plan provider class also needs an extension, as the dial routing needs to contain the cases when the Voice Mail extension is called. There are two cases when the Voice Mail will answer a call (Code 2).

When an extension does not answer a call, the PBX will redirect the call to the Voice Mail extension that automatically answers the call and records a voice message from the caller.

When an end point directly calls the Voice Mail extension, they can listen to the voice mails left to that extension. The voice mails are deleted after listening.

		public IDialplanResult GetDestination(RouteInfo routeInfo)
        {
            var currentDialed = routeInfo.DialInfo.Dialed;

            // if dialed number was busy or not answered, hang up call
            if (routeInfo.State != RoutingState.Calling)
            {
                UserInfo userInfo;
                if (userInfoContainer.TryGetUserInfo(currentDialed, out userInfo))
                {
                    var destination = new Destination(MyPBX.VoiceMailNumber);
                    destination.AddAdditionalParameter("voicemailtarget", currentDialed);
                    return destination;
                }

                return null;
            }

            // if "200" dialed, ring the first available extension
            if (currentDialed == "200")
            {
                var userInfos = userInfoContainer.GetUserInfos();
                foreach (var userInfo in userInfos)
                {
                    // skip the caller extension
                    if (userInfo.UserName == routeInfo.SourceExtension.ExtensionID)
                        continue;

                    var extension = extensionContainer.GetExtension(userInfo.UserName);
                    if (extension != null)
                        return new Destination(extension.ExtensionID);
                }

                return null;
            }

            return new Destination(currentDialed);
        }
Code 2 - Setting a dial routing rule for the Voice Mail

The Voice Mail is a virtual extension that needs to be defined in a class derived from the IExtension interface (Code 3). The extension needs to handle the contact information as the system needs to know which extension wants to listen their messages or leave a message to the called party.

class VoiceMailExtension : IExtension
    {
        List<VoiceMailCallHandler> voiceMailCallHandlers;
        ICallManager callManager;
        string voiceMailRootPath;

        public VoiceMailExtension(string id, ICallManager callManager, string voiceMailRootPath)
        {
            ExtensionID = id;
            this.callManager = callManager;
            this.voiceMailRootPath = voiceMailRootPath;

            voiceMailCallHandlers = new List<VoiceMailCallHandler>();
        }
Code 3 - Defining a virtual extension

The PBX extensions need to create PBXCall objects that handle the calls of the given extensions (Code 4).

		public void OnCalled(IPBXCall call, IncomingCallParams callParams)
        {
            string target = null;
            if (callParams.AdditionalParameters.ContainsKey("voicemailtarget"))
                target = callParams.AdditionalParameters["voicemailtarget"];

            var voiceMailCallHandler = new VoiceMailCallHandler(call, target, voiceMailRootPath, callManager);

            voiceMailCallHandlers.Add(voiceMailCallHandler);
            voiceMailCallHandler.Completed += HandlerCompleted;
        }
Code 4 - Creating a PBX call

The actual voice mail calls are handled in a separate class, the definition is shown in Code 5. The call needs to be subscribed for the call state changed event that is implemented in the constructor of the class.

class VoiceMailCallHandler
    {
        IPBXCall call;
        IVoiceMailCommand voiceMailCommand;
        ICallManager callManager;
        string target;
        string voiceMailRootPath;

        public VoiceMailCallHandler(IPBXCall pbxCall, string target, string voiceMailRootPath, ICallManager callManager)
        {
            this.voiceMailRootPath = voiceMailRootPath;
            this.callManager = callManager;
            this.target = target;

            call = pbxCall;
            call.CallStateChanged += call_CallStateChanged;
        }
Code 5 - Voice mail call handling

The event handler method of the call state change is implemented in the method shown in Code 6. There are different cases that need to be handled in case of an incoming call (Ringing call state). In case of any problems like missing caller or callee data, the call is rejected.

When the call is a direct call to the Voice Mail extension (the caller actually dialed 800), the voice mails will be played for listening. This is implemented in the VoiceMailPlayCommand class.

When the caller called an extension that did not answer and the call was redirected to the Voice Mail extension, the caller can leave a message by using the VoiceMailRecordCommand class functionalities.

		void call_CallStateChanged(object sender, CallStateChangedArgs e)
        {
            if (e.State == CallState.Ringing)
            {
                var other = callManager.SessionContainer.GetOtherCall(call);
                if (other == null)
                {
                    call.HangUp();
                    return;
                }

                var caller = other.Owner.ExtensionID;

                if (target != null)
                {
                    voiceMailCommand = new VoiceMailRecordCommand(call, voiceMailRootPath, target, caller);
                    voiceMailCommand.Completed += ActionCompleted;
                }
                else
                {
                    voiceMailCommand = new VoiceMailPlayCommand(call, voiceMailRootPath, caller);
                    voiceMailCommand.Completed += ActionCompleted;
                }

                voiceMailCommand.Execute();
            }
        }
Code 6 - Handling the incoming calls

The voice mail behavior is implemented in two separate classes, both derived from the IVoiceMailCommand interface (Code 7).

class VoiceMailPlayCommand : IVoiceMailCommand
Code 7 - One of the voice mail command class definitions

The voice mail listening starts with a text to speech message that informs the caller about the number of messages to listen. The constructor of this class (Code 8) makes the basic settings for this text to speech process, all the other functionalities are defined in other methods.

		public VoiceMailPlayCommand(IPBXCall call, string voiceMailRootPath, string caller)
        {
            extensionVoiceMailPath = Path.Combine(voiceMailRootPath, caller);

            this.call = call;

            mediaConnector = new MediaConnector();
            textToSpeech = new TextToSpeech();
            phoneCallAudioSender = new PhoneCallAudioSender();
            phoneCallAudioSender.AttachToCall(call);

            mediaConnector.Connect(textToSpeech, phoneCallAudioSender);

            playingState = VoiceMailPlayingState.Welcome;
            this.call.CallStateChanged += CallStateChanged;
            textToSpeech.Stopped += TextToSpeechCompleted;
        }
Code 8 - Starting the message listening

In case of message listening, the incoming call is automatically accepted (Code 9). This Execute method is called in the VoiceMailCallHandler class introduced above.

public void Execute()
{
    call.Answer();
}
Code 9 - The Voice Mail extension automatically accepts the call

When the call is accepted, the call state changes to InCall and the text message is read (Code 10).

 		void CallStateChanged(object sender, CallStateChangedArgs e)
        {
            if (e.State == CallState.Answered)
            {
                GotoNextStep();
                return;
            }

            if (e.State.IsCallEnded())
                CleanUp();
        }
Code 10 - The Voice Mail reads up the number of the waiting messages

The text to speech object needs to be subscribed for the Completed event - this is done in the constructor. When the text reading ended, this event will occur and the event handler shown in Code 11 will be invoked.

After the number of waiting messages the Voice Mail extension starts to play the actual waiting messages. If there are no waiting messages, the call will be ended.

		void TextToSpeechCompleted(object sender, EventArgs e)
        {
            List<string> voiceMails;
            switch (playingState)
            {
                case VoiceMailPlayingState.Welcome:
                    voiceMails = GetVoiceMails();
                    if (voiceMails.Count == 0)
                    {
                        playingState = VoiceMailPlayingState.GoodBye;
                        textToSpeech.AddAndStartText("You have no new messages. Good bye, and have a nice day!");
                    }
                    else
                    {
                        textToSpeech.AddAndStartText(string.Format("You have {0} new messages. ", voiceMails.Count));
                        playingState = VoiceMailPlayingState.MessageInfo;
                    }
                        
                    break;

                case VoiceMailPlayingState.MessageInfo:
                    
                    voiceMails = GetVoiceMails();
                    if (voiceMails.Count == 0)
                    {
                        playingState = VoiceMailPlayingState.GoodBye;
                        textToSpeech.AddAndStartText("You have no more new messages. Good bye, and have a nice day!");
                    }
                    else
                    {
                        playingState = VoiceMailPlayingState.Message;
                        PlayVoiceMailInfo();
                    }
         
                    break;

                case VoiceMailPlayingState.Message:
                    voiceMails = GetVoiceMails();
                    if (voiceMails.Count == 0)
                    {
                        playingState = VoiceMailPlayingState.GoodBye;
                        textToSpeech.AddAndStartText("You have no more new messages. Good bye, and have a nice day!");
                    }
                    else
                    {
                        playingState = VoiceMailPlayingState.MessageInfo;
                        PlayVoiceMail();
                    }
                    break;

                case VoiceMailPlayingState.GoodBye:
                    call.HangUp();
                    break;

                default:
                    throw new ArgumentOutOfRangeException();
            }      
        }
Code 11 - Starting to play the waiting messages

Playing the voice mails is a recursive process. The previously played voice message is deleted and if there is at least one waiting message it will be played (Code 12).

		void PlayVoiceMail()
        {
            try
            {
                if (currentVoiceMailPath == null)
                {
                    playingState = VoiceMailPlayingState.GoodBye;
                    textToSpeech.AddAndStartText("You have no more messages. Good bye, and have a nice day!");
                    return;
                }

                waveStreamPlayback = new WaveStreamPlayback(currentVoiceMailPath);
                mediaConnector.Connect(waveStreamPlayback, phoneCallAudioSender);

                waveStreamPlayback.Stopped += VoiceMailPlayingCompleted;
                waveStreamPlayback.Start();
            }
            catch (Exception ex)
            {
                Console.WriteLine("Unexcepted error occurred.", ex);
            }
        }
Code 12 - Recursive method for voice message playing

The Voice Mail extension deletes the previously played voice mails by calling the method shown in Code 13. This method chooses the file that was set as the previous current message and deletes it.

		void DeletePlayedVoiceMail()
        {
            try
            {
                if (currentVoiceMailPath == null)
                    return;

                if(waveStreamPlayback == null)
                    return;

                mediaConnector.Disconnect(waveStreamPlayback, phoneCallAudioSender);
                waveStreamPlayback.Dispose();

                File.Delete(currentVoiceMailPath);
                currentVoiceMailPath = null;
            }
            catch (Exception ex)
            {
                Console.WriteLine("Unexcepted error occurred.", ex);
            }
        }
Code 13 - Deleting the previously played voice mail

When the Voice Mail extension was not called directly, but the call was redirected to it as the called party did not answered the call, the caller can leave a voice message to the called person. This functionality is implemented in this example program in the VoiceMailRecordCommand class.

The constructor of the class is shown in Code 14. This class also implements the IVoiceMailCommand interface and automatically accepts the call.

The constructor initializes a text to speech reader that reads up a simple message informing the caller that it is a voice mail.

		public VoiceMailRecordCommand(IPBXCall call, string voiceMailRootPath, string target, string caller)
        {
            extensionVoiceMailPath = Path.Combine(voiceMailRootPath, target);
            this.caller = caller;

            mediaConnector = new MediaConnector();

            phoneCallAudioReceiver = new PhoneCallAudioReceiver();
            phoneCallAudioReceiver.AttachToCall(call);

            phoneCallAudioSender = new PhoneCallAudioSender();
            phoneCallAudioSender.AttachToCall(call);

            dtmfEventWavePlayer = new DtmfEventWavePlayer();

            textToSpeech = new TextToSpeech();
            textToSpeech.AddText(string.Format("{0} is not available. Please leave a message after the beep.", target));

            mediaConnector.Connect(textToSpeech, phoneCallAudioSender);
            mediaConnector.Connect(dtmfEventWavePlayer, phoneCallAudioSender);

            this.call = call;
            this.call.CallStateChanged += CallStateChanged;
            textToSpeech.Stopped += TextToSpeechCompleted;
        }
Code 14 - Initializing a voice mail recorder

The beep sound for the message leaving is implemented with a simple DTMF signal sending (Code 15). This sound is played after the information message has been read by the text to speech converter.

		
		private void SendBeep()
        {
            dtmfEventWavePlayer.Start(DtmfNamedEvents.Dtmf0);
            Thread.Sleep(500);
            dtmfEventWavePlayer.Stop();
        }
Code 15 - Sending a beep sound

The voice mail recorder automatically accepts the call and plays the text message and the beep sound. This functionality is implemented in the call state change and text to speech completed event handler methods (Code 16).

		void CallStateChanged(object sender, CallStateChangedArgs e)
        {
            if (e.State.IsInCall())
            {
                textToSpeech.Start();
                return;
            }

            if (e.State.IsCallEnded())
                CleanUp();
        }

		void TextToSpeechCompleted(object sender, EventArgs e)
        {
            SendBeep();
            RecordVoiceMail();
        }
Code 16 - Basic event handlers in the voice mail recorder class

The actual voice mail recording is implemented in the RecordVoiceMail method (Code 17). The method uses a standard Ozeki VoIP SDK tool, the WaveStreamRecorder for recording the messages in .wav audio formats. The recording works exactly the same as in the case of a simple softphone.

The recorded audio file will be stored in a folder with the name of the called extension and the file name will contain the name of the caller.

		private void RecordVoiceMail()
        {
            lock (sync)
            {
                try
                {
                    tempFileName = Path.GetTempFileName();

                    waveStreamRecorder = new WaveStreamRecorder(tempFileName);
                    mediaConnector.Connect(phoneCallAudioReceiver, waveStreamRecorder);

                    waveStreamRecorder.Start();
                    currentDate = DateTime.Now.ToString("yyyy-MM-dd-HH-mm");
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Unexcepted error occurred.", ex);
                }
            }
        }
Code 17 - Voice mail recording

Now you can have a view about voice mail handling and virtual extension creation in an Ozeki VoIP SIP SDK supported PBX system. The example program showed you the concept behind the process, and now, you are ready to use your knowledge to write your own PBX with a voice mail extension using Ozeki SIP SDK.

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

You can select a suitable Ozeki VoIP SIP SDK license for PBX development on Pricing and licensing information page

Related Pages

More information