Added HF Fax mode

This commit is contained in:
Marek Ossowski 2025-08-22 22:27:08 +02:00
parent 5fee5e0672
commit 03dc577ae8
5 changed files with 78 additions and 18 deletions

View file

@ -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<Mode> syncPulse5msModes;
private final ArrayList<Mode> syncPulse9msModes;
private final ArrayList<Mode> 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;

View file

@ -1,22 +1,31 @@
/*
Raw decoder
HF Fax mode
Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
Copyright 2025 Marek Ossowski <marek0ossowski@gmail.com>
*/
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;

View file

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

View file

@ -97,6 +97,9 @@
android:title="@string/wraase_sc2_180" />
</menu>
</item>
<item
android:id="@+id/action_force_hffax_mode"
android:title="@string/hf_fax" />
<item
android:id="@+id/action_force_raw_mode"
android:icon="@drawable/baseline_image_not_supported_24"

View file

@ -21,6 +21,7 @@
<string name="scottie2" translatable="false">Scottie 2</string>
<string name="scottie_dx" translatable="false">Scottie DX</string>
<string name="wraase_sc2_180" translatable="false">Wraase SC2180</string>
<string name="hf_fax" translatable="false">HF Fax</string>
<string name="rate_8000" translatable="false">8 kHz</string>
<string name="rate_16000" translatable="false">16 kHz</string>
<string name="rate_32000" translatable="false">32 kHz</string>