diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..43945c8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,215 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+x64/
+build/
+bld/
+[Bb]in/
+[Oo]bj/
+
+# Roslyn cache directories
+*.ide/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+#NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding addin-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+## TODO: Comment the next line if you want to checkin your
+## web deploy settings but do note that will include unencrypted
+## passwords
+#*.pubxml
+
+# NuGet Packages Directory
+packages/*
+## TODO: If the tool you use requires repositories.config
+## uncomment the next line
+#!packages/repositories.config
+
+# Enable "build/" folder in the NuGet Packages folder since
+# NuGet packages use it for MSBuild targets.
+# This line needs to be after the ignore of the build folder
+# (and the packages folder if the line above has been uncommented)
+!packages/build/
+
+# Windows Azure Build Output
+csx/
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Others
+sql/
+*.Cache
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# LightSwitch generated files
+GeneratedArtifacts/
+_Pvt_Extensions/
+ModelManifest.xml
+
+# Lattice
+work/
+synwork/
+syntmp/
+synlog/
+
+# GVIM
+*.v~
+*.swp
+
+*.rel
+*.sym
+*.lst
+*.asm
+*.adb
+*.ihx
+*.lk
+*.map
+*.mem
+*.noi
+*.d
+
+*.lib
+*.rst
+sta[0-9][0-9][0-9][0-9][0-9]
\ No newline at end of file
diff --git a/MorseTrainer.sln b/MorseTrainer.sln
new file mode 100644
index 0000000..5868d0e
--- /dev/null
+++ b/MorseTrainer.sln
@@ -0,0 +1,22 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 14
+VisualStudioVersion = 14.0.24720.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MorseTrainer", "MorseTrainer\MorseTrainer.csproj", "{48E042F6-B6A3-4ABF-B272-A76F9533FDF9}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {48E042F6-B6A3-4ABF-B272-A76F9533FDF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {48E042F6-B6A3-4ABF-B272-A76F9533FDF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {48E042F6-B6A3-4ABF-B272-A76F9533FDF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {48E042F6-B6A3-4ABF-B272-A76F9533FDF9}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/MorseTrainer/Analyzer.cs b/MorseTrainer/Analyzer.cs
new file mode 100644
index 0000000..8aba9f3
--- /dev/null
+++ b/MorseTrainer/Analyzer.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MorseTrainer
+{
+ public class Analyzer
+ {
+ public Analyzer(System.Windows.Forms.RichTextBox resultsRTB)
+ {
+ _resultsRTB = resultsRTB;
+ }
+
+ public void Analyze(String sent, String recorded)
+ {
+ _resultsRTB.Clear();
+ MorseCompareResults results = Comparer.Compare(sent, recorded);
+ Write("I sent : ");
+ ResultsDisplayFlags flags = ResultsDisplayFlags.Valid | ResultsDisplayFlags.Dropped;
+ foreach (MorseSubstring substring in results.SubStrings)
+ {
+ Write(substring.Str(flags), substring.Color);
+ }
+ Write(Environment.NewLine);
+
+ Write("You typed: ");
+ flags = ResultsDisplayFlags.Valid | ResultsDisplayFlags.Extra;
+ foreach (MorseSubstring substring in results.SubStrings)
+ {
+ Write(substring.Str(flags), substring.Color);
+ }
+ Write(Environment.NewLine);
+
+ }
+
+ private void Write(String text)
+ {
+ _resultsRTB.AppendText(text);
+ }
+
+ private void Write(String text, System.Drawing.Color color)
+ {
+ _resultsRTB.SelectionStart = _resultsRTB.TextLength;
+ _resultsRTB.SelectionLength = 0;
+ _resultsRTB.SelectionColor = color;
+ _resultsRTB.AppendText(text);
+ _resultsRTB.SelectionColor = _resultsRTB.ForeColor;
+ }
+
+ private System.Windows.Forms.RichTextBox _resultsRTB;
+ }
+}
diff --git a/MorseTrainer/App.config b/MorseTrainer/App.config
new file mode 100644
index 0000000..88fa402
--- /dev/null
+++ b/MorseTrainer/App.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MorseTrainer/CharGenerator.cs b/MorseTrainer/CharGenerator.cs
new file mode 100644
index 0000000..0f7a361
--- /dev/null
+++ b/MorseTrainer/CharGenerator.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MorseTrainer
+{
+ public class CharGenerator
+ {
+ public const int STRING_LENGTH_MIN = 2;
+ public const int STRING_LENGTH_MAX = 7;
+
+ public enum Method
+ {
+ Koch,
+ Custom
+ };
+
+ public CharGenerator()
+ {
+ _randomizer = new Random();
+ }
+
+ ///
+ /// Get or set the generation method
+ ///
+ public Method GenerationMethod
+ {
+ get
+ {
+ return _method;
+ }
+ set
+ {
+ _method = value;
+ }
+ }
+
+ ///
+ /// Get or set the index into the Koch order. Must be at least 1
+ ///
+ public int KochIndex
+ {
+ get
+ {
+ return _kochIndex;
+ }
+ set
+ {
+ if (value < 0 || value > Koch.Length)
+ {
+ throw new ArgumentException("KochIndex");
+ }
+ _kochIndex = Math.Max(value, 1);
+ }
+ }
+
+ ///
+ /// Get or set whether to favor new characters in Koch order
+ ///
+ public bool FavorNew
+ {
+ get
+ {
+ return _favorNew;
+ }
+ set
+ {
+ _favorNew = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the custom string for custom. Characters are pulled from
+ /// this string randomly in the custom method.
+ ///
+ public String Custom
+ {
+ get
+ {
+ return _custom;
+ }
+ set
+ {
+ _custom = value;
+ }
+ }
+
+ ///
+ /// Creates a random string. The caller is responsible for spaces
+ ///
+ /// A stringf containing characters to send
+ public String CreateRandomString()
+ {
+ int size = 2 + _randomizer.Next() % (STRING_LENGTH_MAX - STRING_LENGTH_MIN);
+ StringBuilder cc = new StringBuilder();
+ for (int i = 0; i < size; ++i)
+ {
+ cc.Append(CreateRandomChar());
+ }
+ return cc.ToString();
+ }
+
+ ///
+ /// Gets a random character
+ ///
+ /// A character
+ private char CreateRandomChar()
+ {
+ int rangeStart;
+ int rangeLength;
+ int index;
+ char c;
+ String s;
+
+ if (_method == Method.Koch)
+ {
+ rangeStart = 0;
+ rangeLength = _kochIndex+1;
+ if (_favorNew)
+ {
+ // reduce the range to the upper part once in a while
+ if (_randomizer.Next() % 4 == 0)
+ {
+ rangeStart = Math.Max(0, rangeLength - 4);
+ rangeLength -= rangeStart;
+ }
+ }
+ s = Koch.Order;
+ }
+ else
+ {
+ rangeStart = 0;
+ rangeLength = _custom.Length;
+ s = _custom;
+ }
+ index = rangeStart + _randomizer.Next() % rangeLength;
+ c = s[index];
+ return c;
+ }
+
+ private Method _method;
+ private int _kochIndex;
+ private String _custom;
+ private bool _favorNew;
+ private Random _randomizer;
+ }
+}
diff --git a/MorseTrainer/Comparer.cs b/MorseTrainer/Comparer.cs
new file mode 100644
index 0000000..5f51ea8
--- /dev/null
+++ b/MorseTrainer/Comparer.cs
@@ -0,0 +1,140 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MorseTrainer
+{
+ public class Comparer
+ {
+ private static readonly int[] matchLengthThresholdArray = { 5, 3, 2 };
+
+ public static MorseCompareResults Compare(string sent, string recorded)
+ {
+ const int SEARCH_AMOUNT = 20;
+
+ int sentStart = 0;
+ int recordedStart = 0;
+ int sentLength = sent.Length;
+ int recordedLength = recorded.Length;
+ int sum = 0;
+ int sentOffset = 0;
+ int recordedOffset = 0;
+ int matched = 0;
+ int threshold;
+ int matchedTotal = 0;
+
+ List substringList = new List();
+
+ // As long as there are unprocessed characters
+ while (sentStart < sentLength || recordedStart < recordedLength)
+ {
+ bool searching = true;
+ int sentRemaining = sentLength - sentStart;
+ int recordedRemaining = recordedLength - recordedStart;
+ int maxSum = sentRemaining + recordedRemaining;
+
+ // start with a higher threshold to filter false positives, then loosen
+ for (int thresholdIndex = 0; searching && (thresholdIndex < matchLengthThresholdArray.Length); ++thresholdIndex)
+ {
+ // a match is considered a match if it is contains at least 'threshold' contiguous matching characters
+ threshold = matchLengthThresholdArray[thresholdIndex];
+ int searchAmount = Math.Min(SEARCH_AMOUNT, maxSum);
+
+ // The algorithm here is to match positive offsets on each string
+ // starting from (0,0) (0,1) (1,0) (0,2) (1,1) (2,0) (0,3) (1,2) (2,1) (3,0) (0,4) ...
+ // until (0,searchAmount) ... (searchAmount,0)
+ for (sum = 0; searching && (sum < searchAmount); ++sum)
+ {
+ for (sentOffset = 0; sentOffset <= sum; ++sentOffset)
+ {
+ // get the respective offsets from the start
+ recordedOffset = sum - sentOffset;
+
+ // number of matching characters starting at the respective offsets
+ matched = matchCount(sent, recorded, sentStart + sentOffset, recordedStart + recordedOffset);
+
+ // got matched, leave sentOffset for loop
+ if (matched >= threshold)
+ {
+ // found!
+ searching = false;
+ break;
+ }
+ }
+ }
+ }
+
+ // At this point we have matched and the offsets
+ if (searching)
+ {
+ // didn't find a match--punt and just put everything into the mismatch output
+ sentOffset = sentLength - sentStart;
+ recordedOffset = recordedLength - recordedStart;
+ }
+
+ // sum > 0 means that there was either extra or dropped characters detected before a substring match
+ if (sum > 0)
+ {
+ if (sentOffset > 0)
+ {
+ // at least one character was dropped
+ MorseSubstring dropped = new MorseDropped(sent.Substring(sentStart, sentOffset));
+ substringList.Add(dropped);
+
+ // skip over the dropped characters to the start of the match
+ sentStart += sentOffset;
+ }
+ if (recordedOffset > 0)
+ {
+ // at least one unsent character was detected before a substring match
+ MorseSubstring extra = new MorseExtra(recorded.Substring(recordedStart, recordedOffset));
+ substringList.Add(extra);
+
+ // skip over the extra characters to the start of the match
+ recordedStart += recordedOffset;
+ }
+ }
+ if (matched > 0)
+ {
+ // there was a match
+ MorseSubstring valid = new MorseValid(sent.Substring(sentStart, matched));
+ substringList.Add(valid);
+
+ // skip over the match in each string
+ recordedStart += matched;
+ sentStart += matched;
+
+ // keep track of the matched total
+ matchedTotal += matched;
+ }
+ }
+
+ MorseCompareResults results = new MorseCompareResults(substringList, sent, recorded);
+ return results;
+ }
+
+ private static int matchCount(string string1, string string2, int str1Offset, int str2Offset)
+ {
+ if (str1Offset >= string1.Length || str2Offset >= string2.Length)
+ {
+ return 0;
+ }
+
+ int str1Comparable = string1.Length - str1Offset;
+ int str2Comparable = string2.Length - str2Offset;
+
+ int maxCount = Math.Min(str1Comparable, str2Comparable);
+ int match = 0;
+ for (; match < maxCount; ++match)
+ {
+ if (string1[str1Offset + match] != string2[str2Offset + match])
+ {
+ return match;
+ }
+ }
+ return match;
+ }
+ }
+}
diff --git a/MorseTrainer/Config.cs b/MorseTrainer/Config.cs
new file mode 100644
index 0000000..e5f508d
--- /dev/null
+++ b/MorseTrainer/Config.cs
@@ -0,0 +1,218 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MorseTrainer
+{
+ [Serializable]
+ public class Config
+ {
+ public static readonly Config Default;
+
+ static Config()
+ {
+ Default = new Config();
+ Default._frequency = 1000;
+ Default._wpm = 20;
+ Default._farnsworthWpm = 13;
+ Default._duration = 60;
+ Default._startDelay = 2;
+ Default._stopDelay = 2;
+ Default._volume = 1.0f;
+
+ Default._method = CharGenerator.Method.Koch;
+ Default._kochIndex = 1;
+ Default._favorNew = true;
+ Default._custom = "";
+ }
+
+ private UInt16 _frequency;
+
+ ///
+ /// Gets or sets the frequency in Hz
+ ///
+ public UInt16 Frequency
+ {
+ get
+ {
+ return _frequency;
+ }
+ set
+ {
+ _frequency = value;
+ }
+ }
+
+ private float _wpm;
+
+ ///
+ /// Gets or sets the frequency in WPM in 0.5 increments
+ ///
+ public float WPM
+ {
+ get
+ {
+ return _wpm;
+ }
+ set
+ {
+ _wpm = value;
+ }
+ }
+
+ private float _farnsworthWpm;
+
+ ///
+ /// Gets or sets the frequency in Farnsworth WPM in 0.5 increments
+ ///
+ public float FarnsworthWPM
+ {
+ get
+ {
+ return _farnsworthWpm;
+ }
+ set
+ {
+ _farnsworthWpm = value;
+ }
+ }
+
+ private UInt16 _duration;
+
+ ///
+ /// Gets or sets the running duration in seconds increments
+ ///
+ public UInt16 Duration
+ {
+ get
+ {
+ return _duration;
+ }
+ set
+ {
+ _duration = value;
+ }
+ }
+
+ private UInt16 _startDelay;
+
+ ///
+ /// Gets or sets the start delay in seconds
+ ///
+ public UInt16 StartDelay
+ {
+ get
+ {
+ return _startDelay;
+ }
+ set
+ {
+ _startDelay = value;
+ }
+ }
+
+ private UInt16 _stopDelay;
+
+ ///
+ /// Gets or sets the stop delay in seconds
+ ///
+ public UInt16 StopDelay
+ {
+ get
+ {
+ return _stopDelay;
+ }
+ set
+ {
+ _stopDelay = value;
+ }
+ }
+
+ private float _volume;
+
+ ///
+ /// Gets or sets the volume in 0.0f - 1.0f
+ ///
+ public float Volume
+ {
+ get
+ {
+ return _volume;
+ }
+ set
+ {
+ _volume = value;
+ }
+ }
+
+ private UInt16 _kochIndex;
+
+ ///
+ /// Gets or sets the Koch order index
+ ///
+ public UInt16 KochIndex
+ {
+ get
+ {
+ return _kochIndex;
+ }
+ set
+ {
+ _kochIndex = value;
+ }
+ }
+
+ private CharGenerator.Method _method;
+
+ ///
+ /// Gets or sets the generation method (Koch or Custom)
+ ///
+ public CharGenerator.Method GenerationMethod
+ {
+ get
+ {
+ return _method;
+ }
+ set
+ {
+ _method = value;
+ }
+ }
+
+ private String _custom;
+
+ ///
+ /// Gets or sets the custom string generator
+ ///
+ public String Custom
+ {
+ get
+ {
+ return _custom;
+ }
+ set
+ {
+ _custom = value;
+ }
+ }
+
+ private bool _favorNew;
+
+ ///
+ /// Gets or sets whether to favor newly learned Koch characters
+ ///
+ public bool FavorNew
+ {
+ get
+ {
+ return _favorNew;
+ }
+ set
+ {
+ _favorNew = value;
+ }
+ }
+ }
+}
diff --git a/MorseTrainer/Form1.Designer.cs b/MorseTrainer/Form1.Designer.cs
new file mode 100644
index 0000000..f39270f
--- /dev/null
+++ b/MorseTrainer/Form1.Designer.cs
@@ -0,0 +1,437 @@
+namespace MorseTrainer
+{
+ 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();
+
+ // Manually entered disposals
+ if (_player != null)
+ {
+ _player.Dispose();
+ _player = null;
+ }
+ if (_runner != null)
+ {
+ _runner.Dispose();
+ _runner = null;
+ }
+ }
+ 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.label1 = new System.Windows.Forms.Label();
+ this.label2 = new System.Windows.Forms.Label();
+ this.label3 = new System.Windows.Forms.Label();
+ this.label4 = new System.Windows.Forms.Label();
+ this.sliderFrequency = new System.Windows.Forms.TrackBar();
+ this.sliderWPM = new System.Windows.Forms.TrackBar();
+ this.sliderDuration = new System.Windows.Forms.TrackBar();
+ this.sliderFarnsworth = new System.Windows.Forms.TrackBar();
+ this.txtAnalysis = new System.Windows.Forms.RichTextBox();
+ this.label5 = new System.Windows.Forms.Label();
+ this.label6 = new System.Windows.Forms.Label();
+ this.label7 = new System.Windows.Forms.Label();
+ this.sliderStartDelay = new System.Windows.Forms.TrackBar();
+ this.sliderStopDelay = new System.Windows.Forms.TrackBar();
+ this.txtFrequency = new System.Windows.Forms.TextBox();
+ this.txtWPM = new System.Windows.Forms.TextBox();
+ this.txtFarnsworth = new System.Windows.Forms.TextBox();
+ this.txtDuration = new System.Windows.Forms.TextBox();
+ this.btnStartStop = new System.Windows.Forms.Button();
+ this.btnKoch = new System.Windows.Forms.RadioButton();
+ this.btnCustom = new System.Windows.Forms.RadioButton();
+ this.cmbKoch = new System.Windows.Forms.ComboBox();
+ this.txtCustom = new System.Windows.Forms.TextBox();
+ this.chkFavorNew = new System.Windows.Forms.CheckBox();
+ this.sliderVolume = new System.Windows.Forms.TrackBar();
+ this.label8 = new System.Windows.Forms.Label();
+ this.txtStartDelay = new System.Windows.Forms.TextBox();
+ this.txtStopDelay = new System.Windows.Forms.TextBox();
+ this.txtVolume = new System.Windows.Forms.TextBox();
+ ((System.ComponentModel.ISupportInitialize)(this.sliderFrequency)).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)(this.sliderWPM)).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)(this.sliderDuration)).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)(this.sliderFarnsworth)).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)(this.sliderStartDelay)).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)(this.sliderStopDelay)).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)(this.sliderVolume)).BeginInit();
+ this.SuspendLayout();
+ //
+ // label1
+ //
+ this.label1.AutoSize = true;
+ this.label1.Location = new System.Drawing.Point(16, 23);
+ this.label1.Name = "label1";
+ this.label1.Size = new System.Drawing.Size(84, 20);
+ this.label1.TabIndex = 0;
+ this.label1.Text = "Frequency";
+ //
+ // label2
+ //
+ this.label2.AutoSize = true;
+ this.label2.Location = new System.Drawing.Point(16, 62);
+ this.label2.Name = "label2";
+ this.label2.Size = new System.Drawing.Size(47, 20);
+ this.label2.TabIndex = 1;
+ this.label2.Text = "WPM";
+ //
+ // label3
+ //
+ this.label3.AutoSize = true;
+ this.label3.Location = new System.Drawing.Point(16, 135);
+ this.label3.Name = "label3";
+ this.label3.Size = new System.Drawing.Size(112, 20);
+ this.label3.TabIndex = 3;
+ this.label3.Text = "Send Duration";
+ //
+ // label4
+ //
+ this.label4.AutoSize = true;
+ this.label4.Location = new System.Drawing.Point(16, 96);
+ this.label4.Name = "label4";
+ this.label4.Size = new System.Drawing.Size(131, 20);
+ this.label4.TabIndex = 2;
+ this.label4.Text = "Farnsworth WPM";
+ //
+ // sliderFrequency
+ //
+ this.sliderFrequency.LargeChange = 100;
+ this.sliderFrequency.Location = new System.Drawing.Point(229, 25);
+ this.sliderFrequency.Maximum = 1000;
+ this.sliderFrequency.Minimum = 400;
+ this.sliderFrequency.Name = "sliderFrequency";
+ this.sliderFrequency.Size = new System.Drawing.Size(637, 69);
+ this.sliderFrequency.SmallChange = 50;
+ this.sliderFrequency.TabIndex = 4;
+ this.sliderFrequency.TickFrequency = 50;
+ this.sliderFrequency.Value = 700;
+ this.sliderFrequency.Scroll += new System.EventHandler(this.sliderFrequency_Scroll);
+ //
+ // sliderWPM
+ //
+ this.sliderWPM.Location = new System.Drawing.Point(229, 62);
+ this.sliderWPM.Maximum = 80;
+ this.sliderWPM.Minimum = 8;
+ this.sliderWPM.Name = "sliderWPM";
+ this.sliderWPM.Size = new System.Drawing.Size(637, 69);
+ this.sliderWPM.TabIndex = 5;
+ this.sliderWPM.Value = 20;
+ this.sliderWPM.Scroll += new System.EventHandler(this.sliderWPM_Scroll);
+ //
+ // sliderDuration
+ //
+ this.sliderDuration.Location = new System.Drawing.Point(229, 137);
+ this.sliderDuration.Minimum = 1;
+ this.sliderDuration.Name = "sliderDuration";
+ this.sliderDuration.Size = new System.Drawing.Size(637, 69);
+ this.sliderDuration.TabIndex = 7;
+ this.sliderDuration.Value = 2;
+ this.sliderDuration.Scroll += new System.EventHandler(this.sliderDuration_Scroll);
+ //
+ // sliderFarnsworth
+ //
+ this.sliderFarnsworth.Location = new System.Drawing.Point(229, 100);
+ this.sliderFarnsworth.Maximum = 80;
+ this.sliderFarnsworth.Minimum = 8;
+ this.sliderFarnsworth.Name = "sliderFarnsworth";
+ this.sliderFarnsworth.Size = new System.Drawing.Size(637, 69);
+ this.sliderFarnsworth.TabIndex = 6;
+ this.sliderFarnsworth.Value = 13;
+ this.sliderFarnsworth.Scroll += new System.EventHandler(this.sliderFarnsworth_Scroll);
+ //
+ // txtAnalysis
+ //
+ this.txtAnalysis.Location = new System.Drawing.Point(20, 288);
+ this.txtAnalysis.Name = "txtAnalysis";
+ this.txtAnalysis.ReadOnly = true;
+ this.txtAnalysis.Size = new System.Drawing.Size(955, 177);
+ this.txtAnalysis.TabIndex = 8;
+ this.txtAnalysis.Text = "";
+ //
+ // label5
+ //
+ this.label5.AutoSize = true;
+ this.label5.Location = new System.Drawing.Point(22, 247);
+ this.label5.Name = "label5";
+ this.label5.Size = new System.Drawing.Size(182, 20);
+ this.label5.TabIndex = 9;
+ this.label5.Text = "Your Recording/Analysis";
+ //
+ // label6
+ //
+ this.label6.AutoSize = true;
+ this.label6.Location = new System.Drawing.Point(16, 186);
+ this.label6.Name = "label6";
+ this.label6.Size = new System.Drawing.Size(88, 20);
+ this.label6.TabIndex = 10;
+ this.label6.Text = "Start Delay";
+ //
+ // label7
+ //
+ this.label7.AutoSize = true;
+ this.label7.Location = new System.Drawing.Point(428, 186);
+ this.label7.Name = "label7";
+ this.label7.Size = new System.Drawing.Size(87, 20);
+ this.label7.TabIndex = 11;
+ this.label7.Text = "Stop Delay";
+ //
+ // sliderStartDelay
+ //
+ this.sliderStartDelay.Location = new System.Drawing.Point(229, 175);
+ this.sliderStartDelay.Maximum = 5;
+ this.sliderStartDelay.Name = "sliderStartDelay";
+ this.sliderStartDelay.Size = new System.Drawing.Size(135, 69);
+ this.sliderStartDelay.TabIndex = 12;
+ this.sliderStartDelay.Value = 2;
+ this.sliderStartDelay.Scroll += new System.EventHandler(this.sliderStartDelay_Scroll);
+ //
+ // sliderStopDelay
+ //
+ this.sliderStopDelay.Location = new System.Drawing.Point(521, 175);
+ this.sliderStopDelay.Maximum = 5;
+ this.sliderStopDelay.Name = "sliderStopDelay";
+ this.sliderStopDelay.Size = new System.Drawing.Size(128, 69);
+ this.sliderStopDelay.TabIndex = 13;
+ this.sliderStopDelay.Value = 2;
+ this.sliderStopDelay.Scroll += new System.EventHandler(this.sliderStopDelay_Scroll);
+ //
+ // txtFrequency
+ //
+ this.txtFrequency.Location = new System.Drawing.Point(887, 27);
+ this.txtFrequency.Name = "txtFrequency";
+ this.txtFrequency.Size = new System.Drawing.Size(88, 26);
+ this.txtFrequency.TabIndex = 14;
+ this.txtFrequency.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.txtFrequency_KeyPress);
+ //
+ // txtWPM
+ //
+ this.txtWPM.Location = new System.Drawing.Point(887, 64);
+ this.txtWPM.Name = "txtWPM";
+ this.txtWPM.Size = new System.Drawing.Size(88, 26);
+ this.txtWPM.TabIndex = 15;
+ this.txtWPM.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.txtWPM_KeyPress);
+ //
+ // txtFarnsworth
+ //
+ this.txtFarnsworth.Location = new System.Drawing.Point(887, 102);
+ this.txtFarnsworth.Name = "txtFarnsworth";
+ this.txtFarnsworth.Size = new System.Drawing.Size(88, 26);
+ this.txtFarnsworth.TabIndex = 16;
+ this.txtFarnsworth.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.txtFarnsworth_KeyPress);
+ //
+ // txtDuration
+ //
+ this.txtDuration.Location = new System.Drawing.Point(887, 137);
+ this.txtDuration.Name = "txtDuration";
+ this.txtDuration.Size = new System.Drawing.Size(88, 26);
+ this.txtDuration.TabIndex = 17;
+ this.txtDuration.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.txtDuration_KeyPress);
+ //
+ // btnStartStop
+ //
+ this.btnStartStop.Location = new System.Drawing.Point(229, 239);
+ this.btnStartStop.Name = "btnStartStop";
+ this.btnStartStop.Size = new System.Drawing.Size(118, 43);
+ this.btnStartStop.TabIndex = 18;
+ this.btnStartStop.Text = "Start";
+ this.btnStartStop.UseVisualStyleBackColor = true;
+ this.btnStartStop.Click += new System.EventHandler(this.btnStartStop_Click);
+ //
+ // btnKoch
+ //
+ this.btnKoch.AutoSize = true;
+ this.btnKoch.Location = new System.Drawing.Point(24, 484);
+ this.btnKoch.Name = "btnKoch";
+ this.btnKoch.Size = new System.Drawing.Size(70, 24);
+ this.btnKoch.TabIndex = 19;
+ this.btnKoch.TabStop = true;
+ this.btnKoch.Text = "Koch";
+ this.btnKoch.UseVisualStyleBackColor = true;
+ this.btnKoch.Click += new System.EventHandler(this.btnKoch_Click);
+ //
+ // btnCustom
+ //
+ this.btnCustom.AutoSize = true;
+ this.btnCustom.Location = new System.Drawing.Point(24, 522);
+ this.btnCustom.Name = "btnCustom";
+ this.btnCustom.Size = new System.Drawing.Size(89, 24);
+ this.btnCustom.TabIndex = 20;
+ this.btnCustom.TabStop = true;
+ this.btnCustom.Text = "Custom";
+ this.btnCustom.UseVisualStyleBackColor = true;
+ this.btnCustom.Click += new System.EventHandler(this.btnCustom_Click);
+ //
+ // cmbKoch
+ //
+ this.cmbKoch.FormattingEnabled = true;
+ this.cmbKoch.Location = new System.Drawing.Point(139, 483);
+ this.cmbKoch.Name = "cmbKoch";
+ this.cmbKoch.Size = new System.Drawing.Size(100, 28);
+ this.cmbKoch.TabIndex = 21;
+ this.cmbKoch.SelectedIndexChanged += new System.EventHandler(this.cmbKoch_SelectedIndexChanged);
+ //
+ // txtCustom
+ //
+ this.txtCustom.Location = new System.Drawing.Point(139, 522);
+ this.txtCustom.Name = "txtCustom";
+ this.txtCustom.Size = new System.Drawing.Size(510, 26);
+ this.txtCustom.TabIndex = 22;
+ //
+ // chkFavorNew
+ //
+ this.chkFavorNew.AutoSize = true;
+ this.chkFavorNew.Location = new System.Drawing.Point(259, 484);
+ this.chkFavorNew.Name = "chkFavorNew";
+ this.chkFavorNew.Size = new System.Drawing.Size(192, 24);
+ this.chkFavorNew.TabIndex = 23;
+ this.chkFavorNew.Text = "Favor New Characters";
+ this.chkFavorNew.UseVisualStyleBackColor = true;
+ this.chkFavorNew.CheckStateChanged += new System.EventHandler(this.chkFavorNew_CheckStateChanged);
+ //
+ // sliderVolume
+ //
+ this.sliderVolume.LargeChange = 10;
+ this.sliderVolume.Location = new System.Drawing.Point(805, 175);
+ this.sliderVolume.Name = "sliderVolume";
+ this.sliderVolume.Size = new System.Drawing.Size(116, 69);
+ this.sliderVolume.TabIndex = 25;
+ this.sliderVolume.TickFrequency = 2;
+ this.sliderVolume.Value = 10;
+ this.sliderVolume.Scroll += new System.EventHandler(this.sliderVolume_Scroll);
+ //
+ // label8
+ //
+ this.label8.AutoSize = true;
+ this.label8.Location = new System.Drawing.Point(712, 186);
+ this.label8.Name = "label8";
+ this.label8.Size = new System.Drawing.Size(63, 20);
+ this.label8.TabIndex = 24;
+ this.label8.Text = "Volume";
+ //
+ // txtStartDelay
+ //
+ this.txtStartDelay.Location = new System.Drawing.Point(370, 180);
+ this.txtStartDelay.Name = "txtStartDelay";
+ this.txtStartDelay.Size = new System.Drawing.Size(35, 26);
+ this.txtStartDelay.TabIndex = 26;
+ //
+ // txtStopDelay
+ //
+ this.txtStopDelay.Location = new System.Drawing.Point(642, 180);
+ this.txtStopDelay.Name = "txtStopDelay";
+ this.txtStopDelay.Size = new System.Drawing.Size(35, 26);
+ this.txtStopDelay.TabIndex = 27;
+ //
+ // txtVolume
+ //
+ this.txtVolume.Location = new System.Drawing.Point(927, 180);
+ this.txtVolume.Name = "txtVolume";
+ this.txtVolume.Size = new System.Drawing.Size(35, 26);
+ this.txtVolume.TabIndex = 28;
+ //
+ // Form1
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.ClientSize = new System.Drawing.Size(991, 631);
+ this.Controls.Add(this.txtVolume);
+ this.Controls.Add(this.txtStopDelay);
+ this.Controls.Add(this.txtStartDelay);
+ this.Controls.Add(this.sliderVolume);
+ this.Controls.Add(this.label8);
+ this.Controls.Add(this.chkFavorNew);
+ this.Controls.Add(this.txtCustom);
+ this.Controls.Add(this.cmbKoch);
+ this.Controls.Add(this.btnCustom);
+ this.Controls.Add(this.btnKoch);
+ this.Controls.Add(this.btnStartStop);
+ this.Controls.Add(this.txtDuration);
+ this.Controls.Add(this.txtFarnsworth);
+ this.Controls.Add(this.txtWPM);
+ this.Controls.Add(this.txtFrequency);
+ this.Controls.Add(this.sliderStopDelay);
+ this.Controls.Add(this.sliderStartDelay);
+ this.Controls.Add(this.label7);
+ this.Controls.Add(this.label6);
+ this.Controls.Add(this.label5);
+ this.Controls.Add(this.txtAnalysis);
+ this.Controls.Add(this.sliderDuration);
+ this.Controls.Add(this.sliderFarnsworth);
+ this.Controls.Add(this.sliderWPM);
+ this.Controls.Add(this.sliderFrequency);
+ this.Controls.Add(this.label3);
+ this.Controls.Add(this.label4);
+ this.Controls.Add(this.label2);
+ this.Controls.Add(this.label1);
+ this.KeyPreview = true;
+ this.Name = "Form1";
+ this.Text = "Morse Code Trainer";
+ this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.Form1_FormClosed);
+ this.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.Form1_KeyPress);
+ ((System.ComponentModel.ISupportInitialize)(this.sliderFrequency)).EndInit();
+ ((System.ComponentModel.ISupportInitialize)(this.sliderWPM)).EndInit();
+ ((System.ComponentModel.ISupportInitialize)(this.sliderDuration)).EndInit();
+ ((System.ComponentModel.ISupportInitialize)(this.sliderFarnsworth)).EndInit();
+ ((System.ComponentModel.ISupportInitialize)(this.sliderStartDelay)).EndInit();
+ ((System.ComponentModel.ISupportInitialize)(this.sliderStopDelay)).EndInit();
+ ((System.ComponentModel.ISupportInitialize)(this.sliderVolume)).EndInit();
+ this.ResumeLayout(false);
+ this.PerformLayout();
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.Label label1;
+ private System.Windows.Forms.Label label2;
+ private System.Windows.Forms.Label label3;
+ private System.Windows.Forms.Label label4;
+ private System.Windows.Forms.TrackBar sliderFrequency;
+ private System.Windows.Forms.TrackBar sliderWPM;
+ private System.Windows.Forms.TrackBar sliderDuration;
+ private System.Windows.Forms.TrackBar sliderFarnsworth;
+ private System.Windows.Forms.RichTextBox txtAnalysis;
+ private System.Windows.Forms.Label label5;
+ private System.Windows.Forms.Label label6;
+ private System.Windows.Forms.Label label7;
+ private System.Windows.Forms.TrackBar sliderStartDelay;
+ private System.Windows.Forms.TrackBar sliderStopDelay;
+ private System.Windows.Forms.TextBox txtFrequency;
+ private System.Windows.Forms.TextBox txtWPM;
+ private System.Windows.Forms.TextBox txtFarnsworth;
+ private System.Windows.Forms.TextBox txtDuration;
+ private System.Windows.Forms.Button btnStartStop;
+ private System.Windows.Forms.RadioButton btnKoch;
+ private System.Windows.Forms.RadioButton btnCustom;
+ private System.Windows.Forms.ComboBox cmbKoch;
+ private System.Windows.Forms.TextBox txtCustom;
+ private System.Windows.Forms.CheckBox chkFavorNew;
+ private System.Windows.Forms.TrackBar sliderVolume;
+ private System.Windows.Forms.Label label8;
+ private System.Windows.Forms.TextBox txtStartDelay;
+ private System.Windows.Forms.TextBox txtStopDelay;
+ private System.Windows.Forms.TextBox txtVolume;
+ }
+}
+
diff --git a/MorseTrainer/Form1.cs b/MorseTrainer/Form1.cs
new file mode 100644
index 0000000..322b891
--- /dev/null
+++ b/MorseTrainer/Form1.cs
@@ -0,0 +1,1263 @@
+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 MorseTrainer
+{
+ public partial class Form1 : Form
+ {
+ [Flags]
+ public enum ControlToUpdate
+ {
+ None = 0x00,
+ Slider = 0x01,
+ TextBox = 0x02,
+ All = 0x03
+ }
+
+ public Form1()
+ {
+ InitializeComponent();
+
+ // Setup objects
+ _toneGenerator = new ToneGenerator();
+ _charGenerator = new CharGenerator();
+ _runner = new Runner();
+ _player = new SoundPlayerAsync();
+ _analyzer = new Analyzer(txtAnalysis);
+ _builder = new WordToToneBuilder(_toneGenerator);
+ _recorded = new StringBuilder();
+
+ // Do initialization
+ FrequencyInitialize(
+ sliderFrequency, txtFrequency,
+ 400,
+ ToneGenerator.MIN_FREQUENCY,
+ ToneGenerator.MAX_FREQUENCY,
+ 50
+ );
+
+ WPMInitialize(
+ sliderWPM, txtWPM,
+ 20.0f,
+ ToneGenerator.MIN_WPM,
+ ToneGenerator.MAX_WPM,
+ 0.5f
+ );
+
+ FarnsworthWPMInitialize(
+ sliderFarnsworth, txtFarnsworth,
+ 20.0f,
+ ToneGenerator.MIN_FARNSWORTH_WPM,
+ ToneGenerator.MAX_FARNSWORTH_WPM,
+ 0.5f
+ );
+
+ DurationInitialize(
+ sliderDuration, txtDuration,
+ 30,
+ Runner.MIN_DURATION,
+ Runner.MAX_DURATION,
+ 30
+ );
+
+ StartDelayInitialize(
+ sliderStartDelay, txtStartDelay,
+ 0,
+ Runner.MIN_START_DELAY,
+ Runner.MAX_START_DELAY,
+ 1
+ );
+
+ StopDelayInitialize(
+ sliderStopDelay, txtStopDelay,
+ 0,
+ Runner.MIN_STOP_DELAY,
+ Runner.MAX_STOP_DELAY,
+ 1
+ );
+
+ VolumeInitialize(
+ sliderVolume, txtVolume,
+ 1.0f,
+ ToneGenerator.MIN_VOLUME,
+ ToneGenerator.MAX_VOLUME,
+ 0.1f
+ );
+
+ _runner.StartDelayEnter += _runner_StartDelayEnter;
+ _runner.StartDelayExit += _runner_StartDelayExit;
+ _runner.MorseEnter += _runner_MorseEnter;
+ _runner.MorseExit += _runner_MorseExit;
+ _runner.StopDelayEnter += _runner_StopDelayEnter;
+ _runner.StopDelayExit += _runner_StopDelayExit;
+ _runner.Abort += _runner_Abort;
+ _player.QueueEmpty += _player_QueueEmpty;
+ _player.PlayingFinished += _player_PlayingFinished;
+
+ cmbKoch.Items.Clear();
+ for (int i = 0; i < Koch.Length; ++i)
+ {
+ cmbKoch.Items.Add(MorseInfo.ExpandProsigns(Koch.Order[i].ToString()));
+ }
+
+ Config config = LoadConfig();
+ ApplyConfig(config);
+ }
+
+ #region Configuration
+ private Config LoadConfig()
+ {
+ Config config = null;
+
+ if (!System.IO.File.Exists("config.cfg"))
+ {
+ SaveConfig(Config.Default);
+ }
+
+ System.IO.Stream stream = null;
+ try
+ {
+ stream = System.IO.File.Open("config.cfg", System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.None);
+ System.Runtime.Serialization.Formatters.Binary.BinaryFormatter bf = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
+ config = (Config)bf.Deserialize(stream);
+ }
+ catch (Exception)
+ {
+ throw;
+ }
+ finally
+ {
+ if (stream != null)
+ {
+ stream.Close();
+ }
+ }
+ return config;
+ }
+
+ private void SaveConfig(Config config)
+ {
+ System.IO.Stream stream = null;
+ try
+ {
+ stream = System.IO.File.Open("config.cfg", System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.None);
+ System.Runtime.Serialization.Formatters.Binary.BinaryFormatter bf = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
+ bf.Serialize(stream, config);
+ }
+ catch (Exception ex)
+ {
+ throw ex;
+ }
+ finally
+ {
+ if (stream != null)
+ {
+ stream.Close();
+ }
+ }
+ }
+
+ private void ApplyConfig(Config config)
+ {
+ Frequency = config.Frequency;
+ FrequencySlider = config.Frequency;
+ FrequencyText = config.Frequency;
+
+ WPM = config.WPM;
+ WPMSlider = config.WPM;
+ WPMText = config.WPM;
+
+ FarnsworthWPM = config.FarnsworthWPM;
+ FarnsworthWPMSlider = config.FarnsworthWPM;
+ FarnsworthWPMText = config.FarnsworthWPM;
+
+ Duration = config.Duration;
+ DurationSlider = config.Duration;
+ DurationText = config.Duration;
+
+ StartDelay = config.StartDelay;
+ StartDelaySlider = config.StartDelay;
+ StartDelayText = config.StartDelay;
+
+ StopDelay = config.StopDelay;
+ StopDelaySlider = config.StopDelay;
+ StopDelayText = config.StopDelay;
+
+ Volume = config.Volume;
+ VolumeSlider = config.Volume;
+ VolumeText = config.Volume;
+
+ if (config.GenerationMethod == CharGenerator.Method.Custom)
+ {
+ btnKoch.Checked = false;
+ btnCustom.Checked = true;
+ }
+ else
+ {
+ btnCustom.Checked = false;
+ btnKoch.Checked = true;
+ }
+ cmbKoch.SelectedIndex = config.KochIndex;
+ chkFavorNew.Checked = config.FavorNew;
+ txtCustom.Text = config.Custom;
+ }
+
+ private Config ExtractConfig()
+ {
+ Config config = new Config();
+ config.Frequency = (UInt16)sliderFrequency.Value;
+ config.WPM = (float)sliderWPM.Value / 2.0f;
+ config.FarnsworthWPM = (float)sliderFarnsworth.Value / 2.0f;
+ config.Duration = (UInt16)(sliderDuration.Value);
+
+ config.StartDelay = (UInt16)sliderStartDelay.Value;
+ config.StopDelay = (UInt16)sliderStopDelay.Value;
+ config.Volume = (float)sliderVolume.Value / 10.0f;
+
+ config.GenerationMethod = btnKoch.Checked ? CharGenerator.Method.Koch : CharGenerator.Method.Custom;
+ config.KochIndex = (UInt16)cmbKoch.SelectedIndex;
+ config.FavorNew = chkFavorNew.Checked;
+ config.Custom = txtCustom.Text;
+
+ return config;
+ }
+ #endregion
+
+ #region Runner/Tone Generator/Start Button Events
+ private void btnStartStop_Click(object sender, EventArgs e)
+ {
+ if (_runner.IsRunning)
+ {
+ _pendingWavestream = null;
+ _player.Clear();
+ btnStartStop.Enabled = false;
+ _runner.RequestStop();
+ }
+ else
+ {
+ _recorded.Clear();
+ String word = _charGenerator.CreateRandomString();
+ _builder.StartBuildAsync(word, new AsyncCallback(FirstWaveReadyCallback));
+ btnStartStop.Text = "Stop";
+ txtAnalysis.Focus();
+ txtAnalysis.Clear();
+ }
+ }
+
+ private void FirstWaveReadyCallback(IAsyncResult result)
+ {
+ _pendingWavestream = (WaveStream)result.AsyncState;
+ _runner.RequestStart();
+ }
+
+ private void WaveReadyCallback(IAsyncResult result)
+ {
+ _pendingWavestream = (WaveStream)result.AsyncState;
+ }
+
+ private void _runner_StartDelayEnter(object sender, EventArgs e)
+ {
+ }
+
+ private void _runner_StartDelayExit(object sender, EventArgs e)
+ {
+ }
+
+ private void _runner_MorseEnter(object sender, EventArgs e)
+ {
+ _player.Start(_pendingWavestream);
+ _pendingWavestream = null;
+ String word = _charGenerator.CreateRandomString();
+ _builder.StartBuildAsync(word, new AsyncCallback(WaveReadyCallback));
+ }
+
+ private void _player_QueueEmpty(object sender, EventArgs e)
+ {
+ if (_runner.ContinueMorse)
+ {
+ _player.Enqueue(_pendingWavestream);
+ _builder.StartBuildAsync(_charGenerator.CreateRandomString(), new AsyncCallback(WaveReadyCallback));
+ }
+ }
+
+ private void _player_PlayingFinished(object sender, EventArgs e)
+ {
+ _runner.AcknowledgeSendEnd();
+ }
+
+ private void _runner_MorseExit(object sender, EventArgs e)
+ {
+ }
+
+ private void _runner_StopDelayEnter(object sender, EventArgs e)
+ {
+ }
+
+ private void _runner_StopDelayExit(object sender, EventArgs e)
+ {
+ if (this.InvokeRequired)
+ {
+ Invoke(new EventHandler(_runner_StopDelayExit), sender, e);
+ }
+ else
+ {
+ Analyze();
+ btnStartStop.Text = "Start";
+ btnStartStop.Enabled = true;
+ }
+ }
+
+ private void _runner_Abort(object sender, EventArgs e)
+ {
+ if (this.InvokeRequired)
+ {
+ Invoke(new EventHandler(_runner_Abort), sender, e);
+ }
+ else
+ {
+ //Analyze();
+ btnStartStop.Text = "Start";
+ btnStartStop.Enabled = true;
+ }
+ }
+ #endregion
+
+ private void Analyze()
+ {
+ String sent = _player.Sent;
+ String recorded = _recorded.ToString();
+ _analyzer.Analyze(sent, recorded);
+ }
+
+ #region User Interface
+
+ #region Helper Functions
+ private int ScrollSnap(int scrollValue, int min, int max, int increment)
+ {
+ if (scrollValue < min || scrollValue > max)
+ {
+ throw new ArgumentOutOfRangeException();
+ }
+ if (increment != 1)
+ {
+ scrollValue += increment / 2;
+ scrollValue -= scrollValue % increment;
+ }
+ return scrollValue;
+ }
+
+ #endregion
+
+ #region Frequency
+ private void FrequencyInitialize(TrackBar slider, TextBox textbox, UInt16 defaultValue, UInt16 min, UInt16 max, UInt16 increment)
+ {
+ slider.Value = FrequencyValueToSlider(defaultValue);
+ slider.Minimum = FrequencyValueToSlider(min);
+ slider.Maximum = FrequencyValueToSlider(max);
+ slider.TickFrequency = FrequencyValueToSlider(increment);
+ }
+
+ private UInt16 FrequencySliderToValue(int scroll)
+ {
+ return (UInt16)scroll;
+ }
+
+ private int FrequencyValueToSlider(UInt16 frequency)
+ {
+ return frequency;
+ }
+
+ private UInt16 FrequencyTextToValue(String text)
+ {
+ UInt16 frequency = 0;
+ if (!UInt16.TryParse(text, out frequency))
+ {
+ throw new ArgumentException("Unparseable", "frequency");
+ }
+ return frequency;
+ }
+
+ private String FrequencyValueToText(UInt16 frequency)
+ {
+ return String.Format("{0}", frequency);
+ }
+
+ public UInt16 FrequencySlider
+ {
+ get
+ {
+ return FrequencySliderToValue(sliderFrequency.Value);
+ }
+ set
+ {
+ sliderFrequency.Value = FrequencyValueToSlider(value);
+ }
+ }
+
+ public UInt16 FrequencyText
+ {
+ get
+ {
+ return FrequencyTextToValue(txtFrequency.Text);
+ }
+ set
+ {
+ txtFrequency.Text = FrequencyValueToText(value);
+ txtFrequency.BackColor = SystemColors.Window;
+
+ }
+ }
+
+ public UInt16 Frequency
+ {
+ get
+ {
+ return _toneGenerator.Frequency;
+ }
+ set
+ {
+ _toneGenerator.Frequency = value;
+ }
+ }
+
+ private void sliderFrequency_Scroll(object sender, EventArgs e)
+ {
+ try
+ {
+ TrackBar slider = (TrackBar)sender;
+ int sliderValue = ScrollSnap(
+ slider.Value,
+ FrequencyValueToSlider(ToneGenerator.MIN_FREQUENCY),
+ FrequencyValueToSlider(ToneGenerator.MAX_FREQUENCY),
+ FrequencyValueToSlider(50));
+ if (slider.Value != sliderValue)
+ {
+ slider.Value = sliderValue;
+ }
+ UInt16 freq = FrequencySliderToValue(slider.Value);
+ Frequency = freq;
+ FrequencyText = freq;
+ }
+ catch (Exception ex)
+ {
+ throw ex;
+ }
+ }
+
+ private void txtFrequency_KeyPress(object sender, KeyPressEventArgs e)
+ {
+ TextBox textbox = (TextBox)sender;
+ try
+ {
+ if (e.KeyChar == '\r')
+ {
+ UInt16 freq = FrequencyTextToValue(textbox.Text);
+ Frequency = freq;
+ FrequencySlider = freq;
+ textbox.BackColor = SystemColors.Window;
+ }
+ }
+ catch
+ {
+ textbox.BackColor = Color.Red;
+ }
+ }
+ #endregion
+
+ #region WPM
+ private void WPMInitialize(TrackBar slider, TextBox textbox, float defaultValue, float min, float max, float increment)
+ {
+ slider.Value = WPMValueToSlider(defaultValue);
+ slider.Minimum = WPMValueToSlider(min);
+ slider.Maximum = WPMValueToSlider(max);
+ slider.TickFrequency = WPMValueToSlider(increment);
+ }
+
+ private float WPMSliderToValue(int scroll)
+ {
+ return (float)(scroll / 2.0f);
+ }
+
+ private int WPMValueToSlider(float farnsworthWpm)
+ {
+ return (int)Math.Round(farnsworthWpm*2);
+ }
+
+ private float WPMTextToValue(String text)
+ {
+ float farnsworthWpm = 0;
+ if (!float.TryParse(text, out farnsworthWpm))
+ {
+ throw new ArgumentException("Unparseable", "farnsworthWpm");
+ }
+ return farnsworthWpm;
+ }
+
+ private String WPMValueToText(float farnsworthWpm)
+ {
+ return String.Format("{0:#0.0}", farnsworthWpm);
+ }
+
+ public float WPMSlider
+ {
+ get
+ {
+ return WPMSliderToValue(sliderWPM.Value);
+ }
+ set
+ {
+ sliderWPM.Value = WPMValueToSlider(value);
+ }
+ }
+
+ public float WPMText
+ {
+ get
+ {
+ return WPMTextToValue(txtWPM.Text);
+ }
+ set
+ {
+ txtWPM.Text = WPMValueToText(value);
+ txtWPM.BackColor = SystemColors.Window;
+ }
+ }
+
+ public float WPM
+ {
+ get
+ {
+ return _toneGenerator.WPM;
+ }
+ set
+ {
+ _toneGenerator.WPM = value;
+ if (_toneGenerator.WPM < _toneGenerator.FarnsworthWPM)
+ {
+ FarnsworthWPM = value;
+ FarnsworthWPMSlider = value;
+ FarnsworthWPMText = value;
+ }
+ }
+ }
+
+ private void sliderWPM_Scroll(object sender, EventArgs e)
+ {
+ try
+ {
+ TrackBar slider = (TrackBar)sender;
+ int sliderValue = ScrollSnap(slider.Value,
+ WPMValueToSlider(ToneGenerator.MIN_WPM),
+ WPMValueToSlider(ToneGenerator.MAX_WPM),
+ WPMValueToSlider(0.5f));
+ if (slider.Value != sliderValue)
+ {
+ slider.Value = sliderValue;
+ }
+ float val = WPMSliderToValue(slider.Value);
+ WPM = val;
+ WPMText = val;
+ }
+ catch (Exception ex)
+ {
+ throw ex;
+ }
+ }
+
+ private void txtWPM_KeyPress(object sender, KeyPressEventArgs e)
+ {
+ TextBox textbox = (TextBox)sender;
+ try
+ {
+ if (e.KeyChar == '\r')
+ {
+ float val = WPMTextToValue(textbox.Text);
+ WPM = val;
+ WPMSlider = val;
+ textbox.BackColor = SystemColors.Window;
+ }
+ }
+ catch
+ {
+ textbox.BackColor = Color.Red;
+ }
+ }
+ #endregion
+
+ #region FarnsworthWPM
+ private void FarnsworthWPMInitialize(TrackBar slider, TextBox textbox, float defaultValue, float min, float max, float increment)
+ {
+ slider.Value = FarnsworthWPMValueToSlider(defaultValue);
+ slider.Minimum = FarnsworthWPMValueToSlider(min);
+ slider.Maximum = FarnsworthWPMValueToSlider(max);
+ slider.TickFrequency = FarnsworthWPMValueToSlider(increment);
+ }
+
+ private float FarnsworthWPMSliderToValue(int scroll)
+ {
+ return (float)(scroll / 2.0f);
+ }
+
+ private int FarnsworthWPMValueToSlider(float farnsworthWpm)
+ {
+ return (int)Math.Round(farnsworthWpm * 2);
+ }
+
+ private float FarnsworthWPMTextToValue(String text)
+ {
+ float farnsworthWpm = 0;
+ if (!float.TryParse(text, out farnsworthWpm))
+ {
+ throw new ArgumentException("Unparseable", "farnsworthWpm");
+ }
+ return farnsworthWpm;
+ }
+
+ private String FarnsworthWPMValueToText(float farnsworthWpm)
+ {
+ return String.Format("{0:#0.0}", farnsworthWpm);
+ }
+
+ public float FarnsworthWPMSlider
+ {
+ get
+ {
+ return FarnsworthWPMSliderToValue(sliderFarnsworth.Value);
+ }
+ set
+ {
+ sliderFarnsworth.Value = FarnsworthWPMValueToSlider(value);
+ }
+ }
+
+ public float FarnsworthWPMText
+ {
+ get
+ {
+ return FarnsworthWPMTextToValue(txtFarnsworth.Text);
+ }
+ set
+ {
+ txtFarnsworth.Text = FarnsworthWPMValueToText(value);
+ txtFarnsworth.BackColor = SystemColors.Window;
+ }
+ }
+
+ public float FarnsworthWPM
+ {
+ get
+ {
+ return _toneGenerator.FarnsworthWPM;
+ }
+ set
+ {
+ _toneGenerator.FarnsworthWPM = value;
+ if (_toneGenerator.WPM < _toneGenerator.FarnsworthWPM)
+ {
+ WPM = value;
+ WPMSlider = value;
+ WPMText = value;
+ }
+ }
+ }
+
+ private void sliderFarnsworth_Scroll(object sender, EventArgs e)
+ {
+ try
+ {
+ TrackBar slider = (TrackBar)sender;
+ int sliderValue = ScrollSnap(slider.Value,
+ FarnsworthWPMValueToSlider(ToneGenerator.MIN_FARNSWORTH_WPM),
+ FarnsworthWPMValueToSlider(ToneGenerator.MAX_FARNSWORTH_WPM),
+ FarnsworthWPMValueToSlider(0.5f));
+ if (slider.Value != sliderValue)
+ {
+ slider.Value = sliderValue;
+ }
+ float val = FarnsworthWPMSliderToValue(slider.Value);
+ FarnsworthWPM = val;
+ FarnsworthWPMText = val;
+ }
+ catch (Exception ex)
+ {
+ throw ex;
+ }
+ }
+
+ private void txtFarnsworth_KeyPress(object sender, KeyPressEventArgs e)
+ {
+ TextBox textbox = (TextBox)sender;
+ try
+ {
+ if (e.KeyChar == '\r')
+ {
+ float val = FarnsworthWPMTextToValue(textbox.Text);
+ FarnsworthWPM = val;
+ FarnsworthWPMSlider = val;
+ textbox.BackColor = SystemColors.Window;
+ }
+ }
+ catch
+ {
+ textbox.BackColor = Color.Red;
+ }
+ }
+ #endregion
+
+ #region Duration
+ private void DurationInitialize(TrackBar slider, TextBox textbox, UInt16 defaultValue, UInt16 min, UInt16 max, UInt16 increment)
+ {
+ slider.Minimum = DurationValueToSlider(min);
+ slider.Maximum = DurationValueToSlider(max);
+ slider.Value = DurationValueToSlider(defaultValue);
+ slider.TickFrequency = DurationValueToSlider(increment);
+ }
+
+ private int DurationSliderToValue(int scroll)
+ {
+ return scroll;
+ }
+
+ private int DurationValueToSlider(int duration)
+ {
+ return duration;
+ }
+
+ private int DurationTextToValue(String text)
+ {
+ int duration = 0;
+ if (!int.TryParse(text, out duration))
+ {
+ throw new ArgumentException("Unparseable", "duration");
+ }
+ return duration;
+ }
+
+ private String DurationValueToText(int duration)
+ {
+ return String.Format("{0}", duration);
+ }
+
+ public int DurationSlider
+ {
+ get
+ {
+ return DurationSliderToValue(sliderDuration.Value);
+ }
+ set
+ {
+ sliderDuration.Value = DurationValueToSlider(value);
+ }
+ }
+
+ public int DurationText
+ {
+ get
+ {
+ return DurationTextToValue(txtDuration.Text);
+ }
+ set
+ {
+ txtDuration.Text = DurationValueToText(value);
+ txtDuration.BackColor = SystemColors.Window;
+
+ }
+ }
+
+ public int Duration
+ {
+ get
+ {
+ return _runner.SendDuration;
+ }
+ set
+ {
+ _runner.SendDuration = value;
+ }
+ }
+
+ private void sliderDuration_Scroll(object sender, EventArgs e)
+ {
+ try
+ {
+ TrackBar slider = (TrackBar)sender;
+ int sliderValue = ScrollSnap(
+ slider.Value,
+ DurationValueToSlider(Runner.MIN_DURATION),
+ DurationValueToSlider(Runner.MAX_DURATION),
+ DurationValueToSlider(30));
+ if (slider.Value != sliderValue)
+ {
+ slider.Value = sliderValue;
+ }
+ int val = DurationSliderToValue(slider.Value);
+ Duration = val;
+ DurationText = val;
+ }
+ catch (Exception ex)
+ {
+ throw ex;
+ }
+ }
+
+ private void txtDuration_KeyPress(object sender, KeyPressEventArgs e)
+ {
+ TextBox textbox = (TextBox)sender;
+ try
+ {
+ if (e.KeyChar == '\r')
+ {
+ int val = DurationTextToValue(textbox.Text);
+ Duration = val;
+ DurationSlider = val;
+ textbox.BackColor = SystemColors.Window;
+ }
+ }
+ catch
+ {
+ textbox.BackColor = Color.Red;
+ }
+ }
+ #endregion
+
+ #region Start Delay
+ private void StartDelayInitialize(TrackBar slider, TextBox textbox, UInt16 defaultValue, UInt16 min, UInt16 max, UInt16 increment)
+ {
+ slider.Minimum = StartDelayValueToSlider(min);
+ slider.Maximum = StartDelayValueToSlider(max);
+ slider.Value = StartDelayValueToSlider(defaultValue);
+ slider.TickFrequency = StartDelayValueToSlider(increment);
+ }
+
+ private int StartDelaySliderToValue(int scroll)
+ {
+ return scroll;
+ }
+
+ private int StartDelayValueToSlider(int startDelay)
+ {
+ return startDelay;
+ }
+
+ private int StartDelayTextToValue(String text)
+ {
+ int startDelay = 0;
+ if (!int.TryParse(text, out startDelay))
+ {
+ throw new ArgumentException("Unparseable", "startDelay");
+ }
+ return startDelay;
+ }
+
+ private String StartDelayValueToText(int startDelay)
+ {
+ return String.Format("{0}", startDelay);
+ }
+
+ public int StartDelaySlider
+ {
+ get
+ {
+ return StartDelaySliderToValue(sliderStartDelay.Value);
+ }
+ set
+ {
+ sliderStartDelay.Value = StartDelayValueToSlider(value);
+ }
+ }
+
+ public int StartDelayText
+ {
+ get
+ {
+ return StartDelayTextToValue(txtStartDelay.Text);
+ }
+ set
+ {
+ txtStartDelay.Text = StartDelayValueToText(value);
+ txtStartDelay.BackColor = SystemColors.Window;
+
+ }
+ }
+
+ public int StartDelay
+ {
+ get
+ {
+ return _runner.StartDelay;
+ }
+ set
+ {
+ _runner.StartDelay = value;
+ }
+ }
+
+ private void sliderStartDelay_Scroll(object sender, EventArgs e)
+ {
+ try
+ {
+ TrackBar slider = (TrackBar)sender;
+ int sliderValue = ScrollSnap(
+ slider.Value,
+ StartDelayValueToSlider(Runner.MIN_START_DELAY),
+ StartDelayValueToSlider(Runner.MAX_START_DELAY),
+ StartDelayValueToSlider(1));
+ if (slider.Value != sliderValue)
+ {
+ slider.Value = sliderValue;
+ }
+ int val = StartDelaySliderToValue(slider.Value);
+ StartDelay = val;
+ StartDelayText = val;
+ }
+ catch (Exception ex)
+ {
+ throw ex;
+ }
+ }
+
+ private void txtStartDelay_KeyPress(object sender, KeyPressEventArgs e)
+ {
+ TextBox textbox = (TextBox)sender;
+ try
+ {
+ if (e.KeyChar == '\r')
+ {
+ int val = StartDelayTextToValue(textbox.Text);
+ StartDelay = val;
+ StartDelaySlider = val;
+ textbox.BackColor = SystemColors.Window;
+ }
+ }
+ catch
+ {
+ textbox.BackColor = Color.Red;
+ }
+ }
+ #endregion
+
+ #region Stop Delay
+ private void StopDelayInitialize(TrackBar slider, TextBox textbox, UInt16 defaultValue, UInt16 min, UInt16 max, UInt16 increment)
+ {
+ slider.Value = StopDelayValueToSlider(defaultValue);
+ slider.Minimum = StopDelayValueToSlider(min);
+ slider.Maximum = StopDelayValueToSlider(max);
+ slider.TickFrequency = StopDelayValueToSlider(increment);
+ }
+
+ private int StopDelaySliderToValue(int scroll)
+ {
+ return scroll;
+ }
+
+ private int StopDelayValueToSlider(int stopDelay)
+ {
+ return stopDelay;
+ }
+
+ private int StopDelayTextToValue(String text)
+ {
+ int stopDelay = 0;
+ if (!int.TryParse(text, out stopDelay))
+ {
+ throw new ArgumentException("Unparseable", "stopDelay");
+ }
+ return stopDelay;
+ }
+
+ private String StopDelayValueToText(int stopDelay)
+ {
+ return String.Format("{0}", stopDelay);
+ }
+
+ public int StopDelaySlider
+ {
+ get
+ {
+ return StopDelaySliderToValue(sliderStopDelay.Value);
+ }
+ set
+ {
+ sliderStopDelay.Value = StopDelayValueToSlider(value);
+ }
+ }
+
+ public int StopDelayText
+ {
+ get
+ {
+ return StopDelayTextToValue(txtStopDelay.Text);
+ }
+ set
+ {
+ txtStopDelay.Text = StopDelayValueToText(value);
+ txtStopDelay.BackColor = SystemColors.Window;
+
+ }
+ }
+
+ public int StopDelay
+ {
+ get
+ {
+ return _runner.StopDelay;
+ }
+ set
+ {
+ _runner.StopDelay = value;
+ }
+ }
+
+ private void sliderStopDelay_Scroll(object sender, EventArgs e)
+ {
+ try
+ {
+ TrackBar slider = (TrackBar)sender;
+ int sliderValue = ScrollSnap(
+ slider.Value,
+ StopDelayValueToSlider(Runner.MIN_STOP_DELAY),
+ StopDelayValueToSlider(Runner.MAX_STOP_DELAY),
+ StopDelayValueToSlider(1));
+ if (slider.Value != sliderValue)
+ {
+ slider.Value = sliderValue;
+ }
+ int val = StopDelaySliderToValue(slider.Value);
+ StopDelay = val;
+ StopDelayText = val;
+ }
+ catch (Exception ex)
+ {
+ throw ex;
+ }
+ }
+
+ private void txtStopDelay_KeyPress(object sender, KeyPressEventArgs e)
+ {
+ TextBox textbox = (TextBox)sender;
+ try
+ {
+ if (e.KeyChar == '\r')
+ {
+ int val = StopDelayTextToValue(textbox.Text);
+ StopDelay = val;
+ StopDelaySlider = val;
+ textbox.BackColor = SystemColors.Window;
+ }
+ }
+ catch
+ {
+ textbox.BackColor = Color.Red;
+ }
+ }
+ #endregion
+
+ #region Volume
+ private void VolumeInitialize(TrackBar slider, TextBox textbox, float defaultValue, float min, float max, float increment)
+ {
+ slider.Minimum = VolumeValueToSlider(min);
+ slider.Maximum = VolumeValueToSlider(max);
+ slider.Value = VolumeValueToSlider(defaultValue);
+ slider.TickFrequency = VolumeValueToSlider(increment);
+ }
+
+ private float VolumeSliderToValue(int scroll)
+ {
+ return (float)(scroll / 10.0f);
+ }
+
+ private int VolumeValueToSlider(float volume)
+ {
+ return (int)Math.Round(volume * 10.0f);
+ }
+
+ private float VolumeTextToValue(String text)
+ {
+ float volume = 0;
+ if (!float.TryParse(text, out volume))
+ {
+ throw new ArgumentException("Unparseable", "volume");
+ }
+ return volume;
+ }
+
+ private String VolumeValueToText(float volume)
+ {
+ return String.Format("{0:0}", volume*10);
+ }
+
+ public float VolumeSlider
+ {
+ get
+ {
+ return VolumeSliderToValue(sliderVolume.Value);
+ }
+ set
+ {
+ sliderVolume.Value = VolumeValueToSlider(value);
+ }
+ }
+
+ public float VolumeText
+ {
+ get
+ {
+ return VolumeTextToValue(txtVolume.Text);
+ }
+ set
+ {
+ txtVolume.Text = VolumeValueToText(value);
+ txtVolume.BackColor = SystemColors.Window;
+
+ }
+ }
+
+ public float Volume
+ {
+ get
+ {
+ return _toneGenerator.Volume;
+ }
+ set
+ {
+ _toneGenerator.Volume = value;
+ }
+ }
+
+ private void sliderVolume_Scroll(object sender, EventArgs e)
+ {
+ try
+ {
+ TrackBar slider = (TrackBar)sender;
+ int sliderValue = ScrollSnap(
+ slider.Value,
+ VolumeValueToSlider(ToneGenerator.MIN_VOLUME),
+ VolumeValueToSlider(ToneGenerator.MAX_VOLUME),
+ VolumeValueToSlider(0.1f));
+ if (slider.Value != sliderValue)
+ {
+ slider.Value = sliderValue;
+ }
+ float val = VolumeSliderToValue(slider.Value);
+ Volume = val;
+ VolumeText = val;
+ }
+ catch (Exception ex)
+ {
+ throw ex;
+ }
+ }
+
+ private void txtVolume_KeyPress(object sender, KeyPressEventArgs e)
+ {
+ TextBox textbox = (TextBox)sender;
+ try
+ {
+ if (e.KeyChar == '\r')
+ {
+ float val = VolumeTextToValue(textbox.Text);
+ Volume = val;
+ VolumeSlider = val;
+ textbox.BackColor = SystemColors.Window;
+ }
+ }
+ catch
+ {
+ textbox.BackColor = Color.Red;
+ }
+ }
+ #endregion
+
+ #region Koch/Custom
+ private void btnCustom_Click(object sender, EventArgs e)
+ {
+ _charGenerator.GenerationMethod = CharGenerator.Method.Custom;
+ }
+
+ private void btnKoch_Click(object sender, EventArgs e)
+ {
+ _charGenerator.GenerationMethod = CharGenerator.Method.Koch;
+ }
+
+ #endregion
+
+ #region Koch Combo
+ private void cmbKoch_SelectedIndexChanged(object sender, EventArgs e)
+ {
+ try
+ {
+ int index = cmbKoch.SelectedIndex;
+ if (index >= 0 && index < Koch.Length)
+ {
+ _charGenerator.KochIndex = index;
+ }
+ }
+ catch (Exception ex)
+ {
+ throw ex;
+ }
+ }
+ #endregion
+
+ #region Favor New
+ private void chkFavorNew_CheckStateChanged(object sender, EventArgs e)
+ {
+ _charGenerator.FavorNew = chkFavorNew.CheckState == CheckState.Checked;
+ }
+
+ #endregion
+
+ #region Custom String
+ #endregion
+
+ #endregion
+
+
+ #region User Key
+ private bool UserKey(char key)
+ {
+ bool processed = false;
+ String expanded = MorseInfo.ExpandProsigns(key.ToString()).ToUpperInvariant();
+ txtAnalysis.AppendText(expanded);
+ _recorded.Append(expanded);
+ processed = true;
+
+ return processed;
+ }
+
+ private void Form1_KeyPress(object sender, KeyPressEventArgs e)
+ {
+ if (_runner.IsListenMode)
+ {
+ e.Handled = UserKey(e.KeyChar);
+ }
+ }
+ #endregion
+
+ private ToneGenerator _toneGenerator;
+ private CharGenerator _charGenerator;
+ private WordToToneBuilder _builder;
+ private SoundPlayerAsync _player;
+ private Runner _runner;
+ private Analyzer _analyzer;
+ private StringBuilder _recorded;
+ private WaveStream _pendingWavestream;
+
+ private void Form1_FormClosed(object sender, FormClosedEventArgs e)
+ {
+ SaveConfig(ExtractConfig());
+ if (_runner.IsRunning)
+ {
+ _runner.RequestStop();
+ }
+ _player.CloseAndJoin();
+ }
+ }
+}
diff --git a/MorseTrainer/Form1.resx b/MorseTrainer/Form1.resx
new file mode 100644
index 0000000..1af7de1
--- /dev/null
+++ b/MorseTrainer/Form1.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
\ No newline at end of file
diff --git a/MorseTrainer/Koch.cs b/MorseTrainer/Koch.cs
new file mode 100644
index 0000000..09ceeac
--- /dev/null
+++ b/MorseTrainer/Koch.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MorseTrainer
+{
+ public class Koch
+ {
+ ///
+ /// Gets the
+ ///
+ ///
+ /// A string with the characters
+ static public String CharsUpToAndIncluding(Char c)
+ {
+ int end = IndexOf(c);
+ return CharsUpToAndIncluding(end);
+ }
+
+ ///
+ /// Gets the
+ ///
+ ///
+ /// A string with the characters
+ static public String CharsUpToAndIncluding(int index)
+ {
+ String ret = null;
+ if (index > 0 && index < Length)
+ {
+ ret = Order.Substring(0, index);
+ }
+ return ret;
+ }
+
+ static public Char[] RecentFromChar(Char c, int numberOfChars)
+ {
+ int end = IndexOf(c);
+ int start = Math.Max(0, end - numberOfChars);
+ return Order.Substring(start, numberOfChars).ToCharArray();
+ }
+
+ static public Char[] RecentFromIndex(Char c, int numberOfChars)
+ {
+ int end = IndexOf(c);
+ int start = Math.Max(0, end - numberOfChars);
+ return Order.Substring(start, numberOfChars).ToCharArray();
+ }
+
+ static public int IndexOf(Char c)
+ {
+ return Order.IndexOf(c);
+ }
+
+ static Koch()
+ {
+ Order = String.Concat("KMRSUAPTLOWI.NJEF0Y,VG5/Q9ZH38B?427C1D6X",
+ "\x80\x81\x82");
+ Length = Order.Length;
+ }
+
+ static public readonly String Order;
+ static public readonly int Length;
+ }
+}
diff --git a/MorseTrainer/MorseCompareResults.cs b/MorseTrainer/MorseCompareResults.cs
new file mode 100644
index 0000000..15451b1
--- /dev/null
+++ b/MorseTrainer/MorseCompareResults.cs
@@ -0,0 +1,156 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MorseTrainer
+{
+ [Flags]
+ public enum ResultsDisplayFlags
+ {
+ Valid = 0x01,
+ Dropped = 0x02,
+ Extra = 0x04,
+ All = 0x07
+ }
+
+ public class MorseCompareResults
+ {
+ public MorseCompareResults(IEnumerable substrings, String sent, String recorded)
+ {
+ _substringList = new List(substrings);
+ _sent = sent;
+ _recorded = recorded;
+ }
+
+ public String Sent
+ {
+ get
+ {
+ return _sent;
+ }
+ }
+
+ public String Recorded
+ {
+ get
+ {
+ return _recorded;
+ }
+ }
+
+ public IList SubStrings
+ {
+ get
+ {
+ return _substringList.AsReadOnly();
+ }
+ }
+
+ private String _sent;
+ private String _recorded;
+ private List _substringList;
+ }
+
+ public abstract class MorseSubstring
+ {
+ protected MorseSubstring() { }
+ protected MorseSubstring(String str)
+ {
+ _str = str;
+ }
+
+ public String Chars
+ {
+ get
+ {
+ return _str;
+ }
+ }
+
+ public abstract System.Drawing.Color Color
+ {
+ get;
+ }
+ public abstract String Str(ResultsDisplayFlags flags = ResultsDisplayFlags.All);
+
+ protected string _str;
+ }
+
+ public class MorseValid : MorseSubstring
+ {
+ public MorseValid(String str) : base(str)
+ {
+ }
+
+ public override string Str(ResultsDisplayFlags flags)
+ {
+ String ret = "";
+ if ((flags & ResultsDisplayFlags.Valid) != 0)
+ {
+ ret = MorseInfo.ExpandProsigns(_str);
+ }
+ return ret;
+ }
+
+ public override Color Color
+ {
+ get
+ {
+ return Color.Black;
+ }
+ }
+ }
+
+ public class MorseDropped : MorseSubstring
+ {
+ public MorseDropped(String str) : base(str)
+ {
+ }
+
+ public override string Str(ResultsDisplayFlags flags)
+ {
+ String ret = "";
+ if ((flags & ResultsDisplayFlags.Dropped) != 0)
+ {
+ ret = MorseInfo.ExpandProsigns(_str);
+ }
+ return ret;
+ }
+
+ public override Color Color
+ {
+ get
+ {
+ return Color.Red;
+ }
+ }
+ }
+
+ public class MorseExtra : MorseSubstring
+ {
+ public MorseExtra(String str) : base(str)
+ {
+ }
+
+ public override string Str(ResultsDisplayFlags flags)
+ {
+ String ret = "";
+ if ((flags & ResultsDisplayFlags.Extra) != 0)
+ {
+ ret = MorseInfo.ExpandProsigns(_str);
+ }
+ return ret;
+ }
+
+ public override Color Color
+ {
+ get
+ {
+ return Color.Orange;
+ }
+ }
+ }
+}
diff --git a/MorseTrainer/MorseInfo.cs b/MorseTrainer/MorseInfo.cs
new file mode 100644
index 0000000..970c8a0
--- /dev/null
+++ b/MorseTrainer/MorseInfo.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MorseTrainer
+{
+ public class MorseInfo
+ {
+ public const char PROSIGN_BT = '\x80';
+ public const char PROSIGN_SK = '\x81';
+ public const char PROSIGN_AR = '\x82';
+
+ static MorseInfo()
+ {
+ // save the morse conversions into an array
+ __conversions = new String[256];
+ __conversions['A'] = ".-";
+ __conversions['B'] = "-...";
+ __conversions['C'] = "-.-.";
+ __conversions['D'] = "-..";
+ __conversions['E'] = ".";
+ __conversions['F'] = "..-.";
+ __conversions['G'] = "--.";
+ __conversions['H'] = "....";
+ __conversions['I'] = "..";
+ __conversions['J'] = ".---";
+ __conversions['K'] = "-.-";
+ __conversions['L'] = ".-..";
+ __conversions['M'] = "--";
+ __conversions['N'] = "-.";
+ __conversions['O'] = "---";
+ __conversions['P'] = ".--.";
+ __conversions['Q'] = "--.-";
+ __conversions['R'] = ".-.";
+ __conversions['S'] = "...";
+ __conversions['T'] = "-";
+ __conversions['U'] = "..-";
+ __conversions['V'] = "...-";
+ __conversions['W'] = ".--";
+ __conversions['X'] = "";
+ __conversions['Y'] = "-.--";
+ __conversions['Z'] = "--..";
+
+ __conversions['0'] = "-----";
+ __conversions['1'] = ".----";
+ __conversions['2'] = "..---";
+ __conversions['3'] = "...--";
+ __conversions['4'] = "....-";
+ __conversions['5'] = ".....";
+ __conversions['6'] = "-....";
+ __conversions['7'] = "--...";
+ __conversions['8'] = "---..";
+ __conversions['9'] = "----.";
+
+ __conversions['.'] = ".-.-.-";
+ __conversions[','] = "--..--";
+ __conversions['?'] = "..--..";
+ __conversions['/'] = "-..-.";
+
+ __conversions[PROSIGN_BT] = "-...-";
+ __conversions[PROSIGN_SK] = "...-.-";
+ __conversions[PROSIGN_AR] = ".-.-.";
+
+ __prosignExpansionToValue = new Dictionary();
+ __prosignExpansionToValue.Add("", PROSIGN_BT);
+ __prosignExpansionToValue.Add("", PROSIGN_SK);
+ __prosignExpansionToValue.Add("", PROSIGN_AR);
+
+ __prosignValueToExpansion = new Dictionary();
+ foreach (KeyValuePair kv in __prosignExpansionToValue)
+ {
+ __prosignValueToExpansion.Add(kv.Value, kv.Key);
+ }
+
+
+ // save the number of elements--needed to calculate Farnsworth timing
+ __elements = new int[256];
+ for (int i = 0; i < __elements.Length; ++i)
+ {
+ int elements = 0;
+ String s = __conversions[i];
+ if (!String.IsNullOrEmpty(s))
+ {
+ foreach (char c in s)
+ {
+ if (c == '.')
+ {
+ elements += 1;
+ }
+ else if (c == '-')
+ {
+ elements += 3;
+ }
+ }
+ }
+ __elements[i] = elements;
+ }
+ }
+
+ private static Dictionary __prosignExpansionToValue;
+ private static Dictionary __prosignValueToExpansion;
+ private static String[] __conversions;
+ private static int[] __elements;
+
+ public static int ToElements(Char c)
+ {
+ if (c < 0 || c >= __elements.Length)
+ {
+ throw new ArgumentException("c");
+ }
+ return __elements[c];
+ }
+
+ public static String ToMorse(Char c)
+ {
+ if (c < 0 || c >= __elements.Length)
+ {
+ throw new ArgumentException("c");
+ }
+ String s = __conversions[c];
+ return s;
+ }
+
+ public static String ReplaceProsigns(String expandedProsigns)
+ {
+ String replaced = expandedProsigns;
+ foreach (KeyValuePair kv in __prosignExpansionToValue)
+ {
+ String expansion = kv.Key;
+ String value = kv.Value.ToString();
+ replaced = replaced.Replace(expansion, value);
+ }
+ return replaced;
+ }
+ public static String ExpandProsigns(String valuedProsigns)
+ {
+ String replaced = valuedProsigns;
+ foreach (KeyValuePair kv in __prosignExpansionToValue)
+ {
+ String expansion = kv.Key;
+ String value = kv.Value.ToString();
+ replaced = replaced.Replace(value, expansion);
+ }
+ return replaced;
+ }
+ }
+}
diff --git a/MorseTrainer/MorseTrainer.csproj b/MorseTrainer/MorseTrainer.csproj
new file mode 100644
index 0000000..3e969e9
--- /dev/null
+++ b/MorseTrainer/MorseTrainer.csproj
@@ -0,0 +1,103 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {48E042F6-B6A3-4ABF-B272-A76F9533FDF9}
+ WinExe
+ Properties
+ MorseTrainer
+ MorseTrainer
+ v4.5.2
+ 512
+ true
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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/MorseTrainer/Program.cs b/MorseTrainer/Program.cs
new file mode 100644
index 0000000..1f50b3c
--- /dev/null
+++ b/MorseTrainer/Program.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace MorseTrainer
+{
+ 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/MorseTrainer/Properties/AssemblyInfo.cs b/MorseTrainer/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..a92da16
--- /dev/null
+++ b/MorseTrainer/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("MorseTrainer")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MorseTrainer")]
+[assembly: AssemblyCopyright("Copyright © 2016")]
+[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("48e042f6-b6a3-4abf-b272-a76f9533fdf9")]
+
+// 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/MorseTrainer/Properties/Resources.Designer.cs b/MorseTrainer/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..f7d2316
--- /dev/null
+++ b/MorseTrainer/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 MorseTrainer.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("MorseTrainer.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/MorseTrainer/Properties/Resources.resx b/MorseTrainer/Properties/Resources.resx
new file mode 100644
index 0000000..af7dbeb
--- /dev/null
+++ b/MorseTrainer/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/MorseTrainer/Properties/Settings.Designer.cs b/MorseTrainer/Properties/Settings.Designer.cs
new file mode 100644
index 0000000..7743c69
--- /dev/null
+++ b/MorseTrainer/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 MorseTrainer.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/MorseTrainer/Properties/Settings.settings b/MorseTrainer/Properties/Settings.settings
new file mode 100644
index 0000000..3964565
--- /dev/null
+++ b/MorseTrainer/Properties/Settings.settings
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/MorseTrainer/Runner.cs b/MorseTrainer/Runner.cs
new file mode 100644
index 0000000..78fbcd3
--- /dev/null
+++ b/MorseTrainer/Runner.cs
@@ -0,0 +1,377 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MorseTrainer
+{
+ ///
+ /// The Runner class handles the timing of the morse code runnning.
+ /// It tracks 5 basic states:
+ /// Idle: no tests being run
+ /// StartDelay: countdown to starting the morse code
+ /// Sending: Sending morse code
+ /// SendFinished: Send time is up, waiting for last word to finish up
+ /// StopDelay: Still allowing user to enter characters
+ ///
+ public class Runner : IDisposable
+ {
+ private enum State
+ {
+ // waiting
+ Idle,
+
+ // waiting
+ StartRequest,
+
+ // user has started and delay is counting
+ StartDelay,
+
+ // sending morse
+ Sending,
+
+ // send is done, but finishing up the rest of the word
+ SendFinished,
+
+ // send is
+ StopDelay,
+
+ // stop requested
+ StopRequest
+ }
+
+ public const int MIN_DURATION = 30;
+ public const int MAX_DURATION = 60*10;
+ public const int MIN_START_DELAY = 0;
+ public const int MAX_START_DELAY = 5;
+ public const int MIN_STOP_DELAY = 0;
+ public const int MAX_STOP_DELAY = 5;
+
+ public Runner()
+ {
+ _timer = new System.Timers.Timer();
+ _timer.Enabled = true;
+ _timer.AutoReset = false;
+ _timer.Elapsed += _timer_Elapsed;
+ }
+
+ ///
+ /// Gets whether the runner is idle (false) or in one of the running states (true)
+ ///
+ public bool IsRunning
+ {
+ get
+ {
+ return _state != State.Idle;
+ }
+ }
+
+ ///
+ /// Gets is the runner is in a mode where the user input should be recorded.
+ /// These states are Sending, SendFinished, and StopDelay
+ ///
+ public bool IsListenMode
+ {
+ get
+ {
+ return _state == State.Sending ||
+ _state == State.SendFinished ||
+ _state == State.StopDelay;
+ }
+ }
+
+ ///
+ /// Gets a bool that indicate if the morse is being sent and a new word
+ /// should be sent to the tone generator.
+ ///
+ public bool ContinueMorse
+ {
+ get
+ {
+ return _state == State.Sending;
+ }
+ }
+
+ private void _timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
+ {
+ State nextState = StateExit();
+ StateEnter(nextState);
+ }
+
+ private State StateExit()
+ {
+ State nextState = _state;
+ switch (_state)
+ {
+ case State.StartDelay:
+ _timer.Stop();
+ OnStartDelayExit();
+ nextState = State.Sending;
+ break;
+ case State.Sending:
+ _timer.Stop();
+ OnMorseExit();
+ nextState = State.SendFinished;
+ break;
+ case State.StopDelay:
+ _timer.Stop();
+ OnStopDelayExit();
+ nextState = State.Idle;
+ break;
+ }
+ return nextState;
+ }
+
+
+ private void StateEnter(State nextState)
+ {
+ _state = nextState;
+ switch (_state)
+ {
+ case State.StartDelay:
+ _timer.Interval = _startDelay * 1000;
+ _timer.Start();
+ OnStartDelayEnter();
+ break;
+ case State.Sending:
+ _timer.Interval = _sendDuration * 1000;
+ _timer.Start();
+ OnMorseEnter();
+ break;
+ case State.StopDelay:
+ _timer.Interval = _stopDelay * 1000;
+ _timer.Start();
+ OnStopDelayEnter();
+ break;
+ }
+ }
+
+ ///
+ /// Gets or sets the start delay in seconds. This is the time for the user
+ /// to recover from pressing the start button to positioning their hands
+ /// for input, for example.
+ ///
+ public int StartDelay
+ {
+ get
+ {
+ return _startDelay;
+ }
+ set
+ {
+ _startDelay = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the stop delay in seconds. This is the time after the last word
+ /// was sent to let the user input the remaining letters.
+ ///
+ public int StopDelay
+ {
+ get
+ {
+ return _stopDelay;
+ }
+ set
+ {
+ _stopDelay = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the send duration in seconds.
+ ///
+ public int SendDuration
+ {
+ get
+ {
+ return _sendDuration;
+ }
+ set
+ {
+ _sendDuration = value;
+ }
+ }
+
+ ///
+ /// Starts the runner.
+ ///
+ public void RequestStart()
+ {
+ State startState = (_startDelay > 0) ? State.StartDelay : State.Sending;
+ StateEnter(startState);
+ }
+
+ ///
+ /// Exits the current state and then fires the Abort event. The abort event
+ /// is only fired if the runner is not running
+ ///
+ public void RequestStop()
+ {
+ bool abort = _state != State.Idle;
+ _state = StateExit();
+
+ // stop request during start delay
+ if (_state == State.Sending)
+ {
+ _state = State.Idle;
+ }
+ if (abort)
+ {
+ OnAbort();
+ }
+ }
+
+ ///
+ /// Used when the SendMorseEnd is sent while sending is still occurring.
+ /// Call this in the ToneGenerator.CharactersSent event
+ ///
+ public void AcknowledgeSendEnd()
+ {
+ if (_state == State.SendFinished)
+ {
+ StateEnter(State.StopDelay);
+ }
+ }
+
+ ///
+ /// Countdown to morse code sending has started
+ ///
+ public event EventHandler StartDelayEnter;
+ protected void OnStartDelayEnter()
+ {
+ EventHandler handler = StartDelayEnter;
+ if (handler != null)
+ {
+ handler(this, EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// The countdown delay has ended
+ ///
+ public event EventHandler StartDelayExit;
+ protected void OnStartDelayExit()
+ {
+ EventHandler handler = StartDelayExit;
+ if (handler != null)
+ {
+ handler(this, EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Start morse code sending.
+ ///
+ public event EventHandler MorseEnter;
+ protected void OnMorseEnter()
+ {
+ EventHandler handler = MorseEnter;
+ if (handler != null)
+ {
+ handler(this, EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Stop feeding tone generator with characters and let it finish up
+ ///
+ public event EventHandler MorseExit;
+ protected void OnMorseExit()
+ {
+ EventHandler handler = MorseExit;
+ if (handler != null)
+ {
+ handler(this, EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Morse code has stopped, but the user is still allowed to type in keys
+ ///
+ public event EventHandler StopDelayEnter;
+ protected void OnStopDelayEnter()
+ {
+ EventHandler handler = StopDelayEnter;
+ if (handler != null)
+ {
+ handler(this, EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Post sending delay is over, time to analyze
+ ///
+ public event EventHandler StopDelayExit;
+ protected void OnStopDelayExit()
+ {
+ EventHandler handler = StopDelayExit;
+ if (handler != null)
+ {
+ handler(this, EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Abort
+ ///
+ public event EventHandler Abort;
+ protected void OnAbort()
+ {
+ EventHandler handler = Abort;
+ if (handler != null)
+ {
+ handler(this, EventArgs.Empty);
+ }
+ }
+
+ private State _state;
+ private System.Timers.Timer _timer;
+ private int _startDelay;
+ private int _sendDuration;
+ private int _stopDelay;
+
+ #region IDisposable Support
+ private bool disposedValue = false; // To detect redundant calls
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ if (_timer != null)
+ {
+ _timer.Stop();
+ _timer.Dispose();
+ _timer = null;
+ }
+ }
+
+ // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
+ // TODO: set large fields to null.
+
+ disposedValue = true;
+ }
+ }
+
+ // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
+ // ~Runner() {
+ // // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
+ // Dispose(false);
+ // }
+
+ // This code added to correctly implement the disposable pattern.
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
+ Dispose(true);
+ // TODO: uncomment the following line if the finalizer is overridden above.
+ // GC.SuppressFinalize(this);
+ }
+ #endregion
+
+ }
+}
diff --git a/MorseTrainer/SoundPlayerAsync.cs b/MorseTrainer/SoundPlayerAsync.cs
new file mode 100644
index 0000000..51a68da
--- /dev/null
+++ b/MorseTrainer/SoundPlayerAsync.cs
@@ -0,0 +1,230 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MorseTrainer
+{
+ public class SoundPlayerAsync : IDisposable
+ {
+ public SoundPlayerAsync()
+ {
+ _mediaSoundPlayer = new System.Media.SoundPlayer();
+ _queue = new Queue();
+ _sentString = new StringBuilder();
+
+ _stopThread = false;
+ _thread = new System.Threading.Thread(ThreadMain);
+ _thread.Name = "Async Player";
+ _thread.Start();
+ }
+
+ private void ThreadMain()
+ {
+ while (!_stopThread)
+ {
+ WaveStream waveToPlay = Dequeue();
+ if (waveToPlay != null)
+ {
+ _mediaSoundPlayer.Stream = waveToPlay.Stream;
+ _mediaSoundPlayer.Load();
+ _mediaSoundPlayer.PlaySync();
+ _sentString.Append(waveToPlay.Text);
+ _sentString.Append(' ');
+ // All done
+ if (Count == 0)
+ {
+ OnPlayingFinished();
+ }
+ }
+ }
+ lock(this)
+ {
+ _stopped = true;
+ System.Threading.Monitor.Pulse(this);
+ }
+ }
+
+ private WaveStream Dequeue()
+ {
+ WaveStream wave = null;
+ lock (this)
+ {
+ while (_queue.Count == 0 && !_stopThread)
+ {
+ System.Threading.Monitor.Wait(this);
+ }
+ if (_queue.Count > 0)
+ {
+ wave = _queue.Dequeue();
+ if (_queue.Count == 0)
+ {
+ OnQueueEmpty();
+ }
+ }
+ }
+ return wave;
+ }
+
+ public void Start(WaveStream wave)
+ {
+ Enqueue(wave);
+ _sentString.Clear();
+ }
+
+ public String Sent
+ {
+ get
+ {
+ return _sentString.ToString().TrimEnd();
+ }
+ }
+
+ ///
+ /// Give the SoundPlayerAsync a wave to play immediately (if noty busy) or
+ /// following the currently playing/enqueued waves
+ ///
+ ///
+ public void Enqueue(WaveStream wave)
+ {
+ lock(this)
+ {
+ _queue.Enqueue(wave);
+ System.Threading.Monitor.Pulse(this);
+ }
+ }
+
+ ///
+ /// Clear all waves from the queue when aborting
+ ///
+ public void Clear()
+ {
+ lock(this)
+ {
+ _queue.Clear();
+ System.Threading.Monitor.Pulse(this);
+ }
+ }
+
+ ///
+ /// Gets the number of enqueued waves
+ ///
+ public int Count
+ {
+ get
+ {
+ lock(this)
+ {
+ return _queue.Count;
+ }
+ }
+ }
+
+ ///
+ /// Playing has finished and no more waves are enqueued
+ ///
+ public event EventHandler PlayingFinished;
+ protected void OnPlayingFinished()
+ {
+ EventHandler handler = PlayingFinished;
+ if (handler != null)
+ {
+ handler(this, EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// The queue has been emptied. Action in this should be quick
+ ///
+ public event EventHandler QueueEmpty;
+ protected void OnQueueEmpty()
+ {
+ EventHandler handler = QueueEmpty;
+ if (handler != null)
+ {
+ handler(this, EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Close the thread.
+ ///
+ public void CloseAndJoin()
+ {
+ lock(this)
+ {
+ if (_thread != null && !_stopThread)
+ {
+ _stopThread = true;
+ _stopped = false;
+ _queue.Clear();
+ System.Threading.Monitor.Pulse(this);
+ if (!_stopped)
+ {
+ System.Threading.Monitor.Wait(this);
+ }
+ _thread.Join();
+ _thread = null;
+ }
+ }
+
+ }
+
+ private System.Threading.Thread _thread;
+ private bool _stopThread;
+ private bool _stopped;
+ private Queue _queue;
+ private System.Media.SoundPlayer _mediaSoundPlayer;
+ private StringBuilder _sentString;
+
+ #region IDisposable Support
+ private bool disposedValue = false; // To detect redundant calls
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ // TODO: dispose managed state (managed objects).
+ CloseAndJoin();
+
+ if (_queue != null)
+ {
+ _queue.Clear();
+ _queue = null;
+ }
+
+ if (_mediaSoundPlayer != null)
+ {
+ _mediaSoundPlayer.Stop();
+ _mediaSoundPlayer.Dispose();
+ _mediaSoundPlayer = null;
+ }
+ }
+
+ // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
+ // TODO: set large fields to null.
+
+ disposedValue = true;
+ }
+ }
+
+ // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
+ // ~SoundPlayerAsync() {
+ // // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
+ // Dispose(false);
+ // }
+
+ // This code added to correctly implement the disposable pattern.
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
+ Dispose(true);
+ // TODO: uncomment the following line if the finalizer is overridden above.
+ // GC.SuppressFinalize(this);
+ }
+ #endregion
+ }
+}
diff --git a/MorseTrainer/ToneGenerator.cs b/MorseTrainer/ToneGenerator.cs
new file mode 100644
index 0000000..87825cb
--- /dev/null
+++ b/MorseTrainer/ToneGenerator.cs
@@ -0,0 +1,297 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Media;
+
+namespace MorseTrainer
+{
+ public class ToneGenerator
+ {
+ public const UInt16 MIN_FREQUENCY = 300;
+ public const UInt16 MAX_FREQUENCY = 1200;
+ public const float MIN_WPM = 3.0f;
+ public const float MAX_WPM = 40.0f;
+ public const float MIN_FARNSWORTH_WPM = 3.0f;
+ public const float MAX_FARNSWORTH_WPM = 40.0f;
+ public const float MIN_VOLUME = 0.0f;
+ public const float MAX_VOLUME = 1.0f;
+
+ public const Int32 SAMPLES_PER_SECOND = 8000;
+
+ public ToneGenerator()
+ {
+ _frequency = 0;
+ _WPM = 0;
+ _farnsworthWPM = 0;
+ _volume = 0.0f;
+ }
+
+ private float WpmToMillesecPerElement(float wpm)
+ {
+ float millisecPerElement =
+ 1000.0f /* ms per sec */
+ * 60.0f /* Sec / min */
+ / wpm
+ / 50; /* elem per word */
+ return millisecPerElement;
+ }
+
+ public void Update()
+ {
+ // changing tone frequency or WPM will cause the tones to be regenerated
+ if (_frequency == 0 || _frequency != _newFrequency || _newWPM != _WPM || _newVolume != _volume)
+ {
+ _volume = _newVolume;
+ _frequency = _newFrequency;
+ _WPM = _newWPM;
+ // ms per element = WPM * 1000 ms per sec / 60 words per second / 50 elements
+ _msPerElement = WpmToMillesecPerElement(_WPM);
+
+ float samplesPerCycle = SAMPLES_PER_SECOND / _frequency;
+
+ _samplesPerCycle = (UInt16)(samplesPerCycle + 0.5);
+ CreateTones();
+ }
+ if (_farnsworthWPM == 0 || _farnsworthWPM != _newFarnsworthWPM)
+ {
+ _farnsworthWPM = _newFarnsworthWPM;
+ _msPerFarnsworthElement = WpmToMillesecPerElement(_farnsworthWPM);
+ }
+ }
+
+ public UInt16 CurrentFrequency
+ {
+ get
+ {
+ return _frequency;
+ }
+ }
+
+ public UInt16 Frequency
+ {
+ get
+ {
+ return _newFrequency;
+ }
+ set
+ {
+ if (value < MIN_FREQUENCY || value > MAX_FREQUENCY)
+ {
+ throw new ArgumentOutOfRangeException("Frequency");
+ }
+ _newFrequency = value;
+ }
+ }
+
+ public float Volume
+ {
+ get
+ {
+ return _newVolume;
+ }
+ set
+ {
+ if (value < MIN_VOLUME || value > MAX_VOLUME)
+ {
+ throw new ArgumentOutOfRangeException("Volume");
+ }
+ _newVolume = value;
+ }
+ }
+ public float CurrentVolume
+ {
+ get
+ {
+ return _volume;
+ }
+ }
+
+ public float WPM
+ {
+ get
+ {
+ return _newWPM;
+ }
+ set
+ {
+ if (value < MIN_WPM || value > MAX_WPM)
+ {
+ throw new ArgumentOutOfRangeException("WPM");
+ }
+ _newWPM = (float)Math.Round(value * 2) / 2;
+ }
+ }
+
+ public float CurrentFarnsworthWPM
+ {
+ get
+ {
+ return _farnsworthWPM;
+ }
+ }
+
+ public float FarnsworthWPM
+ {
+ get
+ {
+ return _newFarnsworthWPM;
+ }
+ set
+ {
+ if (value < MIN_WPM || value > MAX_WPM)
+ {
+ throw new ArgumentOutOfRangeException("WPM");
+ }
+ _newFarnsworthWPM = (float)Math.Round(value * 2) / 2;
+ }
+ }
+
+ public UInt32 SamplesPerCycle
+ {
+ get
+ {
+ return _samplesPerCycle;
+ }
+ }
+
+ private void CreateTones()
+ {
+ System.IO.MemoryStream streamDot = new System.IO.MemoryStream();
+ System.IO.MemoryStream streamDash = new System.IO.MemoryStream();
+
+ _dotToneWaveform = CreateTone(_msPerElement);
+ _dashToneWaveform = CreateTone(_msPerElement*3);
+ _dotSpaceWaveform = CreateSpace(_msPerElement);
+ _letterSpaceWaveform = CreateSpace(_msPerElement*3);
+ _wordSpaceWaveform = CreateSpace(_msPerElement * 7);
+ }
+
+ private Int16[] CreateSpace(float millisec)
+ {
+ float samples = SAMPLES_PER_SECOND * millisec / 1000;
+
+ UInt32 actualSamples = (UInt32)samples;
+
+ Int16[] waveform = new Int16[actualSamples];
+
+ // Fill in the space
+ for (UInt32 sample = 0; sample < actualSamples; ++sample)
+ {
+ waveform[sample] = 0;
+ }
+
+ return waveform;
+ }
+
+ private Int16[] CreateTone(float millisec)
+ {
+ float samples = SAMPLES_PER_SECOND * millisec / 1000;
+
+ // Nyquist check
+ if (samples < _samplesPerCycle/2)
+ {
+ throw new ArgumentException("samples");
+ }
+
+ // Create stream
+ // Get a number of cyucles to make the tone end at the end of a sinewave to prevent a pop
+ UInt32 actualSamples = (UInt32)samples;
+ actualSamples = actualSamples - (actualSamples % _samplesPerCycle) + 1;
+
+ Int16[] waveform = new Int16[actualSamples];
+
+ uint fade = _samplesPerCycle;
+ // Fill in the tone
+ for (UInt32 sample = 0; sample < actualSamples; ++sample)
+ {
+ float envelope = 1.0f;
+ if (sample < fade)
+ {
+ envelope = (float)sample / (float)fade;
+ }
+ else if (sample > actualSamples - fade)
+ {
+ envelope = (float)(actualSamples - sample) / (float)fade;
+ }
+ float instantaneous = (float)Math.Sin((float)(sample % _samplesPerCycle) / _samplesPerCycle * (2 * Math.PI )) * _volume * envelope;
+ waveform[sample] = (Int16)(32767 * instantaneous);
+ }
+
+ return waveform;
+ }
+
+ public Int16[] DotToneWaveform
+ {
+ get
+ {
+ return _dotToneWaveform;
+ }
+ }
+
+ public Int16[] DashToneWaveform
+ {
+ get
+ {
+ return _dashToneWaveform;
+ }
+ }
+
+ public Int16[] DotSpaceWaveform
+ {
+ get
+ {
+ return _dotSpaceWaveform;
+ }
+ }
+
+ public Int16[] LetterSpaceWaveform
+ {
+ get
+ {
+ return _letterSpaceWaveform;
+ }
+ }
+
+ public Int16[] WordSpaceWaveform
+ {
+ get
+ {
+ return _wordSpaceWaveform;
+ }
+ }
+
+ public Int16[] FarnsworthSpacingWaveform(Char c)
+ {
+ // Get the equivalent dots
+ int elements = MorseInfo.ToElements(c);
+
+ // Farnsworth timing stretches time between characters.
+ // Character has already been sent WPM and now we have to
+ // wait as if it were sent FarnsworthWPM
+ float wpmTime = _msPerElement * elements;
+ float farnsworthTime = _msPerFarnsworthElement * elements;
+ float difference = farnsworthTime - wpmTime;
+
+ return CreateSpace(difference);
+ }
+
+ private UInt16 _frequency;
+ private UInt16 _newFrequency;
+ private float _WPM;
+ private float _newWPM;
+ private float _farnsworthWPM;
+ private float _newFarnsworthWPM;
+ private float _volume;
+ private float _newVolume;
+ private float _msPerElement;
+ private float _msPerFarnsworthElement;
+ private UInt32 _samplesPerCycle;
+ private Int16[] _dotToneWaveform;
+ private Int16[] _dashToneWaveform;
+ private Int16[] _dotSpaceWaveform;
+ private Int16[] _letterSpaceWaveform;
+ private Int16[] _wordSpaceWaveform;
+ }
+}
diff --git a/MorseTrainer/WaveBuilder.cs b/MorseTrainer/WaveBuilder.cs
new file mode 100644
index 0000000..79c56a4
--- /dev/null
+++ b/MorseTrainer/WaveBuilder.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MorseTrainer
+{
+ public class WaveBuilder
+ {
+ public WaveBuilder()
+ {
+ }
+
+ public Int16[] Dot
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ set
+ {
+ }
+ }
+
+ public Int16[] Dash
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ set
+ {
+ }
+ }
+
+ public Int16[] DotSpace
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ set
+ {
+ }
+ }
+
+ public Int16[] DashSpace
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ set
+ {
+ }
+ }
+
+ public UInt16[] WordSpace
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ set
+ {
+ }
+ }
+
+ public void Append(String morse)
+ {
+ }
+
+ public void Clear()
+ {
+ }
+
+ public WaveStream GetFinalStream()
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/MorseTrainer/WaveStream.cs b/MorseTrainer/WaveStream.cs
new file mode 100644
index 0000000..449dcc2
--- /dev/null
+++ b/MorseTrainer/WaveStream.cs
@@ -0,0 +1,159 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MorseTrainer
+{
+ public class WaveStream
+ {
+ public WaveStream(String text, Int16[] waveform, UInt32 sampleRate, UInt32 samplesPerCycle)
+ {
+ _text = text;
+ SetupStream(waveform, sampleRate, samplesPerCycle);
+ }
+
+ public WaveStream(String text, IEnumerable waveforms, UInt32 sampleRate, UInt32 samplesPerCycle)
+ {
+ _text = text;
+ SetupStream(waveforms, sampleRate, samplesPerCycle);
+ }
+
+ public String Text
+ {
+ get
+ {
+ return _text;
+ }
+ }
+
+ public System.IO.Stream Stream
+ {
+ get
+ {
+ return _stream;
+ }
+ }
+
+ // Create a stream
+ // Header
+ // 4 bytes "RIFF"
+ // 4 bytes chunkSize
+ // 4 bytes "WAVE"
+ //
+ // Subchunk 1
+ // 4 bytes subchunk ID ("fmt ")
+ // 4 bytes subchunk size (16)
+ // 2 bytes audio format (1)
+ // 2 bytes num channels (1)
+ // 4 bytes sample rate (8000)
+ // 4 bytes byte rate = SampleRate * NumChannels * BitsPerSample/8 ()
+ // 2 bytes block align = NumChannels * BitsPerSample/8 (2)
+ // 2 bytes bits per sample (16)
+ //
+ // Subchunk 2
+ // 4 bytes subchunk ID ("data")
+ // 4 bytes subchunk size (2*waveform.Length)
+ // copy of waveform
+ private void SetupStream(
+ Int16[] waveform,
+ UInt32 sampleRate,
+ UInt32 samplesPerCycle
+ )
+ {
+ List waveforms = new List();
+ waveforms.Add(waveform);
+ SetupStream(waveforms, sampleRate, samplesPerCycle);
+ }
+
+ private void SetupStream(
+ IEnumerable waveforms,
+ UInt32 sampleRate,
+ UInt32 samplesPerCycle
+ )
+ {
+ // Create stream
+ _stream = new System.IO.MemoryStream();
+
+ UInt32 totalWaveformLength = GetSize(waveforms);
+
+ _stream.Write(StringToBytes("RIFF"), 0, 4);
+ _stream.Write(LittleEndian(36 + 2* totalWaveformLength, 4), 0, 4);
+ _stream.Write(StringToBytes("WAVE"), 0, 4);
+
+ _stream.Write(StringToBytes("fmt "), 0, 4);
+ _stream.Write(LittleEndian(16, 4), 0, 4); // chunk1Size
+ _stream.Write(LittleEndian(1, 2), 0, 2); // uncompressed linear
+ _stream.Write(LittleEndian(1, 2), 0, 2); // 1 channel
+ _stream.Write(LittleEndian(sampleRate, 4), 0, 4); // sampleRate
+ _stream.Write(LittleEndian(sampleRate * 2, 4), 0, 4); // sampleRate * channels * bitrate / 8
+ _stream.Write(LittleEndian(2, 2), 0, 2); // block align
+ _stream.Write(LittleEndian(16, 2), 0, 2); // bits per sample
+
+ // Fill in the tone
+ _stream.Write(StringToBytes("data"), 0, 4);
+ _stream.Write(LittleEndian(totalWaveformLength * 2, 4), 0, 4);
+ foreach (Int16[] waveform in waveforms)
+ {
+ foreach (Int16 sample in waveform)
+ {
+ _stream.Write(LittleEndian(sample, 2), 0, 2);
+ }
+ }
+ _stream.Seek(0, System.IO.SeekOrigin.Begin);
+ }
+
+ private UInt32 GetSize(IEnumerable partials)
+ {
+ UInt32 count = 0;
+ foreach (Int16[] waveform in partials)
+ {
+ count += (UInt32)waveform.Length;
+ }
+ return count;
+ }
+
+ private void FillWaveForms(IEnumerable partials)
+ {
+ foreach (UInt16[] waveform in partials)
+ {
+ FillWaveForm(waveform);
+ }
+ }
+
+ private void FillWaveForm(UInt16[] waveform)
+ {
+ foreach (Int16 sample in waveform)
+ {
+ _stream.Write(LittleEndian(sample, 2), 0, 2);
+ }
+ }
+
+ private byte[] StringToBytes(String str)
+ {
+ return System.Text.UTF7Encoding.ASCII.GetBytes(str);
+ }
+
+ private byte[] LittleEndian(UInt32 n, int byteCount)
+ {
+ byte[] bytes = new byte[byteCount];
+ int i = 0;
+ while (i < byteCount)
+ {
+ bytes[i] = (byte)n;
+ i++;
+ n = n >> 8;
+ }
+ return bytes;
+ }
+
+ private byte[] LittleEndian(Int32 n, int byteCount)
+ {
+ return LittleEndian((UInt32)n, byteCount);
+ }
+
+ private String _text;
+ private System.IO.MemoryStream _stream;
+ }
+}
diff --git a/MorseTrainer/WordToToneBuilder.cs b/MorseTrainer/WordToToneBuilder.cs
new file mode 100644
index 0000000..a28e0cc
--- /dev/null
+++ b/MorseTrainer/WordToToneBuilder.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MorseTrainer
+{
+ ///
+ /// The WordToToneBuilder class builds a tone in a worker
+ /// thread and makes it available.
+ ///
+ public class WordToToneBuilder
+ {
+ public WordToToneBuilder(ToneGenerator toneGenerator)
+ {
+ _toneGenerator = toneGenerator;
+ }
+
+ public IAsyncResult StartBuildAsync(String word, AsyncCallback callback)
+ {
+ BuildWaverformAsync asyncResult = new BuildWaverformAsync(word, callback);
+ WaitCallback cb = new WaitCallback(MakeWaveform);
+ if (System.Threading.ThreadPool.QueueUserWorkItem(cb, asyncResult))
+ {
+ }
+ return asyncResult;
+ }
+
+ private void MakeWaveform(object state)
+ {
+ BuildWaverformAsync buildInfo = (BuildWaverformAsync)state;
+ List soundsList = new List();
+ _toneGenerator.Update();
+ foreach (Char c in buildInfo.Word)
+ {
+ String morse = MorseInfo.ToMorse(c);
+ bool first = true;
+ foreach (Char d in morse)
+ {
+ if (first)
+ {
+ first = false;
+ }
+ else
+ {
+ soundsList.Add(_toneGenerator.DotSpaceWaveform);
+ }
+ switch (d)
+ {
+ case '.':
+ soundsList.Add(_toneGenerator.DotToneWaveform);
+ break;
+ case '-':
+ soundsList.Add(_toneGenerator.DashToneWaveform);
+ break;
+ case ' ':
+ soundsList.Add(_toneGenerator.WordSpaceWaveform);
+ break;
+ }
+ }
+
+ soundsList.Add(_toneGenerator.LetterSpaceWaveform);
+
+ // Farnsworth timing
+ if (_toneGenerator.FarnsworthWPM < _toneGenerator.WPM)
+ {
+ soundsList.Add(_toneGenerator.FarnsworthSpacingWaveform(c));
+ }
+ }
+ soundsList.Add(_toneGenerator.WordSpaceWaveform);
+ WaveStream stream = new WaveStream(buildInfo.Word, soundsList, ToneGenerator.SAMPLES_PER_SECOND, _toneGenerator.SamplesPerCycle);
+ buildInfo.SetWaveform(stream);
+ buildInfo.Callback();
+ }
+
+ private ToneGenerator _toneGenerator;
+ }
+
+ public class BuildWaverformAsync : IAsyncResult
+ {
+ public BuildWaverformAsync(String word, AsyncCallback callback)
+ {
+ _word = word;
+ _callback = callback;
+ _waveStream = null;
+ }
+
+ public void SetWaveform(WaveStream wavestream)
+ {
+ _waveStream = wavestream;
+ }
+
+ public object AsyncState
+ {
+ get
+ {
+ return _waveStream;
+ }
+ }
+
+ public WaitHandle AsyncWaitHandle
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public bool CompletedSynchronously
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public bool IsCompleted
+ {
+ get
+ {
+ return _waveStream != null;
+ }
+ }
+
+ public String Word
+ {
+ get
+ {
+ return _word;
+ }
+ }
+
+ public void Callback()
+ {
+ if (_callback != null)
+ {
+ _callback(this);
+ }
+ }
+
+ private String _word;
+ private AsyncCallback _callback;
+ private WaveStream _waveStream;
+ }
+}