'****************************************************************************** '* SignalViewer * '* * '* This class implements a custom control to show the spectrum of an audio * '* signal, or the signal itself (in oscilloscope mode). * '* * '* It uses the FFTCalculator class, included in the FFT_wrapper project, * '* wich is an adaptation of the open-source library fftw (www.fftw.org) * '* * '* * '* EQUITEL - C.V. February 2007 ALMDI * '****************************************************************************** Imports System.ComponentModel Imports System.Math Imports FFTw_wrapper Public Class SignalViewer #Region "Fields" Private mNumSignalSamples As Integer = 512 Private mLineColor As Color = Color.Yellow Private mLineWidth As Integer = 1 Private mLinePen As System.Drawing.Pen Private mGridLineColor As Color = Color.DarkGray Private mGridLineWidth As Integer = 1 Private mGridLinePen As System.Drawing.Pen Private mReferencePower As Double = 500000 Private mScaleY As Double = 10 Private mReferenceLevel As Double = 0 Private mNumberHorDiv As Integer = 10 Private mNumberVertDiv As Integer = 10 Private VerticalTextMargin As Integer = 50 Private HorizontalTextMargin As Integer = 20 Private mTextFont As System.Drawing.Font = New System.Drawing.Font(FontFamily.GenericSansSerif, 8, FontStyle.Regular) Private mTextColor As Color = Color.White Private mFFTCalculator As FFTCalculator Enum Modes SpectrumAnalyzer Oscilloscope End Enum Private mMode As Modes = Modes.SpectrumAnalyzer Private mLogarithmicScale As Boolean = True Private mMaxLinearScale As Double = 0.5 Private mMaxOscilloscopeScale As Double = 1 Private Signal As Short() Private SignalPointer As Integer = 0 Private BuffOut As Double() Private Intialized As Boolean = False Private MustRedraw As Boolean = False #End Region #Region "Properties" _ Public Property NumSignalSamples() As Integer Get Return mNumSignalSamples End Get Set(ByVal value As Integer) Dim L As Double L = Log(value, 2) If Int(L) = L Then mNumSignalSamples = value End If End Set End Property _ Public Property LineColor() As Color Get Return mLineColor End Get Set(ByVal Value As Color) mLineColor = Value Me.Invalidate() End Set End Property _ Public Property LineWidth() As Integer Get Return mLineWidth End Get Set(ByVal Value As Integer) If Value > 0 AndAlso Value < 10 Then mLineWidth = Value Me.Invalidate() End If End Set End Property _ Public Property GridLineColor() As Color Get Return mGridLineColor End Get Set(ByVal Value As Color) mGridLineColor = Value Me.Invalidate() End Set End Property _ Public Property GridLineWidth() As Integer Get Return mGridLineWidth End Get Set(ByVal Value As Integer) If Value > 0 AndAlso Value < 10 Then mGridLineWidth = Value Me.Invalidate() End If End Set End Property _ Public Property NumberHorDiv() As Integer Get Return mNumberHorDiv End Get Set(ByVal value As Integer) If value > 0 AndAlso value < 51 Then mNumberHorDiv = value End If End Set End Property _ Public Property NumberVertDiv() As Integer Get Return mNumberVertDiv End Get Set(ByVal value As Integer) If value > 0 AndAlso value < 51 Then mNumberVertDiv = value End If End Set End Property _ Public Property TextFont() As System.Drawing.Font Get Return mTextFont End Get Set(ByVal value As System.Drawing.Font) mTextFont = value End Set End Property _ Public Property TextColor() As Color Get Return mTextColor End Get Set(ByVal value As System.Drawing.Color) mTextColor = value End Set End Property _ Public Property ReferencePower() As Double Get Return mReferencePower End Get Set(ByVal value As Double) mReferencePower = value End Set End Property _ Public Property ScaleY() As Double Get Return mScaleY End Get Set(ByVal value As Double) mScaleY = value End Set End Property _ Public Property ReferenceLevel() As Double Get Return mReferenceLevel End Get Set(ByVal value As Double) mReferenceLevel = value End Set End Property _ Public Property LogarihmicScale() As Boolean Get Return mLogarithmicScale End Get Set(ByVal value As Boolean) mLogarithmicScale = value End Set End Property _ Public Property Mode() As Modes Get Return mMode End Get Set(ByVal value As Modes) mMode = value End Set End Property #End Region #Region "Private functions" Private Sub SignalViewer_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load BtnOscilloscopeMode.Checked = (mMode = Modes.Oscilloscope) BtnAnalyzerMode.Checked = (mMode = Modes.SpectrumAnalyzer) BtnLogarithmic.Checked = LogarihmicScale BtnLinear.Checked = Not BtnLogarithmic.Checked ShowLogarithmicVerticalScale() End Sub Private Function LogarithmicAnalyzerGrid() As Bitmap 'Returns a bitmap representing the grid for the control when in 'logarithmic analyzer mode Dim i As Integer Dim P1 As System.Drawing.Point Dim P2 As System.Drawing.Point Static LocalBitmap As Bitmap Dim g As System.Drawing.Graphics Dim b As System.Drawing.SolidBrush Static H As Integer = 0 Static W As Integer = 0 Dim TextSize As Size Dim P As Single Dim S As String If W <> PBOutput.Width OrElse H <> PBOutput.Height OrElse MustRedraw Then W = PBOutput.Width H = PBOutput.Height LocalBitmap = New Bitmap(W, H) g = Graphics.FromImage(LocalBitmap) g.Clear(Me.BackColor) 'Pintamos la rejilla en el buffer interno 'Lineas horizontales b = New System.Drawing.SolidBrush(mTextColor) TextSize = TextRenderer.MeasureText("-000dB", mTextFont) VerticalTextMargin = TextSize.Width * 1.05 HorizontalTextMargin = TextSize.Height * 1.3 For i = 0 To mNumberVertDiv P1.X = VerticalTextMargin P1.Y = i * ((H - HorizontalTextMargin) / mNumberVertDiv) P2.X = W P2.Y = P1.Y g.DrawLine(mGridLinePen, P1, P2) P = mReferenceLevel - i * mScaleY S = Format(P, "##0dB") TextSize = TextRenderer.MeasureText(S, mTextFont) g.DrawString(S, mTextFont, b, VerticalTextMargin * 0.9 - TextSize.Width, P1.Y - TextSize.Height / 2) Next 'Lineas verticales For i = 0 To mNumberHorDiv P1.X = VerticalTextMargin + i * ((W - VerticalTextMargin) / mNumberHorDiv) P1.Y = 0 P2.X = P1.X P2.Y = H - HorizontalTextMargin g.DrawLine(mGridLinePen, P1, P2) P = i * (4 / mNumberHorDiv) S = Format(P, "0.0kHz") TextSize = TextRenderer.MeasureText(S, mTextFont) g.DrawString(S, mTextFont, b, P1.X - TextSize.Width / 2, P2.Y + 1) Next End If MustRedraw = False Return LocalBitmap End Function Private Function LinearAnalyzerGrid() As Bitmap 'Returns a bitmap representing the grid for the control when in 'linear analyzer mode Dim i As Integer Dim P1 As System.Drawing.Point Dim P2 As System.Drawing.Point Static LocalBitmap As Bitmap Dim g As System.Drawing.Graphics Dim b As System.Drawing.SolidBrush Static H As Integer = 0 Static W As Integer = 0 Dim TextSize As Size Dim P As Single Dim S As String If W <> PBOutput.Width OrElse H <> PBOutput.Height OrElse MustRedraw Then W = PBOutput.Width H = PBOutput.Height LocalBitmap = New Bitmap(W, H) g = Graphics.FromImage(LocalBitmap) g.Clear(Me.BackColor) 'Pintamos la rejilla en el buffer interno 'Lineas horizontales b = New System.Drawing.SolidBrush(mTextColor) TextSize = TextRenderer.MeasureText("0.00", mTextFont) VerticalTextMargin = TextSize.Width * 1.05 HorizontalTextMargin = TextSize.Height * 1.3 For i = 0 To mNumberVertDiv P1.X = VerticalTextMargin P1.Y = i * ((H - HorizontalTextMargin) / mNumberVertDiv) P2.X = W P2.Y = P1.Y g.DrawLine(mGridLinePen, P1, P2) P = mMaxLinearScale * (1 - (i / mNumberVertDiv)) S = Format(P, "0.00") TextSize = TextRenderer.MeasureText(S, mTextFont) g.DrawString(S, mTextFont, b, VerticalTextMargin * 0.9 - TextSize.Width, P1.Y - TextSize.Height / 2) Next 'Lineas verticales For i = 0 To mNumberHorDiv P1.X = VerticalTextMargin + i * ((W - VerticalTextMargin) / mNumberHorDiv) P1.Y = 0 P2.X = P1.X P2.Y = H - HorizontalTextMargin g.DrawLine(mGridLinePen, P1, P2) P = i * (4 / mNumberHorDiv) S = Format(P, "0.0kHz") TextSize = TextRenderer.MeasureText(S, mTextFont) g.DrawString(S, mTextFont, b, P1.X - TextSize.Width / 2, P2.Y + 1) Next End If MustRedraw = False Return LocalBitmap End Function Private Function OscilloscopeGrid() As Bitmap 'Returns a bitmap representing the grid for the control when in 'oscilloscope mode Dim i As Integer Dim P1 As System.Drawing.Point Dim P2 As System.Drawing.Point Static LocalBitmap As Bitmap Dim g As System.Drawing.Graphics Dim b As System.Drawing.SolidBrush Static H As Integer = 0 Static W As Integer = 0 Dim TextSize As Size Dim P As Single Dim S As String If W <> PBOutput.Width OrElse H <> PBOutput.Height OrElse MustRedraw Then W = PBOutput.Width H = PBOutput.Height LocalBitmap = New Bitmap(W, H) g = Graphics.FromImage(LocalBitmap) g.Clear(Me.BackColor) 'Pintamos la rejilla en el buffer interno 'Lineas horizontales b = New System.Drawing.SolidBrush(mTextColor) TextSize = TextRenderer.MeasureText("-0.00", mTextFont) VerticalTextMargin = TextSize.Width * 1.05 HorizontalTextMargin = TextSize.Height * 1.3 For i = 0 To mNumberVertDiv P1.X = VerticalTextMargin P1.Y = i * ((H - HorizontalTextMargin) / (mNumberVertDiv)) P2.X = Width P2.Y = P1.Y g.DrawLine(mGridLinePen, P1, P2) P = mMaxOscilloscopeScale * (1 - (i / (mNumberVertDiv / 2))) S = Format(P, "#0.00") TextSize = TextRenderer.MeasureText(S, mTextFont) g.DrawString(S, mTextFont, b, VerticalTextMargin * 0.9 - TextSize.Width, P1.Y - TextSize.Height / 2) Next 'Linea central P1.X = VerticalTextMargin P1.Y = (H - HorizontalTextMargin) / 2 P2.X = Width P2.Y = P1.Y g.DrawLine(mGridLinePen, P1, P2) S = Format(0, "#0.00") TextSize = TextRenderer.MeasureText(S, mTextFont) g.DrawString(S, mTextFont, b, VerticalTextMargin * 0.9 - TextSize.Width, P1.Y - TextSize.Height / 2) 'Lineas verticales For i = 0 To mNumberHorDiv P1.X = VerticalTextMargin + i * ((W - VerticalTextMargin) / mNumberHorDiv) P1.Y = 0 P2.X = P1.X P2.Y = H - HorizontalTextMargin g.DrawLine(mGridLinePen, P1, P2) P = i * 0.125 * (mNumSignalSamples / mNumberHorDiv) S = Format(P, "00.0 ms") TextSize = TextRenderer.MeasureText(S, mTextFont) g.DrawString(S, mTextFont, b, P1.X - TextSize.Width / 2, P2.Y + 1) Next End If MustRedraw = False Return LocalBitmap End Function Private Sub DrawSpectrum() 'Draws the spectrum of the stored signal Dim i As Integer Dim LocalBitmap As Bitmap Dim g1 As System.Drawing.Graphics Dim g2 As System.Drawing.Graphics Dim P((mNumSignalSamples / 2) - 1) As System.Drawing.Point Dim XScale As Double Dim YScale As Double Dim Power As Double Static H As Integer = 0 Static W As Integer = 0 'The control must be intialized first If Not Intialized Then Return 'If the control has changed its size, don't draw anything. This is done 'to not waste too much time while resizing the control because it is 'necessary to recalculate the grid If W = PBOutput.Width - VerticalTextMargin AndAlso H = PBOutput.Height - HorizontalTextMargin Then 'FFT calculation 'mFFTCalculator returns in Buffout the result of the FFT for the signal 'we've passed it. mFFTCalculator.Calculate() 'Scale calculation XScale = W / (mNumSignalSamples / 2) YScale = H / (mNumberVertDiv * mScaleY) 'Graphic calculation For i = 0 To (mNumSignalSamples / 2) - 1 Power = BuffOut(i) If Power = 0D OrElse Double.IsNaN(Power) Then 'This is to prevent log(0) errors Power = 1.0E-200 End If If mLogarithmicScale Then P(i).Y = (mReferenceLevel - (10 * Log(Power / mReferencePower))) * YScale If P(i).Y > H Then P(i).Y = H Else P(i).Y = H - (Power * H / mMaxLinearScale) End If P(i).X = i * XScale + VerticalTextMargin Next Else H = PBOutput.Height - HorizontalTextMargin W = PBOutput.Width - VerticalTextMargin End If 'Grid drawing. If mLogarithmicScale Then LocalBitmap = LogarithmicAnalyzerGrid().Clone Else LocalBitmap = LinearAnalyzerGrid().Clone End If 'We create two graphic buffers. One for drawing and other referenced to 'the control we must draw. g1 = Graphics.FromImage(LocalBitmap) g2 = PBOutput.CreateGraphics() 'Graphic drawing g1.DrawLines(mLinePen, P) 'Control updtating: is the actual drawing of the complete graphic. g2.DrawImage(LocalBitmap, 0, 0) End Sub Private Sub DrawSignal() 'Drawing of the stored signal Dim i As Integer Dim LocalBitmap As Bitmap Dim g1 As System.Drawing.Graphics Dim g2 As System.Drawing.Graphics Dim P(mNumSignalSamples - 1) As System.Drawing.Point Dim EscalaX As Double Dim EscalaY As Double Static H As Integer = 0 Static W As Integer = 0 'If the control has changed its size, don't draw anything. This is done 'to not waste too much time while resizing the control because it is 'necessary to recalculate the grid If Not Intialized Then Return If W = PBOutput.Width - VerticalTextMargin AndAlso H = PBOutput.Height - HorizontalTextMargin Then EscalaX = W / mNumSignalSamples EscalaY = 32768 * mMaxOscilloscopeScale 'We calculate the graphic For i = 0 To mNumSignalSamples - 1 P(i).Y = (H / 2) * (1 - Signal(i) / EscalaY) P(i).X = i * EscalaX + VerticalTextMargin Next Else H = PBOutput.Height - HorizontalTextMargin W = PBOutput.Width - VerticalTextMargin End If 'Grid drawing LocalBitmap = OscilloscopeGrid().Clone 'We create two graphic buffers. One for drawing and other referenced to 'the control we must draw. g1 = Graphics.FromImage(LocalBitmap) g2 = PBOutput.CreateGraphics() 'Graphic drawing g1.DrawLines(mLinePen, P) 'Control updtating: is the actual drawing of the complete graphic. g2.DrawImage(LocalBitmap, 0, 0) End Sub #End Region #Region "Public functions" Public Sub Initialize() 'This routine must be called before using the control. 'We create two buffers: one for the stored signal and other for the 'FFT result. Then we create a FFTCalculator object with reference to 'these two buffers. Dim i As Integer ReDim Signal(mNumSignalSamples - 1) ReDim BuffOut(mNumSignalSamples / 2) For i = 0 To mNumSignalSamples - 1 Signal(i) = 0 Next mFFTCalculator = New FFTCalculator(mNumSignalSamples, Signal, BuffOut) 'We create pen objects for the drawing. So, the colours and line widths 'may not be changed at runtime. (This must be changed if necessary...) mLinePen = New System.Drawing.Pen(mLineColor, mLineWidth) mGridLinePen = New System.Drawing.Pen(GridLineColor, GridLineWidth) mLinePen.LineJoin = Drawing2D.LineJoin.Round Intialized = True End Sub Public Sub NewAudioPacket(ByRef Packet As Short(), ByVal Length As Integer) 'This sub must be called every time a new fragment of the signal to analyze is ready. 'The signal is passed as an 16bit signed integer array. 'When the internal buffer is full, automatically starts the graphic drawing. Dim i As Integer If (mNumSignalSamples - SignalPointer) > Length Then For i = 0 To Length - 1 Signal(i + SignalPointer) = Packet(i) Next SignalPointer += Length Else For i = 0 To mNumSignalSamples - SignalPointer - 1 Signal(i + SignalPointer) = Packet(i) Next SignalPointer = 0 Select Case mMode Case Modes.SpectrumAnalyzer DrawSpectrum() Case Modes.Oscilloscope DrawSignal() End Select End If End Sub #End Region #Region "Toolbar" Private Sub ShowLogarithmicVerticalScale() LlbVerticalRef.Text = Format(ReferenceLevel, "00.00") + " dB" LblVerticalScale.Text = Format(ScaleY, "00") + " dB/div" End Sub Private Sub ShowLinearVerticalScale() LblVerticalScale.Text = Format(mMaxLinearScale, "0.00") End Sub Private Sub ShowOscilloscopeVerticalScale() LblVerticalScale.Text = Format(mMaxOscilloscopeScale, "0.00") End Sub Private Sub BtnAnalyzerMode_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnAnalyzerMode.Click BtnOscilloscopeMode.Checked = False BtnAnalyzerMode.Checked = True mMode = Modes.SpectrumAnalyzer LogarihmicScale = True BtnLinear.Visible = True BtnLinear.Checked = False BtnLogarithmic.Visible = True BtnLogarithmic.Checked = True LlbVerticalRef.Visible = True BtnVerticalRefDec.Visible = True BtnVerticalRefInc.Visible = True ShowLogarithmicVerticalScale() ToolStrip1.Refresh() MustRedraw = True End Sub Private Sub BtnOscilloscopeMode_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnOscilloscopeMode.Click BtnOscilloscopeMode.Checked = True BtnAnalyzerMode.Checked = False mMode = Modes.Oscilloscope BtnLinear.Visible = False BtnLogarithmic.Visible = False LlbVerticalRef.Visible = False BtnVerticalRefDec.Visible = False BtnVerticalRefInc.Visible = False ShowOscilloscopeVerticalScale() ToolStrip1.Refresh() MustRedraw = True End Sub Private Sub BtnLinear_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnLinear.Click LogarihmicScale = False BtnLinear.Checked = True BtnLogarithmic.Checked = False LlbVerticalRef.Visible = False BtnVerticalRefDec.Visible = False BtnVerticalRefInc.Visible = False ShowLinearVerticalScale() ToolStrip1.Refresh() MustRedraw = True End Sub Private Sub BtnLogarithmic_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnLogarithmic.Click LogarihmicScale = True BtnLinear.Checked = False BtnLogarithmic.Checked = True LlbVerticalRef.Visible = True BtnVerticalRefDec.Visible = True BtnVerticalRefInc.Visible = True ShowLogarithmicVerticalScale() ToolStrip1.Refresh() MustRedraw = True End Sub Private Sub BtnVerticalRefDec_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnVerticalRefDec.Click If ReferenceLevel > -50 Then ReferenceLevel -= 5 ShowLogarithmicVerticalScale() MustRedraw = True End Sub Private Sub BtnVerticalRefInc_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnVerticalRefInc.Click If ReferenceLevel < 0 Then ReferenceLevel += 5 ShowLogarithmicVerticalScale() MustRedraw = True End Sub Private Sub BtnVerticalScaleDec_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnVerticalScaleDec.Click If mMode = Modes.SpectrumAnalyzer Then If mLogarithmicScale Then If ScaleY = 5 Then ScaleY = 1 If ScaleY > 5 Then ScaleY -= 5 ShowLogarithmicVerticalScale() Else If mMaxLinearScale > 0.06 Then mMaxLinearScale -= 0.05 ShowLinearVerticalScale() End If Else If mMaxOscilloscopeScale > 0.06 Then mMaxOscilloscopeScale -= 0.05 ShowOscilloscopeVerticalScale() End If MustRedraw = True End Sub Private Sub BtnVerticalScaleInc_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnVerticalScaleInc.Click If mMode = Modes.SpectrumAnalyzer Then If mLogarithmicScale Then If ScaleY = 1 Then ScaleY = 5 Else If ScaleY < 25 Then ScaleY += 5 End If ShowLogarithmicVerticalScale() Else If mMaxLinearScale < 1 Then mMaxLinearScale += 0.05 ShowLinearVerticalScale() End If Else If mMaxOscilloscopeScale < 1 Then mMaxOscilloscopeScale += 0.05 ShowOscilloscopeVerticalScale() End If MustRedraw = True End Sub #End Region End Class