diff --git a/README.md b/README.md index 84852d0..8082007 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,6 @@ Description | Screenshot **[Drawing with the Mouse](examples/2019-06-02-drawing-with-mouse/readme.md)** - This project uses a PictureBox's MouseMove event handler to create a MSPaint-like drawing surface with only a few lines of code. | ![](examples/2019-06-02-drawing-with-mouse/screenshot.png) **[Plotting on a 2D Coordinate System](/examples/2019-06-03-coordinate-system/readme.md)** - A simple but challenging task when plotting data on a bitmap is the conversion between 2D data space and bitmap pixel coordinates. If your axis limits are -10 and +10 (horizontally and vertically), what pixel position on the bitmap corresponds to (-1.23, 3.21)? This example demonstrates a minimal-case unit-to-pixel method and uses it to plot X/Y data on a bitmap. | ![](/examples/2019-06-03-coordinate-system/screenshot.png) - - ### Bitmap Pixel Manipulation Description | Screenshot @@ -32,6 +30,14 @@ Description | Screenshot **[Modifying Bitmap Data in Memory](/examples/2019-06-04-pixel-setting/readme.md)** - Bitmaps in memory have a certain number of bytes per pixel, so they're easy to convert to/from byte arrays. This example shows how to convert a Bitmap to a byte array, fill the array with random values, and convert it back to a Bitmap to display in a PictureBox. This method can be faster than using drawing methods like GetPixel and PutPixel. | ![](/examples/2019-06-04-pixel-setting/screenshot.png) **[Setting Pixel Intensity from a Value](/examples/2019-06-05-grayscale-image/readme.md)** - This example shows how to create an 8-bit grayscale image where pixel intensities are calculated from a formula (but could easily be assigned from a data array). This example also demonstrates the important difference between Bitmap _width_ and _span_ when working with byte positions in memory. | ![](/examples/2019-06-05-grayscale-image/screenshot.png) +### Audio + +Description | Screenshot +---|--- +**[Plotting Audio Amplitude](/examples/2019-06-06-audio-level-monitor/readme.md)** - This example uses [NAudio](https://github.com/naudio/NAudio) to access the sound card, calculates the amplitude of short recordings, then graphs them continuously in real time with [ScottPlot](https://github.com/swharden/ScottPlot). This project is a good place to get started to see how to interface audio input devices. | ![](/examples/2019-06-06-audio-level-monitor/screenshot.png) +**[Plotting Audio Values](/examples/2019-06-07-audio-visualizer/readme.md)** - This example uses [NAudio](https://github.com/naudio/NAudio) to access the sound card and plots raw PCM values with [ScottPlot](https://github.com/swharden/ScottPlot). These graphs contain tens of thousands of data points, but remain fully interactive even as they are being updated in real time. | ![](/examples/2019-06-07-audio-visualizer/screenshot.gif) +**[Plotting Audio FFT](/examples/2019-06-08-audio-fft/screenshot.gif)** - This example continuously plots the frequency component of an audio input device. The [NAudio](https://github.com/naudio/NAudio) library is used to acquire the audio data and process the FFT and [ScottPlot](https://github.com/swharden/ScottPlot) is used for the plotting. | ![](/examples/2019-06-08-audio-fft/screenshot.gif) + ## Additional Projects * These projects are first-pass implementations of ideas, but they work, so learn and from them what you can and take whatever you find useful! They're not as polished as the ones in the previous section. * Each example below is a standalone Visual Studio solution diff --git a/examples/2019-06-06-audio-level-monitor/App.config b/examples/2019-06-06-audio-level-monitor/App.config new file mode 100644 index 0000000..56efbc7 --- /dev/null +++ b/examples/2019-06-06-audio-level-monitor/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/examples/2019-06-06-audio-level-monitor/AudioPeak.csproj b/examples/2019-06-06-audio-level-monitor/AudioPeak.csproj new file mode 100644 index 0000000..157adeb --- /dev/null +++ b/examples/2019-06-06-audio-level-monitor/AudioPeak.csproj @@ -0,0 +1,90 @@ + + + + + Debug + AnyCPU + {9A49B8F0-FFB7-4F25-8FA7-14D239BF807D} + WinExe + AudioPeak + AudioPeak + v4.7.2 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + packages\NAudio.1.9.0\lib\net35\NAudio.dll + + + packages\ScottPlot.3.0.3\lib\net45\ScottPlot.dll + + + + + + + + + + + + + + + + Form + + + Form1.cs + + + + + Form1.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + + + \ No newline at end of file diff --git a/examples/2019-06-06-audio-level-monitor/AudioPeak.sln b/examples/2019-06-06-audio-level-monitor/AudioPeak.sln new file mode 100644 index 0000000..f765b26 --- /dev/null +++ b/examples/2019-06-06-audio-level-monitor/AudioPeak.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28922.388 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AudioPeak", "AudioPeak.csproj", "{9A49B8F0-FFB7-4F25-8FA7-14D239BF807D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9A49B8F0-FFB7-4F25-8FA7-14D239BF807D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A49B8F0-FFB7-4F25-8FA7-14D239BF807D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A49B8F0-FFB7-4F25-8FA7-14D239BF807D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A49B8F0-FFB7-4F25-8FA7-14D239BF807D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1A0721F2-DB45-4148-BA38-3E7FCDCBECF4} + EndGlobalSection +EndGlobal diff --git a/examples/2019-06-06-audio-level-monitor/Form1.Designer.cs b/examples/2019-06-06-audio-level-monitor/Form1.Designer.cs new file mode 100644 index 0000000..8d1761b --- /dev/null +++ b/examples/2019-06-06-audio-level-monitor/Form1.Designer.cs @@ -0,0 +1,137 @@ +namespace AudioPeak +{ + partial class Form1 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.cbDevice = new System.Windows.Forms.ComboBox(); + this.label1 = new System.Windows.Forms.Label(); + this.btnStart = new System.Windows.Forms.Button(); + this.btnStop = new System.Windows.Forms.Button(); + this.scottPlotUC1 = new ScottPlot.ScottPlotUC(); + this.timer1 = new System.Windows.Forms.Timer(this.components); + this.cbAutoAxis = new System.Windows.Forms.CheckBox(); + this.SuspendLayout(); + // + // cbDevice + // + this.cbDevice.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cbDevice.FormattingEnabled = true; + this.cbDevice.Location = new System.Drawing.Point(116, 12); + this.cbDevice.Name = "cbDevice"; + this.cbDevice.Size = new System.Drawing.Size(171, 21); + this.cbDevice.TabIndex = 0; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(12, 15); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(98, 13); + this.label1.TabIndex = 1; + this.label1.Text = "Audio input device:"; + // + // btnStart + // + this.btnStart.Location = new System.Drawing.Point(293, 11); + this.btnStart.Name = "btnStart"; + this.btnStart.Size = new System.Drawing.Size(75, 23); + this.btnStart.TabIndex = 2; + this.btnStart.Text = "Start"; + this.btnStart.UseVisualStyleBackColor = true; + this.btnStart.Click += new System.EventHandler(this.BtnStart_Click); + // + // btnStop + // + this.btnStop.Location = new System.Drawing.Point(374, 11); + this.btnStop.Name = "btnStop"; + this.btnStop.Size = new System.Drawing.Size(75, 23); + this.btnStop.TabIndex = 3; + this.btnStop.Text = "Stop"; + this.btnStop.UseVisualStyleBackColor = true; + this.btnStop.Click += new System.EventHandler(this.BtnStop_Click); + // + // scottPlotUC1 + // + this.scottPlotUC1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.scottPlotUC1.Location = new System.Drawing.Point(12, 40); + this.scottPlotUC1.Name = "scottPlotUC1"; + this.scottPlotUC1.Size = new System.Drawing.Size(776, 398); + this.scottPlotUC1.TabIndex = 4; + // + // timer1 + // + this.timer1.Enabled = true; + this.timer1.Interval = 20; + this.timer1.Tick += new System.EventHandler(this.Timer1_Tick); + // + // cbAutoAxis + // + this.cbAutoAxis.AutoSize = true; + this.cbAutoAxis.Checked = true; + this.cbAutoAxis.CheckState = System.Windows.Forms.CheckState.Checked; + this.cbAutoAxis.Location = new System.Drawing.Point(455, 14); + this.cbAutoAxis.Name = "cbAutoAxis"; + this.cbAutoAxis.Size = new System.Drawing.Size(69, 17); + this.cbAutoAxis.TabIndex = 5; + this.cbAutoAxis.Text = "Auto-axis"; + this.cbAutoAxis.UseVisualStyleBackColor = true; + // + // Form1 + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(800, 450); + this.Controls.Add(this.cbAutoAxis); + this.Controls.Add(this.scottPlotUC1); + this.Controls.Add(this.btnStop); + this.Controls.Add(this.btnStart); + this.Controls.Add(this.label1); + this.Controls.Add(this.cbDevice); + this.Name = "Form1"; + this.Text = "C# Audio Monitor"; + this.Load += new System.EventHandler(this.Form1_Load); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.ComboBox cbDevice; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Button btnStart; + private System.Windows.Forms.Button btnStop; + private ScottPlot.ScottPlotUC scottPlotUC1; + private System.Windows.Forms.Timer timer1; + private System.Windows.Forms.CheckBox cbAutoAxis; + } +} + diff --git a/examples/2019-06-06-audio-level-monitor/Form1.cs b/examples/2019-06-06-audio-level-monitor/Form1.cs new file mode 100644 index 0000000..c347d5f --- /dev/null +++ b/examples/2019-06-06-audio-level-monitor/Form1.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace AudioPeak +{ + public partial class Form1 : Form + { + public Form1() + { + InitializeComponent(); + ScanSoundCards(); + PlotInitialize(); + } + + private void Form1_Load(object sender, EventArgs e) + { + } + + private void ScanSoundCards() + { + cbDevice.Items.Clear(); + for (int i = 0; i < NAudio.Wave.WaveIn.DeviceCount; i++) + cbDevice.Items.Add(NAudio.Wave.WaveIn.GetCapabilities(i).ProductName); + if (cbDevice.Items.Count > 0) + cbDevice.SelectedIndex = 0; + else + MessageBox.Show("ERROR: no recording devices available"); + } + + private double[] amplitudes; + private void PlotInitialize(int pointCount = 500) + { + amplitudes = new double[pointCount]; + scottPlotUC1.plt.Clear(); + scottPlotUC1.plt.PlotSignal(amplitudes, sampleRate: 1000.0 / 20, markerSize: 0); + scottPlotUC1.plt.PlotVLine(0, color: Color.Red, lineWidth: 2); + scottPlotUC1.plt.PlotHLine(0, color: Color.Black, lineWidth: 2); + scottPlotUC1.plt.YLabel("Amplitude (%)"); + scottPlotUC1.plt.XLabel("Time (seconds)"); + scottPlotUC1.Render(); + } + + private void PlotAddPoint(double value) + { + int amplitudesIndex = buffersRead%amplitudes.Length; + amplitudes[amplitudesIndex] = value; + } + + private NAudio.Wave.WaveInEvent wvin; + private int buffersRead = -1; + private double peakAmplitudeSeen = 0; + private void OnDataAvailable(object sender, NAudio.Wave.WaveInEventArgs args) + { + int bytesPerSample = wvin.WaveFormat.BitsPerSample / 8; + int samplesRecorded = args.BytesRecorded / bytesPerSample; + Int16[] lastBuffer = new Int16[samplesRecorded]; + for (int i = 0; i < samplesRecorded; i++) + lastBuffer[i] = BitConverter.ToInt16(args.Buffer, i * bytesPerSample); + int lastBufferAmplitude = lastBuffer.Max() - lastBuffer.Min(); + double amplitude = (double)lastBufferAmplitude / Math.Pow(2, wvin.WaveFormat.BitsPerSample); + if (amplitude > peakAmplitudeSeen) + peakAmplitudeSeen = amplitude; + amplitude = amplitude / peakAmplitudeSeen * 100; + buffersRead += 1; + + // TODO: make this sane + ScottPlot.PlottableAxLine axLine = (ScottPlot.PlottableAxLine)scottPlotUC1.plt.GetPlottables()[1]; + axLine.position = (buffersRead % amplitudes.Length) * 20.0 / 1000.0; + + Console.WriteLine(string.Format("Buffer {0:000} amplitude: {1:00.00}%", buffersRead, amplitude)); + PlotAddPoint(amplitude); + } + + private void AudioMonitorInitialize(int DeviceIndex, int sampleRate = 8000, int bitRate = 16, + int channels = 1, int bufferMilliseconds = 20, bool start = true) + { + if (wvin == null) + { + wvin = new NAudio.Wave.WaveInEvent(); + wvin.DeviceNumber = DeviceIndex; + wvin.WaveFormat = new NAudio.Wave.WaveFormat(sampleRate, bitRate, channels); + wvin.DataAvailable += OnDataAvailable; + wvin.BufferMilliseconds = bufferMilliseconds; + if (start) + wvin.StartRecording(); + } + } + + + private void BtnStart_Click(object sender, EventArgs e) + { + AudioMonitorInitialize(cbDevice.SelectedIndex); + } + + private void BtnStop_Click(object sender, EventArgs e) + { + if (wvin != null) + { + wvin.StopRecording(); + wvin = null; + } + } + + private void Timer1_Tick(object sender, EventArgs e) + { + if (cbAutoAxis.Checked) + { + scottPlotUC1.plt.AxisAuto(); + scottPlotUC1.plt.TightenLayout(); + scottPlotUC1.plt.Axis(null, null, -5, null); + } + scottPlotUC1.Render(); + } + } +} diff --git a/examples/2019-06-06-audio-level-monitor/Form1.resx b/examples/2019-06-06-audio-level-monitor/Form1.resx new file mode 100644 index 0000000..1f666f2 --- /dev/null +++ b/examples/2019-06-06-audio-level-monitor/Form1.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file diff --git a/examples/2019-06-06-audio-level-monitor/Program.cs b/examples/2019-06-06-audio-level-monitor/Program.cs new file mode 100644 index 0000000..37a3c16 --- /dev/null +++ b/examples/2019-06-06-audio-level-monitor/Program.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace AudioPeak +{ + static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new Form1()); + } + } +} diff --git a/examples/2019-06-06-audio-level-monitor/Properties/AssemblyInfo.cs b/examples/2019-06-06-audio-level-monitor/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..9bc7a0d --- /dev/null +++ b/examples/2019-06-06-audio-level-monitor/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AudioPeak")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AudioPeak")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9a49b8f0-ffb7-4f25-8fa7-14d239bf807d")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/examples/2019-06-06-audio-level-monitor/Properties/Resources.Designer.cs b/examples/2019-06-06-audio-level-monitor/Properties/Resources.Designer.cs new file mode 100644 index 0000000..c52fdd9 --- /dev/null +++ b/examples/2019-06-06-audio-level-monitor/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace AudioPeak.Properties +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AudioPeak.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/examples/2019-06-06-audio-level-monitor/Properties/Resources.resx b/examples/2019-06-06-audio-level-monitor/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/examples/2019-06-06-audio-level-monitor/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/examples/2019-06-06-audio-level-monitor/Properties/Settings.Designer.cs b/examples/2019-06-06-audio-level-monitor/Properties/Settings.Designer.cs new file mode 100644 index 0000000..dbc1407 --- /dev/null +++ b/examples/2019-06-06-audio-level-monitor/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace AudioPeak.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/examples/2019-06-06-audio-level-monitor/Properties/Settings.settings b/examples/2019-06-06-audio-level-monitor/Properties/Settings.settings new file mode 100644 index 0000000..3964565 --- /dev/null +++ b/examples/2019-06-06-audio-level-monitor/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/2019-06-06-audio-level-monitor/packages.config b/examples/2019-06-06-audio-level-monitor/packages.config new file mode 100644 index 0000000..b0419de --- /dev/null +++ b/examples/2019-06-06-audio-level-monitor/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/examples/2019-06-06-audio-level-monitor/readme.md b/examples/2019-06-06-audio-level-monitor/readme.md new file mode 100644 index 0000000..0d7592d --- /dev/null +++ b/examples/2019-06-06-audio-level-monitor/readme.md @@ -0,0 +1,112 @@ +# Graphing Audio Level +This program continuously reads an audio input device, determines the amplitude of the input signal, then graphs it using a [ScottPlot](https://github.com/swharden/ScottPlot). Audio interfacing is provided with the [NAudio](https://github.com/naudio/NAudio) library. Reviewing how this program works is a good way to learn how to work with audio signals from the microphone. + +![](screenshot.png) + +## Core Concepts + +### Scanning for Audio Input Devices +I always let the user decide which audio device to sample from. The easiest way to do this is to populate a combo box with audio devices. + +```cs +private void ScanSoundCards() +{ + cbDevice.Items.Clear(); + for (int i = 0; i < NAudio.Wave.WaveIn.DeviceCount; i++) + cbDevice.Items.Add(NAudio.Wave.WaveIn.GetCapabilities(i).ProductName); + if (cbDevice.Items.Count > 0) + cbDevice.SelectedIndex = 0; + else + MessageBox.Show("ERROR: no recording devices available"); +} +``` + +### Audio Device Initialization +When the user clicks "Start", a connection is made to the audio device using the index of the combo box that is currently selected. At this time a `WaveInEvent` is created whatever audio recording settings we desire. We also assign a function to the `DataAvailable` field. This function gets called every time an audio buffer is filled with data. + +```cs +private NAudio.Wave.WaveInEvent wvin; + +private void AudioMonitorInitialize( + int DeviceIndex, int sampleRate = 8000, int bitRate = 16, + int channels = 1, int bufferMilliseconds = 100, bool start = true + ) +{ + if (wvin == null) + { + wvin = new NAudio.Wave.WaveInEvent(); + wvin.DeviceNumber = DeviceIndex; + wvin.WaveFormat = new NAudio.Wave.WaveFormat(sampleRate, bitRate, channels); + wvin.DataAvailable += OnDataAvailable; + wvin.BufferMilliseconds = bufferMilliseconds; + if (start) + wvin.StartRecording(); + } +} + +``` + +```cs +private int bufferReadCount = 0; + +private void OnDataAvailable(object sender, NAudio.Wave.WaveInEventArgs args) +{ + int bytesPerSample = wvin.WaveFormat.BitsPerSample / 8; + int samplesRecorded = args.BytesRecorded / bytesPerSample; + Int16[] lastBuffer = new Int16[samplesRecorded]; + for (int i = 0; i < samplesRecorded; i++) + lastBuffer[i] = BitConverter.ToInt16(args.Buffer, i * bytesPerSample); + int lastBufferAmplitude = lastBuffer.Max() - lastBuffer.Min(); + bufferReadCount += 1; + Console.WriteLine($"Buffer {bufferReadCount} amplitude: {lastBufferAmplitude}"); +} +``` + +### Start/Stop Listening + +```cs +private void BtnStart_Click(object sender, EventArgs e) +{ + AudioMonitorInitialize(cbDevice.SelectedIndex); +} + +private void BtnStop_Click(object sender, EventArgs e) +{ + if (wvin!= null) + { + wvin.StopRecording(); + wvin = null; + } +} +``` + +### Plotting Amplitude with ScottPlot +I created a double array with `500` values and every time the buffer-filled function is called I use `bufferReadCount % 500` to indicate which buffer position to fill with the latest amplitude. I use the same technique to position the vertical line where I want it. I only have to plot this with ScottPlot once, and I user a timer to re-render the graph every 20ms. + +```cs +private double[] amplitudes; +private void PlotInitialize(int pointCount = 500) +{ + amplitudes = new double[pointCount]; + scottPlotUC1.plt.Clear(); + scottPlotUC1.plt.PlotSignal(amplitudes, sampleRate: 1000.0 / 20, markerSize: 0); + scottPlotUC1.plt.PlotVLine(0, color: Color.Red, lineWidth: 2); + scottPlotUC1.plt.PlotHLine(0, color: Color.Black, lineWidth: 2); + scottPlotUC1.plt.YLabel("Amplitude (%)"); + scottPlotUC1.plt.XLabel("Time (seconds)"); + scottPlotUC1.Render(); +} +``` + +```cs +private void Timer1_Tick(object sender, EventArgs e) +{ + if (cbAutoAxis.Checked) + scottPlotUC1.plt.AxisAuto(); + scottPlotUC1.Render(); +} +``` + + +### Output +![](screenshot.png) \ No newline at end of file diff --git a/examples/2019-06-06-audio-level-monitor/screenshot.png b/examples/2019-06-06-audio-level-monitor/screenshot.png new file mode 100644 index 0000000..d13dc28 Binary files /dev/null and b/examples/2019-06-06-audio-level-monitor/screenshot.png differ diff --git a/examples/2019-06-07-audio-visualizer/App.config b/examples/2019-06-07-audio-visualizer/App.config new file mode 100644 index 0000000..56efbc7 --- /dev/null +++ b/examples/2019-06-07-audio-visualizer/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/examples/2019-06-07-audio-visualizer/AudioPeak.csproj b/examples/2019-06-07-audio-visualizer/AudioPeak.csproj new file mode 100644 index 0000000..157adeb --- /dev/null +++ b/examples/2019-06-07-audio-visualizer/AudioPeak.csproj @@ -0,0 +1,90 @@ + + + + + Debug + AnyCPU + {9A49B8F0-FFB7-4F25-8FA7-14D239BF807D} + WinExe + AudioPeak + AudioPeak + v4.7.2 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + packages\NAudio.1.9.0\lib\net35\NAudio.dll + + + packages\ScottPlot.3.0.3\lib\net45\ScottPlot.dll + + + + + + + + + + + + + + + + Form + + + Form1.cs + + + + + Form1.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + + + \ No newline at end of file diff --git a/examples/2019-06-07-audio-visualizer/AudioPeak.sln b/examples/2019-06-07-audio-visualizer/AudioPeak.sln new file mode 100644 index 0000000..f765b26 --- /dev/null +++ b/examples/2019-06-07-audio-visualizer/AudioPeak.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28922.388 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AudioPeak", "AudioPeak.csproj", "{9A49B8F0-FFB7-4F25-8FA7-14D239BF807D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9A49B8F0-FFB7-4F25-8FA7-14D239BF807D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A49B8F0-FFB7-4F25-8FA7-14D239BF807D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A49B8F0-FFB7-4F25-8FA7-14D239BF807D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A49B8F0-FFB7-4F25-8FA7-14D239BF807D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1A0721F2-DB45-4148-BA38-3E7FCDCBECF4} + EndGlobalSection +EndGlobal diff --git a/examples/2019-06-07-audio-visualizer/Form1.Designer.cs b/examples/2019-06-07-audio-visualizer/Form1.Designer.cs new file mode 100644 index 0000000..8d1761b --- /dev/null +++ b/examples/2019-06-07-audio-visualizer/Form1.Designer.cs @@ -0,0 +1,137 @@ +namespace AudioPeak +{ + partial class Form1 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.cbDevice = new System.Windows.Forms.ComboBox(); + this.label1 = new System.Windows.Forms.Label(); + this.btnStart = new System.Windows.Forms.Button(); + this.btnStop = new System.Windows.Forms.Button(); + this.scottPlotUC1 = new ScottPlot.ScottPlotUC(); + this.timer1 = new System.Windows.Forms.Timer(this.components); + this.cbAutoAxis = new System.Windows.Forms.CheckBox(); + this.SuspendLayout(); + // + // cbDevice + // + this.cbDevice.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cbDevice.FormattingEnabled = true; + this.cbDevice.Location = new System.Drawing.Point(116, 12); + this.cbDevice.Name = "cbDevice"; + this.cbDevice.Size = new System.Drawing.Size(171, 21); + this.cbDevice.TabIndex = 0; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(12, 15); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(98, 13); + this.label1.TabIndex = 1; + this.label1.Text = "Audio input device:"; + // + // btnStart + // + this.btnStart.Location = new System.Drawing.Point(293, 11); + this.btnStart.Name = "btnStart"; + this.btnStart.Size = new System.Drawing.Size(75, 23); + this.btnStart.TabIndex = 2; + this.btnStart.Text = "Start"; + this.btnStart.UseVisualStyleBackColor = true; + this.btnStart.Click += new System.EventHandler(this.BtnStart_Click); + // + // btnStop + // + this.btnStop.Location = new System.Drawing.Point(374, 11); + this.btnStop.Name = "btnStop"; + this.btnStop.Size = new System.Drawing.Size(75, 23); + this.btnStop.TabIndex = 3; + this.btnStop.Text = "Stop"; + this.btnStop.UseVisualStyleBackColor = true; + this.btnStop.Click += new System.EventHandler(this.BtnStop_Click); + // + // scottPlotUC1 + // + this.scottPlotUC1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.scottPlotUC1.Location = new System.Drawing.Point(12, 40); + this.scottPlotUC1.Name = "scottPlotUC1"; + this.scottPlotUC1.Size = new System.Drawing.Size(776, 398); + this.scottPlotUC1.TabIndex = 4; + // + // timer1 + // + this.timer1.Enabled = true; + this.timer1.Interval = 20; + this.timer1.Tick += new System.EventHandler(this.Timer1_Tick); + // + // cbAutoAxis + // + this.cbAutoAxis.AutoSize = true; + this.cbAutoAxis.Checked = true; + this.cbAutoAxis.CheckState = System.Windows.Forms.CheckState.Checked; + this.cbAutoAxis.Location = new System.Drawing.Point(455, 14); + this.cbAutoAxis.Name = "cbAutoAxis"; + this.cbAutoAxis.Size = new System.Drawing.Size(69, 17); + this.cbAutoAxis.TabIndex = 5; + this.cbAutoAxis.Text = "Auto-axis"; + this.cbAutoAxis.UseVisualStyleBackColor = true; + // + // Form1 + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(800, 450); + this.Controls.Add(this.cbAutoAxis); + this.Controls.Add(this.scottPlotUC1); + this.Controls.Add(this.btnStop); + this.Controls.Add(this.btnStart); + this.Controls.Add(this.label1); + this.Controls.Add(this.cbDevice); + this.Name = "Form1"; + this.Text = "C# Audio Monitor"; + this.Load += new System.EventHandler(this.Form1_Load); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.ComboBox cbDevice; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Button btnStart; + private System.Windows.Forms.Button btnStop; + private ScottPlot.ScottPlotUC scottPlotUC1; + private System.Windows.Forms.Timer timer1; + private System.Windows.Forms.CheckBox cbAutoAxis; + } +} + diff --git a/examples/2019-06-07-audio-visualizer/Form1.cs b/examples/2019-06-07-audio-visualizer/Form1.cs new file mode 100644 index 0000000..0a3631d --- /dev/null +++ b/examples/2019-06-07-audio-visualizer/Form1.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace AudioPeak +{ + public partial class Form1 : Form + { + public Form1() + { + InitializeComponent(); + ScanSoundCards(); + PlotInitialize(); + } + + private void Form1_Load(object sender, EventArgs e) + { + } + + private void ScanSoundCards() + { + cbDevice.Items.Clear(); + for (int i = 0; i < NAudio.Wave.WaveIn.DeviceCount; i++) + cbDevice.Items.Add(NAudio.Wave.WaveIn.GetCapabilities(i).ProductName); + if (cbDevice.Items.Count > 0) + cbDevice.SelectedIndex = 0; + else + MessageBox.Show("ERROR: no recording devices available"); + } + + private double[] pcmValues; + private void PlotInitialize(int pointCount = 8_000 * 10) + { + pcmValues = new double[pointCount]; + scottPlotUC1.plt.Clear(); + scottPlotUC1.plt.PlotSignal(pcmValues, sampleRate: 8000, markerSize: 0); + scottPlotUC1.plt.PlotVLine(0, color: Color.Red, lineWidth: 2); + scottPlotUC1.plt.PlotHLine(0, color: Color.Black, lineWidth: 1); + scottPlotUC1.plt.YLabel("Amplitude (%)"); + scottPlotUC1.plt.XLabel("Time (seconds)"); + scottPlotUC1.Render(); + } + + private NAudio.Wave.WaveInEvent wvin; + private int buffersRead = 0; + private void OnDataAvailable(object sender, NAudio.Wave.WaveInEventArgs args) + { + int bytesPerSample = wvin.WaveFormat.BitsPerSample / 8; + int samplesRecorded = args.BytesRecorded / bytesPerSample; + int buffersToDisplay = 80_000 / samplesRecorded; + int offset = (buffersRead % buffersToDisplay) * samplesRecorded; + for (int i = 0; i < samplesRecorded; i++) + pcmValues[i + offset] = BitConverter.ToInt16(args.Buffer, i * bytesPerSample); + + buffersRead += 1; + + // TODO: make this sane + ScottPlot.PlottableAxLine axLine = (ScottPlot.PlottableAxLine)scottPlotUC1.plt.GetPlottables()[1]; + axLine.position = offset / 8000.0; + } + + private void AudioMonitorInitialize(int DeviceIndex, int sampleRate = 8000, int bitRate = 16, + int channels = 1, int bufferMilliseconds = 20, bool start = true) + { + if (wvin == null) + { + wvin = new NAudio.Wave.WaveInEvent(); + wvin.DeviceNumber = DeviceIndex; + wvin.WaveFormat = new NAudio.Wave.WaveFormat(sampleRate, bitRate, channels); + wvin.DataAvailable += OnDataAvailable; + wvin.BufferMilliseconds = bufferMilliseconds; + if (start) + wvin.StartRecording(); + } + } + + + private void BtnStart_Click(object sender, EventArgs e) + { + AudioMonitorInitialize(cbDevice.SelectedIndex); + } + + private void BtnStop_Click(object sender, EventArgs e) + { + if (wvin != null) + { + wvin.StopRecording(); + wvin = null; + } + } + + private void Timer1_Tick(object sender, EventArgs e) + { + if (cbAutoAxis.Checked) + { + scottPlotUC1.plt.AxisAuto(); + scottPlotUC1.plt.TightenLayout(); + } + scottPlotUC1.Render(); + } + } +} diff --git a/examples/2019-06-07-audio-visualizer/Form1.resx b/examples/2019-06-07-audio-visualizer/Form1.resx new file mode 100644 index 0000000..1f666f2 --- /dev/null +++ b/examples/2019-06-07-audio-visualizer/Form1.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file diff --git a/examples/2019-06-07-audio-visualizer/Program.cs b/examples/2019-06-07-audio-visualizer/Program.cs new file mode 100644 index 0000000..37a3c16 --- /dev/null +++ b/examples/2019-06-07-audio-visualizer/Program.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace AudioPeak +{ + static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new Form1()); + } + } +} diff --git a/examples/2019-06-07-audio-visualizer/Properties/AssemblyInfo.cs b/examples/2019-06-07-audio-visualizer/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..9bc7a0d --- /dev/null +++ b/examples/2019-06-07-audio-visualizer/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AudioPeak")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AudioPeak")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9a49b8f0-ffb7-4f25-8fa7-14d239bf807d")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/examples/2019-06-07-audio-visualizer/Properties/Resources.Designer.cs b/examples/2019-06-07-audio-visualizer/Properties/Resources.Designer.cs new file mode 100644 index 0000000..c52fdd9 --- /dev/null +++ b/examples/2019-06-07-audio-visualizer/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace AudioPeak.Properties +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AudioPeak.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/examples/2019-06-07-audio-visualizer/Properties/Resources.resx b/examples/2019-06-07-audio-visualizer/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/examples/2019-06-07-audio-visualizer/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/examples/2019-06-07-audio-visualizer/Properties/Settings.Designer.cs b/examples/2019-06-07-audio-visualizer/Properties/Settings.Designer.cs new file mode 100644 index 0000000..dbc1407 --- /dev/null +++ b/examples/2019-06-07-audio-visualizer/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace AudioPeak.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/examples/2019-06-07-audio-visualizer/Properties/Settings.settings b/examples/2019-06-07-audio-visualizer/Properties/Settings.settings new file mode 100644 index 0000000..3964565 --- /dev/null +++ b/examples/2019-06-07-audio-visualizer/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/2019-06-07-audio-visualizer/packages.config b/examples/2019-06-07-audio-visualizer/packages.config new file mode 100644 index 0000000..b0419de --- /dev/null +++ b/examples/2019-06-07-audio-visualizer/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/examples/2019-06-07-audio-visualizer/readme.md b/examples/2019-06-07-audio-visualizer/readme.md new file mode 100644 index 0000000..44caa10 --- /dev/null +++ b/examples/2019-06-07-audio-visualizer/readme.md @@ -0,0 +1,8 @@ +# Realtime Audio FFT Graph +This program builds on the previous example ([graphing audio levels](/examples/2019-06-06-audio-level-monitor/readme.md)) but uses [ScottPlot](https://github.com/swharden/ScottPlot)'s PlotSignal function to continuously display 10 seconds of raw PCM audio values. Notice that even though the audio data is continuously updating, the graph remains fully interactive... + +![](screenshot.gif) + +## Core Concepts + +### FFT \ No newline at end of file diff --git a/examples/2019-06-07-audio-visualizer/screenshot.gif b/examples/2019-06-07-audio-visualizer/screenshot.gif new file mode 100644 index 0000000..2823129 Binary files /dev/null and b/examples/2019-06-07-audio-visualizer/screenshot.gif differ diff --git a/examples/2019-06-08-audio-fft/App.config b/examples/2019-06-08-audio-fft/App.config new file mode 100644 index 0000000..56efbc7 --- /dev/null +++ b/examples/2019-06-08-audio-fft/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/examples/2019-06-08-audio-fft/AudioPeak.csproj b/examples/2019-06-08-audio-fft/AudioPeak.csproj new file mode 100644 index 0000000..157adeb --- /dev/null +++ b/examples/2019-06-08-audio-fft/AudioPeak.csproj @@ -0,0 +1,90 @@ + + + + + Debug + AnyCPU + {9A49B8F0-FFB7-4F25-8FA7-14D239BF807D} + WinExe + AudioPeak + AudioPeak + v4.7.2 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + packages\NAudio.1.9.0\lib\net35\NAudio.dll + + + packages\ScottPlot.3.0.3\lib\net45\ScottPlot.dll + + + + + + + + + + + + + + + + Form + + + Form1.cs + + + + + Form1.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + + + \ No newline at end of file diff --git a/examples/2019-06-08-audio-fft/AudioPeak.sln b/examples/2019-06-08-audio-fft/AudioPeak.sln new file mode 100644 index 0000000..f765b26 --- /dev/null +++ b/examples/2019-06-08-audio-fft/AudioPeak.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28922.388 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AudioPeak", "AudioPeak.csproj", "{9A49B8F0-FFB7-4F25-8FA7-14D239BF807D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9A49B8F0-FFB7-4F25-8FA7-14D239BF807D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A49B8F0-FFB7-4F25-8FA7-14D239BF807D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A49B8F0-FFB7-4F25-8FA7-14D239BF807D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A49B8F0-FFB7-4F25-8FA7-14D239BF807D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1A0721F2-DB45-4148-BA38-3E7FCDCBECF4} + EndGlobalSection +EndGlobal diff --git a/examples/2019-06-08-audio-fft/Form1.Designer.cs b/examples/2019-06-08-audio-fft/Form1.Designer.cs new file mode 100644 index 0000000..8d1761b --- /dev/null +++ b/examples/2019-06-08-audio-fft/Form1.Designer.cs @@ -0,0 +1,137 @@ +namespace AudioPeak +{ + partial class Form1 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.cbDevice = new System.Windows.Forms.ComboBox(); + this.label1 = new System.Windows.Forms.Label(); + this.btnStart = new System.Windows.Forms.Button(); + this.btnStop = new System.Windows.Forms.Button(); + this.scottPlotUC1 = new ScottPlot.ScottPlotUC(); + this.timer1 = new System.Windows.Forms.Timer(this.components); + this.cbAutoAxis = new System.Windows.Forms.CheckBox(); + this.SuspendLayout(); + // + // cbDevice + // + this.cbDevice.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cbDevice.FormattingEnabled = true; + this.cbDevice.Location = new System.Drawing.Point(116, 12); + this.cbDevice.Name = "cbDevice"; + this.cbDevice.Size = new System.Drawing.Size(171, 21); + this.cbDevice.TabIndex = 0; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(12, 15); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(98, 13); + this.label1.TabIndex = 1; + this.label1.Text = "Audio input device:"; + // + // btnStart + // + this.btnStart.Location = new System.Drawing.Point(293, 11); + this.btnStart.Name = "btnStart"; + this.btnStart.Size = new System.Drawing.Size(75, 23); + this.btnStart.TabIndex = 2; + this.btnStart.Text = "Start"; + this.btnStart.UseVisualStyleBackColor = true; + this.btnStart.Click += new System.EventHandler(this.BtnStart_Click); + // + // btnStop + // + this.btnStop.Location = new System.Drawing.Point(374, 11); + this.btnStop.Name = "btnStop"; + this.btnStop.Size = new System.Drawing.Size(75, 23); + this.btnStop.TabIndex = 3; + this.btnStop.Text = "Stop"; + this.btnStop.UseVisualStyleBackColor = true; + this.btnStop.Click += new System.EventHandler(this.BtnStop_Click); + // + // scottPlotUC1 + // + this.scottPlotUC1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.scottPlotUC1.Location = new System.Drawing.Point(12, 40); + this.scottPlotUC1.Name = "scottPlotUC1"; + this.scottPlotUC1.Size = new System.Drawing.Size(776, 398); + this.scottPlotUC1.TabIndex = 4; + // + // timer1 + // + this.timer1.Enabled = true; + this.timer1.Interval = 20; + this.timer1.Tick += new System.EventHandler(this.Timer1_Tick); + // + // cbAutoAxis + // + this.cbAutoAxis.AutoSize = true; + this.cbAutoAxis.Checked = true; + this.cbAutoAxis.CheckState = System.Windows.Forms.CheckState.Checked; + this.cbAutoAxis.Location = new System.Drawing.Point(455, 14); + this.cbAutoAxis.Name = "cbAutoAxis"; + this.cbAutoAxis.Size = new System.Drawing.Size(69, 17); + this.cbAutoAxis.TabIndex = 5; + this.cbAutoAxis.Text = "Auto-axis"; + this.cbAutoAxis.UseVisualStyleBackColor = true; + // + // Form1 + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(800, 450); + this.Controls.Add(this.cbAutoAxis); + this.Controls.Add(this.scottPlotUC1); + this.Controls.Add(this.btnStop); + this.Controls.Add(this.btnStart); + this.Controls.Add(this.label1); + this.Controls.Add(this.cbDevice); + this.Name = "Form1"; + this.Text = "C# Audio Monitor"; + this.Load += new System.EventHandler(this.Form1_Load); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.ComboBox cbDevice; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Button btnStart; + private System.Windows.Forms.Button btnStop; + private ScottPlot.ScottPlotUC scottPlotUC1; + private System.Windows.Forms.Timer timer1; + private System.Windows.Forms.CheckBox cbAutoAxis; + } +} + diff --git a/examples/2019-06-08-audio-fft/Form1.cs b/examples/2019-06-08-audio-fft/Form1.cs new file mode 100644 index 0000000..7ab778a --- /dev/null +++ b/examples/2019-06-08-audio-fft/Form1.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace AudioPeak +{ + public partial class Form1 : Form + { + public Form1() + { + InitializeComponent(); + ScanSoundCards(); + PlotInitialize(); + } + + private void Form1_Load(object sender, EventArgs e) + { + } + + private void ScanSoundCards() + { + cbDevice.Items.Clear(); + for (int i = 0; i < NAudio.Wave.WaveIn.DeviceCount; i++) + cbDevice.Items.Add(NAudio.Wave.WaveIn.GetCapabilities(i).ProductName); + if (cbDevice.Items.Count > 0) + cbDevice.SelectedIndex = 0; + else + MessageBox.Show("ERROR: no recording devices available"); + } + private void PlotInitialize() + { + if (dataFft != null) + { + scottPlotUC1.plt.Clear(); + double fftSpacing = (double)wvin.WaveFormat.SampleRate / dataFft.Length; + scottPlotUC1.plt.PlotSignal(dataFft, sampleRate: fftSpacing, markerSize: 0); + scottPlotUC1.plt.PlotHLine(0, color: Color.Black, lineWidth: 1); + scottPlotUC1.plt.YLabel("Power"); + scottPlotUC1.plt.XLabel("Frequency (kHz)"); + scottPlotUC1.Render(); + } + } + + private NAudio.Wave.WaveInEvent wvin; + private void AudioMonitorInitialize( + int DeviceIndex, int sampleRate = 32_000, + int bitRate = 16, int channels = 1, + int bufferMilliseconds = 50, bool start = true + ) + { + if (wvin == null) + { + wvin = new NAudio.Wave.WaveInEvent(); + wvin.DeviceNumber = DeviceIndex; + wvin.WaveFormat = new NAudio.Wave.WaveFormat(sampleRate, bitRate, channels); + wvin.DataAvailable += OnDataAvailable; + wvin.BufferMilliseconds = bufferMilliseconds; + if (start) + wvin.StartRecording(); + } + } + + Int16[] dataPcm; + private void OnDataAvailable(object sender, NAudio.Wave.WaveInEventArgs args) + { + int bytesPerSample = wvin.WaveFormat.BitsPerSample / 8; + int samplesRecorded = args.BytesRecorded / bytesPerSample; + if (dataPcm == null) + dataPcm = new Int16[samplesRecorded]; + for (int i = 0; i < samplesRecorded; i++) + dataPcm[i] = BitConverter.ToInt16(args.Buffer, i * bytesPerSample); + } + + double[] dataFft; + + private void updateFFT() + { + // the PCM size to be analyzed with FFT must be a power of 2 + int fftPoints = 2; + while (fftPoints * 2 <= dataPcm.Length) + fftPoints *= 2; + + // apply a Hamming window function as we load the FFT array then calculate the FFT + NAudio.Dsp.Complex[] fftFull = new NAudio.Dsp.Complex[fftPoints]; + for (int i = 0; i < fftPoints; i++) + fftFull[i].X = (float)(dataPcm[i] * NAudio.Dsp.FastFourierTransform.HammingWindow(i, fftPoints)); + NAudio.Dsp.FastFourierTransform.FFT(true, (int)Math.Log(fftPoints, 2.0), fftFull); + + // copy the complex values into the double array that will be plotted + if (dataFft == null) + dataFft = new double[fftPoints / 2]; + for (int i = 0; i < fftPoints / 2; i++) + { + double fftLeft = Math.Abs(fftFull[i].X + fftFull[i].Y); + double fftRight = Math.Abs(fftFull[fftPoints - i - 1].X + fftFull[fftPoints - i - 1].Y); + dataFft[i] = fftLeft + fftRight; + } + } + + private void BtnStart_Click(object sender, EventArgs e) + { + AudioMonitorInitialize(cbDevice.SelectedIndex); + } + + private void BtnStop_Click(object sender, EventArgs e) + { + if (wvin != null) + { + wvin.StopRecording(); + wvin = null; + } + } + + private void Timer1_Tick(object sender, EventArgs e) + { + if (dataPcm == null) + return; + + updateFFT(); + + if (scottPlotUC1.plt.GetPlottables().Count == 0) + PlotInitialize(); + + if (cbAutoAxis.Checked) + { + scottPlotUC1.plt.AxisAuto(); + scottPlotUC1.plt.TightenLayout(); + } + scottPlotUC1.Render(); + } + } +} diff --git a/examples/2019-06-08-audio-fft/Form1.resx b/examples/2019-06-08-audio-fft/Form1.resx new file mode 100644 index 0000000..1f666f2 --- /dev/null +++ b/examples/2019-06-08-audio-fft/Form1.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file diff --git a/examples/2019-06-08-audio-fft/Program.cs b/examples/2019-06-08-audio-fft/Program.cs new file mode 100644 index 0000000..37a3c16 --- /dev/null +++ b/examples/2019-06-08-audio-fft/Program.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace AudioPeak +{ + static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new Form1()); + } + } +} diff --git a/examples/2019-06-08-audio-fft/Properties/AssemblyInfo.cs b/examples/2019-06-08-audio-fft/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..9bc7a0d --- /dev/null +++ b/examples/2019-06-08-audio-fft/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AudioPeak")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AudioPeak")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9a49b8f0-ffb7-4f25-8fa7-14d239bf807d")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/examples/2019-06-08-audio-fft/Properties/Resources.Designer.cs b/examples/2019-06-08-audio-fft/Properties/Resources.Designer.cs new file mode 100644 index 0000000..c52fdd9 --- /dev/null +++ b/examples/2019-06-08-audio-fft/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace AudioPeak.Properties +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AudioPeak.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/examples/2019-06-08-audio-fft/Properties/Resources.resx b/examples/2019-06-08-audio-fft/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/examples/2019-06-08-audio-fft/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/examples/2019-06-08-audio-fft/Properties/Settings.Designer.cs b/examples/2019-06-08-audio-fft/Properties/Settings.Designer.cs new file mode 100644 index 0000000..dbc1407 --- /dev/null +++ b/examples/2019-06-08-audio-fft/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace AudioPeak.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/examples/2019-06-08-audio-fft/Properties/Settings.settings b/examples/2019-06-08-audio-fft/Properties/Settings.settings new file mode 100644 index 0000000..3964565 --- /dev/null +++ b/examples/2019-06-08-audio-fft/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/2019-06-08-audio-fft/packages.config b/examples/2019-06-08-audio-fft/packages.config new file mode 100644 index 0000000..b0419de --- /dev/null +++ b/examples/2019-06-08-audio-fft/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/examples/2019-06-08-audio-fft/readme.md b/examples/2019-06-08-audio-fft/readme.md new file mode 100644 index 0000000..fced919 --- /dev/null +++ b/examples/2019-06-08-audio-fft/readme.md @@ -0,0 +1,79 @@ +# Graphing FFT Audio Data +Many applications benefit from measuring the _frequency_ component of audio (or other signals). A simple way to convert a linear stream of data into its frequency components is by applying the [Fast Fourier transform](https://en.wikipedia.org/wiki/Fast_Fourier_transform). + +This example uses the FFT provided with [NAudio](https://github.com/naudio/NAudio/) to graph the frequency component of a PCN signal from an audio input device. This program builds on the previous examples [graphing PCM data](/examples/2019-06-07-audio-visualizer/readme.md) and [graphing audio levels](/examples/2019-06-06-audio-level-monitor/readme.md) which use [ScottPlot](https://github.com/swharden/ScottPlot)'s PlotSignal function to continuously display data. + +![](screenshot.gif) + +## Core Concepts + +### Notes about the FFT + +* The FFT procedure will take in `pcmData` and output `fftData`. +* The FFT must be fed with PCM data whose length is a power of 2. +* The length of `fftData` will be half the `pcmData` Length. +* The maximum frequency of the FFT is always half of the inverse of the sample rate. +* Use a [windowing function](https://en.wikipedia.org/wiki/Window_function) on the PCM input to reduce artifacts on the FFT output. + +### Capturing PCM Data +This is similar to before, but notice I only record and save a single buffer any time. Notice this data capture function isn't weighed-down with FFT processing. To save a bit of memory I keep the PCM data as `Int16` (it'll just get converted to a complex number later). + +```cs +Int16[] dataPcm; + +private void OnDataAvailable(object sender, NAudio.Wave.WaveInEventArgs args) +{ + int bytesPerSample = wvin.WaveFormat.BitsPerSample / 8; + int samplesRecorded = args.BytesRecorded / bytesPerSample; + if (dataPcm == null) + dataPcm = new Int16[samplesRecorded]; + for (int i = 0; i < samplesRecorded; i++) + dataPcm[i] = BitConverter.ToInt16(args.Buffer, i * bytesPerSample); +} +``` + +### Performing the FFT +The only fancy thing this does is find the largest power of 2 that's less than the length of `dataPcm` and only FFT-process those values. Note that the FFT output is complex, and the real and imaginary components are added together to produce data values for the output array. + +```cs +private double[] dataFft; + +private void updateFFT() +{ + // the PCM size to be analyzed with FFT must be a power of 2 + int fftPoints = 2; + while (fftPoints * 2 <= dataPcm.Length) + fftPoints *= 2; + + // apply a Hamming window function as we load the FFT array then calculate the FFT + NAudio.Dsp.Complex[] fftFull = new NAudio.Dsp.Complex[fftPoints]; + for (int i = 0; i < fftPoints; i++) + fftFull[i].X = (float)(dataPcm[i] * NAudio.Dsp.FastFourierTransform.HammingWindow(i, fftPoints)); + NAudio.Dsp.FastFourierTransform.FFT(true, (int)Math.Log(fftPoints, 2.0), fftFull); + + // copy the complex values into the double array that will be plotted + if (dataFft == null) + dataFft = new double[fftPoints / 2]; + for (int i = 0; i < fftPoints / 2; i++) + { + double fftLeft = Math.Abs(fftFull[i].X + fftFull[i].Y); + double fftRight = Math.Abs(fftFull[fftPoints - i - 1].X + fftFull[fftPoints - i - 1].Y); + dataFft[i] = fftLeft + fftRight; + } +} +``` + +### Updating the Display +I placed the FFT processing call in the timer to make sure it doesn't slow down the data capture function. Every time the timer runs the FFT is updated and plotted. + +```cs +private void Timer1_Tick(object sender, EventArgs e) +{ + updateFFT(); + scottPlotUC1.Render(); +} +``` + +### Want a smoother output? + +To get a smoother output use a large input buffer and store a few buffers in memory. Then step through the input buffers fractionally (perhaps 1/10th of a buffer size) and calculate the FFT and graph it for each fractional step. In other words, lay two buffers' data side by side and slide across it calculating the FFT many times as you go. \ No newline at end of file diff --git a/examples/2019-06-08-audio-fft/screenshot.gif b/examples/2019-06-08-audio-fft/screenshot.gif new file mode 100644 index 0000000..719fa5a Binary files /dev/null and b/examples/2019-06-08-audio-fft/screenshot.gif differ