kopia lustrzana https://github.com/xdsopl/robot36
estimate and compensate up to +-50 Hz of frequency offset
rodzic
e466e86b40
commit
1f221aa79a
|
@ -21,6 +21,9 @@ public class Decoder {
|
|||
private final int[] last5msScanLines;
|
||||
private final int[] last9msScanLines;
|
||||
private final int[] last20msScanLines;
|
||||
private final float[] last5msFrequencyOffsets;
|
||||
private final float[] last9msFrequencyOffsets;
|
||||
private final float[] last20msFrequencyOffsets;
|
||||
private final int scanLineToleranceSamples;
|
||||
private final int scopeWidth;
|
||||
private final int scopeHeight;
|
||||
|
@ -51,6 +54,9 @@ public class Decoder {
|
|||
last5msSyncPulses = new int[syncPulseCount];
|
||||
last9msSyncPulses = new int[syncPulseCount];
|
||||
last20msSyncPulses = new int[syncPulseCount];
|
||||
last5msFrequencyOffsets = new float[syncPulseCount];
|
||||
last9msFrequencyOffsets = new float[syncPulseCount];
|
||||
last20msFrequencyOffsets = new float[syncPulseCount];
|
||||
double scanLineToleranceSeconds = 0.001;
|
||||
scanLineToleranceSamples = (int) Math.round(scanLineToleranceSeconds * sampleRate);
|
||||
rawMode = new RawDecoder();
|
||||
|
@ -95,6 +101,14 @@ public class Decoder {
|
|||
return stdDev;
|
||||
}
|
||||
|
||||
private double frequencyOffsetMean(float[] offsets) {
|
||||
double mean = 0;
|
||||
for (float diff : offsets)
|
||||
mean += diff;
|
||||
mean /= offsets.length;
|
||||
return mean;
|
||||
}
|
||||
|
||||
private Mode detectMode(ArrayList<Mode> modes, int line) {
|
||||
Mode bestMode = rawMode;
|
||||
int bestDist = Integer.MAX_VALUE;
|
||||
|
@ -121,18 +135,22 @@ public class Decoder {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean processSyncPulse(ArrayList<Mode> modes, int[] pulses, int[] lines, int index) {
|
||||
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 < freqOffs.length; ++i)
|
||||
freqOffs[i - 1] = freqOffs[i];
|
||||
freqOffs[pulses.length - 1] = demodulator.frequencyOffset;
|
||||
if (lines[0] == 0)
|
||||
return false;
|
||||
double mean = scanLineMean(lines);
|
||||
if (scanLineStdDev(lines, mean) > scanLineToleranceSamples)
|
||||
return false;
|
||||
float frequencyOffset = (float) frequencyOffsetMean(freqOffs);
|
||||
int meanSamples = (int) Math.round(mean);
|
||||
Mode mode = detectMode(modes, meanSamples);
|
||||
curMode = mode.getName();
|
||||
|
@ -141,10 +159,10 @@ public class Decoder {
|
|||
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));
|
||||
copyLines(mode.decodeScanLine(evenBuffer, oddBuffer, scanLineBuffer, pulseIndex, meanSamples, frequencyOffset));
|
||||
}
|
||||
for (int i = 0; i < lines.length; ++i)
|
||||
copyLines(mode.decodeScanLine(evenBuffer, oddBuffer, scanLineBuffer, pulses[i], lines[i]));
|
||||
copyLines(mode.decodeScanLine(evenBuffer, oddBuffer, scanLineBuffer, pulses[i], lines[i], frequencyOffset));
|
||||
int reserve = (meanSamples * 3) / 4;
|
||||
int shift = pulses[pulses.length - 1] - reserve;
|
||||
if (shift > reserve) {
|
||||
|
@ -178,11 +196,11 @@ public class Decoder {
|
|||
if (syncPulseDetected) {
|
||||
switch (demodulator.syncPulseWidth) {
|
||||
case FiveMilliSeconds:
|
||||
return processSyncPulse(syncPulse5msModes, last5msSyncPulses, last5msScanLines, syncPulseIndex);
|
||||
return processSyncPulse(syncPulse5msModes, last5msFrequencyOffsets, last5msSyncPulses, last5msScanLines, syncPulseIndex);
|
||||
case NineMilliSeconds:
|
||||
return processSyncPulse(syncPulse9msModes, last9msSyncPulses, last9msScanLines, syncPulseIndex);
|
||||
return processSyncPulse(syncPulse9msModes, last9msFrequencyOffsets, last9msSyncPulses, last9msScanLines, syncPulseIndex);
|
||||
case TwentyMilliSeconds:
|
||||
return processSyncPulse(syncPulse20msModes, last20msSyncPulses, last20msScanLines, syncPulseIndex);
|
||||
return processSyncPulse(syncPulse20msModes, last20msFrequencyOffsets, last20msSyncPulses, last20msScanLines, syncPulseIndex);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
|
|
@ -12,6 +12,9 @@ public class Demodulator {
|
|||
private final FrequencyModulation frequencyModulation;
|
||||
private final SchmittTrigger syncPulseTrigger;
|
||||
private final Phasor baseBandOscillator;
|
||||
private final Delay syncPulseValueDelay;
|
||||
private final float syncPulseFrequencyValue;
|
||||
private final float syncPulseFrequencyTolerance;
|
||||
private final int syncPulse5msMinSamples;
|
||||
private final int syncPulse5msMaxSamples;
|
||||
private final int syncPulse9msMaxSamples;
|
||||
|
@ -28,6 +31,7 @@ public class Demodulator {
|
|||
|
||||
public SyncPulseWidth syncPulseWidth;
|
||||
public int syncPulseOffset;
|
||||
public float frequencyOffset;
|
||||
|
||||
Demodulator(int sampleRate) {
|
||||
float blackFrequency = 1500;
|
||||
|
@ -49,6 +53,7 @@ public class Demodulator {
|
|||
int syncPulseFilterSamples = (int) Math.round(syncPulseFilterSeconds * sampleRate) | 1;
|
||||
syncPulseFilterDelay = (syncPulseFilterSamples - 1) / 2;
|
||||
syncPulseFilter = new SimpleMovingAverage(syncPulseFilterSamples);
|
||||
syncPulseValueDelay = new Delay(syncPulseFilterSamples);
|
||||
float lowestFrequency = 1000;
|
||||
float highestFrequency = 2800;
|
||||
float cutoffFrequency = (highestFrequency - lowestFrequency) / 2;
|
||||
|
@ -61,6 +66,8 @@ public class Demodulator {
|
|||
float centerFrequency = (lowestFrequency + highestFrequency) / 2;
|
||||
baseBandOscillator = new Phasor(-centerFrequency, sampleRate);
|
||||
float syncPulseFrequency = 1200;
|
||||
syncPulseFrequencyValue = (syncPulseFrequency - centerFrequency) * 2 / scanLineBandwidth;
|
||||
syncPulseFrequencyTolerance = 50 * 2 / scanLineBandwidth;
|
||||
float syncPorchFrequency = 1500;
|
||||
float syncHighFrequency = (syncPulseFrequency + syncPorchFrequency) / 2;
|
||||
float syncLowFrequency = (syncPulseFrequency + syncHighFrequency) / 2;
|
||||
|
@ -76,28 +83,22 @@ public class Demodulator {
|
|||
baseBand = baseBandLowPass.push(baseBand.set(buffer[i]).mul(baseBandOscillator.rotate()));
|
||||
float frequencyValue = frequencyModulation.demod(baseBand);
|
||||
float syncPulseValue = syncPulseFilter.avg(frequencyValue);
|
||||
float scanLineLevel = 0.5f * (frequencyValue + 1);
|
||||
buffer[i] = scanLineLevel;
|
||||
float syncPulseDelayedValue = syncPulseValueDelay.push(syncPulseValue);
|
||||
buffer[i] = frequencyValue;
|
||||
if (!syncPulseTrigger.latch(syncPulseValue)) {
|
||||
++syncPulseCounter;
|
||||
} else if (syncPulseCounter < syncPulse5msMinSamples) {
|
||||
syncPulseCounter = 0;
|
||||
} else if (syncPulseCounter < syncPulse5msMaxSamples) {
|
||||
syncPulseWidth = SyncPulseWidth.FiveMilliSeconds;
|
||||
syncPulseOffset = i - syncPulseFilterDelay;
|
||||
syncPulseDetected = true;
|
||||
syncPulseCounter = 0;
|
||||
} else if (syncPulseCounter < syncPulse9msMaxSamples) {
|
||||
syncPulseWidth = SyncPulseWidth.NineMilliSeconds;
|
||||
syncPulseOffset = i - syncPulseFilterDelay;
|
||||
syncPulseDetected = true;
|
||||
syncPulseCounter = 0;
|
||||
} else if (syncPulseCounter < syncPulse20msMaxSamples) {
|
||||
syncPulseWidth = SyncPulseWidth.TwentyMilliSeconds;
|
||||
syncPulseOffset = i - syncPulseFilterDelay;
|
||||
syncPulseDetected = true;
|
||||
} else if (syncPulseCounter < syncPulse5msMinSamples || syncPulseCounter > syncPulse20msMaxSamples || Math.abs(syncPulseDelayedValue - syncPulseFrequencyValue) > syncPulseFrequencyTolerance) {
|
||||
syncPulseCounter = 0;
|
||||
} else {
|
||||
if (syncPulseCounter < syncPulse5msMaxSamples)
|
||||
syncPulseWidth = SyncPulseWidth.FiveMilliSeconds;
|
||||
else if (syncPulseCounter < syncPulse9msMaxSamples)
|
||||
syncPulseWidth = SyncPulseWidth.NineMilliSeconds;
|
||||
else
|
||||
syncPulseWidth = SyncPulseWidth.TwentyMilliSeconds;
|
||||
syncPulseOffset = i - syncPulseFilterDelay;
|
||||
frequencyOffset = syncPulseDelayedValue - syncPulseFrequencyValue;
|
||||
syncPulseDetected = true;
|
||||
syncPulseCounter = 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,5 +11,5 @@ public interface Mode {
|
|||
|
||||
int getScanLineSamples();
|
||||
|
||||
int decodeScanLine(int[] evenBuffer, int[] oddBuffer, float[] scanLineBuffer, int prevPulseIndex, int scanLineSamples);
|
||||
int decodeScanLine(int[] evenBuffer, int[] oddBuffer, float[] scanLineBuffer, int prevPulseIndex, int scanLineSamples, float frequencyOffset);
|
||||
}
|
||||
|
|
|
@ -40,6 +40,10 @@ public class PaulDon implements Mode {
|
|||
lowPassFilter = new ExponentialMovingAverage();
|
||||
}
|
||||
|
||||
private float freqToLevel(float frequency, float offset) {
|
||||
return 0.5f * (frequency - offset + 1.f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
|
@ -51,7 +55,7 @@ public class PaulDon implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int decodeScanLine(int[] evenBuffer, int[] oddBuffer, float[] scanLineBuffer, int prevPulseIndex, int scanLineSamples) {
|
||||
public int decodeScanLine(int[] evenBuffer, int[] oddBuffer, float[] scanLineBuffer, int prevPulseIndex, int scanLineSamples, float frequencyOffset) {
|
||||
if (prevPulseIndex + beginSamples < 0 || prevPulseIndex + endSamples > scanLineBuffer.length)
|
||||
return 0;
|
||||
lowPassFilter.reset(evenBuffer.length / (float) channelSamples);
|
||||
|
@ -59,7 +63,7 @@ public class PaulDon implements Mode {
|
|||
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]);
|
||||
scanLineBuffer[i] = freqToLevel(lowPassFilter.avg(scanLineBuffer[i]), frequencyOffset);
|
||||
for (int i = 0; i < evenBuffer.length; ++i) {
|
||||
int position = (i * channelSamples) / evenBuffer.length + prevPulseIndex;
|
||||
int yEvenPos = position + yEvenBeginSamples;
|
||||
|
|
|
@ -33,6 +33,10 @@ public class RGBDecoder implements Mode {
|
|||
lowPassFilter = new ExponentialMovingAverage();
|
||||
}
|
||||
|
||||
private float freqToLevel(float frequency, float offset) {
|
||||
return 0.5f * (frequency - offset + 1.f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
|
@ -44,7 +48,7 @@ public class RGBDecoder implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int decodeScanLine(int[] evenBuffer, int[] oddBuffer, float[] scanLineBuffer, int prevPulseIndex, int scanLineSamples) {
|
||||
public int decodeScanLine(int[] evenBuffer, int[] oddBuffer, float[] scanLineBuffer, int prevPulseIndex, int scanLineSamples, float frequencyOffset) {
|
||||
if (prevPulseIndex + beginSamples < 0 || prevPulseIndex + endSamples > scanLineBuffer.length)
|
||||
return 0;
|
||||
lowPassFilter.reset(evenBuffer.length / (float) greenSamples);
|
||||
|
@ -52,7 +56,7 @@ public class RGBDecoder implements Mode {
|
|||
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]);
|
||||
scanLineBuffer[i] = freqToLevel(lowPassFilter.avg(scanLineBuffer[i]), frequencyOffset);
|
||||
for (int i = 0; i < evenBuffer.length; ++i) {
|
||||
int redPos = redBeginSamples + (i * redSamples) / evenBuffer.length + prevPulseIndex;
|
||||
int greenPos = greenBeginSamples + (i * greenSamples) / evenBuffer.length + prevPulseIndex;
|
||||
|
|
|
@ -13,6 +13,10 @@ public class RawDecoder implements Mode {
|
|||
lowPassFilter = new ExponentialMovingAverage();
|
||||
}
|
||||
|
||||
private float freqToLevel(float frequency, float offset) {
|
||||
return 0.5f * (frequency - offset + 1.f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Raw";
|
||||
|
@ -24,7 +28,7 @@ public class RawDecoder implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int decodeScanLine(int[] evenBuffer, int[] oddBuffer, float[] scanLineBuffer, int prevPulseIndex, int scanLineSamples) {
|
||||
public int decodeScanLine(int[] evenBuffer, int[] oddBuffer, float[] scanLineBuffer, int prevPulseIndex, int scanLineSamples, float frequencyOffset) {
|
||||
if (prevPulseIndex < 0 || prevPulseIndex + scanLineSamples > scanLineBuffer.length)
|
||||
return 0;
|
||||
lowPassFilter.reset(evenBuffer.length / (float) scanLineSamples);
|
||||
|
@ -32,7 +36,7 @@ public class RawDecoder implements Mode {
|
|||
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]);
|
||||
scanLineBuffer[i] = freqToLevel(lowPassFilter.avg(scanLineBuffer[i]), frequencyOffset);
|
||||
for (int i = 0; i < evenBuffer.length; ++i) {
|
||||
int position = (i * scanLineSamples) / evenBuffer.length + prevPulseIndex;
|
||||
evenBuffer[i] = ColorConverter.GRAY(scanLineBuffer[position]);
|
||||
|
|
|
@ -44,6 +44,10 @@ public class Robot_36_Color implements Mode {
|
|||
lowPassFilter = new ExponentialMovingAverage();
|
||||
}
|
||||
|
||||
private float freqToLevel(float frequency, float offset) {
|
||||
return 0.5f * (frequency - offset + 1.f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Robot 36 Color";
|
||||
|
@ -55,20 +59,20 @@ public class Robot_36_Color implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int decodeScanLine(int[] evenBuffer, int[] oddBuffer, float[] scanLineBuffer, int prevPulseIndex, int scanLineSamples) {
|
||||
public int decodeScanLine(int[] evenBuffer, int[] oddBuffer, float[] scanLineBuffer, int prevPulseIndex, int scanLineSamples, float frequencyOffset) {
|
||||
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;
|
||||
boolean even = separator < 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]);
|
||||
scanLineBuffer[i] = freqToLevel(lowPassFilter.avg(scanLineBuffer[i]), frequencyOffset);
|
||||
for (int i = 0; i < evenBuffer.length; ++i) {
|
||||
int luminancePos = luminanceBeginSamples + (i * luminanceSamples) / evenBuffer.length + prevPulseIndex;
|
||||
int chrominancePos = chrominanceBeginSamples + (i * chrominanceSamples) / evenBuffer.length + prevPulseIndex;
|
||||
|
@ -76,7 +80,7 @@ public class Robot_36_Color implements Mode {
|
|||
evenBuffer[i] = ColorConverter.RGB(scanLineBuffer[luminancePos], 0, scanLineBuffer[chrominancePos]);
|
||||
} else {
|
||||
int evenYUV = evenBuffer[i];
|
||||
int oddYUV = ColorConverter.RGB(scanLineBuffer[luminancePos], scanLineBuffer[chrominancePos], 0);
|
||||
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));
|
||||
}
|
||||
|
|
|
@ -43,6 +43,10 @@ public class Robot_72_Color implements Mode {
|
|||
lowPassFilter = new ExponentialMovingAverage();
|
||||
}
|
||||
|
||||
private float freqToLevel(float frequency, float offset) {
|
||||
return 0.5f * (frequency - offset + 1.f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Robot 72 Color";
|
||||
|
@ -54,7 +58,7 @@ public class Robot_72_Color implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int decodeScanLine(int[] evenBuffer, int[] oddBuffer, float[] scanLineBuffer, int prevPulseIndex, int scanLineSamples) {
|
||||
public int decodeScanLine(int[] evenBuffer, int[] oddBuffer, float[] scanLineBuffer, int prevPulseIndex, int scanLineSamples, float frequencyOffset) {
|
||||
if (prevPulseIndex + beginSamples < 0 || prevPulseIndex + endSamples > scanLineBuffer.length)
|
||||
return 0;
|
||||
lowPassFilter.reset(evenBuffer.length / (float) luminanceSamples);
|
||||
|
@ -62,7 +66,7 @@ public class Robot_72_Color implements Mode {
|
|||
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]);
|
||||
scanLineBuffer[i] = freqToLevel(lowPassFilter.avg(scanLineBuffer[i]), frequencyOffset);
|
||||
for (int i = 0; i < evenBuffer.length; ++i) {
|
||||
int yPos = yBeginSamples + (i * luminanceSamples) / evenBuffer.length + prevPulseIndex;
|
||||
int uPos = uBeginSamples + (i * chrominanceSamples) / evenBuffer.length + prevPulseIndex;
|
||||
|
|
Ładowanie…
Reference in New Issue