Porównaj commity

...

37 Commity

Autor SHA1 Wiadomość Data
Ahmet Inan bbbb8c4fbe removed scanline SMA and relaxed base band filter 2024-04-22 18:04:50 +02:00
Ahmet Inan 8fc46077c8 avoid confusion by adding sample rate 2024-04-22 17:48:50 +02:00
Ahmet Inan e52c2ee3ef replaced sma with a sinc filter 2024-04-22 17:37:28 +02:00
Ahmet Inan 085fa2a408 added complex convolution 2024-04-22 16:14:48 +02:00
Ahmet Inan 9e84f1cd8f added low pass filter 2024-04-22 16:14:48 +02:00
Ahmet Inan 5d92cb4b29 added Kaiser window 2024-04-22 14:16:42 +02:00
Ahmet Inan 86b33327a4 looks nicer 2024-04-22 09:13:24 +02:00
Ahmet Inan d3c2c2bec3 increased scan line count to four 2024-04-21 13:36:34 +02:00
Ahmet Inan 26073e1e2b oops 2024-04-21 12:41:36 +02:00
Ahmet Inan 38f0521158 only shift if possible 2024-04-21 12:41:36 +02:00
Ahmet Inan 844dc6a0b3 increased 5ms sync pulse width by 0.5ms
this improves jitter on the Wraase modes
2024-04-21 12:41:36 +02:00
Ahmet Inan 2d6a4d7aca add smoothing to raw mode too and fix mistakes 2024-04-20 18:08:55 +02:00
Ahmet Inan 6584934516 removed bufferWidth argument 2024-04-20 17:51:57 +02:00
Ahmet Inan 0556940817 do a bit of smoothing 2024-04-20 16:55:06 +02:00
Ahmet Inan 1a0c2e5aaf added Robot36 color decoder 2024-04-20 14:57:28 +02:00
Ahmet Inan eceb792e8f added simple Robot36 monochrome decoder 2024-04-20 12:48:58 +02:00
Ahmet Inan 00a9ddb21a be consistent with begin and end samples 2024-04-20 12:14:45 +02:00
Ahmet Inan 68ff54c214 added Robot 72 decoder 2024-04-20 11:24:23 +02:00
Ahmet Inan 5b7ace099a added RGB decoder to reduce code duplication 2024-04-20 10:41:43 +02:00
Ahmet Inan dc07b809ae added Scottie decoder 2024-04-20 09:18:14 +02:00
Ahmet Inan 705a49d214 give confused Scottie a fighting chance 2024-04-20 09:17:27 +02:00
Ahmet Inan e843719923 reduced code duplication 2024-04-20 08:37:03 +02:00
Ahmet Inan 0649ac48d5 added decoder for PD modes and did cleanup 2024-04-19 22:09:39 +02:00
Ahmet Inan 3eda3a44ef prepare for PD modes, which give us two lines per sync pulse 2024-04-19 20:36:50 +02:00
Ahmet Inan e18988f76a added decoder for Wraase SC2-180 2024-04-19 19:27:18 +02:00
Ahmet Inan b68a66b4ce added decoder for Martin 2024-04-19 19:27:18 +02:00
Ahmet Inan 4f716e76a5 prepared for mode individual decoders 2024-04-19 17:34:58 +02:00
Ahmet Inan cfef97e6b4 use mean of scan line samples 2024-04-19 16:42:03 +02:00
Ahmet Inan 5f7652e8b5 detect mode 2024-04-19 16:17:28 +02:00
Ahmet Inan 0fa1a1e205 removed tint 2024-04-19 12:02:27 +02:00
Ahmet Inan ee58ec3349 added Decoder class 2024-04-19 11:58:16 +02:00
Ahmet Inan c4e4838cab moved bitmap invalidation out 2024-04-19 11:19:49 +02:00
Ahmet Inan a369d1754f extrapolate missing pulses 2024-04-19 09:34:18 +02:00
Ahmet Inan 4cd2c86802 simplified sync pulse handling 2024-04-19 08:13:21 +02:00
Ahmet Inan ef8f7df1fe added scan line mean method 2024-04-18 17:18:59 +02:00
Ahmet Inan 59281e7248 disable scan line after processing 2024-04-18 16:28:49 +02:00
Ahmet Inan 21dd812de8 wait for regular sync pulse 2024-04-18 14:49:54 +02:00
15 zmienionych plików z 810 dodań i 83 usunięć

Wyświetl plik

@ -0,0 +1,54 @@
/*
Color converter
Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
*/
package xdsopl.robot36;
public final class ColorConverter {
private static int clamp(int value) {
return Math.min(Math.max(value, 0), 255);
}
private static float clamp(float value) {
return Math.min(Math.max(value, 0), 1);
}
private static int float2int(float level) {
int intensity = Math.round(255 * level);
return clamp(intensity);
}
private static int compress(float level) {
float compressed = (float) Math.sqrt(clamp(level));
return float2int(compressed);
}
private static int YUV2RGB(int Y, int U, int V) {
Y -= 16;
U -= 128;
V -= 128;
int R = clamp((298 * Y + 409 * V + 128) >> 8);
int G = clamp((298 * Y - 100 * U - 208 * V + 128) >> 8);
int B = clamp((298 * Y + 516 * U + 128) >> 8);
return 0xff000000 | (R << 16) | (G << 8) | B;
}
public static int GRAY(float level) {
return 0xff000000 | 0x00010101 * compress(level);
}
public static int RGB(float red, float green, float blue) {
return 0xff000000 | (float2int(red) << 16) | (float2int(green) << 8) | float2int(blue);
}
public static int YUV2RGB(float Y, float U, float V) {
return YUV2RGB(float2int(Y), float2int(U), float2int(V));
}
public static int YUV2RGB(int YUV) {
return YUV2RGB((YUV & 0x00ff0000) >> 16, (YUV & 0x0000ff00) >> 8, YUV & 0x000000ff);
}
}

