package com.talixa.specan.demod.fsk;
import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayDeque;
import java.util.Queue;
import com.talixa.audio.wav.WaveFile;
import com.talixa.audio.wav.WaveReader;
import com.talixa.specan.SpectrumAnalyzer;
import com.talixa.specan.demod.fsk.BaudotDecoder.Mode;
/*
* This implementation of an RTTY demod determines ones and zeros based on
* the period of the waveform. This works very well for my test data but needs
* much more testing on a wide range of samples. Before entering the demod,
* it would be better if the sample was passed through a narrow band filter
* to block out unwanted frequencies.
*
* Input data is assumed to be sampled at 8K and have 7.5 bits per frame
*
* For my sample, with spikes at 930 and 1100, I made the following calculations
*
* 50 bits per second = each bit lasts 2 milliseconds (1/50 = .02)
* 2 milliseconds = 160 samples / bit (8000*.02)
* 3 milliseconds = 1.5 stop bits (2ms*1.5)
* Our sample has spikes at aprox 930 and 1100 hz
* 930 hz * .02 = 18.6 waves for a bit
* 1100 hz * .02 = 22 waves for a bit
* 930 hz has a period of 0.001075268817204301
* 1100 hz has a period of 0.0009090909090909091
* 930 hz would take about 8.60215 samples to complete a single wave
* 1100 hz would take about 7.272727 samples to complete a single wave
*
* Conclusions we can make:
* if transition > 8, low freq
* if transition < 8, high freq
* if 18 consecutive low freq, add 0 bit
* if 22 consecutive high freq, add 1 bit
*
* I have tried to generalize these calculations for use on other frequency pairs.
* This algorithm would work best on lower frequencies where the difference
* between low and high would be far more pronounced. As such, a further
* enhancement would be to downshift the data so that the low frequency is at 100
* hz and the high freq - 170 hz up - would be at 270hz - nearly three times as
* high. These larger values would create a much better demodulation.
*
* A band pass filter should be applied to the data before it goes into the demod
*/
public class RttyDemod {
private static final int SAMPLE_RATE = 8000;
private static final int BITS_PER_FRAME = 7;
private static final double WAVE_COUNT_MOI = 0.9;
private String inputFile;
private String outputFile;
private static boolean debug = false;
private static boolean gui = true;
private Queue<Byte> baudotData = new ArrayDeque<Byte>();
public RttyDemod(String inputFile, String outputFile) {
this.inputFile = inputFile;
this.outputFile = outputFile;
}
public RttyDemod(String inputFile) {
this.inputFile = inputFile;
this.outputFile = null;
}
private void doDemodulation(int lowFreq, int highFreq, double baudRate) {
double baudLength = 1f / baudRate;
double samplesPerBit = SAMPLE_RATE * baudLength;
double samplesPerStop = samplesPerBit * 1.5;
double wavesFor0 = lowFreq * baudLength;
double wavesFor1 = highFreq * baudLength;
double periodLow = 1f / lowFreq;
double periodHigh = 1f / highFreq;
double samplesPerWaveLow = SAMPLE_RATE * periodLow;
double samplesPerWaveHigh = SAMPLE_RATE * periodHigh;
if (debug) {
System.out.println("Sample rate: " + SAMPLE_RATE);
System.out.println("Baud Rate: " + baudRate + " bps");
System.out.println("Baud Length: " + baudLength + " s");
System.out.println("Samples per bit: " + samplesPerBit);
System.out.println("Samples per stop: " + samplesPerStop);
System.out.println("Waves for 0 bit: " + wavesFor0);
System.out.println("Waves for 1 bit: " + wavesFor1);
System.out.println("Low freq period: " + periodLow);
System.out.println("High freq period: " + periodHigh);
System.out.println("Samples per low wave: " + samplesPerWaveLow);
System.out.println("Samples per high wave: " + samplesPerWaveHigh);
}
double spaceThresholdLow = samplesPerWaveLow - .6;
double spaceThresholdHigh = samplesPerWaveLow + .6;
double markThresholdLow = samplesPerWaveHigh - .65;
double markThresholdHigh = spaceThresholdLow;
try {
FileOutputStream fos = null;
if (outputFile != null) {
File output = new File(outputFile);
fos = new FileOutputStream(output);
}
Queue<Integer> bitQueue = new ArrayDeque<Integer>();
WaveFile waveFile;
if (gui) {
waveFile = SpectrumAnalyzer.readWaveFile(inputFile);
} else {
waveFile = WaveReader.readFromFile(inputFile);
}
if (waveFile == null) {
return;
}
short signedSample = (short)waveFile.getSampleAt(0); // two-byte samples
double previousSample = ((double) signedSample)/ ((1 << waveFile.getFormatChunk().getBitsPerSample()) / 2);
double currentSample;
int consecutiveLows = 0; // number of waves of low frequency
int consecutiveHighs = 0; // number of waves of high frequency
double currentWaveLength = 0; // length of current wave
boolean startOutput = false; // true after first stop bit encountered
int bitsSinceLastStop = 0; // keep track of bits/frame so we don't loose sync
for(int i = 1; i < waveFile.getNumberOfSamples(); ++i) {
signedSample = (short)waveFile.getSampleAt(i*2); // two-byte samples
currentSample = ((double) signedSample)/ ((1 << waveFile.getFormatChunk().getBitsPerSample()) / 2);
// if we crossed from negative to positive, the magic starts!
if ((currentSample >= 0 && previousSample < 0)) {
// deviation = abs(low) + high
double deviation = Math.abs(previousSample) + currentSample;
double previousFrameToZeroLength = 1.0 / deviation * previousSample;
double currentFrameFromZeroLength = 1.0 / deviation * currentSample;
// calculate total length of current wave including fractional portion from this iteration
currentWaveLength += Math.abs(previousFrameToZeroLength);
// is the wavelength within the margin of error for a mark?
if (currentWaveLength >= markThresholdLow && currentWaveLength <= markThresholdHigh) {
consecutiveLows = 0;
++consecutiveHighs;
// enough highs to be a 1?
if (consecutiveHighs >= Math.floor(wavesFor1 - WAVE_COUNT_MOI)) {
bitsSinceLastStop++;
bitQueue.add(1);
consecutiveHighs = 0;
if (debug) System.out.print(1);
}
// is the wavelength within the margin of error for a space?
} else if (currentWaveLength >= spaceThresholdLow && currentWaveLength <= spaceThresholdHigh) {
// was the previous iteration a half-bit? (IE stop?)
if (consecutiveHighs >= (wavesFor1/2.0)) {
// Try to prevent slip by inserting a 1 so we stay in sync
if(bitsSinceLastStop != BITS_PER_FRAME) {
if (debug) System.out.print("S");
bitQueue.add(1);
}
if (debug) System.out.println("X");
// if we didn't start outputting yet, now's the time!
if (!startOutput) {
startOutput = true;
bitQueue.clear();
}
bitsSinceLastStop = 0;
}
consecutiveHighs = 0;
++consecutiveLows;
// enough lows to be a 0?
if (consecutiveLows >= Math.floor(wavesFor0 - WAVE_COUNT_MOI)) {
bitsSinceLastStop++;
bitQueue.add(0);
consecutiveLows = 0;
if (debug) System.out.print(0);
}
} else {
// Decode error
if (debug) System.out.print("*");
}
// add the fractional portion to this wave
currentWaveLength = currentFrameFromZeroLength;
} else {
// Did not cross 0, increment wavelength counter
currentWaveLength += 1;
}
previousSample = currentSample;
// if we have a byte, output it
if (fos != null) {
// writing to output
while (bitQueue.size() >= 8 && startOutput) {
int newByte =
bitQueue.poll() << 7 |
bitQueue.poll() << 6 |
bitQueue.poll() << 5 |
bitQueue.poll() << 4 |
bitQueue.poll() << 3 |
bitQueue.poll() << 2 |
bitQueue.poll() << 1 |
bitQueue.poll() << 0;
fos.write((byte)newByte);
}
} else {
// direct decode
while(bitQueue.size() >= 7 && startOutput) {
// remove start bit
bitQueue.poll();
// reverse bits
int baudotByte =
bitQueue.poll() << 0 |
bitQueue.poll() << 1 |
bitQueue.poll() << 2 |
bitQueue.poll() << 3 |
bitQueue.poll() << 4;
// remote stop bit
bitQueue.poll();
baudotData.add((byte)baudotByte);
}
}
}
if (fos != null) {
fos.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void demodulate(int lowFreq, int highFreq, double baudRate) {
doDemodulation(lowFreq, highFreq, baudRate);
}
public String demodulateBaudot(int lowFreq, int highFreq, double baudRate) {
doDemodulation(lowFreq, highFreq, baudRate);
byte[] data = new byte[baudotData.size()];
int lastIndex = 0;
while(!baudotData.isEmpty()) {
data[lastIndex++] = baudotData.poll();
}
BaudotDecoder decoder = new BaudotDecoder(data, Mode.US);
return decoder.decode();
}
private static final String PATH_WIN = "C:\\Users\\tgerlach\\git\\specan\\SpecAn\\res\\";
private static final String PATH_LINUX = "/home/thomas/git/specan/SpecAn/res/";
private static final String[] TEST_FILES = {"rtty-170-45.wav", "rtty-170-50.wav", "rtty-425-50.wav", "rtty-850-50.wav"};
private static final int[] TEST_LOW = { 920, 930, 800, 585};
private static final int[] TEST_HIGH = {1090, 1100, 1230, 1430};
private static final double[] TEST_BAUD = {45.45, 50, 50, 50};
public static void main(String[] args) {
//debug = true;
testDemod();
}
public static void testDemod() {
boolean usingWindows = System.getProperty("os.name").toLowerCase().contains("windows");
String path;
if (usingWindows) {
path = PATH_WIN;
} else {
path = PATH_LINUX;
}
gui = false;
for(int i = 0; i < TEST_FILES.length; ++i) {
System.out.print("***********************");
System.out.print(TEST_FILES[i]);
System.out.println("***********************");
String inputFile = path + TEST_FILES[i];
RttyDemod demod = new RttyDemod(inputFile);
String data = demod.demodulateBaudot(TEST_LOW[i], TEST_HIGH[i], TEST_BAUD[i]);
System.out.println(data);
}
}
}