estimate and compensate up to +-50 Hz of frequency offset

pull/11/head
Ahmet Inan 2024-04-23 17:35:47 +02:00
rodzic e466e86b40
commit 1f221aa79a
8 zmienionych plików z 76 dodań i 37 usunięć

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;

Wyświetl plik

@ -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]);

Wyświetl plik

@ -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));
}

Wyświetl plik

@ -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;