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: 119
Joined: Sun Apr 01, 2018 11:26 am

Re: Draw a waveform with Purebasic and BASS library

Post by Martin Verlaan »

Yes, I posted a message on the BASS forum and I got this explanation:
Comparing the posted waveforms, it looks your one is slightly ahead of the Cool Edit one. I suspect that is because BASS is removing the encoding delay (and padding) from the decoded data while Cool Edit isn't. Which one is correct is probably a matter of opinion but I would say the former is more correct. For example, if you try generating and encoding a sine wave to MP3 with LAME and then load that into Cool Edit, you will probably see a little gap added at the start and end. Those gaps won't be present if you decode the MP3 with BASS because it removes them.
Martin Verlaan
Enthusiast
Enthusiast
Posts: 119
Joined: Sun Apr 01, 2018 11:26 am

Re: Draw a waveform with Purebasic and BASS library

Post by Martin Verlaan »

To genius Wilbert: :mrgreen:

I am using your latest assembly code. It's fast but there's still a small delay when I zoom in or zoom out, because every time I need to call BASS_ChannelGetData(). Like this:

Code: Select all

Length = BASS_ChannelSeconds2Bytes(Channel, AfterPos - BeforePos)  
NumSamples.q = Length >> 2
SamplesPerElement = (NumSamples / WaveformXPixels)
BytesPerPixel = SamplesPerElement << 2
*WaveData = AllocateMemory(Length)
  
