Draw a waveform with Purebasic and BASS library

Just starting out? Need help? Post your questions and find answers here.
Martin Verlaan
Enthusiast
Enthusiast
Posts: 124
Joined: Sun Apr 01, 2018 11:26 am

Re: Draw a waveform with Purebasic and BASS library

Post by Martin Verlaan »

Thanks Wilbert!! I should test it some more but your updated code seems to work! You just saved me from a depression :mrgreen:
User avatar
CELTIC88
Enthusiast
Enthusiast
Posts: 154
Joined: Thu Sep 17, 2015 3:39 pm

Re: Draw a waveform with Purebasic and BASS library

Post by CELTIC88 »

:D re good code, is better than audacity for me and more fast
I added support for mono channels, after your permission @wilbert...

Code: Select all

;Coder Dr wilbert
;add support for mono channels


Structure Sample
  l.w
  r.w
EndStructure

IncludeFile "bass.pbi"


Procedure UpdateWaveImage(PBImage, *SampleData.Sample, SampleCount,Numchans)
  Protected.i Width, Height, X, VOffsetL, VOffsetR, MUL
  Protected.i SamplesPerPixel, Sample
  Protected.w MinL, MaxL, MinR, MaxR
  If *SampleData And SampleCount
    StartDrawing(ImageOutput(PBImage))
    Width = OutputWidth() : Height = OutputHeight()
    VOffsetL = Height >> Numchans : VOffsetR = VOffsetL + Height >> 1
    MUL = (VOffsetL * $19999) >> 16
    Box(0, 0, Width, Height, $ffe0e0e0)
    Line(0, VOffsetL, Width, 1, $ffa0a0a0)
    If Numchans = 2 :Line(0, VOffsetR, Width, 1, $ffa0a0a0):EndIf
    SamplesPerPixel = (SampleCount + Width - 1) / Width
    SampleSize = Numchans * 2
    If SampleCount
      MinL = *SampleData\l : MinR = *SampleData\r : MaxL = MinL : MaxR = MinR      
      While SampleCount
        If *SampleData\l < MinL : MinL = *SampleData\l : ElseIf *SampleData\l > MaxL : MaxL = *SampleData\l : EndIf
        If *SampleData\r < MinR : MinR = *SampleData\r : ElseIf *SampleData\r > MaxR : MaxR = *SampleData\r : EndIf
        Sample + 1 : SampleCount - 1
        If Sample = SamplesPerPixel Or SampleCount = 0
          MinL = (MinL * MUL) >> 16 : MaxL = (MaxL * MUL) >> 16
          MinR = (MinR * MUL) >> 16 : MaxR = (MaxR * MUL) >> 16
          LineXY(X, VOffsetL - MinL, X, VOffsetL - MaxL, $ffff8000)
          If Numchans = 2 :LineXY(X, VOffsetR - MinR, X, VOffsetR - MaxR, $ffff8000):EndIf
          MinL = *SampleData\l : MinR = *SampleData\r : MaxL = MinL : MaxR = MinR
          X + 1 : Sample = 0
        EndIf
        *SampleData + SampleSize
      Wend
    EndIf
    StopDrawing()
  EndIf  
EndProcedure


