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 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, etc.)
How to split a stereo sound to 2 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 a 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 and 16 bit. 8 bit is one byte, therefore the sound is stored in arrays of Byte type, and 16 bit waves (2 bytes) are store in Integer array type. Though you can also store a 16 bit wave in a Byte array also (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 like Byte and Integer, corresponding to 8 bit and 16 bit waves. Another reason why you should store the sound in Byte and Integer data type is that it is easier to manipulate the sound (i.e. change the volume, etc.) 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 it really fast, as in thousands of times per second. Usually sample rates range from 8,000 Hz to 44,100 Hz, though I’ve seen sample rates from 4,000 Hz to 110,000 Hz. When the sound is mono, it reads 1 number per sample, when the sample is stereo, then it reads 2 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, then the block align should be 1 byte, if your sound is 16 bits, and stereo then 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, then it means that the sound has 1000 impulses per second. Impulse: An impulse looks like a sine (or cosine) when represented graphically, it looks like this:
An impulse consists of un upper "bump" and a lower "bump".
Last edited by CVMichael; Dec 23rd, 2008 at 09:29 AM.
A wave file has a header, that is composed of 3 individual small headers, and the wave data (raw data).
These are the headers:
VB Code:
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 it is in the code above.
First is the File Header, witch tells the program reading the file that this file IS a wave. You have the total file size, wave format identifier, the format identifier, 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, then write them in the file one by one, like here:
VB Code:
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 yet.
Right after you write the header information (having the 2 variables 0), you have to write the wave data, and when you are done, then you will know the exact length of the file, and length of the wave data.
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.
VB Code:
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
Put #OutFileNum, Len(header) + Len(HdrFormat) + 5, Lng ' write the ChunkHeader.lLen value
End Sub
And the function to read from a wave file:
VB Code:
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
If header.lRiff <> &H46464952 Then Exit Function ' Check for "RIFF" tag and exit if not found.
If header.lWave <> &H45564157 Then Exit Function ' Check for "WAVE" tag and exit if not found.
If header.lFormat <> &H20746D66 Then Exit Function ' Check for "fmt " tag and exit if not found.
' 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
End Function
I will attach a project that shows the previous 3 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 on how it's done:
VB Code:
' 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 2 seconds of silence
' complete the header values (file length, and chunk length)
WaveWriteHeaderEnd FileNum
' close the file
Close FileNum
End Sub
' Sample 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
Last edited by CVMichael; Feb 24th, 2006 at 10:49 PM.
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:
Make sure your latest DirectX is selected. Everyone should have DirectX 8 by now.
Here is the simplest and shortest possible way to play a wave file in DirectXSound:
VB Code:
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 have to also give a structure of WAVEFORMATEX to CreateSoundBufferFromFile.
VB Code:
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. That is because if you put it in the Form_Load, the sound buffer will be destroyed when it will go 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 our case we 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 differences 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:
VB Code:
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 lowdest, and -10,000 is silence)
DSSecBuffer.SetVolume -1500
DSSecBuffer.Play DSBPLAY_DEFAULT
End Sub
So, first modify the 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 ?
VB Code:
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 lowdest, 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, this is because in this case we 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, since the function was designed to take 2 parameters.
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 as 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 is CreateSoundBufferFromFile really doing ?
Well, this:
VB Code:
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 FileName For Binary Access Read Lock Write As FileNum ' 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 streamming 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
Yes, that's right... with everything that we learned until now, we can re-create the CreateSoundBufferFromFile function of the DirectX Sound, easy isn't it ?
Last edited by CVMichael; Dec 23rd, 2008 at 09:31 AM.
How to record/play with DirectXSound using 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. 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.
What happens is 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:
Since DirectSound writes continuously to the same portion of the buffer, you won't get a chance to read the data.
In order to read the data properly, you have to divide the buffer in 2, 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, since 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.
Recording:
Initializing DirectSound for Recording 1) Choose the sound device to record from. 2) Fill in the buffer description structure 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.
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 (since 4 is the largest block align number). 4) Create the DirectSound buffer. 5) Lastly, create the events (Notification Positions) for the buffer.
Here is the code to initialize (and UnInitialize) the Direct Sound for recording:
VB Code:
' 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
Set SEnum = DX.GetDSCaptureEnum ' get the device enumeration object
' if GUID is empty, then 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 3 event notifications
ReDim EventsNotify(0 To 2) As DSBPOSITIONNOTIFY
' create event to signal that 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 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
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:
VB Code:
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
Now there are 2 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 we 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.
You will see that there are 2 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 (same as you use a module file).
When DirectSound raises the event to read from it's 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.
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 following project, and read the comments in the project to understand how playing sound works: DirectSound, Split Buffer Play
This project I made it 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.
Last edited by CVMichael; Dec 23rd, 2008 at 09:33 AM.
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 3 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.
VB Code:
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 Single, 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)
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 by simply copying the data from the Byte array to a new Integer array.
VB Code:
' 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 16 bit wave to 8 bit wave is just a simple matter of division.
But since the 8 bit wave (byte) is unsigned, we have to make the 16 bit wave data unsigned first, then divide.
VB Code:
' 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 8 bit wave to 16 bit wave is the opposite.
You have to subtract to make it signed, then multiply to fit the Integer variable.
VB Code:
' 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.
VB Code:
' convert mono to stereo for 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
When converting from Stereo to Mono, is a little tricky. You have to mix the 2 channels. So you first add the 2 channels together, then divide by 2 (making an average of the 2).
VB Code:
' 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
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 some 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 2 groups that are divisible by one another:
8000
16000
32000
11025
22050
44100
This means, and 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 bit 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, then you would call the function ConvertWave16MultiplySamplesBy2.
VB Code:
' convert a 16 bit wave, multiply samples by 2
Public Function ConvertWave16MultiplySamplesBy2(Buffer() As Integer, ByVal Stereo As Boolean) As Integer()
To convert from one sample format to another where they are not divisible by 2 (for example: 8000 to 11025), it is kinda tricky.
There are 2 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:
VB Code:
Private Function ReSample16(Buff() As Integer, FromSample As Long, ToSample As Long) As Integer()
Here is an example on how it looks like when you convert by the nearest index:
In the previous image it's converting from 8000 Hz sample rate to 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:
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 on how it looks like when you use line intersection formula:
And here is the code to convert using the line intersection formula:
VB Code:
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
Converting from any format to any other format
With all the functions together from this post and previous post, 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 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.
VB Code:
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
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, since the value is stored in a Byte is from 0 to 255, and silence is 127, you have to first make the value signed ie. from -128 to 127, then multiply, and then bring it back to range 0 to 255.
Here is how it's done for 8 Bit Mono and Stereo sound:
vb Code:
' Change volume for 8 Bit Mono sound
Public Sub ChangeVolume8Mono(Buffer8() As Byte, Percent As Single)
For 16 bit sound, it's even easier because the sound is already signed, so all you have to do, is multiply it's value.
Here's how it's done for 16 Bit Mono and Stereo sound:
vb Code:
' 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
Last edited by CVMichael; Dec 23rd, 2008 at 09:20 AM.
In a Stereo buffer, the left channel is stored first, 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 Stereo buffer to 2 Mono buffers, for 8 and 16 Bit:
vb Code:
Public Sub SoundStereoToMono16(InStereo() As Integer, OutLeftChannel() As Integer, OutRightChannel() As Integer)
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, since you add 2 (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 posted 2 functions, one mix 2 sounds, and another to mix 3 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 4 sounds (and more), because all you do is to add all of them together.
The last parameter is the "Divisor" (Div), if you mix 2 sounds you might want to put the divisor to 1/2 (0.5), or if you mix 3 sounds, 1/3 (0.33), and so on. I put the Div value equal to 1, because this is optional.
vb Code:
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
Last edited by CVMichael; Dec 23rd, 2008 at 09:34 AM.