From e8be1f960099d16b4db81c09e483d6e936b2d833 Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Sat, 2 Aug 2025 21:36:37 +0200 Subject: [PATCH] First version of HF Fax mode (copied from raw mode) --- app/src/main/java/xdsopl/robot36/Decoder.java | 4 + app/src/main/java/xdsopl/robot36/HFFax.java | 86 +++++++++++++++++++ .../java/xdsopl/robot36/MainActivity.java | 4 + app/src/main/res/menu/menu_main.xml | 5 ++ app/src/main/res/values/strings.xml | 1 + 5 files changed, 100 insertions(+) create mode 100644 app/src/main/java/xdsopl/robot36/HFFax.java diff --git a/app/src/main/java/xdsopl/robot36/Decoder.java b/app/src/main/java/xdsopl/robot36/Decoder.java index dc7d9ea..caeccd6 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("HF Fax", sampleRate); Mode robot36 = new Robot_36_Color(sampleRate); currentMode = robot36; currentScanLineSamples = robot36.getScanLineSamples(); @@ -450,6 +452,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 new file mode 100644 index 0000000..5854419 --- /dev/null +++ b/app/src/main/java/xdsopl/robot36/HFFax.java @@ -0,0 +1,86 @@ +package xdsopl.robot36; + +public class HFFax implements Mode { + private final ExponentialMovingAverage lowPassFilter; + private final int smallPictureMaxSamples; + private final int mediumPictureMaxSamples; + private final String name; + + private final int sr; + + HFFax(String name, int sampleRate) { + this.name = name; + smallPictureMaxSamples = (int) Math.round(0.125 * sampleRate); + mediumPictureMaxSamples = (int) Math.round(0.175 * sampleRate); + lowPassFilter = new ExponentialMovingAverage(); + this.sr = sampleRate; + } + + private float freqToLevel(float frequency, float offset) { + return 0.5f * (frequency - offset + 1.f); + } + + @Override + public String getName() { + return name; + } + + @Override + public int getCode() { + return -1; + } + + @Override + public int getWidth() { + return -1; + } + + @Override + public int getHeight() { + return -1; + } + + @Override + public int getBegin() { + return 0; + } + + @Override + public int getFirstSyncPulseIndex() { + return -1; + } + + @Override + public int getScanLineSamples() { + return sr / 2; + } + + @Override + public void reset() { + } + + @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; + lowPassFilter.cutoff(horizontalPixels, 2 * scanLineSamples, 2); + lowPassFilter.reset(); + for (int i = 0; i < scanLineSamples; ++i) + scratchBuffer[i] = lowPassFilter.avg(scanLineBuffer[syncPulseIndex + 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]); + } + 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 0a51384..2ed2563 100644 --- a/app/src/main/java/xdsopl/robot36/MainActivity.java +++ b/app/src/main/java/xdsopl/robot36/MainActivity.java @@ -612,6 +612,10 @@ public class MainActivity extends AppCompatActivity { setMode(R.string.raw_mode); return true; } + if (id == R.id.action_force_hffax_mode) { + setMode("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..25e28e5 100644 --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_main.xml @@ -102,6 +102,11 @@ android:icon="@drawable/baseline_image_not_supported_24" android:title="@string/raw_mode" app:iconTint="@color/tint" /> + Auto Mode Lock Mode Raw Mode + HF Fax Mode Listening Audio Settings Sample Rate