Voice Chat in C#
If you remember from my previous tutorial on Recording audio in C#, streaming from the microphone was quite simple. Within about 10 minutes of coding you were able to create a recording application that worked with any recording device plugged into or connected to your machine. In another article, I talked about my XNA TCP Networking API. In this article, I'm going to show you how easy it is to create your own voice chat application using the two apis. In this article I will be teaching you the following techniques:
- Streaming audio from the microphone
- Sending audio data over the network
- Playing network audio data back
- Visual C# Express
- TCP Networking API
- Bass.NET 2.4 (download the zip in the forum)
- Un4seen Bass 2.4 (download the zip in the forum)
It's been a while, but in my Recording audio in C# sample I used an encoder (specifically the LAME Encoder) to encode data to an Mp3 via the recording stream. Since we will not be needing any encoding we won't need the lame encoder, as well we will not need the bassenc library. To start create a new Windows Form Project, VoiceChat.
Since we need the bass.dll included in our solution, add it as en existing item. Be sure to set the property Copy to Output Directory to Copy if newer. Finally, add the Bass.NET 2.4 reference located in the .NET References tab by the Add Reference dialog. You will also need to add a reference to the Network API that you downloaded as a requirement. You can browse for the library to where you unzipped it to.
- Init - Should initialize recording and audio devices
- Record - Should create the playback and recording streams
- Play - Should play the playback and recording streams
- Stop - Should stop all playback and recording
- Free - Should free all resources and streams
- PlayBuffer - Should play a byte stream buffer
- SendBufferHandler - Delegate for sending a buffer.
This method will first initialize the default recording device followed by initializing the default audio playback device and allocating that device stream by the 8Bit flag or byte flag. The 8Bit flag correlates to our byte output stream that we will later deal with on the recording callback function. Next, the Record method.
Within this method there are three field level variables being used: myRecordProc, recordStream, and playbackStream. The first, myRecordProc, is the delegate that refers to the callback on each sample recorded from Bass. The second, recordStream, refers to the actual handle created when starting the recording channel and it is designated as an int. You can see that in Bass.BASS_RecordStart the first parameter refers to the sampling rate (44100), the second as mono output, the third as the starting flags, the fourth as the period (in milliseconds) between each sample callback, the fifth referring to the callback, and the user.
Lastly, playbackStream, uses a new feature in Bass 2.4 called push streaming. Push streaming gives us the ability to create a stream and effectively push a byte stream buffer in order to create playback. Nice, huh!? We simply pass in the frequency (sample rate) of 44100, set the channels to mono, pass in the default bass flag, and the user.
At each sample callback, we will reallocate the local myBuffer byte array when either the local buffer is null or the sample buffer length is greater than the local. It would be unwise to allocate at every interval as new can be quite costly. Next, we use the Marshal.Copy method to copy the IntPtr buffer memory into our local myBuffer at index 0 and the length being the parameter length. Lastly, we simply trigger our callback function passing in the two variables.
As I mentioned before Bass provides a new push streaming feature to actually provide the stream with a buffer in order to playback. This is done with the Bass.BASS_StreamPutData method.
The last thing we need to do for our RecordUtil is provide the Play, Stop, Free and destructor methods. We will use the corresponding Bass calls to perform each action.
And there we have it! A fully functional recording utility that we can use for the rest of this project. We need a user interface to support what we want from our application, let's design that now. At the very least, our application will be able to connect to an existing voice chat session or create a local one.
Each button refers to a call associated with the Session class located in the Networking api. Let's move into that now.
Networking
This TCP Networking API was built with bare minimum requirements for a stable TCP Network implementation. It is enhanced with a few features like multithreaded callbacks with the ability to throttle expected transfer rates. The newest addition is the Session class. This class allows you to create servers and clients fairly easily. Create a new class called NetworkUtil. We will add our network layer to the application within it. We will accomplish the following:
- Constructor - Should call Session.Init to initialize the network api.
- Join - Should call Session.Join passing in the destination ip and port
- Leave - Should call Session.Leave passing in the session id
- Create - Should call Session.Create passing in the local service port to start on
- Close - Should call Session.Close passing in the session id to close the server
- Send - Should call Session.SendData passing in the byte stream buffer and session id (as a client)
- Send - Should call Session.SendData passing in the byte stream buffer, client, and session id (as the server)
As you can see, it makes since to stop recording and sending data when we're not connected and vice-versa we should start sending and recording as soon as we are connected. The only issue here is that we would be coupling our RecordUtil with our NetworkUtil if we coded it this way. We need to create two events. Start and Stop.
With these two events, we can reuse them with our AsyncServer component. To finish, we need to replace the comments with the triggering of these two events. We can also fill in our Leave method at this time.
Next on the list is the Session Create method. Creating your own session works very much like joining one. The only difference is that you will be creating the session on your local ip address and you pass in the port. We can use the same StateChange event in the AsyncServer class located in the Session.OpenSessions dictionary. However, we will only need to worry about the ServerState.Shutdown to Stop. Starting will depend on when a client joins, as well stopping when our only client disconnects. Wire to the events and fill in the code below.
Almost done, we need to fill in the Send method. Since the data will be virtually the same, we will follow the delegate heading for SendBufferCallback in the RecordUtil class. The next step will be to write the length into a new packet along with the buffer being sent. After that, we can send the data to it's rightful owner.
The nice part about the Networking API is that sending data can be as seamless as you like. The SendData in the Session class is overloaded. You may send the data to a specific destination, or send the data to the the endpoint. The key on this method is that if you attempt to send data as a server, it assumes you mean to Broadcast the data. You can access the all the clients within a given created server with the OpenSessionClients property. Also, if you're not connected the session class will simply throw your packet away before even getting to the sockets.
In the last part, receiving the data, we will create one final event called Receive which will effectively just pass the byte stream buffer. If you noticed in the Send method we wrote the length to the buffer and then we wrote the actual byte stream based on the length provided.
To receive the actual data, we need to first find out the length of the buffer. Then once we know that, we simply grab the bytes from the data and trigger the Receive event. (Simply declare a new EventHandler called Receive at the top). You'll notice that in each of the Receive and Send I am using the number 4. I use 4 because an int is four bytes.
Finale
Excellent! I think we're ready to finish off the last details in our application. Open up the code-behind for Form1.cs. Declare two variables at the top for RecordUtil, and NetworkUtil. Next, override the onload method. In this method, create initialize the two variables and wire to each of their events. As well, we will call the RecordUtil's Init method. Be sure to wire NetworkUtil's SendBuffer event to NetworkUtil's Send method.
To ensure that our UI looks superb we will disable portions when the session has started, and enable them when the session has stopped. We can also change the text of the buttons from "Join" to "Leave" (or "Create to Close") and vice-versa. With each event in the Form code there won't be much code to insert.
When the networker determines that the session can start, the recorder can begin recording. As well, the ui can then be disabled via a method we will create in a moment. When the networker stops, the recorder should stop the recording and free the streams; as well, the ui can be enabled.
NOTE: Because this is called asynchronously we need to use the BeginInvoke method to switch to the UI thread when we are using it. To do this we simply use the EmptyDelegate that we declared at the top and use anonymous syntax to invoke.
Now we can fill in the Enable and Disable ui methods. The buttons will change text depending on which was pressed and all the other fields will be enabled or disabled.
The ui is changed based on which button is enabled (btnJoin or btnCreate). In order to make the right calls to the networking class, we need to first add the button click methods to our code. The easiest way to do this is check the text of the button to determine what action to take. ("Create" should call create, "Close" should call close, "Join" should call join, and "Leave" should call leave.
We need to call network_start when we join a session because the api will already have finished it's code structure before we can even wire to the event. As well, we need to call disable ui when btnCreate is pressed because the start event is triggered when the client joins.
We're Done!
Congratulations! You have successfully created your own Voice Chat program. To test it, put the executing assemblies on one computer and another on your computer. To determine what your IP address is, just type ipconfig in the command prompt. (Your firewall may ask you to allow the service to run.) If you both have microphones you should be hearing each other talk with an incredible quality of sound. There are plenty of upgrades we can make to this program, but we got the basics across.
Comments
1. Will this work over the Internet? I mean, other than a LAN connection? Say I want to chat with someone over the Internet.
2. Does it allow many people to join in and talk at the same time, or is it only for 2 people to talk in private?
3. Could you make a text, chat system with private message ability? Again, in order to work over the Internet and allow many people to join in.
For question #3, if you need to charge a fee in order to create this, please let me know. I am willing to pay for it.
There is an alternative to traversing which is UPnP, and it's fairly simple to implement; most routers have UPnP enabled by default. Please contact me with my e-mail address if you have any further inquires. :D
As for texting/chat, adding that would be even easier than the voice chat because all that needs to be done is to convert the byte stream into text. This of course will require a packet protocol to be implemented, but overall pretty easy.