added Decoder class

pull/11/head
Ahmet Inan 2024-04-19 11:52:22 +02:00
rodzic c4e4838cab
commit ee58ec3349
2 zmienionych plików z 147 dodań i 129 usunięć

Wyświetl plik

@ -0,0 +1,143 @@
/*
SSTV Decoder
Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
*/
package xdsopl.robot36;
public class Decoder {
private final Demodulator demodulator;
private final float[] scanLineBuffer;
private final int[] scopePixels;
private final int[] last5msSyncPulses;
private final int[] last9msSyncPulses;
private final int[] last20msSyncPulses;
private final int[] last5msScanLines;
private final int[] last9msScanLines;
private final int[] last20msScanLines;
private final int scanLineToleranceSamples;
private final int scopeWidth;
private final int scopeHeight;
public int curLine;
private int curSample;
Decoder(int[] scopePixels, int scopeWidth, int scopeHeight, int sampleRate) {
this.scopePixels = scopePixels;
this.scopeWidth = scopeWidth;
this.scopeHeight = scopeHeight;
demodulator = new Demodulator(sampleRate);
double scanLineMaxSeconds = 5;
int scanLineMaxSamples = (int) Math.round(scanLineMaxSeconds * sampleRate);
scanLineBuffer = new float[scanLineMaxSamples];
int scanLineCount = 3;
last5msScanLines = new int[scanLineCount];
last9msScanLines = new int[scanLineCount];
last20msScanLines = new int[scanLineCount];
int syncPulseCount = scanLineCount + 1;
last5msSyncPulses = new int[syncPulseCount];
last9msSyncPulses = new int[syncPulseCount];
last20msSyncPulses = new int[syncPulseCount];
double scanLineToleranceSeconds = 0.001;
scanLineToleranceSamples = (int) Math.round(scanLineToleranceSeconds * sampleRate);
}
private void adjustSyncPulses(int[] pulses, int shift) {
for (int i = 0; i < pulses.length; ++i)
pulses[i] -= shift;
}
private double scanLineMean(int[] lines) {
double mean = 0;
for (int diff : lines)
mean += diff;
mean /= lines.length;
return mean;
}
private int scanLineStdDev(int[] lines) {
double mean = scanLineMean(lines);
double stdDev = 0;
for (int diff : lines)
stdDev += (diff - mean) * (diff - mean);
stdDev = Math.sqrt(stdDev / lines.length);
return (int) Math.round(stdDev);
}
private void processOneLine(int prevPulseIndex, int scanLineSamples) {
if (prevPulseIndex < 0 || prevPulseIndex + scanLineSamples >= scanLineBuffer.length)
return;
for (int i = 0; i < scopeWidth; ++i) {
int position = (i * scanLineSamples) / scopeWidth + prevPulseIndex;
int intensity = (int) Math.round(255 * Math.sqrt(scanLineBuffer[position]));
int pixelColor = 0xff000000 | 0x00010101 * intensity;
scopePixels[scopeWidth * curLine + i] = pixelColor;
}
}
private boolean processSyncPulse(int[] pulses, int[] lines, int index) {
for (int i = 1; i < lines.length; ++i)
lines[i - 1] = lines[i];
lines[lines.length - 1] = index - pulses[pulses.length - 1];
for (int i = 1; i < pulses.length; ++i)
pulses[i - 1] = pulses[i];
pulses[pulses.length - 1] = index;
if (lines[0] == 0)
return false;
if (scanLineStdDev(lines) > scanLineToleranceSamples)
return false;
if (pulses[0] >= lines[0]) {
int lineSamples = lines[0];
int endPulse = pulses[0];
int extrapolate = endPulse / lineSamples;
int firstPulse = endPulse - extrapolate * lineSamples;
for (int pulseIndex = firstPulse; pulseIndex < endPulse; pulseIndex += lineSamples)
processOneLine(pulseIndex, lineSamples);
}
for (int i = 0; i < lines.length; ++i)
processOneLine(pulses[i], lines[i]);
int shift = pulses[pulses.length - 1];
adjustSyncPulses(last5msSyncPulses, shift);
adjustSyncPulses(last9msSyncPulses, shift);
adjustSyncPulses(last20msSyncPulses, shift);
int endSample = curSample;
curSample = 0;
for (int i = shift; i < endSample; ++i)
scanLineBuffer[curSample++] = scanLineBuffer[i];
for (int i = 0; i < scopeWidth; ++i)
scopePixels[scopeWidth * (curLine + scopeHeight) + i] = scopePixels[scopeWidth * curLine + i];
curLine = (curLine + 1) % scopeHeight;
return true;
}
public boolean process(float[] recordBuffer) {
boolean syncPulseDetected = demodulator.process(recordBuffer);
int syncPulseIndex = curSample + demodulator.syncPulseOffset;
for (float v : recordBuffer) {
scanLineBuffer[curSample++] = v;
if (curSample >= scanLineBuffer.length) {
int shift = scanLineBuffer.length / 2;
syncPulseIndex -= shift;
adjustSyncPulses(last5msSyncPulses, shift);
adjustSyncPulses(last9msSyncPulses, shift);
adjustSyncPulses(last20msSyncPulses, shift);
curSample = 0;
for (int i = shift; i < scanLineBuffer.length; ++i)
scanLineBuffer[curSample++] = scanLineBuffer[i];
}
}
if (syncPulseDetected) {
switch (demodulator.syncPulseWidth) {
case FiveMilliSeconds:
return processSyncPulse(last5msSyncPulses, last5msScanLines, syncPulseIndex);
case NineMilliSeconds:
return processSyncPulse(last9msSyncPulses, last9msScanLines, syncPulseIndex);
case TwentyMilliSeconds:
return processSyncPulse(last20msSyncPulses, last20msScanLines, syncPulseIndex);
}
}
return false;
}
}

