Porównaj commity

...

28 Commity

Autor SHA1 Wiadomość Data
Ahmet Inan 5b2ad998c3 v2.4 2024-05-02 12:17:40 +02:00
Ahmet Inan f64d9e8254 handle calibration header using pulse edges 2024-05-02 11:06:09 +02:00
Ahmet Inan c9ca4e69c3 reworded privacy policy 2024-05-02 07:47:24 +02:00
Ahmet Inan 5a264c2431 don't change the mode until end on header detection 2024-05-01 14:11:13 +02:00
Ahmet Inan 9f53c04b26 always add sync pulses 2024-05-01 10:44:08 +02:00
Ahmet Inan 4c11d82654 find sync pulse edge right after VIS code 2024-05-01 10:28:10 +02:00
Ahmet Inan 391e73c1f3 draw black bars around picture change signal 2024-05-01 08:11:36 +02:00
Ahmet Inan 97820b504a cant shift more than what we have 2024-04-30 22:40:50 +02:00
Ahmet Inan 1a7c2d341f signal when picture changes 2024-04-30 20:42:36 +02:00
Ahmet Inan e0542ea00b extracted shifting of samples 2024-04-30 20:06:46 +02:00
Ahmet Inan f809555ec9 let modes decide how much we can shift 2024-04-30 19:57:56 +02:00
Ahmet Inan e4b6e84d8b extracted header handler 2024-04-30 19:01:22 +02:00
Ahmet Inan 1bc83096f0 don't need the extra reserve 2024-04-30 18:45:05 +02:00
Ahmet Inan 5f1870bfa3 v2.3 2024-04-29 18:24:24 +02:00
Ahmet Inan ace7962573 store image on header detection 2024-04-29 18:17:31 +02:00
Ahmet Inan 1c6bb123a6 removed support for PD290 2024-04-29 16:59:27 +02:00
Ahmet Inan 214b9913a4 added option to reset decoder on header detection 2024-04-29 16:52:51 +02:00
Ahmet Inan daf4d88702 throw away up to end of header and signal status 2024-04-29 16:16:22 +02:00
Ahmet Inan cc7a6dcf71 replace with new pulse if too close to the previous one 2024-04-29 14:28:28 +02:00
Ahmet Inan f725809fa8 ignore scan lines shorter than 50 ms 2024-04-29 13:28:53 +02:00
Ahmet Inan e5ce8a5ee1 fake pulses for faster synchronization 2024-04-29 13:28:53 +02:00
Ahmet Inan 51c241b6fb prepare using header 2024-04-29 11:05:42 +02:00
Ahmet Inan c2efb8036f find mode from VIS code 2024-04-29 11:05:42 +02:00
Ahmet Inan 0a506406f8 estimate frequency offset using leader tone 2024-04-29 11:05:42 +02:00
Ahmet Inan 6f3cea00b2 omit transition in the sum 2024-04-29 11:05:42 +02:00
Ahmet Inan af06ae6fdc detect header 2024-04-29 11:05:42 +02:00
Ahmet Inan 97881c770c start in Robot36 free-run mode 2024-04-29 11:05:22 +02:00
Ahmet Inan e81d29f89a made the freq plot a bit nicer 2024-04-28 10:25:52 +02:00
11 zmienionych plików z 436 dodań i 73 usunięć

Wyświetl plik

