kopia lustrzana https://github.com/xdsopl/robot36
Porównaj commity
37 Commity
503fc50bb2
...
bbbb8c4fbe
Autor | SHA1 | Data |
---|---|---|
Ahmet Inan | bbbb8c4fbe | |
Ahmet Inan | 8fc46077c8 | |
Ahmet Inan | e52c2ee3ef | |
Ahmet Inan | 085fa2a408 | |
Ahmet Inan | 9e84f1cd8f | |
Ahmet Inan | 5d92cb4b29 | |
Ahmet Inan | 86b33327a4 | |
Ahmet Inan | d3c2c2bec3 | |
Ahmet Inan | 26073e1e2b | |
Ahmet Inan | 38f0521158 | |
Ahmet Inan | 844dc6a0b3 | |
Ahmet Inan | 2d6a4d7aca | |
Ahmet Inan | 6584934516 | |
Ahmet Inan | 0556940817 | |
Ahmet Inan | 1a0c2e5aaf | |
Ahmet Inan | eceb792e8f | |
Ahmet Inan | 00a9ddb21a | |
Ahmet Inan | 68ff54c214 | |
Ahmet Inan | 5b7ace099a | |
Ahmet Inan | dc07b809ae | |
Ahmet Inan | 705a49d214 | |
Ahmet Inan | e843719923 | |
Ahmet Inan | 0649ac48d5 | |
Ahmet Inan | 3eda3a44ef | |
Ahmet Inan | e18988f76a | |
Ahmet Inan | b68a66b4ce | |
Ahmet Inan | 4f716e76a5 | |
Ahmet Inan | cfef97e6b4 | |
Ahmet Inan | 5f7652e8b5 | |
Ahmet Inan | 0fa1a1e205 | |
Ahmet Inan | ee58ec3349 | |
Ahmet Inan | c4e4838cab | |
Ahmet Inan | a369d1754f | |
Ahmet Inan | 4cd2c86802 | |
Ahmet Inan | ef8f7df1fe | |
Ahmet Inan | 59281e7248 | |
Ahmet Inan | 21dd812de8 |
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
Ładowanie…
Reference in New Issue