202 lines
6.5 KiB
C#
202 lines
6.5 KiB
C#
|
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");
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|