Wyświetl plik

@ -0,0 +1,41 @@
/*
Complex Convolution
Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
*/
package xdsopl.robot36;
public class ComplexConvolution {
public final int length;
public final float[] taps;
private final float[] real;
private final float[] imag;
private final Complex sum;
private int pos;
ComplexConvolution(int length) {
this.length = length;
this.taps = new float[length];
this.real = new float[length];
this.imag = new float[length];
this.sum = new Complex();
this.pos = 0;
}
Complex push(Complex input) {
real[pos] = input.real;
imag[pos] = input.imag;
if (++pos >= length)
pos = 0;
sum.real = 0;
sum.imag = 0;
for (float tap : taps) {
sum.real += tap * real[pos];
sum.imag += tap * imag[pos];
if (++pos >= length)
pos = 0;
}
return sum;
}
}

Wyświetl plik

@ -0,0 +1,190 @@
/*
SSTV Decoder
Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
*/
package xdsopl.robot36;
import java.util.ArrayList;
public class Decoder {
private final Demodulator demodulator;
private final float[] scanLineBuffer;
private final int[] evenBuffer;
private final int[] oddBuffer;
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;
private final Mode rawMode;
private final ArrayList<Mode> syncPulse5msModes;
private final ArrayList<Mode> syncPulse9msModes;
private final ArrayList<Mode> syncPulse20msModes;
public String curMode;
public int curLine;
private int curSample;
Decoder(int[] scopePixels, int scopeWidth, int scopeHeight, int sampleRate) {
this.scopePixels = scopePixels;
this.scopeWidth = scopeWidth;
this.scopeHeight = scopeHeight;
evenBuffer = new int[scopeWidth];
oddBuffer = new int[scopeWidth];
demodulator = new Demodulator(sampleRate);
double scanLineMaxSeconds = 5;
int scanLineMaxSamples = (int) Math.round(scanLineMaxSeconds * sampleRate);
scanLineBuffer = new float[scanLineMaxSamples];
int scanLineCount = 4;
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);
rawMode = new RawDecoder();
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));
syncPulse9msModes = new ArrayList<>();
syncPulse9msModes.add(new Robot_36_Color(sampleRate));
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));
syncPulse20msModes = new ArrayList<>();
syncPulse20msModes.add(new PaulDon("50", 0.09152, sampleRate));
syncPulse20msModes.add(new PaulDon("90", 0.17024, sampleRate));
syncPulse20msModes.add(new PaulDon("120", 0.1216, sampleRate));
syncPulse20msModes.add(new PaulDon("160", 0.195584, sampleRate));
syncPulse20msModes.add(new PaulDon("180", 0.18304, sampleRate));
syncPulse20msModes.add(new PaulDon("240", 0.24448, sampleRate));
syncPulse20msModes.add(new PaulDon("290", 0.2288, 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 double scanLineStdDev(int[] lines, double mean) {
double stdDev = 0;
for (int diff : lines)
stdDev += (diff - mean) * (diff - mean);
stdDev = Math.sqrt(stdDev / lines.length);
return stdDev;
}
private Mode detectMode(ArrayList<Mode> modes, int line) {
Mode bestMode = rawMode;
int bestDist = Integer.MAX_VALUE;
for (Mode mode : modes) {
int dist = Math.abs(line - mode.getScanLineSamples());
if (dist <= scanLineToleranceSamples && dist < bestDist) {
bestDist = dist;
bestMode = mode;
}
}
return bestMode;
}
private void copyLines(int lines) {
if (lines > 0) {
System.arraycopy(evenBuffer, 0, scopePixels, scopeWidth * curLine, scopeWidth);
System.arraycopy(evenBuffer, 0, scopePixels, scopeWidth * (curLine + scopeHeight), scopeWidth);
curLine = (curLine + 1) % scopeHeight;
}
if (lines == 2) {
System.arraycopy(oddBuffer, 0, scopePixels, scopeWidth * curLine, scopeWidth);
System.arraycopy(oddBuffer, 0, scopePixels, scopeWidth * (curLine + scopeHeight), scopeWidth);
curLine = (curLine + 1) % scopeHeight;
}
}
private boolean processSyncPulse(ArrayList<Mode> modes, 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;
double mean = scanLineMean(lines);
if (scanLineStdDev(lines, mean) > scanLineToleranceSamples)
return false;
int meanSamples = (int) Math.round(mean);
Mode mode = detectMode(modes, meanSamples);
curMode = mode.getName();
if (pulses[0] >= meanSamples) {
int endPulse = pulses[0];
int extrapolate = endPulse / meanSamples;
int firstPulse = endPulse - extrapolate * meanSamples;
for (int pulseIndex = firstPulse; pulseIndex < endPulse; pulseIndex += meanSamples)
copyLines(mode.decodeScanLine(evenBuffer, oddBuffer, scanLineBuffer, pulseIndex, meanSamples));
}
for (int i = 0; i < lines.length; ++i)
copyLines(mode.decodeScanLine(evenBuffer, oddBuffer, scanLineBuffer, pulses[i], lines[i]));
int reserve = (meanSamples * 3) / 4;
int shift = pulses[pulses.length - 1] - reserve;
if (shift > reserve) {
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];
}
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(syncPulse5msModes, last5msSyncPulses, last5msScanLines, syncPulseIndex);
case NineMilliSeconds:
return processSyncPulse(syncPulse9msModes, last9msSyncPulses, last9msScanLines, syncPulseIndex);
case TwentyMilliSeconds:
return processSyncPulse(syncPulse20msModes, last20msSyncPulses, last20msScanLines, syncPulseIndex);
}
}
return false;
}
}

