package com.talixa.specan.demod.psk;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Queue;
import com.talixa.audio.riff.exceptions.RiffFormatException;
import com.talixa.audio.wav.WaveFile;
import com.talixa.audio.wav.WaveReader;
import com.talixa.specan.SpectrumAnalyzer;
import com.talixa.specan.dsp.SharedDSPFunctions;
/*
* see www.arrl.org/psk31-spec
* 0 = phase reversal
* 1 = steady carrier
* To demodulate, calculate length of baud in ms, and check for phase reversal.
* If present, output 0 else output 1
* Preamble is continuous phase reversals (IE 0's)
* Postamble is continuous carrier (IE 1's)
* A minimum of two zeros exists between ALL characters
*
* Baud length = 1/31.25 = 0.032 ms
* Samples / baud = 8000/31.25 = 256
*
* To begin demod, sync on the phase reversals
* At these reversals, the amplitude of the signal should near zero.
* In fact, we don't really need to look at the phase, but rather the amplitude.
* If we observe a low point after 256 samples, we have a zero. If it's a high
* point, we have a 1. In order to stay synced, and since we know signals may
* not shift at exactly the 256 sample mark, we need to recenter on every 0.
*
* For my sample at 1900hz
* 1900 hz has a period of 0.0005263157894736842
* 1900 hz would take about 4.2105 samples to complete a single wave
*
* A band pass filter should be applied to the data before it goes into the demod
*/
public class Psk31Demod {
private static final int SAMPLE_RATE = 8000; // 8K sample rate
private static final int FREQ_DEV = 50; // 50 hz either side of center freq
private static final int MIN_FREQ_HIT_COUNT = 10; // min hits for frequency detection
private static final int MIN_STABLE_COUNT = 20; // time for signal to come to full power
private static final int REVERSAL_WINDOW = 20; // number of bauds to check for a reversal
private static final int ZERO_AMPLITUDE = 10; // maximum value to be considered a zero
private static boolean debug = false; // true for logging
private static boolean gui = true;
private short[] data; // pcm samples
public Psk31Demod(String inputFile) throws IOException, RiffFormatException {
WaveFile waveFile;
if (gui) {
waveFile = SpectrumAnalyzer.readWaveFile(inputFile);
} else {
waveFile = WaveReader.readFromFile(inputFile);
}
data = SharedDSPFunctions.extractWaveFileData(waveFile);
}
public Psk31Demod(short[] data) {
this.data = data;
}
public String demodulate(int centerFreq) {
StringBuilder demodData = new StringBuilder();
try {
debug("Samples in file: " + data.length);
// lets start by populating an array with all the sample values
// not very memory efficient, but it will make this more simple
double[] samples = new double[data.length];
for(int i = 0; i < data.length; ++i) {
short signedSample = data[i]; // two-byte samples
double doubleSample = ((double) signedSample)/ (Short.MAX_VALUE) * 100;
samples[i] = doubleSample;
}
// Step 1 - frequency detect
// Find point in samples where frequency carrier present
int frequencyDetectedAt = frequencyDetect(centerFreq, samples, MIN_FREQ_HIT_COUNT);
debug("Frequency detected at " + frequencyDetectedAt);
// Step 2 - signal coming up, wait for max amplitude
int stabilizedAt = waitToStabilize(samples, frequencyDetectedAt, MIN_STABLE_COUNT);
debug("System stabilized at " + stabilizedAt);
// Step 3 - find reversal
int reversalAt = findReversal(samples, stabilizedAt);
debug("Reversal at " + reversalAt + " - " + getFivePointAverage(samples, reversalAt));
// step 4 - demod
Queue<Integer> bitQueue = new ArrayDeque<Integer>();
for(int i = reversalAt ; i < samples.length; i += 256) {
double avgEnergy = getFivePointAverage(samples, i);
if (avgEnergy < ZERO_AMPLITUDE) {
i = recenter(samples, i); // make sure we are at the center of the 0
bitQueue.add(0);
debugBit(0);
} else {
bitQueue.add(1);
debugBit(1);
}
}
// Step 5 - run through varicode decoder
int currentCode = 0; // build code to send to varicode decoder
int previousBit = -1; // keep track of last and current bit since no
int currentBit = -1; // psk31 character contains two 0 beside each other
while (!bitQueue.isEmpty()) {
currentBit = bitQueue.poll();
if (currentBit == 0 && previousBit == 0 && currentCode != 0) {
currentCode = currentCode >> 1; // remove last zero
demodData.append((char)VaricodeDecoder.psk_varicode_decode(currentCode));
currentCode = 0;
} else {
currentCode = (currentCode << 1) | currentBit;
previousBit = currentBit;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return demodData.toString();
}
// When we get a 0, this function is called to verify that we are at the low point of the zero.
// This will help to keep us on track if the bit rate changes slightly
private int recenter(double[] samples, int start) {
int lowPointIndex = 0;
double lowPoint = 999;
for(int i = -15; i < 15; ++i) {
double avg = getFivePointAverage(samples, start + i);
if (avg < lowPoint) {
lowPoint = avg;
lowPointIndex = i;
}
}
return start + lowPointIndex;
}
// iterate through samples until the required frequency is found
private int frequencyDetect(int centerFreq, double[] samples, int minHits) {
int frequencyDetectedAt = 0;
double currentWaveLength = 0; // length of current wave
int frequencyHits = 0; // when we have 10 consecutive hits, we have detected frequency
double currentSample;
double previousSample;
for(int i = 1; i < samples.length && frequencyDetectedAt == 0 ; ++i) {
currentSample = samples[i];
previousSample = samples[i-1];
// transition - lets measure the period of the wave
if (currentSample >= 0 && previousSample < 0) {
// calculation time portion to zero for previous and current wave
double deviation = Math.abs(previousSample) + currentSample;
double lengthFromNegativeToZero = 1.0 / deviation * previousSample;
double lengthFromZeroToCurrent = 1.0 / deviation * currentSample;
currentWaveLength += Math.abs(lengthFromNegativeToZero);
double currentFrequency = SAMPLE_RATE / currentWaveLength;
if (currentFrequency > (centerFreq - FREQ_DEV) && currentFrequency < (centerFreq + FREQ_DEV)) {
++frequencyHits;
} else {
if (frequencyHits > 0) {
frequencyHits = 0;
}
}
if (frequencyHits == minHits) {
frequencyDetectedAt = i;
}
currentWaveLength = lengthFromZeroToCurrent;
} else {
// Did not cross 0, increment wavelength counter
currentWaveLength += 1;
}
}
return frequencyDetectedAt;
}
// when a signal starts, it will be a tone increasing in amplitude
// this looks for the end of that brief period of signal life
private int waitToStabilize(double samples[], int sampleStart, int stableCount) {
int stableCycles = 0; // how many stable cycles in a row
double maxAmplitude = 0; // max amplitude during cycle
int stabilizedAt = 0; // point where stable
for(int i = sampleStart ; i < samples.length && stabilizedAt == 0; ++i) {
// check for amplitude change
double amp = Math.abs(samples[i]);
if (amp > maxAmplitude) {
maxAmplitude = amp;
stableCycles = 0;
} else {
++stableCycles;
}
if (stableCycles == stableCount) {
stabilizedAt = i;
}
}
debug("Max amplitude is " + maxAmplitude);
return stabilizedAt;
}
// find a reversal within the reversal window
// this point will serve as the beginning of the decode process
private int findReversal(double[] samples, int startAt) {
// a reversal should exist at the point of lowest average amplitude
// we should find another low point at 256 bytes past the first
int checkLength = 256 * REVERSAL_WINDOW;
double[] averageAmplitude = new double[checkLength];
// 256 samples per baud, generate all 5-point averages then find smallest
for(int i = 0; i < checkLength; ++i) {
averageAmplitude[i] = getFivePointAverage(samples, startAt + i);
}
double smallestValue = 999;
int smallestIndex = 0;
for(int i = 0; i < checkLength; ++i) {
if (averageAmplitude[i] < smallestValue) {
smallestValue = averageAmplitude[i];
smallestIndex = i;
}
}
return smallestIndex + startAt;
}
// instead of ever using a single point, average 5 points together to determine high/low
private double getFivePointAverage(double[] samples, int averagePoint) {
double sum = 0;
for(int sampleId = -2; sampleId < 2; ++sampleId) {
sum += Math.abs(samples[averagePoint + sampleId]);
}
return sum/5;
}
private void debug(String msg) {
if (debug) {
System.out.println(msg);
}
}
private void debugBit(int bit) {
if (debug) {
System.out.print(bit);
}
}
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 = {"psk31-1900hz.wav", "SK01-061228-BPSK.wav"};
private static final int[] CENTER_FREQS = {1900, 1023};
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];
try {
WaveFile wave = WaveReader.readFromFile(inputFile);
short[] data = SharedDSPFunctions.extractWaveFileData(wave);
Psk31Demod demod = new Psk31Demod(data);
String demodData = demod.demodulate(CENTER_FREQS[i]);
System.out.println(demodData);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}