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; + } +}