diff --git a/app/src/main/java/xdsopl/robot36/Decoder.java b/app/src/main/java/xdsopl/robot36/Decoder.java index ced9c00..8c62a05 100644 --- a/app/src/main/java/xdsopl/robot36/Decoder.java +++ b/app/src/main/java/xdsopl/robot36/Decoder.java @@ -38,6 +38,7 @@ public class Decoder { private final int visCodeBitSamples; private final int visCodeSamples; private final Mode rawMode; + private final Mode hfFaxMode; private final ArrayList syncPulse5msModes; private final ArrayList syncPulse9msModes; private final ArrayList syncPulse20msModes; @@ -95,6 +96,7 @@ public class Decoder { double scanLineToleranceSeconds = 0.001; scanLineToleranceSamples = (int) Math.round(scanLineToleranceSeconds * sampleRate); rawMode = new RawDecoder(rawName, sampleRate); + hfFaxMode = new HFFax(sampleRate); Mode robot36 = new Robot_36_Color(sampleRate); currentMode = robot36; currentScanLineSamples = robot36.getScanLineSamples(); @@ -454,6 +456,8 @@ public class Decoder { mode = findMode(syncPulse9msModes, name); if (mode == null) mode = findMode(syncPulse20msModes, name); + if (mode == null && hfFaxMode.getName().equals(name)) + mode = hfFaxMode; if (mode == currentMode) { lockMode = true; return; diff --git a/app/src/main/java/xdsopl/robot36/HFFax.java b/app/src/main/java/xdsopl/robot36/HFFax.java index 95f1f61..b4dc7ac 100644 --- a/app/src/main/java/xdsopl/robot36/HFFax.java +++ b/app/src/main/java/xdsopl/robot36/HFFax.java @@ -1,22 +1,31 @@ /* -Raw decoder +HF Fax mode -Copyright 2024 Ahmet Inan +Copyright 2025 Marek Ossowski */ package xdsopl.robot36; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Rect; + +/** + * HF Fax, IOC 576, 120 lines per minute + */ public class HFFax extends BaseMode { private final ExponentialMovingAverage lowPassFilter; - private final int smallPictureMaxSamples; - private final int mediumPictureMaxSamples; private final String name; + private final int sampleRate; + private final float[] cumulated; + private int horizontalShift = 0; - HFFax(String name, int sampleRate) { - this.name = name; - smallPictureMaxSamples = (int) Math.round(0.125 * sampleRate); - mediumPictureMaxSamples = (int) Math.round(0.175 * sampleRate); + HFFax(int sampleRate) { + this.name = "HF Fax"; lowPassFilter = new ExponentialMovingAverage(); + this.sampleRate = sampleRate; + cumulated = new float[getWidth()]; } private float freqToLevel(float frequency, float offset) { @@ -35,12 +44,12 @@ public class HFFax extends BaseMode { @Override public int getWidth() { - return -1; + return 640; } @Override public int getHeight() { - return -1; + return 1200; } @Override @@ -55,33 +64,72 @@ public class HFFax extends BaseMode { @Override public int getScanLineSamples() { - return -1; + return sampleRate / 2; } @Override public void resetState() { } + @Override + public Bitmap postProcessScopeImage(Bitmap bmp) { + int realWidth = 1808; + int realHorizontalShift = horizontalShift * realWidth / getWidth(); + Bitmap bmpMutable = Bitmap.createBitmap(realWidth, bmp.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bmpMutable); + if (horizontalShift > 0) { + canvas.drawBitmap( + bmp, + new Rect(0, 0, horizontalShift, bmp.getHeight()), + new Rect(realWidth - realHorizontalShift, 0, realWidth, bmp.getHeight()), + null); + } + canvas.drawBitmap( + bmp, + new Rect(horizontalShift, 0, getWidth(), bmp.getHeight()), + new Rect(0, 1, realWidth - realHorizontalShift, bmp.getHeight() + 1), + null); + + return bmpMutable; + } + @Override public boolean decodeScanLine(PixelBuffer pixelBuffer, float[] scratchBuffer, float[] scanLineBuffer, int scopeBufferWidth, int syncPulseIndex, int scanLineSamples, float frequencyOffset) { if (syncPulseIndex < 0 || syncPulseIndex + scanLineSamples > scanLineBuffer.length) return false; - int horizontalPixels = scopeBufferWidth; - if (scanLineSamples < smallPictureMaxSamples) - horizontalPixels /= 2; - if (scanLineSamples < mediumPictureMaxSamples) - horizontalPixels /= 2; + int horizontalPixels = getWidth(); lowPassFilter.cutoff(horizontalPixels, 2 * scanLineSamples, 2); lowPassFilter.reset(); for (int i = 0; i < scanLineSamples; ++i) - scratchBuffer[i] = lowPassFilter.avg(scanLineBuffer[syncPulseIndex + i]); + scratchBuffer[i] = lowPassFilter.avg(scanLineBuffer[i]); lowPassFilter.reset(); for (int i = scanLineSamples - 1; i >= 0; --i) scratchBuffer[i] = freqToLevel(lowPassFilter.avg(scratchBuffer[i]), frequencyOffset); for (int i = 0; i < horizontalPixels; ++i) { int position = (i * scanLineSamples) / horizontalPixels; - pixelBuffer.pixels[i] = ColorConverter.GRAY(scratchBuffer[position]); + int color = ColorConverter.GRAY(scratchBuffer[position]); + pixelBuffer.pixels[i] = color; + + //accumulate recent values, forget old + float decay = 0.99f; + cumulated[i] = cumulated[i] * decay + Color.luminance(color) * (1 - decay); } + + //try to detect "sync": thick white margin + int bestIndex = 0; + float bestValue = 0; + for (int x = 0; x < getWidth(); ++x) + { + float val = cumulated[x]; + if (val > bestValue) + { + bestIndex = x; + bestValue = val; + } + } + + horizontalShift = bestIndex; + pixelBuffer.width = horizontalPixels; pixelBuffer.height = 1; return true; diff --git a/app/src/main/java/xdsopl/robot36/MainActivity.java b/app/src/main/java/xdsopl/robot36/MainActivity.java index c73ab9d..1eaf491 100644 --- a/app/src/main/java/xdsopl/robot36/MainActivity.java +++ b/app/src/main/java/xdsopl/robot36/MainActivity.java @@ -618,6 +618,10 @@ public class MainActivity extends AppCompatActivity { setMode(R.string.raw_mode); return true; } + if (id == R.id.action_force_hffax_mode) { + setMode(R.string.hf_fax); + return true; + } if (id == R.id.action_force_robot36_color) { setMode(R.string.robot36_color); return true; diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml index c06b4d5..f6b4cbc 100644 --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_main.xml @@ -97,6 +97,9 @@ android:title="@string/wraase_sc2_180" /> + Scottie 2 Scottie DX Wraase SC2–180 + HF Fax 8 kHz 16 kHz 32 kHz