@ -10,8 +10,8 @@ android {
applicationId "xdsopl.robot36"
minSdk 24
targetSdk 34
versionCode 52
versionName "2.2"
versionCode 54
versionName "2.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

Wyświetl plik

@ -11,9 +11,11 @@ import java.util.Arrays;
public class Decoder {
private final SimpleMovingAverage pulseFilter;
private final Demodulator demodulator;
private final PixelBuffer pixelBuffer;
private final PixelBuffer scopeBuffer;
private final PixelBuffer imageBuffer;
private final float[] scanLineBuffer;
private final float[] scratchBuffer;
private final int[] last5msSyncPulses;
@ -25,9 +27,16 @@ public class Decoder {
private final float[] last5msFrequencyOffsets;
private final float[] last9msFrequencyOffsets;
private final float[] last20msFrequencyOffsets;
private final int scanLineReserveSamples;
private final float[] visCodeBitFrequencies;
private final int pulseFilterDelay;
private final int scanLineMinSamples;
private final int syncPulseToleranceSamples;
private final int scanLineToleranceSamples;
private final int leaderToneSamples;
private final int leaderToneToleranceSamples;
private final int transitionSamples;
private final int visCodeBitSamples;
private final int visCodeSamples;
private final Mode rawMode;
private final ArrayList<Mode> syncPulse5msModes;
private final ArrayList<Mode> syncPulse9msModes;
@ -35,20 +44,38 @@ public class Decoder {
public Mode lastMode;
private int curSample;
private int leaderBreakIndex;
private int lastSyncPulseIndex;
private int lastScanLineSamples;
private float lastFrequencyOffset;
Decoder(PixelBuffer scopeBuffer, int sampleRate) {
Decoder(PixelBuffer scopeBuffer, PixelBuffer imageBuffer, int sampleRate) {
this.scopeBuffer = scopeBuffer;
this.imageBuffer = imageBuffer;
imageBuffer.line = -1;
pixelBuffer = new PixelBuffer(scopeBuffer.width, 2);
demodulator = new Demodulator(sampleRate);
double pulseFilterSeconds = 0.0025;
int pulseFilterSamples = (int) Math.round(pulseFilterSeconds * sampleRate) | 1;
pulseFilterDelay = (pulseFilterSamples - 1) / 2;
pulseFilter = new SimpleMovingAverage(pulseFilterSamples);
double scanLineMaxSeconds = 7;
int scanLineMaxSamples = (int) Math.round(scanLineMaxSeconds * sampleRate);
scanLineBuffer = new float[scanLineMaxSamples];
double scratchBufferSeconds = 1.1;
int scratchBufferSamples = (int) Math.round(scratchBufferSeconds * sampleRate);
scratchBuffer = new float[scratchBufferSamples];
double leaderToneSeconds = 0.3;
leaderToneSamples = (int) Math.round(leaderToneSeconds * sampleRate);
double leaderToneToleranceSeconds = leaderToneSeconds * 0.2;
leaderToneToleranceSamples = (int) Math.round(leaderToneToleranceSeconds * sampleRate);
double transitionSeconds = 0.0005;
transitionSamples = (int) Math.round(transitionSeconds * sampleRate);
double visCodeBitSeconds = 0.03;
visCodeBitSamples = (int) Math.round(visCodeBitSeconds * sampleRate);
double visCodeSeconds = 0.3;
visCodeSamples = (int) Math.round(visCodeSeconds * sampleRate);
visCodeBitFrequencies = new float[10];
int scanLineCount = 4;
last5msScanLines = new int[scanLineCount];
last9msScanLines = new int[scanLineCount];
@ -60,37 +87,34 @@ public class Decoder {
last5msFrequencyOffsets = new float[syncPulseCount];
last9msFrequencyOffsets = new float[syncPulseCount];
last20msFrequencyOffsets = new float[syncPulseCount];
double scanLineMinSeconds = 0.05;
scanLineMinSamples = (int) Math.round(scanLineMinSeconds * sampleRate);
double syncPulseToleranceSeconds = 0.03;
syncPulseToleranceSamples = (int) Math.round(syncPulseToleranceSeconds * sampleRate);
double scanLineToleranceSeconds = 0.001;
scanLineToleranceSamples = (int) Math.round(scanLineToleranceSeconds * sampleRate);
scanLineReserveSamples = sampleRate;
rawMode = new RawDecoder(sampleRate);
lastMode = rawMode;
lastScanLineSamples = (int) Math.round(0.150 * sampleRate);
Mode robot36 = new Robot_36_Color(sampleRate);
lastMode = robot36;
lastScanLineSamples = robot36.getScanLineSamples();
lastSyncPulseIndex = curSample;
syncPulse5msModes = new ArrayList<>();
syncPulse5msModes.add(RGBModes.Wraase_SC2_180(sampleRate));
syncPulse5msModes.add(RGBModes.Martin("1", 0.146432, sampleRate));
syncPulse5msModes.add(RGBModes.Martin("2", 0.073216, sampleRate));
syncPulse5msModes.add(RGBModes.Martin("1", 44, 0.146432, sampleRate));
syncPulse5msModes.add(RGBModes.Martin("2", 40, 0.073216, sampleRate));
syncPulse9msModes = new ArrayList<>();
syncPulse9msModes.add(new Robot_36_Color(sampleRate));
syncPulse9msModes.add(robot36);
syncPulse9msModes.add(new Robot_72_Color(sampleRate));
syncPulse9msModes.add(RGBModes.Scottie("1", 0.138240, sampleRate));
syncPulse9msModes.add(RGBModes.Scottie("2", 0.088064, sampleRate));
syncPulse9msModes.add(RGBModes.Scottie("DX", 0.3456, sampleRate));
syncPulse9msModes.add(RGBModes.Scottie("1", 60, 0.138240, sampleRate));
syncPulse9msModes.add(RGBModes.Scottie("2", 56, 0.088064, sampleRate));
syncPulse9msModes.add(RGBModes.Scottie("DX", 76, 0.3456, sampleRate));
syncPulse20msModes = new ArrayList<>();
syncPulse20msModes.add(new PaulDon("50", 320, 0.09152, sampleRate));
syncPulse20msModes.add(new PaulDon("90", 320, 0.17024, sampleRate));
syncPulse20msModes.add(new PaulDon("120", 640, 0.1216, sampleRate));
syncPulse20msModes.add(new PaulDon("160", 512, 0.195584, sampleRate));
syncPulse20msModes.add(new PaulDon("180", 640, 0.18304, sampleRate));
syncPulse20msModes.add(new PaulDon("240", 640, 0.24448, sampleRate));
syncPulse20msModes.add(new PaulDon("290", 640, 0.2288, sampleRate));
}
private void adjustSyncPulses(int[] pulses, int shift) {
for (int i = 0; i < pulses.length; ++i)
pulses[i] -= shift;
syncPulse20msModes.add(new PaulDon("50", 93, 320, 256, 0.09152, sampleRate));
syncPulse20msModes.add(new PaulDon("90", 99, 320, 256, 0.17024, sampleRate));
syncPulse20msModes.add(new PaulDon("120", 95, 640, 496, 0.1216, sampleRate));
syncPulse20msModes.add(new PaulDon("160", 98, 512, 400, 0.195584, sampleRate));
syncPulse20msModes.add(new PaulDon("180", 96, 640, 496, 0.18304, sampleRate));
syncPulse20msModes.add(new PaulDon("240", 97, 640, 496, 0.24448, sampleRate));
}
private double scanLineMean(int[] lines) {
@ -130,6 +154,13 @@ public class Decoder {
return bestMode;
}
private Mode findMode(ArrayList<Mode> modes, int code) {
for (Mode mode : modes)
if (mode.getCode() == code)
return mode;
return null;
}
private void copyUnscaled() {
for (int row = 0; row < pixelBuffer.height; ++row) {
int line = scopeBuffer.width * scopeBuffer.line;
@ -160,20 +191,161 @@ public class Decoder {
private void copyLines(boolean okay) {
if (!okay)
return;
boolean finish = false;
if (imageBuffer.line >= 0 && imageBuffer.line < imageBuffer.height && imageBuffer.width == pixelBuffer.width) {
int width = imageBuffer.width;
for (int row = 0; row < pixelBuffer.height && imageBuffer.line < imageBuffer.height; ++row, ++imageBuffer.line)
System.arraycopy(pixelBuffer.pixels, row * width, imageBuffer.pixels, imageBuffer.line * width, width);
finish = imageBuffer.line == imageBuffer.height;
}
int scale = scopeBuffer.width / pixelBuffer.width;
if (scale == 1)
copyUnscaled();
else
copyScaled(scale);
if (finish)
drawLines(0xff000000, 10);
}
private void drawLines(int color, int count) {
for (int i = 0; i < count; ++i) {
Arrays.fill(scopeBuffer.pixels, scopeBuffer.line * scopeBuffer.width, (scopeBuffer.line + 1) * scopeBuffer.width, color);
Arrays.fill(scopeBuffer.pixels, (scopeBuffer.line + scopeBuffer.height / 2) * scopeBuffer.width, (scopeBuffer.line + 1 + scopeBuffer.height / 2) * scopeBuffer.width, color);
scopeBuffer.line = (scopeBuffer.line + 1) % (scopeBuffer.height / 2);
}
}
private void adjustSyncPulses(int[] pulses, int shift) {
for (int i = 0; i < pulses.length; ++i)
pulses[i] -= shift;
}
private void shiftSamples(int shift) {
if (shift <= 0 || shift > curSample)
return;
leaderBreakIndex -= shift;
lastSyncPulseIndex -= shift;
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];
}
private boolean handleHeader() {
if (leaderBreakIndex < visCodeBitSamples + leaderToneToleranceSamples || curSample < leaderBreakIndex + leaderToneSamples + leaderToneToleranceSamples + visCodeSamples + visCodeBitSamples)
return false;
int breakPulseIndex = leaderBreakIndex;
leaderBreakIndex = 0;
float preBreakFreq = 0;
for (int i = 0; i < leaderToneToleranceSamples; ++i)
preBreakFreq += scanLineBuffer[breakPulseIndex - visCodeBitSamples - leaderToneToleranceSamples + i];
float leaderToneFrequency = 1900;
float centerFrequency = 1900;
float toleranceFrequency = 50;
float halfBandWidth = 400;
preBreakFreq = preBreakFreq * halfBandWidth / leaderToneToleranceSamples + centerFrequency;
if (Math.abs(preBreakFreq - leaderToneFrequency) > toleranceFrequency)
return false;
float leaderFreq = 0;
for (int i = transitionSamples; i < leaderToneSamples - leaderToneToleranceSamples; ++i)
leaderFreq += scanLineBuffer[breakPulseIndex + i];
float leaderFreqOffset = leaderFreq / (leaderToneSamples - transitionSamples - leaderToneToleranceSamples);
leaderFreq = leaderFreqOffset * halfBandWidth + centerFrequency;
if (Math.abs(leaderFreq - leaderToneFrequency) > toleranceFrequency)
return false;
float stopBitFrequency = 1200;
float pulseThresholdFrequency = (stopBitFrequency + leaderToneFrequency) / 2;
float pulseThresholdValue = (pulseThresholdFrequency - centerFrequency) / halfBandWidth;
int visBeginIndex = breakPulseIndex + leaderToneSamples - leaderToneToleranceSamples;
int visEndIndex = breakPulseIndex + leaderToneSamples + leaderToneToleranceSamples + visCodeBitSamples;
for (int i = 0; i < pulseFilter.length; ++i)
pulseFilter.avg(scanLineBuffer[visBeginIndex++] - leaderFreqOffset);
while (++visBeginIndex < visEndIndex)
if (pulseFilter.avg(scanLineBuffer[visBeginIndex] - leaderFreqOffset) < pulseThresholdValue)
break;
if (visBeginIndex >= visEndIndex)
return false;
visBeginIndex -= pulseFilterDelay;
visEndIndex = visBeginIndex + visCodeSamples;
Arrays.fill(visCodeBitFrequencies, 0);
for (int j = 0; j < 10; ++j)
for (int i = transitionSamples; i < visCodeBitSamples - transitionSamples; ++i)
visCodeBitFrequencies[j] += scanLineBuffer[visBeginIndex + visCodeBitSamples * j + i] - leaderFreqOffset;
for (int i = 0; i < 10; ++i)
visCodeBitFrequencies[i] = visCodeBitFrequencies[i] * halfBandWidth / (visCodeBitSamples - 2 * transitionSamples) + centerFrequency;
if (Math.abs(visCodeBitFrequencies[0] - stopBitFrequency) > toleranceFrequency || Math.abs(visCodeBitFrequencies[9] - stopBitFrequency) > toleranceFrequency)
return false;
float oneBitFrequency = 1100;
float zeroBitFrequency = 1300;
for (int i = 1; i < 9; ++i)
if (Math.abs(visCodeBitFrequencies[i] - oneBitFrequency) > toleranceFrequency && Math.abs(visCodeBitFrequencies[i] - zeroBitFrequency) > toleranceFrequency)
return false;
int visCode = 0;
for (int i = 0; i < 8; ++i)
visCode |= (visCodeBitFrequencies[i + 1] < stopBitFrequency ? 1 : 0) << i;
boolean check = true;
for (int i = 0; i < 8; ++i)
check ^= (visCode & 1 << i) != 0;
visCode &= 127;
if (!check)
return false;
float syncPorchFrequency = 1500;
float syncPulseFrequency = 1200;
float syncThresholdFrequency = (syncPulseFrequency + syncPorchFrequency) / 2;
float syncThresholdValue = (syncThresholdFrequency - centerFrequency) / halfBandWidth;
int syncPulseIndex = visEndIndex - visCodeBitSamples;
int syncPulseMaxIndex = visEndIndex + visCodeBitSamples;
for (int i = 0; i < pulseFilter.length; ++i)
pulseFilter.avg(scanLineBuffer[syncPulseIndex++] - leaderFreqOffset);
while (++syncPulseIndex < syncPulseMaxIndex)
if (pulseFilter.avg(scanLineBuffer[syncPulseIndex] - leaderFreqOffset) > syncThresholdValue)
break;
if (syncPulseIndex >= syncPulseMaxIndex)
return false;
syncPulseIndex -= pulseFilterDelay;
Mode mode;
int[] pulses;
int[] lines;
if ((mode = findMode(syncPulse5msModes, visCode)) != null) {
pulses = last5msSyncPulses;
lines = last5msScanLines;
} else if ((mode = findMode(syncPulse9msModes, visCode)) != null) {
pulses = last9msSyncPulses;
lines = last9msScanLines;
} else if ((mode = findMode(syncPulse20msModes, visCode)) != null) {
pulses = last20msSyncPulses;
lines = last20msScanLines;
} else {
drawLines(0xffff0000, 8);
return false;
}
mode.reset();
imageBuffer.width = mode.getWidth();
imageBuffer.height = mode.getHeight();
imageBuffer.line = 0;
lastMode = mode;
lastSyncPulseIndex = syncPulseIndex + mode.getFirstSyncPulseIndex();
lastScanLineSamples = mode.getScanLineSamples();
lastFrequencyOffset = leaderFreqOffset;
for (int i = 0; i < pulses.length; ++i)
pulses[i] = lastSyncPulseIndex + (i - pulses.length + 1) * lastScanLineSamples;
Arrays.fill(lines, lastScanLineSamples);
shiftSamples(lastSyncPulseIndex + mode.getBegin());
drawLines(0xff00ff00, 8);
drawLines(0xff000000, 10);
return true;
}
private boolean processSyncPulse(ArrayList<Mode> modes, float[] freqOffs, 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;
for (int i = 1; i < lines.length; ++i)
lines[i - 1] = lines[i];
lines[lines.length - 1] = pulses[pulses.length - 1] - pulses[pulses.length - 2];
for (int i = 1; i < freqOffs.length; ++i)
freqOffs[i - 1] = freqOffs[i];
freqOffs[pulses.length - 1] = demodulator.frequencyOffset;
@ -181,38 +353,37 @@ public class Decoder {
return false;
double mean = scanLineMean(lines);
int scanLineSamples = (int) Math.round(mean);
if (scanLineSamples > scratchBuffer.length)
if (scanLineSamples < scanLineMinSamples || scanLineSamples > scratchBuffer.length)
return false;
if (scanLineStdDev(lines, mean) > scanLineToleranceSamples)
return false;
float frequencyOffset = (float) frequencyOffsetMean(freqOffs);
Mode mode = detectMode(modes, scanLineSamples);
boolean pictureChanged = lastMode != mode
|| Math.abs(lastScanLineSamples - scanLineSamples) > scanLineToleranceSamples
|| Math.abs(lastSyncPulseIndex + scanLineSamples - pulses[pulses.length - 1]) > syncPulseToleranceSamples;
boolean pictureChanged = false;
if (imageBuffer.line < 0 || imageBuffer.line >= imageBuffer.height) {
Mode prevMode = lastMode;
lastMode = detectMode(modes, scanLineSamples);
pictureChanged = lastMode != prevMode
|| Math.abs(lastScanLineSamples - scanLineSamples) > scanLineToleranceSamples
|| Math.abs(lastSyncPulseIndex + scanLineSamples - pulses[pulses.length - 1]) > syncPulseToleranceSamples;
}
if (pictureChanged) {
drawLines(0xff000000, 10);
drawLines(0xff00ffff, 8);
drawLines(0xff000000, 10);
}
if (pulses[0] >= scanLineSamples && pictureChanged) {
int endPulse = pulses[0];
int extrapolate = endPulse / scanLineSamples;
int firstPulse = endPulse - extrapolate * scanLineSamples;
for (int pulseIndex = firstPulse; pulseIndex < endPulse; pulseIndex += scanLineSamples)
copyLines(mode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, pulseIndex, scanLineSamples, frequencyOffset));
copyLines(lastMode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, pulseIndex, scanLineSamples, frequencyOffset));
}
for (int i = pictureChanged ? 0 : lines.length - 1; i < lines.length; ++i)
copyLines(mode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, pulses[i], lines[i], frequencyOffset));
int shift = pulses[pulses.length - 1] - scanLineReserveSamples;
if (shift > scanLineReserveSamples) {
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];
}
lastMode = mode;
copyLines(lastMode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, pulses[i], lines[i], frequencyOffset));
lastSyncPulseIndex = pulses[pulses.length - 1];
lastScanLineSamples = scanLineSamples;
lastFrequencyOffset = frequencyOffset;
shiftSamples(lastSyncPulseIndex + lastMode.getBegin());
return true;
}
@ -223,15 +394,8 @@ public class Decoder {
for (int j = 0; j < recordBuffer.length / channels; ++j) {
scanLineBuffer[curSample++] = recordBuffer[j];
if (curSample >= scanLineBuffer.length) {
int shift = scanLineReserveSamples;
syncPulseIndex -= shift;
lastSyncPulseIndex -= shift;
adjustSyncPulses(last5msSyncPulses, shift);
adjustSyncPulses(last9msSyncPulses, shift);
adjustSyncPulses(last20msSyncPulses, shift);
curSample = 0;
for (int i = shift; i < scanLineBuffer.length; ++i)
scanLineBuffer[curSample++] = scanLineBuffer[i];
shiftSamples(lastScanLineSamples);
syncPulseIndex -= lastScanLineSamples;
}
}
if (syncPulseDetected) {
@ -239,11 +403,18 @@ public class Decoder {
case FiveMilliSeconds:
return processSyncPulse(syncPulse5msModes, last5msFrequencyOffsets, last5msSyncPulses, last5msScanLines, syncPulseIndex);
case NineMilliSeconds:
leaderBreakIndex = syncPulseIndex;
return processSyncPulse(syncPulse9msModes, last9msFrequencyOffsets, last9msSyncPulses, last9msScanLines, syncPulseIndex);
case TwentyMilliSeconds:
leaderBreakIndex = syncPulseIndex;
return processSyncPulse(syncPulse20msModes, last20msFrequencyOffsets, last20msSyncPulses, last20msScanLines, syncPulseIndex);
default:
return false;
}
} else if (lastSyncPulseIndex >= scanLineReserveSamples && curSample > lastSyncPulseIndex + (lastScanLineSamples * 5) / 4) {
}
if (handleHeader())
return true;
if (curSample > lastSyncPulseIndex + (lastScanLineSamples * 5) / 4) {
copyLines(lastMode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, lastSyncPulseIndex, lastScanLineSamples, lastFrequencyOffset));
lastSyncPulseIndex += lastScanLineSamples;
return true;

Wyświetl plik

@ -64,6 +64,7 @@ public class MainActivity extends AppCompatActivity {
private Bitmap peakMeterBitmap;
private PixelBuffer peakMeterBuffer;
private ImageView peakMeterView;
private PixelBuffer imageBuffer;
private float[] recordBuffer;
private AudioRecord audioRecord;
private Decoder decoder;
@ -95,6 +96,7 @@ public class MainActivity extends AppCompatActivity {
processFreqPlot();
if (newLines) {
processScope();
processImage();
setStatus(decoder.lastMode.getName());
}
}
@ -120,12 +122,18 @@ public class MainActivity extends AppCompatActivity {
int stride = freqPlotBuffer.width;
int line = stride * freqPlotBuffer.line;
int channels = recordChannel > 0 ? 2 : 1;
int samples = recordBuffer.length / channels;
int spread = 2;
Arrays.fill(freqPlotBuffer.pixels, line, line + stride, 0);
for (int i = 0; i < recordBuffer.length / channels; ++i) {
for (int i = 0; i < samples; ++i) {
int x = Math.round((recordBuffer[i] + 2.5f) * 0.25f * stride);
if (x >= 0 && x < stride)
freqPlotBuffer.pixels[line + x] = tint;
if (x >= spread && x < stride - spread)
for (int j = - spread; j <= spread; ++j)
freqPlotBuffer.pixels[line + x + j] += 1 + spread * spread - j * j;
}
int factor = 960 / samples;
for (int i = 0; i < stride; ++i)
freqPlotBuffer.pixels[line + i] = 0xff000000 | 0x00010101 * Math.min(factor * freqPlotBuffer.pixels[line + i], 255);
System.arraycopy(freqPlotBuffer.pixels, line, freqPlotBuffer.pixels, line + stride * (freqPlotBuffer.height / 2), stride);
freqPlotBuffer.line = (freqPlotBuffer.line + 1) % (freqPlotBuffer.height / 2);
int offset = stride * (freqPlotBuffer.line + freqPlotBuffer.height / 2 - height);
@ -142,6 +150,13 @@ public class MainActivity extends AppCompatActivity {
scopeView.invalidate();
}
private void processImage() {
if (imageBuffer.line < imageBuffer.height)
return;
imageBuffer.line = -1;
storeBitmap(Bitmap.createBitmap(imageBuffer.pixels, imageBuffer.width, imageBuffer.height, Bitmap.Config.ARGB_8888));
}
private void initAudioRecord() {
boolean rateChanged = true;
if (audioRecord != null) {
@ -173,7 +188,7 @@ public class MainActivity extends AppCompatActivity {
audioRecord.setRecordPositionUpdateListener(recordListener);
audioRecord.setPositionNotificationPeriod(frameCount);
if (rateChanged)
decoder = new Decoder(scopeBuffer, recordRate);
decoder = new Decoder(scopeBuffer, imageBuffer, recordRate);
startListening();
} else {
setStatus(R.string.audio_init_failed);
@ -347,6 +362,7 @@ public class MainActivity extends AppCompatActivity {
createFreqPlot(config);
peakMeterBuffer = new PixelBuffer(1, 16);
createPeakMeter();
imageBuffer = new PixelBuffer(640, 496);
List<String> permissions = new ArrayList<>();
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
permissions.add(Manifest.permission.RECORD_AUDIO);

Wyświetl plik

@ -9,7 +9,19 @@ package xdsopl.robot36;
public interface Mode {
String getName();
int getCode();
int getWidth();
int getHeight();
int getBegin();
int getFirstSyncPulseIndex();
int getScanLineSamples();
void reset();
boolean decodeScanLine(PixelBuffer pixelBuffer, float[] scratchBuffer, float[] scanLineBuffer, int scopeBufferWidth, int syncPulseIndex, int scanLineSamples, float frequencyOffset);
}

Wyświetl plik

@ -9,6 +9,7 @@ package xdsopl.robot36;
public class PaulDon implements Mode {
private final ExponentialMovingAverage lowPassFilter;
private final int horizontalPixels;
private final int verticalPixels;
private final int scanLineSamples;
private final int channelSamples;
private final int beginSamples;
@ -18,11 +19,14 @@ public class PaulDon implements Mode {
private final int yOddBeginSamples;
private final int endSamples;
private final String name;
private final int code;
@SuppressWarnings("UnnecessaryLocalVariable")
PaulDon(String name, int horizontalPixels, double channelSeconds, int sampleRate) {
PaulDon(String name, int code, int horizontalPixels, int verticalPixels, double channelSeconds, int sampleRate) {
this.name = "PD " + name;
this.code = code;
this.horizontalPixels = horizontalPixels;
this.verticalPixels = verticalPixels;
double syncPulseSeconds = 0.02;
double syncPorchSeconds = 0.00208;
double scanLineSeconds = syncPulseSeconds + syncPorchSeconds + 4 * (channelSeconds);
@ -51,11 +55,40 @@ public class PaulDon implements Mode {
return name;
}
@Override
public int getCode() {
return code;
}
@Override
public int getWidth() {
return horizontalPixels;
}
@Override
public int getHeight() {
return verticalPixels;
}
@Override
public int getBegin() {
return beginSamples;
}
@Override
public int getFirstSyncPulseIndex() {
return 0;
}
@Override
public int getScanLineSamples() {
return scanLineSamples;
}
@Override
public void reset() {
}
@Override
public boolean decodeScanLine(PixelBuffer pixelBuffer, float[] scratchBuffer, float[] scanLineBuffer, int scopeBufferWidth, int syncPulseIndex, int scanLineSamples, float frequencyOffset) {
if (syncPulseIndex + beginSamples < 0 || syncPulseIndex + endSamples > scanLineBuffer.length)

Wyświetl plik

@ -9,6 +9,8 @@ package xdsopl.robot36;
public class RGBDecoder implements Mode {
private final ExponentialMovingAverage lowPassFilter;
private final int horizontalPixels;
private final int verticalPixels;
private final int firstSyncPulseIndex;
private final int scanLineSamples;
private final int beginSamples;
private final int redBeginSamples;
@ -19,10 +21,14 @@ public class RGBDecoder implements Mode {
private final int blueSamples;
private final int endSamples;
private final String name;
private final int code;
RGBDecoder(String name, int horizontalPixels, double scanLineSeconds, double beginSeconds, double redBeginSeconds, double redEndSeconds, double greenBeginSeconds, double greenEndSeconds, double blueBeginSeconds, double blueEndSeconds, double endSeconds, int sampleRate) {
RGBDecoder(String name, int code, int horizontalPixels, int verticalPixels, double firstSyncPulseSeconds, double scanLineSeconds, double beginSeconds, double redBeginSeconds, double redEndSeconds, double greenBeginSeconds, double greenEndSeconds, double blueBeginSeconds, double blueEndSeconds, double endSeconds, int sampleRate) {
this.name = name;
this.code = code;
this.horizontalPixels = horizontalPixels;
this.verticalPixels = verticalPixels;
firstSyncPulseIndex = (int) Math.round(firstSyncPulseSeconds * sampleRate);
scanLineSamples = (int) Math.round(scanLineSeconds * sampleRate);
beginSamples = (int) Math.round(beginSeconds * sampleRate);
redBeginSamples = (int) Math.round(redBeginSeconds * sampleRate) - beginSamples;
@ -44,11 +50,40 @@ public class RGBDecoder implements Mode {
return name;
}
@Override
public int getCode() {
return code;
}
@Override
public int getWidth() {
return horizontalPixels;
}
@Override
public int getHeight() {
return verticalPixels;
}
@Override
public int getBegin() {
return beginSamples;
}
@Override
public int getFirstSyncPulseIndex() {
return firstSyncPulseIndex;
}
@Override
public int getScanLineSamples() {
return scanLineSamples;
}
@Override
public void reset() {
}
@Override
public boolean decodeScanLine(PixelBuffer pixelBuffer, float[] scratchBuffer, float[] scanLineBuffer, int scopeBufferWidth, int syncPulseIndex, int scanLineSamples, float frequencyOffset) {
if (syncPulseIndex + beginSamples < 0 || syncPulseIndex + endSamples > scanLineBuffer.length)

Wyświetl plik

@ -9,7 +9,7 @@ package xdsopl.robot36;
@SuppressWarnings("UnnecessaryLocalVariable")
public final class RGBModes {
public static RGBDecoder Martin(String name, double channelSeconds, int sampleRate) {
public static RGBDecoder Martin(String name, int code, double channelSeconds, int sampleRate) {
double syncPulseSeconds = 0.004862;
double separatorSeconds = 0.000572;
double scanLineSeconds = syncPulseSeconds + separatorSeconds + 3 * (channelSeconds + separatorSeconds);
@ -19,12 +19,13 @@ public final class RGBModes {
double blueEndSeconds = blueBeginSeconds + channelSeconds;
double redBeginSeconds = blueEndSeconds + separatorSeconds;
double redEndSeconds = redBeginSeconds + channelSeconds;
return new RGBDecoder("Martin " + name, 320, scanLineSeconds, greenBeginSeconds, redBeginSeconds, redEndSeconds, greenBeginSeconds, greenEndSeconds, blueBeginSeconds, blueEndSeconds, redEndSeconds, sampleRate);
return new RGBDecoder("Martin " + name, code, 320, 256, 0, scanLineSeconds, greenBeginSeconds, redBeginSeconds, redEndSeconds, greenBeginSeconds, greenEndSeconds, blueBeginSeconds, blueEndSeconds, redEndSeconds, sampleRate);
}
public static RGBDecoder Scottie(String name, double channelSeconds, int sampleRate) {
public static RGBDecoder Scottie(String name, int code, double channelSeconds, int sampleRate) {
double syncPulseSeconds = 0.009;
double separatorSeconds = 0.0015;
double firstSyncPulseSeconds = syncPulseSeconds + 2 * (separatorSeconds + channelSeconds);
double scanLineSeconds = syncPulseSeconds + 3 * (channelSeconds + separatorSeconds);
double blueEndSeconds = -syncPulseSeconds;
double blueBeginSeconds = blueEndSeconds - channelSeconds;
@ -32,7 +33,7 @@ public final class RGBModes {
double greenBeginSeconds = greenEndSeconds - channelSeconds;
double redBeginSeconds = separatorSeconds;
double redEndSeconds = redBeginSeconds + channelSeconds;
return new RGBDecoder("Scottie " + name, 320, scanLineSeconds, greenBeginSeconds, redBeginSeconds, redEndSeconds, greenBeginSeconds, greenEndSeconds, blueBeginSeconds, blueEndSeconds, redEndSeconds, sampleRate);
return new RGBDecoder("Scottie " + name, code, 320, 256, firstSyncPulseSeconds, scanLineSeconds, greenBeginSeconds, redBeginSeconds, redEndSeconds, greenBeginSeconds, greenEndSeconds, blueBeginSeconds, blueEndSeconds, redEndSeconds, sampleRate);
}
public static RGBDecoder Wraase_SC2_180(int sampleRate) {
@ -46,6 +47,6 @@ public final class RGBModes {
double greenEndSeconds = greenBeginSeconds + channelSeconds;
double blueBeginSeconds = greenEndSeconds;
double blueEndSeconds = blueBeginSeconds + channelSeconds;
return new RGBDecoder("Wraase SC2-180", 320, scanLineSeconds, redBeginSeconds, redBeginSeconds, redEndSeconds, greenBeginSeconds, greenEndSeconds, blueBeginSeconds, blueEndSeconds, blueEndSeconds, sampleRate);
return new RGBDecoder("Wraase SC2-180", 55, 320, 256, 0, scanLineSeconds, redBeginSeconds, redBeginSeconds, redEndSeconds, greenBeginSeconds, greenEndSeconds, blueBeginSeconds, blueEndSeconds, blueEndSeconds, sampleRate);
}
}

Wyświetl plik

@ -26,11 +26,40 @@ public class RawDecoder implements Mode {
return "Raw";
}
@Override
public int getCode() {
return -1;
}
@Override
public int getWidth() {
return -1;
}
@Override
public int getHeight() {
return -1;
}
@Override
public int getBegin() {
return 0;
}
@Override
public int getFirstSyncPulseIndex() {
return -1;
}
@Override
public int getScanLineSamples() {
return -1;
}
@Override
public void reset() {
}
@Override
public boolean decodeScanLine(PixelBuffer pixelBuffer, float[] scratchBuffer, float[] scanLineBuffer, int scopeBufferWidth, int syncPulseIndex, int scanLineSamples, float frequencyOffset) {
if (syncPulseIndex < 0 || syncPulseIndex + scanLineSamples > scanLineBuffer.length)

Wyświetl plik

@ -9,6 +9,7 @@ package xdsopl.robot36;
public class Robot_36_Color implements Mode {
private final ExponentialMovingAverage lowPassFilter;
private final int horizontalPixels;
private final int verticalPixels;
private final int scanLineSamples;
private final int luminanceSamples;
private final int separatorSamples;
@ -23,6 +24,7 @@ public class Robot_36_Color implements Mode {
@SuppressWarnings("UnnecessaryLocalVariable")
Robot_36_Color(int sampleRate) {
horizontalPixels = 320;
verticalPixels = 240;
double syncPulseSeconds = 0.009;
double syncPorchSeconds = 0.003;
double luminanceSeconds = 0.088;
@ -56,11 +58,41 @@ public class Robot_36_Color implements Mode {
return "Robot 36 Color";
}
@Override
public int getCode() {
return 8;
}
@Override
public int getWidth() {
return horizontalPixels;
}
@Override
public int getHeight() {
return verticalPixels;
}
@Override
public int getBegin() {
return beginSamples;
}
@Override
public int getFirstSyncPulseIndex() {
return 0;
}
@Override
public int getScanLineSamples() {
return scanLineSamples;
}
@Override
public void reset() {
lastEven = false;
}
@Override
public boolean decodeScanLine(PixelBuffer pixelBuffer, float[] scratchBuffer, float[] scanLineBuffer, int scopeBufferWidth, int syncPulseIndex, int scanLineSamples, float frequencyOffset) {
if (syncPulseIndex + beginSamples < 0 || syncPulseIndex + endSamples > scanLineBuffer.length)

Wyświetl plik

@ -9,6 +9,7 @@ package xdsopl.robot36;
public class Robot_72_Color implements Mode {
private final ExponentialMovingAverage lowPassFilter;
private final int horizontalPixels;
private final int verticalPixels;
private final int scanLineSamples;
private final int luminanceSamples;
private final int chrominanceSamples;
@ -21,6 +22,7 @@ public class Robot_72_Color implements Mode {
@SuppressWarnings("UnnecessaryLocalVariable")
Robot_72_Color(int sampleRate) {
horizontalPixels = 320;
verticalPixels = 240;
double syncPulseSeconds = 0.009;
double syncPorchSeconds = 0.003;
double luminanceSeconds = 0.138;
@ -54,11 +56,40 @@ public class Robot_72_Color implements Mode {
return "Robot 72 Color";
}
@Override
public int getCode() {
return 12;
}
@Override
public int getWidth() {
return horizontalPixels;
}
@Override
public int getHeight() {
return verticalPixels;
}
@Override
public int getBegin() {
return beginSamples;
}
@Override
public int getFirstSyncPulseIndex() {
return 0;
}
@Override
public int getScanLineSamples() {
return scanLineSamples;
}
@Override
public void reset() {
}
@Override
public boolean decodeScanLine(PixelBuffer pixelBuffer, float[] scratchBuffer, float[] scanLineBuffer, int scopeBufferWidth, int syncPulseIndex, int scanLineSamples, float frequencyOffset) {
if (syncPulseIndex + beginSamples < 0 || syncPulseIndex + endSamples > scanLineBuffer.length)

Wyświetl plik

@ -37,15 +37,18 @@
<string name="disable">Disable</string>
<string name="close">Close</string>
<string name="privacy_policy">Privacy Policy</string>
<string name="privacy_policy_text">To be able to decode SSTV encoded images the app needs access to the microphone.
Having access to the microphone is considered to be a sensitive permission and you have the right to know what the app does with that access:
The data recorded from the microphone is only used to fed the SSTV decoder, VU meter and the spectrum analyzer for visualization of its frequency content.
The app uses a very small temporary buffer in volatile memory and constantly overwrites this buffer with new data from the microphone.
The resulting images from the SSTV decoder is the only data that gets stored in persistent storage on your Android device.</string>
<string name="privacy_policy_text">Microphone Access:
\n\nThis app requires access to your device\'s microphone to decode Slow Scan Television (SSTV) signals.
The microphone captures the audio containing the SSTV transmission.
\n\nData Handling:
\n\nThe app uses a small temporary buffer in memory to process the audio data in real-time.
This buffer is constantly overwritten with new data as the decoding progresses.
The app does not store the raw audio captured from the microphone.
Only the decoded images resulting from the SSTV process are saved on your device\'s storage.</string>
<string name="about">About Robot36</string>
<string name="about_text">Robot36 %1$s\nCopyright 2024 Ahmet Inan
\n\nPlease read DISCLAIMER at the bottom of this page.
\n\nRobot36 decodes SSTV encoded audio signals to images.
\n\nRobot36 decodes Slow Scan Television (SSTV) images from audio.
\n\nImplementation:\nhttps://github.com/xdsopl/robot36\nBSD Zero Clause License
\n\nMode specifications:\nhttp://www.barberdsp.com/downloads/Dayton%%20Paper.pdf\nby JL Barber - 2000
\n\nDISCLAIMER:\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.</string>