Draw a waveform with Purebasic and BASS library
-
- Enthusiast
- Posts: 124
- Joined: Sun Apr 01, 2018 11:26 am
Re: Draw a waveform with Purebasic and BASS library
Thanks Wilbert!! I should test it some more but your updated code seems to work! You just saved me from a depression
Re: Draw a waveform with Purebasic and BASS library
re good code, is better than audacity for me and more fast
I added support for mono channels, after your permission @wilbert...
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..
Re: Draw a waveform with Purebasic and BASS library
ThanksCELTIC88 wrote: re good code, is better than audacity for me and more fast
I added support for mono channels, after your permission @wilbert...
Feel free to modify or improve the code.
Windows (x64)
Raspberry Pi OS (Arm64)
Raspberry Pi OS (Arm64)
-
- Enthusiast
- Posts: 124
- Joined: Sun Apr 01, 2018 11:26 am
Re: Draw a waveform with Purebasic and BASS library
That looks great !Martin Verlaan wrote:I managed to build a simple wave viewer with zoom and marker options. Very happy now
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)
Raspberry Pi OS (Arm64)
-
- Enthusiast
- Posts: 124
- Joined: Sun Apr 01, 2018 11:26 am
Re: Draw a waveform with Purebasic and BASS library
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:
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.
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
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
Re: Draw a waveform with Purebasic and BASS library
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)
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)
Raspberry Pi OS (Arm64)
-
- Enthusiast
- Posts: 124
- Joined: Sun Apr 01, 2018 11:26 am
Re: Draw a waveform with Purebasic and BASS library
Found a strange bug. A zoomed view does not start exactly on the requested position. How to solve this?
Cool Edit screenshot (correct view)
Purebasic screenshot (wrong view) with same position and length as in screenshot above
Cool Edit screenshot (correct view)
Purebasic screenshot (wrong view) with same position and length as in screenshot above
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
Re: Draw a waveform with Purebasic and BASS library
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.
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)
Raspberry Pi OS (Arm64)
Re: Draw a waveform with Purebasic and BASS library
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.
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
-
- Enthusiast
- Posts: 124
- Joined: Sun Apr 01, 2018 11:26 am
Re: Draw a waveform with Purebasic and BASS library
@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.
Re: Draw a waveform with Purebasic and BASS library
@Wolfram: it's not difficult to add RMS support.
@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.
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
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)
Raspberry Pi OS (Arm64)
Re: Draw a waveform with Purebasic and BASS library
@ Martin
can you send me the wav file which is shown in the pictures above.
can you send me the wav file which is shown in the pictures above.
macOS Catalina 10.15.7
-
- Enthusiast
- Posts: 124
- Joined: Sun Apr 01, 2018 11:26 am
Re: Draw a waveform with Purebasic and BASS library
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...Wolfram wrote:@ Martin
can you send me the wav file which is shown in the pictures above.
Re: Draw a waveform with Purebasic and BASS library
Martin Verlaan wrote: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...Wolfram wrote:@ Martin
can you send me the wav file which is shown in the pictures above.
Just wondering, have you been able to solve your issue?