Gal Ratner
Gal Ratner is a Techie who lives and works in Los Angeles CA and Austin TX. Follow galratner on Twitter Google
How to record Skype voice conversations

First let’s start at the end. If you are looking for free software to record Skype voice conversations, please skip to the bottom of this page and download the attached file. It contains a Windows Installer msi file and should install a recorder on your machine. 

The rest of you, please keep reading :)


Skype uses a public API to listen and transmit messages to all programs on your computer. Messages are being transmitted via the native windows API and will require us to use some external method calls.
In this article we are going to build a WPF client that will communicate with Skype, detect voice calls, redirect the incoming and outgoing streams into files and finally, create a complete conversation file.


Let’s start by including the necessary external methods:

 

[DllImport("user32.dll")]
static extern uint RegisterWindowMessage(string lpString);
 
[DllImport("user32.dll")]
public static extern IntPtr SendMessageTimeout(IntPtr windowHandle, uint Msg, IntPtr wParam, IntPtr lParam, SendMessageTimeoutFlags flags, uint timeout, out IntPtr result);
 
[DllImport("user32.dll")]
public static extern IntPtr SendMessageTimeout(IntPtr windowHandle, uint Msg, IntPtr wParam, ref COPYDATASTRUCT lParam, SendMessageTimeoutFlags flags, uint timeout, out IntPtr result);

 


Communication with Skype


Sending message


SendMessageTimeout sends the specified message to one or more windows. Notice we have two overloads of SendMessageTimeout. One accepts a value to be sent and the other accepts a stuct. We are going to use the first overload to connect to Skype and the second to send API commands.


Receiving messages


In order to receive messages from Skype we need to register our window in the system. RegisterWindowMessage defines a new window message that is guaranteed to be unique throughout the system. The lpString can be used when sending or posting messages.


First let’s get a handle to our window and then register it:

 

IntPtr windowHandle = new WindowInteropHelper(this).Handle;
NativeCalls.DetectSkype(windowHandle);

 

/// <summary>
        /// Run at startup to register a system window message
        /// </summary>
        /// <param name="windowHandle">Handle to the current program window</param>
        /// <returns></returns>
        public static bool DetectSkype(IntPtr windowHandle)
        {
            hWnd = windowHandle;
            APIDiscover = RegisterWindowMessage(Utils.SkypeDiscover);
            if (APIDiscover == 0)
                return false;
            APIAttach = RegisterWindowMessage(Utils.SkypeAttach);
            if (APIAttach == 0)
                return false;
 
            return true;
        }


Next we need to intercept incoming messages. We can do that by attaching a native hook to the method WndProc. This method is available to us in windows forms; however, since we are using WPF we will need to explicitly attach it to our window:

 

HwndSource source = PresentationSource.FromVisual(thisas HwndSource;
source.AddHook(WndProc);

 

private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            if (msg == NativeCalls.APIAttach && (uint)lParam == NativeCalls.SKYPECONTROLAPI_ATTACH_SUCCESS)
            {
                // Get the current handle to the Skype window
                NativeCalls.HWND_BROADCAST = wParam;
                handled = true;
                return new IntPtr(1);
            }
            // Skype sends our program messages using WM_COPYDATA. the data is in lParam
            if (msg == NativeCalls.WM_COPYDATA && wParam == NativeCalls.HWND_BROADCAST)
            {
                COPYDATASTRUCT data = (COPYDATASTRUCT)Marshal.PtrToStructure(lParam, typeof(COPYDATASTRUCT));
                StatusTextBox.AppendText(data.lpData + Environment.NewLine);
                // Check for connection
                if (data.lpData.IndexOf("CONNSTATUS ONLINE") > -1)
                    ConnectButton.IsEnabled = false;
                // Check for calls
                IsCallInProgress(data.lpData);
                handled = true;
                return new IntPtr(1);
            }
 
            return IntPtr.Zero;
        }


Every time we receive a notification from Skype we need to check for beginning and end of voice calls and if a call that we are recording has ended we need to process the results.


Connecting to a working instance of Skype


Once have launched and logged into our account in Skype we need to try and connect to the working instance. We can do that by sending a message with the DiscoverAPI handle. Skype will pop up a dialog asking us if we allow a program to connect to Skype. Once we clicked allow we will get back a message indicating we are online:

 

public static void ConnectToSkype()
        {
            // To initiate communication, a client application broadcasts the SkypeControlAPIDiscover message, 
            // including its window handle as a wParam parameter. 
            // Skype responds with a SkypeControlAPIAttach message to the specified window and indicates the connection status.
            IntPtr result;
            IntPtr aResult = SendMessageTimeout(HWND_BROADCAST, APIDiscover, hWnd, IntPtr.Zero, SendMessageTimeoutFlags.SMTO_NORMAL, 100, out result);
        }


Starting and stopping voice recording


Each conversation in Skype has an ID. We need to capture the ID in order to create the conversation file name:

 

