'*************************************************************************** '* Class frmAudio * '* * '* This form manages the audio transmission to and from the E301 modules. * '* The audio format choosen is the ITU G.711-A law (8 bits/sample, 8Ks/s, * '* non-linearized to reduce noise). * '* * '* The transport protocol is RTP over UDP, according to RFC's 3550 and * '* 3551 * '* * '* For the computer audio management, Managed DirectX SDK is used. * '* * '* See more info in ClassRTPAgent.Vb, the Microsoft DirectX documentation * '* and the correspondant RFC's and standards. * '* * '* This form includes two graphical signal analyzers (one for audio sent * '* and one for the audio received) for debug and demonstration purposes * '* only. * '* * '* Also provides a tone and a chirp generator very usefull in testing the * '* audio quality of the phones. * * '* * '* IMPORTANT NOTE: The DirectX audio part of the application is not fully * '* debugged. Is an adaptation of the microsoft examples provided with the * '* DirectX SDK. It is given as a starting point and mainly to demonstrate * '* the audio RTP encapsulation. * '* * '* EQUITEL C.V. February 2007 ALMDI* '*************************************************************************** Imports System Imports System.Math Imports System.Threading Imports Microsoft.DirectX Imports Microsoft.DirectX.DirectSound Public Class frmAudio #Region "Definitions" #Region "General definitions" 'When sending audio via RTP according to the G.711(A-law) standard, 'each packet has to be 20 miliseconds: 20 ms * 80 samples/ms = 160 samples Private Const AudioSamplesPerPacket As Integer = 160 'Size in bytes of the audio packet from/to the computer audio system '(160 Samples/packet * 2 Bytes/sample = 320 bytes/packet) Private Const PCMAudioPacketLength As Integer = 320 'Object to send and receive audio packets with RTP/UDP protocol Private RTPAgent As ClassRTPAgent 'Object to hold the format of the audio (see directX documentation) Private AudioFormat As WaveFormat 'Indicates if the audio engine to read and send audio to the PC is created Private AudioDevicesSelected As Boolean = False 'Indicates if the audio is active Private AudioON As Boolean = False 'Indicates if we are sending a sinusoidal tone to the E301 instead the actual audio Private ToneON As Boolean = False 'Indicates if we are sending a chirp tone to the E301... Private ChirpON As Boolean = False 'Holds which phone attached to the E301 is active Public MasterOrSlave As Byte 'Form of the tone generator Private WithEvents ToneWindow As frmTone 'Form of the Chirp generator Private WithEvents ChirpWindow As frmChirp #End Region #Region "DirectX Audio input definitions" Private RecordDevice As Capture = Nothing Private RecordBuffer As CaptureBuffer = Nothing Private Const NumberRecordNotifications As Integer = 16 Private RecordPositionNotify(NumberRecordNotifications) As BufferPositionNotify Private RecordNotificationEvent As AutoResetEvent = Nothing Private RecordNotify As Notify = Nothing Private RecordThread As Thread = Nothing Private RecordBufferSize As Integer = 0 Private RecordNextOffset As Integer = 0 Private RecordNotifySize As Integer = 0 #End Region #Region "DirectX Audio output definitions" Private PlayDevice As Microsoft.DirectX.DirectSound.Device Private PlayBuffer As Microsoft.DirectX.DirectSound.SecondaryBuffer Private Const NumberPlayNotifications As Integer = 16 Private PlayPositionNotify(NumberRecordNotifications) As BufferPositionNotify Private PlayBufferSize As Integer = 0 Private PlayNotifySize As Integer = 0 Private PlayNextOffset As Integer = 0 Private PlayThread As Thread = Nothing Private PlayNotificationEvent As AutoResetEvent = Nothing Private PlayNotify As Notify = Nothing #End Region #Region "Events" Public Event Log(ByVal Texto As String) #End Region #End Region #Region "Methods" #Region "Audio management" Private Sub SelectAudioDevices() 'Audio format: 16 bit/sample, 8000 samples/second, 1 channel, PCM AudioFormat.BitsPerSample = 16 AudioFormat.Channels = 1 AudioFormat.SamplesPerSecond = 8000 AudioFormat.FormatTag = WaveFormatTag.Pcm AudioFormat.BlockAlign = CShort(AudioFormat.Channels * (AudioFormat.BitsPerSample / 8)) AudioFormat.AverageBytesPerSecond = AudioFormat.BlockAlign * AudioFormat.SamplesPerSecond 'Select the audio input and ouput devices If SelectInputDevice() Then If Not SelectOutputDevice() Then MsgBox("ERROR: unable to create the audio output device") Close() Else AudioDevicesSelected = True End If Else MsgBox("Error: unable to create the audio input device") Close() End If End Sub Public Sub AudioStart(ByVal DestinationIP As String, ByVal DestinationPort As Integer, ByVal LocalPort As Integer, ByVal mMasterOrSlave As Byte) 'Select the audio devices if not selcted yet If Not AudioDevicesSelected Then SelectAudioDevices() If Not AudioON Then 'Initialize analyzers ToIPAnalyzer.Initialize() FromIPAnalyzer.Initialize() 'Create the RTP Agent RTPAgent = New ClassRTPAgent(DestinationIP, DestinationPort, LocalPort) 'Create the input notify engine and begin audio capture CreateInputNotify() RecordBuffer.Start(True) 'Create the output notify engine and begin audio output CreateOutputNotify() PlayBuffer.Play(0, BufferPlayFlags.Looping) 'Store the state MasterOrSlave = mMasterOrSlave AudioON = True End If End Sub Public Sub AudioStop() If AudioON Then 'Dispose the event monitoring threads PlayThread.Abort() RecordThread.Abort() 'Stop the audio capture and output If Not RecordBuffer Is Nothing Then RecordBuffer.Stop() End If If Not PlayBuffer Is Nothing Then PlayBuffer.Stop() End If 'Dispose the RTP agent RTPAgent.Dispose() RTPAgent = Nothing AudioON = False End If End Sub #End Region #Region "Audio input from soundcard" Private Function SelectInputDevice() As Boolean Dim DevCol As New CaptureDevicesCollection Dim CaptureDeviceGuid As Guid = Guid.Empty 'We select the first audio interface avalaible in the computer 'In computer with more than one audio interfaces, this code may 'be changed to select wich one to use. 'The sound source is the choosen one by the user in the operative system. Try CaptureDeviceGuid = DevCol(0).DriverGuid RecordDevice = New Capture(CaptureDeviceGuid) Catch End Try Return Not (RecordDevice Is Nothing) End Function Private Sub CreateInputNotify() 'Creates the input object to read data from the PC audio card. Dim Desc As New CaptureBufferDescription() Dim i As Integer 'If it is already created, dispose it and create again If Not RecordNotify Is Nothing Then RecordNotify.Dispose() RecordNotify = Nothing End If If Not RecordBuffer Is Nothing Then RecordBuffer.Dispose() RecordBuffer = Nothing End If 'We want to be advised each time a audio packet of PCMAudioPacket 'bytes is ready to be read. Tis size must be multiple of AudioFormat.BlockAlign RecordNotifySize = PCMAudioPacketLength RecordNotifySize -= RecordNotifySize Mod AudioFormat.BlockAlign 'The total buffer size must be the product of the RecordNotifySize by the 'number of notifications we want RecordBufferSize = RecordNotifySize * NumberRecordNotifications 'Buffer creation Desc.BufferBytes = RecordBufferSize Desc.Format = AudioFormat Desc.WaveMapped = True Try RecordBuffer = New CaptureBuffer(Desc, RecordDevice) Catch e As Exception MsgBox("ERROR opening audio device.", MsgBoxStyle.Critical) Close() End Try RecordNextOffset = 0 If Nothing Is RecordBuffer Then MsgBox("ERROR opening audio device.", MsgBoxStyle.Critical) Close() End If 'This object will indicate when an audio packet is ready to be read from 'the audio card RecordNotificationEvent = New AutoResetEvent(False) 'We configure the object RecordPositionNotify to indicate the postitions of the buffer 'that must signal an event when reached For i = 0 To NumberRecordNotifications - 1 'Position of the buffer RecordPositionNotify(i).Offset = RecordNotifySize * (i + 1) - 1 'Handler to the event generator object RecordPositionNotify(i).EventNotifyHandle = RecordNotificationEvent.SafeWaitHandle.DangerousGetHandle Next i 'Tell DirectSound when to notify the app. The notification will come in the from 'of signaled events that are handled in the notify thread. RecordNotify = New Notify(RecordBuffer) RecordNotify.SetNotificationPositions(RecordPositionNotify, NumberRecordNotifications) 'Create a thread to monitor the notify events RecordThread = New Thread(New ThreadStart(AddressOf InputProcess)) RecordThread.Start() End Sub Private Sub InputProcess() 'Monitoring thread for the audio input events While True 'Wait until a new audio packet is ready If RecordNotificationEvent.WaitOne(1000, True) Then ReadAudio() Else RaiseEvent Log("Timeout waiting for audio input from PC") End If End While End Sub Private Sub ReadAudio() 'We enter here each 20 miliseconds to read a new audio packet 'from the PC audio card 'Packet to be read Dim ReadPacket(AudioSamplesPerPacket) As Short 'Pointers to read the DirectSound buffer Dim ReadPos As Integer Dim CapturePos As Integer Dim LockSize As Integer 'If recordBuffer is not created then do nothing If RecordBuffer Is Nothing Then Return End If RecordBuffer.GetCurrentPosition(CapturePos, ReadPos) LockSize = ReadPos - RecordNextOffset If LockSize < 0 Then LockSize += RecordBufferSize End If 'Block align lock size so that we are always write on a boundary LockSize -= LockSize Mod RecordNotifySize If 0 = LockSize Then Return End If 'Read the capture buffer. Try 'If we must send tone instead real audio, get the packet from the tone generation window 'If ToneON Then 'ToneWindow.ToneGenerator.OutputPacket(ReadPacket, AudioSamplesPerPacket) 'Else 'The same with the Chirp tone 'If ChirpON Then 'ChirpWindow.ChirpGenerator.OutputPacket(ReadPacket, AudioSamplesPerPacket) 'Else 'Actual read from the PC audio card ReadPacket = RecordBuffer.Read(RecordNextOffset, GetType(Short), LockFlag.None, LockSize) 'End If 'End If 'RTP packet send RTPAgent.SendAudioPacket(ReadPacket) Catch e As Exception RaiseEvent Log("Error in audio input" + e.ToString) End Try 'Signal analyzer update ''''''' ToIPAnalyzer.NewAudioPacket(ReadPacket, AudioSamplesPerPacket) ' Move the capture offset along RecordNextOffset += ReadPacket.Length RecordNextOffset = RecordNextOffset Mod RecordBufferSize ' Circular buffer End Sub #End Region #Region "Audio output to soundcard" Private Function SelectOutputDevice() As Boolean Dim DevCol As New Microsoft.DirectX.DirectSound.DevicesCollection() Dim DeviceGuid As Guid 'We select the first audio interface avalaible in the computer 'In computer with more than one audio interfaces, this code may 'be changed to select wich one to use. Try DeviceGuid = DevCol(0).DriverGuid PlayDevice = New Microsoft.DirectX.DirectSound.Device(DeviceGuid) Catch End Try If Not PlayDevice Is Nothing Then PlayDevice.SetCooperativeLevel(Me, CooperativeLevel.Priority) End If Return Not (PlayDevice Is Nothing) End Function Private Sub CreateOutputNotify() 'Creates the output system to send data to the PC audio card Dim Desc As New Microsoft.DirectX.DirectSound.BufferDescription() Dim Silencio As Byte() Dim i As Integer 'If it is already created, dispose it and create again If Not PlayNotify Is Nothing Then PlayNotify.Dispose() PlayNotify = Nothing End If If Not PlayBuffer Is Nothing Then PlayBuffer.Dispose() PlayBuffer = Nothing End If 'We want to be advised each time buffer of PCMAudioPacket 'bytes is ready to be written. Tis size must be multiple of AudioFormat.BlockAlign PlayNotifySize = PCMAudioPacketLength PlayNotifySize -= PlayNotifySize Mod AudioFormat.BlockAlign 'The total buffer size must be the product of the RecordNotifySize by the 'number of notifications we want PlayBufferSize = RecordNotifySize * NumberPlayNotifications 'Create the output buffer with the same format than the input one Desc.Format = RecordBuffer.Format Desc.BufferBytes = PlayBufferSize Desc.ControlPositionNotify = True Desc.CanGetCurrentPosition = True If PlayBuffer Is Nothing Then PlayBuffer = New Microsoft.DirectX.DirectSound.SecondaryBuffer(Desc, PlayDevice) End If 'Fill the buffer with silience ReDim Silencio(PlayBufferSize - 1) For i = 0 To PlayBufferSize - 1 Silencio(i) = 0 Next PlayBuffer.Write(0, Silencio, LockFlag.None) 'This object will indicate when an audio packet is needed to be written to 'the audio card PlayNotificationEvent = New AutoResetEvent(False) 'We configure the object RecordPositionNotify to indicate the postitions of the buffer 'that must signal an event when reached For i = 0 To NumberPlayNotifications - 1 PlayPositionNotify(i).Offset = PlayNotifySize * i + PlayNotifySize - 1 PlayPositionNotify(i).EventNotifyHandle = PlayNotificationEvent.SafeWaitHandle.DangerousGetHandle Next i PlayNotify = New Notify(PlayBuffer) 'Tell DirectSound when to notify the app. The notification will come in the from 'of signaled events that are handled in the notify thread. PlayNotify.SetNotificationPositions(PlayPositionNotify, NumberPlayNotifications) 'Create a thread to monitor the notify events PlayThread = New Thread(New ThreadStart(AddressOf OutputProcess)) PlayThread.Start() End Sub Private Sub OutputProcess() 'Monitoring thread for the audio output events While True 'Wait until a new audio packet is needed If PlayNotificationEvent.WaitOne(1000, True) Then OutputAudio() Else RaiseEvent Log("Timeout waiting for audio output to PC") End If End While End Sub Private Sub OutputAudio() 'Each 20 miliseconsd we must send a new audio packet to the 'PC audio card Dim ReceivedPacket(AudioSamplesPerPacket) As Short Dim WritePos As Integer Dim PlayPos As Integer Dim LockSize As Integer 'If PlayBuffer is not created yet, do nothing If PlayBuffer Is Nothing Then Return End If 'Get the writting position in the buffer PlayBuffer.GetCurrentPosition(PlayPos, WritePos) LockSize = WritePos - PlayNextOffset If LockSize < 0 Then LockSize += PlayBufferSize End If ' Block align lock size so that we are always write on a boundary LockSize -= LockSize Mod PlayNotifySize If 0 = LockSize Then Return End If 'Get a new audio packet from the RTP agent (i.e. from the E301 module) ReceivedPacket = RTPAgent.ReceivedAudioPacket 'If we have to send the audio to the PC sound card, copy this packet to the 'play buffer. Else, the silent play buffer continues playing. 'If Not ChkSilent.Checked Then PlayBuffer.Write(PlayNextOffset, ReceivedPacket, LockFlag.None) 'End If 'Update the signal analyzer FromIPAnalyzer.NewAudioPacket(ReceivedPacket, AudioSamplesPerPacket) ' Move the capture offset along PlayNextOffset += PCMAudioPacketLength PlayNextOffset = PlayNextOffset Mod PlayBufferSize ' Circular buffer End Sub #End Region #Region "Form and controls behaviour" Private Sub ChkSilent_CheckedChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles ChkSilent.CheckedChanged 'Sometimes, can be usefull to not send the actual audio to the PC audio system. 'If you send to the E301 the output of the PC wave mixer, the same output 'you are hearing, sending the real incoming audio to the PC will make a loop. 'With this utility you can break the loop and allows you to send easily any audio 'you can hear in the PC to the sos phone (music etc.) Dim i As Integer Dim Silence As Byte() If ChkSilent.Checked Then If Not PlayBuffer Is Nothing Then 'Clear the play buffer ReDim Silence(PlayBufferSize - 1) For i = 0 To PlayBufferSize - 1 Silence(i) = 0 Next PlayBuffer.Write(0, Silence, LockFlag.None) End If End If End Sub Private Sub MainForm_Closing(ByVal sender As Object, ByVal e As System.ComponentModel.CancelEventArgs) Handles MyBase.Closing If Not RecordNotificationEvent Is Nothing Then RecordNotificationEvent.Set() End If If Not PlayNotificationEvent Is Nothing Then PlayNotificationEvent.Set() End If AudioStop() End Sub Private Sub ToneWindow_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles ToneWindow.FormClosing ToneON = False BtnTone.Enabled = True BtnChirp.Enabled = True End Sub Private Sub BtnTone_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnTone.Click BtnTone.Enabled = False BtnChirp.Enabled = False ToneWindow = New frmTone ToneON = True ToneWindow.Show() End Sub Private Sub BtnChirp_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnChirp.Click BtnTone.Enabled = False BtnChirp.Enabled = False ChirpWindow = New frmChirp ChirpON = True ChirpWindow.Show() End Sub Private Sub ChirpWindow_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles ChirpWindow.FormClosing ChirpON = False BtnTone.Enabled = True BtnChirp.Enabled = True End Sub #End Region #End Region Public Sub New() ' Llamada necesaria para el Diseñador de Windows Forms. InitializeComponent() ' Agregue cualquier inicialización después de la llamada a InitializeComponent(). 'Hide() End Sub Private Sub GroupBox1_Enter(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles GroupBox1.Enter End Sub End Class