BASS_Init(-1, 44100, 0, 0, #Null)

Channel.l = BASS_StreamCreateFile(#False, @"notify.wav", 0, 0, #BASS_STREAM_PRESCAN|#BASS_STREAM_DECODE|#BASS_UNICODE)
Length.q = BASS_ChannelGetLength(Channel, #BASS_POS_BYTE)
Info.BASS_CHANNELINFO
Bass_ChannelGetInfo(Channel, @Info)    

Dim Buffer.Sample(Length >> Info\chans)
BASS_ChannelGetData(Channel, @Buffer(), Length)

OpenWindow(0, 0, 0, 620, 320, "Waveform", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)

WaveImage = CreateImage(#PB_Any, 8192, 256)
UpdateWaveImage(WaveImage, @Buffer(), Length >> Info\chans,Info\chans)

ScrollAreaGadget(0, 10, 10, 600, 300, ImageWidth(WaveImage), ImageHeight(WaveImage), 10, #PB_ScrollArea_Center)
ImageGadget(1, 0, 0, 600, 300, ImageID(WaveImage))
CloseGadgetList()

Repeat
  Event = WaitWindowEvent()
Until Event = #PB_Event_CloseWindow
interested in Cybersecurity..
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Draw a waveform with Purebasic and BASS library

Post by wilbert »

CELTIC88 wrote::D re good code, is better than audacity for me and more fast
I added support for mono channels, after your permission @wilbert...
Thanks :)
Feel free to modify or improve the code.
Windows (x64)
Raspberry Pi OS (Arm64)
Martin Verlaan
Enthusiast
Enthusiast
Posts: 124
Joined: Sun Apr 01, 2018 11:26 am

Re: Draw a waveform with Purebasic and BASS library

Post by Martin Verlaan »

Thanks to Wilbert, I managed to build a simple wave viewer with zoom and marker options. Very happy now :)

Image
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Draw a waveform with Purebasic and BASS library

Post by wilbert »

Martin Verlaan wrote:I managed to build a simple wave viewer with zoom and marker options. Very happy now :)
That looks great ! :)

I was working on an alternative way using a min/max array to draw but it seems you probably won't need it.
I will post it anyway; maybe someone else can use it.
The idea was to create an array of min max values so the drawing of the wave would be faster when you want to update the waveform while playing audio.
But maybe the previous approach was already fast enough for that. I didn't try that.

MMX Version

Code: Select all

Procedure.q MinMaxScaled(*MinMaxArray, SamplesPerElement, *SampleData, SampleCount, 
                         Scale = 48, Stereo = #True, Cont.q=$800080007fff7fff)
  
  ; <<< MinMaxScaled >>>
  
  ; This mmx optimized procedure fills an array with scaled min/max values.
  ; A scale of 64 means the output values will be in range [-64, 63].
  ; Max scale is 128 so each value fit in a byte [-128, 127].
  
  Protected.i reg_b
  If (*MinMaxArray And SamplesPerElement > 0) And (*SampleData And SampleCount > 0)
    If Scale > 128 : Scale = 128 : EndIf
    CompilerIf #PB_Compiler_Processor = #PB_Processor_x64
      !mov [p.v_reg_b], rbx             ; backup rbx
      !mov rax, [p.p_SampleData]
      !mov rdx, [p.p_MinMaxArray]
    CompilerElse
      !mov [p.v_reg_b], ebx             ; backup ebx
      !mov eax, [p.p_SampleData]
      !mov edx, [p.p_MinMaxArray]
    CompilerEndIf
    !mov ecx, [p.v_SamplesPerElement]
    !movq mm0, [p.v_Cont]               ; load continuation value
    !pcmpeqw mm3, mm3                   ; create inversion mask
    !psrlq mm3, 32
    !mov ebx, [p.v_Scale]               ; load scale value
    !imul ebx, 0x00020002
    !movd mm4, ebx
    !punpckldq mm4, mm4
    !.l0:
    !cmp ecx, [p.v_SampleCount]         ; compare SamplesPerElement with SampleCount
    !jbe .l1
    !mov ecx, [p.v_SampleCount]
    !.l1:
    !cmp dword [p.v_Stereo], 0          ; stereo / mono check
    !jnz .l2
    ; load mono sample
    CompilerIf #PB_Compiler_Processor = #PB_Processor_x64
      !movzx ebx, word [rax]            
      !add rax, 2
    CompilerElse
      !movzx ebx, word [eax]
      !add eax, 2
    CompilerEndIf
    !movd mm1, ebx
    !punpcklwd mm1, mm1
    !jmp .l3
    !.l2:
    ; load stereo sample
    CompilerIf #PB_Compiler_Processor = #PB_Processor_x64
      !movd mm1, [rax]                    
      !add rax, 4
    CompilerElse
      !movd mm1, [eax]
      !add eax, 4
    CompilerEndIf
    !.l3:
    !punpckldq mm1, mm1                 ; duplicate sample into l-r-l-r
    !movq mm2, mm0
    !pcmpgtw mm2, mm1                   ; compare min/max with new sample
    !pxor mm2, mm3
    !pand mm0, mm2                      ; update min/max
    !pandn mm2, mm1
    !por mm0, mm2
    !sub ecx, 1
    !jnz .l1
    !pmulhw mm0, mm4                    ; scale min/max values
    !packsswb mm0, mm0                  ; convert min/max from word to byte
    ; store scaled min/max values
    CompilerIf #PB_Compiler_Processor = #PB_Processor_x64
      !movd [rdx], mm0                    
      !add rdx, 4
    CompilerElse
      !movd [edx], mm0                    
      !add edx, 4
    CompilerEndIf
    !movq mm0, mm1
    !mov ecx, [p.v_SamplesPerElement]
    !sub [p.v_SampleCount], ecx         ; decrease SampleCount
    !ja .l0
    !movq [p.v_Cont], mm1               ; store continuation value
    !emms
    CompilerIf #PB_Compiler_Processor = #PB_Processor_x64
      !mov rbx, [p.v_reg_b]             ; restore rbx
    CompilerElse
      !mov ebx, [p.v_reg_b]             ; restore ebx
    CompilerEndIf 
  EndIf
  ProcedureReturn Cont
EndProcedure




Structure MinMax
  min.b[2]
  max.b[2]
EndStructure

Procedure UpdateCanvas(Canvas, Array MM.MinMax(1), Offset = 0)
  Protected.i Width, Height, VOffsetL, VOffsetR, MaxX, X, X_ 
  StartDrawing(CanvasOutput(Canvas))
  Width = OutputWidth() : Height = OutputHeight()
  VOffsetL = Height >> 2 : VOffsetR = VOffsetL + Height >> 1
  Box(0, 0, Width, Height, $ffe0e0e0)
  Line(0, VOffsetL, Width, 1, $ffa0a0a0)
  Line(0, VOffsetR, Width, 1, $ffa0a0a0)
  X_ = Offset : MaxX = ArraySize(MM())
  While X < Width And X <= MaxX
    LineXY(X, VOffsetL - MM(X_)\min[0], X, VOffsetL - MM(X_)\max[0], $ffff8000)
    LineXY(X, VOffsetR - MM(X_)\min[1], X, VOffsetR - MM(X_)\max[1], $ffff8000)
    X + 1 : X_ + 1
  Wend
  StopDrawing()  
EndProcedure


SamplesPerElement = 64

BASS_Init(-1, 44100, 0, 0, #Null)

Channel.l = BASS_StreamCreateFile(#False, @"Test.mp3", 0, 0, #BASS_STREAM_PRESCAN|#BASS_STREAM_DECODE|#BASS_UNICODE)
Length.q = BASS_ChannelGetLength(Channel, #BASS_POS_BYTE)
NumSamples.q = Length >> 2

*WaveData = AllocateMemory(Length)
BASS_ChannelGetData(Channel, *WaveData, Length)

Dim MM.MinMax(NumSamples / SamplesPerElement)
MinMaxScaled(@MM(), SamplesPerElement, *WaveData, NumSamples, 52)

OpenWindow(0, 0, 0, 620, 360, "Waveform", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
CanvasGadget(0, 10, 10, 600, 300)
ButtonGadget(1, 20, 320, 40, 20, "<<")
ButtonGadget(2, 70, 320, 40, 20, ">>")
ButtonGadget(3, 120, 320, 40, 20, "+")
ButtonGadget(4, 170, 320, 40, 20, "-")

UpdateCanvas(0, MM())
Offset = 0

Repeat
  Event = WaitWindowEvent()
  If Event = #PB_Event_Gadget
    Select EventGadget()
      Case 1
        Offset - 600
        If Offset < 0 : Offset = 0 : EndIf
      Case 2
        Offset + 600
      Case 3
        If SamplesPerElement > 1
          SamplesPerElement >> 1 : Offset << 1 
        EndIf
        ReDim MM.MinMax(NumSamples / SamplesPerElement)
        MinMaxScaled(@MM(), SamplesPerElement, *WaveData, NumSamples, 52)
      Case 4
        If SamplesPerElement < 65536
          SamplesPerElement << 1 : Offset >> 1 
        EndIf
        ReDim MM.MinMax(NumSamples / SamplesPerElement)
        MinMaxScaled(@MM(), SamplesPerElement, *WaveData, NumSamples, 52)
    EndSelect
    UpdateCanvas(0, MM(), Offset)
  EndIf
Until Event = #PB_Event_CloseWindow

More advanced SSE2 version

Code: Select all

Procedure.l MinMaxScaled(*MinMaxArray, SamplesPerElement, *SampleData, SampleCount, 
                         Scale = 48, Stereo = #True, Cont.l=0)
  
  ; <<< MinMaxScaled >>>
  
  ; This sse2 optimized procedure fills an array with scaled min/max values.
  ; A scale of 64 means the output values will be in range [-64, 63].
  ; Max scale is 128 so each value fit in a byte [-128, 127].
  
  If (*MinMaxArray And SamplesPerElement > 0) And (*SampleData And SampleCount > 0)
    If Scale > 128 : Scale = 128 : EndIf
    CompilerIf #PB_Compiler_Processor = #PB_Processor_x64
      !mov [rsp-8], rbx                 ; backup rbx
      !movdqu [rsp-24], xmm6            ; backup xmm6
      !movdqu [rsp-40], xmm7            ; backup xmm7
      !mov rax, [p.p_SampleData]
      !mov rdx, [p.p_MinMaxArray]
    CompilerElse
      !mov [esp-4], ebx                 ; backup ebx
      !mov eax, [p.p_SampleData]
      !mov edx, [p.p_MinMaxArray]
    CompilerEndIf
    !mov ecx, [p.v_SamplesPerElement]
    !mov ebx, 1                         ; set xmm6 to 1/SamplesPerElement
    !cvtsi2sd xmm6, ebx
    !cvtsi2sd xmm1, ecx
    !divsd xmm6, xmm1                   
    !movlhps xmm6, xmm6
    !mov ebx, [p.v_Scale]               ; set xmm7 to scale value
    !imul ebx, 0x00020002
    !movd xmm7, ebx
    !pshufd xmm7, xmm7, 0
    !movd xmm0, [p.v_Cont]              ; load continuation value
    !.l0:
    !movdqa xmm2, xmm0
    !movdqa xmm3, xmm0
    !xorpd xmm4, xmm4
    !xorpd xmm5, xmm5    
    !cmp ecx, [p.v_SampleCount]         ; compare SamplesPerElement with SampleCount
    !jbe .l1
    !mov ebx, 1
    !mov ecx, [p.v_SampleCount]
    !cvtsi2sd xmm6, ebx
    !cvtsi2sd xmm1, ecx
    !divsd xmm6, xmm1
    !movlhps xmm6, xmm6    
    !.l1:
    !cmp dword [p.v_Stereo], 0          ; stereo / mono check
    !jnz .l2
    ; load mono sample
    CompilerIf #PB_Compiler_Processor = #PB_Processor_x64
      !movzx ebx, word [rax]            
      !add rax, 2
    CompilerElse
      !movzx ebx, word [eax]
      !add eax, 2
    CompilerEndIf
    !movd xmm0, ebx
    !punpcklwd xmm0, xmm0
    !jmp .l3
    !.l2:
    ; load stereo sample
    CompilerIf #PB_Compiler_Processor = #PB_Processor_x64
      !movd xmm0, [rax]                    
      !add rax, 4
    CompilerElse
      !movd xmm0, [eax]
      !add eax, 4
    CompilerEndIf
    !.l3:
    !movdqa xmm1, xmm0
    !punpcklwd xmm1, xmm1               ; convert word to long
    !psrad xmm1, 16
    !cvtdq2pd xmm1, xmm1                ; convert long to double
    !pminsw xmm2, xmm0                  ; update min
    !pmaxsw xmm3, xmm0                  ; update max
    !addpd xmm4, xmm1                   ; update sum
    !mulpd xmm1, xmm1
    !addpd xmm5, xmm1                   ; update sumsq    
    !sub ecx, 1
    !jnz .l1
    !mulpd xmm4, xmm6                   ; divide sum by samples per element
    !mulpd xmm5, xmm6                   ; divide sumsq by samples per element
    !sqrtpd xmm5, xmm5                  ; square root of sumsq
    !cvtpd2dq xmm4, xmm4                ; convert to long
    !cvtpd2dq xmm5, xmm5
    !packssdw xmm4, xmm4                ; convert to word
    !packssdw xmm5, xmm5
    !punpckldq xmm2, xmm3
    !punpckldq xmm4, xmm5
    !punpcklqdq xmm2, xmm4              ; combine min/max/avg/rms
    !pmulhw xmm2, xmm7                  ; scale values
    !packsswb xmm2, xmm2                ; convert from to byte
    ; store scaled values
    CompilerIf #PB_Compiler_Processor = #PB_Processor_x64
      !movq [rdx], xmm2                    
      !add rdx, 8
    CompilerElse
      !movq [edx], xmm2                    
      !add edx, 8
    CompilerEndIf
    !mov ecx, [p.v_SamplesPerElement]
    !sub [p.v_SampleCount], ecx         ; decrease SampleCount
    !ja .l0
    CompilerIf #PB_Compiler_Processor = #PB_Processor_x64
      !movdqu xmm7, [rsp-40]            ; restore xmm7
      !movdqu xmm6, [rsp-24]            ; restore xmm6
      !mov rbx, [rsp-8]                 ; restore rbx
    CompilerElse
      !mov ebx, [esp-4]                 ; restore ebx
    CompilerEndIf 
  EndIf
  !movd eax, xmm0                       ; return continuation value
  ProcedureReturn
EndProcedure




Structure MinMax
  min.b[2]
  max.b[2]
  avg.b[2]
  rms.a[2]
EndStructure

Procedure UpdateCanvas(Canvas, Array MM.MinMax(1), Offset = 0, RMS = #False)
  Protected.i Width, Height, VOffsetL, VOffsetR, MaxX, X, X_ 
  StartDrawing(CanvasOutput(Canvas))
  Width = OutputWidth() : Height = OutputHeight()
  VOffsetL = Height >> 2 : VOffsetR = VOffsetL + Height >> 1
  X_ = Offset : MaxX = ArraySize(MM())
  Box(0, 0, Width, Height, $ffe0e0e0)
  If RMS
    Line(0, VOffsetL, Width, 1, $ffa0a0a0)
    Line(0, VOffsetR, Width, 1, $ffa0a0a0)
    While X < Width And X_ <= MaxX
      LineXY(X, VOffsetL, X, VOffsetL - MM(X_)\rms[0], $ffc0c000)
      LineXY(X, VoffsetR, X, VOffsetR - MM(X_)\rms[1], $ffc0c000)
      X + 1 : X_ + 1
    Wend    
  Else
    Line(0, VOffsetL, Width, 1, $ffa0a0a0)
    Line(0, VOffsetR, Width, 1, $ffa0a0a0)
    While X < Width And X_ <= MaxX
      LineXY(X, VOffsetL - MM(X_)\min[0], X, VOffsetL - MM(X_)\max[0], $ffffc000)
      LineXY(X, VOffsetR - MM(X_)\min[1], X, VOffsetR - MM(X_)\max[1], $ffffc000)
      Plot(X, VOffsetL - MM(X_)\avg[0], $ffff8000)
      Plot(X, VOffsetR - MM(X_)\avg[1], $ffff8000)
      X + 1 : X_ + 1
    Wend
  EndIf
  StopDrawing()  
EndProcedure



SamplesPerElement = 64

BASS_Init(-1, 44100, 0, 0, #Null)

Channel.l = BASS_StreamCreateFile(#False, @"Test.mp3", 0, 0, #BASS_STREAM_PRESCAN|#BASS_STREAM_DECODE|#BASS_UNICODE)
Length.q = BASS_ChannelGetLength(Channel, #BASS_POS_BYTE)
NumSamples.q = Length >> 2

*WaveData = AllocateMemory(Length)
BASS_ChannelGetData(Channel, *WaveData, Length)

Dim MM.MinMax(NumSamples / SamplesPerElement)
MinMaxScaled(@MM(), SamplesPerElement, *WaveData, NumSamples, 64)

OpenWindow(0, 0, 0, 620, 360, "Waveform", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
CanvasGadget(0, 10, 10, 600, 300)
ButtonGadget(1, 20, 320, 40, 20, "<<")
ButtonGadget(2, 70, 320, 40, 20, ">>")
ButtonGadget(3, 120, 320, 40, 20, "+")
ButtonGadget(4, 170, 320, 40, 20, "-")
CheckBoxGadget(5, 250, 320, 60, 20, "RMS")
StringGadget(6, 500, 320, 100, 20, "0.000")


Offset = 0
UpdateCanvas(0, MM(), Offset, GetGadgetState(5))

Repeat
  Event = WaitWindowEvent()
  If Event = #PB_Event_Gadget
    Select EventGadget()
      Case 1
        Offset - 600
        If Offset < 0 : Offset = 0 : EndIf
      Case 2
        Offset + 600
      Case 3
        If SamplesPerElement > 1
          SamplesPerElement >> 1 : Offset << 1 
        EndIf
        ReDim MM.MinMax(NumSamples / SamplesPerElement)
        MinMaxScaled(@MM(), SamplesPerElement, *WaveData, NumSamples, 64)
      Case 4
        If SamplesPerElement < 65536
          SamplesPerElement << 1 : Offset >> 1 
        EndIf
        ReDim MM.MinMax(NumSamples / SamplesPerElement)
        MinMaxScaled(@MM(), SamplesPerElement, *WaveData, NumSamples, 64)
      Case 6
        Offset = ValD(GetGadgetText(6))*44100/SamplesPerElement
        UpdateCanvas(0, MM(), Offset, GetGadgetState(5))
        Continue
    EndSelect
    UpdateCanvas(0, MM(), Offset, GetGadgetState(5))
    SetGadgetText(6, StrD(Offset*SamplesPerElement/44100, 3))
  EndIf
Until Event = #PB_Event_CloseWindow
Last edited by wilbert on Sat Jun 23, 2018 6:40 am, edited 1 time in total.
Windows (x64)
Raspberry Pi OS (Arm64)
Martin Verlaan
Enthusiast
Enthusiast
Posts: 124
Joined: Sun Apr 01, 2018 11:26 am

Re: Draw a waveform with Purebasic and BASS library

Post by Martin Verlaan »

Nice (and very interesting) surprise Wilbert! Your previous code was already pretty fast but the faster the better. I am struggeling to understand how to use your updated code. How to show the full waveform and is it also possible to show a part based on a begin and end position?

This is the way I zoom with your previous example:

Code: Select all

Procedure LoadWaveform()
  Define Filename.s = GetGadgetText(FileString)
  
  Chan = BASS_StreamCreateFile(#False, @Filename, 0, 0, #BASS_STREAM_PRESCAN|#BASS_UNICODE) ;global variable
  Channel = BASS_StreamCreateFile(#False, @Filename, 0, 0, #BASS_STREAM_PRESCAN|#BASS_STREAM_DECODE|#BASS_UNICODE) ;global variable
  Length = BASS_ChannelGetLength(Channel, #BASS_POS_BYTE)   ;global variable
  BytesPerPixel = Length / #WaveformXPixels ;global variable
    
  ReDim Buffer.Sample(Length >> 2);global variable
 
  BytesRead = 0 ;global variable
  BufTel = 0 ;global variable
  Part = 1024 * 40 ;global variable
  Procent = 0 ;global variable
  
  StartDrawing(CanvasOutput(WaveformPic)) 
    Box(0, 0, #WaveformXPixels, WaveformYPixels, RGB(0, 0, 0))   
    DrawText(290, (WaveformYPixels / 2) - 10, "Loading waveform... (0%)", RGB(255, 255, 255), RGB(0, 0, 0))  
  StopDrawing()
    
  If Not BufTimerActive 
    AddWindowTimer(dbWindow, #BufTimer, 1)  
    AddWindowTimer(dbWindow, #ProcentTimer, 250)      
  EndIf  
EndProcedure
To continue events during BASS_ChannelGetLength I read the buffer in small parts with a timer. See code below. This way I can also show the loading progress (in percents) to the user. Maybe it's better and faster to use Threading for this but I don't understand yet how to use it. If the user clicks on the play button a timer will be activated to calculate the position of the playing-cursor. If anyone is interested you can see in the code below how I did this.

Code: Select all

Repeat      
  Event = WaitWindowEvent()
  
  If EventWindow() = dbWindow    
    If Event = #PB_Event_Timer
      Select EventTimer() 
        Case #ProcentTimer
          Procent = Int(100 * (BytesRead / Length))
 
          StartDrawing(CanvasOutput(WaveformPic))  
            DrawText(290, (WaveformYPixels / 2) - 10, "Loading waveform... (" + Procent +  "%)", RGB(255, 255, 255), RGB(0, 0, 0))  
          StopDrawing()
        Case #BufTimer     
          BufTimerActive = #True
          ReDim Buf.Sample(Part >> 2)
          Bytes = BASS_ChannelGetData(Channel, @Buf(), part) 
          BytesRead + bytes

          For i = 0 To Bytes - 1
           Buffer(BufTel >> 2) = Buf(i >> 2)
           BufTel + 1
          Next i    
           
          If BytesRead >= Length
            RemoveWindowTimer(dbWindow, #BufTimer)
            RemoveWindowTimer(dbWindow, #ProcentTimer)
                        
            BufTimerActive = #False
            WaveImage = CreateImage(#PB_Any, #WaveformXPixels, WaveformYPixels) 
            UpdateWaveImage(@Buffer(), Length >> 2)  
            DrawMarkers()
            
            Define EndSec.d = BASS_ChannelBytes2Seconds(Channel, Length)
            
            SetGadgetText(SelBeginString, SecondsToTime(0))
            SetGadgetText(SelEndString, SecondsToTime(0))
            SetGadgetText(SelLengthString, SecondsToTime(0))            
            SetGadgetText(ViewBeginString, SecondsToTime(0))
            SetGadgetText(ViewEndString, SecondsToTime(EndSec))
            SetGadgetText(ViewLengthString, SecondsToTime(EndSec))            
            SetGadgetText(CursorPosString, SecondsToTime(0)) 
            
            DisableGadget(Button_ZoomToSelection, #True)
            DisableGadget(Button_ZoomOut, #True)
            DisableGadget(Button_ZoomOutFull, #True)
            DisableGadget(ButtonPlay, #False)
            DisableGadget(Combo_Markers, #False)
            DisableGadget(ButtonMarker, #False)
            DisableGadget(ButtonDeleteMarker, #False)
            DisableGadget(Button_Resize, #False)
           
            CursorPos = 0  
           
            DrawCursor()      
          EndIf      
        Case #PlayTimer   
          If GetGadgetText(SelLengthString) <> "00:00.000"
            Define StopPosition.q = BASS_ChannelSeconds2Bytes(Chan, Time2Seconds(GetGadgetText(SelEndString)))          
          Else
            Define StopPosition.q =  BASS_ChannelSeconds2Bytes(Chan, Time2Seconds(GetGadgetText(ViewEndString))) 
          EndIf       
          
          If BASS_ChannelGetPosition(Chan, #BASS_POS_BYTE) >= StopPosition
            StopPlaying()
          Else      
            Define CurrentPosBytes.q = BASS_ChannelGetPosition(Chan, #BASS_POS_BYTE) 
            Define CurrentPosSec.d = BASS_ChannelBytes2Seconds(Chan, CurrentPosBytes) 
            Define PosBeginBytes.q = BASS_ChannelSeconds2Bytes(Chan, Time2Seconds(GetGadgetText(ViewBeginString)))        
            
            SetGadgetText(CursorPosString, SecondsToTime(CurrentPosSec))           
            
            StartDrawing(CanvasOutput(WaveformPic))
              If GetGadgetText(SelLengthString) <> "00:00.000"       
                DrawImage(ImageID(#SelectionWaveImage), 0, 0)            
                Line((CurrentPosBytes - PosBeginBytes) / BytesPerPixel, 0, 1, WaveformYPixels, RGB(0, 0, 0))                    
              Else
                DrawImage(ImageID(#CursorWaveImage), 0, 0)     
                Line((CurrentPosBytes - PosBeginBytes) / BytesPerPixel, 0, 1, WaveformYPixels, RGB(255, 255, 255))                      
              EndIf
            StopDrawing()  
          EndIf
      EndSelect
    EndIf
    dbWindow_Events(Event)   
  EndIf
Until Event = #PB_Event_CloseWindow  
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Draw a waveform with Purebasic and BASS library

Post by wilbert »

Martin Verlaan wrote:Nice (and very interesting) surprise Wilbert! Your previous code was already pretty fast but the faster the better. I am struggeling to understand how to use your updated code. How to show the full waveform and is it also possible to show a part based on a begin and end position?

Code: Select all

Procedure.q MinMaxScaled(*MinMaxArray, SamplesPerElement, *SampleData, SampleCount, Scale = 48, Stereo = #True, Cont.q=$800080007fff7fff)
SamplesPerElement is similar to BytesPerPixel.
It's the number of samples to process for each array element.
So if you want to show the full waveform, just calculate SamplesPerElement like you did with BytesPerPixel.

To zoom, you can redim the min/max array and fill it again with a different SamplesPerElement value.
If you want to do it in parts, you can use the Cont value. The procedure returns the last sample from the previous block which can be passed on when you process the next block. Of course the *MinMaxArray pointer needs to point to the right array element if you process a new block.
Windows (x64)
Raspberry Pi OS (Arm64)
Martin Verlaan
Enthusiast
Enthusiast
Posts: 124
Joined: Sun Apr 01, 2018 11:26 am

Re: Draw a waveform with Purebasic and BASS library

Post by Martin Verlaan »

Found a strange bug. :? A zoomed view does not start exactly on the requested position. How to solve this?

Cool Edit screenshot (correct view)
Image

Purebasic screenshot (wrong view) with same position and length as in screenshot above
Image

Code: Select all

IncludeFile "include/bass.pbi"
 
Structure Sample
  l.w
  r.w
EndStructure

Procedure UpdateWaveImage(PBImage, *SampleData.Sample, SampleCount)
  Protected.i Width, Height, X, VOffsetL, VOffsetR, Mul
  Protected.i SamplesPerPixel, Sample
  Protected.w MinL, MaxL, MinR, MaxR
  If *SampleData And SampleCount
    StartDrawing(ImageOutput(PBImage))
    Width = OutputWidth() : Height = OutputHeight()
    VOffsetL = Height >> 2 : VOffsetR = VOffsetL + Height >> 1
    Mul = (VOffsetL * $19999) >> 16
    Box(0, 0, Width, Height, $ffe0e0e0)
    Line(0, VOffsetL, Width, 1, $ffa0a0a0)
    Line(0, VOffsetR, Width, 1, $ffa0a0a0)
    SamplesPerPixel = (SampleCount + Width - 1) / Width
    If SampleCount
      MinL = *SampleData\l : MinR = *SampleData\r : MaxL = MinL : MaxR = MinR     
      While SampleCount
        If *SampleData\l < MinL : MinL = *SampleData\l : ElseIf *SampleData\l > MaxL : MaxL = *SampleData\l : EndIf
        If *SampleData\r < MinR : MinR = *SampleData\r : ElseIf *SampleData\r > MaxR : MaxR = *SampleData\r : EndIf
        Sample + 1 : SampleCount - 1
        If Sample = SamplesPerPixel Or SampleCount = 0
          MinL = (MinL * Mul) >> 16 : MaxL = (MaxL * Mul) >> 16
          MinR = (MinR * Mul) >> 16 : MaxR = (MaxR * Mul) >> 16
          LineXY(X, VOffsetL - MinL, X, VOffsetL - MaxL, $ffff8000)
          LineXY(X, VOffsetR - MinR, X, VOffsetR - MaxR, $ffff8000)
          MinL = *SampleData\l : MinR = *SampleData\r : MaxL = MinL : MaxR = MinR
          X + 1 : Sample = 0
        EndIf
        *SampleData + 4
      Wend
    EndIf
    StopDrawing()
  EndIf 
EndProcedure

BASS_Init(-1, 44100, 0, 0, #Null)

filename.s = "/media/martin/Extern/smartmix-Data/mp3/100 bpm/casbl 6400731 cube - two heads are better than one [time 5m55s, kbps 128, bpm 100.81, year 1982, genre italodisco].mp3"
Channel  = BASS_StreamCreateFile(#False, @filename, 0, 0, #BASS_STREAM_PRESCAN|#BASS_STREAM_DECODE|#BASS_UNICODE)
BASS_ChannelSetPosition(Channel, BASS_ChannelSeconds2Bytes(Channel, 200.360), #BASS_POS_BYTE) 

length = BASS_ChannelSeconds2Bytes(Channel, 0.500) 
Dim Buffer.Sample(length >> 2)
BASS_ChannelGetData(Channel, @Buffer(), length)

OpenWindow(0, 0, 0, 620, 320, "Pos: 3:20.360 - Length: 0.500", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)

WaveImage = CreateImage(#PB_Any, 600, 300) 
UpdateWaveImage(WaveImage, @Buffer(), length >> 2)
ImageGadget(1, 10, 0, 600, 300, ImageID(WaveImage))

Repeat
  Event = WaitWindowEvent()
Until Event = #PB_Event_CloseWindow
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Draw a waveform with Purebasic and BASS library

Post by wilbert »

I don’t see anything strange in your code.
Maybe you should try with another editor like Audacity as well to verify if it is the same as Cool Edit.
Windows (x64)
Raspberry Pi OS (Arm64)
Wolfram
Enthusiast
Enthusiast
Posts: 567
Joined: Thu May 30, 2013 4:39 pm

Re: Draw a waveform with Purebasic and BASS library

Post by Wolfram »

There are different ways to draw a zoomed wave.
The main different is view the wave Peak or the RMS. (I think RMS is mostly the best way)
In some audio editors you can switch between this options.
The next different is how to calculate the RMS.
macOS Catalina 10.15.7
Martin Verlaan
Enthusiast
Enthusiast
Posts: 124
Joined: Sun Apr 01, 2018 11:26 am

Re: Draw a waveform with Purebasic and BASS library

Post by Martin Verlaan »

@Wilbert: Audacity shows exact the same picture as Cool Edit. I discovered this problem when I was testing code to display a marker into the waveform. I see the marker on the correct position in the full waveform, but the more I zoom in, the marker is a bit off the exact position.
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Draw a waveform with Purebasic and BASS library

Post by wilbert »

@Wolfram: it's not difficult to add RMS support.

Code: Select all

Procedure UpdateWaveImage(PBImage, *SampleData.Sample, SampleCount, Mode=0)
  ; Mode 0 : Min/max
  ; Mode 1 : RMS
  Protected.i Width, Height, X, VOffsetL, VOffsetR, Mul
  Protected.i SamplesPerPixel, Sample
  Protected.w MinL, MaxL, MinR, MaxR
  Protected.d SumsqL, SumsqR, Rcp
  If *SampleData And SampleCount
    StartDrawing(ImageOutput(PBImage))
    Width = OutputWidth() : Height = OutputHeight()
    VOffsetL = Height >> 2 : VOffsetR = VOffsetL + Height >> 1
    Mul = (VOffsetL * $19999) >> 16
    Box(0, 0, Width, Height, $ffe0e0e0)
    Line(0, VOffsetL, Width, 1, $ffa0a0a0)
    Line(0, VOffsetR, Width, 1, $ffa0a0a0)
    SamplesPerPixel = (SampleCount + Width - 1) / Width
    If SampleCount
      If Mode = 0
        MinL = *SampleData\l : MinR = *SampleData\r : MaxL = MinL : MaxR = MinR     
        While SampleCount
          If *SampleData\l < MinL : MinL = *SampleData\l : ElseIf *SampleData\l > MaxL : MaxL = *SampleData\l : EndIf
          If *SampleData\r < MinR : MinR = *SampleData\r : ElseIf *SampleData\r > MaxR : MaxR = *SampleData\r : EndIf
          Sample + 1 : SampleCount - 1
          If Sample = SamplesPerPixel Or SampleCount = 0
            MinL = (MinL * Mul) >> 16 : MaxL = (MaxL * Mul) >> 16
            MinR = (MinR * Mul) >> 16 : MaxR = (MaxR * Mul) >> 16
            LineXY(X, VOffsetL - MinL, X, VOffsetL - MaxL, $ffff8000)
            LineXY(X, VOffsetR - MinR, X, VOffsetR - MaxR, $ffff8000)
            MinL = *SampleData\l : MinR = *SampleData\r : MaxL = MinL : MaxR = MinR
            X + 1 : Sample = 0
          EndIf
          *SampleData + 4
        Wend
      Else
        Rcp = 1.0 / SamplesPerPixel
        While SampleCount
          SumsqL + (*SampleData\l * *SampleData\l) : SumsqR + (*SampleData\r * *SampleData\r)
          Sample + 1 : SampleCount - 1
          If Sample = SamplesPerPixel Or SampleCount = 0
            If SampleCount = 0 : Rcp = 1.0 / Sample : EndIf
            MaxL = Sqr(SumsqL * Rcp) : MaxR = Sqr(SumsqR * Rcp)
            MaxL = (MaxL * Mul) >> 16 : MaxR = (MaxR * Mul) >> 16    
            LineXY(X, VOffsetL, X, VOffsetL - MaxL, $ffff8000)
            LineXY(X, VOffsetR, X, VOffsetR - MaxR, $ffff8000)
            SumsqL = 0 : SumsqR = 0
            X + 1 : Sample = 0
          EndIf
          *SampleData + 4
        Wend        
      EndIf
    EndIf
    StopDrawing()
  EndIf 
EndProcedure
@Martin:
It seems BASS calculates the byte position correctly.
The only thing I can think of is that it might make a difference to load the entire song into memory. :?
Windows (x64)
Raspberry Pi OS (Arm64)
Wolfram
Enthusiast
Enthusiast
Posts: 567
Joined: Thu May 30, 2013 4:39 pm

Re: Draw a waveform with Purebasic and BASS library

Post by Wolfram »

@ Martin
can you send me the wav file which is shown in the pictures above.
macOS Catalina 10.15.7
Martin Verlaan
Enthusiast
Enthusiast
Posts: 124
Joined: Sun Apr 01, 2018 11:26 am

Re: Draw a waveform with Purebasic and BASS library

Post by Martin Verlaan »

Wolfram wrote:@ Martin
can you send me the wav file which is shown in the pictures above.
It's an mp3 file. As a test I saved it to WAV and tried again. The waveform looks correct then, exactly the same as Cool Edit. So BASS_ChannelSetPosition seems not 100% accurate with mp3 files. I also tried the BASS_POS_DECODETO flag but without success. If you still want the MP3 file, then let me know...
firace
Addict
Addict
Posts: 899
Joined: Wed Nov 09, 2011 8:58 am

Re: Draw a waveform with Purebasic and BASS library

Post by firace »

Martin Verlaan wrote:
Wolfram wrote:@ Martin
can you send me the wav file which is shown in the pictures above.
It's an mp3 file. As a test I saved it to WAV and tried again. The waveform looks correct then, exactly the same as Cool Edit. So BASS_ChannelSetPosition seems not 100% accurate with mp3 files. I also tried the BASS_POS_DECODETO flag but without success. If you still want the MP3 file, then let me know...

Just wondering, have you been able to solve your issue?
Post Reply