T
tommytwotrain
Guest
Hi all,
This is a continuation of this thread where we covered the wav chart basics:
How to draw a WAV sound file Chart graph and get the header data.
I did some further testing and found that the chart control is much faster than drawing your own wav graph. I drew the wav graph with gdi+ on a bufferedgraphic and found the chart to be much faster.
So I further optimized the chart part by only plotting every 40th point in the byte array. This can vary to as much as /100 and you dont see much difference in the blondie rapture.wav. I also used the .fastline chart instead of .line for more speed.
The blondi rapture wav file is available here for a limited time.
Now I am trying to figure what the time increments are for the x axis.
You can see in the example that I have added a timer and each timer tick I get the playback position in seconds from media player and convert that to the actual chart coordinates. I found this code works ok for the blondi wav which is 16 bit stereo (mono not working):
Dim t As Double = 1.25 * NumChannels * BitsPerSample * Player.controls.currentItem.duration
Notice the 1.25 factor I needed to get the correct ratio to convert with. However I think with proper use of the header info the factor should not be needed. It seems the equation should be a simple ratio of something but I am not sure what?
Does anyone know how to calc what the actual time step is for each value in the wav bytearray?
Furthermore, the time scale shown is off x200 the actual time ie the blondi wav is about 8.4 secs so the x axis scale should be 8000 millisecs at the end not 40000. I need to make that correction somewhere.
Finally, I am using mediaplayer in this example v1 but maybe there is a better way? Like Media.SoundPlayer or mcisendstring?
Here is the example code for v1. The example makes all the controls all you have to do is cut and paster to an empty form.
The moving red line in the animation below is the play position when you play the wav. That's what I am talking about (runs smoother in real life).
PS I just realized part of the problem in the time scale in the example is I don't consider I am only plotting every 40th point. So need a x40 somewhere I guess.
Edit: v2 has different time function as described below.
'chart wav sound file with play postion v2
'lmb drag the chart area to zoom
Imports System.IO
Imports System.Windows.Forms.DataVisualization.Charting
Public Class Form6
Private cBackClr As Color = Color.Black
Private cForeClr As Color = Color.AntiqueWhite
Private cBorder As Integer = 10
Private WithEvents Player As WMPLib.WindowsMediaPlayer
Private WithEvents Timer1 As New Timer With {.Interval = 10}
Private WithEvents OpenBtn As New Button With {.Parent = Me, .Text = "Open",
.Location = New Point(20, cBorder), .ForeColor = cForeClr, .BackColor = cBackClr}
Private WithEvents PlayBtn As New Button With {.Parent = Me, .Text = "Play",
.Location = New Point(100, cBorder), .ForeColor = cForeClr, .BackColor = cBackClr}
Private WithEvents ZoomResetBtn As New Button With {.Parent = Me, .Text = "Zoom Out",
.Location = New Point(180, cBorder), .ForeColor = cForeClr, .BackColor = cBackClr}
Private WithEvents PathLabel As New Label With {.Parent = Me, .Font = New Font("tahoma", 10),
.Location = New Point(270, cBorder), .ForeColor = cForeClr, .BackColor = Color.Transparent, .AutoSize = True}
Private WithEvents Chart1 As New DataVisualization.Charting.Chart With {.Parent = Me,
.Location = New Point(cBorder, 50), .Size = New Size(200, 150)}
Private WithEvents Chart2 As New DataVisualization.Charting.Chart With {.Parent = Me,
.Location = New Point(cBorder, Chart1.Bottom), .Size = New Size(200, 150)}
Private SoundFilePath As String
Private ByteArray() As Byte
Private NumChannels As Integer
Private BytesPerSample As Integer
Private SampleRate As Double
Private CurrentPosition As Double
Private Sub Form5_Load(sender As Object, e As EventArgs) Handles MyBase.Load
ClientSize = New Size(500, 400)
BackColor = Color.DimGray
Text = "Draw Wav"
Chart1.ChartAreas.Add("Left")
Chart2.ChartAreas.Add("Right")
InitilizeChartAreas(Chart1)
InitilizeChartAreas(Chart2)
InitilizeData()
DrawChart()
End Sub
Private Sub Form5_Resize(sender As Object, e As EventArgs) Handles Me.Resize
Chart1.Width = ClientSize.Width - (2 * cBorder)
Chart2.Width = Chart1.Width
End Sub
Private Sub OpenBtn_Click(sender As Object, e As EventArgs) Handles OpenBtn.Click
Using d As New OpenFileDialog
d.ShowDialog()
If d.FileName <> "" Then
SoundFilePath = d.FileName
InitilizeData()
DrawChart()
End If
End Using
End Sub
Private Sub PlayFile(ByVal url As String)
Player = New WMPLib.WindowsMediaPlayer
Player.URL = url
Player.controls.play()
End Sub
Private Sub PlayBtn_Click(sender As Object, e As EventArgs) Handles PlayBtn.Click
'play the current wav
If ByteArray IsNot Nothing AndAlso ByteArray.Length > 0 Then
PlayFile(SoundFilePath)
Timer1.Start()
End If
End Sub
Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
If Player.controls.currentItem.duration > 0 Then
Dim t As Double = 1.25 * NumChannels * BytesPerSample * Player.controls.currentItem.duration
Dim r As Double = (ByteArray.Length - 144) / t
If Player.controls.currentPosition + 0.1 > Player.controls.currentItem.duration Then
Timer1.Stop()
CurrentPosition = GetIndexFromTimePoint(
Player.controls.currentItem.duration, SampleRate, BytesPerSample)
Else
CurrentPosition = GetIndexFromTimePoint(
Player.controls.currentPosition, SampleRate, BytesPerSample)
End If
Chart1.Invalidate()
Chart2.Invalidate()
End If
End Sub
'Private Function GetTimePointFromIndex(index As Integer, thisSampleRate As Integer, thisBitsPerSample As Short, thisChannelCount As Short) As TimeSpan
' Dim secondsPerSample As Double = 1 / thisSampleRate
' Dim sampleBypeCount As Integer = (thisBitsPerSample \ 8) * thisChannelCount
' index = CInt(Math.Floor(index / sampleBypeCount) * sampleBypeCount)
' Return TimeSpan.FromSeconds((sampleBypeCount * index) * secondsPerSample)
'End Function
Private Function GetIndexFromTimePoint(elapsedTime As Double, thisSampleRate As Double, thisBytesPerSample As Integer) As Integer
Dim i As Integer = -1
i = CInt(((256 / 10) * elapsedTime * thisSampleRate) / (thisBytesPerSample * thisBytesPerSample))
Return i
End Function
Private Sub ZoomResetBtn_Click(sender As Object, e As EventArgs) Handles ZoomResetBtn.Click
'reset zoom
Chart1.ChartAreas(0).AxisX.ScaleView.ZoomReset()
Chart2.ChartAreas(0).AxisX.ScaleView.ZoomReset()
DrawChart()
End Sub
Private Sub InitilizeData()
If SoundFilePath <> "" Then
'read the wav file and make the header data
Cursor = Cursors.WaitCursor
ByteArray = File.ReadAllBytes(SoundFilePath)
NumChannels = BitConverter.ToInt16(CType(ByteArray, Byte()), 22)
BytesPerSample = BitConverter.ToInt16(CType(ByteArray, Byte()), 34)
SampleRate = BitConverter.ToInt32(CType(ByteArray, Byte()), 24)
If NumChannels = 1 Then Chart2.Visible = False Else Chart2.Visible = True
PathLabel.Text = SoundFilePath & vbCrLf &
"Channels: " & NumChannels &
" Bits: " & BytesPerSample &
" Sample Rate: " & SampleRate
Form5_Resize(0, Nothing)
Cursor = Cursors.Default
End If
End Sub
Private Sub InitilizeChartAreas(thisChart As Chart)
thisChart.BackColor = Color.FromArgb(144, 144, 144)
thisChart.BackGradientStyle = System.Windows.Forms.DataVisualization.Charting.GradientStyle.TopBottom
thisChart.BackSecondaryColor = System.Drawing.Color.Black
thisChart.BorderlineColor = Color.FromArgb(26, 59, 105)
thisChart.BorderlineDashStyle = System.Windows.Forms.DataVisualization.Charting.ChartDashStyle.Solid
thisChart.BorderlineWidth = 2
thisChart.BorderSkin.SkinStyle = System.Windows.Forms.DataVisualization.Charting.BorderSkinStyle.Emboss
With thisChart.ChartAreas(0)
.AxisX.MajorGrid.LineColor = Color.DimGray
.AxisX.MajorGrid.LineDashStyle = ChartDashStyle.Dash
.AxisX.LabelStyle.ForeColor = cForeClr
.AxisX.Minimum = 0
.AxisX.Title = "Time (ms)"
.AxisX.TitleForeColor = cForeClr
.AxisY.MajorGrid.LineColor = Color.DimGray
.AxisY.MajorGrid.LineDashStyle = ChartDashStyle.Dash
.AxisY.LabelStyle.Enabled = False
.AxisY.IsMarginVisible = False
.AxisX.ScrollBar.Enabled = True
.AxisX.ScaleView.ZoomReset()
.CursorX.IsUserEnabled = True
.CursorX.IsUserSelectionEnabled = True
.AxisX.ScaleView.Zoomable = True
.BackColor = cBackClr
End With
End Sub
Private Sub DrawChart()
If ByteArray IsNot Nothing AndAlso ByteArray.Length > 0 Then
Dim x, y, thismax As Integer
Dim max As Integer = 400000
Chart1.Series.Clear()
Chart1.Series.Add("Channel 1")
Chart1.Series(0).ChartType = SeriesChartType.FastLine
Chart1.Series(0).Color = Color.LimeGreen
Chart2.Series.Clear()
Chart2.Series.Add("Channel 2")
Chart2.Series(0).ChartType = SeriesChartType.FastLine
Chart2.Series(0).Color = Color.LimeGreen
thismax = ByteArray.Length - 2
' If thismax > max Then thismax = max
Select Case NumChannels
Case 1 'mono
Select Case BytesPerSample
Case 16
For i As Integer = 44 To thismax Step 2
Chart1.Series(0).Points.AddXY(x, (256 * ByteArray(i)) + ByteArray(i + 1) - 32512)
x += 1
Next
Case 8
For i As Integer = 44 To thismax 'Step 10
Chart1.Series(0).Points.AddXY(i, ByteArray(i) - 127)
Next
Case Else
MessageBox.Show(BytesPerSample.ToString & " Bits Per Sample Mono is not supported.")
End Select
Case 2 'stereo
Select Case BytesPerSample
Case 16
For i As Integer = 44 To thismax Step 40
'left channel
y = BitConverter.ToInt16(CType(ByteArray, Byte()), i)
Chart1.Series("Channel 1").Points.AddXY(x, y)
'right channel
y = BitConverter.ToInt16(CType(ByteArray, Byte()), i + 2)
Chart2.Series("Channel 2").Points.AddXY(x, y)
x += 1
Next
Case Else
MessageBox.Show(BytesPerSample.ToString & " Bits Per Sample Stereo is not supported.")
End Select
End Select
End If
End Sub
Private Sub Chart1_PostPaint(sender As Object, e As ChartPaintEventArgs) Handles Chart1.PostPaint
DrawPostion(e.ChartGraphics, Chart1)
End Sub
Private Sub Chart2_PostPaint(sender As Object, e As ChartPaintEventArgs) Handles Chart2.PostPaint
DrawPostion(e.ChartGraphics, Chart2)
End Sub
Private Sub DrawPostion(g As ChartGraphics, thisChart As Chart)
If ByteArray IsNot Nothing AndAlso ByteArray.Length > 0 Then
With g
'get the coords in chart coords
Dim x As Single = CSng(.GetPositionFromAxis(thisChart.ChartAreas(0).Name, AxisName.X, CurrentPosition))
Dim y As Single = CSng(.GetPositionFromAxis(thisChart.ChartAreas(0).Name, AxisName.Y, 0))
'convert to model
Dim point1 As PointF = New PointF(x, y)
point1 = .GetAbsolutePoint(point1)
x = CSng(.GetPositionFromAxis(thisChart.ChartAreas(0).Name, AxisName.X, CurrentPosition))
Dim point2 As PointF = New PointF(x, 2 * y)
point2 = .GetAbsolutePoint(point2)
.Graphics.DrawLine(Pens.Red, point1.X, point2.Y, point1.X, -point2.Y)
End With
End If
End Sub
End Class
Continue reading...
This is a continuation of this thread where we covered the wav chart basics:
How to draw a WAV sound file Chart graph and get the header data.
I did some further testing and found that the chart control is much faster than drawing your own wav graph. I drew the wav graph with gdi+ on a bufferedgraphic and found the chart to be much faster.
So I further optimized the chart part by only plotting every 40th point in the byte array. This can vary to as much as /100 and you dont see much difference in the blondie rapture.wav. I also used the .fastline chart instead of .line for more speed.
The blondi rapture wav file is available here for a limited time.
Now I am trying to figure what the time increments are for the x axis.
You can see in the example that I have added a timer and each timer tick I get the playback position in seconds from media player and convert that to the actual chart coordinates. I found this code works ok for the blondi wav which is 16 bit stereo (mono not working):
Dim t As Double = 1.25 * NumChannels * BitsPerSample * Player.controls.currentItem.duration
Notice the 1.25 factor I needed to get the correct ratio to convert with. However I think with proper use of the header info the factor should not be needed. It seems the equation should be a simple ratio of something but I am not sure what?
Does anyone know how to calc what the actual time step is for each value in the wav bytearray?
Furthermore, the time scale shown is off x200 the actual time ie the blondi wav is about 8.4 secs so the x axis scale should be 8000 millisecs at the end not 40000. I need to make that correction somewhere.
Finally, I am using mediaplayer in this example v1 but maybe there is a better way? Like Media.SoundPlayer or mcisendstring?
Here is the example code for v1. The example makes all the controls all you have to do is cut and paster to an empty form.
The moving red line in the animation below is the play position when you play the wav. That's what I am talking about (runs smoother in real life).
PS I just realized part of the problem in the time scale in the example is I don't consider I am only plotting every 40th point. So need a x40 somewhere I guess.
Edit: v2 has different time function as described below.
'chart wav sound file with play postion v2
'lmb drag the chart area to zoom
Imports System.IO
Imports System.Windows.Forms.DataVisualization.Charting
Public Class Form6
Private cBackClr As Color = Color.Black
Private cForeClr As Color = Color.AntiqueWhite
Private cBorder As Integer = 10
Private WithEvents Player As WMPLib.WindowsMediaPlayer
Private WithEvents Timer1 As New Timer With {.Interval = 10}
Private WithEvents OpenBtn As New Button With {.Parent = Me, .Text = "Open",
.Location = New Point(20, cBorder), .ForeColor = cForeClr, .BackColor = cBackClr}
Private WithEvents PlayBtn As New Button With {.Parent = Me, .Text = "Play",
.Location = New Point(100, cBorder), .ForeColor = cForeClr, .BackColor = cBackClr}
Private WithEvents ZoomResetBtn As New Button With {.Parent = Me, .Text = "Zoom Out",
.Location = New Point(180, cBorder), .ForeColor = cForeClr, .BackColor = cBackClr}
Private WithEvents PathLabel As New Label With {.Parent = Me, .Font = New Font("tahoma", 10),
.Location = New Point(270, cBorder), .ForeColor = cForeClr, .BackColor = Color.Transparent, .AutoSize = True}
Private WithEvents Chart1 As New DataVisualization.Charting.Chart With {.Parent = Me,
.Location = New Point(cBorder, 50), .Size = New Size(200, 150)}
Private WithEvents Chart2 As New DataVisualization.Charting.Chart With {.Parent = Me,
.Location = New Point(cBorder, Chart1.Bottom), .Size = New Size(200, 150)}
Private SoundFilePath As String
Private ByteArray() As Byte
Private NumChannels As Integer
Private BytesPerSample As Integer
Private SampleRate As Double
Private CurrentPosition As Double
Private Sub Form5_Load(sender As Object, e As EventArgs) Handles MyBase.Load
ClientSize = New Size(500, 400)
BackColor = Color.DimGray
Text = "Draw Wav"
Chart1.ChartAreas.Add("Left")
Chart2.ChartAreas.Add("Right")
InitilizeChartAreas(Chart1)
InitilizeChartAreas(Chart2)
InitilizeData()
DrawChart()
End Sub
Private Sub Form5_Resize(sender As Object, e As EventArgs) Handles Me.Resize
Chart1.Width = ClientSize.Width - (2 * cBorder)
Chart2.Width = Chart1.Width
End Sub
Private Sub OpenBtn_Click(sender As Object, e As EventArgs) Handles OpenBtn.Click
Using d As New OpenFileDialog
d.ShowDialog()
If d.FileName <> "" Then
SoundFilePath = d.FileName
InitilizeData()
DrawChart()
End If
End Using
End Sub
Private Sub PlayFile(ByVal url As String)
Player = New WMPLib.WindowsMediaPlayer
Player.URL = url
Player.controls.play()
End Sub
Private Sub PlayBtn_Click(sender As Object, e As EventArgs) Handles PlayBtn.Click
'play the current wav
If ByteArray IsNot Nothing AndAlso ByteArray.Length > 0 Then
PlayFile(SoundFilePath)
Timer1.Start()
End If
End Sub
Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
If Player.controls.currentItem.duration > 0 Then
Dim t As Double = 1.25 * NumChannels * BytesPerSample * Player.controls.currentItem.duration
Dim r As Double = (ByteArray.Length - 144) / t
If Player.controls.currentPosition + 0.1 > Player.controls.currentItem.duration Then
Timer1.Stop()
CurrentPosition = GetIndexFromTimePoint(
Player.controls.currentItem.duration, SampleRate, BytesPerSample)
Else
CurrentPosition = GetIndexFromTimePoint(
Player.controls.currentPosition, SampleRate, BytesPerSample)
End If
Chart1.Invalidate()
Chart2.Invalidate()
End If
End Sub
'Private Function GetTimePointFromIndex(index As Integer, thisSampleRate As Integer, thisBitsPerSample As Short, thisChannelCount As Short) As TimeSpan
' Dim secondsPerSample As Double = 1 / thisSampleRate
' Dim sampleBypeCount As Integer = (thisBitsPerSample \ 8) * thisChannelCount
' index = CInt(Math.Floor(index / sampleBypeCount) * sampleBypeCount)
' Return TimeSpan.FromSeconds((sampleBypeCount * index) * secondsPerSample)
'End Function
Private Function GetIndexFromTimePoint(elapsedTime As Double, thisSampleRate As Double, thisBytesPerSample As Integer) As Integer
Dim i As Integer = -1
i = CInt(((256 / 10) * elapsedTime * thisSampleRate) / (thisBytesPerSample * thisBytesPerSample))
Return i
End Function
Private Sub ZoomResetBtn_Click(sender As Object, e As EventArgs) Handles ZoomResetBtn.Click
'reset zoom
Chart1.ChartAreas(0).AxisX.ScaleView.ZoomReset()
Chart2.ChartAreas(0).AxisX.ScaleView.ZoomReset()
DrawChart()
End Sub
Private Sub InitilizeData()
If SoundFilePath <> "" Then
'read the wav file and make the header data
Cursor = Cursors.WaitCursor
ByteArray = File.ReadAllBytes(SoundFilePath)
NumChannels = BitConverter.ToInt16(CType(ByteArray, Byte()), 22)
BytesPerSample = BitConverter.ToInt16(CType(ByteArray, Byte()), 34)
SampleRate = BitConverter.ToInt32(CType(ByteArray, Byte()), 24)
If NumChannels = 1 Then Chart2.Visible = False Else Chart2.Visible = True
PathLabel.Text = SoundFilePath & vbCrLf &
"Channels: " & NumChannels &
" Bits: " & BytesPerSample &
" Sample Rate: " & SampleRate
Form5_Resize(0, Nothing)
Cursor = Cursors.Default
End If
End Sub
Private Sub InitilizeChartAreas(thisChart As Chart)
thisChart.BackColor = Color.FromArgb(144, 144, 144)
thisChart.BackGradientStyle = System.Windows.Forms.DataVisualization.Charting.GradientStyle.TopBottom
thisChart.BackSecondaryColor = System.Drawing.Color.Black
thisChart.BorderlineColor = Color.FromArgb(26, 59, 105)
thisChart.BorderlineDashStyle = System.Windows.Forms.DataVisualization.Charting.ChartDashStyle.Solid
thisChart.BorderlineWidth = 2
thisChart.BorderSkin.SkinStyle = System.Windows.Forms.DataVisualization.Charting.BorderSkinStyle.Emboss
With thisChart.ChartAreas(0)
.AxisX.MajorGrid.LineColor = Color.DimGray
.AxisX.MajorGrid.LineDashStyle = ChartDashStyle.Dash
.AxisX.LabelStyle.ForeColor = cForeClr
.AxisX.Minimum = 0
.AxisX.Title = "Time (ms)"
.AxisX.TitleForeColor = cForeClr
.AxisY.MajorGrid.LineColor = Color.DimGray
.AxisY.MajorGrid.LineDashStyle = ChartDashStyle.Dash
.AxisY.LabelStyle.Enabled = False
.AxisY.IsMarginVisible = False
.AxisX.ScrollBar.Enabled = True
.AxisX.ScaleView.ZoomReset()
.CursorX.IsUserEnabled = True
.CursorX.IsUserSelectionEnabled = True
.AxisX.ScaleView.Zoomable = True
.BackColor = cBackClr
End With
End Sub
Private Sub DrawChart()
If ByteArray IsNot Nothing AndAlso ByteArray.Length > 0 Then
Dim x, y, thismax As Integer
Dim max As Integer = 400000
Chart1.Series.Clear()
Chart1.Series.Add("Channel 1")
Chart1.Series(0).ChartType = SeriesChartType.FastLine
Chart1.Series(0).Color = Color.LimeGreen
Chart2.Series.Clear()
Chart2.Series.Add("Channel 2")
Chart2.Series(0).ChartType = SeriesChartType.FastLine
Chart2.Series(0).Color = Color.LimeGreen
thismax = ByteArray.Length - 2
' If thismax > max Then thismax = max
Select Case NumChannels
Case 1 'mono
Select Case BytesPerSample
Case 16
For i As Integer = 44 To thismax Step 2
Chart1.Series(0).Points.AddXY(x, (256 * ByteArray(i)) + ByteArray(i + 1) - 32512)
x += 1
Next
Case 8
For i As Integer = 44 To thismax 'Step 10
Chart1.Series(0).Points.AddXY(i, ByteArray(i) - 127)
Next
Case Else
MessageBox.Show(BytesPerSample.ToString & " Bits Per Sample Mono is not supported.")
End Select
Case 2 'stereo
Select Case BytesPerSample
Case 16
For i As Integer = 44 To thismax Step 40
'left channel
y = BitConverter.ToInt16(CType(ByteArray, Byte()), i)
Chart1.Series("Channel 1").Points.AddXY(x, y)
'right channel
y = BitConverter.ToInt16(CType(ByteArray, Byte()), i + 2)
Chart2.Series("Channel 2").Points.AddXY(x, y)
x += 1
Next
Case Else
MessageBox.Show(BytesPerSample.ToString & " Bits Per Sample Stereo is not supported.")
End Select
End Select
End If
End Sub
Private Sub Chart1_PostPaint(sender As Object, e As ChartPaintEventArgs) Handles Chart1.PostPaint
DrawPostion(e.ChartGraphics, Chart1)
End Sub
Private Sub Chart2_PostPaint(sender As Object, e As ChartPaintEventArgs) Handles Chart2.PostPaint
DrawPostion(e.ChartGraphics, Chart2)
End Sub
Private Sub DrawPostion(g As ChartGraphics, thisChart As Chart)
If ByteArray IsNot Nothing AndAlso ByteArray.Length > 0 Then
With g
'get the coords in chart coords
Dim x As Single = CSng(.GetPositionFromAxis(thisChart.ChartAreas(0).Name, AxisName.X, CurrentPosition))
Dim y As Single = CSng(.GetPositionFromAxis(thisChart.ChartAreas(0).Name, AxisName.Y, 0))
'convert to model
Dim point1 As PointF = New PointF(x, y)
point1 = .GetAbsolutePoint(point1)
x = CSng(.GetPositionFromAxis(thisChart.ChartAreas(0).Name, AxisName.X, CurrentPosition))
Dim point2 As PointF = New PointF(x, 2 * y)
point2 = .GetAbsolutePoint(point2)
.Graphics.DrawLine(Pens.Red, point1.X, point2.Y, point1.X, -point2.Y)
End With
End If
End Sub
End Class
Continue reading...