diff --git a/app/src/main/java/xdsopl/robot36/Decoder.java b/app/src/main/java/xdsopl/robot36/Decoder.java index ccfd0dd..455c15c 100644 --- a/app/src/main/java/xdsopl/robot36/Decoder.java +++ b/app/src/main/java/xdsopl/robot36/Decoder.java @@ -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 modes, int line) { Mode bestMode = rawMode; int bestDist = Integer.MAX_VALUE; @@ -121,18 +135,22 @@ public class Decoder { } } - private boolean processSyncPulse(ArrayList modes, int[] pulses, int[] lines, int index) { + private boolean processSyncPulse(ArrayList 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; diff --git a/app/src/main/java/xdsopl/robot36/Demodulator.java b/app/src/main/java/xdsopl/robot36/Demodulator.java index 2a4ea69..9d2eaf8 100644 --- a/app/src/main/java/xdsopl/robot36/Demodulator.java +++ b/app/src/main/java/xdsopl/robot36/Demodulator.java @@ -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; } } diff --git a/app/src/main/java/xdsopl/robot36/Mode.java b/app/src/main/java/xdsopl/robot36/Mode.java index 3df04d6..5fcf40b 100644 --- a/app/src/main/java/xdsopl/robot36/Mode.java +++ b/app/src/main/java/xdsopl/robot36/Mode.java @@ -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); } diff --git a/app/src/main/java/xdsopl/robot36/PaulDon.java b/app/src/main/java/xdsopl/robot36/PaulDon.java index 3d46a56..5234498 100644 --- a/app/src/main/java/xdsopl/robot36/PaulDon.java +++ b/app/src/main/java/xdsopl/robot36/PaulDon.java @@ -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; diff --git a/app/src/main/java/xdsopl/robot36/RGBDecoder.java b/app/src/main/java/xdsopl/robot36/RGBDecoder.java index dbaf6c7..29fac62 100644 --- a/app/src/main/java/xdsopl/robot36/RGBDecoder.java +++ b/app/src/main/java/xdsopl/robot36/RGBDecoder.java @@ -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; diff --git a/app/src/main/java/xdsopl/robot36/RawDecoder.java b/app/src/main/java/xdsopl/robot36/RawDecoder.java index 262cdae..df5fb5f 100644 --- a/app/src/main/java/xdsopl/robot36/RawDecoder.java +++ b/app/src/main/java/xdsopl/robot36/RawDecoder.java @@ -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]); diff --git a/app/src/main/java/xdsopl/robot36/Robot_36_Color.java b/app/src/main/java/xdsopl/robot36/Robot_36_Color.java index e4f5343..507a97f 100644 --- a/app/src/main/java/xdsopl/robot36/Robot_36_Color.java +++ b/app/src/main/java/xdsopl/robot36/Robot_36_Color.java @@ -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)); } diff --git a/app/src/main/java/xdsopl/robot36/Robot_72_Color.java b/app/src/main/java/xdsopl/robot36/Robot_72_Color.java index f359641..adf824b 100644 --- a/app/src/main/java/xdsopl/robot36/Robot_72_Color.java +++ b/app/src/main/java/xdsopl/robot36/Robot_72_Color.java @@ -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;