用于生成和显示数字信号的 C# 库






4.91/5 (12投票s)
生成和显示 Windows Forms 中各种数字信号的 C# 库
引言
数字信号通常用于压缩数据。在某个范围内,将模拟信号的所有值存储在某些介质上实际上是不可能的(考虑到介于两个实数之间的数字的数量是无限的,无论它们有多接近)。相反,我们会在固定的时间间隔内采样,然后存储以供进一步处理。生成和显示数字信号的 C# 库将按照其组成数据结构和函数的需求顺序进行描述。附带的 ZIP 文件包含所有代码。
时间和幅度点
数字信号只是时间-幅度点的序列,它们在时间(水平)维度上等距分布。下面的类将用于跟踪数据点。
/// <summary>Class to encapsulate one data point of a digital signal.
/// </summary>
public class TimeMagnitude
{
public double Time, // Time (in seconds) of signal magnitude generation.
Magnitude; // Signal magnitude in volts.
public TimeMagnitude( double time, double magnitude )
{
Time = time; Magnitude = magnitude;
}
public TimeMagnitude Copy()
{
return new TimeMagnitude( this.Time, this.Magnitude );
}// Copy
}// TimeMagnitude (class)
在某些应用程序中,有必要创建现有 TimeMagnitude
实例的新副本。这就是 Copy
函数的目的。
信号参数
为了生成数字信号,必须假设在固定时间间隔内对相应模拟信号的幅度进行采样。必须指定几个参数才能正确生成信号,如下面的类所定义。大多数参数是不言而喻的,有些参数(如频率和周期)是冗余的,因为如果已知其中一个,则可以从另一个计算出来。samplingFactor
参数对于正确生成数字信号数据点以供进一步处理至关重要。
/// <summary>Class to encapsulate the parameters of a digital signal.
/// </summary>
public class SignalParameters
{
public double amplitude, offset, // Signal amplitude and DC offset in volts.
frequency, // Signal frequency in Hertz.
period, // Signal period in seconds.
halfPeriod, // Half of the signal period.
samplingFrequency, // Frequency (in Hertz) for signal generation.
frequencyResolution, // Spacing (in Hertz) between adjacent signal values.
timeStep; // Spacing (in seconds) between two adjacent time points.
public int samplingFactor, // Factor to multiply by the signal frequency.
nSamples; // Number of signal "samples" to be generated.
/// <summary>Create an instance of signal parameters.
/// </summary>
/// <param name="_amplitude">Amplitude of the signal in volts.</param>
/// <param name="_frequency">Frequency of the signal in hertz (cycles/second).</param>
/// <param name="_offset">DC offset (in volts) to be added to the amplitude.</param>
/// <param name="_nSamples">Number of "samples" to be generated for the signal.</param>
/// <param name="_samplingFactor">Factor to multiply by {_frequency} for signal-processing.
/// </param>
public SignalParameters( double _amplitude, double _frequency, // Mandatory.
double _offset, int _nSamples, int _samplingFactor ) // Optional.
{
double one = (double)1.0;
amplitude =_amplitude;
frequency = _frequency;
samplingFactor = _samplingFactor;
nSamples = _nSamples;
offset = _offset;
period = one / frequency;
halfPeriod = period / (double)2.0;
samplingFrequency = (double)samplingFactor * frequency;
frequencyResolution = samplingFrequency / (double)nSamples;
timeStep = one / samplingFrequency;
}
}// SignalParameters (class)
数字信号
生成数字信号涉及模拟在等距时间点上对其相应模拟信号幅度的采样。为了生成任意数量的连续数据点,可以将信号生成函数实现为枚举器,它们在调用之间保持其状态。要生成的信号是正弦波、余弦波、方波、锯齿波、三角波和白噪声。
数字信号由 GeneratingFn
类中的函数成员生成。该类的数据成员如下
public SignalParameters parameters; // Parameters of a signal to be generated.
public List<TimeMagnitude> sineSignalValues, cosineSignalValues,
squareSignalValues, sawtoothSignalValues,
triangleSignalValues, whiteNoiseValues;
public List<double> zeroCrossings; // Signal crossings on the time (X) axis.
private SignalPlot sineSignal, cosineSignal, squareSignal,
sawtoothSignal, triangleSignal, whiteNoise;
构造函数创建一个类实例,初始化信号参数、将包含信号值的列表、零交叉点的列表以及信号图。
/// <summary>Create an instance of a signal-generating function.
/// </summary>
/// <param name="_amplitude">Amplitude of the signal in Volts.</param>
/// <param name="_frequency">Frequency of the signal in Hertz (cycles/second).</param>
/// <param name="_offset">DC offset to be added to the magnitude of the signal.</param>
/// <param name="_nSamples">Number of samples to generate for signal-processing.</param>
/// <param name="_samplingFactor">Factor to multiply by the frequency.
/// </param>
public GeneratingFn( double _amplitude, double _frequency, double _offset = 0.0,
int _nSamples = 512, int _samplingFactor = 32 )
{
parameters = new SignalParameters( _amplitude, _frequency, // Mandatory
// arguments.
_offset, _nSamples, _samplingFactor ); // Optional
// arguments.
sineSignalValues = new List<TimeMagnitude>();
cosineSignalValues = new List<TimeMagnitude>();
squareSignalValues = new List<TimeMagnitude>();
sawtoothSignalValues = new List<TimeMagnitude>();
triangleSignalValues = new List<TimeMagnitude>();
whiteNoiseValues = new List<TimeMagnitude>();
sineSignal = new SignalPlot( "sine-signal", SignalShape.sine );
sineSignal.Text = "Sine signal plot";
cosineSignal = new SignalPlot( "cosine-signal", SignalShape.cosine );
cosineSignal.Text = "Cosine signal plot";
squareSignal = new SignalPlot( "square-signal", SignalShape.square,
SignalContinuity.discontinuous );
squareSignal.Text = "Square signal plot";
sawtoothSignal = new SignalPlot( "sawtooth-signal", SignalShape.sawtooth,
SignalContinuity.discontinuous );
sawtoothSignal.Text = "Sawtooth signal plot";
triangleSignal = new SignalPlot( "triangle-signal", SignalShape.triangle );
triangleSignal.Text = "Triangle signal plot";
whiteNoise = new SignalPlot( "white-noise signal", SignalShape.whiteNoise );
whiteNoise.Text = "White noise plot";
}// GeneratingFn
在某些应用程序中,方便地从 TimeMagnitude
元素的列表中收集幅度,这可以通过以下方式简单地完成
/// <summary>Collect the magnitudes from a list of {TimeMagnitude} elements.
/// </summary>
/// <param name="tmList">List of (time, magnitude) elements.</param>
/// <returns>List of magnitudes.
/// </returns>
public double[] Magnitudes( List<TimeMagnitude> tmList )
{
double[] mags = null;
if ( tmList != null )
{
int n = tmList.Count;
mags = new double[ n ];
for ( int i = 0; i < n; ++i )
{
mags[ i ] = tmList[ i ].Magnitude;
}
}
return mags;
}// Magnitudes
GeneratingFn
构造函数及其 public
函数成员由驱动程序或用户程序调用。稍后将在本文中通过测试控制台应用程序来说明这一点。信号生成函数会重复调用任意次数。每次调用它们时,函数都会创建一个新的 TimeMagnitude
元素,将其添加到相应的列表中,并返回该元素的双重幅度。
正弦波和余弦波信号
下面的枚举器用于生成正弦信号的元素。
/// <summary>Generate the next sine-signal value.
/// </summary>
/// <returns>Current magnitude of the sine signal.
/// </returns>
public IEnumerator<double> NextSineSignalValue()
{
double angularFreq = 2.0 * Math.PI * parameters.frequency; // w == 2 * pi * f
double time = 0.0;
double wt, sinOFwt, magnitude = 0.0;
TimeMagnitude previousTM = null;
zeroCrossings = new List<double>();
while ( true )
{
try
{
wt = angularFreq * time;
sinOFwt = Math.Sin( wt );
magnitude = parameters.offset + ( parameters.amplitude * sinOFwt );
TimeMagnitude tm = new TimeMagnitude( time, magnitude );
sineSignalValues.Add( tm );
CheckZeroCrossing( previousTM, tm );
previousTM = tm.Copy();
}
catch ( Exception exc )
{
MessageBox.Show( exc.Message );
Abort();
}
yield return magnitude;
time += parameters.timeStep;
}
}// NextSineSignalValue
该函数被定义为 IEnumerator
。第一次调用时,它会初始化其局部变量,然后进入无限循环。如果 try
-catch
子句中的一切都按预期进行,则会更新 sineSignalValues
列表,并调用 CheckZeroCrossing
函数来确定信号是否已穿过时间轴。
/// <summary>If the {magnitude} of the current {TimeMagnitude} element is near 0.0,
/// or if there is a magnitude transition through the time axis from the
/// previous {TimeMagnitude} element to the current {TimeMagnitude}
/// element, then update the {zeroCrossings} list.
/// </summary>
/// <param name="previousTM">Previous {TimeMagnitude} element.</param>
/// <param name="tm">Current {TimeMagnitude} element.
/// </param>
private void CheckZeroCrossing( TimeMagnitude previousTM, TimeMagnitude tm )
{
if ( UtilFn.NearZero( tm.Magnitude ) )
{
zeroCrossings.Add( tm.Time );
}
else if ( previousTM != null && MagnitudeTransition( previousTM, tm ) )
{
zeroCrossings.Add( previousTM.Time + ( ( tm.Time - previousTM.Time ) / 2.0 ) );
}
}// CheckZeroCrossing
零交叉可能以三种方式发生。在最佳情况下,信号幅度可能非常接近 0.0
。但是,由于精确比较双精度数的棘手问题,类 UtilFn
中定义在文件 Util_Lib.cs 中的以下实用程序代码用于将 double
与零进行比较。
// Definitions to deal with zero-comparisons of {double}s.
//
// After Microsoft.Docs "Double.Equals Method".
// https://docs.microsoft.com/en-us/dotnet/api/system.double.equals?view=net-5.0
private static double fraction = (double)0.333333,
dTolerance = Math.Abs( fraction * (double)0.00001 ),
zero = (double)0.0;
//
// In comparisons, use Math.Abs( {double}1 - {double}2 ) <= {dTolerance} )
// (see function {EQdoubles}).
/// <summary>Determine whether two {double} numbers are "equal".
/// </summary>
/// <param name="d1">Double number.</param>
/// <param name="d2">Double number.
/// </param>
/// <returns>Whether {d1} "equals" {d2}.
/// </returns>
public static bool EQdoubles( double d1, double d2 )
{
d1 = Math.Abs( d1 );
d2 = Math.Abs( d2 );
return Math.Abs( d1 - d2 ) <= dTolerance;
}// EQdoubles
/// <summary>Determine whether a {double} is close to {zero}.
/// </summary>
/// <param name="d">{double} to be tested.
/// </param>
/// <returns>Whether {d} is close to {zero}.
/// </returns>
public static bool NearZero( double d )
{
return EQdoubles( d, zero );
}// NearZero
零交叉发生的另外两种情况是:当前 TimeMagnitude
元素的幅度低于时间轴而前一个元素的幅度高于时间轴,或者前一个 TimeMagnitude
元素的幅度低于时间轴而当前元素的幅度高于时间轴。这些情况由以下函数检查
/// <summary>Determine whether there is a magnitude transition through the time
/// axis from the previous {TimeMagnitude} element to the current element.
/// </summary>
/// <param name="previousTM">Previous {TimeMagnitude} element.</param>
/// <param name="currentTM">Current {TimeMagnitude} element.</param>
/// <returns>{true} if there was a transition, {false} otherwise.
/// </returns>
private bool MagnitudeTransition( TimeMagnitude previousTM, TimeMagnitude currentTM )
{
return ( previousTM.Magnitude > 0.0 && currentTM.Magnitude < 0.0 )
||
( previousTM.Magnitude < 0.0 && currentTM.Magnitude > 0.0 );
}// MagnitudeTransition
在检查零交叉后,更新变量 previousTM
,函数退出 try
-catch
子句并执行 yield
return magnitude 以返回信号值。
下次调用该函数时,执行将继续在 yield
return 语句之后,时间局部变量会更新,无限循环继续。请注意,实际上,将函数实现为枚举器并使用 yield
return 语句使函数的局部变量表现得像老式的 C 和 C++ static
变量。这一点非常 remarkable,因为 C# 本身不支持 static
变量。
如果执行到达 try
-catch
子句的 catch
部分,则发生了异常。函数将显示一个带有异常消息的 MessageBox
,然后调用 Abort
函数终止执行。
/// <summary>Abort execution of the 'user'.
/// </summary>
private void Abort()
{
if ( System.Windows.Forms.Application.MessageLoop )
{
// Windows application
System.Windows.Forms.Application.Exit();
}
else
{
// Console application
System.Environment.Exit( 1 );
}
}// Abort
下一个余弦值的生成方式类似,通过调用 Math.Cos
而不是 Math.Sin
来完成,这里不展示。(同样,完整代码在附带的 ZIP 文件中。)
方波信号
方波信号的生成几乎是直观的。唯一的困难是处理垂直不连续性,这必须在辅助时间变量 t
接近信号周期的一半(parameters.halfPeriod
)时发生。
/// <summary>Generate the next square-signal value.
/// </summary>
/// <returns>Current magnitude of the square signal.
/// </returns>
public IEnumerator<double> NextSquareSignalValue()
{
double _amplitude = parameters.amplitude,
magnitude = parameters.offset + _amplitude;
double time = 0.0, t = 0.0;
bool updateZeroCrossings = magnitude > (double)0.0;
zeroCrossings = new List<double>();
while ( true )
{
try
{
TimeMagnitude tm = new TimeMagnitude( time, magnitude );
squareSignalValues.Add( new TimeMagnitude( time, magnitude ) );
}
catch ( Exception exc )
{
MessageBox.Show( exc.Message );
Abort();
}
yield return magnitude;
time += parameters.timeStep;
t += parameters.timeStep;
if ( UtilFn.NearZero( t - parameters.halfPeriod ) )
{
_amplitude = -_amplitude; // Vertical discontinuity.
t = 0.0;
if ( updateZeroCrossings )
{
zeroCrossings.Add( time );
}
}
magnitude = parameters.offset + _amplitude;
}
}// NextSquareSignalValue
除了处理垂直不连续性之外,方波信号的枚举器遵循与正弦波和余弦波信号的枚举器相同的逻辑。
锯齿波信号
锯齿波信号是通过重复一条斜直线生成的,其方程为
其中 m
是斜率,b
是 y
轴截距。除了处理垂直不连续性的部分,相应枚举器的实现也是直观的。
/// <summary>Generate the next sawtooth-signal value.
/// </summary>
/// <returns>Current magnitude of the sawtooth signal.
/// </returns>
public IEnumerator<double> NextSawtoothSignalValue()
{
/*
* A sawtooth signal is generated by repeating a sloped straight
* line, whose equation is
*
* y = m * t + b
*
* where {m} is the slope and {b} is the y-axis ordinate.
*/
double m = 10.0 / parameters.period,
b = -parameters.amplitude;
double time = 0.0, t = 0.0;
double magnitude = 0.0;
TimeMagnitude previousTM, tm;
zeroCrossings = new List<double>();
while ( true )
{
previousTM = tm = null;
try
{
magnitude = parameters.offset + ( m * t + b );
tm = new TimeMagnitude( time, magnitude );
sawtoothSignalValues.Add( tm );
CheckZeroCrossing( previousTM, tm );
previousTM = tm.Copy();
}
catch ( Exception exc )
{
MessageBox.Show( exc.Message );
Abort();
}
yield return magnitude;
if ( UtilFn.NearZero( t - parameters.period ) )
{
t = 0.0;
if ( tm.Magnitude > (double)0.0 )
{
zeroCrossings.Add( time ); // Vertical discontinuity.
}
}
time += parameters.timeStep;
t += parameters.timeStep;
}
}// NextSawtoothSignalValue
请注意,锯齿波信号的垂直不连续性发生在信号周期的整数倍处。
三角波信号
三角波信号可以看作是锯齿波信号的两个镜像斜线,上升线用于周期的前半部分,下降线用于周期的后半部分。
/// <summary>Generate the next triangle-signal value.
/// </summary>
/// <returns>Current magnitude of the triangle signal.
/// </returns>
public IEnumerator<double> NextTriangleSignalValue()
{
/*
* A triangle signal consists of mirrored sloped straight lines,
* which can be obtained using part of the code for a sawtooth signal.
*/
double m = 10.0 / parameters.period,
b = -parameters.amplitude;
double time = 0.0, t = 0.0;
double magnitude = 0.0;
int j = 0;
TimeMagnitude previousTM, tm;
tm = previousTM = null;
bool mirror = false; // No mirroring.
zeroCrossings = new List<double>();
while ( true )
{
try
{
if ( !mirror ) // Line with ascending slope.
{
magnitude = parameters.offset + ( m * t + b );
tm = new TimeMagnitude( time, magnitude );
triangleSignalValues.Add( tm );
++j;
}
else // Mirroring: line with descending slope.
{
if ( j > 0 )
{
magnitude = triangleSignalValues[ --j ].Magnitude;
tm = new TimeMagnitude( time, magnitude );
triangleSignalValues.Add( tm );
}
}
}
catch ( Exception exc )
{
MessageBox.Show( exc.Message );
Abort();
}
CheckZeroCrossing( previousTM, tm );
previousTM = tm.Copy();
yield return magnitude;
if ( UtilFn.NearZero( t - parameters.halfPeriod ) )
{
mirror = true; // Start mirroring.
}
if ( UtilFn.NearZero( t - parameters.period ) )
{
t = 0.0;
j = 0;
mirror = false; // Stop mirroring.
}
time += parameters.timeStep;
t += parameters.timeStep;
}
}// NextTriangleSignalValue
白噪声信号
白噪声是一种随机信号。其数据点根据从随机数生成器获得的值随机出现。为了或多或少均匀地分布数据点,可以使用两个随机数生成器:一个用于幅值,另一个用于符号。
/// <summary>Generate the next white-noise value.
/// </summary>
/// <returns>Current value of white noise.
/// </returns>
public IEnumerator<double> NextWhiteNoiseSignalValue()
{
double magnitude = 0.0, time = 0.0, sign;
Random magRand, signRand;
magRand = new Random(); // Magnitude random number generator.
signRand = new Random(); // Random number generator for signal sign.
zeroCrossings = new List<double>(); // This list will remain empty.
while ( true )
{
sign = ( signRand.Next( 10 ) > 5 ) ? 1.0 : -1.0;
magnitude = parameters.offset
+ sign * ( magRand.NextDouble() * parameters.amplitude );
whiteNoiseValues.Add( new TimeMagnitude( time, magnitude ) );
yield return magnitude;
time += parameters.timeStep;
}
}// NextWhiteNoiseSignalValue
此函数初始化零交叉点的列表,但从不向其中插入元素。原因是白噪声不能被视为对应于正弦波、余弦波、方波、锯齿波或三角波等具有确定趋势的幅值值的函数。
信号图
GeneratingFn
类构造函数使用 SignalPlot
类实例初始化一些 private
数据成员,这些成员用于显示显示生成信号数据点的 Windows 窗体。此类定义在文件 SignalPlot.cs 中。有两个枚举,一个用于指定信号的连续性,一个用于指定信号的形状。
public enum SignalContinuity { continuous, discontinuous };
public enum SignalShape { sine, cosine, square, sawtooth, triangle, whiteNoise };
用于创建绘制信号的 Windows 窗体实例的类的数据成员和构造函数定义如下
public partial class SignalPlot : Form
{
private string description; // Windows-form title.
private SignalShape shape;
private SignalContinuity continuity;
private Bitmap bmp;
private int xAxis_Y, // Y coordinate of x-axis (middle of bmp.Height).
sigMin_Y, // Minimum Y coordinate of a signal
// (has nothing to do with the actual signal value).
sigMax_Y; // Maximum Y coordinate of a signal
// (has nothing to do with the actual signal value).
private Graphics gr;
private Font drawFont;
private StringFormat drawFormat;
private int iScale, // Scaling factor for plotting signals.
iScaleDIV2;
private double dScale; // {double} of {iScale}.
private int nextParameter_Y; // Y coordinate to draw {deltaY}
// and {timeStep} in function {DrawDelta_Y_X}.
public SignalPlot( string _description, SignalShape _shape,
SignalContinuity _continuity = SignalContinuity.continuous )
{
InitializeComponent();
bmp = new Bitmap( pictureBox1.Width, pictureBox1.Height );
pictureBox1.Image = bmp;
gr = Graphics.FromImage( bmp );
gr.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
gr.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
gr.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
gr.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
gr.Clear( Color.Transparent );
drawFont = new System.Drawing.Font( "Calibri", 10,
FontStyle.Regular, GraphicsUnit.Point );
drawFormat = new StringFormat();
description = _description;
shape = _shape;
continuity = _continuity;
xAxis_Y = bmp.Height / 2; // Y coordinate of x-axis
iScale = 10; // Arbitrary scaling factor.
iScaleDIV2 = iScale / 2;
dScale = (double)iScale;
}// SignalPlot (constructor)
GeneratingFn
类构造函数初始化了几个 private
数据成员(sineSignal
、cosineSignal
、squareSignal
、sawtoothSignal
、triangleSignal
和 whiteNoiseSignal
)以及 SignalPlot
类实例。这些实例可用于通过调用 SignalPlot.Plot
函数的以下函数来绘制生成的信号。
public void PlotSineSignal()
{
sineSignal.Plot( parameters, sineSignalValues );
}// PlotSineSignal
public void PlotCosineSignal()
{
cosineSignal.Plot( parameters, cosineSignalValues );
}// PlotSineSignal
public void PlotSquareSignal()
{
squareSignal.Plot( parameters, squareSignalValues, SignalContinuity.discontinuous );
}// PlotSquareSignal
public void PlotSawtoothSignal()
{
sawtoothSignal.Plot
( parameters, sawtoothSignalValues, SignalContinuity.discontinuous );
}// PlotSawtoothSignal
/// <summary>
/// By definition, a triangle signal is discontinuous because the derivative (slope)
/// of the function at the peaks does not exist. However, the discontinuity at
/// the peaks is not as sharp (i.e., vertical) as in a square signal or a sawtooth
/// signal. Hence, the third argument to function {triangleSignal.Plot} is left as
/// {SignalContinuity.continuous}.
/// </summary>
public void PlotTriangleSignal()
{
triangleSignal.Plot( parameters, triangleSignalValues );
}// PlotTriangleSignal
/// <summary>
/// By definition, a white noise signal is completely discontinuous because it is
/// made up by random points on the amplitude vs. time scales. However, the
/// discontinuities are not as sharp (i.e., vertical) as in a square signal or a
/// sawtooth signal. Therefore, the third argument to function {whiteNoise.Plot}
/// is left as {SignalContinuity.continuous}.
/// </summary>
public void PlotWhiteNoiseSignal()
{
whiteNoiseSignal.Plot( parameters, whiteNoiseValues );
}// PlotWhiteNoiseSignal
绘制信号的函数(SignalPlot.Plot
)几乎是直观的。它将信号参数、时间和幅度点列表以及信号的连续性作为参数。
/// <summary>Plot a list of {TimeMagnitude} points.
/// </summary>
/// <param name="parameters">Parameters of the signal to be plotted.</param>
/// <param name="list">List containing the (time, magnitude) points.</param>
/// <param name="continuity">Continuity of the signal.
/// </param>
public void Plot( SignalParameters parameters, List<TimeMagnitude> list,
SignalContinuity continuity = SignalContinuity.continuous )
{
int n, m;
if ( list == null || ( n = list.Count ) == 0 )
{
MessageBox.Show(
String.Format( "No {0} values to plot", description ) );
}
else
{
int x, deltaX, currY, nextY;
// Increasing signal-magnitude values are drawn from the
// bottom of the {Bitmap} to its top.
sigMax_Y = 0;
sigMin_Y = bmp.Height;
Draw_X_axis();
Draw_Y_axis();
drawFormat.FormatFlags = StringFormatFlags.DirectionRightToLeft;
DrawParameters( parameters, shape );
deltaX = this.Width / n;
x = 0;
m = n - 2;
drawFormat.FormatFlags = StringFormatFlags.DirectionVertical;
for ( int i = 0; i < n; ++i )
{
int iScaledMag = ScaledMagnitude( list[ i ], dScale );
currY = xAxis_Y - iScaledMag;
if ( currY > sigMax_Y )
{
sigMax_Y = currY;
}
if ( currY < sigMin_Y )
{
sigMin_Y = currY;
}
if ( x >= bmp.Width )
{
break;
}
bmp.SetPixel( x, currY, Color.Black );
if ( UtilFn.IsDivisible( list[ i ].Time, parameters.period ) )
{
string label = String.Format( "___ {0:0.0000}", list[ i ].Time );
SizeF size = gr.MeasureString( label, drawFont );
gr.DrawString( label, drawFont, Brushes.Red,
new Point( x, bmp.Height - (int)size.Width ),
drawFormat );
}
if ( continuity == SignalContinuity.discontinuous && i <= m )
{
int iP1ScaledMag = ScaledMagnitude( list[ i + 1 ], dScale );
nextY = xAxis_Y - iP1ScaledMag;
if ( x > 0 && ( shape == SignalShape.square ||
shape == SignalShape.sawtooth ) )
{
if ( i < m )
{
CheckVerticalDiscontinuity( x, currY, nextY );
}
else // i == m
{
DrawVerticalDiscontinuity( x + deltaX, currY );
}
}
}
x += deltaX;
}
Draw_Y_axisNotches( parameters );
this.ShowDialog(); // Display form in modal mode.
}
}// Plot
该函数确定 Y 坐标的最大值和最小值,绘制信号参数,为缩放的幅度数据点设置像素,并在 X(时间)坐标上绘制标签,这些坐标是信号周期可整除的,通过调用文件 Util_Lib.cs 中的实用程序函数来确定。
/// <summary>Determine whether a double is divisible by another double.
/// </summary>
/// <param name="x">Numerator of division.
/// </param>
/// <param name="y">Denominator of division.
/// </param>
/// <returns>Whether {x} is divisible by {y}.
/// </returns>
public static bool IsDivisible( double x, double y )
{
return Math.Abs( ( ( Math.Round( x / y ) * y ) - x ) ) <= ( 1.0E-9 * y );
}// IsDivisible
在文件 Util_Lib.cs 中(两次)使用代码的原因是作者在其他应用程序中使用此类文件中的函数。该文件包含与数字信号的生成和绘制无关的其他函数。
由函数 SignalPlot.Plot
调用的函数几乎是不言而喻的。其中大多数(Draw_X_axis
、Draw_Y_axis
、DrawParameters
、ScaledMagnitude
、Draw_Y_axisNotches
和 DrawNotch
)将在文章中不作描述。
函数 DrawDelta_Y_X
负责绘制信号的幅度和时间步长。时间步长是采样频率的倒数,采样频率是采样因子和信号频率的乘积。这些参数对于在其他应用程序中正确处理数字信号至关重要。
/// <summary>Draw the {amplitude} and the {timeStep} from the
/// parameters of a signal.
/// </summary>
/// <param name="parameters">Signal parameters.
/// </param>
private void DrawDelta_Y_X( SignalParameters parameters )
{
// There are 11 notches on the Y axis.
string delta_Y_X_str = String.Format( "deltaY: {0:00.000} V, time step: {1:0.00000} sec",
parameters.amplitude / 5.0, parameters.timeStep );
drawFormat.FormatFlags = StringFormatFlags.DirectionRightToLeft;
SizeF size = gr.MeasureString( delta_Y_X_str, drawFont );
int x = (int)size.Width + 8;
Point point = new Point( x, nextParameter_Y );
gr.DrawString( delta_Y_X_str, drawFont, Brushes.Red, point, drawFormat );
}// DrawDelta_Y_X
绘制方波和锯齿波信号垂直不连续性的两个有趣的函数。在到达方波或锯齿波信号的末尾之前,有必要检查是否必须绘制不连续性。此外,对于方波信号,不连续性必须绘制为向上或向下。对于锯齿波信号,不连续性总是向下。
/// <summary>Conditionally draw the discontinuity of a square or sawtooth signal.
/// </summary>
/// <param name="x">Position on the x axis (time).</param>
/// <param name="currY">Current position in the y dimension (units of magnitude).</param>
/// <param name="nextY">Next position in the y dimension (units of magnitude).
/// </param>
private void CheckVerticalDiscontinuity( int x, int currY, int nextY )
{
if ( x >= bmp.Width )
{
return;
}
int discLength = Math.Abs( currY - nextY );
if ( discLength > iScaleDIV2 )
{
int y;
if ( currY < nextY )
{
for ( y = currY; y <= nextY; ++y ) // Discontinuity going down.
{
bmp.SetPixel( x, y, Color.Black );
}
}
else // nextY < currY, Discontinuity going up.
{
for ( y = currY; y >= nextY; --y )
{
bmp.SetPixel( x, y, Color.Black );
}
}
}
}// CheckVerticalDiscontinuity
在方波或锯齿波信号的末尾,不连续性被无条件绘制。
/// <summary>Draw the vertical discontinuity at the end of a square or sawtooth signal.
/// </summary>
/// <param name="x">Position on the x axis (time).</param>
/// <param name="currY">Current position in the y dimension (units of magnitude).
/// </param>
private void DrawVerticalDiscontinuity( int x, int currY )
{
if ( x >= bmp.Width )
{
return;
}
int y;
if ( currY < sigMax_Y )
{
for ( y = currY; y <= sigMax_Y; ++y )
{
bmp.SetPixel( x, y, Color.Black );
}
}
else if ( currY > sigMin_Y )
{
for ( y = sigMin_Y; y <= currY; ++y )
{
bmp.SetPixel( x, y, Color.Black );
}
}
}// DrawVerticalDiscontinuity
测试信号生成库
可以编写一个简单的控制台应用程序来测试信号生成库。此应用程序的代码位于附带 ZIP 文件中 TestSignalGenLib 目录下的 Program.cs 文件中。Program
类定义了两个私有的文件相关变量,用于写入发送到控制台应用程序命令提示符窗口的输出。这些变量在应用程序的 Main
函数中初始化如下
fs = new FileStream( @"..\..\_TXT\out.txt", FileMode.Create );
sw = new StreamWriter( fs );
唯一的 public global
变量是 genFn
。Main
函数将此变量绑定到 GeneratingFn
类的一个实例,然后定义生成信号的枚举器。
IEnumerator<double> Sine = genFn.NextSineSignalValue();
IEnumerator<double> Cosine = genFn.NextCosineSignalValue();
IEnumerator<double> Square = genFn.NextSquareSignalValue();
IEnumerator<double> Sawtooth = genFn.NextSawtoothSignalValue();
IEnumerator<double> Triangle = genFn.NextTriangleSignalValue();
IEnumerator<double> WhiteNoise = genFn.NextWhiteNoiseSignalValue();
所有信号发生器都会被调用固定次数,以在命令提示符窗口中枚举和显示信号值。然后,显示时间步长和零交叉点。最后,在 Windows 窗体中绘制信号值。例如,以下代码对应于正弦信号的情况。
int n = 512;
string signalName;
signalName = "Sine";
EnumerateValues( signalName, Sine, genFn.sineSignalValues, n );
DisplayTimeStepAndZeroCrossings( genFn, signalName );
genFn.PlotSineSignal();
在执行结束时,文件 @"..\..\_TXT\out.txt" 包含发送到控制台应用程序命令提示符窗口的所有文本输出。为简洁起见,此处仅显示信号的第一周期和最后一个周期。为了便于参考,对 TimeMagnitude
数据点和零交叉点进行了编号。
正弦信号值
0 0.0000 0.0000 1 0.0003 0.9755 2 0.0006 1.9134 3 0.0009 2.7779
4 0.0013 3.5355 5 0.0016 4.1573 6 0.0019 4.6194 7 0.0022 4.9039
8 0.0025 5.0000 9 0.0028 4.9039 10 0.0031 4.6194 11 0.0034 4.1573
12 0.0038 3.5355 13 0.0041 2.7779 14 0.0044 1.9134 15 0.0047 0.9755
16 0.0050 0.0000
. . .
496 0.1550 0.0000 497 0.1553 -0.9755 498 0.1556 -1.9134 499 0.1559 -2.7779
500 0.1562 -3.5355 501 0.1566 -4.1573 502 0.1569 -4.6194 503 0.1572 -4.9039
504 0.1575 -5.0000 505 0.1578 -4.9039 506 0.1581 -4.6194 507 0.1584 -4.1573
508 0.1587 -3.5355 509 0.1591 -2.7779 510 0.1594 -1.9134 511 0.1597 -0.9755
GeneratingFn
构造函数设置的时间步长:0.00031
GeneratingFn.NextSineSignalValue
找到的零交叉点
0 0.0000 1 0.0050 2 0.0052 3 0.0100 4 0.0150 5 0.0200 6 0.0250
7 0.0300 8 0.0350 9 0.0400 10 0.0450 11 0.0500 12 0.0550 13 0.0600
14 0.0650 15 0.0652 16 0.0700 17 0.0702 18 0.0750 19 0.0752 20 0.0800
21 0.0802 22 0.0850 23 0.0852 24 0.0900 25 0.0902 26 0.0950 27 0.0952
28 0.1000 29 0.1002 30 0.1050 31 0.1052 32 0.1100 33 0.1102 34 0.1150
35 0.1152 36 0.1200 37 0.1202 38 0.1250 39 0.1252 40 0.1300 41 0.1302
42 0.1350 43 0.1352 44 0.1400 45 0.1402 46 0.1450 47 0.1452 48 0.1500
49 0.1502 50 0.1550 51 0.1552
在命令提示符窗口中显示信号值和零交叉点后,应用程序会显示如下所示的信号图
该窗体以模态模式显示(通过在 SignalGenLib.Plot
函数中调用 Form.ShowDialog
)。作为第二个示例,下图显示了一个 500 Hz 正弦信号的图。零交叉点列在图下方
GeneratingFn.NextSineSignalValue
找到的零交叉点
0 0.0000 1 0.0010 2 0.0020 3 0.0030 4 0.0040 5 0.0050 6 0.0060
7 0.0070 8 0.0080 9 0.0090 10 0.0100 11 0.0110 12 0.0120 13 0.0130
14 0.0140 15 0.0150 16 0.0160 17 0.0170 18 0.0180 19 0.0190 20 0.0200
21 0.0210 22 0.0220 23 0.0230 24 0.0240 25 0.0250 26 0.0260 27 0.0270
28 0.0280 29 0.0290 30 0.0300 31 0.0310
请注意,尽管 100 Hz 和 500 Hz 的正弦信号看起来相同,但它们并不相同,因为时间轴标记的值不同。此外,由于它们的频率不同,第一个信号穿过时间轴 52 次,而第二个信号穿过时间轴 32 次。
当通过单击其右上角的交叉按钮关闭 Windows 窗体时,程序会运行类似的代码,使用与生成正弦信号相同的参数来生成余弦波、方波、锯齿波、三角波和白噪声信号的图。每次显示信号图时,必须关闭它才能生成并显示下一个信号图。以下两张图显示了方波和白噪声信号的图。
命令提示符窗口显示白噪声情况下没有零交叉点。这是因为白噪声不像所有其他离散信号,其相邻点遵循规则的趋势。
作为额外示例,下图显示了一个 100 Hz 的三角波信号,其幅度为 6 伏特,直流偏移量为 2.5 伏特,这是由以下代码生成的
genFn = new GeneratingFn( 6.0, 100.0, 2.5 );
IEnumerator<double> Triangle = genFn.NextTriangleSignalValue();
signalName = "Triangle";
EnumerateValues( signalName, Triangle, genFn.triangleSignalValues, n );
DisplayTimeStepAndZeroCrossings( genFn, signalName );
genFn.PlotTriangleSignal();
Using the Code
附带的 ZIP 文件包含三个目录中的八个文件。Util_Lib 目录包含文件 UtilFn.cs。SignalGenLib 目录包含文件 GeneratingFn.cs、SignalParameters.cs、SignalPlot.cs、SignalPlot.Designer.cs、SignalPlot.resx 和 TimeMagnitude.cs。TestSignalGenLib 目录包含文件 Program.cs。
创建一个目录“Generation of Digital Signals”。在 Visual Studio 中,单击“文件”,选择“新建”,然后单击“项目”。选择“类库”,将创建的目录指定为“位置”,并将“名称”指定为“Util_Lib”。在解决方案资源管理器窗格中,右键单击“Class1.cs”,选择“重命名”并将类名更改为“UtilFn.cs”。将附带 ZIP 文件中“UtilFn.cs”文件的代码复制到创建的“UtilFn.cs”文件中。单击“生成”,然后单击“生成解决方案”。构建应成功。单击“文件”,然后单击“关闭解决方案”。
重复以上步骤创建一个名为“SignalGenLib”的库。在解决方案资源管理器窗格中,右键单击“Class1.cs”,选择“重命名”并将类名更改为“GeneratingFn.cs”。右键单击“引用”,选择“添加引用”,单击“.NET”选项卡,选择“System.Windows.Forms”并单击“确定”;同样添加对“System.Drawing”的引用。右键单击“引用”,选择“添加引用”,单击“浏览”选项卡,导航到目录“Util_Lib\bin\Debug”,选择“Util_Lib.dll”并单击“确定”。用附带的“GeneratingFn.cs”文件的内容替换刚刚创建的“GeneratingFn.cs”文件的全部内容。选择“文件”并单击“全部保存”。
将文件 "SignalParameters.cs"、"SignalPlot.cs"、"SignalPlot.Designer.cs"、"SignalPlot.resx" 和 "TimeMagnitude.cs" 复制到 "SignalGenLib" 目录。对于每个复制的文件,在 **解决方案资源管理器** 窗格中,右键单击“SignalGenLib”,选择“添加”,单击“现有项”,选择要添加的文件并单击“添加”。错误列表窗格应显示 0 错误,0 警告和 0 消息。单击“生成”选项卡,然后单击“生成解决方案”。构建应成功。单击“文件”,然后单击“关闭解决方案”。
单击“文件”,选择“新建”,然后单击“项目”,选择“控制台应用程序”,名称为“TestSignalGenLib”。在 **解决方案资源管理器** 中,右键单击“引用”,单击“添加引用”,单击“浏览”选项卡,导航到目录“SignalGenLib\bin\Debug”,选择“SignalGenLib.dll”并单击“确定”。单击“文件”然后单击“全部保存”。添加对“SignalGenLib.dll”的引用。将文件中的全部内容替换为附带的“Program.cs”文件的全部内容。单击“生成”然后单击“生成解决方案”。构建应成功。在 "TestSignalGenLib" 目录下创建一个名为 "_TXT" 的目录。单击“调试”然后单击“开始但不调试”。控制台应用程序应为每个信号生成信号值并显示一个图。关闭当前 Windows 窗体图以生成下一个信号的值并显示其图。关闭白噪声图后,按任意键退出控制台应用程序。
结论
本文讨论了一个 C# 库的设计、实现和测试,用于生成和显示一些常见的数字信号。信号生成函数实现为枚举器,通过使用 yield return,它们实际上在连续调用之间维护了局部变量的状态。该库将再次用于测试数字双二阶带通滤波器的实现。此类测试的结果将在即将发表的文章中报告。
历史
- 2022年3月2日:初始版本