From e8be1f960099d16b4db81c09e483d6e936b2d833 Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Sat, 2 Aug 2025 21:36:37 +0200 Subject: [PATCH 01/24] 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 From f003ea48707d93a52ba7205f6d181670b867d529 Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Sun, 3 Aug 2025 20:58:48 +0200 Subject: [PATCH 02/24] Added some comments --- app/src/main/java/xdsopl/robot36/Decoder.java | 3 ++ .../main/java/xdsopl/robot36/Demodulator.java | 5 ++- app/src/main/java/xdsopl/robot36/Mode.java | 34 +++++++++++++++++++ .../java/xdsopl/robot36/Robot_72_Color.java | 6 ++++ 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/xdsopl/robot36/Decoder.java b/app/src/main/java/xdsopl/robot36/Decoder.java index caeccd6..e131e52 100644 --- a/app/src/main/java/xdsopl/robot36/Decoder.java +++ b/app/src/main/java/xdsopl/robot36/Decoder.java @@ -405,6 +405,9 @@ public class Decoder { return true; } + /** + @return true if new lines present + */ public boolean process(float[] recordBuffer, int channelSelect) { boolean syncPulseDetected = demodulator.process(recordBuffer, channelSelect); int syncPulseIndex = currentSample + demodulator.syncPulseOffset; diff --git a/app/src/main/java/xdsopl/robot36/Demodulator.java b/app/src/main/java/xdsopl/robot36/Demodulator.java index 2bf882a..a758472 100644 --- a/app/src/main/java/xdsopl/robot36/Demodulator.java +++ b/app/src/main/java/xdsopl/robot36/Demodulator.java @@ -66,7 +66,7 @@ public class Demodulator { double centerFrequency = (lowestFrequency + highestFrequency) / 2; baseBandOscillator = new Phasor(-centerFrequency, sampleRate); double syncPulseFrequency = 1200; - syncPulseFrequencyValue = (float) ((syncPulseFrequency - centerFrequency) * 2 / scanLineBandwidth); + syncPulseFrequencyValue = (float) ((syncPulseFrequency - centerFrequency) * 2 / scanLineBandwidth); //converts to range from -1 to 1 syncPulseFrequencyTolerance = (float) (50 * 2 / scanLineBandwidth); double syncPorchFrequency = 1500; double syncHighFrequency = (syncPulseFrequency + syncPorchFrequency) / 2; @@ -77,6 +77,9 @@ public class Demodulator { baseBand = new Complex(); } + /** + * @return true if sync pulse detected + */ public boolean process(float[] buffer, int channelSelect) { boolean syncPulseDetected = false; int channels = channelSelect > 0 ? 2 : 1; diff --git a/app/src/main/java/xdsopl/robot36/Mode.java b/app/src/main/java/xdsopl/robot36/Mode.java index 3614711..36ae102 100644 --- a/app/src/main/java/xdsopl/robot36/Mode.java +++ b/app/src/main/java/xdsopl/robot36/Mode.java @@ -7,21 +7,55 @@ Copyright 2024 Ahmet Inan package xdsopl.robot36; public interface Mode { + /** + * @return mode name + */ String getName(); + /** + * @return VIS code + */ int getCode(); + /** + * @return image width + */ int getWidth(); + /** + * @return image height + */ int getHeight(); + /** + * @return number of samples from sync pulse to start of image data + */ int getBegin(); + /** + * @return number of samples from start of scanline to sync pulse??? nonzero for Scottie only? + */ int getFirstSyncPulseIndex(); + /** + * @return number of samples in a scanline + */ int getScanLineSamples(); + /** + * Reset internal state. + */ void reset(); + /** + * @param pixelBuffer buffer to store decoded pixels + * @param scratchBuffer buffer for temporary data + * @param scanLineBuffer raw samples to be decoded, can contain more than one scanline + * @param scopeBufferWidth ??? used in RawDecoder, initializes width? + * @param syncPulseIndex number of samples from array start to sync pulse + * @param scanLineSamples number of samples per scanline + * @param frequencyOffset correction of frequency of expected vs actual sync pulse (normalized to range (-1, 1)) + * @return true if scanline was decoded + */ boolean decodeScanLine(PixelBuffer pixelBuffer, float[] scratchBuffer, float[] scanLineBuffer, int scopeBufferWidth, int syncPulseIndex, int scanLineSamples, float frequencyOffset); } 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 88a9786..4374937 100644 --- a/app/src/main/java/xdsopl/robot36/Robot_72_Color.java +++ b/app/src/main/java/xdsopl/robot36/Robot_72_Color.java @@ -47,6 +47,12 @@ public class Robot_72_Color implements Mode { lowPassFilter = new ExponentialMovingAverage(); } + /** + * FIXME same in other modes (copy&paste) + * @param frequency frequency, range (-1,1) + * @param offset correction, range (-1,1) + * @return pixel value, range (0,1) + */ private float freqToLevel(float frequency, float offset) { return 0.5f * (frequency - offset + 1.f); } From 9d0a03c9ccce0f5b92071643715be05ff4bbe461 Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Sun, 3 Aug 2025 21:56:31 +0200 Subject: [PATCH 03/24] Allowing to save higher resolution image from scope --- app/src/main/java/xdsopl/robot36/MainActivity.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/xdsopl/robot36/MainActivity.java b/app/src/main/java/xdsopl/robot36/MainActivity.java index 2ed2563..4f3b78b 100644 --- a/app/src/main/java/xdsopl/robot36/MainActivity.java +++ b/app/src/main/java/xdsopl/robot36/MainActivity.java @@ -551,7 +551,7 @@ public class MainActivity extends AppCompatActivity { fgColor = getColor(R.color.fg); thinColor = getColor(R.color.thin); tintColor = getColor(R.color.tint); - scopeBuffer = new PixelBuffer(640, 2 * 1280); + scopeBuffer = new PixelBuffer(640 * 2, 2 * 1280 * 2); waterfallPlotBuffer = new PixelBuffer(256, 2 * 256); peakMeterBuffer = new PixelBuffer(1, 16); imageBuffer = new PixelBuffer(800, 616); @@ -826,7 +826,11 @@ public class MainActivity extends AppCompatActivity { int height = scopeBuffer.height / 2; int stride = scopeBuffer.width; int offset = stride * scopeBuffer.line; - storeBitmap(Bitmap.createBitmap(scopeBuffer.pixels, offset, stride, width, height, Bitmap.Config.ARGB_8888)); + Bitmap bmp = Bitmap.createBitmap(scopeBuffer.pixels, offset, stride, width, height, Bitmap.Config.ARGB_8888); + if (currentMode == null || !currentMode.equals("HF Fax")) { + bmp = Bitmap.createScaledBitmap(bmp, width / 2, height / 2, true); + } + storeBitmap(bmp); } private void createScope(Configuration config) { From 5843c10907aeb390221c83121a962978b6030eda Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Tue, 5 Aug 2025 00:14:52 +0200 Subject: [PATCH 04/24] Added frequency markers to waterfall --- .../main/java/xdsopl/robot36/Demodulator.java | 7 ++++--- .../java/xdsopl/robot36/MainActivity.java | 21 +++++++++++++++++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/xdsopl/robot36/Demodulator.java b/app/src/main/java/xdsopl/robot36/Demodulator.java index a758472..e1621a2 100644 --- a/app/src/main/java/xdsopl/robot36/Demodulator.java +++ b/app/src/main/java/xdsopl/robot36/Demodulator.java @@ -33,9 +33,11 @@ public class Demodulator { public int syncPulseOffset; public float frequencyOffset; + public static final double syncPulseFrequency = 1200; + public static final double blackFrequency = 1500; + public static final double whiteFrequency = 2300; + Demodulator(int sampleRate) { - double blackFrequency = 1500; - double whiteFrequency = 2300; double scanLineBandwidth = whiteFrequency - blackFrequency; frequencyModulation = new FrequencyModulation(scanLineBandwidth, sampleRate); double syncPulse5msSeconds = 0.005; @@ -65,7 +67,6 @@ public class Demodulator { baseBandLowPass.taps[i] = (float) (kaiser.window(2.0, i, baseBandLowPass.length) * Filter.lowPass(cutoffFrequency, sampleRate, i, baseBandLowPass.length)); double centerFrequency = (lowestFrequency + highestFrequency) / 2; baseBandOscillator = new Phasor(-centerFrequency, sampleRate); - double syncPulseFrequency = 1200; syncPulseFrequencyValue = (float) ((syncPulseFrequency - centerFrequency) * 2 / scanLineBandwidth); //converts to range from -1 to 1 syncPulseFrequencyTolerance = (float) (50 * 2 / scanLineBandwidth); double syncPorchFrequency = 1500; diff --git a/app/src/main/java/xdsopl/robot36/MainActivity.java b/app/src/main/java/xdsopl/robot36/MainActivity.java index 4f3b78b..cb67aba 100644 --- a/app/src/main/java/xdsopl/robot36/MainActivity.java +++ b/app/src/main/java/xdsopl/robot36/MainActivity.java @@ -15,6 +15,7 @@ import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Bitmap; +import android.graphics.Color; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.MediaRecorder; @@ -43,6 +44,7 @@ import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.widget.ShareActionProvider; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; +import androidx.core.graphics.ColorUtils; import androidx.core.graphics.Insets; import androidx.core.os.LocaleListCompat; import androidx.core.view.MenuItemCompat; @@ -72,6 +74,7 @@ public class MainActivity extends AppCompatActivity { private ImageView peakMeterView; private PixelBuffer imageBuffer; private ShortTimeFourierTransform stft; + private final int binWidthHz = 10; private short[] shortBuffer; private float[] recordBuffer; private AudioRecord audioRecord; @@ -223,8 +226,22 @@ public class MainActivity extends AppCompatActivity { double lowest = Math.log(1e-9); double highest = Math.log(1); double range = highest - lowest; + int lowestBin = 14; for (int i = 0; i < stride; ++i) - waterfallPlotBuffer.pixels[line + i] = rainbow((Math.log(stft.power[i + 14]) - lowest) / range); + waterfallPlotBuffer.pixels[line + i] = rainbow((Math.log(stft.power[i + lowestBin]) - lowest) / range); + + int[] markerFrequencies = new int[] { + (int)Demodulator.syncPulseFrequency, + (int)Demodulator.blackFrequency, + (int)Demodulator.whiteFrequency, + }; + for (int freq: markerFrequencies) { + int marker = freq / binWidthHz - lowestBin; + waterfallPlotBuffer.pixels[line + marker - 1] = Color.BLACK; + waterfallPlotBuffer.pixels[line + marker] = ColorUtils.blendARGB(waterfallPlotBuffer.pixels[line + marker], Color.GREEN, 0.8f); + waterfallPlotBuffer.pixels[line + marker + 1] = Color.BLACK; + } + System.arraycopy(waterfallPlotBuffer.pixels, line, waterfallPlotBuffer.pixels, line + stride * (waterfallPlotBuffer.height / 2), stride); } } @@ -315,7 +332,7 @@ public class MainActivity extends AppCompatActivity { if (rateChanged) { decoder = new Decoder(scopeBuffer, imageBuffer, getString(R.string.raw_mode), recordRate); decoder.setMode(currentMode); - stft = new ShortTimeFourierTransform(recordRate / 10, 3); + stft = new ShortTimeFourierTransform(recordRate / binWidthHz, 3); } startListening(); } else { From e7621aa8bf90183d1d598fe6b81083c33062f3d8 Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Wed, 6 Aug 2025 23:56:04 +0200 Subject: [PATCH 05/24] Even bigger scope buffer for full resolution --- app/src/main/java/xdsopl/robot36/Decoder.java | 2 +- app/src/main/java/xdsopl/robot36/HFFax.java | 4 ++-- app/src/main/java/xdsopl/robot36/MainActivity.java | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/xdsopl/robot36/Decoder.java b/app/src/main/java/xdsopl/robot36/Decoder.java index e131e52..84fa415 100644 --- a/app/src/main/java/xdsopl/robot36/Decoder.java +++ b/app/src/main/java/xdsopl/robot36/Decoder.java @@ -55,7 +55,7 @@ public class Decoder { this.scopeBuffer = scopeBuffer; this.imageBuffer = imageBuffer; imageBuffer.line = -1; - pixelBuffer = new PixelBuffer(800, 2); + pixelBuffer = new PixelBuffer(2000, 2); demodulator = new Demodulator(sampleRate); double pulseFilterSeconds = 0.0025; int pulseFilterSamples = (int) Math.round(pulseFilterSeconds * sampleRate) | 1; diff --git a/app/src/main/java/xdsopl/robot36/HFFax.java b/app/src/main/java/xdsopl/robot36/HFFax.java index 5854419..4e08d61 100644 --- a/app/src/main/java/xdsopl/robot36/HFFax.java +++ b/app/src/main/java/xdsopl/robot36/HFFax.java @@ -32,12 +32,12 @@ public class HFFax implements Mode { @Override public int getWidth() { - return -1; + return 1808; } @Override public int getHeight() { - return -1; + return 1200; } @Override diff --git a/app/src/main/java/xdsopl/robot36/MainActivity.java b/app/src/main/java/xdsopl/robot36/MainActivity.java index cb67aba..09b30de 100644 --- a/app/src/main/java/xdsopl/robot36/MainActivity.java +++ b/app/src/main/java/xdsopl/robot36/MainActivity.java @@ -568,7 +568,7 @@ public class MainActivity extends AppCompatActivity { fgColor = getColor(R.color.fg); thinColor = getColor(R.color.thin); tintColor = getColor(R.color.tint); - scopeBuffer = new PixelBuffer(640 * 2, 2 * 1280 * 2); + scopeBuffer = new PixelBuffer(640 * 3, 2 * 1280 * 3); waterfallPlotBuffer = new PixelBuffer(256, 2 * 256); peakMeterBuffer = new PixelBuffer(1, 16); imageBuffer = new PixelBuffer(800, 616); @@ -845,7 +845,7 @@ public class MainActivity extends AppCompatActivity { int offset = stride * scopeBuffer.line; Bitmap bmp = Bitmap.createBitmap(scopeBuffer.pixels, offset, stride, width, height, Bitmap.Config.ARGB_8888); if (currentMode == null || !currentMode.equals("HF Fax")) { - bmp = Bitmap.createScaledBitmap(bmp, width / 2, height / 2, true); + bmp = Bitmap.createScaledBitmap(bmp, width / 3, height / 3, true); } storeBitmap(bmp); } From 9558080ff8ea759c0f164a712e37eda121586d3f Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Sat, 16 Aug 2025 21:37:35 +0200 Subject: [PATCH 06/24] Added BaseMode abstract class --- app/src/main/java/xdsopl/robot36/BaseMode.java | 4 ++++ app/src/main/java/xdsopl/robot36/HFFax.java | 2 +- app/src/main/java/xdsopl/robot36/PaulDon.java | 2 +- app/src/main/java/xdsopl/robot36/RGBDecoder.java | 2 +- app/src/main/java/xdsopl/robot36/RawDecoder.java | 2 +- app/src/main/java/xdsopl/robot36/Robot_36_Color.java | 2 +- app/src/main/java/xdsopl/robot36/Robot_72_Color.java | 2 +- 7 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/xdsopl/robot36/BaseMode.java diff --git a/app/src/main/java/xdsopl/robot36/BaseMode.java b/app/src/main/java/xdsopl/robot36/BaseMode.java new file mode 100644 index 0000000..fcdc7e8 --- /dev/null +++ b/app/src/main/java/xdsopl/robot36/BaseMode.java @@ -0,0 +1,4 @@ +package xdsopl.robot36; + +public abstract class BaseMode implements Mode { +} diff --git a/app/src/main/java/xdsopl/robot36/HFFax.java b/app/src/main/java/xdsopl/robot36/HFFax.java index 4e08d61..49e0361 100644 --- a/app/src/main/java/xdsopl/robot36/HFFax.java +++ b/app/src/main/java/xdsopl/robot36/HFFax.java @@ -1,6 +1,6 @@ package xdsopl.robot36; -public class HFFax implements Mode { +public class HFFax extends BaseMode { private final ExponentialMovingAverage lowPassFilter; private final int smallPictureMaxSamples; private final int mediumPictureMaxSamples; diff --git a/app/src/main/java/xdsopl/robot36/PaulDon.java b/app/src/main/java/xdsopl/robot36/PaulDon.java index 8883081..efc369d 100644 --- a/app/src/main/java/xdsopl/robot36/PaulDon.java +++ b/app/src/main/java/xdsopl/robot36/PaulDon.java @@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan package xdsopl.robot36; -public class PaulDon implements Mode { +public class PaulDon extends BaseMode { private final ExponentialMovingAverage lowPassFilter; private final int horizontalPixels; private final int verticalPixels; diff --git a/app/src/main/java/xdsopl/robot36/RGBDecoder.java b/app/src/main/java/xdsopl/robot36/RGBDecoder.java index febef26..34b4034 100644 --- a/app/src/main/java/xdsopl/robot36/RGBDecoder.java +++ b/app/src/main/java/xdsopl/robot36/RGBDecoder.java @@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan package xdsopl.robot36; -public class RGBDecoder implements Mode { +public class RGBDecoder extends BaseMode { private final ExponentialMovingAverage lowPassFilter; private final int horizontalPixels; private final int verticalPixels; diff --git a/app/src/main/java/xdsopl/robot36/RawDecoder.java b/app/src/main/java/xdsopl/robot36/RawDecoder.java index f86a63d..3dffa17 100644 --- a/app/src/main/java/xdsopl/robot36/RawDecoder.java +++ b/app/src/main/java/xdsopl/robot36/RawDecoder.java @@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan package xdsopl.robot36; -public class RawDecoder implements Mode { +public class RawDecoder extends BaseMode { private final ExponentialMovingAverage lowPassFilter; private final int smallPictureMaxSamples; private final int mediumPictureMaxSamples; 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 8ccdcdb..94e22b3 100644 --- a/app/src/main/java/xdsopl/robot36/Robot_36_Color.java +++ b/app/src/main/java/xdsopl/robot36/Robot_36_Color.java @@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan package xdsopl.robot36; -public class Robot_36_Color implements Mode { +public class Robot_36_Color extends BaseMode { private final ExponentialMovingAverage lowPassFilter; private final int horizontalPixels; private final int verticalPixels; 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 4374937..049f6eb 100644 --- a/app/src/main/java/xdsopl/robot36/Robot_72_Color.java +++ b/app/src/main/java/xdsopl/robot36/Robot_72_Color.java @@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan package xdsopl.robot36; -public class Robot_72_Color implements Mode { +public class Robot_72_Color extends BaseMode { private final ExponentialMovingAverage lowPassFilter; private final int horizontalPixels; private final int verticalPixels; From 638484ae78e183dcaf96d351de397bcbd4b6975c Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Sat, 16 Aug 2025 22:09:29 +0200 Subject: [PATCH 07/24] Detecting white margin in HF Fax mode and correcting image shift when saving --- .../main/java/xdsopl/robot36/BaseMode.java | 4 ++ app/src/main/java/xdsopl/robot36/HFFax.java | 45 ++++++++++++++----- .../java/xdsopl/robot36/MainActivity.java | 14 ++++++ app/src/main/java/xdsopl/robot36/Mode.java | 5 +++ 4 files changed, 57 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/xdsopl/robot36/BaseMode.java b/app/src/main/java/xdsopl/robot36/BaseMode.java index fcdc7e8..64d7607 100644 --- a/app/src/main/java/xdsopl/robot36/BaseMode.java +++ b/app/src/main/java/xdsopl/robot36/BaseMode.java @@ -1,4 +1,8 @@ package xdsopl.robot36; public abstract class BaseMode implements Mode { + @Override + public int getEstimatedHorizontalShift() { + return 0; + } } diff --git a/app/src/main/java/xdsopl/robot36/HFFax.java b/app/src/main/java/xdsopl/robot36/HFFax.java index 49e0361..a526681 100644 --- a/app/src/main/java/xdsopl/robot36/HFFax.java +++ b/app/src/main/java/xdsopl/robot36/HFFax.java @@ -1,19 +1,21 @@ package xdsopl.robot36; +import android.graphics.Color; + public class HFFax extends BaseMode { private final ExponentialMovingAverage lowPassFilter; - private final int smallPictureMaxSamples; - private final int mediumPictureMaxSamples; private final String name; private final int sr; + 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); lowPassFilter = new ExponentialMovingAverage(); this.sr = sampleRate; + cumulated = new float[getWidth()]; } private float freqToLevel(float frequency, float offset) { @@ -55,6 +57,11 @@ public class HFFax extends BaseMode { return sr / 2; } + @Override + public int getEstimatedHorizontalShift() { + return horizontalShift; + } + @Override public void reset() { } @@ -63,22 +70,38 @@ public class HFFax extends BaseMode { 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; + + cumulated[i] *= 0.99f; //decay old data + cumulated[i] += Color.luminance(color); } + + //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 09b30de..45b5879 100644 --- a/app/src/main/java/xdsopl/robot36/MainActivity.java +++ b/app/src/main/java/xdsopl/robot36/MainActivity.java @@ -15,6 +15,7 @@ import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Bitmap; +import android.graphics.Canvas; import android.graphics.Color; import android.media.AudioFormat; import android.media.AudioRecord; @@ -846,6 +847,19 @@ public class MainActivity extends AppCompatActivity { Bitmap bmp = Bitmap.createBitmap(scopeBuffer.pixels, offset, stride, width, height, Bitmap.Config.ARGB_8888); if (currentMode == null || !currentMode.equals("HF Fax")) { bmp = Bitmap.createScaledBitmap(bmp, width / 3, height / 3, true); + } else { + Mode mode = decoder.currentMode; + int shift = mode.getEstimatedHorizontalShift(); + if (shift > 0) { + Bitmap part1 = Bitmap.createBitmap(bmp, 0, 0, shift, bmp.getHeight()); + Bitmap part2 = Bitmap.createBitmap(bmp, shift, 0, mode.getWidth() - shift, bmp.getHeight()); + + Bitmap bmpMutable = Bitmap.createBitmap(mode.getWidth(), bmp.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new android.graphics.Canvas(bmpMutable); + canvas.drawBitmap(part2, 0, 1, null); + canvas.drawBitmap(part1, mode.getWidth() - shift, 0, null); + bmp = bmpMutable; + } } storeBitmap(bmp); } diff --git a/app/src/main/java/xdsopl/robot36/Mode.java b/app/src/main/java/xdsopl/robot36/Mode.java index 36ae102..1994c4d 100644 --- a/app/src/main/java/xdsopl/robot36/Mode.java +++ b/app/src/main/java/xdsopl/robot36/Mode.java @@ -42,6 +42,11 @@ public interface Mode { */ int getScanLineSamples(); + /** + * @return number of pixels of horizontal shift based on recent data, nonzero for HF Fax + */ + int getEstimatedHorizontalShift(); + /** * Reset internal state. */ From 36eece717b6deb3b01e542cd9dcf5fb019b41ee3 Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Sat, 16 Aug 2025 22:35:36 +0200 Subject: [PATCH 08/24] Small refactoring --- app/src/main/java/xdsopl/robot36/Decoder.java | 6 ++++++ app/src/main/java/xdsopl/robot36/HFFax.java | 11 ++++++----- app/src/main/java/xdsopl/robot36/MainActivity.java | 2 +- app/src/main/java/xdsopl/robot36/Mode.java | 4 ++-- app/src/main/res/menu/menu_main.xml | 8 +++----- app/src/main/res/values/strings.xml | 2 +- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/xdsopl/robot36/Decoder.java b/app/src/main/java/xdsopl/robot36/Decoder.java index 84fa415..93ae520 100644 --- a/app/src/main/java/xdsopl/robot36/Decoder.java +++ b/app/src/main/java/xdsopl/robot36/Decoder.java @@ -354,6 +354,12 @@ public class Decoder { return true; } + /** + * @param freqOffs offsets from expected sync frequency + * @param pulses positions of sync pulses + * @param lines lengths of scan lines + * @param index position of latest sync pulse + */ private boolean processSyncPulse(ArrayList modes, float[] freqOffs, int[] pulses, int[] lines, int index) { for (int i = 1; i < pulses.length; ++i) pulses[i - 1] = pulses[i]; diff --git a/app/src/main/java/xdsopl/robot36/HFFax.java b/app/src/main/java/xdsopl/robot36/HFFax.java index a526681..7b25c1f 100644 --- a/app/src/main/java/xdsopl/robot36/HFFax.java +++ b/app/src/main/java/xdsopl/robot36/HFFax.java @@ -2,19 +2,20 @@ package xdsopl.robot36; import android.graphics.Color; +/** + * HF Fax, IOC 576, 120 lines per minute + */ public class HFFax extends BaseMode { private final ExponentialMovingAverage lowPassFilter; private final String name; - - private final int sr; - + private final int sampleRate; private final float[] cumulated; private int horizontalShift = 0; HFFax(String name, int sampleRate) { this.name = name; lowPassFilter = new ExponentialMovingAverage(); - this.sr = sampleRate; + this.sampleRate = sampleRate; cumulated = new float[getWidth()]; } @@ -54,7 +55,7 @@ public class HFFax extends BaseMode { @Override public int getScanLineSamples() { - return sr / 2; + return sampleRate / 2; } @Override diff --git a/app/src/main/java/xdsopl/robot36/MainActivity.java b/app/src/main/java/xdsopl/robot36/MainActivity.java index 45b5879..7ee7131 100644 --- a/app/src/main/java/xdsopl/robot36/MainActivity.java +++ b/app/src/main/java/xdsopl/robot36/MainActivity.java @@ -631,7 +631,7 @@ public class MainActivity extends AppCompatActivity { return true; } if (id == R.id.action_force_hffax_mode) { - setMode("HF Fax"); + setMode(R.string.hf_fax); return true; } if (id == R.id.action_force_robot36_color) { diff --git a/app/src/main/java/xdsopl/robot36/Mode.java b/app/src/main/java/xdsopl/robot36/Mode.java index 1994c4d..4a07cee 100644 --- a/app/src/main/java/xdsopl/robot36/Mode.java +++ b/app/src/main/java/xdsopl/robot36/Mode.java @@ -33,7 +33,7 @@ public interface Mode { int getBegin(); /** - * @return number of samples from start of scanline to sync pulse??? nonzero for Scottie only? + * @return number of samples from start of first scanline to first sync pulse?, nonzero for Scottie */ int getFirstSyncPulseIndex(); @@ -56,7 +56,7 @@ public interface Mode { * @param pixelBuffer buffer to store decoded pixels * @param scratchBuffer buffer for temporary data * @param scanLineBuffer raw samples to be decoded, can contain more than one scanline - * @param scopeBufferWidth ??? used in RawDecoder, initializes width? + * @param scopeBufferWidth used in RawDecoder, initializes width? * @param syncPulseIndex number of samples from array start to sync pulse * @param scanLineSamples number of samples per scanline * @param frequencyOffset correction of frequency of expected vs actual sync pulse (normalized to range (-1, 1)) diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml index 25e28e5..f6b4cbc 100644 --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_main.xml @@ -97,16 +97,14 @@ android:title="@string/wraase_sc2_180" /> + - Scottie 2 Scottie DX Wraase SC2–180 + HF Fax 8 kHz 16 kHz 32 kHz @@ -44,7 +45,6 @@ Auto Mode Lock Mode Raw Mode - HF Fax Mode Listening Audio Settings Sample Rate From 055d62b6255829f73f8f1f0b5a6ddb58a5edd2d2 Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Sat, 16 Aug 2025 23:51:03 +0200 Subject: [PATCH 09/24] Refactoring --- .../java/xdsopl/robot36/MainActivity.java | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/xdsopl/robot36/MainActivity.java b/app/src/main/java/xdsopl/robot36/MainActivity.java index 7ee7131..0d05ac3 100644 --- a/app/src/main/java/xdsopl/robot36/MainActivity.java +++ b/app/src/main/java/xdsopl/robot36/MainActivity.java @@ -17,6 +17,7 @@ import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Rect; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.MediaRecorder; @@ -845,22 +846,29 @@ public class MainActivity extends AppCompatActivity { int stride = scopeBuffer.width; int offset = stride * scopeBuffer.line; Bitmap bmp = Bitmap.createBitmap(scopeBuffer.pixels, offset, stride, width, height, Bitmap.Config.ARGB_8888); - if (currentMode == null || !currentMode.equals("HF Fax")) { - bmp = Bitmap.createScaledBitmap(bmp, width / 3, height / 3, true); - } else { + if (decoder != null && decoder.currentMode.getName().equals("HF Fax")) { Mode mode = decoder.currentMode; int shift = mode.getEstimatedHorizontalShift(); if (shift > 0) { - Bitmap part1 = Bitmap.createBitmap(bmp, 0, 0, shift, bmp.getHeight()); - Bitmap part2 = Bitmap.createBitmap(bmp, shift, 0, mode.getWidth() - shift, bmp.getHeight()); - Bitmap bmpMutable = Bitmap.createBitmap(mode.getWidth(), bmp.getHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new android.graphics.Canvas(bmpMutable); - canvas.drawBitmap(part2, 0, 1, null); - canvas.drawBitmap(part1, mode.getWidth() - shift, 0, null); + Canvas canvas = new Canvas(bmpMutable); + canvas.drawBitmap( + bmp, + new Rect(0, 0, shift, bmp.getHeight()), + new Rect(mode.getWidth() - shift, 0, mode.getWidth(), bmp.getHeight()), + null); + canvas.drawBitmap( + bmp, + new Rect(shift, 0, mode.getWidth(), bmp.getHeight()), + new Rect(0, 1, mode.getWidth() - shift, bmp.getHeight() + 1), + null); + bmp = bmpMutable; } + } else { + bmp = Bitmap.createScaledBitmap(bmp, width / 3, height / 3, true); } + storeBitmap(bmp); } From 34d34f435c2b2cde586abc7ffd70068607217842 Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Sun, 17 Aug 2025 00:04:06 +0200 Subject: [PATCH 10/24] Refactoring, added postProcessScopeImage --- .../main/java/xdsopl/robot36/BaseMode.java | 7 +++++ app/src/main/java/xdsopl/robot36/HFFax.java | 26 +++++++++++++++++++ .../java/xdsopl/robot36/MainActivity.java | 25 +++--------------- app/src/main/java/xdsopl/robot36/Mode.java | 7 +++++ 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/xdsopl/robot36/BaseMode.java b/app/src/main/java/xdsopl/robot36/BaseMode.java index 64d7607..e18b584 100644 --- a/app/src/main/java/xdsopl/robot36/BaseMode.java +++ b/app/src/main/java/xdsopl/robot36/BaseMode.java @@ -1,8 +1,15 @@ package xdsopl.robot36; +import android.graphics.Bitmap; + public abstract class BaseMode implements Mode { @Override public int getEstimatedHorizontalShift() { return 0; } + + @Override + public Bitmap postProcessScopeImage(Bitmap bmp) { + return Bitmap.createScaledBitmap(bmp, bmp.getWidth() / 3, bmp.getHeight() / 3, true); + } } diff --git a/app/src/main/java/xdsopl/robot36/HFFax.java b/app/src/main/java/xdsopl/robot36/HFFax.java index 7b25c1f..2ee09c7 100644 --- a/app/src/main/java/xdsopl/robot36/HFFax.java +++ b/app/src/main/java/xdsopl/robot36/HFFax.java @@ -1,6 +1,9 @@ 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 @@ -67,6 +70,29 @@ public class HFFax extends BaseMode { public void reset() { } + @Override + public Bitmap postProcessScopeImage(Bitmap bmp) { + int shift = getEstimatedHorizontalShift(); + if (shift > 0) { + Bitmap bmpMutable = Bitmap.createBitmap(getWidth(), bmp.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bmpMutable); + canvas.drawBitmap( + bmp, + new Rect(0, 0, shift, bmp.getHeight()), + new Rect(getWidth() - shift, 0, getWidth(), bmp.getHeight()), + null); + canvas.drawBitmap( + bmp, + new Rect(shift, 0, getWidth(), bmp.getHeight()), + new Rect(0, 1, getWidth() - shift, bmp.getHeight() + 1), + null); + + return bmpMutable; + } + + return bmp; + } + @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) diff --git a/app/src/main/java/xdsopl/robot36/MainActivity.java b/app/src/main/java/xdsopl/robot36/MainActivity.java index 0d05ac3..bd54b1a 100644 --- a/app/src/main/java/xdsopl/robot36/MainActivity.java +++ b/app/src/main/java/xdsopl/robot36/MainActivity.java @@ -15,9 +15,7 @@ import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Bitmap; -import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.Rect; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.MediaRecorder; @@ -846,27 +844,10 @@ public class MainActivity extends AppCompatActivity { int stride = scopeBuffer.width; int offset = stride * scopeBuffer.line; Bitmap bmp = Bitmap.createBitmap(scopeBuffer.pixels, offset, stride, width, height, Bitmap.Config.ARGB_8888); - if (decoder != null && decoder.currentMode.getName().equals("HF Fax")) { - Mode mode = decoder.currentMode; - int shift = mode.getEstimatedHorizontalShift(); - if (shift > 0) { - Bitmap bmpMutable = Bitmap.createBitmap(mode.getWidth(), bmp.getHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bmpMutable); - canvas.drawBitmap( - bmp, - new Rect(0, 0, shift, bmp.getHeight()), - new Rect(mode.getWidth() - shift, 0, mode.getWidth(), bmp.getHeight()), - null); - canvas.drawBitmap( - bmp, - new Rect(shift, 0, mode.getWidth(), bmp.getHeight()), - new Rect(0, 1, mode.getWidth() - shift, bmp.getHeight() + 1), - null); - bmp = bmpMutable; - } - } else { - bmp = Bitmap.createScaledBitmap(bmp, width / 3, height / 3, true); + if (decoder != null) + { + bmp = decoder.currentMode.postProcessScopeImage(bmp); } storeBitmap(bmp); diff --git a/app/src/main/java/xdsopl/robot36/Mode.java b/app/src/main/java/xdsopl/robot36/Mode.java index 4a07cee..39dc64e 100644 --- a/app/src/main/java/xdsopl/robot36/Mode.java +++ b/app/src/main/java/xdsopl/robot36/Mode.java @@ -6,6 +6,8 @@ Copyright 2024 Ahmet Inan package xdsopl.robot36; +import android.graphics.Bitmap; + public interface Mode { /** * @return mode name @@ -47,6 +49,11 @@ public interface Mode { */ int getEstimatedHorizontalShift(); + /** + * Adjust scope image before saving + */ + Bitmap postProcessScopeImage(Bitmap bmp); + /** * Reset internal state. */ From c002d513cb6493f266169e31bb703fe3afa1a665 Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Sun, 17 Aug 2025 00:07:15 +0200 Subject: [PATCH 11/24] Removed unused method --- app/src/main/java/xdsopl/robot36/BaseMode.java | 5 ----- app/src/main/java/xdsopl/robot36/HFFax.java | 16 +++++----------- app/src/main/java/xdsopl/robot36/Mode.java | 5 ----- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/xdsopl/robot36/BaseMode.java b/app/src/main/java/xdsopl/robot36/BaseMode.java index e18b584..534b377 100644 --- a/app/src/main/java/xdsopl/robot36/BaseMode.java +++ b/app/src/main/java/xdsopl/robot36/BaseMode.java @@ -3,11 +3,6 @@ package xdsopl.robot36; import android.graphics.Bitmap; public abstract class BaseMode implements Mode { - @Override - public int getEstimatedHorizontalShift() { - return 0; - } - @Override public Bitmap postProcessScopeImage(Bitmap bmp) { return Bitmap.createScaledBitmap(bmp, bmp.getWidth() / 3, bmp.getHeight() / 3, true); diff --git a/app/src/main/java/xdsopl/robot36/HFFax.java b/app/src/main/java/xdsopl/robot36/HFFax.java index 2ee09c7..1534ad2 100644 --- a/app/src/main/java/xdsopl/robot36/HFFax.java +++ b/app/src/main/java/xdsopl/robot36/HFFax.java @@ -61,30 +61,24 @@ public class HFFax extends BaseMode { return sampleRate / 2; } - @Override - public int getEstimatedHorizontalShift() { - return horizontalShift; - } - @Override public void reset() { } @Override public Bitmap postProcessScopeImage(Bitmap bmp) { - int shift = getEstimatedHorizontalShift(); - if (shift > 0) { + if (horizontalShift > 0) { Bitmap bmpMutable = Bitmap.createBitmap(getWidth(), bmp.getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bmpMutable); canvas.drawBitmap( bmp, - new Rect(0, 0, shift, bmp.getHeight()), - new Rect(getWidth() - shift, 0, getWidth(), bmp.getHeight()), + new Rect(0, 0, horizontalShift, bmp.getHeight()), + new Rect(getWidth() - horizontalShift, 0, getWidth(), bmp.getHeight()), null); canvas.drawBitmap( bmp, - new Rect(shift, 0, getWidth(), bmp.getHeight()), - new Rect(0, 1, getWidth() - shift, bmp.getHeight() + 1), + new Rect(horizontalShift, 0, getWidth(), bmp.getHeight()), + new Rect(0, 1, getWidth() - horizontalShift, bmp.getHeight() + 1), null); return bmpMutable; diff --git a/app/src/main/java/xdsopl/robot36/Mode.java b/app/src/main/java/xdsopl/robot36/Mode.java index 39dc64e..46eae04 100644 --- a/app/src/main/java/xdsopl/robot36/Mode.java +++ b/app/src/main/java/xdsopl/robot36/Mode.java @@ -44,11 +44,6 @@ public interface Mode { */ int getScanLineSamples(); - /** - * @return number of pixels of horizontal shift based on recent data, nonzero for HF Fax - */ - int getEstimatedHorizontalShift(); - /** * Adjust scope image before saving */ From 05763ff3e4c6323ac0a30ef66aa6243a48ced668 Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Sun, 17 Aug 2025 00:09:45 +0200 Subject: [PATCH 12/24] Small refactoring, rename --- app/src/main/java/xdsopl/robot36/Decoder.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/xdsopl/robot36/Decoder.java b/app/src/main/java/xdsopl/robot36/Decoder.java index 93ae520..f66b7af 100644 --- a/app/src/main/java/xdsopl/robot36/Decoder.java +++ b/app/src/main/java/xdsopl/robot36/Decoder.java @@ -38,7 +38,7 @@ public class Decoder { private final int visCodeBitSamples; private final int visCodeSamples; private final Mode rawMode; - private final Mode hffaxMode; + private final Mode hfFaxMode; private final ArrayList syncPulse5msModes; private final ArrayList syncPulse9msModes; private final ArrayList syncPulse20msModes; @@ -96,7 +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); + hfFaxMode = new HFFax("HF Fax", sampleRate); Mode robot36 = new Robot_36_Color(sampleRate); currentMode = robot36; currentScanLineSamples = robot36.getScanLineSamples(); @@ -461,8 +461,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 == null && hfFaxMode.getName().equals(name)) + mode = hfFaxMode; if (mode == currentMode) { lockMode = true; return; From fd3d6fa3a1048418c1a2bfde68ca4478afad038d Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Sun, 17 Aug 2025 00:36:21 +0200 Subject: [PATCH 13/24] Fixed accumulating of image rows --- app/src/main/java/xdsopl/robot36/HFFax.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/xdsopl/robot36/HFFax.java b/app/src/main/java/xdsopl/robot36/HFFax.java index 1534ad2..a418861 100644 --- a/app/src/main/java/xdsopl/robot36/HFFax.java +++ b/app/src/main/java/xdsopl/robot36/HFFax.java @@ -104,8 +104,9 @@ public class HFFax extends BaseMode { int color = ColorConverter.GRAY(scratchBuffer[position]); pixelBuffer.pixels[i] = color; - cumulated[i] *= 0.99f; //decay old data - cumulated[i] += Color.luminance(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 From 02018686b6e5037ae765554a93ebf2bda9ecb03a Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Sun, 17 Aug 2025 17:29:31 +0200 Subject: [PATCH 14/24] Increased audio buffer size to not lose samples during HF Fax saving --- app/src/main/java/xdsopl/robot36/MainActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/xdsopl/robot36/MainActivity.java b/app/src/main/java/xdsopl/robot36/MainActivity.java index bd54b1a..98a4042 100644 --- a/app/src/main/java/xdsopl/robot36/MainActivity.java +++ b/app/src/main/java/xdsopl/robot36/MainActivity.java @@ -319,7 +319,7 @@ public class MainActivity extends AppCompatActivity { int sampleSize = audioFormat == AudioFormat.ENCODING_PCM_FLOAT ? 4 : 2; int frameSize = sampleSize * channelCount; int readsPerSecond = 50; - int bufferSize = Integer.highestOneBit(recordRate) * frameSize; + int bufferSize = Integer.highestOneBit(recordRate) * frameSize * 4; int frameCount = recordRate / readsPerSecond; int bufferCount = frameCount * channelCount; recordBuffer = new float[bufferCount]; From 5aa3be6cc986c4a7bb859cd56dcfef1cd5e07e11 Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Tue, 19 Aug 2025 23:19:26 +0200 Subject: [PATCH 15/24] Fixed indents to tabs --- .../main/java/xdsopl/robot36/BaseMode.java | 8 +- app/src/main/java/xdsopl/robot36/HFFax.java | 202 +++++++++--------- 2 files changed, 105 insertions(+), 105 deletions(-) diff --git a/app/src/main/java/xdsopl/robot36/BaseMode.java b/app/src/main/java/xdsopl/robot36/BaseMode.java index 534b377..481c3c2 100644 --- a/app/src/main/java/xdsopl/robot36/BaseMode.java +++ b/app/src/main/java/xdsopl/robot36/BaseMode.java @@ -3,8 +3,8 @@ package xdsopl.robot36; import android.graphics.Bitmap; public abstract class BaseMode implements Mode { - @Override - public Bitmap postProcessScopeImage(Bitmap bmp) { - return Bitmap.createScaledBitmap(bmp, bmp.getWidth() / 3, bmp.getHeight() / 3, true); - } + @Override + public Bitmap postProcessScopeImage(Bitmap bmp) { + return Bitmap.createScaledBitmap(bmp, bmp.getWidth() / 3, bmp.getHeight() / 3, true); + } } diff --git a/app/src/main/java/xdsopl/robot36/HFFax.java b/app/src/main/java/xdsopl/robot36/HFFax.java index a418861..622fda7 100644 --- a/app/src/main/java/xdsopl/robot36/HFFax.java +++ b/app/src/main/java/xdsopl/robot36/HFFax.java @@ -9,123 +9,123 @@ import android.graphics.Rect; * HF Fax, IOC 576, 120 lines per minute */ public class HFFax extends BaseMode { - private final ExponentialMovingAverage lowPassFilter; - private final String name; - private final int sampleRate; - private final float[] cumulated; - private int horizontalShift = 0; + private final ExponentialMovingAverage lowPassFilter; + private final String name; + private final int sampleRate; + private final float[] cumulated; + private int horizontalShift = 0; - HFFax(String name, int sampleRate) { - this.name = name; - lowPassFilter = new ExponentialMovingAverage(); - this.sampleRate = sampleRate; - cumulated = new float[getWidth()]; - } + HFFax(String name, int sampleRate) { + this.name = name; + lowPassFilter = new ExponentialMovingAverage(); + this.sampleRate = sampleRate; + cumulated = new float[getWidth()]; + } - private float freqToLevel(float frequency, float offset) { - return 0.5f * (frequency - offset + 1.f); - } + private float freqToLevel(float frequency, float offset) { + return 0.5f * (frequency - offset + 1.f); + } - @Override - public String getName() { - return name; - } + @Override + public String getName() { + return name; + } - @Override - public int getCode() { - return -1; - } + @Override + public int getCode() { + return -1; + } - @Override - public int getWidth() { - return 1808; - } + @Override + public int getWidth() { + return 1808; + } - @Override - public int getHeight() { - return 1200; - } + @Override + public int getHeight() { + return 1200; + } - @Override - public int getBegin() { - return 0; - } + @Override + public int getBegin() { + return 0; + } - @Override - public int getFirstSyncPulseIndex() { - return -1; - } + @Override + public int getFirstSyncPulseIndex() { + return -1; + } - @Override - public int getScanLineSamples() { - return sampleRate / 2; - } + @Override + public int getScanLineSamples() { + return sampleRate / 2; + } - @Override - public void reset() { - } + @Override + public void reset() { + } - @Override - public Bitmap postProcessScopeImage(Bitmap bmp) { - if (horizontalShift > 0) { - Bitmap bmpMutable = Bitmap.createBitmap(getWidth(), bmp.getHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bmpMutable); - canvas.drawBitmap( - bmp, - new Rect(0, 0, horizontalShift, bmp.getHeight()), - new Rect(getWidth() - horizontalShift, 0, getWidth(), bmp.getHeight()), - null); - canvas.drawBitmap( - bmp, - new Rect(horizontalShift, 0, getWidth(), bmp.getHeight()), - new Rect(0, 1, getWidth() - horizontalShift, bmp.getHeight() + 1), - null); + @Override + public Bitmap postProcessScopeImage(Bitmap bmp) { + if (horizontalShift > 0) { + Bitmap bmpMutable = Bitmap.createBitmap(getWidth(), bmp.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bmpMutable); + canvas.drawBitmap( + bmp, + new Rect(0, 0, horizontalShift, bmp.getHeight()), + new Rect(getWidth() - horizontalShift, 0, getWidth(), bmp.getHeight()), + null); + canvas.drawBitmap( + bmp, + new Rect(horizontalShift, 0, getWidth(), bmp.getHeight()), + new Rect(0, 1, getWidth() - horizontalShift, bmp.getHeight() + 1), + null); - return bmpMutable; - } + return bmpMutable; + } - return bmp; - } + return bmp; + } - @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 = getWidth(); - lowPassFilter.cutoff(horizontalPixels, 2 * scanLineSamples, 2); - lowPassFilter.reset(); - for (int i = 0; i < scanLineSamples; ++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; - int color = ColorConverter.GRAY(scratchBuffer[position]); - pixelBuffer.pixels[i] = color; + @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 = getWidth(); + lowPassFilter.cutoff(horizontalPixels, 2 * scanLineSamples, 2); + lowPassFilter.reset(); + for (int i = 0; i < scanLineSamples; ++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; + 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); - } + //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; - } - } + //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; + horizontalShift = bestIndex; - pixelBuffer.width = horizontalPixels; - pixelBuffer.height = 1; - return true; - } + pixelBuffer.width = horizontalPixels; + pixelBuffer.height = 1; + return true; + } } From 09d5f6dc5a3bbfe69cca65c96c1990b66edb1c4a Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Thu, 21 Aug 2025 00:02:52 +0200 Subject: [PATCH 16/24] Pull request fixes: moved string --- app/src/main/java/xdsopl/robot36/Decoder.java | 2 +- app/src/main/java/xdsopl/robot36/HFFax.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/xdsopl/robot36/Decoder.java b/app/src/main/java/xdsopl/robot36/Decoder.java index f66b7af..6104bc5 100644 --- a/app/src/main/java/xdsopl/robot36/Decoder.java +++ b/app/src/main/java/xdsopl/robot36/Decoder.java @@ -96,7 +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); + hfFaxMode = new HFFax(sampleRate); Mode robot36 = new Robot_36_Color(sampleRate); currentMode = robot36; currentScanLineSamples = robot36.getScanLineSamples(); diff --git a/app/src/main/java/xdsopl/robot36/HFFax.java b/app/src/main/java/xdsopl/robot36/HFFax.java index 622fda7..ee65069 100644 --- a/app/src/main/java/xdsopl/robot36/HFFax.java +++ b/app/src/main/java/xdsopl/robot36/HFFax.java @@ -15,8 +15,8 @@ public class HFFax extends BaseMode { private final float[] cumulated; private int horizontalShift = 0; - HFFax(String name, int sampleRate) { - this.name = name; + HFFax(int sampleRate) { + this.name = "HF Fax"; lowPassFilter = new ExponentialMovingAverage(); this.sampleRate = sampleRate; cumulated = new float[getWidth()]; From 8173f147d850b818c12c9e3fa97a01431b12f50b Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Thu, 21 Aug 2025 00:04:27 +0200 Subject: [PATCH 17/24] Pull request fixes: Decoder.process() added newLinesPresent --- app/src/main/java/xdsopl/robot36/Decoder.java | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/xdsopl/robot36/Decoder.java b/app/src/main/java/xdsopl/robot36/Decoder.java index 6104bc5..b49ce81 100644 --- a/app/src/main/java/xdsopl/robot36/Decoder.java +++ b/app/src/main/java/xdsopl/robot36/Decoder.java @@ -411,10 +411,8 @@ public class Decoder { return true; } - /** - @return true if new lines present - */ public boolean process(float[] recordBuffer, int channelSelect) { + boolean newLinesPresent = false; boolean syncPulseDetected = demodulator.process(recordBuffer, channelSelect); int syncPulseIndex = currentSample + demodulator.syncPulseOffset; int channels = channelSelect > 0 ? 2 : 1; @@ -428,25 +426,28 @@ public class Decoder { if (syncPulseDetected) { switch (demodulator.syncPulseWidth) { case FiveMilliSeconds: - return processSyncPulse(syncPulse5msModes, last5msFrequencyOffsets, last5msSyncPulses, last5msScanLines, syncPulseIndex); + newLinesPresent = processSyncPulse(syncPulse5msModes, last5msFrequencyOffsets, last5msSyncPulses, last5msScanLines, syncPulseIndex); + break; case NineMilliSeconds: leaderBreakIndex = syncPulseIndex; - return processSyncPulse(syncPulse9msModes, last9msFrequencyOffsets, last9msSyncPulses, last9msScanLines, syncPulseIndex); + newLinesPresent = processSyncPulse(syncPulse9msModes, last9msFrequencyOffsets, last9msSyncPulses, last9msScanLines, syncPulseIndex); + break; case TwentyMilliSeconds: leaderBreakIndex = syncPulseIndex; - return processSyncPulse(syncPulse20msModes, last20msFrequencyOffsets, last20msSyncPulses, last20msScanLines, syncPulseIndex); + newLinesPresent = processSyncPulse(syncPulse20msModes, last20msFrequencyOffsets, last20msSyncPulses, last20msScanLines, syncPulseIndex); + break; default: - return false; + break; } - } - if (handleHeader()) - return true; - if (currentSample > lastSyncPulseIndex + (currentScanLineSamples * 5) / 4) { + } else if (handleHeader()) { + newLinesPresent = true; + } else if (currentSample > lastSyncPulseIndex + (currentScanLineSamples * 5) / 4) { copyLines(currentMode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, lastSyncPulseIndex, currentScanLineSamples, lastFrequencyOffset)); lastSyncPulseIndex += currentScanLineSamples; - return true; + newLinesPresent = true; } - return false; + + return newLinesPresent; } public void setMode(String name) { From 4779d671809a59497a71f0ef9510ae3cc7597a97 Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Thu, 21 Aug 2025 00:34:25 +0200 Subject: [PATCH 18/24] Extracted Demodulator.normalizeFrequency() --- .../main/java/xdsopl/robot36/Demodulator.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/xdsopl/robot36/Demodulator.java b/app/src/main/java/xdsopl/robot36/Demodulator.java index e1621a2..e10f1d7 100644 --- a/app/src/main/java/xdsopl/robot36/Demodulator.java +++ b/app/src/main/java/xdsopl/robot36/Demodulator.java @@ -13,6 +13,8 @@ public class Demodulator { private final SchmittTrigger syncPulseTrigger; private final Phasor baseBandOscillator; private final Delay syncPulseValueDelay; + private final double scanLineBandwidth; + private final double centerFrequency; private final float syncPulseFrequencyValue; private final float syncPulseFrequencyTolerance; private final int syncPulse5msMinSamples; @@ -38,7 +40,7 @@ public class Demodulator { public static final double whiteFrequency = 2300; Demodulator(int sampleRate) { - double scanLineBandwidth = whiteFrequency - blackFrequency; + scanLineBandwidth = whiteFrequency - blackFrequency; frequencyModulation = new FrequencyModulation(scanLineBandwidth, sampleRate); double syncPulse5msSeconds = 0.005; double syncPulse9msSeconds = 0.009; @@ -65,19 +67,23 @@ public class Demodulator { Kaiser kaiser = new Kaiser(); for (int i = 0; i < baseBandLowPass.length; ++i) baseBandLowPass.taps[i] = (float) (kaiser.window(2.0, i, baseBandLowPass.length) * Filter.lowPass(cutoffFrequency, sampleRate, i, baseBandLowPass.length)); - double centerFrequency = (lowestFrequency + highestFrequency) / 2; + centerFrequency = (lowestFrequency + highestFrequency) / 2; baseBandOscillator = new Phasor(-centerFrequency, sampleRate); - syncPulseFrequencyValue = (float) ((syncPulseFrequency - centerFrequency) * 2 / scanLineBandwidth); //converts to range from -1 to 1 + syncPulseFrequencyValue = (float) normalizeFrequency(syncPulseFrequency); syncPulseFrequencyTolerance = (float) (50 * 2 / scanLineBandwidth); double syncPorchFrequency = 1500; double syncHighFrequency = (syncPulseFrequency + syncPorchFrequency) / 2; double syncLowFrequency = (syncPulseFrequency + syncHighFrequency) / 2; - double syncLowValue = (syncLowFrequency - centerFrequency) * 2 / scanLineBandwidth; - double syncHighValue = (syncHighFrequency - centerFrequency) * 2 / scanLineBandwidth; + double syncLowValue = normalizeFrequency(syncLowFrequency); + double syncHighValue = normalizeFrequency(syncHighFrequency); syncPulseTrigger = new SchmittTrigger((float) syncLowValue, (float) syncHighValue); baseBand = new Complex(); } + private double normalizeFrequency(double frequency) { + return (frequency - centerFrequency) * 2 / scanLineBandwidth; + } + /** * @return true if sync pulse detected */ From 4f647e14b4c989dcae8509ecd756ec38ec99afde Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Thu, 21 Aug 2025 01:02:26 +0200 Subject: [PATCH 19/24] Removed some comments and renamed methods in Mode --- app/src/main/java/xdsopl/robot36/Decoder.java | 8 ++-- app/src/main/java/xdsopl/robot36/HFFax.java | 6 +-- app/src/main/java/xdsopl/robot36/Mode.java | 41 ++----------------- app/src/main/java/xdsopl/robot36/PaulDon.java | 6 +-- .../main/java/xdsopl/robot36/RGBDecoder.java | 6 +-- .../main/java/xdsopl/robot36/RawDecoder.java | 6 +-- .../java/xdsopl/robot36/Robot_36_Color.java | 6 +-- .../java/xdsopl/robot36/Robot_72_Color.java | 6 +-- 8 files changed, 26 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/xdsopl/robot36/Decoder.java b/app/src/main/java/xdsopl/robot36/Decoder.java index b49ce81..ccf79cb 100644 --- a/app/src/main/java/xdsopl/robot36/Decoder.java +++ b/app/src/main/java/xdsopl/robot36/Decoder.java @@ -159,7 +159,7 @@ public class Decoder { private Mode findMode(ArrayList modes, int code) { for (Mode mode : modes) - if (mode.getCode() == code) + if (mode.getVISCode() == code) return mode; return null; } @@ -334,7 +334,7 @@ public class Decoder { } if (lockMode && mode != currentMode) return false; - mode.reset(); + mode.resetState(); imageBuffer.width = mode.getWidth(); imageBuffer.height = mode.getHeight(); imageBuffer.line = 0; @@ -348,7 +348,7 @@ public class Decoder { for (int i = 0; i < pulses.length; ++i) pulses[i] = oldestSyncPulseIndex + i * currentScanLineSamples; Arrays.fill(lines, currentScanLineSamples); - shiftSamples(lastSyncPulseIndex + mode.getBegin()); + shiftSamples(lastSyncPulseIndex + mode.getFirstPixelSampleIndex()); drawLines(0xff00ff00, 8); drawLines(0xff000000, 10); return true; @@ -407,7 +407,7 @@ public class Decoder { lastSyncPulseIndex = pulses[pulses.length - 1]; currentScanLineSamples = scanLineSamples; lastFrequencyOffset = frequencyOffset; - shiftSamples(lastSyncPulseIndex + currentMode.getBegin()); + shiftSamples(lastSyncPulseIndex + currentMode.getFirstPixelSampleIndex()); return true; } diff --git a/app/src/main/java/xdsopl/robot36/HFFax.java b/app/src/main/java/xdsopl/robot36/HFFax.java index ee65069..f6f43eb 100644 --- a/app/src/main/java/xdsopl/robot36/HFFax.java +++ b/app/src/main/java/xdsopl/robot36/HFFax.java @@ -32,7 +32,7 @@ public class HFFax extends BaseMode { } @Override - public int getCode() { + public int getVISCode() { return -1; } @@ -47,7 +47,7 @@ public class HFFax extends BaseMode { } @Override - public int getBegin() { + public int getFirstPixelSampleIndex() { return 0; } @@ -62,7 +62,7 @@ public class HFFax extends BaseMode { } @Override - public void reset() { + public void resetState() { } @Override diff --git a/app/src/main/java/xdsopl/robot36/Mode.java b/app/src/main/java/xdsopl/robot36/Mode.java index 46eae04..2650ee5 100644 --- a/app/src/main/java/xdsopl/robot36/Mode.java +++ b/app/src/main/java/xdsopl/robot36/Mode.java @@ -9,59 +9,26 @@ package xdsopl.robot36; import android.graphics.Bitmap; public interface Mode { - /** - * @return mode name - */ String getName(); - /** - * @return VIS code - */ - int getCode(); + int getVISCode(); - /** - * @return image width - */ int getWidth(); - /** - * @return image height - */ int getHeight(); - /** - * @return number of samples from sync pulse to start of image data - */ - int getBegin(); + int getFirstPixelSampleIndex(); - /** - * @return number of samples from start of first scanline to first sync pulse?, nonzero for Scottie - */ int getFirstSyncPulseIndex(); - /** - * @return number of samples in a scanline - */ int getScanLineSamples(); - /** - * Adjust scope image before saving - */ Bitmap postProcessScopeImage(Bitmap bmp); - /** - * Reset internal state. - */ - void reset(); + void resetState(); /** - * @param pixelBuffer buffer to store decoded pixels - * @param scratchBuffer buffer for temporary data - * @param scanLineBuffer raw samples to be decoded, can contain more than one scanline - * @param scopeBufferWidth used in RawDecoder, initializes width? - * @param syncPulseIndex number of samples from array start to sync pulse - * @param scanLineSamples number of samples per scanline - * @param frequencyOffset correction of frequency of expected vs actual sync pulse (normalized to range (-1, 1)) + * @param frequencyOffset normalized correction of frequency (expected vs actual) * @return true if scanline was decoded */ boolean decodeScanLine(PixelBuffer pixelBuffer, float[] scratchBuffer, float[] scanLineBuffer, int scopeBufferWidth, int syncPulseIndex, 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 efc369d..ee6e3cb 100644 --- a/app/src/main/java/xdsopl/robot36/PaulDon.java +++ b/app/src/main/java/xdsopl/robot36/PaulDon.java @@ -56,7 +56,7 @@ public class PaulDon extends BaseMode { } @Override - public int getCode() { + public int getVISCode() { return code; } @@ -71,7 +71,7 @@ public class PaulDon extends BaseMode { } @Override - public int getBegin() { + public int getFirstPixelSampleIndex() { return beginSamples; } @@ -86,7 +86,7 @@ public class PaulDon extends BaseMode { } @Override - public void reset() { + public void resetState() { } @Override diff --git a/app/src/main/java/xdsopl/robot36/RGBDecoder.java b/app/src/main/java/xdsopl/robot36/RGBDecoder.java index 34b4034..fa1ef00 100644 --- a/app/src/main/java/xdsopl/robot36/RGBDecoder.java +++ b/app/src/main/java/xdsopl/robot36/RGBDecoder.java @@ -51,7 +51,7 @@ public class RGBDecoder extends BaseMode { } @Override - public int getCode() { + public int getVISCode() { return code; } @@ -66,7 +66,7 @@ public class RGBDecoder extends BaseMode { } @Override - public int getBegin() { + public int getFirstPixelSampleIndex() { return beginSamples; } @@ -81,7 +81,7 @@ public class RGBDecoder extends BaseMode { } @Override - public void reset() { + public void resetState() { } @Override diff --git a/app/src/main/java/xdsopl/robot36/RawDecoder.java b/app/src/main/java/xdsopl/robot36/RawDecoder.java index 3dffa17..931144d 100644 --- a/app/src/main/java/xdsopl/robot36/RawDecoder.java +++ b/app/src/main/java/xdsopl/robot36/RawDecoder.java @@ -29,7 +29,7 @@ public class RawDecoder extends BaseMode { } @Override - public int getCode() { + public int getVISCode() { return -1; } @@ -44,7 +44,7 @@ public class RawDecoder extends BaseMode { } @Override - public int getBegin() { + public int getFirstPixelSampleIndex() { return 0; } @@ -59,7 +59,7 @@ public class RawDecoder extends BaseMode { } @Override - public void reset() { + public void resetState() { } @Override 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 94e22b3..baf448e 100644 --- a/app/src/main/java/xdsopl/robot36/Robot_36_Color.java +++ b/app/src/main/java/xdsopl/robot36/Robot_36_Color.java @@ -59,7 +59,7 @@ public class Robot_36_Color extends BaseMode { } @Override - public int getCode() { + public int getVISCode() { return 8; } @@ -74,7 +74,7 @@ public class Robot_36_Color extends BaseMode { } @Override - public int getBegin() { + public int getFirstPixelSampleIndex() { return beginSamples; } @@ -89,7 +89,7 @@ public class Robot_36_Color extends BaseMode { } @Override - public void reset() { + public void resetState() { lastEven = false; } 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 049f6eb..00414fd 100644 --- a/app/src/main/java/xdsopl/robot36/Robot_72_Color.java +++ b/app/src/main/java/xdsopl/robot36/Robot_72_Color.java @@ -63,7 +63,7 @@ public class Robot_72_Color extends BaseMode { } @Override - public int getCode() { + public int getVISCode() { return 12; } @@ -78,7 +78,7 @@ public class Robot_72_Color extends BaseMode { } @Override - public int getBegin() { + public int getFirstPixelSampleIndex() { return beginSamples; } @@ -93,7 +93,7 @@ public class Robot_72_Color extends BaseMode { } @Override - public void reset() { + public void resetState() { } @Override From df16e3c318dc395dd562b69333cfc7f4514b75ae Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Thu, 21 Aug 2025 01:06:59 +0200 Subject: [PATCH 20/24] Removed comment --- app/src/main/java/xdsopl/robot36/Robot_72_Color.java | 6 ------ 1 file changed, 6 deletions(-) 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 00414fd..f1bf907 100644 --- a/app/src/main/java/xdsopl/robot36/Robot_72_Color.java +++ b/app/src/main/java/xdsopl/robot36/Robot_72_Color.java @@ -47,12 +47,6 @@ public class Robot_72_Color extends BaseMode { lowPassFilter = new ExponentialMovingAverage(); } - /** - * FIXME same in other modes (copy&paste) - * @param frequency frequency, range (-1,1) - * @param offset correction, range (-1,1) - * @return pixel value, range (0,1) - */ private float freqToLevel(float frequency, float offset) { return 0.5f * (frequency - offset + 1.f); } From 1ed9f9094cba06c30b9c1d47fe0756dffc75ec62 Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Thu, 21 Aug 2025 01:19:56 +0200 Subject: [PATCH 21/24] Restored buffer sizes --- app/src/main/java/xdsopl/robot36/BaseMode.java | 2 +- app/src/main/java/xdsopl/robot36/MainActivity.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/xdsopl/robot36/BaseMode.java b/app/src/main/java/xdsopl/robot36/BaseMode.java index 481c3c2..96c9827 100644 --- a/app/src/main/java/xdsopl/robot36/BaseMode.java +++ b/app/src/main/java/xdsopl/robot36/BaseMode.java @@ -5,6 +5,6 @@ import android.graphics.Bitmap; public abstract class BaseMode implements Mode { @Override public Bitmap postProcessScopeImage(Bitmap bmp) { - return Bitmap.createScaledBitmap(bmp, bmp.getWidth() / 3, bmp.getHeight() / 3, true); + return bmp; } } diff --git a/app/src/main/java/xdsopl/robot36/MainActivity.java b/app/src/main/java/xdsopl/robot36/MainActivity.java index 98a4042..520c166 100644 --- a/app/src/main/java/xdsopl/robot36/MainActivity.java +++ b/app/src/main/java/xdsopl/robot36/MainActivity.java @@ -319,7 +319,7 @@ public class MainActivity extends AppCompatActivity { int sampleSize = audioFormat == AudioFormat.ENCODING_PCM_FLOAT ? 4 : 2; int frameSize = sampleSize * channelCount; int readsPerSecond = 50; - int bufferSize = Integer.highestOneBit(recordRate) * frameSize * 4; + int bufferSize = Integer.highestOneBit(recordRate) * frameSize; int frameCount = recordRate / readsPerSecond; int bufferCount = frameCount * channelCount; recordBuffer = new float[bufferCount]; @@ -568,7 +568,7 @@ public class MainActivity extends AppCompatActivity { fgColor = getColor(R.color.fg); thinColor = getColor(R.color.thin); tintColor = getColor(R.color.tint); - scopeBuffer = new PixelBuffer(640 * 3, 2 * 1280 * 3); + scopeBuffer = new PixelBuffer(640, 2 * 1280); waterfallPlotBuffer = new PixelBuffer(256, 2 * 256); peakMeterBuffer = new PixelBuffer(1, 16); imageBuffer = new PixelBuffer(800, 616); From a54b3f7fad0970b8cfd601e4b1324890e3fce397 Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Thu, 21 Aug 2025 22:57:45 +0200 Subject: [PATCH 22/24] Storing 640 pixel wide bitmap and stretching later --- app/src/main/java/xdsopl/robot36/HFFax.java | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/xdsopl/robot36/HFFax.java b/app/src/main/java/xdsopl/robot36/HFFax.java index f6f43eb..30de43a 100644 --- a/app/src/main/java/xdsopl/robot36/HFFax.java +++ b/app/src/main/java/xdsopl/robot36/HFFax.java @@ -38,7 +38,7 @@ public class HFFax extends BaseMode { @Override public int getWidth() { - return 1808; + return 640; } @Override @@ -67,24 +67,24 @@ public class HFFax extends BaseMode { @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) { - Bitmap bmpMutable = Bitmap.createBitmap(getWidth(), bmp.getHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bmpMutable); canvas.drawBitmap( bmp, new Rect(0, 0, horizontalShift, bmp.getHeight()), - new Rect(getWidth() - horizontalShift, 0, getWidth(), 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, getWidth() - horizontalShift, bmp.getHeight() + 1), - null); - - return bmpMutable; } + canvas.drawBitmap( + bmp, + new Rect(horizontalShift, 0, getWidth(), bmp.getHeight()), + new Rect(0, 1, realWidth - realHorizontalShift, bmp.getHeight() + 1), + null); - return bmp; + return bmpMutable; } @Override From f745a6bddcaa64e392a0493ad99702f6909d71d9 Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Fri, 22 Aug 2025 00:35:30 +0200 Subject: [PATCH 23/24] This buffer can also be reverted now --- app/src/main/java/xdsopl/robot36/Decoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/xdsopl/robot36/Decoder.java b/app/src/main/java/xdsopl/robot36/Decoder.java index ccf79cb..4ffbe0c 100644 --- a/app/src/main/java/xdsopl/robot36/Decoder.java +++ b/app/src/main/java/xdsopl/robot36/Decoder.java @@ -55,7 +55,7 @@ public class Decoder { this.scopeBuffer = scopeBuffer; this.imageBuffer = imageBuffer; imageBuffer.line = -1; - pixelBuffer = new PixelBuffer(2000, 2); + pixelBuffer = new PixelBuffer(800, 2); demodulator = new Demodulator(sampleRate); double pulseFilterSeconds = 0.0025; int pulseFilterSamples = (int) Math.round(pulseFilterSeconds * sampleRate) | 1; From 46f376e10153597906b4fe79adce5ed894ba5750 Mon Sep 17 00:00:00 2001 From: Marek Ossowski Date: Fri, 22 Aug 2025 00:45:58 +0200 Subject: [PATCH 24/24] Replaced more comments with renames --- app/src/main/java/xdsopl/robot36/Decoder.java | 40 ++++++++----------- .../main/java/xdsopl/robot36/Demodulator.java | 3 -- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/xdsopl/robot36/Decoder.java b/app/src/main/java/xdsopl/robot36/Decoder.java index 4ffbe0c..8c62a05 100644 --- a/app/src/main/java/xdsopl/robot36/Decoder.java +++ b/app/src/main/java/xdsopl/robot36/Decoder.java @@ -354,29 +354,23 @@ public class Decoder { return true; } - /** - * @param freqOffs offsets from expected sync frequency - * @param pulses positions of sync pulses - * @param lines lengths of scan lines - * @param index position of latest sync pulse - */ - private boolean processSyncPulse(ArrayList modes, float[] freqOffs, int[] pulses, int[] lines, int index) { - for (int i = 1; i < pulses.length; ++i) - pulses[i - 1] = pulses[i]; - pulses[pulses.length - 1] = index; - for (int i = 1; i < lines.length; ++i) - lines[i - 1] = lines[i]; - lines[lines.length - 1] = pulses[pulses.length - 1] - pulses[pulses.length - 2]; + private boolean processSyncPulse(ArrayList modes, float[] freqOffs, int[] syncIndexes, int[] lineLengths, int latestSyncIndex) { + for (int i = 1; i < syncIndexes.length; ++i) + syncIndexes[i - 1] = syncIndexes[i]; + syncIndexes[syncIndexes.length - 1] = latestSyncIndex; + for (int i = 1; i < lineLengths.length; ++i) + lineLengths[i - 1] = lineLengths[i]; + lineLengths[lineLengths.length - 1] = syncIndexes[syncIndexes.length - 1] - syncIndexes[syncIndexes.length - 2]; for (int i = 1; i < freqOffs.length; ++i) freqOffs[i - 1] = freqOffs[i]; - freqOffs[pulses.length - 1] = demodulator.frequencyOffset; - if (lines[0] == 0) + freqOffs[syncIndexes.length - 1] = demodulator.frequencyOffset; + if (lineLengths[0] == 0) return false; - double mean = scanLineMean(lines); + double mean = scanLineMean(lineLengths); int scanLineSamples = (int) Math.round(mean); if (scanLineSamples < scanLineMinSamples || scanLineSamples > scratchBuffer.length) return false; - if (scanLineStdDev(lines, mean) > scanLineToleranceSamples) + if (scanLineStdDev(lineLengths, mean) > scanLineToleranceSamples) return false; boolean pictureChanged = false; if (lockMode || imageBuffer.line >= 0 && imageBuffer.line < imageBuffer.height) { @@ -387,7 +381,7 @@ public class Decoder { currentMode = detectMode(modes, scanLineSamples); pictureChanged = currentMode != prevMode || Math.abs(currentScanLineSamples - scanLineSamples) > scanLineToleranceSamples - || Math.abs(lastSyncPulseIndex + scanLineSamples - pulses[pulses.length - 1]) > syncPulseToleranceSamples; + || Math.abs(lastSyncPulseIndex + scanLineSamples - syncIndexes[syncIndexes.length - 1]) > syncPulseToleranceSamples; } if (pictureChanged) { drawLines(0xff000000, 10); @@ -395,16 +389,16 @@ public class Decoder { drawLines(0xff000000, 10); } float frequencyOffset = (float) frequencyOffsetMean(freqOffs); - if (pulses[0] >= scanLineSamples && pictureChanged) { - int endPulse = pulses[0]; + if (syncIndexes[0] >= scanLineSamples && pictureChanged) { + int endPulse = syncIndexes[0]; int extrapolate = endPulse / scanLineSamples; int firstPulse = endPulse - extrapolate * scanLineSamples; for (int pulseIndex = firstPulse; pulseIndex < endPulse; pulseIndex += scanLineSamples) copyLines(currentMode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, pulseIndex, scanLineSamples, frequencyOffset)); } - for (int i = pictureChanged ? 0 : lines.length - 1; i < lines.length; ++i) - copyLines(currentMode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, pulses[i], lines[i], frequencyOffset)); - lastSyncPulseIndex = pulses[pulses.length - 1]; + for (int i = pictureChanged ? 0 : lineLengths.length - 1; i < lineLengths.length; ++i) + copyLines(currentMode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, syncIndexes[i], lineLengths[i], frequencyOffset)); + lastSyncPulseIndex = syncIndexes[syncIndexes.length - 1]; currentScanLineSamples = scanLineSamples; lastFrequencyOffset = frequencyOffset; shiftSamples(lastSyncPulseIndex + currentMode.getFirstPixelSampleIndex()); diff --git a/app/src/main/java/xdsopl/robot36/Demodulator.java b/app/src/main/java/xdsopl/robot36/Demodulator.java index e10f1d7..b499dbf 100644 --- a/app/src/main/java/xdsopl/robot36/Demodulator.java +++ b/app/src/main/java/xdsopl/robot36/Demodulator.java @@ -84,9 +84,6 @@ public class Demodulator { return (frequency - centerFrequency) * 2 / scanLineBandwidth; } - /** - * @return true if sync pulse detected - */ public boolean process(float[] buffer, int channelSelect) { boolean syncPulseDetected = false; int channels = channelSelect > 0 ? 2 : 1;