Music tracker / sample sequencer engine - optimization
Posted: Wed Aug 30, 2006 11:00 am
The code below will build a sequence of samples (a beat rhythm) and play it back, mixing the samples. To run the code you need DirectSound 8, PureBasic 4 and three drum samples (see the code).
I would like someone to review this code and give me some ideas on how I might be able to refine it or do things differently. Especially I would like an idea on how to optimize the mixing stuff - right now this code is very inefficient - when I try to mix more than 4 channels my Pentium 200 starts to sweat.. Please note that my reason for converting the samples to a float representation is that I plan to use VST effects which require that.
I would like someone to review this code and give me some ideas on how I might be able to refine it or do things differently. Especially I would like an idea on how to optimize the mixing stuff - right now this code is very inefficient - when I try to mix more than 4 channels my Pentium 200 starts to sweat.. Please note that my reason for converting the samples to a float representation is that I plan to use VST effects which require that.
Code: Select all
;-{ Sound API definitions
#DS_OK = 0
#DSSCL_NORMAL = 1
#DSBPLAY_LOOPING = $1
#DSBCAPS_LOCSOFTWARE = $8
#DSBCAPS_CTRLPOSITIONNOTIFY = $100
#DSBCAPS_GLOBALFOCUS = $8000
#DSBCAPS_GETCURRENTPOSITION2 =$10000
Structure WAVEFORMATEX
wFormatTag.w
nChannels.w
nSamplesPerSec.l
nAvgBytesPerSec.l
nBlockAlign.w
wBitsPerSample.w
cbSize.w
EndStructure
Structure DSBUFFERDESC
dwSize.l
dwFlags.l
dwBufferBytes.l
dwReserved.l
*lpwfxFormat.WAVEFORMATEX
guid3DAlgorithm.GUID
EndStructure
Structure DSBPOSITIONNOTIFY
dwOffset.l
hEventNotify.l
EndStructure
DataSection
IID_DirectSoundBuffer8:
Data.l $6825A449
Data.w $7524,$4D82
Data.b $92,$0F,$50,$E3,$6A,$B3,$AB,$1E
IID_DirectSoundNotify8:
Data.l $b0210783
Data.w $89cd, $11d0
Data.b $af,$08,$00,$a0,$c9,$25,$cd,$16
EndDataSection
;}
;-{ Program definitions
#BytesPerDSoundBuffer = 16800
#Notifications = 6
#BytesPerNotification = #BytesPerDSoundBuffer/#Notifications
#NotificationsPerTick = 3
#Samples = 4
#Sounds = 4
#Channels = 4
Structure Sample
mLeft.l ;Pointer to array of floats representing the left channel of the sample
mRight.l ;Pointer to array of floats representing the right channel of the sample
nBytesPerChannel.l ;Number of bytes in each of the arrays
EndStructure
Structure Sound
*sample.Sample ;Pointer to a Sample
nTicks.l ;Number of ticks before the Sample is to be turned off
EndStructure
Structure Event
tick.l ;Tick when the event is to occur
*sound.Sound ;Sound that is to be played when the event occurs
EndStructure
Structure Channel
*sample.Sample ;Pointer to a Sample that is played back through the Channel at a specific time
iTick.l ;iTick counts down to zero from nTicks of a Sound that points to the playing Sample - when zero sound is stopped
iPlaybackPos.l ;The play back position at a specific time (the number of bytes into the arrays of the sample)
EndStructure
;}
Procedure loadWav(filename$, *sample.Sample)
If ReadFile(0,filename$)
If ReadLong(0) <> 'FFIR'
ProcedureReturn #False
EndIf
ReadLong(0)
If ReadLong(0) <> 'EVAW'
ProcedureReturn #False
EndIf
While ReadLong(0) <> ' tmf' And Eof(0) = #False
FileSeek(0, Loc(0)-3)
Wend
ReadLong(0)
formatTag = ReadWord(0)
nChannels = ReadWord(0)
nSamplesPerSec = ReadLong(0)
nAvgBytesPerSec = ReadLong(0)
nBlockAlign = ReadWord(0)
nBitsPerSample = ReadWord(0)
If formatTag <> #WAVE_FORMAT_PCM Or nChannels <> 2 Or nSamplesPerSec <> 44100 Or nAvgBytesPerSec <> 176400 Or nBlockAlign <> 4 Or nBitsPerSample <> 16
ProcedureReturn #False
EndIf
FileSeek(0,0)
While ReadLong(0) <> 'atad' And Eof(0) = #False
FileSeek(0, Loc(0)-3)
Wend
nBytesOfWavData = ReadLong(0)
mWavData = AllocateMemory(nBytesOfWavData)
ReadData(0, mWavData, nBytesOfWavData)
*sample\nBytesPerChannel = nBytesOfWavData
*sample\mLeft = AllocateMemory(*sample\nBytesPerChannel)
*sample\mRight = AllocateMemory(*sample\nBytesPerChannel)
m = mWavData
mLeft = *sample\mLeft
mRight = *sample\mRight
For i = 0 To *sample\nBytesPerChannel/4-1
PokeF(mLeft, PeekW(m)/32768.0)
PokeF(mRight, PeekW(m+2)/32768.0)
m + 4
mLeft + 4
mRight + 4
Next
FreeMemory(mWavData)
CloseFile(0)
ProcedureReturn #True
Else
ProcedureReturn #False
EndIf
EndProcedure
;-{ Program input (a sequence)
Dim sample.Sample(#Samples-1)
If Not loadWav("snare.wav",sample(0)) Or Not loadWav("kickdrum.wav",sample(1)) Or Not loadWav("hihat.wav",sample(2))
Debug "Please put the needed samples in same directory as this code"
EndIf
Dim sound.Sound(#Sounds-1)
sound(0)\sample = sample(0)
sound(0)\nTicks = 8
sound(1)\sample = sample(1)
sound(1)\nTicks = 8
sound(2)\sample = sample(2)
sound(2)\nTicks = 8
Dim event.Event(6)
event(0)\tick = 0
event(0)\sound = sound(1)
event(1)\tick = 0
event(1)\sound = sound(2)
event(2)\tick = 5
event(2)\sound = sound(2)
event(3)\tick = 10
event(3)\sound = sound(0)
event(4)\tick = 10
event(4)\sound = sound(2)
event(5)\tick = 15
event(5)\sound = sound(2)
event(6)\tick = -1
;}
If Not OpenLibrary(0, "dsound.dll")
Debug "DirectSound could not be loaded"
EndIf
hwnd.l
hwnd = OpenWindow(0, 0, 0, 200, 200, "Sound", #PB_Window_SystemMenu|#PB_Window_ScreenCentered)
dsound.IDirectSound8
CallFunction(0,"DirectSoundCreate8", 0, @dsound, 0)
dsb.IDirectSoundBuffer
bufferFormat.WAVEFORMATEX\wFormatTag = #WAVE_FORMAT_PCM
bufferFormat\nChannels = 2
bufferFormat\nSamplesPerSec = 44100
bufferFormat\nAvgBytesPerSec = 176400
bufferFormat\nBlockAlign = 4
bufferFormat\wBitsPerSample = 16
bufferFormat\cbSize = 0
desc.DSBUFFERDESC\dwSize = SizeOf(DSBUFFERDESC)
desc\dwFlags = 1
desc\dwFlags = #DSBCAPS_LOCSOFTWARE|#DSBCAPS_CTRLPOSITIONNOTIFY|#DSBCAPS_GLOBALFOCUS|#DSBCAPS_GETCURRENTPOSITION2
desc\dwBufferBytes = #BytesPerDSoundBuffer
desc\lpwfxFormat = bufferFormat
dsound\CreateSoundBuffer(@desc, @dsb,0)
event.l
event = CreateEvent_(#Null,#False,#False,#Null)
dsb8.IDirectSoundBuffer8
dsb\QueryInterface(?IID_DirectSoundBuffer8,@dsb8)
Dim channel.Channel(#Channels)
Dim notify.DSBPOSITIONNOTIFY(#Notifications-1)
For i = 0 To #Notifications-1
notify(i)\dwOffset = #BytesPerNotification*i
notify(i)\hEventNotify = event
Next
dsb\QueryInterface(?IID_DirectSoundNotify8, @dsnotify.IDirectSoundNotify)
dsnotify\SetNotificationPositions(#Notifications, notify())
dsound\SetCooperativeLevel(hwnd, #DSSCL_NORMAL)
dsb8\Play(0, 0, #DSBPLAY_LOOPING)
For i = 0 To 125 ;126 notifications = 42 ticks
WaitForSingleObject_(event, #INFINITE)
If dsb8\Lock(offset, #BytesPerNotification, @m, @dummy1, @dummy2, @dummy3, 0) = #DS_OK
If iNotification > 1
iNotification - 1
Else ;Tick occured - update channels
For j = 0 To #Channels
If channel(j)\iTick
channel(j)\iTick - 1
EndIf
Next
While event(eventPos)\tick <> -1 And event(eventPos)\tick = tick
For j = 0 To #Channels-1
If Not channel(j)\iTick
channel(j)\sample = event(eventPos)\sound\sample
channel(j)\iTick = event(eventPos)\sound\nTicks
channel(j)\iPlaybackPos = 0
Break
EndIf
Next
eventPos + 1
Wend
tick + 1
iNotification = #NotificationsPerTick
EndIf
;TODO: apply VST effects on channels
;Mix channels
iM = m
mStop = m + #BytesPerNotification
While iM < mStop
leftSampleF.f = 0.0
rightSampleF.f = 0.0
For j = 0 To #Channels
If channel(j)\iTick
If channel(j)\iPlaybackPos >= channel(j)\sample\nBytesPerChannel ;sound ended
channel(j)\iTick = 0
Else
leftSampleF = leftSampleF + PeekF(channel(j)\sample\mLeft + channel(j)\iPlaybackPos)
rightSampleF = rightSampleF + PeekF(channel(j)\sample\mRight + channel(j)\iPlaybackPos)
channel(j)\iPlaybackPos + 4
EndIf
EndIf
Next
leftSample.l = leftSampleF * 32768.0
rightSample.l = rightSampleF * 32768.0
If leftSample > 32767
leftSample = 32767
ElseIf leftSample < -32767
leftSample = -32767
EndIf
If rightSample > 32767
rightSample = 32767
ElseIf rightSample < -32767
rightSample = -32767
EndIf
PokeW(iM, leftSample)
iM + 2
PokeW(iM, rightSample)
iM + 2
Wend
dsb8\UnLock(m,#BytesPerNotification,0,0)
offset + #BytesPerNotification
offset % #BytesPerDSoundBuffer
EndIf
Next
dsb8\Stop()
dsnotify\Release()
dsb\Release()
dsb8\Release()
dsound\Release()