BASS_ChannelSetPosition(Channel, PosBytes, #BASS_POS_BYTE)    
BASS_ChannelGetData(Channel, *WaveData, Length)

Dim MM.MinMax(NumSamples / SamplesPerElement)
MinMaxScaled(@MM(), SamplesPerElement, *WaveData, NumSamples)
UpdateCanvas(MM())    
There must be a faster way to access the needed waveform-data without calling BASS_ChannelGetData() everytime. Do you have an idea how I can do this?
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:There must be a faster way to access the needed waveform-data without calling BASS_ChannelGetData() everytime. Do you have an idea how I can do this?
The fastest way is to keep the entire song in memory at the maximum zoom-in level and zoom out from there.

It's important in this case to think about what accuracy you need.
Sample accurate means about 10 megabyte of data per minute to store which is quite a lot if you keep the entire song in memory.
If it's enough for you to have an accuracy of about a half or one millisecond per pixel you would have to store much less data and it's faster when zooming.
Once you have that entire song in memory as an array of min/max values, I could write another asm procedure to zoom out from that to the level you want to display (like the entire song).
Windows (x64)
Raspberry Pi OS (Arm64)
Martin Verlaan
Enthusiast
Enthusiast
Posts: 119
Joined: Sun Apr 01, 2018 11:26 am

Re: Draw a waveform with Purebasic and BASS library

Post by Martin Verlaan »

wilbert wrote:
Martin Verlaan wrote:Once you have that entire song in memory as an array of min/max values, I could write another asm procedure to zoom out from that to the level you want to display (like the entire song).
I think the accuracy is only important when zooming in deeply. So I would really appreciate if you could write such procedure (if it's not to much hassle for you)! That would make zooming with the mouse wheel also faster. Thanks in advance!
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 think the accuracy is only important when zooming in deeply. So I would really appreciate if you could write such procedure (if it's not to much hassle for you)! That would make zooming with the mouse wheel also faster. Thanks in advance!
Can you try if the MMCombine procedure below work for you ?

Code: Select all

Procedure MMCombine(*MMSrc, *MMDst, SrcElements, Combine); SSE
  ; load xor mask into mm2
  !mov eax, 0x807f807f
  !movd mm2, eax
  !punpcklbw mm2, mm2
  ; load *MMSrc and *MMDst pointers
  CompilerIf #PB_Compiler_Processor = #PB_Processor_x64
    !mov rax, [p.p_MMSrc]
    !mov rdx, [p.p_MMDst]
  CompilerElse
    !mov eax, [p.p_MMSrc]
    !mov edx, [p.p_MMDst]
  CompilerEndIf
  !mov ecx, [p.v_Combine]
  ; verify if Combine <= SrcElements
  !.l0:
  !cmp ecx, [p.v_SrcElements]
  !jbe .l1
  ; if not, set Combine to SrcElements
  !mov ecx, [p.v_SrcElements]
  !mov [p.v_Combine], ecx
  !.l1:
  !pxor mm0, mm0
  !test ecx, 1
  !jz .l2
  !add ecx, 1
  CompilerIf #PB_Compiler_Processor = #PB_Processor_x64
    !movd mm1, [rax]
    !add rax, 4
  CompilerElse
    !movd mm1, [eax]
    !add eax, 4
  CompilerEndIf
  !pshufw mm1, mm1, 01000100b
  !jmp .l3
  !.l2:
  CompilerIf #PB_Compiler_Processor = #PB_Processor_x64
    !movq mm1, [rax]
    !add rax, 8
  CompilerElse
    !movq mm1, [eax]
    !add eax, 8
  CompilerEndIf
  !.l3:
  !pxor mm1, mm2
  !pmaxub mm0, mm1
  !sub ecx, 2
  !jnz .l2
  !pshufw mm1, mm0, 1110b
  !pmaxub mm0, mm1
  !pxor mm0, mm2
  CompilerIf #PB_Compiler_Processor = #PB_Processor_x64
    !movd [rdx], mm0
    !add rdx, 4
  CompilerElse
    !movd [edx], mm0
    !add edx, 4
  CompilerEndIf
  !mov ecx, [p.v_Combine]
  !sub [p.v_SrcElements], ecx
  !jnz .l0
  !emms
EndProcedure



; >> Test <<

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

Dim MM.MinMax(4)
Dim MM_Zoomed.MinMax(2)

MM(0)\min[0] = -5
MM(0)\min[1] = -9
MM(0)\max[0] = 25
MM(0)\max[1] = 75

MM(1)\min[0] = -15
MM(1)\min[1] = -19
MM(1)\max[0] = 45
MM(1)\max[1] = 95

MM(2)\min[0] = -1
MM(2)\min[1] = -89
MM(2)\max[0] = 77
MM(2)\max[1] = 1

MM(3)\min[0] = 0
MM(3)\min[1] = 1
MM(3)\max[0] = 0
MM(3)\max[1] = 3

; Combine 2 MinMax values into 1
MMCombine(@MM(), @MM_Zoomed(), 4, 2)
  
Debug MM_Zoomed(0)\min[0]
Debug MM_Zoomed(0)\min[1]
Debug MM_Zoomed(0)\max[0]
Debug MM_Zoomed(0)\max[1]

Debug MM_Zoomed(1)\min[0]
Debug MM_Zoomed(1)\min[1]
Debug MM_Zoomed(1)\max[0]
Debug MM_Zoomed(1)\max[1]
  • The first procedure argument (*MMSrc) needs to be the address of the source array.
    This would be the array of MinMax values of the entire song in it's most expanded state.
  • The second argument is the address of the destination array.
  • The third argument is the number of elements the source array contains.
  • The last argument is how many source elements to combine into one destination element.
The example in the code above combines two elements into one.
Windows (x64)
Raspberry Pi OS (Arm64)
Martin Verlaan
Enthusiast
Enthusiast
Posts: 119
Joined: Sun Apr 01, 2018 11:26 am

Re: Draw a waveform with Purebasic and BASS library

Post by Martin Verlaan »

First of all: Thank you!

Sorry, I find it difficult to understand your explanation.
The way I zoom without your new procedure was like this:

Code: Select all

  PosBytes.q = BASS_ChannelSeconds2Bytes(Channel, BeforePos) 
  Length.q = BASS_ChannelSeconds2Bytes(Channel, AfterPos - BeforePos)
  NumSamples.q = Length >> 2
  SamplesPerElement = (NumSamples / WaveformXPixels)
  *WaveData = AllocateMemory(Length)
  
  BASS_ChannelSetPosition(Channel, PosBytes, #BASS_POS_BYTE)    
  BASS_ChannelGetData(Channel, *WaveData, Length)
  Dim MM.MinMax(NumSamples / SamplesPerElement)
  MinMaxScaled(@MM(), SamplesPerElement, *WaveData, NumSamples)
  UpdateCanvas(MM())  
How can I make above code work with your procedure?
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:How can I make above code work with your procedure?
Unfortunately it's not one simple change.

The main problem is with BASS_ChannelGetData because it has to decode every time. That's why I suggested to store the entire decoded mp3 in memory.
The fastest way is to do that when it comes to working with the data afterwards, is as an array of min/max values but it would also be possible to do it as decoded samples if there's enough memory.
But you probably would have to change your code in other places as well for this to load an entire song and work from that data instead of using BASS_ChannelGetData all the time.
If you don't want to do that, there's still a little room to improve the speed but not much as the most time consuming thing is probably the mp3 decode.
Windows (x64)
Raspberry Pi OS (Arm64)
Martin Verlaan
Enthusiast
Enthusiast
Posts: 119
Joined: Sun Apr 01, 2018 11:26 am

Re: Draw a waveform with Purebasic and BASS library

Post by Martin Verlaan »

No problem, I'm going to play with it and try to implement it. I don't give up :)
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 »

Hi Wilbert,

can you show me how the MinMaxScaled() procedure must look if I change the structure to word?

Code: Select all

Structure MinMax
  min.W[2]
  max.W[2]
EndStructure
wilbert wrote:
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
macOS Catalina 10.15.7
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 wrote:can you show me how the MinMaxScaled() procedure must look if I change the structure to word?
Are you looking for the same kind of input (16 bit audio) but a bigger output scale ?
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 »

wilbert wrote:
Wolfram wrote:can you show me how the MinMaxScaled() procedure must look if I change the structure to word?
Are you looking for the same kind of input (16 bit audio) but a bigger output scale ?
Yes, if I want a bigger amplitude (Y zoom) I need words.
And I store the values in the array as final Y1 and Y2 values for the draw Line() function.

Code: Select all

MinMaxArray(px)\y = centerOfCanvas -minValue
MinMaxArray(px)\hight = maxValue +minValue
macOS Catalina 10.15.7
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 wrote:Yes, if I want a bigger amplitude (Y zoom) I need words.
And I store the values in the array as final Y1 and Y2 values for the draw Line() function.
Can you try if the code below works for you ?
Scale can now be between -16384 and 16383. I also added offset values for left and right.
So you could let the procedure calculate the final Y coordinates for you by adding an offset value and inverting the scale (like -300) because of the need for vertically flipped coordinates.

Code: Select all

Procedure.q MinMaxScaled(*MinMaxArray, SamplesPerElement, *SampleData, SampleCount, 
                         Scale = 48, Stereo = #True, OffsetL = 0, OffsetR = 0, 
                         Cont.q=$800080007fff7fff)
  
  ; <<< MinMaxScaled >>>
  
  ; This mmx optimized procedure fills an array with scaled min/max values.
  ; Scale should be in range [-16384, 16383].
  ; A scale of 64 means the output values will be in range [-64, 63].
  
  Protected.i reg_b
  If (*MinMaxArray And SamplesPerElement > 0) And (*SampleData And SampleCount > 0)
    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
    !movd mm4, [p.v_Scale]              ; load scale value
    !paddw mm4, mm4
    !punpcklwd mm4, mm4
    !punpckldq mm4, mm4
    !movd mm5, [p.v_OffsetL]            ; load offset values
    !movd mm6, [p.v_OffsetR]
    !punpcklwd mm5, mm6
    !punpckldq mm5, mm5
    !.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
    !paddw mm0, mm5                     ; add offset
    ; store scaled min/max values
    CompilerIf #PB_Compiler_Processor = #PB_Processor_x64
      !movq [rdx], mm0                    
      !add rdx, 8
    CompilerElse
      !movq [edx], mm0                    
      !add edx, 8
    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.w[2]
  max.w[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, #True)

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
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 »

wilbert wrote:
Wolfram wrote:Yes, if I want a bigger amplitude (Y zoom) I need words.
And I store the values in the array as final Y1 and Y2 values for the draw Line() function.
Can you try if the code below works for you ?
Scale can now be between -16384 and 16383. I also added offset values for left and right.
So you could let the procedure calculate the final Y coordinates for you by adding an offset value and inverting the scale (like -300) because of the need for vertically flipped coordinates.
I tried to remove the stereo option as well, but get no luck. For me assembler is hard to read.

Code: Select all

Structure MinMax
  min.w
  max.w
EndStructure

Procedure.q MinMaxScaled(*MinMaxArray, SamplesPerElement, *SampleData, SampleCount, 
                         Scale = 120, OffsetL =150, Cont.q=$800080007fff7fff)
  
  ; <<< MinMaxScaled >>>
  
  ; This mmx optimized procedure fills an array with scaled min/max values.
  ; Scale should be in range [-16384, 16383].
  ; A scale of 64 means the output values will be in range [-64, 63].
  
  Protected.i reg_b
  If (*MinMaxArray And SamplesPerElement > 0) And (*SampleData And SampleCount > 0)
    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
    !movd mm4, [p.v_Scale]              ; load scale value
    !paddw mm4, mm4
    !punpcklwd mm4, mm4
    !punpckldq mm4, mm4
    !movd mm5, [p.v_OffsetL]            ; load offset values
;     !movd mm6, [p.v_OffsetR]
;     !punpcklwd mm5, mm6
;     !punpckldq mm5, mm5
    !.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, 2
;      
;     CompilerElse
;        
;       !movd mm1, [eax]
;       !add eax, 2
;     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
    !paddw mm0, mm5                     ; add offset
    ; store scaled min/max values
    CompilerIf #PB_Compiler_Processor = #PB_Processor_x64
      !movq [rdx], mm0                    
      !add rdx, 4
    CompilerElse
      !movq [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

macOS Catalina 10.15.7
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 wrote:I tried to remove the stereo option as well, but get no luck. For me assembler is hard to read.
The code I posted can handle a mono input with the Stereo argument set to #False and on that case outputs identical values for left and right channel.
So you could draw only one of those channels in that case.
If you want the output to be also one channel, that can be changed but in that case the structure also needs to be changed to

Code: Select all

Structure MinMax
  min.W
  max.W
EndStructure
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 »

wilbert wrote:
Wolfram wrote:I tried to remove the stereo option as well, but get no luck. For me assembler is hard to read.
The code I posted can handle a mono input with the Stereo argument set to #False and on that case outputs identical values for left and right channel.
So you could draw only one of those channels in that case.
If you want the output to be also one channel, that can be changed but in that case the structure also needs to be changed to

Code: Select all

Structure MinMax
  min.W
  max.W
EndStructure
Ohhh, I got it!
My thoughts went in a different direction.

Last question, whats is the secret of the Cont.q=$800080007fff7fff.
Can you give me an PB code snipe which shows how it work?
macOS Catalina 10.15.7
Post Reply