Music tracker / sample sequencer engine - optimization

Just starting out? Need help? Post your questions and find answers here.
soerenkj
User
User
Posts: 95
Joined: Mon Jun 14, 2004 10:19 pm

Music tracker / sample sequencer engine - optimization

Post by soerenkj »

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.

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()
soerenkj
User
User
Posts: 95
Joined: Mon Jun 14, 2004 10:19 pm

Post by soerenkj »

Also, does anyone have an idea of optimal settings for #BytesPerDSoundBuffer and #Notifications (the size of the DirectSound buffer and the number of notifications directsound will create each time the buffer is played back)? Right now the buffer contains around 0.1 seconds of sound and #Notifications I set completely arbitrarily..
soerenkj
User
User
Posts: 95
Joined: Mon Jun 14, 2004 10:19 pm

Post by soerenkj »

Hello again..
Has anyone tried to run my code? Any comments are welcome..
I'm sure someone here must have tried to code his own music software.. some question, for example:
Do you have good experiences using some other api than DirectSound? Have you worked with DSP on a non-float representation of a sample (so that conversions are not necessary)? etc.etc.
Post Reply