/// <summary>
        /// Check is a call is in progress and activate the record button
        /// </summary>
        /// <param name="status"></param>
        private void IsCallInProgress(string status)
        {
            //Listen to: CALL {} STATUS INPROGRESS
            if (status.IndexOf("CALL") > -1 && status.IndexOf("STATUS INPROGRESS") > -1)
            {
                string callRegex = @"CALL\s+(\d+)";
                Regex r = new Regex(callRegex, RegexOptions.IgnoreCase);
                Match m = r.Match(status);
                Utils.CurrentCallNumber = Convert.ToInt32(m.Groups[1].Value);
                RecordButton.IsEnabled = true;
            }
            else if (status.IndexOf("CALL") > -1 && status.IndexOf("STATUS FINISHED") > -1)
            {
                // Call ended.
                Utils.CurrentCallNumber = 0;
                RecordButton.IsEnabled = false;
                // If we are still recording, stop and prcess the conversation
                if (RecordButton.Content.ToString() == "Stop Recording")
                {
                    RecordButton.Content = "Start Recording";
                    MakeConversationFile();
                }
            }
        }


Once we have discovered that a conversation has started we need to send two commands to Skype. The first command will redirect the incoming audio stream to a wave file and the second will do the same to the local microphone audio stream:

 

public static string RecordOutputCommand = @"ALTER CALL {0} SET_OUTPUT FILE=""{1}.output""";
public static string RecordInputCommand = @"ALTER CALL {0} SET_CAPTURE_MIC FILE=""{1}.input""";

 

private void RecordButton_Click(object sender, RoutedEventArgs e)
        {
            // Fill in the new conversation file name
             string conversationFile = String.Format(Utils.SkypeConversationsFile, Utils.SkypeConversationsFolder, Utils.CurrentCallNumber);
             FileNameTextBox.Text = conversationFile;
             try
             {
                 if (!Directory.Exists(Utils.SkypeConversationsFolder))
                     Directory.CreateDirectory(Utils.SkypeConversationsFolder);
             }
             catch (Exception ex)
             {
                 MessageBox.Show("Cannot create directory " + Utils.SkypeConversationsFolder + ". " + ex.Message);
             }
            // Start recording
            if (RecordButton.Content.ToString() == "Start Recording")
            {
                string recordOutputCommand = String.Format(Utils.RecordOutputCommand, Utils.CurrentCallNumber, conversationFile);
                string recordInputCommand = String.Format(Utils.RecordInputCommand, Utils.CurrentCallNumber, conversationFile);
                NativeCalls.SendSkypeMessage(recordOutputCommand);
                NativeCalls.SendSkypeMessage(recordInputCommand);
                RecordButton.Content = "Stop Recording";
            }
            else
            {
                // Stop recording
                string recordEndOutputCommand = String.Format(Utils.RecordEndOutputCommand, Utils.CurrentCallNumber);
                string recordEndInputCommand = String.Format(Utils.RecordEndInputCommand, Utils.CurrentCallNumber);
                NativeCalls.SendSkypeMessage(recordEndOutputCommand);
                NativeCalls.SendSkypeMessage(recordEndInputCommand);
                RecordButton.Content = "Start Recording";
                MakeConversationFile();
            }
        }


To stop recording we simply redirect both stream back into their default location:

 

public static string RecordEndOutputCommand = @"ALTER CALL {0} SET_OUTPUT SOUNDCARD=""default""";
public static string RecordEndInputCommand = @"ALTER CALL {0} SET_CAPTURE_MIC PORT=""356""";


Now that we have finished recording the conversation we have two files, each fine contains different output and we need to merge them into a single conversation file.


Merging wav files with SoX


SoX is a small media processing utility program. You can download it from sourceforge here.
We are going to invoke SoX from an external process and use its command line parameters to merge the files.

 

/// <summary>
        /// Process both input and microphone files into a single conversation file
        /// </summary>
        private void MakeConversationFile()
        {
            string conversationFile = FileNameTextBox.Text;
            Task.Factory.StartNew(() =>
            {
                Thread.Sleep(5000); // Time for skype to release the files
                // Run Sox to merge both files into a single file
                Utils.RunExternalProcess(Environment.CurrentDirectory + @"\Sox\Sox"@"-m """ + conversationFile + @".output""" + @" """ + conversationFile + @".input""" + @" """ + conversationFile + @""""true, 3000, string.Empty, string.Empty, falsetruefalsetrue);
                // If sox created the new file, delete both original files
                try
                {
                    if (File.Exists(conversationFile))
                    {
                        File.Delete(conversationFile + ".input");
                        File.Delete(conversationFile + ".output");
                    }
                }
                catch { }
                // Open the folder
                Process.Start("explorer.exe""/select," + conversationFile);
            });
        }


You can tell that the last step was to open the location of the conversation file for the user to inspect.


Conclusion


The Skype open API contains many more useful commands and you can see the complete documentation here. For your connivance I have attached the complete code to this example and a full msi installer for a working demo program.


Posted 27 Aug 2011 5:32 PM by Gal Ratner
Filed under: ,
Attachment: SkypeRecording.zip

Powered by Community Server (Non-Commercial Edition), by Telligent Systems