Wyświetl plik

@ -26,7 +26,6 @@ import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class MainActivity extends AppCompatActivity {
@ -36,21 +35,10 @@ public class MainActivity extends AppCompatActivity {
private int[] scopePixels;
private ImageView scopeView;
private float[] recordBuffer;
private float[] scanLineBuffer;
private AudioRecord audioRecord;
private TextView status;
private Demodulator demodulator;
private int[] last5msSyncPulses;
private int[] last9msSyncPulses;
private int[] last20msSyncPulses;
private int[] last5msScanLines;
private int[] last9msScanLines;
private int[] last20msScanLines;
private Decoder decoder;
private int tint;
private int curLine;
private int curSample;
private int scanLineToleranceSamples;
private void setStatus(int id) {
status.setText(id);
@ -64,126 +52,13 @@ public class MainActivity extends AppCompatActivity {
@Override
public void onPeriodicNotification(AudioRecord audioRecord) {
audioRecord.read(recordBuffer, 0, recordBuffer.length, AudioRecord.READ_BLOCKING);
if (visualizeSignal(demodulator.process(recordBuffer))) {
scopeBitmap.setPixels(scopePixels, scopeWidth * curLine, scopeWidth, 0, 0, scopeWidth, scopeHeight);
if (decoder.process(recordBuffer)) {
scopeBitmap.setPixels(scopePixels, scopeWidth * decoder.curLine, scopeWidth, 0, 0, scopeWidth, scopeHeight);
scopeView.invalidate();
}
}
};
private void adjustSyncPulses(int[] pulses, int shift) {
for (int i = 0; i < pulses.length; ++i)
pulses[i] -= shift;
}
private double scanLineMean(int[] lines) {
double mean = 0;
for (int diff : lines)
mean += diff;
mean /= lines.length;
return mean;
}
private int scanLineStdDev(int[] lines) {
double mean = scanLineMean(lines);
double stdDev = 0;
for (int diff : lines)
stdDev += (diff - mean) * (diff - mean);
stdDev = Math.sqrt(stdDev / lines.length);
return (int) Math.round(stdDev);
}
private void processOneLine(int prevPulseIndex, int scanLineSamples) {
if (prevPulseIndex < 0 || prevPulseIndex + scanLineSamples >= scanLineBuffer.length)
return;
for (int i = 0; i < scopeWidth; ++i) {
int position = (i * scanLineSamples) / scopeWidth + prevPulseIndex;
int intensity = (int) Math.round(255 * Math.sqrt(scanLineBuffer[position]));
int pixelColor = 0xff000000 | 0x00010101 * intensity;
scopePixels[scopeWidth * curLine + i] = pixelColor;
}
}
private boolean processSyncPulse(int[] pulses, int[] lines, int index) {
for (int i = 1; i < lines.length; ++i)
lines[i - 1] = lines[i];
lines[lines.length - 1] = index - pulses[pulses.length - 1];
for (int i = 1; i < pulses.length; ++i)
pulses[i - 1] = pulses[i];
pulses[pulses.length - 1] = index;
if (lines[0] == 0)
return false;
if (scanLineStdDev(lines) > scanLineToleranceSamples)
return false;
if (pulses[0] >= lines[0]) {
int lineSamples = lines[0];
int endPulse = pulses[0];
int extrapolate = endPulse / lineSamples;
int firstPulse = endPulse - extrapolate * lineSamples;
for (int pulseIndex = firstPulse; pulseIndex < endPulse; pulseIndex += lineSamples)
processOneLine(pulseIndex, lineSamples);
}
for (int i = 0; i < lines.length; ++i)
processOneLine(pulses[i], lines[i]);
int shift = pulses[pulses.length - 1];
adjustSyncPulses(last5msSyncPulses, shift);
adjustSyncPulses(last9msSyncPulses, shift);
adjustSyncPulses(last20msSyncPulses, shift);
int endSample = curSample;
curSample = 0;
for (int i = shift; i < endSample; ++i)
scanLineBuffer[curSample++] = scanLineBuffer[i];
for (int i = 0; i < scopeWidth; ++i)
scopePixels[scopeWidth * (curLine + scopeHeight) + i] = scopePixels[scopeWidth * curLine + i];
curLine = (curLine + 1) % scopeHeight;
return true;
}
private boolean visualizeSignal(boolean syncPulseDetected) {
int syncPulseIndex = curSample + demodulator.syncPulseOffset;
for (float v : recordBuffer) {
scanLineBuffer[curSample++] = v;
if (curSample >= scanLineBuffer.length) {
int shift = scanLineBuffer.length / 2;
syncPulseIndex -= shift;
adjustSyncPulses(last5msSyncPulses, shift);
adjustSyncPulses(last9msSyncPulses, shift);
adjustSyncPulses(last20msSyncPulses, shift);
curSample = 0;
for (int i = shift; i < scanLineBuffer.length; ++i)
scanLineBuffer[curSample++] = scanLineBuffer[i];
}
}
if (syncPulseDetected) {
switch (demodulator.syncPulseWidth) {
case FiveMilliSeconds:
return processSyncPulse(last5msSyncPulses, last5msScanLines, syncPulseIndex);
case NineMilliSeconds:
return processSyncPulse(last9msSyncPulses, last9msScanLines, syncPulseIndex);
case TwentyMilliSeconds:
return processSyncPulse(last20msSyncPulses, last20msScanLines, syncPulseIndex);
}
}
return false;
}
void initTools(int sampleRate) {
demodulator = new Demodulator(sampleRate);
double scanLineMaxSeconds = 5;
int scanLineMaxSamples = (int) Math.round(scanLineMaxSeconds * sampleRate);
scanLineBuffer = new float[scanLineMaxSamples];
int scanLineCount = 3;
last5msScanLines = new int[scanLineCount];
last9msScanLines = new int[scanLineCount];
last20msScanLines = new int[scanLineCount];
int syncPulseCount = scanLineCount + 1;
last5msSyncPulses = new int[syncPulseCount];
last9msSyncPulses = new int[syncPulseCount];
last20msSyncPulses = new int[syncPulseCount];
double scanLineToleranceSeconds = 0.001;
scanLineToleranceSamples = (int) Math.round(scanLineToleranceSeconds * sampleRate);
}
private void initAudioRecord() {
int audioSource = MediaRecorder.AudioSource.UNPROCESSED;
int channelConfig = AudioFormat.CHANNEL_IN_MONO;
@ -200,7 +75,7 @@ public class MainActivity extends AppCompatActivity {
if (audioRecord.getState() == AudioRecord.STATE_INITIALIZED) {
audioRecord.setRecordPositionUpdateListener(recordListener);
audioRecord.setPositionNotificationPeriod(recordBuffer.length);
initTools(sampleRate);
decoder = new Decoder(scopePixels, scopeWidth, scopeHeight, sampleRate);
startListening();
} else {
setStatus(R.string.audio_init_failed);