Sound and DirectXSound Tutorial

My Objectives

Basic

  • A small explanation about sound, and sound format.
  • How to read/write to a wave file.
  • How to play short length sounds with DirectXSound buffers (loading the entire sound file at once)

Medium difficulty

  • How to record/play with DirectXSound using a split buffer for very long sounds (for streaming).
  • How to display the wave data (oscilloscope).
  • How to convert from one format to another (like 8-bit to 16-bit, or 11000 Hz to 8000 Hz, Stereo to Mono, and so forth).
  • How to split a stereo sound to two mono sounds.
  • How to mix sounds, change the volume digitally.

What Is Sound?

In computers, sound is digital. That means that sound is represented by a long array of numbers. (That is not the official definition, by the way). Therefore, if you want to modify the sound (the volume, for example), you have to change those numbers in one way or another (I'll explain how to do that later).

Sound Format: What Is ... ?

  • Bit Rate: In a wave file, the bit rate can be of 8-bit or 16-bit. 8-bit is one byte; therefore, the sound is stored in arrays of Byte type; 16-bit waves (2 bytes) are stored in an Integer array type. However, you also can store a 16-bit wave in a Byte array (as long as the length is always divisible by 2). Don't get me wrong; this does not mean that you can't store sound in any other data type. In fact, sound can be stored in almost any data type, even strings; as long as you know how to handle the data buffer, it really does not matter. But preferably, it is better to store the sound in simple data types such as Byte and Integer, corresponding to 8-bit and 16-bit waves. Another reason why you should store the sound in Byte and Integer data types is that it is easier to manipulate the sound (in other words, change the volume, and so on).
  • Stereo, Mono: Everyone who has any clue about music should know this. It is how many channels the wave file has. Mono means one channel, Stereo means two channels.
  • Samples Per Second: When the sound card converts from analog to digital, it takes "samples" of the wave, and it does so really fast, as in thousands of times per second. Usually, sample rates range from 8,000 Hz to 44,100 Hz, although I've seen sample rates from 4,000 Hz to 110,000 Hz. When the sound is mono, it reads one number per sample; when the sample is stereo, it reads two numbers per sample. So, a sample includes either one channel or two channels of data.
  • Block Align: This is the most complicated one. Block Align is a number that tells the program how much it should read (how many bytes) to get a complete sample. The sample can be 8- or 16-bit, and also Stereo or Mono.
    It is calculated using this formula: BlockAlign = BitsPerSample * Channels / 8
    So, if you have a sound that is 8 bits, and mono, the block align should be 1 byte; if your sound is 16 bits, and stereo, it should be 4 bytes.
  • Average Bytes Per Second: Yes you guessed it, this tells the program how many bytes are in one second for this sound format.
    The easiest way to calculate this number is with this formula: AvgBytesPerSec = SamplesPerSec * BlockAlign
  • Frequency: A frequency is how many impulses are in one second. A frequency is represented by hertz (Hz). For example, if the frequency is 1000 Hz, it means that the sound has 1000 impulses per second.
  • Impulse: An impulse looks like a sinus (or cosinus). When represented graphically, it looks like this:
  • An impulse consists of an upper "bump" and a lower "bump."

A wave file has a header that is composed of three individual small headers, and the wave data (raw data). These are the headers:

Private Type FileHeader
   lRiff As Long
   lFileSize As Long
   lWave As Long
   lFormat As Long
   lFormatLength As Long
End Type

Private Type WaveFormat
   wFormatTag As Integer
   nChannels As Integer
   nSamplesPerSec As Long
   nAvgBytesPerSec As Long
   nBlockAlign As Integer
   wBitsPerSample As Integer
End Type

Private Type ChunkHeader
   lType As Long
   lLen As Long
End Type

They are written in the wave file in the same order you see in the code above.

First is the File Header, which tells the program reading the file that this file IS a wave. You have the total file size, wave format identifier, the format identifier, and the length of the wave format structure, following the wave structure itself.

The last part of the header is the Chunk Header. The wave data (raw data) does not have to be in one big chunk; it can be in multiple chunks, as long as the data is preceded by the chunk header.

Writing to a wave file is the easiest ever. You just fill in the values in the structures, and then write them in the file one by one, like here:

Private Sub WaveWriteHeader(ByVal OutFileNum As Integer, _
                            WaveFmt As WaveFormat)
   Dim header As FileHeader
   Dim chunk As ChunkHeader

   With header
      .lRiff = &H46464952      ' "RIFF"
      .lFileSize = 0
      .lWave = &H45564157      ' "WAVE"
      .lFormat = &H20746D66    ' "fmt "
      .lFormatLength = Len(WaveFmt)
   End With

   chunk.lType = &H61746164    ' "data"
   chunk.lLen = 0

   Put #OutFileNum, 1, header
   Put #OutFileNum, , WaveFmt
   Put #OutFileNum, , chunk
End Sub

If you look at the code closely, you will see that the variable lFileSize of the FileHeader structure is 0, and the lLen of ChunkHeader is 0. This is because you did not write the wave data yet, so you don't know the sizes.

Right after you write the header information (having the two variables of 0), you have to write the wave data. When you are done, you will know the exact length of the file, and the length of the wave data.

Sound and DirectXSound Tutorial

I created this small sub that calculates those values and updates the sizes. All you have to do is give the file number to it. This function should be called when you are done saving the wave data into the file, and right before closing the file.

Private Sub WaveWriteHeaderEnd(ByVal OutFileNum As Integer)
   Dim header As FileHeader
   Dim HdrFormat As WaveFormat
   Dim chunk As ChunkHeader
   Dim Lng As Long

    Lng = LOF(OutFileNum)
    Put #OutFileNum, 5, Lng   ' write the FileHeader.lFileSize
                              ' value

   Lng = LOF(OutFileNum) - (Len(header) + Len(HdrFormat) _
      + Len(chunk))
   Put #OutFileNum, Len(header) + Len(HdrFormat) _
      + 5, Lng                ' write the ChunkHeader.lLen value
End Sub

And the function to read from a wave file:

Private Function WaveReadFormat(ByVal InFileNum As Integer, _
   ByRef lDataLength As Long) As WaveFormat
   Dim header As FileHeader
   Dim HdrFormat As WaveFormat
   Dim chunk As ChunkHeader
   Dim by As Byte
   Dim i As Long

   Get #InFileNum, 1, header

   ' Check for "RIFF" tag and exit if not found.
   If header.lRiff <> &H46464952 Then Exit Function
   ' Check for "WAVE" tag and exit if not found.
   If header.lWave <> &H45564157 Then Exit Function
   ' Check for "fmt " tag and exit if not found.
   If header.lFormat <> &H20746D66 Then Exit Function

   ' Check format chunk length; if less than 16, it's not PCM data
   ' so we can't use it.
   If header.lFormatLength < 16 Then Exit Function

   Get #InFileNum, , HdrFormat    ' Retrieve format.

   ' Seek to next chunk by discarding any format bytes.
   For i = 1 To header.lFormatLength - 16
      Get #InFileNum, , by
   Next
   ' Ignore chunks until we get to the "data" chunk.
   Get #InFileNum, , chunk
   Do While chunk.lType <> &H61746164
      For i = 1 To chunk.lLen
         Get #InFileNum, , by
      Next
      Get #InFileNum, , chunk
   Loop

   lDataLength = chunk.lLen    ' Retrieve the size of the data.

   WaveReadFormat = HdrFormat
EndFunction

I will attach a project that shows the previous three functions in use later on in the tutorial.

Right now, you won't be able to use the functions because you are missing something: the wave data...

But if you want to see a small example of how it's done:

' Sample how to write to a wave file

Private Sub Create_Wave_File_Example()
   Dim Buffer() As Integer
   Dim WaveFmt As WaveFormat
   Dim FileNum As Integer

' make a 1 second buffer (knowing that the sample rate is 22050)
' VB initializes the array with 0 (zero)s; 0 means silence
' (for 16 bit wave file)
   ReDim Buffer(22050 * 1) As Integer

   ' fill in the Wave Format
   With WaveFmt
      .wFormatTag = 1    ' PCM

      .nChannels = 1
      .nSamplesPerSec = 22050
      .wBitsPerSample = 16

      .nBlockAlign = .wBitsPerSample * .nChannels / 8
      .nAvgBytesPerSec = .nBlockAlign * .nSamplesPerSec
   End With

   ' Create the wave tile
   FileNum = FreeFile
   Open "C:\My Wave File.WAV" For Binary Access Write Lock _
      Write As FileNum

   WaveWriteHeader FileNum, WaveFmt    ' write the wave headers

   Put FileNum, , Buffer    ' write 1 second
   Put FileNum, , Buffer    ' write another second
                            ' (to simulate streaming)

   ' in this case we will have a wave file with two seconds
   ' of silence

   ' complete the header values (file length and chunk length)
    WaveWriteHeaderEnd FileNum

   ' close the file
   Close FileNum
End Sub

' Sample of how to read a wave file

Private Sub Read_Wave_File_Example()
   Dim WaveFmt As WaveFormat
   Dim FileNum As Integer
   Dim DataLength As Long
   Dim Buffer() As Integer

   ' open the wave file
   FileNum = FreeFile
   Open "C:\My Wave File.WAV" For Binary Access Read Lock _
      Write As FileNum

   ' read the header, and get the chunk size (data length)
   WaveFmt = WaveReadFormat(FileNum, DataLength)

   ' resize our buffer to hold the entire wave data
   ' (DataLength is in Bytes)
   ReDim Buffer((DataLength \ 2) - 1)

   ' read the wave data from the file into our buffer
   Get FileNum, , Buffer

   Close FileNum    ' close the file
End Sub

Before you start using DirectXSound, guess what you have to do first? Yup, you guessed it, you have to add a reference to DirectX in your project. So, go to the "Project" menu, and click on "References". You will see a screen like this:

[Sound2.jpg]

Make sure your latest DirectX is selected. Everyone should have DirectX 8 by now.

Sound and DirectXSound Tutorial

Here is the simplest and shortest possible way to play a wave file in DirectXSound:

Private DX As New DirectX8
Private DSEnum As DirectSoundEnum8
Private DIS As DirectSound8

Private DSSecBuffer As DirectSoundSecondaryBuffer8

Private Sub Form_Load()
   Dim BuffDesc As DSBUFFERDESC
   ' get enumeration object
   Set DSEnum = DX.GetDSEnum

   ' select the first sound device, and create the Direct Sound
   ' object
   Set DIS = DX.DirectSoundCreate(DSEnum.GetGuid(1))

   ' Set the Cooperative Level to normal
   DIS.SetCooperativeLevel Me.hWnd, DSSCL_NORMAL

   ' load the wave file, and create the buffer for it
   Set DSSecBuffer = DIS.CreateSoundBufferFromFile _
      ("C:\WINDOWS\Media\tada.wav", BuffDesc)

   DSSecBuffer.Play DSBPLAY_DEFAULT
End Sub

In DirectX 7, you do it the same, except you also have to give a structure of WAVEFORMATEX to CreateSoundBufferFromFile.

Private DX As New DirectX7
Private DSEnum As DirectSoundEnum
Private DIS As DirectSound

Private DSSecBuffer As DirectSoundBuffer

Private Sub Form_Load()
   Dim BuffDesc As DSBUFFERDESC, WaveFormat As WAVEFORMATEX

   ' get enumeration object
   Set DSEnum = DX.GetDSEnum

   ' select the first sound device, and create the Direct Sound
   ' object
   Set DIS = DX.DirectSoundCreate(DSEnum.GetGuid(1))

   ' Set the Cooperative Level to normal
   DIS.SetCooperativeLevel Me.hWnd, DSSCL_NORMAL

   ' load the wave file, and create the buffer for it
   Set DSSecBuffer = DIS.CreateSoundBufferFromFile _
      ("C:\WINDOWS\Media\tada.wav", BuffDesc, WaveFormat)

   DSSecBuffer.Play DSBPLAY_DEFAULT
End Sub

Note that the buffer was declared in the global section. If you put it in the Form_Load, the sound buffer will be destroyed when it goes out of scope. In other words, you should not create the buffer in the same function where you play it; preferably, it should be created one level below the play statement. OR... make a loop right after the play statement, that will loop continuously while the buffer is playing, and stop when the buffer is done, but this is not recommended.

The buffer description structure that you pass to CreateSoundBufferFromFile function will be filled with the sound format while it loads the file. In your case, you will not use the resulting structure, but it has to be passed to the function. You can use this structure if you want to do more advanced things where you need to know the sound format of the file.

Note: This will be the last time I will refer to DirectX 7. The tutorial will get way to big if I have to post DirectX 7 code also. If you understand how DirectX 8 works, it is easy to convert to DirectX 7. It is the same theory for both, there are only small diferences between them. And on top of that, everyone should be using DirectX 8 by now, it's been out for a long time.

So, back to the code.

You are probably wondering, "How can I change the volume?" or "How can I make it play faster or slower?"

Well, easy... all you have to do is tell the buffer what you are planning to do, and then do it. Here's an example:

Private Sub Form_Load()
   Dim BuffDesc As DSBUFFERDESC
   ' get enumeration object
   Set DSEnum = DX.GetDSEnum

   ' select the first sound device, and create the Direct Sound
   ' object
   Set DIS = DX.DirectSoundCreate(DSEnum.GetGuid(1))

   ' Set the Cooperative Level to normal
   DIS.SetCooperativeLevel Me.hWnd, DSSCL_NORMAL

   ' allow frequency changes and volume changes
   BuffDesc.lFlags = DSBCAPS_CTRLFREQUENCY Or DSBCAPS_CTRLVOLUME

   ' load the wave file and create the buffer for it
   Set DSSecBuffer = DIS.CreateSoundBufferFromFile _
      ("C:\WINDOWS\Media\tada.wav", BuffDesc)

   ' frequency can range from 100 Hz to ~ 100,000
   ' (depending on your sound card)
   DSSecBuffer.SetFrequency 22050 * 1.8

   ' volume is from 0 to -10,000 (where 0 is the loudest,
   ' and -10,000 is silence)
   DSSecBuffer.SetVolume -1500

   DSSecBuffer.Play DSBPLAY_DEFAULT
End Sub

So, first modify BuffDesc.lFlags to the constant for frequency or sound volume (or both as in the example), then call the appropriate functions to set the values after you load the file, and before you play the sound.

How to play more than one sound at the same time:

Dim DX As New DirectX8
Dim DSEnum As DirectSoundEnum8
Dim DIS As DirectSound8

Dim DSSecBuffer As DirectSoundSecondaryBuffer8
Dim DSSecBuffer2 As DirectSoundSecondaryBuffer8

Private Sub Form_Load()
   Dim BuffDesc As DSBUFFERDESC

   ' get enumeration object
   Set DSEnum = DX.GetDSEnum

   ' select the first sound device, and create the Direct Sound
   ' object
   Set DIS = DX.DirectSoundCreate(DSEnum.GetGuid(1))

   ' Set the Cooperative Level to normal
   DIS.SetCooperativeLevel Me.hWnd, DSSCL_NORMAL

   ' allow frequency changes and volume changes
   BuffDesc.lFlags = DSBCAPS_CTRLFREQUENCY Or DSBCAPS_CTRLVOLUME

   ' load the wave file and create the buffer for it
   Set DSSecBuffer = DIS.CreateSoundBufferFromFile _
      ("C:\WINDOWS\Media\tada.wav", BuffDesc)
   Set DSSecBuffer2 = DIS.CreateSoundBufferFromFile _
      ("C:\WINDOWS\Media\notify.wav", BuffDesc)

   ' volume is from 0 to -10,000 (where 0 is the loudest,
   ' and -10,000 is silence)
   DSSecBuffer.SetVolume -500
   DSSecBuffer.Play DSBPLAY_DEFAULT

   DSSecBuffer2.SetFrequency 22050 * 0.7
   DSSecBuffer2.Play DSBPLAY_DEFAULT
End Sub

I used the same buffer description on both buffers because, in this case, you don't need to know the sound format of the sounds, and I passed the structure to the CreateSoundBufferFromFile function just to make DirectSound happy, because the function was designed to take two parameters.

Sound and DirectXSound Tutorial

Just keep in mind that, when using CreateSoundBufferFromFile, Direct Sound will load the entire wave file into memory, and play it from there. It is NOT recommended to load large files this way because it will use a lot of memory. I will talk about loading large wave files later in the tutorial.

Now, if you are wondering what CreateSoundBufferFromFile is really doing, it's this:

Private DX As New DirectX8
Private DSEnum As DirectSoundEnum8
Private DIS As DirectSound8

Private DSSecBuffer As DirectSoundSecondaryBuffer8

Private Sub Form_Load()
   Dim BuffDesc As DSBUFFERDESC

   ' get enumeration object
   Set DSEnum = DX.GetDSEnum

   ' select the first sound device, and create the Direct Sound
   ' object
   Set DIS = DX.DirectSoundCreate(DSEnum.GetGuid(1))

   ' Set the Cooperative Level to normal
   DIS.SetCooperativeLevel Me.hWnd, DSSCL_NORMAL

   ' load the wave file, and create the buffer for it
   Set DSSecBuffer = CreateSoundBufferFromFile _
      ("C:\WINDOWS\Media\tada.wav", BuffDesc)

   DSSecBuffer.Play DSBPLAY_DEFAULT
End Sub

Private Function CreateSoundBufferFromFile _
   (ByVal FileName As String, ByRef BuffDesc As DSBUFFERDESC) _
   As DirectSoundSecondaryBuffer8
   Dim SecBuff As DirectSoundSecondaryBuffer8
   Dim WaveFmt As WaveFormat
   Dim FileNum As Integer, DataLength As Long
   Dim WaveData() As Byte

   FileNum = FreeFile
   ' open file
   WaveFmt = WaveReadFormat(FileNum, DataLength)    ' read format

   ' copy the wave format values into the WAVEFORMATEX of the
   ' DSBUFFERDESC structure
   With BuffDesc.fxFormat
      .nFormatTag = WaveFmt.wFormatTag

      .nChannels = WaveFmt.nChannels
      .nBitsPerSample = WaveFmt.wBitsPerSample
      .lSamplesPerSec = WaveFmt.nSamplesPerSec

      .nBlockAlign = WaveFmt.nBlockAlign
      .lAvgBytesPerSec = WaveFmt.nAvgBytesPerSec
   End With

   ' the next line is not really needed
   ' DSBCAPS_STICKYFOCUS means it will keep playing even if our
   ' application does not have focus
   ' DSBCAPS_STATIC simply means that the buffer is static;
   ' otherwise, DirectX Sound will think that this is a streaming
   ' buffer
   BuffDesc.lFlags = DSBCAPS_STICKYFOCUS Or DSBCAPS_STATIC

   ' tell the buffer how long it should be; in this case, we are
   ' loading the entire wave file
   BuffDesc.lBufferBytes = DataLength

   ' create the buffer
   Set SecBuff = DIS.CreateSoundBuffer(BuffDesc)

   ' resize our array to fit the whole sound file
   ReDim WaveData(DataLength - 1)

   ' read the wave data
   Get FileNum, , WaveData

   ' copy our array to the DirectX Sound buffer
   SecBuff.WriteBuffer 0, DataLength, WaveData(0), DSBLOCK_DEFAULT

   Close FileNum    ' close file

   ' return the DirectX Sound Buffer
   Set CreateSoundBufferFromFile = SecBuff
End Function

Yes, that's right... with everything that you learned until now, you can re-create the CreateSoundBufferFromFile function of the DirectX Sound. Easy, isn't it?

How to Record/Play with DirectXSound Using a Split Buffer for Very Long Sounds

If you want to play or record for a long time, instead of using a very big buffer that holds all the data, you must use a small buffer where DirectSound has to write the sound, and you to read it from there. Especially if you use streaming, if you record for minutes or hours, there is not enough memory to hold that much data. This is best done with a split buffer.

When DirectSound writes to the buffer and you read the buffer at the same time DirectSound writes, the sound won't be clear.

Here is an example of a single buffer:

Because DirectSound writes continuously to the same portion of the buffer, you won't get a chance to read the data.

To read the data properly, you have to divide the buffer in TWO, and read the section where DirectSound is not writing.

So, when Direct Sound is writing on the left side of the buffer, you have to read the right side of the buffer, and vice versa. This means that if you have a buffer of 1 second (for example), you have to read from the buffer only 500 Ms of sound at a time, because it's divided in 2. Also, reading the sound this way gives you plenty of time to process the sound, convert to other formats, and save it.

When you play sound, you still have to use a split buffer, but the reading/writing will be the opposite; you have to write to the buffer in the half of the buffer that DirectSound does not read.

Sound and DirectXSound Tutorial

Recording

Initializing DirectSound for recording:

  1. Choose the sound device to record from.
  2. Fill in the buffer description structure with the format that you will use to record.
  3. Calculate the buffer size, and half buffer size (buffer size / 2), and make sure that the buffer is aligned.
  4. Making sure the buffer is aligned is very important because if you read data and the buffer is not aligned the sound won't be clear (very distorted). Aligning the buffer simply means that is it divisible by the format Block Align number, or simply, make sure it's aligned by 4 (because 4 is the largest block align number).
  5. Create the DirectSound buffer.
  6. Lastly, create the events (Notification Positions) for the buffer.

Here is the code to initialize (and UnInitialize) the Direct Sound for recording:

' Direct Sound objects
Private DX As New DirectX8
Private SEnum As DirectSoundEnum8
Private DISCap As DirectSoundCapture8

' buffer, and buffer description
Private Buff As DirectSoundCaptureBuffer8
Private BuffDesc As DSCBUFFERDESC

' For the events
Private EventsNotify() As DSBPOSITIONNOTIFY
Private EndEvent As Long, MidEvent As Long, StartEvent As Long

' to know the buffer size
Private BuffLen As Long, HalfBuffLen As Long

Public Function Initialize(Optional ByVal SamplesPerSec _
                              As Long = 44100, _
                           Optional ByVal BitsPerSample _
                              As Integer = 16, _
                           Optional ByVal Channels _
                              As Integer = 2, _
                           Optional ByVal HalfBufferLen _
                              As Long = 0, _
                           Optional ByVal GUID As String = "") _
                              As String

   ' if there is any error, go to ReturnError
   On Error GoTo ReturnError

   ' get the device enumeration object
   Set SEnum = DX.GetDSCaptureEnum

   ' if GUID is empty, assign the first sound device
   If Len(GUID) = 0 Then GUID = SEnum.GetGuid(1)

   ' choose the sound device, and create the Direct Sound object
   Set DISCap = DX.DirectSoundCaptureCreate(GUID)

   ' set the format to use for recording
   With BuffDesc.fxFormat
      .nFormatTag = WAVE_FORMAT_PCM
      .nChannels = Channels
      .nBitsPerSample = BitsPerSample
      .lSamplesPerSec = SamplesPerSec

      .nBlockAlign = (nBitsPerSample * .nChannels) \ 8
      .lAvgBytesPerSec = .lSamplesPerSec * .nBlockAlign

      If HalfBufferLen <= 0 Then
         ' make half of the buffer to be 100 ms
         HalfBuffLen = .lAvgBytesPerSec / 10
      Else
         ' using a "custom" size buffer
         HalfBuffLen = HalfBufferLen
      End If

      ' make sure the buffer is aligned
      HalfBuffLen = HalfBuffLen - (HalfBuffLen Mod .nBlockAlign)
   End With

   ' calculate the total size of the buffer
   BuffLen = HalfBuffLen * 2

   BuffDesc.lBufferBytes = BuffLen
   BuffDesc.lFlags = DSCBCAPS_DEFAULT

   ' create the buffer object
   Set Buff = DISCap.CreateCaptureBuffer(BuffDesc)

   ' Create three event notifications
   ReDim EventsNotify(0 To 2) As DSBPOSITIONNOTIFY

   ' create event to signal that the DirectSound write cursor
   ' is at the beginning of the buffer
   StartEvent = DX.CreateEvent(Me)
   EventsNotify(0).hEventNotify = StartEvent
   EventsNotify(0).lOffset = 1

   ' create event to signal that the DirectSound write cursor
   ' is at half of the buffer
   MidEvent = DX.CreateEvent(Me)
   EventsNotify(1).hEventNotify = MidEvent
   EventsNotify(1).lOffset = HalfBuffLen

   ' create the event to signal the sound has stopped
   EndEvent = DX.CreateEvent(Me)
   EventsNotify(2).hEventNotify = EndEvent
   EventsNotify(2).lOffset = DSBPN_OFFSETSTOP

   ' Assign the notification points to the buffer
   Buff.SetNotificationPositions 3, EventsNotify()

   Initialize = ""
   Exit Function
ReturnError:

   ' return error number, description, and source
   Initialize = "Error: " & Err.Number & vbNewLine & _
      "Desription: " & Err.Description & vbNewLine & _
      "Source: " & Err.Source

   Err.Clear
   UninitializeSound
   Exit Function
End Function

Public Sub UninitializeSound()
   ' destroy all events
   DX.DestroyEvent EventsNotify(0).hEventNotify
   DX.DestroyEvent EventsNotify(1).hEventNotify
   DX.DestroyEvent EventsNotify(2).hEventNotify

   Erase EventsNotify

   Set Buff = Nothing
   Set DISCap = Nothing
   Set SEnum = Nothing
End Sub

Now, you have to read the data from the DirectSound buffer. This is done in the events that come from DirectSound. To receive the events, you have to implement the DirectXEvent8 interface. Once implemented, you will receive the events through the DirectXEvent8_DXCallback event.

This is how it's done:

Implements DirectXEvent8

Private Sub DirectXEvent8_DXCallback(ByVal eventid As Long)
   Dim WaveBuffer() As Byte

   ' make sure that Buff object is actually initialized to
   ' a buffer instance
   If Not (Buff Is Nothing) Then
      ReDim WaveBuffer(HalfBuffLen - 1)

      Select Case eventid
      Case StartEvent
         ' we got the event that the write cursor is at the
         ' beginning of the buffer; therefore, read from the
         ' middle of the buffer to the end
          Buff.ReadBuffer HalfBuffLen, HalfBuffLen, _
            WaveBuffer(0), DSCBLOCK_DEFAULT
      Case MidEvent
         ' we got an event that the write cursor is at the middle
         ' of the buffer; therefore, read from the beginning of
         ' the buffer to the middle
          Buff.ReadBuffer 0, HalfBuffLen, WaveBuffer(0), _
            DSCBLOCK_DEFAULT
      Case EndEvent
         ' not used right now
      End Select

      ' use the wave buffer here
   End If
End Sub

Now, there are two more things to make it complete:

  1. Functions to Start/Stop the actual recording.
  2. Use the wave data in a way or another...

So, applying everything that you learned until now, I've made a project that records the audio from the main sound device, and saves the wave data into a wave file. Please see the Split Buffer Record project.

You will see that there are two forms. I placed all the DirectSound initialization, buffer event, and start and stop recording in one form. This way, I can use that form in multiple projects (the same as you use a module file).

When DirectSound raises the event to read from its buffer, a byte array buffer is created and the wave data is read into it. I placed an event in the DirectSound form. The event is used to transfer the wave data to the parent form. The parent form creates a wave file, and saves all the wave data that comes from the other form (DirectSound form) into that file.

Sound and DirectXSound Tutorial

Playing

When playing sound, initializing the sound is somewhat similar, but reading/writing to the buffer is exactly the opposite as recording. When playing, you have to write to the buffer in the half that DirectSound is not reading, and vice versa.

Because playing sound is so similar to recording, I'm not going to explain in detail how it's done. Just open the Split Buffer Play project, and read the comments in the project to understand how playing sound works:

I made this project similar to the recording one. I've put the DirectSound in a separate form, and the main form opens the wave file, and sends the wave data to DirectSound form.

This is very easily done because you simply display the wave data "as is." The only problem is that you have to write separate code for 8- and 16-bit sound formats, and also for mono and stereo.

In the following code, you will see three functions:

  • DisplayWaveData8 is for displaying 8- bit data, stereo and mono.
  • DisplayWaveData16_8 is for displaying 16-bit data, but the data is recorded in byte arrays (instead of integer arrays). Therefore, the function simply copies the array to an integer array.
  • DisplayWaveData16 is for displaying 16-bit data, stereo and mono.
Private Sub DisplayWaveData8(DataBuff() As Byte, _
   Pic As PictureBox, Stereo As Boolean)
   Dim Stp As Single, HBuffer As Long, Q As Single
   Dim LX As Single, LY As Single, RX As Single, RY As Single
   Dim LVal As Single, RVal As , K As Long


   If Not Stereo Then
      HBuffer = UBound(DataBuff)
      Stp = HBuffer / (Pic.Width / 15)

      Pic.Scale (0, 127)-(HBuffer, -127)
      Pic.PSet (0, 0)

      Pic.Cls
      For Q = 0 To HBuffer - 2 Step Stp
         Pic.Line -(Fix(Q), DataBuff(Fix(Q)) - 127)
      Next Q
   Else
      HBuffer = UBound(DataBuff) \ 2
      Stp = HBuffer / (Pic.Width / 15)

      Pic.Scale (0, 256)-(HBuffer, -256)
      Pic.PSet (0, 0)

      Pic.Cls
      LX = 0
      LY = -127
      RX = 0
      RY = 127

      For Q = 0 To HBuffer - 2 Step Stp
         K = Q
         K = K - (K Mod 2)

         LVal = DataBuff(K + 1) - 255
         RVal = DataBuff(K)

         Pic.Line (LX, LY)-(K, LVal)
         Pic.Line (RX, RY)-(K, RVal)

         LX = K
         LY = LVal

         RX = K
         RY = RVal
      Next Q
   End If
End Sub

' the sound is 16-bit, but it comes in Bytes, not Integer
Private Sub DisplayWaveData16_8(DataBuff() As Byte, _
   Pic As PictureBox, Stereo As Boolean)
   Dim Buff() As Integer

   ReDim Buff(UBound(DataBuff) \ 2 - 1)

   CopyMemory Buff(0), DataBuff(0), UBound(DataBuff) + 1

   DisplayWaveData16 Buff, Pic, Stereo
End Sub

Private Sub DisplayWaveData16(DataBuff() As Integer, _
   Pic As PictureBox, Stereo As Boolean)
   Dim Stp As Single, HBuffer As Long, Q As Single
   Dim LX As Single, LY As Single, RX As Single, RY As Single
   Dim LVal As Single, RVal As Single, K As Long

   If Not Stereo Then
      HBuffer = UBound(DataBuff)
      Stp = HBuffer / (Pic.Width / 15)

      Pic.Scale (0, 0.5)-(HBuffer, -0 5)
      Pic.PSet (0, 0)

      Pic.Cls
      For Q = 0 To HBuffer - 2 Step Stp
         Pic.Line -(Fix(Q), DataBuff(Fix(Q)) / 65536#)
      Next Q
   Else
      HBuffer = UBound(DataBuff) \ 2
      Stp = HBuffer / (Pic.Width / 15)

      Pic.Scale (0, 1)-(HBuffer, -1)
      Pic.PSet (0, 0)

      Pic.Cls

      LX = 0
      LY = -0.5
      RX = 0
      RY = 0.5

      For Q = 0 To HBuffer - 2 Step Stp
         K = Q
         K = K - (K Mod 2)

         LVal = DataBuff(K + 1) / 65536# - 0 5
         RVal = DataBuff(K) / 65536# + 0.5

         Pic.Line (LX, LY)-(K, LVal)
         Pic.Line (RX, RY)-(K, RVal)

         LX = K
         LY = LVal

         RX = K
         RY = RVal
      Next Q
   End If
End Sub

To see the previous functions in use, see the Recording project, and displaying the wave data.

Note: The functions are made assuming that the picture box is sitting on a form with ScaleMode = vbTwips (the default).

First things first. As in the previous code (displaying the wave data), sometimes you need to convert a 16-bit sound in a Byte array to a Integer array. This is done simply by copying the data from the Byte array to a new Integer array.

' convert a 16 bit sound from a Byte array to Integer array
Public Function Convert16_8To16(Buffer() As Byte) As Integer()
   Dim Buff() As Integer

   ReDim Buff((UBound(Buffer) + 1) / 2 - 1 + 0.1)

   CopyMemory Buff(0), Buffer(0), UBound(Buffer) + 1

   Convert16_8To16 = Buff
End Function

Converting from a 16-bit wave to an 8-bit wave is just a simple matter of division. But, because the 8-bit wave (byte) is unsigned, you have to make the 16-bit wave data unsigned first, and then divide.

' convert 16-bit to 8-bit
Public Function ConvertWave16to8(Buffer() As Integer) As Byte()
   Dim K As Long, Val As Long
   Dim RetBuff() As Byte

   ReDim RetBuff(UBound(Buffer))

   For K = 0 To UBound(Buffer)
      Val = Buffer(K)
      Val = (Val + 32768) \ 256

      RetBuff(K) = Val
   Next K

   ConvertWave16to8 = RetBuff
End Function

Converting from an 8-bit wave to a 16-bit wave is the opposite. You have to subtract to make it signed, and then multiply to fit the Integer variable.

' convert 8-bit to 16-bit
Public Function ConvertWave8to16(Buffer() As Byte) As Integer()
   Dim K As Long, Val As Long
   Dim RetBuff() As Integer

   ReDim RetBuff(UBound(Buffer))

   For K = 0 To UBound(Buffer)
      Val = (Buffer(K) - 127) * 256

       RetBuff(K) = Val
   Next K

   ConvertWave8to16 = RetBuff
End Function

When converting from Mono to Stereo, you just copy the same value to left channel and right channel.

' convert mono to stereo for a 16-bit buffer
Public Function ConvertWaveMonoToStereo16(Buffer() As Integer) _
   As Integer()
   Dim K As Long
   Dim RetBuff() As Integer

   ReDim RetBuff(UBound(Buffer) * 2)

   For K = 0 To UBound(Buffer)
      RetBuff(K * 2 + 0) = Buffer(K)
      RetBuff(K * 2 + 1) = Buffer(K)
   Next K

   ConvertWaveMonoToStereo16 = RetBuff
End Function

' convert mono to stereo for 8-bit buffer
Public Function ConvertWaveMonoToStereo8(Buffer() As Byte) _
   As Byte()
   Dim K As Long
   Dim RetBuff() As Byte

   ReDim RetBuff(UBound(Buffer) * 2)

   For K = 0 To UBound(Buffer)
      RetBuff(K * 2 + 0) = Buffer(K)
      RetBuff(K * 2 + 1) = Buffer(K)
   Next K

   ConvertWaveMonoToStereo8 = RetBuff
End Function

Converting from Stereo to Mono is a little tricky. You have to mix the two channels. So, you first add the two channels together, and then divide by 2 (making an average of the 2).

' convert stereo to mono for 16-bit buffer
Public Function ConvertWaveStereoToMono16(Buffer() As Integer) _
   As Integer()
   Dim K As Long, Val As Long
   Dim RetBuff() As Integer

   ReDim Buff((UBound(Buffer) + 1) \ 2 - 1)

   For K = 0 To UBound(RetBuff)
      Val = Buffer(K * 2)
      Val = (Val + Buffer(K * 2 + 1)) \ 2

      RetBuff(K) = Val
   Next K

   ConvertWaveStereoToMono16 = RetBuff
End Function

' convert stereo to mono for 8 bit buffer
Public Function ConvertWaveStereoToMono8(Buffer() As Byte) _
   As Byte()
   Dim K As Long, Val As Long
   Dim RetBuff() As Byte

   ReDim Buff((UBound(Buffer) + 1) \ 2 - 1)

   For K = 0 To UBound(RetBuff)
      Val = Buffer(K * 2)
      Val = (Val + Buffer(K * 2 + 1)) \ 2

      RetBuff(K) = Val
   Next K

   ConvertWaveStereoToMono8 = RetBuff
End Function

Sound and DirectXSound Tutorial

Converting Between Sample Rates

The standard sample rates are:

  • 8000
  • 11025
  • 16000
  • 22050
  • 32000
  • 44100

Now, when you look at the numbers carefully, you will notice that for some of them when you divide one by another, you get an Integer result (2 or 4), and for others you get a decimal number. For example, if you divide 16000 Hz by 8000 Hz, you get 2, or 44100 Hz by 11025 Hz you get 4, but when you divide 44100 Hz by 32000 Hz you get 1.378125 (decimal number).

These are the two groups that are divisible by one another:

  • 8000
  • 16000
  • 32000

 

  • 11025
  • 22050
  • 44100

This means that it is easy to convert between 8000 Hz and 16000 Hz and 32000 Hz, or 11025 Hz and 22050 Hz and 44100, but not as easy when you convert from 8000 Hz to 11025 Hz (for example).

The next functions are for converting by multiplication or dividing by 2 for 16 bits per sample and 8 bits per sample wave buffers. For example, if you want to convert from 11025 Hz to 22050 Hz at 16 bits per sample, you would call the ConvertWave16MultiplySamplesBy2 function.

' convert a 16 bit wave, multiply samples by 2
Public Function ConvertWave16MultiplySamplesBy2(Buffer() _
   As Integer, _
   ByVal Stereo As Boolean) As Integer()
   Dim K As Long
   Dim RetBuff() As Integer

   ReDim RetBuff(UBound(Buffer) * 2)

   If Not Stereo Then
      For K = 0 To UBound(Buffer) - 1
         RetBuff(K * 2) = Buffer(K)
         RetBuff(K * 2 + 1) = (CLng(Buffer(K)) + Buffer(K + 1)) \ 2
      Next K
   Else
      For K = 0 To UBound(Buffer) - 3 Step 2
         RetBuff(K * 2 + 0) = Buffer(K + 0)
         RetBuff(K * 2 + 2) = (CLng(Buffer(K)) + Buffer(K + 2)) \ 2

         RetBuff(K * 2 + 1) = Buffer(K + 1)
         RetBuff(K * 2 + 3) = (CLng(Buffer(K + 1)) + _
            Buffer(K + 3)) \ 2
      Next K
   End If

   ConvertWave16MultiplySamplesBy2 = RetBuff
End Function

' convert a 8 bit wave, multiply samples by 2
Public Function ConvertWave8MultiplySamplesBy2(Buffer() As Byte, _
   ByVal Stereo As Boolean) As Byte()
   Dim K As Long
   Dim RetBuff() As Byte

   ReDim RetBuff(UBound(Buffer) * 2)

   If Not Stereo Then
      For K = 0 To UBound(Buffer) - 1
         RetBuff(K * 2) = Buffer(K)
         RetBuff(K * 2 + 1) = (CLng(Buffer(K)) + Buffer(K + 1)) \ 2
      Next K
   Else
      For K = 0 To UBound(Buffer) - 3 Step 2
         RetBuff(K * 2 + 0) = Buffer(K + 0)
         RetBuff(K * 2 + 2) = (CLng(Buffer(K)) + Buffer(K + 2)) \ 2

         RetBuff(K * 2 + 1) = Buffer(K + 1)
         RetBuff(K * 2 + 3) = (CLng(Buffer(K + 1)) + _
            Buffer(K + 3)) \ 2
      Next K
   End If

   ConvertWave8MultiplySamplesBy2 = RetBuff
End Function

' convert a 16 bit wave, divide samples by 2
Public Function ConvertWave16DivideSamplesBy2(Buffer() As Integer, _
   ByVal Stereo As Boolean) As Integer()
   Dim K As Long
   Dim RetBuff() As Integer

   ReDim RetBuff((UBound(Buffer) + 1) \ 2 - 1)

   If Not Stereo Then
      For K = 0 To UBound(Buffer) Step 2
         RetBuff(K \ 2) = (CLng(Buffer(K)) + Buffer(K + 1)) \ 2
      Next K
   Else
      For K = 0 To UBound(Buffer) - 4 Step 4
         RetBuff(K \ 2 + 0) = (CLng(Buffer(K + 0)) + _
            Buffer(K + 2)) \ 2
         RetBuff(K \ 2 + 1) = (CLng(Buffer(K + 1)) + _
            Buffer(K + 3)) \ 2
      Next K
   End If

   ConvertWave16DivideSamplesBy2 = RetBuff
End Function

' convert a 8-bit wave, divide samples by 2
Public Function ConvertWave8DivideSamplesBy2(Buffer() As Byte, _
   ByVal Stereo As Boolean) As Byte()
   Dim K As Long
   Dim RetBuff() As Byte

   ReDim RetBuff((UBound(Buffer) + 1) \ 2 - 1)

   If Not Stereo Then
      For K = 0 To UBound(Buffer) Step 2
         RetBuff(K \ 2) = (CLng(Buffer(K)) + Buffer(K + 1)) \ 2
      Next K
   Else
      For K = 0 To UBound(Buffer) - 4 Step 4
         RetBuff(K \ 2 + 0) = (CLng(Buffer(K + 0)) + _
            Buffer(K + 2)) \ 2
         RetBuff(K \ 2 + 1) = (CLng(Buffer(K + 1)) + _
            Buffer(K + 3)) \ 2
      Next K
   End If
   ConvertWave8DivideSamplesBy2 = RetBuff
End Function

Converting from one sample format to another where they are not divisible by 2 (for example: 8000 to 11025), is kinda tricky. There are two ways to do it. The easy way is to convert and assign new values by the nearest index. Here is a sample code for 16 bit:

Private Function ReSample16(Buff() As Integer, FromSample As Long, _
   ToSample As Long) As Integer()
   Dim K As Long, BuffSZ As Long
   Dim Ret() As Integer, Per As Double

   BuffSZ = UBound(Buff) + 1

   ReDim Ret(Fix(BuffSZ * ToSample / FromSample + 0.5))

   For K = 0 To UBound(Ret)
      Per = K / UBound(Ret)

      Ret(K) = Buff(UBound(Buff) * Per)
   Next K

   ReSample16 = Ret
End Function

Here is an example of how it looks like when you convert by the nearest index:

[Sound5.jpg]

In the previous image, it's converting from an 8000 Hz sample rate to a 22050 Hz sample rate. The black line (with black dots) is the original sound that is at 8000 Hz, and the yellow is the converted sound at 22050 Hz.

The better way to do it is by calculating the value by using the line intersection formula, like this:

[Sound6.jpg]

By using the line intersection formula, you can find the exact value that it should be even when the destination sample position does not match the source sample position.

Here is a sample image of how it looks when you use a line intersection formula:

[Sound7.jpg]

Sound and DirectXSound Tutorial

And, here is the code to convert using the line intersection formula:

Public Function FindYForX(ByVal X As Double, ByVal X1 As Double, _
   ByVal Y1 As Double, _
   ByVal X2 As  Double, ByVal Y2 As Double) As Double

   Dim M As Double, B As Double

   M = (Y1 - Y2) / (X1 - X2)
   B = Y1 - M * X1

   FindYForX = M * X + B
End Function

Public Function ConvertWave16ReSample(Buff() As Integer, _
   ByVal FromSample As Long, ByVal ToSample As Long, _
   ByVal Stereo As Boolean) As Integer()
   Dim K As Long, Lx As Long, RX As Long
   Dim Ret() As Integer, Per As Double, NewSize As Long

   If Not Stereo Then
      NewSize = Fix((UBound(Buff) + 1) * ToSample / FromSample + 0.5)
      ReDim Ret(NewSize - 1)

      For K = 0 To UBound(Ret) - 1
         Per = K / UBound(Ret)

         Lx = Fix(UBound(Buff) * Per)

         Ret(K) = FindYForX(UBound(Buff) * Per, Lx, Buff(Lx), _
            Lx + 1, Buff(Lx + 1))
      Next K

      Ret(UBound(Ret)) = Buff(UBound(Buff))
   Else
      NewSize = Fix((UBound(Buff) + 1) * ToSample / FromSample + 0.5)
      NewSize = NewSize - (NewSize Mod 2)
      ReDim Ret(NewSize - 1)

      For K = 0 To UBound(Ret) Step 2
         Per = K / (UBound(Ret) + 2)

         ' Left channel
         Lx = Fix(UBound(Buff) * Per / 2#) * 2
         Ret(K + 0) = FindYForX(UBound(Buff) * Per, Lx, Buff(Lx), _
            Lx + 2, Buff(Lx + 2))

         ' Right channel
         RX = Lx + 1
         Ret(K + 1) = FindYForX(UBound(Buff) * Per + 1, _
            RX, Buff(RX), RX + 2, Buff(RX + 2))
      Next K

      Ret(UBound(Ret) - 1) = Buff(UBound(Buff) - 1)
      Ret(UBound(Ret)) = Buff(UBound(Buff))
   End If

   ConvertWave16ReSample = Ret
End Function

Public Function ConvertWave8ReSample(Buff() As Byte, _
   ByVal FromSample As Long, ByVal ToSample As Long, _
   ByVal Stereo As Boolean) As Byte()
   Dim K As Long, Lx As Long, RX As Long
   Dim Ret() As Byte, Per As Double, NewSize As Long

   If Not Stereo Then
      NewSize = Fix((UBound(Buff) + 1) * ToSample / FromSample + 0.5)
      ReDim Ret(NewSize - 1)

      For K = 0 To UBound(Ret) - 1
         Per = K / UBound(Ret)

         Lx = Fix(UBound(Buff) * Per)

         Ret(K) = FindYForX(UBound(Buff) * Per, Lx, Buff(Lx), _
            Lx + 1, Buff(Lx + 1))
      Next K

      Ret(UBound(Ret)) = Buff(UBound(Buff))
   Else
      NewSize = Fix((UBound(Buff) + 1) * ToSample / FromSample + 0.5)
      NewSize = NewSize - (NewSize Mod 2)
      ReDim Ret(NewSize - 1)

      For K = 0 To UBound(Ret) Step 2
         Per = K / (UBound(Ret) + 2)

         ' Left channel
         Lx = Fix(UBound(Buff) * Per / 2#) * 2
         Ret(K + 0) = FindYForX(UBound(Buff) * Per, Lx, Buff(Lx), _
            Lx + 2, Buff(Lx + 2))

         ' Right channel
         RX = Lx + 1
         Ret(K + 1) = FindYForX(UBound(Buff) * Per + 1, RX, _
            Buff(RX), RX + 2, Buff(RX + 2))
      Next K

      Ret(UBound(Ret) - 1) = Buff(UBound(Buff) - 1)
      Ret(UBound(Ret)) = Buff(UBound(Buff))
   End If

   ConvertWave8ReSample = Ret
End Function

Converting from Any Format to Any Other Format

With all the functions from this article, you can convert from any format to any other format. They work fine if the format is static, but if the format changes often, it is difficult to make a lot of if statements to choose what function to call in each case.

That's why I made things easier. The following function converts to all formats possible by using all the previous functions. The input must be a Byte Array to Integer Array regardless of the Bits Per Sample for the sound. The return array will always be Byte Array for 8-bit return sound, and Integer Array for 16-bit return sound.

Public Function ConvertWave(Buffer As Variant, _
   FromFormat As WAVEFORMATEX, ToFormat As WAVEFORMATEX) _
   As Variant
   Dim Buffer16() As Integer
   Dim Buffer8() As Byte

   Dim RetBuff16() As Integer
   Dim RetBuff8() As Byte

   If FromFormat.nBitsPerSample = 16 Then
      Select Case VarType(Buffer)
      Case (vbArray Or vbByte)
         Buffer8 = Buffer
         Buffer16 = Convert16_8To16(Buffer8)
         Erase Buffer8
      Case (vbArray Or vbInteger)
         Buffer16 = Buffer
      Case Else
         ConvertWave = vbEmpty
         Exit Function
      End Select

      If ToFormat.nBitsPerSample = 8 Then
         RetBuff8 = ConvertWave16to8(Buffer16)
         Erase Buffer16

         GoTo To8Bit    ' JUMP TO 8 BIT
      ElseIf ToFormat.nBitsPerSample = 16 Then
         RetBuff16 = Buffer16
         Erase Buffer16
      Else
         ConvertWave = vbEmpty
         Exit Function
      End If
To16Bit:

      If FromFormat.nChannels = 1 And ToFormat.nChannels = 2 Then
         RetBuff16 = ConvertWaveMonoToStereo16(RetBuff16)
      ElseIf FromFormat.nChannels = 2 And ToFormat.nChannels = 1 Then
         RetBuff16 = ConvertWaveStereoToMono16(RetBuff16)
      ElseIf FromFormat.nChannels <> ToFormat.nChannels Then
         ConvertWave = vbEmpty
         Exit Function
      End If

      If FromFormat.lSamplesPerSec <> ToFormat.lSamplesPerSec Then
         Select Case FromFormat.lSamplesPerSec / _
            ToFormat.lSamplesPerSec
         Case 0.25
            RetBuff16 = ConvertWave16MultiplySamplesBy2(RetBuff16, _
               ToFormat.nChannels = 2)
            RetBuff16 = ConvertWave16MultiplySamplesBy2(RetBuff16, _
               ToFormat.nChannels = 2)
         Case 0.5
            RetBuff16 = ConvertWave16MultiplySamplesBy2(RetBuff16, _
               ToFormat.nChannels = 2)
         Case 2
            RetBuff16 = ConvertWave16DivideSamplesBy2(RetBuff16, _
               ToFormat.nChannels = 2)
         Case 4
            RetBuff16 = ConvertWave16DivideSamplesBy2(RetBuff16, _
               ToFormat.nChannels = 2)
           RetBuff16 = ConvertWave16DivideSamplesBy2(RetBuff16, _
              ToFormat.nChannels = 2)
         Case Else
            RetBuff16 = ConvertWave16ReSample(RetBuff16, _
               FromFormat.lSamplesPerSec, ToFormat.lSamplesPerSec, _
               ToFormat.nChannels = 2)
         End Select
      End If

      ConvertWave = RetBuff16
   ElseIf FromFormat.nBitsPerSample = 8 Then
      Select Case VarType(Buffer)
      Case (vbArray Or vbByte)
         Buffer8 = Buffer
      Case (vbArray Or vbInteger)
         Buffer16 = Buffer

         ReDim Buffer8((UBound(Buffer16) + 1) * 2 - 1)
         CopyMemory Buffer8(0), Buffer(16), UBound(Buffer8) + 1

         Erase Buffer16
      Case Else
         ConvertWave = vbEmpty
         Exit Function
      End Select

      If ToFormat.nBitsPerSample = 16 Then
         RetBuff16 = ConvertWave8to16(Buffer8)
         Erase Buffer8

         GoTo To16Bit    ' JUMP TO 16 BIT
      ElseIf ToFormat.nBitsPerSample = 8 Then
         RetBuff8 = Buffer8
         Erase Buffer8
      Else
         ConvertWave = vbEmpty
         Exit Function
      End If
To8Bit:

      If FromFormat.nChannels = 1 And ToFormat.nChannels = 2 Then
         RetBuff8 = ConvertWaveMonoToStereo8(RetBuff8)
      ElseIf FromFormat.nChannels = 2 And ToFormat.nChannels = 1 Then
         RetBuff8 = ConvertWaveStereoToMono8(RetBuff8)
      ElseIf FromFormat.nChannels <> ToFormat.nChannels Then
         ConvertWave = vbEmpty
         Exit Function
      End If

      If FromFormat.lSamplesPerSec <> ToFormat.lSamplesPerSec Then
         Select Case FromFormat.lSamplesPerSec / _
            ToFormat.lSamplesPerSec
         Case 0.25
            RetBuff8 = ConvertWave8MultiplySamplesBy2(RetBuff8, _
               ToFormat.nChannels = 2)
            RetBuff8 = ConvertWave8MultiplySamplesBy2(RetBuff8, _
               ToFormat.nChannels = 2)
         Case 0.5
            RetBuff8 = ConvertWave8MultiplySamplesBy2(RetBuff8, _
               ToFormat.nChannels = 2)
         Case 2
            RetBuff8 = ConvertWave8DivideSamplesBy2(RetBuff8, _
               ToFormat.nChannels = 2)
         Case 4
            RetBuff8 = ConvertWave8DivideSamplesBy2(RetBuff8, _
               ToFormat.nChannels = 2)
            RetBuff8 = ConvertWave8DivideSamplesBy2(RetBuff8, _
               ToFormat.nChannels = 2)
         Case Else
            RetBuff8 = ConvertWave8ReSample(RetBuff8, _
               FromFormat.lSamplesPerSec, ToFormat.lSamplesPerSec, _
               ToFormat.nChannels = 2)
         End Select
      End If

      ConvertWave = RetBuff8
   Else
      ConvertWave = vbEmpty
      Exit Function
   End If
End Function

Sound and DirectXSound Tutorial

Changing the Buffer Volume

When you change the volume of the sound, you change the amplitude of the waves within the sound. If the amplitude is big, the sound is loud, and if the amplitude is small, the sound is low. To change the volume you basically have to multiply the values within the array sound, to make the amplitude bigger or smaller.

When changing volume for 8-bit sound, because the value is stored in a Byte is from 0 to 255, and silence is 127, you have to first make the value signed—from -128 to 127, then multiply—and then bring it back to a range of 0 to 255.

Here is how it's done for 8 Bit Mono and Stereo sound:

' Change volume for 8-bit Mono sound
Public Sub ChangeVolume8Mono(Buffer8() As Byte, Percent As Single)
   Dim K As Long
   Dim Sample As Long

   For K = LBound(Buffer8) To UBound(Buffer8)
      Sample = ((CSng(Buffer8(K)) - 127) * Percent) + 127

      If Sample < 0 Then Sample = 0
      If Sample > 255 Then Sample = 255

      Buffer8(K) = Sample
   Next K
End Sub

' Change volume for 8-bit Stereo sound
Public Sub ChangeVolume8Stereo(Buffer8() As Byte, _
   LPercent As Single, RPercent As Single)
   Dim K As Long
   Dim LSample As Long
   Dim RSample As Long

   For K = LBound(Buffer8) To UBound(Buffer8) Step 2
      LSample = ((CSng(Buffer8(K)) - 127) * LPercent) + 127
      RSample = ((CSng(Buffer8(K + 1)) - 127) * RPercent) + 127

      If LSample < 0 Then LSample = 0
      If LSample > 255 Then LSample = 255

      If RSample < 0 Then RSample = 0
      If RSample > 255 Then RSample = 255

      Buffer8(K) = LSample
      Buffer8(K + 1) = RSample
   Next K
End Sub

For 16-bit sound, it's even easier because the sound is already signed. All you have to do is multiply its value. Here's how it's done for 16-bit Mono and Stereo sound:

' Change volume for 16 Bit Mono sound
Public Sub ChangeVolume16Mono(Buffer16() As Integer, _
   Percent As Single)
   Dim K As Long
   Dim Sample As Long

   For K = LBound(Buffer16) To UBound(Buffer16)
      Sample = Buffer16(K) * Percent

      If Sample < -32768 Then Sample = -32768
      If Sample > 32767 Then Sample = 32767

      Buffer16(K) = Sample
   Next K
End Sub

' Change volume for 16 Bit Stereo sound
Public Sub ChangeVolume16Stereo(Buffer16() As Integer, _
   LPercent As Single, RPercent As Single)
   Dim K As Long
   Dim LSample As Long
   Dim RSample As Long

   For K = LBound(Buffer16) To UBound(Buffer16) Step 2
      LSample = Buffer16(K) * LPercent
      RSample = Buffer16(K + 1) * RPercent

      If LSample < -32768 Then LSample = -32768
      If LSample >  32767 Then LSample = 32767

      If RSample < -32768 Then RSample = -32768
      If RSample >  32767 Then RSample =  32767

      Buffer16(K) = LSample
      Buffer16(K + 1) = RSample
   Next K
End Sub

Splitting a Stereo Buffer into Two Mono Buffers

In a Stereo buffer, the left channel is stored first, and then the right channel. So, at position 1, you will find the sample of the left channel, position 2 is right channel, position 3 is left channel again, and so on.

Here is code to split a Stereo buffer to 2 Mono buffers, for 8- and 16-bit:

Public Sub SoundStereoToMono16(InStereo() As Integer, _
   OutLeftChannel() As Integer, OutRightChannel() As Integer)
   Dim InPos As Long
   Dim OutPos As Long

   ReDim OutLeftChannel((UBound(InStereo) + 1) \ 2 - 1)
   ReDim OutRightChannel(UBound(OutLeftChannel))

   For InPos = 0 To UBound(InStereo) Step 2
      OutLeftChannel(OutPos) = InStereo(InPos)
      OutRightChannel(OutPos) = InStereo(InPos + 1)

      OutPos = OutPos + 1
   Next InPos
End Sub

Public Sub SoundStereoToMono8(InStereo() As Byte, _
   OutLeftChannel() As Byte, OutRightChannel() As Byte)
   Dim InPos As Long
   Dim OutPos As Long

   ReDim OutLeftChannel((UBound(InStereo) + 1) \ 2 - 1)
   ReDim OutRightChannel(UBound(OutLeftChannel))

   For InPos = 0 To UBound(InStereo) Step 2
      OutLeftChannel(OutPos)  = InStereo(InPos)
      OutRightChannel(OutPos) = InStereo(InPos + 1)

      OutPos = OutPos + 1
   Next InPos
End Sub

Mixing sound is actually very easy. You simply add the value from the first buffer to the value from the second buffer (and so on...). When mixing sounds, because you add two (or more) sounds into one, the volume might get too loud; therefore, I also added a parameter to the function to lower the volume.

I've provided two functions, one mix two sounds, and another to mix three sounds. As you can, see the only difference is where it's adding the buffers together, so it's easy to make a function to mix four sounds (and more), because all you do is to add all of them together.

The last parameter is the "Divisor" (Div). If you mix two sounds, you might want to put the divisor to 1/2 (0.5), or if you mix three sounds, 1/3 (0.33), and so on. I put the Div value equal to 1, because this is optional.

Public Function WaveMIX2(Buffer1() As Integer, _
   Buffer2() As Integer, Optional Div As Single = 1) As Integer()
   Dim K As Long, Sample As Single
   Dim Ret() As Integer

   ReDim Ret(UBound(Buffer1))

   For K = 0 To UBound(Buffer1)
      Sample = (CSng(Buffer1(K)) + Buffer2(K)) * Div

      If Sample < -32768 Then Sample = -32768
      If Sample >  32767 Then Sample =  32767

      Ret(K) = Sample
   Next K

   WaveMIX2 = Ret
End Function

Public Function WaveMIX3(Buffer1() As Integer, _
   Buffer2() As Integer, Buffer3() As Integer, _
   Optional Div As Single = 1) As Integer()
   Dim K As Long, Sample As Single
   Dim Ret() As Integer

   ReDim Ret(UBound(Buffer1))

   For K = 0 To UBound(Buffer1)
      Sample = (CSng(Buffer1(K)) + Buffer2(K) + Buffer3(K)) * Div

      If Sample < -32768 Then Sample = -32768
      If Sample >  32767 Then Sample =  32767

      Ret(K) = Sample
   Next K

   WaveMIX3 = Ret
End Function


Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Today's competitive marketplace requires the organization to frequently release and deploy applications at the pace of user demands, with reduced cost, risk, and increased quality. This book defines the basics of application release and deployment, and provides best practices for implementation with resources for a deeper dive. Inside you will find: The business and technical drivers behind automated application release and deployment. Evaluation guides for application release and deployment solutions. …

  • With JRebel, developers get to see their code changes immediately, fine-tune their code with incremental changes, debug, explore and deploy their code with ease (both locally and remotely), and ultimately spend more time coding instead of waiting for the dreaded application redeploy to finish. Every time a developer tests a code change it takes minutes to build and deploy the application. JRebel keeps the app server running at all times, so testing is instantaneous and interactive.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds