A C# program that uses NAudio to analyze an audio file and decode the detected morse signal. Currently not particularly tolerant of noise.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

201 lines
6.5 KiB

using System;
using System.Linq;
using System.Collections.Generic;
using NAudio.Wave;
using Cairo;
namespace MorseCodeParser
{
enum MorseTokenType
{
ToneStart,
ToneEnd
}
struct MorseToken
{
public MorseTokenType Type;
public int Index;
public MorseToken(MorseTokenType inType, int inIndex)
{
Type = inType;
Index = inIndex;
}
public override string ToString()
{
return string.Format("[MorseToken {0}\tat {1}]", Type, Index);
}
}
class MorseTone
{
public int Index;
public int Length;
public MorseTone(int inIndex, int inLength)
{
Index = inIndex;
Length = inLength;
}
public override string ToString()
{
return string.Format("[MorseTone at {0} of length {1}]", Index, Length);
}
}
public class AudioMorseDecoder
{
private string filename;
private ISampleProvider samples;
private readonly int floatBufferSize = 220500 * 4;
private readonly int windowSize = 100;
private readonly int stepSize = 25;
private readonly float threshold = 0.8f;
public AudioMorseDecoder(string inFilename)
{
filename = inFilename;
AudioFileReader reader = new AudioFileReader(filename);
samples = reader.ToSampleProvider();
}
private List<MorseToken> analyzeSamples()
{
List<MorseToken> result = new List<MorseToken>();
int buffersRead = 0;
float[] rawBuffer = new float[floatBufferSize];
float[] windowedBuffer = new float[(floatBufferSize - windowSize) / stepSize];
int samplesRead;
while(true)
{
samplesRead = samples.Read(rawBuffer, 0, floatBufferSize);
for(int i = 0, s = 0; i < samplesRead - windowSize; i += stepSize, s++)
windowedBuffer[s] = rawBuffer.Skip(i).Take(windowSize).Max();
for(int i = 1; i < windowedBuffer.Length; i++)
{
if(windowedBuffer[i - 1] < threshold && windowedBuffer[i] >= threshold)
result.Add(new MorseToken(MorseTokenType.ToneStart, (buffersRead * windowedBuffer.Length) + i));
else if(windowedBuffer[i - 1] > threshold && windowedBuffer[i] <= threshold)
result.Add(new MorseToken(MorseTokenType.ToneEnd, (buffersRead * windowedBuffer.Length) + i));
}
if(samplesRead < floatBufferSize)
break;
buffersRead++;
}
return result;
}
public List<string> ExtractWords(bool renderImage = false)
{
List<MorseToken> tokens = analyzeSamples();
if(renderImage)
drawFingerprint(tokens);
List<MorseTone> tones = new List<MorseTone>();
for(int i = 0; i < tokens.Count; i += 2)
tones.Add(new MorseTone(tokens[i].Index, tokens[i + 1].Index - tokens[i].Index));
int longestToneLength = tones.Max((MorseTone tone) => tone.Length);
int shortestToneLength = tones.Min((MorseTone tone) => tone.Length);
int shortestTonePause = tones.Zip(tones.Skip(1), (a, b) => Tuple.Create(a, b))
.Min((Tuple<MorseTone, MorseTone> tonePair) =>
tonePair.Item2.Index - (tonePair.Item1.Index + tonePair.Item1.Length)
);
int longestTonePause = tones.Zip(tones.Skip(1), (a, b) => Tuple.Create(a, b))
.Max((Tuple<MorseTone, MorseTone> tonePair) =>
tonePair.Item2.Index - (tonePair.Item1.Index + tonePair.Item1.Length)
);
int mediumTonePause = shortestTonePause * 3;
List<string> resultWords = new List<string>();
string currentWord = "";
for(int i = 0; i < tones.Count; i++)
{
if(i > 0)
{
int toneSpacing = tones[i].Index - (tones[i - 1].Index + tones[i - 1].Length);
int distanceToShortestGap = Math.Abs(shortestTonePause - toneSpacing);
int distanceToMediumGap = Math.Abs(mediumTonePause - toneSpacing);
int distanceToLongestGap = Math.Abs(longestTonePause - toneSpacing);
if(distanceToMediumGap < distanceToLongestGap && distanceToMediumGap < distanceToShortestGap)
{
// It's a letter spacing!
currentWord += " ";
}
if(distanceToLongestGap < distanceToMediumGap && distanceToLongestGap < distanceToShortestGap)
{
// It's a word spacing!
resultWords.Add(currentWord);
currentWord = string.Empty;
}
}
if(Math.Abs(longestToneLength - tones[i].Length) < Math.Abs(shortestToneLength - tones[i].Length))
{
// It's a long tone
currentWord += "-";
}
else
{
// It's a short tone
currentWord += ".";
}
}
// Add the last word decoded to the list
resultWords.Add(currentWord);
return resultWords;
}
private void drawFingerprint(List<MorseToken> tokens)
{
int fingerprintWidth = 1200, fingerprintHeight = 100;
float scaleFactorX = (float)fingerprintWidth / tokens.Last().Index;
using(ImageSurface image = new ImageSurface(Format.Argb32, fingerprintWidth, fingerprintHeight))
using(Context context = new Context(image))
{
context.Antialias = Antialias.Subpixel;
context.SetSourceColor(new Color(1, 0.4901, 0.1019, 0.8));
for(int i = 0; i < tokens.Count; i += 2)
{
MorseToken startToken = tokens[i];
MorseToken endToken = tokens[i + 1];
context.NewPath();
Rectangle tokenArea = new Rectangle(
startToken.Index * scaleFactorX,
0,
(endToken.Index - startToken.Index) * scaleFactorX,
fingerprintHeight
);
context.Rectangle(tokenArea);
context.Fill();
}
image.WriteToPng("fingerprint.png");
}
}
}
}