Wyświetl plik

@ -11,12 +11,10 @@ public class Demodulator {
private final ComplexMovingAverage syncPulse5msFilter;
private final ComplexMovingAverage syncPulse9msFilter;
private final ComplexMovingAverage syncPulse20msFilter;
private final ComplexMovingAverage scanLineFilter;
private final ComplexMovingAverage baseBandLowPass;
private final ComplexConvolution baseBandLowPass;
private final FrequencyModulation scanLineDemod;
private final SchmittTrigger syncPulseTrigger;
private final Phasor syncPulseOscillator;
private final Phasor scanLineOscillator;
private final Phasor baseBandOscillator;
private final Delay syncPulse5msDelay;
private final Delay syncPulse9msDelay;
@ -35,7 +33,6 @@ public class Demodulator {
private Complex syncPulse5ms;
private Complex syncPulse9ms;
private Complex syncPulse20ms;
private Complex scanLine;
public enum SyncPulseWidth {
FiveMilliSeconds,
@ -54,10 +51,7 @@ public class Demodulator {
float whiteFrequency = 2300;
float scanLineBandwidth = whiteFrequency - blackFrequency;
scanLineDemod = new FrequencyModulation(scanLineBandwidth, sampleRate);
float scanLineCutoff = scanLineBandwidth / 2;
int scanLineFilterSamples = (int) Math.round(0.443 * sampleRate / scanLineCutoff) | 1;
scanLineFilter = new ComplexMovingAverage(scanLineFilterSamples);
double syncPulse5msSeconds = 0.005;
double syncPulse5msSeconds = 0.0055;
double syncPulse9msSeconds = 0.009;
double syncPulse20msSeconds = 0.020;
int syncPulse5msSamples = (int) Math.round(syncPulse5msSeconds * sampleRate) | 1;
@ -68,17 +62,19 @@ public class Demodulator {
syncPulse5msFilter = new ComplexMovingAverage(syncPulse5msSamples);
syncPulse9msFilter = new ComplexMovingAverage(syncPulse9msSamples);
syncPulse20msFilter = new ComplexMovingAverage(syncPulse20msSamples);
float lowestFrequency = 1100;
float highestFrequency = 2300;
float lowestFrequency = 1000;
float highestFrequency = 2800;
float cutoffFrequency = (highestFrequency - lowestFrequency) / 2;
int lowPassSamples = (int) Math.round(0.443 * sampleRate / cutoffFrequency) | 1;
baseBandLowPass = new ComplexMovingAverage(lowPassSamples);
double baseBandLowPassSeconds = 0.002;
int baseBandLowPassSamples = (int) Math.round(baseBandLowPassSeconds * sampleRate) | 1;
baseBandLowPass = new ComplexConvolution(baseBandLowPassSamples);
Kaiser kaiser = new Kaiser();
for (int i = 0; i < baseBandLowPass.length; ++i)
baseBandLowPass.taps[i] = (float) (kaiser.window(2.0, i, baseBandLowPass.length) * Filter.lowPass(cutoffFrequency, sampleRate, i, baseBandLowPass.length));
float centerFrequency = (lowestFrequency + highestFrequency) / 2;
baseBandOscillator = new Phasor(-centerFrequency, sampleRate);
float syncPulseFrequency = 1200;
syncPulseOscillator = new Phasor(-(syncPulseFrequency - centerFrequency), sampleRate);
float grayFrequency = (blackFrequency + whiteFrequency) / 2;
scanLineOscillator = new Phasor(-(grayFrequency - centerFrequency), sampleRate);
int syncPulse5msDelaySamples = (powerWindowSamples - 1) / 2 - (syncPulse5msSamples - 1) / 2;
int syncPulse9msDelaySamples = (powerWindowSamples - 1) / 2 - (syncPulse9msSamples - 1) / 2;
int syncPulse20msDelaySamples = (powerWindowSamples - 1) / 2 - (syncPulse20msSamples - 1) / 2;
@ -91,24 +87,22 @@ public class Demodulator {
syncPulse5ms = new Complex();
syncPulse9ms = new Complex();
syncPulse20ms = new Complex();
scanLine = new Complex();
}
public boolean process(float[] buffer) {
boolean syncPulseDetected = false;
for (int i = 0; i < buffer.length; ++i) {
baseBand = baseBandLowPass.avg(baseBand.set(buffer[i]).mul(baseBandOscillator.rotate()));
baseBand = baseBandLowPass.push(baseBand.set(buffer[i]).mul(baseBandOscillator.rotate()));
syncPulse = syncPulse.set(baseBand).mul(syncPulseOscillator.rotate());
syncPulse5ms = syncPulse5msFilter.avg(syncPulse5ms.set(syncPulse));
syncPulse9ms = syncPulse9msFilter.avg(syncPulse9ms.set(syncPulse));
syncPulse20ms = syncPulse20msFilter.avg(syncPulse20ms.set(syncPulse));
scanLine = scanLineFilter.avg(scanLine.set(baseBand).mul(scanLineOscillator.rotate()));
float averagePower = powerAvg.avg(baseBand.norm());
float syncPulse5msValue = syncPulse5msDelay.push(syncPulse5ms.norm()) / averagePower;
float syncPulse9msValue = syncPulse9msDelay.push(syncPulse9ms.norm()) / averagePower;
float syncPulse20msValue = syncPulse20msDelay.push(syncPulse20ms.norm()) / averagePower;
float scanLineValue = scanLineDemod.demod(scanLine);
float scanLineLevel = Math.min(Math.max(0.5f * (scanLineValue + 1), 0), 1);
float scanLineValue = scanLineDemod.demod(baseBand);
float scanLineLevel = 0.5f * (scanLineValue + 1);
if (syncPulseTrigger.latch(syncPulse5msValue)) {
if (syncPulse5msMaxValue < syncPulse5msValue) {
syncPulse5msMaxValue = syncPulse5msValue;
@ -124,7 +118,7 @@ public class Demodulator {
}
++syncPulseCounter;
} else if (syncPulseCounter > syncPulseLowMark && syncPulseCounter < syncPulseHighMark) {
int filterDelay = (powerAvg.length - 1) / 2 - (scanLineFilter.length - 1) / 2;
int filterDelay = (powerAvg.length - 1) / 2;
syncPulseOffset = i - syncPulseCounter - filterDelay;
float mid9ms20msSum = ((9.f / 20.f) + 1.f) / 2.f;
float mid9ms20msPwr = mid9ms20msSum * mid9ms20msSum;

Wyświetl plik

@ -0,0 +1,33 @@
/*
Exponential Moving Average
Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
*/
package xdsopl.robot36;
public class ExponentialMovingAverage {
private float alpha;
private float prev;
ExponentialMovingAverage() {
this.alpha = 1;
}
ExponentialMovingAverage(float alpha) {
this.alpha = alpha;
}
public float avg(float input) {
return prev = prev * (1 - alpha) + alpha * input;
}
public void reset() {
prev = 0;
}
public void reset(float alpha) {
this.alpha = alpha;
prev = 0;
}
}

Wyświetl plik

@ -0,0 +1,22 @@
/*
FIR Filter
Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
*/
package xdsopl.robot36;
public final class Filter {
public static double sinc(double x) {
if (x == 0)
return 1;
x *= Math.PI;
return Math.sin(x) / x;
}
public static double lowPass(double cutoff, double rate, int n, int N) {
double f = 2 * cutoff / rate;
double x = n - (N - 1) / 2.0;
return f * sinc(f * x);
}
}

Wyświetl plik

@ -0,0 +1,46 @@
/*
Kaiser window
Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
*/
package xdsopl.robot36;
import java.util.Arrays;
public class Kaiser {
double[] summands;
Kaiser() {
// i0(x) converges for x inside -3*Pi:3*Pi in less than 35 iterations
summands = new double[35];
}
private double square(double value) {
return value * value;
}
/*
i0() implements the zero-th order modified Bessel function of the first kind:
https://en.wikipedia.org/wiki/Bessel_function#Modified_Bessel_functions:_I%CE%B1,_K%CE%B1
$I_\alpha(x) = i^{-\alpha} J_\alpha(ix) = \sum_{m=0}^\infty \frac{1}{m!\, \Gamma(m+\alpha+1)}\left(\frac{x}{2}\right)^{2m+\alpha}$
$I_0(x) = J_0(ix) = \sum_{m=0}^\infty \frac{1}{m!\, \Gamma(m+1)}\left(\frac{x}{2}\right)^{2m} = \sum_{m=0}^\infty \left(\frac{x^m}{2^m\,m!}\right)^{2}$
We obviously can't use the factorial here, so let's get rid of it:
$= 1 + \left(\frac{x}{2 \cdot 1}\right)^2 + \left(\frac{x}{2 \cdot 1}\cdot \frac{x}{2 \cdot 2}\right)^2 + \left(\frac{x}{2 \cdot 1}\cdot \frac{x}{2 \cdot 2}\cdot \frac{x}{2 \cdot 3}\right)^2 + .. = 1 + \sum_{m=1}^\infty \left(\prod_{n=1}^m \frac
*/
private double i0(double x) {
summands[0] = 1;
double val = 1;
for (int n = 1; n < summands.length; ++n)
summands[n] = square(val *= x / (2 * n));
Arrays.sort(summands);
double sum = 0;
for (int n = summands.length - 1; n >= 0; --n)
sum += summands[n];
return sum;
}
public double window(double a, int n, int N) {
return i0(Math.PI * a * Math.sqrt(1 - square((2.0 * n) / (N - 1) - 1))) / i0(Math.PI * a);
}
}

Wyświetl plik

@ -6,13 +6,13 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
package xdsopl.robot36;
import android.Manifest;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.Manifest;
import android.widget.ImageView;
import android.widget.TextView;
@ -35,14 +35,9 @@ 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 tint;
private int curLine;
private int curSample;
private Decoder decoder;
private void setStatus(int id) {
status.setText(id);
@ -56,63 +51,14 @@ public class MainActivity extends AppCompatActivity {
@Override
public void onPeriodicNotification(AudioRecord audioRecord) {
audioRecord.read(recordBuffer, 0, recordBuffer.length, AudioRecord.READ_BLOCKING);
visualizeSignal(demodulator.process(recordBuffer));
if (decoder.process(recordBuffer)) {
scopeBitmap.setPixels(scopePixels, scopeWidth * decoder.curLine, scopeWidth, 0, 0, scopeWidth, scopeHeight);
scopeView.invalidate();
status.setText(decoder.curMode);
}
}
};
private void processOneLine(int syncPulseIndex) {
for (int i = 0; i < scopeWidth; ++i) {
int position = (i * syncPulseIndex) / scopeWidth;
int intensity = (int) Math.round(255 * Math.sqrt(scanLineBuffer[position]));
int pixelColor = 0xff000000 | 0x00010101 * intensity;
scopePixels[scopeWidth * curLine + i] = pixelColor;
}
for (int i = 0; i < scopeWidth; ++i)
scopePixels[scopeWidth * (curLine + scopeHeight) + i] = scopePixels[scopeWidth * curLine + i];
curLine = (curLine + 1) % scopeHeight;
scopeBitmap.setPixels(scopePixels, scopeWidth * curLine, scopeWidth, 0, 0, scopeWidth, scopeHeight);
scopeView.invalidate();
}
private void 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;
curSample = 0;
for (int i = shift; i < scanLineBuffer.length; ++i)
scanLineBuffer[curSample++] = scanLineBuffer[i];
}
}
if (syncPulseDetected && syncPulseIndex >= 0) {
switch (demodulator.syncPulseWidth) {
case FiveMilliSeconds:
status.setText("5 ms sync pulse");
break;
case NineMilliSeconds:
status.setText("9 ms sync pulse");
break;
case TwentyMilliSeconds:
status.setText("20 ms sync pulse");
break;
}
processOneLine(syncPulseIndex);
int count = curSample - syncPulseIndex;
curSample = 0;
for (int i = 0; i < count; ++i)
scanLineBuffer[curSample++] = scanLineBuffer[syncPulseIndex + i];
}
}
void initTools(int sampleRate) {
demodulator = new Demodulator(sampleRate);
double scanLineMaxSeconds = 1.5;
int scanLineMaxSamples = (int) Math.round(scanLineMaxSeconds * sampleRate);
scanLineBuffer = new float[scanLineMaxSamples];
}
private void initAudioRecord() {
int audioSource = MediaRecorder.AudioSource.UNPROCESSED;
int channelConfig = AudioFormat.CHANNEL_IN_MONO;
@ -129,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);
@ -180,7 +126,6 @@ public class MainActivity extends AppCompatActivity {
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
tint = getColor(R.color.tint);
status = findViewById(R.id.status);
scopeView = findViewById(R.id.scope);
scopeBitmap = Bitmap.createBitmap(scopeWidth, scopeHeight, Bitmap.Config.ARGB_8888);

Wyświetl plik

@ -0,0 +1,15 @@
/*
Mode interface
Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
*/
package xdsopl.robot36;
public interface Mode {
String getName();
int getScanLineSamples();
int decodeScanLine(int[] evenBuffer, int[] oddBuffer, float[] scanLineBuffer, int prevPulseIndex, int scanLineSamples);
}

Wyświetl plik

@ -0,0 +1,73 @@
/*
PD modes
Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
*/
package xdsopl.robot36;
public class PaulDon implements Mode {
private final ExponentialMovingAverage lowPassFilter;
private final int scanLineSamples;
private final int channelSamples;
private final int beginSamples;
private final int yEvenBeginSamples;
private final int vAvgBeginSamples;
private final int uAvgBeginSamples;
private final int yOddBeginSamples;
private final int endSamples;
private final String name;
PaulDon(String name, double channelSeconds, int sampleRate) {
this.name = "PD " + name;
double syncPulseSeconds = 0.02;
double syncPorchSeconds = 0.00208;
double scanLineSeconds = syncPulseSeconds + syncPorchSeconds + 4 * (channelSeconds);
scanLineSamples = (int) Math.round(scanLineSeconds * sampleRate);
channelSamples = (int) Math.round(channelSeconds * sampleRate);
double yEvenBeginSeconds = syncPulseSeconds / 2 + syncPorchSeconds;
yEvenBeginSamples = (int) Math.round(yEvenBeginSeconds * sampleRate);
beginSamples = yEvenBeginSamples;
double vAvgBeginSeconds = yEvenBeginSeconds + channelSeconds;
vAvgBeginSamples = (int) Math.round(vAvgBeginSeconds * sampleRate);
double uAvgBeginSeconds = vAvgBeginSeconds + channelSeconds;
uAvgBeginSamples = (int) Math.round(uAvgBeginSeconds * sampleRate);
double yOddBeginSeconds = uAvgBeginSeconds + channelSeconds;
yOddBeginSamples = (int) Math.round(yOddBeginSeconds * sampleRate);
double yOddEndSeconds = yOddBeginSeconds + channelSeconds;
endSamples = (int) Math.round(yOddEndSeconds * sampleRate);
lowPassFilter = new ExponentialMovingAverage();
}
@Override
public String getName() {
return name;
}
@Override
public int getScanLineSamples() {
return scanLineSamples;
}
@Override
public int decodeScanLine(int[] evenBuffer, int[] oddBuffer, float[] scanLineBuffer, int prevPulseIndex, int scanLineSamples) {
if (prevPulseIndex + beginSamples < 0 || prevPulseIndex + endSamples > scanLineBuffer.length)
return 0;
lowPassFilter.reset(evenBuffer.length / (float) channelSamples);
for (int i = prevPulseIndex + beginSamples; i < prevPulseIndex + endSamples; ++i)
scanLineBuffer[i] = lowPassFilter.avg(scanLineBuffer[i]);
lowPassFilter.reset(evenBuffer.length / (float) channelSamples);
for (int i = prevPulseIndex + endSamples - 1; i >= prevPulseIndex + beginSamples; --i)
scanLineBuffer[i] = lowPassFilter.avg(scanLineBuffer[i]);
for (int i = 0; i < evenBuffer.length; ++i) {
int position = (i * channelSamples) / evenBuffer.length + prevPulseIndex;
int yEvenPos = position + yEvenBeginSamples;
int vAvgPos = position + vAvgBeginSamples;
int uAvgPos = position + uAvgBeginSamples;
int yOddPos = position + yOddBeginSamples;
evenBuffer[i] = ColorConverter.YUV2RGB(scanLineBuffer[yEvenPos], scanLineBuffer[uAvgPos], scanLineBuffer[vAvgPos]);
oddBuffer[i] = ColorConverter.YUV2RGB(scanLineBuffer[yOddPos], scanLineBuffer[uAvgPos], scanLineBuffer[vAvgPos]);
}
return 2;
}
}

Wyświetl plik

@ -0,0 +1,64 @@
/*
Decoder for RGB modes
Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
*/
package xdsopl.robot36;
public class RGBDecoder implements Mode {
private final ExponentialMovingAverage lowPassFilter;
private final int scanLineSamples;
private final int beginSamples;
private final int redBeginSamples;
private final int redSamples;
private final int greenBeginSamples;
private final int greenSamples;
private final int blueBeginSamples;
private final int blueSamples;
private final int endSamples;
private final String name;
RGBDecoder(String name, double scanLineSeconds, double beginSeconds, double redBeginSeconds, double redEndSeconds, double greenBeginSeconds, double greenEndSeconds, double blueBeginSeconds, double blueEndSeconds, double endSeconds, int sampleRate) {
this.name = name;
scanLineSamples = (int) Math.round(scanLineSeconds * sampleRate);
beginSamples = (int) Math.round(beginSeconds * sampleRate);
redBeginSamples = (int) Math.round(redBeginSeconds * sampleRate);
redSamples = (int) Math.round((redEndSeconds - redBeginSeconds) * sampleRate);
greenBeginSamples = (int) Math.round(greenBeginSeconds * sampleRate);
greenSamples = (int) Math.round((greenEndSeconds - greenBeginSeconds) * sampleRate);
blueBeginSamples = (int) Math.round(blueBeginSeconds * sampleRate);
blueSamples = (int) Math.round((blueEndSeconds - blueBeginSeconds) * sampleRate);
endSamples = (int) Math.round(endSeconds * sampleRate);
lowPassFilter = new ExponentialMovingAverage();
}
@Override
public String getName() {
return name;
}
@Override
public int getScanLineSamples() {
return scanLineSamples;
}
@Override
public int decodeScanLine(int[] evenBuffer, int[] oddBuffer, float[] scanLineBuffer, int prevPulseIndex, int scanLineSamples) {
if (prevPulseIndex + beginSamples < 0 || prevPulseIndex + endSamples > scanLineBuffer.length)
return 0;
lowPassFilter.reset(evenBuffer.length / (float) greenSamples);
for (int i = prevPulseIndex + beginSamples; i < prevPulseIndex + endSamples; ++i)
scanLineBuffer[i] = lowPassFilter.avg(scanLineBuffer[i]);
lowPassFilter.reset(evenBuffer.length / (float) greenSamples);
for (int i = prevPulseIndex + endSamples - 1; i >= prevPulseIndex + beginSamples; --i)
scanLineBuffer[i] = lowPassFilter.avg(scanLineBuffer[i]);
for (int i = 0; i < evenBuffer.length; ++i) {
int redPos = redBeginSamples + (i * redSamples) / evenBuffer.length + prevPulseIndex;
int greenPos = greenBeginSamples + (i * greenSamples) / evenBuffer.length + prevPulseIndex;
int bluePos = blueBeginSamples + (i * blueSamples) / evenBuffer.length + prevPulseIndex;
evenBuffer[i] = ColorConverter.RGB(scanLineBuffer[redPos], scanLineBuffer[greenPos], scanLineBuffer[bluePos]);
}
return 1;
}
}

Wyświetl plik

@ -0,0 +1,50 @@
/*
RGB modes
Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
*/
package xdsopl.robot36;
public final class RGBModes {
public static RGBDecoder Martin(String name, double channelSeconds, int sampleRate) {
double syncPulseSeconds = 0.004862;
double separatorSeconds = 0.000572;
double scanLineSeconds = syncPulseSeconds + separatorSeconds + 3 * (channelSeconds + separatorSeconds);
double greenBeginSeconds = syncPulseSeconds / 2 + separatorSeconds;
double greenEndSeconds = greenBeginSeconds + channelSeconds;
double blueBeginSeconds = greenEndSeconds + separatorSeconds;
double blueEndSeconds = blueBeginSeconds + channelSeconds;
double redBeginSeconds = blueEndSeconds + separatorSeconds;
double redEndSeconds = redBeginSeconds + channelSeconds;
return new RGBDecoder("Martin " + name, scanLineSeconds, greenBeginSeconds, redBeginSeconds, redEndSeconds, greenBeginSeconds, greenEndSeconds, blueBeginSeconds, blueEndSeconds, redEndSeconds, sampleRate);
}
public static RGBDecoder Scottie(String name, double channelSeconds, int sampleRate) {
double syncPulseSeconds = 0.009;
double separatorSeconds = 0.0015;
double scanLineSeconds = syncPulseSeconds + 3 * (channelSeconds + separatorSeconds);
double blueEndSeconds = -syncPulseSeconds / 2;
double blueBeginSeconds = blueEndSeconds - channelSeconds;
double greenEndSeconds = blueBeginSeconds - separatorSeconds;
double greenBeginSeconds = greenEndSeconds - channelSeconds;
double redBeginSeconds = syncPulseSeconds / 2 + separatorSeconds;
double redEndSeconds = redBeginSeconds + channelSeconds;
return new RGBDecoder("Scottie " + name, scanLineSeconds, greenBeginSeconds, redBeginSeconds, redEndSeconds, greenBeginSeconds, greenEndSeconds, blueBeginSeconds, blueEndSeconds, redEndSeconds, sampleRate);
}
public static RGBDecoder Wraase_SC2_180(int sampleRate) {
double syncPulseSeconds = 0.0055225;
double syncPorchSeconds = 0.0005;
double channelSeconds = 0.235;
double scanLineSeconds = syncPulseSeconds + syncPorchSeconds + 3 * channelSeconds;
double redBeginSeconds = syncPulseSeconds / 2 + syncPorchSeconds;
double redEndSeconds = redBeginSeconds + channelSeconds;
double greenBeginSeconds = redEndSeconds;
double greenEndSeconds = greenBeginSeconds + channelSeconds;
double blueBeginSeconds = greenEndSeconds;
double blueEndSeconds = blueBeginSeconds + channelSeconds;
return new RGBDecoder("Wraase SC2-180", scanLineSeconds, redBeginSeconds, redBeginSeconds, redEndSeconds, greenBeginSeconds, greenEndSeconds, blueBeginSeconds, blueEndSeconds, blueEndSeconds, sampleRate);
}
}

Wyświetl plik

@ -0,0 +1,42 @@
/*
Raw decoder
Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
*/
package xdsopl.robot36;
public class RawDecoder implements Mode {
private final ExponentialMovingAverage lowPassFilter;
RawDecoder() {
lowPassFilter = new ExponentialMovingAverage();
}
@Override
public String getName() {
return "Raw";
}
@Override
public int getScanLineSamples() {
return -1;
}
@Override
public int decodeScanLine(int[] evenBuffer, int[] oddBuffer, float[] scanLineBuffer, int prevPulseIndex, int scanLineSamples) {
if (prevPulseIndex < 0 || prevPulseIndex + scanLineSamples > scanLineBuffer.length)
return 0;
lowPassFilter.reset(evenBuffer.length / (float) scanLineSamples);
for (int i = prevPulseIndex; i < prevPulseIndex + scanLineSamples; ++i)
scanLineBuffer[i] = lowPassFilter.avg(scanLineBuffer[i]);
lowPassFilter.reset(evenBuffer.length / (float) scanLineSamples);
for (int i = prevPulseIndex + scanLineSamples - 1; i >= prevPulseIndex; --i)
scanLineBuffer[i] = lowPassFilter.avg(scanLineBuffer[i]);
for (int i = 0; i < evenBuffer.length; ++i) {
int position = (i * scanLineSamples) / evenBuffer.length + prevPulseIndex;
evenBuffer[i] = ColorConverter.GRAY(scanLineBuffer[position]);
}
return 1;
}
}

Wyświetl plik

@ -0,0 +1,85 @@
/*
Robot 36 Color
Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
*/
package xdsopl.robot36;
public class Robot_36_Color implements Mode {
private final ExponentialMovingAverage lowPassFilter;
private final int scanLineSamples;
private final int luminanceSamples;
private final int separatorSamples;
private final int chrominanceSamples;
private final int beginSamples;
private final int luminanceBeginSamples;
private final int separatorBeginSamples;
private final int chrominanceBeginSamples;
private final int endSamples;
Robot_36_Color(int sampleRate) {
double syncPulseSeconds = 0.009;
double syncPorchSeconds = 0.003;
double luminanceSeconds = 0.088;
double separatorSeconds = 0.0045;
double porchSeconds = 0.0015;
double chrominanceSeconds = 0.044;
double scanLineSeconds = syncPulseSeconds + syncPorchSeconds + luminanceSeconds + separatorSeconds + porchSeconds + chrominanceSeconds;
scanLineSamples = (int) Math.round(scanLineSeconds * sampleRate);
luminanceSamples = (int) Math.round(luminanceSeconds * sampleRate);
separatorSamples = (int) Math.round(separatorSeconds * sampleRate);
chrominanceSamples = (int) Math.round(chrominanceSeconds * sampleRate);
double luminanceBeginSeconds = syncPulseSeconds / 2 + syncPorchSeconds;
luminanceBeginSamples = (int) Math.round(luminanceBeginSeconds * sampleRate);
beginSamples = luminanceBeginSamples;
double separatorBeginSeconds = luminanceBeginSeconds + luminanceSeconds;
separatorBeginSamples = (int) Math.round(separatorBeginSeconds * sampleRate);
double separatorEndSeconds = separatorBeginSeconds + separatorSeconds;
double chrominanceBeginSeconds = separatorEndSeconds + porchSeconds;
chrominanceBeginSamples = (int) Math.round(chrominanceBeginSeconds * sampleRate);
double chrominanceEndSeconds = chrominanceBeginSeconds + chrominanceSeconds;
endSamples = (int) Math.round(chrominanceEndSeconds * sampleRate);
lowPassFilter = new ExponentialMovingAverage();
}
@Override
public String getName() {
return "Robot 36 Color";
}
@Override
public int getScanLineSamples() {
return scanLineSamples;
}
@Override
public int decodeScanLine(int[] evenBuffer, int[] oddBuffer, float[] scanLineBuffer, int prevPulseIndex, int scanLineSamples) {
if (prevPulseIndex + beginSamples < 0 || prevPulseIndex + endSamples > scanLineBuffer.length)
return 0;
float separator = 0;
for (int i = 0; i < separatorSamples; ++i)
separator += scanLineBuffer[prevPulseIndex + separatorBeginSamples + i];
separator /= separatorSamples;
boolean even = separator < 0.5f;
lowPassFilter.reset(evenBuffer.length / (float) luminanceSamples);
for (int i = prevPulseIndex + beginSamples; i < prevPulseIndex + endSamples; ++i)
scanLineBuffer[i] = lowPassFilter.avg(scanLineBuffer[i]);
lowPassFilter.reset(evenBuffer.length / (float) luminanceSamples);
for (int i = prevPulseIndex + endSamples - 1; i >= prevPulseIndex + beginSamples; --i)
scanLineBuffer[i] = lowPassFilter.avg(scanLineBuffer[i]);
for (int i = 0; i < evenBuffer.length; ++i) {
int luminancePos = luminanceBeginSamples + (i * luminanceSamples) / evenBuffer.length + prevPulseIndex;
int chrominancePos = chrominanceBeginSamples + (i * chrominanceSamples) / evenBuffer.length + prevPulseIndex;
if (even) {
evenBuffer[i] = ColorConverter.RGB(scanLineBuffer[luminancePos], 0, scanLineBuffer[chrominancePos]);
} else {
int evenYUV = evenBuffer[i];
int oddYUV = ColorConverter.RGB(scanLineBuffer[luminancePos], scanLineBuffer[chrominancePos], 0);
evenBuffer[i] = ColorConverter.YUV2RGB((evenYUV & 0x00ff00ff) | (oddYUV & 0x0000ff00));
oddBuffer[i] = ColorConverter.YUV2RGB((oddYUV & 0x00ffff00) | (evenYUV & 0x000000ff));
}
}
return even ? 0 : 2;
}
}

Wyświetl plik

@ -0,0 +1,73 @@
/*
Robot 72 Color
Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
*/
package xdsopl.robot36;
public class Robot_72_Color implements Mode {
private final ExponentialMovingAverage lowPassFilter;
private final int scanLineSamples;
private final int luminanceSamples;
private final int chrominanceSamples;
private final int beginSamples;
private final int yBeginSamples;
private final int vBeginSamples;
private final int uBeginSamples;
private final int endSamples;
Robot_72_Color(int sampleRate) {
double syncPulseSeconds = 0.009;
double syncPorchSeconds = 0.003;
double luminanceSeconds = 0.138;
double separatorSeconds = 0.0045;
double porchSeconds = 0.0015;
double chrominanceSeconds = 0.069;
double scanLineSeconds = syncPulseSeconds + syncPorchSeconds + luminanceSeconds + 2 * (separatorSeconds + porchSeconds + chrominanceSeconds);
scanLineSamples = (int) Math.round(scanLineSeconds * sampleRate);
luminanceSamples = (int) Math.round(luminanceSeconds * sampleRate);
chrominanceSamples = (int) Math.round(chrominanceSeconds * sampleRate);
double yBeginSeconds = syncPulseSeconds / 2 + syncPorchSeconds;
yBeginSamples = (int) Math.round(yBeginSeconds * sampleRate);
beginSamples = yBeginSamples;
double yEndSeconds = yBeginSeconds + luminanceSeconds;
double vBeginSeconds = yEndSeconds + separatorSeconds + porchSeconds;
vBeginSamples = (int) Math.round(vBeginSeconds * sampleRate);
double vEndSeconds = vBeginSeconds + chrominanceSeconds;
double uBeginSeconds = vEndSeconds + separatorSeconds + porchSeconds;
uBeginSamples = (int) Math.round(uBeginSeconds * sampleRate);
double uEndSeconds = uBeginSeconds + chrominanceSeconds;
endSamples = (int) Math.round(uEndSeconds * sampleRate);
lowPassFilter = new ExponentialMovingAverage();
}
@Override
public String getName() {
return "Robot 72 Color";
}
@Override
public int getScanLineSamples() {
return scanLineSamples;
}
@Override
public int decodeScanLine(int[] evenBuffer, int[] oddBuffer, float[] scanLineBuffer, int prevPulseIndex, int scanLineSamples) {
if (prevPulseIndex + beginSamples < 0 || prevPulseIndex + endSamples > scanLineBuffer.length)
return 0;
lowPassFilter.reset(evenBuffer.length / (float) luminanceSamples);
for (int i = prevPulseIndex + beginSamples; i < prevPulseIndex + endSamples; ++i)
scanLineBuffer[i] = lowPassFilter.avg(scanLineBuffer[i]);
lowPassFilter.reset(evenBuffer.length / (float) luminanceSamples);
for (int i = prevPulseIndex + endSamples - 1; i >= prevPulseIndex + beginSamples; --i)
scanLineBuffer[i] = lowPassFilter.avg(scanLineBuffer[i]);
for (int i = 0; i < evenBuffer.length; ++i) {
int yPos = yBeginSamples + (i * luminanceSamples) / evenBuffer.length + prevPulseIndex;
int uPos = uBeginSamples + (i * chrominanceSamples) / evenBuffer.length + prevPulseIndex;
int vPos = vBeginSamples + (i * chrominanceSamples) / evenBuffer.length + prevPulseIndex;
evenBuffer[i] = ColorConverter.YUV2RGB(scanLineBuffer[yPos], scanLineBuffer[uPos], scanLineBuffer[vPos]);
}
return 1;
}
}