Compare commits

..

No commits in common. "v2" and "v2.7" have entirely different histories.
v2 ... v2.7

42 changed files with 346 additions and 1997 deletions

View file

@ -1,46 +1,9 @@
# Robot36 - SSTV Image Decoder
### Robot36: The Java Cut
## Audio Line-Level to Microphone-Level Converter
Decoding SSTV signals is more reliable with a clean input. Using a direct cable connection instead of acoustic coupling avoids echo, distortion, and environmental noise.
This is not a drill!
Most smartphones use TRRS connectors for headsets. In these connectors, the sleeve and the second ring (next to the sleeve) serve dual roles: depending on the standard, one is the microphone input and the other is ground. The tip and first ring carry the left and right audio channels.
Get ready to beam in a whole new Robot36, rebuilt from the ground up in pure Java! Just like that nostalgic reboot of your favorite childhood show (but hopefully better written!), things might be a little different this time around. There will be glitches, there will be bugs, but hey, that's the beauty of live transmission, right? Hold on to your spacesuits, folks, it's gonna be a bumpy ride!
When a TRS plug is inserted into a TRRS jack, the sleeve and second ring are shorted together. This allows regular stereo headphones (without a microphone) to work correctly.
Stay tuned for further transmissions...
Instead of determining which pin is MIC or GND for each device, galvanic isolation can be used. This avoids compatibility issues, eliminates ground loops, protects against damage, and improves robustness.
Using a line-level output (e.g., from a radio or sound card) as a microphone input introduces several challenges:
* Line-level signals swing around 1V, while electret microphones produce signals in the millivolt range, so attenuation is needed.
* Electret microphones are biased via the TRRS connector, allowing their internal amplifiers to function. This bias must be blocked to avoid distortion.
* To make the smartphone recognize the input as a microphone, a resistor must be placed between the second ring and sleeve.
To reduce power consumption on the line-out device, a higher impedance can be achieved by inserting a resistor in series with the primary winding of a 1:1 audio transformer. If the source cannot drive high impedance, the primary can be connected directly, and attenuation applied on the secondary side. This increases power consumption and may heat the transformer.
Because the electret mic input is high-impedance and AC-coupled, the transformers secondary can resonate if left unterminated. Adding a resistor across the secondary dampens this resonance and flattens the frequency response. A value equal to the transformers impedance is typical, but a lower value can be used to both improve damping and provide additional attenuation. The ratio of the series resistor on the primary to the parallel resistor on the secondary determines the overall attenuation.
### Example Values
* Transformer: 1:1 audio transformer, 600Ω impedance, 140Ω DC resistance
* Primary side: 2.2kΩ resistor in series (any value between 1kΩ and 10kΩ is fine; a 10kΩ potentiometer allows adjustment)
* Secondary side: 100Ω resistor across the winding for damping and attenuation
* DC blocking capacitor: 2.2µF film capacitor (anything between 1µF and 100µF works; avoid values below 1µF to keep low-frequency SSTV content intact)
* Microphone sensing resistor: 2.2kΩ between the second ring and sleeve (values near 2kΩ are fine)
### Schematic
```
[Line IN] O---[R1]---+||+---+---|C1|---+---O [Ring 2]
S||S | |
T1: S||S [R2] [R3]
S||S | |
[Line GND] O----------+||+---+----------+---O [Sleeve]
```
Explanation of Symbols:
* [R1]: 2.2kΩ series resistor (primary side attenuation)
* T1: 1:1 audio transformer
* [C1]: 2.2µF capacitor (DC blocking)
* [R2]: 100Ω damping resistor (across secondary)
* [R3]: 2.2kΩ MIC detect resistor (between MIC and GND)
* [Line IN], [Line GND]: input from radio/sound card
* [Ring 2], [Sleeve]: TRRS plug connections to smartphone

View file

@ -4,22 +4,18 @@ plugins {
android {
namespace 'xdsopl.robot36'
compileSdk = 36
compileSdk 34
defaultConfig {
applicationId "xdsopl.robot36"
minSdk 24
targetSdk 36
versionCode 66
versionName "2.16"
targetSdk 34
versionCode 57
versionName "2.7"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
androidResources {
generateLocaleConfig true
}
buildFeatures {
buildConfig true
}

View file

@ -1,16 +0,0 @@
/*
Base class for all modes
Copyright 2025 Marek Ossowski <marek0ossowski@gmail.com>
*/
package xdsopl.robot36;
import android.graphics.Bitmap;
public abstract class BaseMode implements Mode {
@Override
public Bitmap postProcessScopeImage(Bitmap bmp) {
return bmp;
}
}

View file

@ -38,7 +38,6 @@ 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;
@ -51,11 +50,11 @@ public class Decoder {
private int currentScanLineSamples;
private float lastFrequencyOffset;
Decoder(PixelBuffer scopeBuffer, PixelBuffer imageBuffer, String rawName, int sampleRate) {
Decoder(PixelBuffer scopeBuffer, PixelBuffer imageBuffer, int sampleRate) {
this.scopeBuffer = scopeBuffer;
this.imageBuffer = imageBuffer;
imageBuffer.line = -1;
pixelBuffer = new PixelBuffer(800, 2);
pixelBuffer = new PixelBuffer(scopeBuffer.width, 2);
demodulator = new Demodulator(sampleRate);
double pulseFilterSeconds = 0.0025;
int pulseFilterSamples = (int) Math.round(pulseFilterSeconds * sampleRate) | 1;
@ -95,8 +94,7 @@ public class Decoder {
syncPulseToleranceSamples = (int) Math.round(syncPulseToleranceSeconds * sampleRate);
double scanLineToleranceSeconds = 0.001;
scanLineToleranceSamples = (int) Math.round(scanLineToleranceSeconds * sampleRate);
rawMode = new RawDecoder(rawName, sampleRate);
hfFaxMode = new HFFax(sampleRate);
rawMode = new RawDecoder(sampleRate);
Mode robot36 = new Robot_36_Color(sampleRate);
currentMode = robot36;
currentScanLineSamples = robot36.getScanLineSamples();
@ -117,7 +115,6 @@ public class Decoder {
syncPulse20msModes.add(new PaulDon("160", 98, 512, 400, 0.195584, sampleRate));
syncPulse20msModes.add(new PaulDon("180", 96, 640, 496, 0.18304, sampleRate));
syncPulse20msModes.add(new PaulDon("240", 97, 640, 496, 0.24448, sampleRate));
syncPulse20msModes.add(new PaulDon("290", 94, 800, 616, 0.2288, sampleRate));
}
private double scanLineMean(int[] lines) {
@ -159,7 +156,7 @@ public class Decoder {
private Mode findMode(ArrayList<Mode> modes, int code) {
for (Mode mode : modes)
if (mode.getVISCode() == code)
if (mode.getCode() == code)
return mode;
return null;
}
@ -172,11 +169,10 @@ public class Decoder {
}
private void copyUnscaled() {
int width = Math.min(scopeBuffer.width, pixelBuffer.width);
for (int row = 0; row < pixelBuffer.height; ++row) {
int line = scopeBuffer.width * scopeBuffer.line;
System.arraycopy(pixelBuffer.pixels, row * pixelBuffer.width, scopeBuffer.pixels, line, width);
Arrays.fill(scopeBuffer.pixels, line + width, line + scopeBuffer.width, 0);
System.arraycopy(pixelBuffer.pixels, row * pixelBuffer.width, scopeBuffer.pixels, line, pixelBuffer.width);
Arrays.fill(scopeBuffer.pixels, line + pixelBuffer.width, line + scopeBuffer.width, 0);
System.arraycopy(scopeBuffer.pixels, line, scopeBuffer.pixels, scopeBuffer.width * (scopeBuffer.line + scopeBuffer.height / 2), scopeBuffer.width);
scopeBuffer.line = (scopeBuffer.line + 1) % (scopeBuffer.height / 2);
}
@ -210,7 +206,7 @@ public class Decoder {
finish = imageBuffer.line == imageBuffer.height;
}
int scale = scopeBuffer.width / pixelBuffer.width;
if (scale <= 1)
if (scale == 1)
copyUnscaled();
else
copyScaled(scale);
@ -334,7 +330,7 @@ public class Decoder {
}
if (lockMode && mode != currentMode)
return false;
mode.resetState();
mode.reset();
imageBuffer.width = mode.getWidth();
imageBuffer.height = mode.getHeight();
imageBuffer.line = 0;
@ -348,29 +344,29 @@ public class Decoder {
for (int i = 0; i < pulses.length; ++i)
pulses[i] = oldestSyncPulseIndex + i * currentScanLineSamples;
Arrays.fill(lines, currentScanLineSamples);
shiftSamples(lastSyncPulseIndex + mode.getFirstPixelSampleIndex());
shiftSamples(lastSyncPulseIndex + mode.getBegin());
drawLines(0xff00ff00, 8);
drawLines(0xff000000, 10);
return true;
}
private boolean processSyncPulse(ArrayList<Mode> 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];
private boolean processSyncPulse(ArrayList<Mode> 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];
for (int i = 1; i < freqOffs.length; ++i)
freqOffs[i - 1] = freqOffs[i];
freqOffs[syncIndexes.length - 1] = demodulator.frequencyOffset;
if (lineLengths[0] == 0)
freqOffs[pulses.length - 1] = demodulator.frequencyOffset;
if (lines[0] == 0)
return false;
double mean = scanLineMean(lineLengths);
double mean = scanLineMean(lines);
int scanLineSamples = (int) Math.round(mean);
if (scanLineSamples < scanLineMinSamples || scanLineSamples > scratchBuffer.length)
return false;
if (scanLineStdDev(lineLengths, mean) > scanLineToleranceSamples)
if (scanLineStdDev(lines, mean) > scanLineToleranceSamples)
return false;
boolean pictureChanged = false;
if (lockMode || imageBuffer.line >= 0 && imageBuffer.line < imageBuffer.height) {
@ -381,7 +377,7 @@ public class Decoder {
currentMode = detectMode(modes, scanLineSamples);
pictureChanged = currentMode != prevMode
|| Math.abs(currentScanLineSamples - scanLineSamples) > scanLineToleranceSamples
|| Math.abs(lastSyncPulseIndex + scanLineSamples - syncIndexes[syncIndexes.length - 1]) > syncPulseToleranceSamples;
|| Math.abs(lastSyncPulseIndex + scanLineSamples - pulses[pulses.length - 1]) > syncPulseToleranceSamples;
}
if (pictureChanged) {
drawLines(0xff000000, 10);
@ -389,24 +385,23 @@ public class Decoder {
drawLines(0xff000000, 10);
}
float frequencyOffset = (float) frequencyOffsetMean(freqOffs);
if (syncIndexes[0] >= scanLineSamples && pictureChanged) {
int endPulse = syncIndexes[0];
if (pulses[0] >= scanLineSamples && pictureChanged) {
int endPulse = pulses[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 : 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];
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];
currentScanLineSamples = scanLineSamples;
lastFrequencyOffset = frequencyOffset;
shiftSamples(lastSyncPulseIndex + currentMode.getFirstPixelSampleIndex());
shiftSamples(lastSyncPulseIndex + currentMode.getBegin());
return true;
}
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;
@ -420,31 +415,28 @@ public class Decoder {
if (syncPulseDetected) {
switch (demodulator.syncPulseWidth) {
case FiveMilliSeconds:
newLinesPresent = processSyncPulse(syncPulse5msModes, last5msFrequencyOffsets, last5msSyncPulses, last5msScanLines, syncPulseIndex);
break;
return processSyncPulse(syncPulse5msModes, last5msFrequencyOffsets, last5msSyncPulses, last5msScanLines, syncPulseIndex);
case NineMilliSeconds:
leaderBreakIndex = syncPulseIndex;
newLinesPresent = processSyncPulse(syncPulse9msModes, last9msFrequencyOffsets, last9msSyncPulses, last9msScanLines, syncPulseIndex);
break;
return processSyncPulse(syncPulse9msModes, last9msFrequencyOffsets, last9msSyncPulses, last9msScanLines, syncPulseIndex);
case TwentyMilliSeconds:
leaderBreakIndex = syncPulseIndex;
newLinesPresent = processSyncPulse(syncPulse20msModes, last20msFrequencyOffsets, last20msSyncPulses, last20msScanLines, syncPulseIndex);
break;
return processSyncPulse(syncPulse20msModes, last20msFrequencyOffsets, last20msSyncPulses, last20msScanLines, syncPulseIndex);
default:
break;
return false;
}
} else if (handleHeader()) {
newLinesPresent = true;
} else if (currentSample > lastSyncPulseIndex + (currentScanLineSamples * 5) / 4) {
}
if (handleHeader())
return true;
if (currentSample > lastSyncPulseIndex + (currentScanLineSamples * 5) / 4) {
copyLines(currentMode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, lastSyncPulseIndex, currentScanLineSamples, lastFrequencyOffset));
lastSyncPulseIndex += currentScanLineSamples;
newLinesPresent = true;
return true;
}
return newLinesPresent;
return false;
}
public void setMode(String name) {
public void forceMode(String name) {
if (rawMode.getName().equals(name)) {
lockMode = true;
imageBuffer.line = -1;
@ -456,8 +448,6 @@ 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

@ -13,8 +13,6 @@ 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;
@ -35,12 +33,10 @@ 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) {
scanLineBandwidth = whiteFrequency - blackFrequency;
double blackFrequency = 1500;
double whiteFrequency = 2300;
double scanLineBandwidth = whiteFrequency - blackFrequency;
frequencyModulation = new FrequencyModulation(scanLineBandwidth, sampleRate);
double syncPulse5msSeconds = 0.005;
double syncPulse9msSeconds = 0.009;
@ -67,23 +63,20 @@ 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));
centerFrequency = (lowestFrequency + highestFrequency) / 2;
double centerFrequency = (lowestFrequency + highestFrequency) / 2;
baseBandOscillator = new Phasor(-centerFrequency, sampleRate);
syncPulseFrequencyValue = (float) normalizeFrequency(syncPulseFrequency);
double syncPulseFrequency = 1200;
syncPulseFrequencyValue = (float) ((syncPulseFrequency - centerFrequency) * 2 / scanLineBandwidth);
syncPulseFrequencyTolerance = (float) (50 * 2 / scanLineBandwidth);
double syncPorchFrequency = 1500;
double syncHighFrequency = (syncPulseFrequency + syncPorchFrequency) / 2;
double syncLowFrequency = (syncPulseFrequency + syncHighFrequency) / 2;
double syncLowValue = normalizeFrequency(syncLowFrequency);
double syncHighValue = normalizeFrequency(syncHighFrequency);
double syncLowValue = (syncLowFrequency - centerFrequency) * 2 / scanLineBandwidth;
double syncHighValue = (syncHighFrequency - centerFrequency) * 2 / scanLineBandwidth;
syncPulseTrigger = new SchmittTrigger((float) syncLowValue, (float) syncHighValue);
baseBand = new Complex();
}
private double normalizeFrequency(double frequency) {
return (frequency - centerFrequency) * 2 / scanLineBandwidth;
}
public boolean process(float[] buffer, int channelSelect) {
boolean syncPulseDetected = false;
int channels = channelSelect > 0 ? 2 : 1;

View file

@ -1,353 +0,0 @@
/*
Fast Fourier Transform
Copyright 2025 Ahmet Inan <xdsopl@gmail.com>
*/
package xdsopl.robot36;
public class FastFourierTransform {
private final Complex[] tf;
private final Complex tmpA, tmpB, tmpC, tmpD, tmpE, tmpF, tmpG, tmpH, tmpI, tmpJ, tmpK, tmpL, tmpM;
private final Complex tin0, tin1, tin2, tin3, tin4, tin5, tin6;
FastFourierTransform(int length) {
int rest = length;
while (rest > 1) {
if (rest % 2 == 0)
rest /= 2;
else if (rest % 3 == 0)
rest /= 3;
else if (rest % 5 == 0)
rest /= 5;
else if (rest % 7 == 0)
rest /= 7;
else
break;
}
if (rest != 1)
throw new IllegalArgumentException(
"Transform length must be a composite of 2, 3, 5 and 7, but was: "
+ length);
tf = new Complex[length];
for (int i = 0; i < length; ++i) {
double x = -(2.0 * Math.PI * i) / length;
float a = (float) Math.cos(x);
float b = (float) Math.sin(x);
tf[i] = new Complex(a, b);
}
tmpA = new Complex();
tmpB = new Complex();
tmpC = new Complex();
tmpD = new Complex();
tmpE = new Complex();
tmpF = new Complex();
tmpG = new Complex();
tmpH = new Complex();
tmpI = new Complex();
tmpJ = new Complex();
tmpK = new Complex();
tmpL = new Complex();
tmpM = new Complex();
tin0 = new Complex();
tin1 = new Complex();
tin2 = new Complex();
tin3 = new Complex();
tin4 = new Complex();
tin5 = new Complex();
tin6 = new Complex();
}
private boolean isPowerOfTwo(int n) {
return n > 0 && (n & (n - 1)) == 0;
}
private boolean isPowerOfFour(int n) {
return isPowerOfTwo(n) && (n & 0x55555555) != 0;
}
private float cos(int n, int N) {
return (float) Math.cos(n * 2.0 * Math.PI / N);
}
private float sin(int n, int N) {
return (float) Math.sin(n * 2.0 * Math.PI / N);
}
private void dft2(Complex out0, Complex out1, Complex in0, Complex in1) {
out0.set(in0).add(in1);
out1.set(in0).sub(in1);
}
private void radix2(Complex[] out, Complex[] in, int O, int I, int N, int S, boolean F) {
if (N == 2) {
dft2(out[O], out[O + 1], in[I], in[I + S]);
} else {
int Q = N / 2;
dit(out, in, O, I, Q, 2 * S, F);
dit(out, in, O + Q, I + S, Q, 2 * S, F);
for (int k0 = O, k1 = O + Q, l1 = 0; k0 < O + Q; ++k0, ++k1, l1 += S) {
tin1.set(tf[l1]);
if (!F)
tin1.conj();
tin0.set(out[k0]);
tin1.mul(out[k1]);
dft2(out[k0], out[k1], tin0, tin1);
}
}
}
private void fwd3(Complex out0, Complex out1, Complex out2, Complex in0, Complex in1, Complex in2) {
tmpA.set(in1).add(in2);
tmpB.set(in1.imag - in2.imag, in2.real - in1.real);
tmpC.set(tmpA).mul(cos(1, 3));
tmpD.set(tmpB).mul(sin(1, 3));
out0.set(in0).add(tmpA);
out1.set(in0).add(tmpC).add(tmpD);
out2.set(in0).add(tmpC).sub(tmpD);
}
private void radix3(Complex[] out, Complex[] in, int O, int I, int N, int S, boolean F) {
if (N == 3) {
if (F)
fwd3(out[O], out[O + 1], out[O + 2],
in[I], in[I + S], in[I + 2 * S]);
else
fwd3(out[O], out[O + 2], out[O + 1],
in[I], in[I + S], in[I + 2 * S]);
} else {
int Q = N / 3;
dit(out, in, O, I, Q, 3 * S, F);
dit(out, in, O + Q, I + S, Q, 3 * S, F);
dit(out, in, O + 2 * Q, I + 2 * S, Q, 3 * S, F);
for (int k0 = O, k1 = O + Q, k2 = O + 2 * Q, l1 = 0, l2 = 0;
k0 < O + Q; ++k0, ++k1, ++k2, l1 += S, l2 += 2 * S) {
tin1.set(tf[l1]);
tin2.set(tf[l2]);
if (!F) {
tin1.conj();
tin2.conj();
}
tin0.set(out[k0]);
tin1.mul(out[k1]);
tin2.mul(out[k2]);
if (F)
fwd3(out[k0], out[k1], out[k2], tin0, tin1, tin2);
else
fwd3(out[k0], out[k2], out[k1], tin0, tin1, tin2);
}
}
}
private void fwd4(Complex out0, Complex out1, Complex out2, Complex out3,
Complex in0, Complex in1, Complex in2, Complex in3) {
tmpA.set(in0).add(in2);
tmpB.set(in0).sub(in2);
tmpC.set(in1).add(in3);
tmpD.set(in1.imag - in3.imag, in3.real - in1.real);
out0.set(tmpA).add(tmpC);
out1.set(tmpB).add(tmpD);
out2.set(tmpA).sub(tmpC);
out3.set(tmpB).sub(tmpD);
}
private void radix4(Complex[] out, Complex[] in, int O, int I, int N, int S, boolean F) {
if (N == 4) {
if (F)
fwd4(out[O], out[O + 1], out[O + 2], out[O + 3],
in[I], in[I + S], in[I + 2 * S], in[I + 3 * S]);
else
fwd4(out[O], out[O + 3], out[O + 2], out[O + 1],
in[I], in[I + S], in[I + 2 * S], in[I + 3 * S]);
} else {
int Q = N / 4;
radix4(out, in, O, I, Q, 4 * S, F);
radix4(out, in, O + Q, I + S, Q, 4 * S, F);
radix4(out, in, O + 2 * Q, I + 2 * S, Q, 4 * S, F);
radix4(out, in, O + 3 * Q, I + 3 * S, Q, 4 * S, F);
for (int k0 = O, k1 = O + Q, k2 = O + 2 * Q, k3 = O + 3 * Q, l1 = 0, l2 = 0, l3 = 0;
k0 < O + Q; ++k0, ++k1, ++k2, ++k3, l1 += S, l2 += 2 * S, l3 += 3 * S) {
tin1.set(tf[l1]);
tin2.set(tf[l2]);
tin3.set(tf[l3]);
if (!F) {
tin1.conj();
tin2.conj();
tin3.conj();
}
tin0.set(out[k0]);
tin1.mul(out[k1]);
tin2.mul(out[k2]);
tin3.mul(out[k3]);
if (F)
fwd4(out[k0], out[k1], out[k2], out[k3], tin0, tin1, tin2, tin3);
else
fwd4(out[k0], out[k3], out[k2], out[k1], tin0, tin1, tin2, tin3);
}
}
}
private void fwd5(Complex out0, Complex out1, Complex out2, Complex out3, Complex out4,
Complex in0, Complex in1, Complex in2, Complex in3, Complex in4) {
tmpA.set(in1).add(in4);
tmpB.set(in2).add(in3);
tmpC.set(in1.imag - in4.imag, in4.real - in1.real);
tmpD.set(in2.imag - in3.imag, in3.real - in2.real);
tmpF.set(tmpA).mul(cos(1, 5)).add(tmpE.set(tmpB).mul(cos(2, 5)));
tmpG.set(tmpC).mul(sin(1, 5)).add(tmpE.set(tmpD).mul(sin(2, 5)));
tmpH.set(tmpA).mul(cos(2, 5)).add(tmpE.set(tmpB).mul(cos(1, 5)));
tmpI.set(tmpC).mul(sin(2, 5)).sub(tmpE.set(tmpD).mul(sin(1, 5)));
out0.set(in0).add(tmpA).add(tmpB);
out1.set(in0).add(tmpF).add(tmpG);
out2.set(in0).add(tmpH).add(tmpI);
out3.set(in0).add(tmpH).sub(tmpI);
out4.set(in0).add(tmpF).sub(tmpG);
}
private void radix5(Complex[] out, Complex[] in, int O, int I, int N, int S, boolean F) {
if (N == 5) {
if (F)
fwd5(out[O], out[O + 1], out[O + 2], out[O + 3], out[O + 4],
in[I], in[I + S], in[I + 2 * S], in[I + 3 * S], in[I + 4 * S]);
else
fwd5(out[O], out[O + 4], out[O + 3], out[O + 2], out[O + 1],
in[I], in[I + S], in[I + 2 * S], in[I + 3 * S], in[I + 4 * S]);
} else {
int Q = N / 5;
dit(out, in, O, I, Q, 5 * S, F);
dit(out, in, O + Q, I + S, Q, 5 * S, F);
dit(out, in, O + 2 * Q, I + 2 * S, Q, 5 * S, F);
dit(out, in, O + 3 * Q, I + 3 * S, Q, 5 * S, F);
dit(out, in, O + 4 * Q, I + 4 * S, Q, 5 * S, F);
for (int k0 = O, k1 = O + Q, k2 = O + 2 * Q, k3 = O + 3 * Q, k4 = O + 4 * Q, l1 = 0, l2 = 0, l3 = 0, l4 = 0;
k0 < O + Q; ++k0, ++k1, ++k2, ++k3, ++k4, l1 += S, l2 += 2 * S, l3 += 3 * S, l4 += 4 * S) {
tin1.set(tf[l1]);
tin2.set(tf[l2]);
tin3.set(tf[l3]);
tin4.set(tf[l4]);
if (!F) {
tin1.conj();
tin2.conj();
tin3.conj();
tin4.conj();
}
tin0.set(out[k0]);
tin1.mul(out[k1]);
tin2.mul(out[k2]);
tin3.mul(out[k3]);
tin4.mul(out[k4]);
if (F)
fwd5(out[k0], out[k1], out[k2], out[k3], out[k4], tin0, tin1, tin2, tin3, tin4);
else
fwd5(out[k0], out[k4], out[k3], out[k2], out[k1], tin0, tin1, tin2, tin3, tin4);
}
}
}
private void fwd7(Complex out0, Complex out1, Complex out2, Complex out3, Complex out4, Complex out5, Complex out6,
Complex in0, Complex in1, Complex in2, Complex in3, Complex in4, Complex in5, Complex in6) {
tmpA.set(in1).add(in6);
tmpB.set(in2).add(in5);
tmpC.set(in3).add(in4);
tmpD.set(in1.imag - in6.imag, in6.real - in1.real);
tmpE.set(in2.imag - in5.imag, in5.real - in2.real);
tmpF.set(in3.imag - in4.imag, in4.real - in3.real);
tmpH.set(tmpA).mul(cos(1, 7)).add(tmpG.set(tmpB).mul(cos(2, 7))).add(tmpG.set(tmpC).mul(cos(3, 7)));
tmpI.set(tmpD).mul(sin(1, 7)).add(tmpG.set(tmpE).mul(sin(2, 7))).add(tmpG.set(tmpF).mul(sin(3, 7)));
tmpJ.set(tmpA).mul(cos(2, 7)).add(tmpG.set(tmpB).mul(cos(3, 7))).add(tmpG.set(tmpC).mul(cos(1, 7)));
tmpK.set(tmpD).mul(sin(2, 7)).sub(tmpG.set(tmpE).mul(sin(3, 7))).sub(tmpG.set(tmpF).mul(sin(1, 7)));
tmpL.set(tmpA).mul(cos(3, 7)).add(tmpG.set(tmpB).mul(cos(1, 7))).add(tmpG.set(tmpC).mul(cos(2, 7)));
tmpM.set(tmpD).mul(sin(3, 7)).sub(tmpG.set(tmpE).mul(sin(1, 7))).add(tmpG.set(tmpF).mul(sin(2, 7)));
out0.set(in0).add(tmpA).add(tmpB).add(tmpC);
out1.set(in0).add(tmpH).add(tmpI);
out2.set(in0).add(tmpJ).add(tmpK);
out3.set(in0).add(tmpL).add(tmpM);
out4.set(in0).add(tmpL).sub(tmpM);
out5.set(in0).add(tmpJ).sub(tmpK);
out6.set(in0).add(tmpH).sub(tmpI);
}
private void radix7(Complex[] out, Complex[] in, int O, int I, int N, int S, boolean F) {
if (N == 7) {
if (F)
fwd7(out[O], out[O + 1], out[O + 2], out[O + 3], out[O + 4], out[O + 5], out[O + 6],
in[I], in[I + S], in[I + 2 * S], in[I + 3 * S], in[I + 4 * S], in[I + 5 * S], in[I + 6 * S]);
else
fwd7(out[O], out[O + 6], out[O + 5], out[O + 4], out[O + 3], out[O + 2], out[O + 1],
in[I], in[I + S], in[I + 2 * S], in[I + 3 * S], in[I + 4 * S], in[I + 5 * S], in[I + 6 * S]);
} else {
int Q = N / 7;
dit(out, in, O, I, Q, 7 * S, F);
dit(out, in, O + Q, I + S, Q, 7 * S, F);
dit(out, in, O + 2 * Q, I + 2 * S, Q, 7 * S, F);
dit(out, in, O + 3 * Q, I + 3 * S, Q, 7 * S, F);
dit(out, in, O + 4 * Q, I + 4 * S, Q, 7 * S, F);
dit(out, in, O + 5 * Q, I + 5 * S, Q, 7 * S, F);
dit(out, in, O + 6 * Q, I + 6 * S, Q, 7 * S, F);
for (int k0 = O, k1 = O + Q, k2 = O + 2 * Q, k3 = O + 3 * Q, k4 = O + 4 * Q, k5 = O + 5 * Q, k6 = O + 6 * Q, l1 = 0, l2 = 0, l3 = 0, l4 = 0, l5 = 0, l6 = 0;
k0 < O + Q; ++k0, ++k1, ++k2, ++k3, ++k4, ++k5, ++k6, l1 += S, l2 += 2 * S, l3 += 3 * S, l4 += 4 * S, l5 += 5 * S, l6 += 6 * S) {
tin1.set(tf[l1]);
tin2.set(tf[l2]);
tin3.set(tf[l3]);
tin4.set(tf[l4]);
tin5.set(tf[l5]);
tin6.set(tf[l6]);
if (!F) {
tin1.conj();
tin2.conj();
tin3.conj();
tin4.conj();
tin5.conj();
tin6.conj();
}
tin0.set(out[k0]);
tin1.mul(out[k1]);
tin2.mul(out[k2]);
tin3.mul(out[k3]);
tin4.mul(out[k4]);
tin5.mul(out[k5]);
tin6.mul(out[k6]);
if (F)
fwd7(out[k0], out[k1], out[k2], out[k3], out[k4], out[k5], out[k6], tin0, tin1, tin2, tin3, tin4, tin5, tin6);
else
fwd7(out[k0], out[k6], out[k5], out[k4], out[k3], out[k2], out[k1], tin0, tin1, tin2, tin3, tin4, tin5, tin6);
}
}
}
private void dit(Complex[] out, Complex[] in, int O, int I, int N, int S, boolean F) {
if (N == 1)
out[O].set(in[I]);
else if (isPowerOfFour(N))
radix4(out, in, O, I, N, S, F);
else if (N % 7 == 0)
radix7(out, in, O, I, N, S, F);
else if (N % 5 == 0)
radix5(out, in, O, I, N, S, F);
else if (N % 3 == 0)
radix3(out, in, O, I, N, S, F);
else if (N % 2 == 0)
radix2(out, in, O, I, N, S, F);
}
void forward(Complex[] out, Complex[] in) {
if (in.length != tf.length)
throw new IllegalArgumentException("Input array length (" + in.length
+ ") must be equal to Transform length (" + tf.length + ")");
if (out.length != tf.length)
throw new IllegalArgumentException("Output array length (" + out.length
+ ") must be equal to Transform length (" + tf.length + ")");
dit(out, in, 0, 0, tf.length, 1, true);
}
@SuppressWarnings("unused")
void backward(Complex[] out, Complex[] in) {
if (in.length != tf.length)
throw new IllegalArgumentException("Input array length (" + in.length
+ ") must be equal to Transform length (" + tf.length + ")");
if (out.length != tf.length)
throw new IllegalArgumentException("Output array length (" + out.length
+ ") must be equal to Transform length (" + tf.length + ")");
dit(out, in, 0, 0, tf.length, 1, false);
}
}

View file

@ -1,137 +0,0 @@
/*
HF Fax mode
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 String name;
private final int sampleRate;
private final float[] cumulated;
private int horizontalShift = 0;
HFFax(int sampleRate) {
this.name = "HF Fax";
lowPassFilter = new ExponentialMovingAverage();
this.sampleRate = sampleRate;
cumulated = new float[getWidth()];
}
private float freqToLevel(float frequency, float offset) {
return 0.5f * (frequency - offset + 1.f);
}
@Override
public String getName() {
return name;
}
@Override
public int getVISCode() {
return -1;
}
@Override
public int getWidth() {
return 640;
}
@Override
public int getHeight() {
return 1200;
}
@Override
public int getFirstPixelSampleIndex() {
return 0;
}
@Override
public int getFirstSyncPulseIndex() {
return -1;
}
@Override
public int getScanLineSamples() {
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 = 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);
}
//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

@ -1,13 +0,0 @@
/*
Hann window
Copyright 2025 Ahmet Inan <xdsopl@gmail.com>
*/
package xdsopl.robot36;
public class Hann {
static double window(int n, int N) {
return 0.5 * (1.0 - Math.cos((2.0 * Math.PI * n) / (N - 1)));
}
}

View file

@ -26,6 +26,7 @@ import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
@ -44,7 +45,6 @@ import androidx.appcompat.widget.ShareActionProvider;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.os.LocaleListCompat;
import androidx.core.view.MenuItemCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
@ -64,33 +64,24 @@ public class MainActivity extends AppCompatActivity {
private Bitmap scopeBitmap;
private PixelBuffer scopeBuffer;
private ImageView scopeView;
private Bitmap waterfallPlotBitmap;
private PixelBuffer waterfallPlotBuffer;
private ImageView waterfallPlotView;
private Bitmap freqPlotBitmap;
private PixelBuffer freqPlotBuffer;
private ImageView freqPlotView;
private Bitmap peakMeterBitmap;
private PixelBuffer peakMeterBuffer;
private ImageView peakMeterView;
private PixelBuffer imageBuffer;
private ShortTimeFourierTransform stft;
private short[] shortBuffer;
private float[] recordBuffer;
private AudioRecord audioRecord;
private Decoder decoder;
private Menu menu;
private String currentMode;
private String language;
private Complex input;
private String forceMode;
private int recordRate;
private int recordChannel;
private int audioSource;
private int audioFormat;
private int fgColor;
private int thinColor;
private int tintColor;
private boolean autoSave;
private boolean showSpectrogram;
private final int binWidthHz = 10;
private final int[] freqMarkers = { 1100, 1300, 1500, 2300 };
private void setStatus(int id) {
setTitle(id);
@ -100,31 +91,25 @@ public class MainActivity extends AppCompatActivity {
setTitle(str);
}
private void setMode(String name) {
int icon;
if (name.equals(getString(R.string.auto_mode)))
icon = R.drawable.baseline_auto_mode_24;
else
icon = R.drawable.baseline_lock_24;
menu.findItem(R.id.action_toggle_mode).setIcon(icon);
currentMode = name;
private void forceMode(int id) {
menu.findItem(R.id.action_auto_mode).setIcon(R.drawable.baseline_lock_24);
forceMode = getString(id);
if (decoder != null)
decoder.setMode(currentMode);
}
private void setMode(int id) {
setMode(getString(id));
decoder.forceMode(forceMode);
}
private void autoMode() {
setMode(R.string.auto_mode);
}
private void toggleMode() {
if (decoder == null || currentMode != null && !currentMode.equals(getString(R.string.auto_mode)))
autoMode();
else
setMode(decoder.currentMode.getName());
int icon;
if (decoder == null || forceMode != null && !forceMode.equals(getString(R.string.auto_mode))) {
icon = R.drawable.baseline_auto_mode_24;
forceMode = getString(R.string.auto_mode);
} else {
icon = R.drawable.baseline_lock_24;
forceMode = decoder.currentMode.getName();
}
menu.findItem(R.id.action_auto_mode).setIcon(icon);
if (decoder != null)
decoder.forceMode(forceMode);
}
private final AudioRecord.OnRecordPositionUpdateListener recordListener = new AudioRecord.OnRecordPositionUpdateListener() {
@ -134,19 +119,10 @@ public class MainActivity extends AppCompatActivity {
@Override
public void onPeriodicNotification(AudioRecord audioRecord) {
if (shortBuffer == null) {
audioRecord.read(recordBuffer, 0, recordBuffer.length, AudioRecord.READ_BLOCKING);
} else {
audioRecord.read(shortBuffer, 0, shortBuffer.length, AudioRecord.READ_BLOCKING);
for (int i = 0; i < shortBuffer.length; ++i)
recordBuffer[i] = .000030517578125f * shortBuffer[i];
}
audioRecord.read(recordBuffer, 0, recordBuffer.length, AudioRecord.READ_BLOCKING);
processPeakMeter();
if (showSpectrogram)
processSpectrogram();
boolean newLines = decoder.process(recordBuffer, recordChannel);
if (!showSpectrogram)
processFreqPlot();
processFreqPlot();
if (newLines) {
processScope();
processImage();
@ -169,104 +145,29 @@ public class MainActivity extends AppCompatActivity {
peakMeterView.invalidate();
}
private double clamp(double x) {
return Math.min(Math.max(x, 0), 1);
}
private int argb(double a, double r, double g, double b) {
a = clamp(a);
r = clamp(r);
g = clamp(g);
b = clamp(b);
r *= a;
g *= a;
b *= a;
r = Math.sqrt(r);
g = Math.sqrt(g);
b = Math.sqrt(b);
int A = (int) Math.rint(255 * a);
int R = (int) Math.rint(255 * r);
int G = (int) Math.rint(255 * g);
int B = (int) Math.rint(255 * b);
return (A << 24) | (R << 16) | (G << 8) | B;
}
private int rainbow(double v) {
v = clamp(v);
double t = 4 * v - 2;
return argb(4 * v, t, 1 - Math.abs(t), -t);
}
private void processSpectrogram() {
boolean process = false;
int channels = recordChannel > 0 ? 2 : 1;
for (int j = 0; j < recordBuffer.length / channels; ++j) {
switch (recordChannel) {
case 1:
input.set(recordBuffer[2 * j]);
break;
case 2:
input.set(recordBuffer[2 * j + 1]);
break;
case 3:
input.set(recordBuffer[2 * j] + recordBuffer[2 * j + 1]);
break;
case 4:
input.set(recordBuffer[2 * j], recordBuffer[2 * j + 1]);
break;
default:
input.set(recordBuffer[j]);
}
if (stft.push(input)) {
process = true;
int stride = waterfallPlotBuffer.width;
waterfallPlotBuffer.line = (waterfallPlotBuffer.line + waterfallPlotBuffer.height / 2 - 1) % (waterfallPlotBuffer.height / 2);
int line = stride * waterfallPlotBuffer.line;
double lowest = Math.log(1e-9);
double highest = Math.log(1);
double range = highest - lowest;
int minFreq = 140;
int minBin = minFreq / binWidthHz;
for (int i = 0; i < stride; ++i)
waterfallPlotBuffer.pixels[line + i] = rainbow((Math.log(stft.power[i + minBin]) - lowest) / range);
for (int freq : freqMarkers)
waterfallPlotBuffer.pixels[line + (freq - minFreq) / binWidthHz] = fgColor;
System.arraycopy(waterfallPlotBuffer.pixels, line, waterfallPlotBuffer.pixels, line + stride * (waterfallPlotBuffer.height / 2), stride);
}
}
if (process) {
int width = waterfallPlotBitmap.getWidth();
int height = waterfallPlotBitmap.getHeight();
int stride = waterfallPlotBuffer.width;
int offset = stride * waterfallPlotBuffer.line;
waterfallPlotBitmap.setPixels(waterfallPlotBuffer.pixels, offset, stride, 0, 0, width, height);
waterfallPlotView.invalidate();
}
}
private void processFreqPlot() {
int width = waterfallPlotBitmap.getWidth();
int height = waterfallPlotBitmap.getHeight();
int stride = waterfallPlotBuffer.width;
waterfallPlotBuffer.line = (waterfallPlotBuffer.line + waterfallPlotBuffer.height / 2 - 1) % (waterfallPlotBuffer.height / 2);
int line = stride * waterfallPlotBuffer.line;
int width = freqPlotBitmap.getWidth();
int height = freqPlotBitmap.getHeight();
int stride = freqPlotBuffer.width;
int line = stride * freqPlotBuffer.line;
int channels = recordChannel > 0 ? 2 : 1;
int samples = recordBuffer.length / channels;
int spread = 2;
Arrays.fill(waterfallPlotBuffer.pixels, line, line + stride, 0);
Arrays.fill(freqPlotBuffer.pixels, line, line + stride, 0);
for (int i = 0; i < samples; ++i) {
int x = Math.round((recordBuffer[i] + 2.5f) * 0.25f * stride);
if (x >= spread && x < stride - spread)
for (int j = -spread; j <= spread; ++j)
waterfallPlotBuffer.pixels[line + x + j] += 1 + spread * spread - j * j;
freqPlotBuffer.pixels[line + x + j] += 1 + spread * spread - j * j;
}
int factor = 960 / samples;
for (int i = 0; i < stride; ++i)
waterfallPlotBuffer.pixels[line + i] = 0x00FFFFFF & fgColor | Math.min(factor * waterfallPlotBuffer.pixels[line + i], 255) << 24;
System.arraycopy(waterfallPlotBuffer.pixels, line, waterfallPlotBuffer.pixels, line + stride * (waterfallPlotBuffer.height / 2), stride);
int offset = stride * waterfallPlotBuffer.line;
waterfallPlotBitmap.setPixels(waterfallPlotBuffer.pixels, offset, stride, 0, 0, width, height);
waterfallPlotView.invalidate();
freqPlotBuffer.pixels[line + i] = 0x00FFFFFF & fgColor | Math.min(factor * freqPlotBuffer.pixels[line + i], 255) << 24;
System.arraycopy(freqPlotBuffer.pixels, line, freqPlotBuffer.pixels, line + stride * (freqPlotBuffer.height / 2), stride);
freqPlotBuffer.line = (freqPlotBuffer.line + 1) % (freqPlotBuffer.height / 2);
int offset = stride * (freqPlotBuffer.line + freqPlotBuffer.height / 2 - height);
freqPlotBitmap.setPixels(freqPlotBuffer.pixels, offset, stride, 0, 0, width, height);
freqPlotView.invalidate();
}
private void processScope() {
@ -282,8 +183,7 @@ public class MainActivity extends AppCompatActivity {
if (imageBuffer.line < imageBuffer.height)
return;
imageBuffer.line = -1;
if (autoSave)
storeBitmap(Bitmap.createBitmap(imageBuffer.pixels, imageBuffer.width, imageBuffer.height, Bitmap.Config.ARGB_8888));
storeBitmap(Bitmap.createBitmap(imageBuffer.pixels, imageBuffer.width, imageBuffer.height, Bitmap.Config.ARGB_8888));
}
private void initAudioRecord() {
@ -292,8 +192,7 @@ public class MainActivity extends AppCompatActivity {
rateChanged = audioRecord.getSampleRate() != recordRate;
boolean channelChanged = audioRecord.getChannelCount() != (recordChannel == 0 ? 1 : 2);
boolean sourceChanged = audioRecord.getAudioSource() != audioSource;
boolean formatChanged = audioRecord.getAudioFormat() != audioFormat;
if (!rateChanged && !channelChanged && !sourceChanged && !formatChanged)
if (!rateChanged && !channelChanged && !sourceChanged)
return;
stopListening();
audioRecord.release();
@ -305,28 +204,24 @@ public class MainActivity extends AppCompatActivity {
channelCount = 2;
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
}
int sampleSize = audioFormat == AudioFormat.ENCODING_PCM_FLOAT ? 4 : 2;
int sampleSize = 4;
int frameSize = sampleSize * channelCount;
int audioFormat = AudioFormat.ENCODING_PCM_FLOAT;
int readsPerSecond = 50;
int bufferSize = Integer.highestOneBit(recordRate) * frameSize;
int frameCount = recordRate / readsPerSecond;
int bufferCount = frameCount * channelCount;
recordBuffer = new float[bufferCount];
shortBuffer = audioFormat == AudioFormat.ENCODING_PCM_FLOAT ? null : new short[bufferCount];
recordBuffer = new float[frameCount * channelCount];
try {
audioRecord = new AudioRecord(audioSource, recordRate, channelConfig, audioFormat, bufferSize);
if (audioRecord.getState() == AudioRecord.STATE_INITIALIZED) {
audioRecord.setRecordPositionUpdateListener(recordListener);
audioRecord.setPositionNotificationPeriod(frameCount);
if (rateChanged) {
decoder = new Decoder(scopeBuffer, imageBuffer, getString(R.string.raw_mode), recordRate);
decoder.setMode(currentMode);
stft = new ShortTimeFourierTransform(recordRate / binWidthHz, 3);
decoder = new Decoder(scopeBuffer, imageBuffer, recordRate);
decoder.forceMode(forceMode);
}
startListening();
} else {
audioRecord.release();
audioRecord = null;
setStatus(R.string.audio_init_failed);
}
} catch (IllegalArgumentException e) {
@ -340,10 +235,7 @@ public class MainActivity extends AppCompatActivity {
if (audioRecord != null) {
audioRecord.startRecording();
if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
if (shortBuffer == null)
audioRecord.read(recordBuffer, 0, recordBuffer.length, AudioRecord.READ_BLOCKING);
else
audioRecord.read(shortBuffer, 0, recordBuffer.length, AudioRecord.READ_BLOCKING);
audioRecord.read(recordBuffer, 0, recordBuffer.length, AudioRecord.READ_BLOCKING);
setStatus(R.string.listening);
} else {
setStatus(R.string.audio_recording_error);
@ -380,42 +272,6 @@ public class MainActivity extends AppCompatActivity {
initAudioRecord();
}
private void setAudioFormat(int newAudioFormat) {
if (audioFormat == newAudioFormat)
return;
audioFormat = newAudioFormat;
updateAudioFormatMenu();
initAudioRecord();
}
private void setShowSpectrogram(boolean newShowSpectrogram) {
if (showSpectrogram == newShowSpectrogram)
return;
showSpectrogram = newShowSpectrogram;
updateWaterfallPlotMenu();
}
private void updateWaterfallPlotMenu() {
if (showSpectrogram)
menu.findItem(R.id.action_show_spectrogram).setChecked(true);
else
menu.findItem(R.id.action_show_frequency_plot).setChecked(true);
}
private void setAutoSave(boolean newAutoSave) {
if (autoSave == newAutoSave)
return;
autoSave = newAutoSave;
updateAutoSaveMenu();
}
private void updateAutoSaveMenu() {
if (autoSave)
menu.findItem(R.id.action_enable_auto_save).setChecked(true);
else
menu.findItem(R.id.action_disable_auto_save).setChecked(true);
}
private void updateRecordRateMenu() {
switch (recordRate) {
case 8000:
@ -476,10 +332,6 @@ public class MainActivity extends AppCompatActivity {
}
}
private void updateAudioFormatMenu() {
menu.findItem(audioFormat == AudioFormat.ENCODING_PCM_FLOAT ? R.id.action_set_floating_point : R.id.action_set_fixed_point).setChecked(true);
}
private final int permissionID = 1;
@Override
@ -498,10 +350,6 @@ public class MainActivity extends AppCompatActivity {
state.putInt("recordRate", recordRate);
state.putInt("recordChannel", recordChannel);
state.putInt("audioSource", audioSource);
state.putInt("audioFormat", audioFormat);
state.putBoolean("autoSave", autoSave);
state.putBoolean("showSpectrogram", showSpectrogram);
state.putString("language", language);
super.onSaveInstanceState(state);
}
@ -512,10 +360,6 @@ public class MainActivity extends AppCompatActivity {
edit.putInt("recordRate", recordRate);
edit.putInt("recordChannel", recordChannel);
edit.putInt("audioSource", audioSource);
edit.putInt("audioFormat", audioFormat);
edit.putBoolean("autoSave", autoSave);
edit.putBoolean("showSpectrogram", showSpectrogram);
edit.putString("language", language);
edit.apply();
}
@ -524,32 +368,19 @@ public class MainActivity extends AppCompatActivity {
final int defaultSampleRate = 44100;
final int defaultChannelSelect = 0;
final int defaultAudioSource = MediaRecorder.AudioSource.MIC;
final int defaultAudioFormat = AudioFormat.ENCODING_PCM_16BIT;
final boolean defaultAutoSave = true;
final boolean defaultShowSpectrogram = true;
final String defaultLanguage = "system";
if (state == null) {
SharedPreferences pref = getPreferences(Context.MODE_PRIVATE);
AppCompatDelegate.setDefaultNightMode(pref.getInt("nightMode", AppCompatDelegate.getDefaultNightMode()));
recordRate = pref.getInt("recordRate", defaultSampleRate);
recordChannel = pref.getInt("recordChannel", defaultChannelSelect);
audioSource = pref.getInt("audioSource", defaultAudioSource);
audioFormat = pref.getInt("audioFormat", defaultAudioFormat);
autoSave = pref.getBoolean("autoSave", defaultAutoSave);
showSpectrogram = pref.getBoolean("showSpectrogram", defaultShowSpectrogram);
language = pref.getString("language", defaultLanguage);
} else {
AppCompatDelegate.setDefaultNightMode(state.getInt("nightMode", AppCompatDelegate.getDefaultNightMode()));
recordRate = state.getInt("recordRate", defaultSampleRate);
recordChannel = state.getInt("recordChannel", defaultChannelSelect);
audioSource = state.getInt("audioSource", defaultAudioSource);
audioFormat = state.getInt("audioFormat", defaultAudioFormat);
autoSave = state.getBoolean("autoSave", defaultAutoSave);
showSpectrogram = state.getBoolean("showSpectrogram", defaultShowSpectrogram);
language = state.getString("language", defaultLanguage);
}
super.onCreate(state);
setLanguage(language);
Configuration config = getResources().getConfiguration();
EdgeToEdge.enable(this);
setContentView(config.orientation == Configuration.ORIENTATION_LANDSCAPE ? R.layout.activity_main_land : R.layout.activity_main);
@ -558,13 +389,12 @@ public class MainActivity extends AppCompatActivity {
thinColor = getColor(R.color.thin);
tintColor = getColor(R.color.tint);
scopeBuffer = new PixelBuffer(640, 2 * 1280);
waterfallPlotBuffer = new PixelBuffer(256, 2 * 256);
peakMeterBuffer = new PixelBuffer(1, 16);
imageBuffer = new PixelBuffer(800, 616);
input = new Complex();
createScope(config);
createWaterfallPlot(config);
freqPlotBuffer = new PixelBuffer(256, 2 * 256);
createFreqPlot(config);
peakMeterBuffer = new PixelBuffer(1, 16);
createPeakMeter();
imageBuffer = new PixelBuffer(640, 496);
List<String> permissions = new ArrayList<>();
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
permissions.add(Manifest.permission.RECORD_AUDIO);
@ -593,9 +423,6 @@ public class MainActivity extends AppCompatActivity {
updateRecordRateMenu();
updateRecordChannelMenu();
updateAudioSourceMenu();
updateAudioFormatMenu();
updateWaterfallPlotMenu();
updateAutoSaveMenu();
return true;
}
@ -606,80 +433,68 @@ public class MainActivity extends AppCompatActivity {
storeScope();
return true;
}
if (id == R.id.action_toggle_mode) {
toggleMode();
return true;
}
if (id == R.id.action_auto_mode) {
autoMode();
return true;
}
if (id == R.id.action_force_raw_mode) {
setMode(R.string.raw_mode);
return true;
}
if (id == R.id.action_force_hffax_mode) {
setMode(R.string.hf_fax);
forceMode(R.string.raw_mode);
return true;
}
if (id == R.id.action_force_robot36_color) {
setMode(R.string.robot36_color);
forceMode(R.string.robot36_color);
return true;
}
if (id == R.id.action_force_robot72_color) {
setMode(R.string.robot72_color);
forceMode(R.string.robot72_color);
return true;
}
if (id == R.id.action_force_pd50) {
setMode(R.string.pd50);
forceMode(R.string.pd50);
return true;
}
if (id == R.id.action_force_pd90) {
setMode(R.string.pd90);
forceMode(R.string.pd90);
return true;
}
if (id == R.id.action_force_pd120) {
setMode(R.string.pd120);
forceMode(R.string.pd120);
return true;
}
if (id == R.id.action_force_pd160) {
setMode(R.string.pd160);
forceMode(R.string.pd160);
return true;
}
if (id == R.id.action_force_pd180) {
setMode(R.string.pd180);
forceMode(R.string.pd180);
return true;
}
if (id == R.id.action_force_pd240) {
setMode(R.string.pd240);
return true;
}
if (id == R.id.action_force_pd290) {
setMode(R.string.pd290);
forceMode(R.string.pd240);
return true;
}
if (id == R.id.action_force_martin1) {
setMode(R.string.martin1);
forceMode(R.string.martin1);
return true;
}
if (id == R.id.action_force_martin2) {
setMode(R.string.martin2);
forceMode(R.string.martin2);
return true;
}
if (id == R.id.action_force_scottie1) {
setMode(R.string.scottie1);
forceMode(R.string.scottie1);
return true;
}
if (id == R.id.action_force_scottie2) {
setMode(R.string.scottie2);
forceMode(R.string.scottie2);
return true;
}
if (id == R.id.action_force_scottie_dx) {
setMode(R.string.scottie_dx);
forceMode(R.string.scottie_dx);
return true;
}
if (id == R.id.action_force_wraase_sc2_180) {
setMode(R.string.wraase_sc2_180);
forceMode(R.string.wraase_sc2_180);
return true;
}
if (id == R.id.action_set_record_rate_8000) {
@ -742,30 +557,6 @@ public class MainActivity extends AppCompatActivity {
setAudioSource(MediaRecorder.AudioSource.UNPROCESSED);
return true;
}
if (id == R.id.action_set_floating_point) {
setAudioFormat(AudioFormat.ENCODING_PCM_FLOAT);
return true;
}
if (id == R.id.action_set_fixed_point) {
setAudioFormat(AudioFormat.ENCODING_PCM_16BIT);
return true;
}
if (id == R.id.action_show_spectrogram) {
setShowSpectrogram(true);
return true;
}
if (id == R.id.action_show_frequency_plot) {
setShowSpectrogram(false);
return true;
}
if (id == R.id.action_enable_auto_save) {
setAutoSave(true);
return true;
}
if (id == R.id.action_disable_auto_save) {
setAutoSave(false);
return true;
}
if (id == R.id.action_enable_night_mode) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
return true;
@ -779,81 +570,28 @@ public class MainActivity extends AppCompatActivity {
return true;
}
if (id == R.id.action_about) {
showTextPage(getString(R.string.about_text, BuildConfig.VERSION_NAME, getString(R.string.disclaimer)));
return true;
}
if (id == R.id.action_english) {
setLanguage("en-US");
return true;
}
if (id == R.id.action_simplified_chinese) {
setLanguage("zh-CN");
return true;
}
if (id == R.id.action_russian) {
setLanguage("ru");
return true;
}
if (id == R.id.action_german) {
setLanguage("de");
return true;
}
if (id == R.id.action_brazilian_portuguese) {
setLanguage("pt-BR");
return true;
}
if (id == R.id.action_polish) {
setLanguage("pl");
return true;
}
if (id == R.id.action_ukrainian) {
setLanguage("uk");
return true;
}
if (id == R.id.action_latin_american_spanish) {
setLanguage("es-r419");
return true;
}
if (id == R.id.action_french) {
setLanguage("fr");
showTextPage(getString(R.string.about_text, BuildConfig.VERSION_NAME));
return true;
}
return super.onOptionsItemSelected(item);
}
private void setLanguage(String language) {
this.language = language;
if (!language.equals("system"))
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(language));
}
private void storeScope() {
int width = scopeBuffer.width;
int height = scopeBuffer.height / 2;
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)
{
bmp = decoder.currentMode.postProcessScopeImage(bmp);
}
storeBitmap(bmp);
storeBitmap(Bitmap.createBitmap(scopeBuffer.pixels, offset, stride, width, height, Bitmap.Config.ARGB_8888));
}
private void createScope(Configuration config) {
int screenWidthDp = config.screenWidthDp;
int screenHeightDp = config.screenHeightDp;
int waterfallPlotHeightDp = 64;
if (config.orientation == Configuration.ORIENTATION_LANDSCAPE)
screenWidthDp /= 2;
else
screenHeightDp -= waterfallPlotHeightDp;
int actionBarHeightDp = 64;
screenHeightDp -= actionBarHeightDp;
int width = scopeBuffer.width;
int height = Math.min(Math.max((width * screenHeightDp) / screenWidthDp, 496), scopeBuffer.height / 2);
int height = scopeBuffer.height / 2;
DisplayMetrics metrics = getResources().getDisplayMetrics();
if (config.orientation == Configuration.ORIENTATION_LANDSCAPE)
height /= 2;
else
height = Math.min(Math.max((width * (metrics.heightPixels - 257)) / metrics.widthPixels, height / 2), height);
scopeBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
int stride = scopeBuffer.width;
int offset = stride * (scopeBuffer.line + scopeBuffer.height / 2 - height);
@ -863,18 +601,18 @@ public class MainActivity extends AppCompatActivity {
scopeView.setImageBitmap(scopeBitmap);
}
private void createWaterfallPlot(Configuration config) {
int width = waterfallPlotBuffer.width;
int height = waterfallPlotBuffer.height / 2;
private void createFreqPlot(Configuration config) {
int width = freqPlotBuffer.width;
int height = freqPlotBuffer.height / 2;
if (config.orientation != Configuration.ORIENTATION_LANDSCAPE)
height /= 4;
waterfallPlotBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
int stride = waterfallPlotBuffer.width;
int offset = stride * waterfallPlotBuffer.line;
waterfallPlotBitmap.setPixels(waterfallPlotBuffer.pixels, offset, stride, 0, 0, width, height);
waterfallPlotView = findViewById(R.id.waterfall_plot);
waterfallPlotView.setScaleType(ImageView.ScaleType.FIT_XY);
waterfallPlotView.setImageBitmap(waterfallPlotBitmap);
freqPlotBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
int stride = freqPlotBuffer.width;
int offset = stride * (freqPlotBuffer.line + freqPlotBuffer.height / 2 - height);
freqPlotBitmap.setPixels(freqPlotBuffer.pixels, offset, stride, 0, 0, width, height);
freqPlotView = findViewById(R.id.freq_plot);
freqPlotView.setScaleType(ImageView.ScaleType.FIT_XY);
freqPlotView.setImageBitmap(freqPlotBitmap);
}
private void createPeakMeter() {
@ -891,7 +629,7 @@ public class MainActivity extends AppCompatActivity {
setContentView(config.orientation == Configuration.ORIENTATION_LANDSCAPE ? R.layout.activity_main_land : R.layout.activity_main);
handleInsets();
createScope(config);
createWaterfallPlot(config);
createFreqPlot(config);
createPeakMeter();
}

View file

@ -6,30 +6,22 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
package xdsopl.robot36;
import android.graphics.Bitmap;
public interface Mode {
String getName();
int getVISCode();
int getCode();
int getWidth();
int getHeight();
int getFirstPixelSampleIndex();
int getBegin();
int getFirstSyncPulseIndex();
int getScanLineSamples();
Bitmap postProcessScopeImage(Bitmap bmp);
void reset();
void resetState();
/**
* @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);
}

View file

@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
package xdsopl.robot36;
public class PaulDon extends BaseMode {
public class PaulDon implements Mode {
private final ExponentialMovingAverage lowPassFilter;
private final int horizontalPixels;
private final int verticalPixels;
@ -56,7 +56,7 @@ public class PaulDon extends BaseMode {
}
@Override
public int getVISCode() {
public int getCode() {
return code;
}
@ -71,7 +71,7 @@ public class PaulDon extends BaseMode {
}
@Override
public int getFirstPixelSampleIndex() {
public int getBegin() {
return beginSamples;
}
@ -86,7 +86,7 @@ public class PaulDon extends BaseMode {
}
@Override
public void resetState() {
public void reset() {
}
@Override

View file

@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
package xdsopl.robot36;
public class RGBDecoder extends BaseMode {
public class RGBDecoder implements Mode {
private final ExponentialMovingAverage lowPassFilter;
private final int horizontalPixels;
private final int verticalPixels;
@ -51,7 +51,7 @@ public class RGBDecoder extends BaseMode {
}
@Override
public int getVISCode() {
public int getCode() {
return code;
}
@ -66,7 +66,7 @@ public class RGBDecoder extends BaseMode {
}
@Override
public int getFirstPixelSampleIndex() {
public int getBegin() {
return beginSamples;
}
@ -81,7 +81,7 @@ public class RGBDecoder extends BaseMode {
}
@Override
public void resetState() {
public void reset() {
}
@Override

View file

@ -6,14 +6,12 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
package xdsopl.robot36;
public class RawDecoder extends BaseMode {
public class RawDecoder implements Mode {
private final ExponentialMovingAverage lowPassFilter;
private final int smallPictureMaxSamples;
private final int mediumPictureMaxSamples;
private final String name;
RawDecoder(String name, int sampleRate) {
this.name = name;
RawDecoder(int sampleRate) {
smallPictureMaxSamples = (int) Math.round(0.125 * sampleRate);
mediumPictureMaxSamples = (int) Math.round(0.175 * sampleRate);
lowPassFilter = new ExponentialMovingAverage();
@ -25,11 +23,11 @@ public class RawDecoder extends BaseMode {
@Override
public String getName() {
return name;
return "Raw Mode";
}
@Override
public int getVISCode() {
public int getCode() {
return -1;
}
@ -44,7 +42,7 @@ public class RawDecoder extends BaseMode {
}
@Override
public int getFirstPixelSampleIndex() {
public int getBegin() {
return 0;
}
@ -59,7 +57,7 @@ public class RawDecoder extends BaseMode {
}
@Override
public void resetState() {
public void reset() {
}
@Override

View file

@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
package xdsopl.robot36;
public class Robot_36_Color extends BaseMode {
public class Robot_36_Color implements Mode {
private final ExponentialMovingAverage lowPassFilter;
private final int horizontalPixels;
private final int verticalPixels;
@ -59,7 +59,7 @@ public class Robot_36_Color extends BaseMode {
}
@Override
public int getVISCode() {
public int getCode() {
return 8;
}
@ -74,7 +74,7 @@ public class Robot_36_Color extends BaseMode {
}
@Override
public int getFirstPixelSampleIndex() {
public int getBegin() {
return beginSamples;
}
@ -89,7 +89,7 @@ public class Robot_36_Color extends BaseMode {
}
@Override
public void resetState() {
public void reset() {
lastEven = false;
}

View file

@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
package xdsopl.robot36;
public class Robot_72_Color extends BaseMode {
public class Robot_72_Color implements Mode {
private final ExponentialMovingAverage lowPassFilter;
private final int horizontalPixels;
private final int verticalPixels;
@ -57,7 +57,7 @@ public class Robot_72_Color extends BaseMode {
}
@Override
public int getVISCode() {
public int getCode() {
return 12;
}
@ -72,7 +72,7 @@ public class Robot_72_Color extends BaseMode {
}
@Override
public int getFirstPixelSampleIndex() {
public int getBegin() {
return beginSamples;
}
@ -87,7 +87,7 @@ public class Robot_72_Color extends BaseMode {
}
@Override
public void resetState() {
public void reset() {
}
@Override

View file

@ -1,50 +0,0 @@
/*
Short Time Fourier Transform
Copyright 2025 Ahmet Inan <xdsopl@gmail.com>
*/
package xdsopl.robot36;
public class ShortTimeFourierTransform {
private final FastFourierTransform fft;
private final Complex[] prev, fold, freq;
private final float[] weight;
private final Complex temp;
private int index;
public final float[] power;
ShortTimeFourierTransform(int length, int overlap) {
fft = new FastFourierTransform(length);
prev = new Complex[length * overlap];
for (int i = 0; i < length * overlap; ++i)
prev[i] = new Complex();
fold = new Complex[length];
for (int i = 0; i < length; ++i)
fold[i] = new Complex();
freq = new Complex[length];
for (int i = 0; i < length; ++i)
freq[i] = new Complex();
temp = new Complex();
power = new float[length];
weight = new float[length * overlap];
for (int i = 0; i < length * overlap; ++i)
weight[i] = (float)(Filter.lowPass(1, length, i, length * overlap) * Hann.window(i, length * overlap));
}
boolean push(Complex input) {
prev[index].set(input);
index = (index + 1) % prev.length;
if (index % fold.length != 0)
return false;
for (int i = 0; i < fold.length; ++i, index = (index + 1) % prev.length)
fold[i].set(prev[index]).mul(weight[i]);
for (int i = fold.length; i < prev.length; ++i, index = (index + 1) % prev.length)
fold[i % fold.length].add(temp.set(prev[index]).mul(weight[i]));
fft.forward(freq, fold);
for (int i = 0; i < power.length; ++i)
power[i] = freq[i].norm();
return true;
}
}

View file

@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M21.9,21.9l-8.49,-8.49l0,0L3.59,3.59l0,0L2.1,2.1L0.69,3.51L3,5.83V19c0,1.1 0.9,2 2,2h13.17l2.31,2.31L21.9,21.9zM5,18l3.5,-4.5l2.5,3.01L12.17,15l3,3H5zM21,18.17L5.83,3H19c1.1,0 2,0.9 2,2V18.17z"/>
</vector>

View file

@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>

View file

@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM18.92,8h-2.95c-0.32,-1.25 -0.78,-2.45 -1.38,-3.56 1.84,0.63 3.37,1.91 4.33,3.56zM12,4.04c0.83,1.2 1.48,2.53 1.91,3.96h-3.82c0.43,-1.43 1.08,-2.76 1.91,-3.96zM4.26,14C4.1,13.36 4,12.69 4,12s0.1,-1.36 0.26,-2h3.38c-0.08,0.66 -0.14,1.32 -0.14,2 0,0.68 0.06,1.34 0.14,2L4.26,14zM5.08,16h2.95c0.32,1.25 0.78,2.45 1.38,3.56 -1.84,-0.63 -3.37,-1.9 -4.33,-3.56zM8.03,8L5.08,8c0.96,-1.66 2.49,-2.93 4.33,-3.56C8.81,5.55 8.35,6.75 8.03,8zM12,19.96c-0.83,-1.2 -1.48,-2.53 -1.91,-3.96h3.82c-0.43,1.43 -1.08,2.76 -1.91,3.96zM14.34,14L9.66,14c-0.09,-0.66 -0.16,-1.32 -0.16,-2 0,-0.68 0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2 0,0.68 -0.07,1.34 -0.16,2zM14.59,19.56c0.6,-1.11 1.06,-2.31 1.38,-3.56h2.95c-0.96,1.65 -2.49,2.93 -4.33,3.56zM16.36,14c0.08,-0.66 0.14,-1.32 0.14,-2 0,-0.68 -0.06,-1.34 -0.14,-2h3.38c0.16,0.64 0.26,1.31 0.26,2s-0.1,1.36 -0.26,2h-3.38z"/>
</vector>

View file

@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z"/>
</vector>

View file

@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M9.5,2c-1.82,0 -3.53,0.5 -5,1.35c2.99,1.73 5,4.95 5,8.65s-2.01,6.92 -5,8.65C5.97,21.5 7.68,22 9.5,22c5.52,0 10,-4.48 10,-10S15.02,2 9.5,2z"/>
</vector>

View file

@ -1,7 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M21,5l-9,-4L3,5v6c0,5.55 3.84,10.74 9,12c2.3,-0.56 4.33,-1.9 5.88,-3.71l-3.12,-3.12c-1.94,1.29 -4.58,1.07 -6.29,-0.64c-1.95,-1.95 -1.95,-5.12 0,-7.07c1.95,-1.95 5.12,-1.95 7.07,0c1.71,1.71 1.92,4.35 0.64,6.29l2.9,2.9C20.29,15.69 21,13.38 21,11V5z"/>
<path android:fillColor="@android:color/white" android:pathData="M12,12m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"/>
</vector>

View file

@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M7,24h2v-2L7,22v2zM12,13c1.66,0 2.99,-1.34 2.99,-3L15,4c0,-1.66 -1.34,-3 -3,-3S9,2.34 9,4v6c0,1.66 1.34,3 3,3zM11,24h2v-2h-2v2zM15,24h2v-2h-2v2zM19,10h-1.7c0,3 -2.54,5.1 -5.3,5.1S6.7,13 6.7,10L5,10c0,3.41 2.72,6.23 6,6.72L11,20h2v-3.28c3.28,-0.49 6,-3.31 6,-6.72z"/>
</vector>

View file

@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M11,4V2c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v2c0,0.55 -0.45,1 -1,1S11,4.55 11,4zM18.36,7.05l1.41,-1.42c0.39,-0.39 0.39,-1.02 0,-1.41c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.41,1.42c-0.39,0.39 -0.39,1.02 0,1.41C17.34,7.44 17.97,7.44 18.36,7.05zM22,11h-2c-0.55,0 -1,0.45 -1,1s0.45,1 1,1h2c0.55,0 1,-0.45 1,-1S22.55,11 22,11zM12,19c-0.55,0 -1,0.45 -1,1v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-2C13,19.45 12.55,19 12,19zM5.64,7.05L4.22,5.64c-0.39,-0.39 -0.39,-1.03 0,-1.41s1.03,-0.39 1.41,0l1.41,1.41c0.39,0.39 0.39,1.03 0,1.41S6.02,7.44 5.64,7.05zM16.95,16.95c-0.39,0.39 -0.39,1.03 0,1.41l1.41,1.41c0.39,0.39 1.03,0.39 1.41,0c0.39,-0.39 0.39,-1.03 0,-1.41l-1.41,-1.41C17.98,16.56 17.34,16.56 16.95,16.95zM2,13h2c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1H2c-0.55,0 -1,0.45 -1,1S1.45,13 2,13zM5.64,19.78l1.41,-1.41c0.39,-0.39 0.39,-1.03 0,-1.41s-1.03,-0.39 -1.41,0l-1.41,1.41c-0.39,0.39 -0.39,1.03 0,1.41C4.61,20.17 5.25,20.17 5.64,19.78zM12,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6s6,-2.69 6,-6S15.31,6 12,6z"/>
</vector>

View file

@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M21.98,14H22H21.98zM5.35,13c1.19,0 1.42,1 3.33,1c1.95,0 2.09,-1 3.33,-1c1.19,0 1.42,1 3.33,1c1.95,0 2.09,-1 3.33,-1c1.19,0 1.4,0.98 3.31,1v-2c-1.19,0 -1.42,-1 -3.33,-1c-1.95,0 -2.09,1 -3.33,1c-1.19,0 -1.42,-1 -3.33,-1c-1.95,0 -2.09,1 -3.33,1c-1.19,0 -1.42,-1 -3.33,-1C3.38,11 3.24,12 2,12v2C3.9,14 4.17,13 5.35,13zM18.67,15c-1.95,0 -2.09,1 -3.33,1c-1.19,0 -1.42,-1 -3.33,-1c-1.95,0 -2.1,1 -3.34,1c-1.24,0 -1.38,-1 -3.33,-1c-1.95,0 -2.1,1 -3.34,1v2c1.95,0 2.11,-1 3.34,-1c1.24,0 1.38,1 3.33,1c1.95,0 2.1,-1 3.34,-1c1.19,0 1.42,1 3.33,1c1.94,0 2.09,-1 3.33,-1c1.19,0 1.42,1 3.33,1v-2C20.76,16 20.62,15 18.67,15zM5.35,9c1.19,0 1.42,1 3.33,1c1.95,0 2.09,-1 3.33,-1c1.19,0 1.42,1 3.33,1c1.95,0 2.09,-1 3.33,-1c1.19,0 1.4,0.98 3.31,1V8c-1.19,0 -1.42,-1 -3.33,-1c-1.95,0 -2.09,1 -3.33,1c-1.19,0 -1.42,-1 -3.33,-1C10.04,7 9.9,8 8.66,8C7.47,8 7.24,7 5.33,7C3.38,7 3.24,8 2,8v2C3.9,10 4.17,9 5.35,9z"/>
</vector>

View file

@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M240,160L240,160L240,360L240,360L240,160L240,360L240,360L240,800Q240,800 240,800Q240,800 240,800Q240,800 240,800Q240,800 240,800Q240,800 240,790.5Q240,781 240,800L240,800Q240,800 240,800Q240,800 240,800L240,160Q240,160 240,160Q240,160 240,160ZM240,880Q207,880 183.5,856.5Q160,833 160,800L160,160Q160,127 183.5,103.5Q207,80 240,80L560,80L800,320L800,494Q781,487 761,483.5Q741,480 720,480L720,360L520,360L520,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800L494,800Q502,823 514,843Q526,863 542,880L240,880ZM636,860L580,804L664,720L580,636L636,580L720,664L804,580L860,636L777,720L860,804L804,860L720,777L636,860Z"/>
</vector>

View file

@ -13,16 +13,16 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/scope_description"
app:layout_constraintBottom_toTopOf="@+id/waterfall_plot"
app:layout_constraintBottom_toTopOf="@+id/freq_plot"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/waterfall_plot"
android:id="@+id/freq_plot"
android:layout_width="0dp"
android:layout_height="64dp"
android:contentDescription="@string/waterfall_plot"
android:layout_height="wrap_content"
android:contentDescription="@string/freq_plot_description"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/peak_meter"
app:layout_constraintStart_toStartOf="parent"
@ -31,10 +31,10 @@
<ImageView
android:id="@+id/peak_meter"
android:layout_width="16dp"
android:layout_height="64dp"
android:layout_height="0dp"
android:contentDescription="@string/peak_meter_description"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/waterfall_plot"
app:layout_constraintStart_toEndOf="@+id/freq_plot"
app:layout_constraintTop_toBottomOf="@+id/scope" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -14,15 +14,15 @@
android:layout_height="0dp"
android:contentDescription="@string/scope_description"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/waterfall_plot"
app:layout_constraintEnd_toStartOf="@+id/freq_plot"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/waterfall_plot"
android:id="@+id/freq_plot"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/waterfall_plot"
android:contentDescription="@string/freq_plot_description"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/peak_meter"
app:layout_constraintStart_toEndOf="@+id/scope"
@ -35,6 +35,6 @@
android:contentDescription="@string/peak_meter_description"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/waterfall_plot"
app:layout_constraintStart_toEndOf="@+id/freq_plot"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -4,11 +4,11 @@
xmlns:tools="http://schemas.android.com/tools"
tools:context="xdsopl.robot36.MainActivity">
<item
android:id="@+id/action_toggle_mode"
android:id="@+id/action_auto_mode"
android:icon="@drawable/baseline_auto_mode_24"
android:title="@string/toggle_mode"
android:title="@string/auto_mode"
app:iconTint="@color/tint"
app:showAsAction="always" />
app:showAsAction="always"/>
<item
android:id="@+id/action_store_scope"
android:icon="@drawable/baseline_save_alt_24"
@ -21,280 +21,156 @@
android:title="@string/share"
app:actionProviderClass="androidx.appcompat.widget.ShareActionProvider"
app:showAsAction="always" />
<item
android:icon="@drawable/baseline_menu_24"
android:title="@string/more_options"
app:iconTint="@color/tint"
app:showAsAction="always">
<item android:title="@string/force_mode">
<menu>
<item
android:icon="@drawable/baseline_lock_24"
android:title="@string/lock_mode"
app:iconTint="@color/tint">
<menu>
<item android:title="@string/robot_modes">
<menu>
<item
android:id="@+id/action_force_robot36_color"
android:title="@string/robot36_color" />
<item
android:id="@+id/action_force_robot72_color"
android:title="@string/robot72_color" />
</menu>
</item>
<item android:title="@string/pd_modes">
<menu>
<item
android:id="@+id/action_force_pd50"
android:title="@string/pd50" />
<item
android:id="@+id/action_force_pd90"
android:title="@string/pd90" />
<item
android:id="@+id/action_force_pd120"
android:title="@string/pd120" />
<item
android:id="@+id/action_force_pd160"
android:title="@string/pd160" />
<item
android:id="@+id/action_force_pd180"
android:title="@string/pd180" />
<item
android:id="@+id/action_force_pd240"
android:title="@string/pd240" />
<item
android:id="@+id/action_force_pd290"
android:title="@string/pd290" />
</menu>
</item>
<item android:title="@string/martin_modes">
<menu>
<item
android:id="@+id/action_force_martin1"
android:title="@string/martin1" />
<item
android:id="@+id/action_force_martin2"
android:title="@string/martin2" />
</menu>
</item>
<item android:title="@string/scottie_modes">
<menu>
<item
android:id="@+id/action_force_scottie1"
android:title="@string/scottie1" />
<item
android:id="@+id/action_force_scottie2"
android:title="@string/scottie2" />
<item
android:id="@+id/action_force_scottie_dx"
android:title="@string/scottie_dx" />
</menu>
</item>
<item android:title="@string/wraase_modes">
<menu>
<item
android:id="@+id/action_force_wraase_sc2_180"
android:title="@string/wraase_sc2_180" />
</menu>
</item>
<item android:title="@string/contributed_modes">
<menu>
<item
android:id="@+id/action_force_hffax_mode"
android:title="@string/hf_fax" />
</menu>
</item>
<item
android:id="@+id/action_force_raw_mode"
android:icon="@drawable/baseline_image_not_supported_24"
android:title="@string/raw_mode"
app:iconTint="@color/tint" />
<item
android:id="@+id/action_auto_mode"
android:icon="@drawable/baseline_auto_mode_24"
android:title="@string/auto_mode"
app:iconTint="@color/tint" />
</menu>
</item>
<item
android:icon="@drawable/baseline_settings_voice_24"
android:title="@string/audio_settings"
app:iconTint="@color/tint">
<menu>
<item android:title="@string/sample_rate">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/action_set_record_rate_8000"
android:title="@string/rate_8000" />
<item
android:id="@+id/action_set_record_rate_16000"
android:title="@string/rate_16000" />
<item
android:id="@+id/action_set_record_rate_32000"
android:title="@string/rate_32000" />
<item
android:id="@+id/action_set_record_rate_44100"
android:title="@string/rate_44100" />
<item
android:id="@+id/action_set_record_rate_48000"
android:title="@string/rate_48000" />
</group>
</menu>
</item>
<item android:title="@string/channel_select">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/action_set_record_channel_default"
android:title="@string/channel_default" />
<item
android:id="@+id/action_set_record_channel_first"
android:title="@string/channel_first" />
<item
android:id="@+id/action_set_record_channel_second"
android:title="@string/channel_second" />
<item
android:id="@+id/action_set_record_channel_summation"
android:title="@string/channel_summation" />
<item
android:id="@+id/action_set_record_channel_analytic"
android:title="@string/channel_analytic" />
</group>
</menu>
</item>
<item android:title="@string/audio_source">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/action_set_source_default"
android:title="@string/source_default" />
<item
android:id="@+id/action_set_source_microphone"
android:title="@string/source_microphone" />
<item
android:id="@+id/action_set_source_camcorder"
android:title="@string/source_camcorder" />
<item
android:id="@+id/action_set_source_voice_recognition"
android:title="@string/source_voice_recognition" />
<item
android:id="@+id/action_set_source_unprocessed"
android:title="@string/source_unprocessed" />
</group>
</menu>
</item>
<item android:title="@string/audio_format">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/action_set_floating_point"
android:title="@string/floating_point" />
<item
android:id="@+id/action_set_fixed_point"
android:title="@string/fixed_point" />
</group>
</menu>
</item>
</menu>
</item>
<item
android:icon="@drawable/baseline_mode_night_24"
android:title="@string/night_mode"
app:iconTint="@color/tint">
android:id="@+id/action_force_raw_mode"
android:title="@string/raw_mode" />
<item android:title="@string/robot_modes">
<menu>
<item
android:id="@+id/action_enable_night_mode"
android:icon="@drawable/baseline_mode_night_24"
android:title="@string/enable"
app:iconTint="@color/tint" />
android:id="@+id/action_force_robot36_color"
android:title="@string/robot36_color" />
<item
android:id="@+id/action_disable_night_mode"
android:icon="@drawable/baseline_sunny_24"
android:title="@string/disable"
app:iconTint="@color/tint" />
android:id="@+id/action_force_robot72_color"
android:title="@string/robot72_color" />
</menu>
</item>
<item
android:icon="@drawable/baseline_water_24"
android:title="@string/waterfall_plot"
app:iconTint="@color/tint">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/action_show_spectrogram"
android:title="@string/spectrogram"
app:iconTint="@color/tint" />
<item
android:id="@+id/action_show_frequency_plot"
android:title="@string/frequency_plot"
app:iconTint="@color/tint" />
</group>
</menu>
</item>
<item
android:icon="@drawable/baseline_auto_mode_24"
android:title="@string/auto_save"
app:iconTint="@color/tint">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/action_enable_auto_save"
android:icon="@drawable/baseline_save_alt_24"
android:title="@string/enable"
app:iconTint="@color/tint" />
<item
android:id="@+id/action_disable_auto_save"
android:icon="@drawable/outline_scan_delete_24"
android:title="@string/disable"
app:iconTint="@color/tint" />
</group>
</menu>
</item>
<item
android:icon="@drawable/baseline_language_24"
android:title="@string/language"
app:iconTint="@color/tint">
<item android:title="@string/pd_modes">
<menu>
<item
android:id="@+id/action_english"
android:title="@string/english" />
android:id="@+id/action_force_pd50"
android:title="@string/pd50" />
<item
android:id="@+id/action_simplified_chinese"
android:title="@string/simplified_chinese" />
android:id="@+id/action_force_pd90"
android:title="@string/pd90" />
<item
android:id="@+id/action_russian"
android:title="@string/russian" />
android:id="@+id/action_force_pd120"
android:title="@string/pd120" />
<item
android:id="@+id/action_german"
android:title="@string/german" />
android:id="@+id/action_force_pd160"
android:title="@string/pd160" />
<item
android:id="@+id/action_brazilian_portuguese"
android:title="@string/brazilian_portuguese" />
android:id="@+id/action_force_pd180"
android:title="@string/pd180" />
<item
android:id="@+id/action_polish"
android:title="@string/polish" />
<item
android:id="@+id/action_ukrainian"
android:title="@string/ukrainian" />
<item
android:id="@+id/action_latin_american_spanish"
android:title="@string/latin_american_spanish" />
<item
android:id="@+id/action_french"
android:title="@string/french" />
android:id="@+id/action_force_pd240"
android:title="@string/pd240" />
</menu>
</item>
<item android:title="@string/martin_modes">
<menu>
<item
android:id="@+id/action_force_martin1"
android:title="@string/martin1" />
<item
android:id="@+id/action_force_martin2"
android:title="@string/martin2" />
</menu>
</item>
<item android:title="@string/scottie_modes">
<menu>
<item
android:id="@+id/action_force_scottie1"
android:title="@string/scottie1" />
<item
android:id="@+id/action_force_scottie2"
android:title="@string/scottie2" />
<item
android:id="@+id/action_force_scottie_dx"
android:title="@string/scottie_dx" />
</menu>
</item>
<item android:title="@string/wraase_modes">
<menu>
<item
android:id="@+id/action_force_wraase_sc2_180"
android:title="@string/wraase_sc2_180" />
</menu>
</item>
<item
android:id="@+id/action_privacy_policy"
android:icon="@drawable/baseline_policy_24"
android:title="@string/privacy_policy"
app:iconTint="@color/tint" />
<item
android:id="@+id/action_about"
android:icon="@drawable/baseline_info_24"
android:title="@string/about"
app:iconTint="@color/tint" />
</menu>
</item>
<item android:title="@string/audio_settings">
<menu>
<item android:title="@string/sample_rate">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/action_set_record_rate_8000"
android:title="@string/rate_8000" />
<item
android:id="@+id/action_set_record_rate_16000"
android:title="@string/rate_16000" />
<item
android:id="@+id/action_set_record_rate_32000"
android:title="@string/rate_32000" />
<item
android:id="@+id/action_set_record_rate_44100"
android:title="@string/rate_44100" />
<item
android:id="@+id/action_set_record_rate_48000"
android:title="@string/rate_48000" />
</group>
</menu>
</item>
<item android:title="@string/channel_select">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/action_set_record_channel_default"
android:title="@string/channel_default" />
<item
android:id="@+id/action_set_record_channel_first"
android:title="@string/channel_first" />
<item
android:id="@+id/action_set_record_channel_second"
android:title="@string/channel_second" />
<item
android:id="@+id/action_set_record_channel_summation"
android:title="@string/channel_summation" />
<item
android:id="@+id/action_set_record_channel_analytic"
android:title="@string/channel_analytic" />
</group>
</menu>
</item>
<item android:title="@string/audio_source">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/action_set_source_default"
android:title="@string/source_default" />
<item
android:id="@+id/action_set_source_microphone"
android:title="@string/source_microphone" />
<item
android:id="@+id/action_set_source_camcorder"
android:title="@string/source_camcorder" />
<item
android:id="@+id/action_set_source_voice_recognition"
android:title="@string/source_voice_recognition" />
<item
android:id="@+id/action_set_source_unprocessed"
android:title="@string/source_unprocessed" />
</group>
</menu>
</item>
</menu>
</item>
<item android:title="@string/night_mode">
<menu>
<item
android:id="@+id/action_enable_night_mode"
android:title="@string/enable" />
<item
android:id="@+id/action_disable_night_mode"
android:title="@string/disable" />
</menu>
</item>
<item
android:id="@+id/action_privacy_policy"
android:title="@string/privacy_policy" />
<item
android:id="@+id/action_about"
android:title="@string/about" />
</menu>

View file

@ -1 +0,0 @@
unqualifiedResLocale=en-US

View file

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="language">Sprache</string>
<string name="share">Teilen</string>
<string name="more_options">Weitere Optionen</string>
<string name="store_scope">Schirm Speichern</string>
<string name="toggle_mode">Modus umschalten</string>
<string name="auto_mode">Automatikmodus</string>
<string name="lock_mode">Sperrmodus</string>
<string name="raw_mode">Rohmodus</string>
<string name="listening">Aufnahme Läuft</string>
<string name="audio_settings">Audioeinstellungen</string>
<string name="sample_rate">Abtastrate</string>
<string name="channel_select">Kanalwahl</string>
<string name="channel_default">Standard</string>
<string name="channel_first">Erste</string>
<string name="channel_second">Zweite</string>
<string name="channel_summation">Summierung</string>
<string name="channel_analytic">Analytisch</string>
<string name="audio_source">Audioquelle</string>
<string name="source_default">Standard</string>
<string name="source_microphone">Mikrofon</string>
<string name="source_camcorder">Videokamera</string>
<string name="source_voice_recognition">Spracherkennung</string>
<string name="source_unprocessed">Unbearbeitet</string>
<string name="audio_format">Audioformat</string>
<string name="fixed_point">Festkomma</string>
<string name="floating_point">Gleitkomma</string>
<string name="audio_init_failed">Audioinitialisierung fehlgeschlagen</string>
<string name="audio_setup_failed">Audioeinrichtung fehlgeschlagen</string>
<string name="audio_permission_denied">Audioberechtigung verweigert</string>
<string name="audio_recording_error">Aufnahme fehlgeschlagen</string>
<string name="creating_picture_directory_failed">Erstellen des Bildverzeichnisses fehlgeschlagen</string>
<string name="creating_picture_file_failed">Erstellen der Bilddatei fehlgeschlagen</string>
<string name="storing_picture_failed">Speichern des Bildes fehlgeschlagen</string>
<string name="scope_description">Dekodiertes SSTV-Bild</string>
<string name="peak_meter_description">Spitzenpegel des Audiosignals</string>
<string name="waterfall_plot">Wasserfalldiagramm</string>
<string name="frequency_plot">Frequenzdiagramm</string>
<string name="spectrogram">Spektrogramm</string>
<string name="auto_save">Automatisches Speichern</string>
<string name="night_mode">Nachtmodus</string>
<string name="enable">Aktivieren</string>
<string name="disable">Deaktivieren</string>
<string name="close">Schließen</string>
<string name="privacy_policy">Datenschutzerklärung</string>
<string name="privacy_policy_text"><![CDATA[
<p><h1>Datenschutzerklärung</h1></p>
<p><h5>Zugriff auf das Mikrofon</h5>
Diese App benötigt Zugriff auf das Mikrofon Ihres Geräts, um Slow Scan Television (SSTV)-Signale zu dekodieren.
Das Mikrofon erfasst den Ton, der das SSTV-Signal enthält.
</p>
<p><h5>Datenverarbeitung</h5>
Die App verwendet einen kleinen temporären Puffer im Speicher, um die Audiodaten in Echtzeit zu verarbeiten.
Dieser Puffer wird kontinuierlich mit neuen Daten überschrieben, während die Dekodierung fortschreitet.
Die App speichert nicht den rohen Ton, der vom Mikrofon erfasst wird.
Nur die dekodierten Bilder, die aus dem SSTV-Prozess resultieren, werden auf Ihrem Gerät gespeichert.
</p>
]]></string>
<string name="about">Über Robot36</string>
<string name="about_text"><![CDATA[
<p><h1>Über Robot36 %1$s</h1>Urheberrecht 2024 Ahmet Inan</p>
<p>Bitte lesen Sie den Haftungsausschluss am Ende dieser Seite</p>
<p><h5>Beschreibung</h5>Decodiert Slow-Scan-Fernsehbilder aus Audio</p>
<p><h5>Implementierung</h5><a href="https://github.com/xdsopl/robot36">Robot36 auf GitHub</a><br />BSD Zero Clause Lizenz</p>
<p><h5>Modusspezifikationen</h5><a href="http://www.barberdsp.com/downloads/Dayton%%20Paper.pdf">Dayton Paper</a><br />von JL Barber - 2000</p>
<p><h5>Haftungsausschluss</h5>%2$s</p>
]]></string>
</resources>

View file

@ -1,68 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="language">Idioma</string>
<string name="share">Compartir</string>
<string name="more_options">Mas opciones</string>
<string name="store_scope">Guardar imagen</string>
<string name="toggle_mode">Alternar modo</string>
<string name="auto_mode">Modo automático</string>
<string name="lock_mode">Modo de bloqueo</string>
<string name="raw_mode">Modo Raw</string>
<string name="listening">Escuchando</string>
<string name="audio_settings">Configuraciones de audio</string>
<string name="sample_rate">Tasa de muestreo</string>
<string name="channel_select">Seleccionar canal</string>
<string name="channel_default">Por defecto</string>
<string name="channel_first">Primer canal</string>
<string name="channel_second">Segundo canal</string>
<string name="channel_summation">Suma de canales</string>
<string name="channel_analytic">Canal analítico</string>
<string name="audio_source">Fuente de audio</string>
<string name="source_default">Por defecto</string>
<string name="source_microphone">Microfono</string>
<string name="source_camcorder">Grabadora</string>
<string name="source_voice_recognition">Reconocimiento de voz</string>
<string name="source_unprocessed">Sin procesar</string>
<string name="audio_format">Formato de audio</string>
<string name="fixed_point">Punto Fijo</string>
<string name="floating_point">Punto Flotante</string>
<string name="audio_init_failed">Fallo en iniciar audio</string>
<string name="audio_setup_failed">Fallo en configuración de audio</string>
<string name="audio_permission_denied">Permiso de audio denegado</string>
<string name="audio_recording_error">Error de grabación de audio</string>
<string name="creating_picture_directory_failed">Fallo en crear directorio de imagen</string>
<string name="creating_picture_file_failed">Fallo en crear imagen</string>
<string name="storing_picture_failed">Fallo en guardar imagen</string>
<string name="scope_description">Imagen SSTV decodificada</string>
<string name="peak_meter_description">Nivel de señal de audio máximo</string>
<string name="waterfall_plot">Gráfico de cascada</string>
<string name="frequency_plot">Gráfico de frecuencia</string>
<string name="spectrogram">Espectrograma</string>
<string name="auto_save">Guardado automático</string>
<string name="night_mode">Modo nocturno</string>
<string name="enable">Habilitar</string>
<string name="disable">Deshabilitar</string>
<string name="close">Cerrar</string>
<string name="privacy_policy">Política de privacidad</string>
<string name="privacy_policy_text"><![CDATA[
<p><h1>Política de privacidad</h1></p>
<p><h5>Acceso a micrófono</h5>
Esta aplicación requiere acceso al microfono de su dispositivo para decodificar señales (SSTV) o el micrófono captura el audio que contiene la transmisión SSTV
</p>
<p><h5>Manipulacion de datos</h5>
La aplicación utiliza un pequeño búfer temporal en la memoria para procesar datos de audio en tiempo real.
Este búfer se sobrescribe constantemente con nuevos datos a medida que avanza la decodificación.
La aplicación no almacena el audio sin procesar capturado por el micrófono.
Sólo las imágenes decodificadas resultantes del proceso SSTV se guardan en el almacenamiento de su dispositivo.
</p>
]]></string>
<string name="about">Sobre Robot36</string>
<string name="about_text"><![CDATA[
<p><h1>Robot36 %1$s</h1>Copyright 2024 Ahmet Inan</p>
<p>Por favor lea el AVISO al final de esta página.</p>
<p><h5>Descripción</h5>Decodifica imágenes de televisión de escaneo lento a partir del audio</p>
<p><h5>Código fuente</h5><a href="https://github.com/xdsopl/robot36">Robot36 en GitHub</a><br />BSD Zero Clause License</p>
<p><h5>Especificaciones técnicas</h5><a href="http://www.barberdsp.com/downloads/Dayton%%20Paper.pdf">Dayton Paper</a><br />por JL Barber - 2000</p>
<p><h5>AVISO</h5>%2$s</p>
]]></string>
</resources>

View file

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="language">Langue</string>
<string name="share">Partager</string>
<string name="more_options">Plus d\'options</string>
<string name="store_scope">Enregistrer écran scope</string>
<string name="toggle_mode">Changer de mode</string>
<string name="auto_mode">Mode automatique</string>
<string name="lock_mode">Mode verrouillé</string>
<string name="raw_mode">Mode brut</string>
<string name="listening">Écoute</string>
<string name="audio_settings">Paramètres audio</string>
<string name="sample_rate">Fréquence d\'échantillonnage</string>
<string name="channel_select">Sélection du canal</string>
<string name="channel_default">Par défaut</string>
<string name="channel_first">Premier</string>
<string name="channel_second">Deuxième</string>
<string name="channel_summation">Somme</string>
<string name="channel_analytic">Analytique</string>
<string name="audio_source">Source audio</string>
<string name="source_default">Par défaut</string>
<string name="source_microphone">Microphone</string>
<string name="source_camcorder">Caméscope</string>
<string name="source_voice_recognition">Reconnaissance vocale</string>
<string name="source_unprocessed">Non traité</string>
<string name="audio_format">Format audio</string>
<string name="fixed_point">Virgule fixe</string>
<string name="floating_point">Virgule flottante</string>
<string name="audio_init_failed">Échec de l\'initialisation audio</string>
<string name="audio_setup_failed">Échec de la configuration audio</string>
<string name="audio_permission_denied">Permission audio refusée</string>
<string name="audio_recording_error">Erreur d\'enregistrement audio</string>
<string name="creating_picture_directory_failed">Échec de la création du dossier d\'images</string>
<string name="creating_picture_file_failed">Échec de la création du fichier image</string>
<string name="storing_picture_failed">Échec de l\'enregistrement de l\'image</string>
<string name="scope_description">Image SSTV décodée</string>
<string name="peak_meter_description">Niveau de signal audio de crête</string>
<string name="waterfall_plot">Graphique en cascade</string>
<string name="frequency_plot">Graphique de fréquence</string>
<string name="spectrogram">Spectrogramme</string>
<string name="auto_save">Sauvegarde automatique</string>
<string name="night_mode">Mode nuit</string>
<string name="enable">Activer</string>
<string name="disable">Désactiver</string>
<string name="close">Fermer</string>
<string name="privacy_policy">Politique de confidentialité</string>
<string name="privacy_policy_text"><![CDATA[
<p><h1>Politique de confidentialité</h1></p>
<p><h5>Accès au microphone</h5>
Cette application nécessite l\'accès au microphone de votre appareil pour décoder les signaux de télévision à balayage lent (SSTV).
Le microphone capture l\'audio contenant la transmission SSTV.
</p>
<p><h5>Gestion des données</h5>
L\'application utilise un petit tampon temporaire en mémoire pour traiter les données audio en temps réel.
Ce tampon est constamment réécrit avec de nouvelles données au fur et à mesure que le décodage progresse.
L\'application ne stocke pas les données audio brutes capturées par le microphone.
Seules les images décodées résultant du processus SSTV sont enregistrées sur le votre appareil.
</p>
]]></string>
<string name="about">À propos de Robot36</string>
<string name="about_text"><![CDATA[
<p><h1>Robot36 %1$s</h1>Copyright 2024 Ahmet Inan</p>
<p>Veuillez lire la CLAUSE DE NON-RESPONSABILITÉ en bas de cette page</p>
<p><h5>Description</h5>Décode les images de télévision à balayage lent à partir de l\'audio</p>
<p><h5>Implémentation</h5><a href="https://github.com/xdsopl/robot36">Robot36 sur GitHub</a><br />Licence BSD Zero Clause</p>
<p><h5>Spécifications des modes</h5><a href="http://www.barberdsp.com/downloads/Dayton%%20Paper.pdf">Dayton Paper</a><br />par JL Barber - 2000</p>
<p><h5>CLAUSE DE NON-RESPONSABILITÉ</h5>%2$s</p>
]]></string>
</resources>

View file

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="language">Język</string>
<string name="share">Udostępnij</string>
<string name="more_options">Więcej opcji</string>
<string name="store_scope">Zapisz ekran</string>
<string name="toggle_mode">Tryb przełączania</string>
<string name="auto_mode">Tryb automatyczny</string>
<string name="lock_mode">Zablokuj tryb</string>
<string name="raw_mode">Tryb surowy (RAW)</string>
<string name="listening">Słuchanie</string>
<string name="audio_settings">Ustawienia dźwięku</string>
<string name="sample_rate">Częstotliwość próbkowania</string>
<string name="channel_select">Wybór kanału</string>
<string name="channel_default">Domyślny</string>
<string name="channel_first">Pierwszy</string>
<string name="channel_second">Drugi</string>
<string name="channel_summation">Sumarycznie</string>
<string name="channel_analytic">Analitycznie</string>
<string name="audio_source">Źródło dźwięku</string>
<string name="source_default">Domyślne</string>
<string name="source_microphone">Mikrofon</string>
<string name="source_camcorder">Kamera</string>
<string name="source_voice_recognition">Rozpoznawanie głosu</string>
<string name="source_unprocessed">Nieprzetworzone</string>
<string name="audio_format">Format dźwięku</string>
<string name="fixed_point">Stałoprzecinkowy</string>
<string name="floating_point">Zmiennoprzecinkowy</string>
<string name="audio_init_failed">Inicjowanie dźwięku nie powiodło się</string>
<string name="audio_setup_failed">Konfiguracja dźwięku nie powiodła się</string>
<string name="audio_permission_denied">Odmowa dostępu do dźwięku</string>
<string name="audio_recording_error">Błąd nagrywania dźwięku</string>
<string name="creating_picture_directory_failed">Tworzenie katalogu obrazów nie powiodło się</string>
<string name="creating_picture_file_failed">Tworzenie pliku obrazu nie powiodło się</string>
<string name="storing_picture_failed">Zapisywanie obrazu nie powiodło się</string>
<string name="scope_description">Zdekodowano obraz SSTV</string>
<string name="peak_meter_description">Szczytowy poziom sygnału audio</string>
<string name="waterfall_plot">Wykres wodospadowy</string>
<string name="frequency_plot">Wykres częstotliwości</string>
<string name="spectrogram">Spektrogram</string>
<string name="auto_save">Automatyczne zapisywanie</string>
<string name="night_mode">Tryb nocny</string>
<string name="enable">Włącz</string>
<string name="disable">Wyłącz</string>
<string name="close">Zamknij</string>
<string name="privacy_policy">Polityka prywatności</string>
<string name="privacy_policy_text"><![CDATA[
<p><h1>Polityka prywatności</h1></p>
<p><h5>Dostęp do mikrofonu</h5>
Ta aplikacja wymaga dostępu do mikrofonu urządzenia, aby dekodować sygnały telewizji o wolnym skanowaniu (SSTV).
Mikrofon przechwytuje dźwięk zawierający transmisję SSTV.
</p>
<p><h5>Obchodzenie się z danymi</h5>
Aplikacja wykorzystuje mały tymczasowy bufor w pamięci do przetwarzania danych audio w czasie rzeczywistym.
W miarę postępu dekodowania bufor ten jest stale nadpisywany nowymi danymi.
Aplikacja nie przechowuje nieprzetworzonego dźwięku przechwyconego z mikrofonu.
Tylko zdekodowane obrazy powstałe w procesie dekodowania SSTV są zapisywane do pamięci twojego urządzenia.
</p>
]]></string>
<string name="about">O Robot36</string>
<string name="about_text"><![CDATA[
<p><h1>Robot36 %1$s</h1>Prawa autorskie 2024 Ahmet Inan</p>
<p>Przeczytaj ZASTRZEŻENIA na dole tej strony</p>
<p><h5>Opis</h5>Dekodowanie sygnałów telewizji o wolnym skanowaniu (SSTV) z dźwięku</p>
<p><h5>Implementacja</h5><a href="https://github.com/xdsopl/robot36">Robot36 na GitHub</a><br />BSD Zero Clause License</p>
<p><h5>Specyfikacje trybów</h5><a href="http://www.barberdsp.com/downloads/Dayton%%20Paper.pdf">Dayton Paper</a><br />by JL Barber - 2000</p>
<p><h5>ZASTRZEŻENIA</h5>%2$s</p>
]]></string>
</resources>

View file

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="language">Idioma</string>
<string name="share">Compartilhar</string>
<string name="more_options">Mais opções</string>
<string name="store_scope">Salvar imagem</string>
<string name="toggle_mode">Alternar modo</string>
<string name="auto_mode">Modo automático</string>
<string name="lock_mode">Modo de bloqueio</string>
<string name="raw_mode">Modo Raw</string>
<string name="listening">Ouvindo</string>
<string name="audio_settings">Configurações de áudio</string>
<string name="sample_rate">Taxa de amostragem</string>
<string name="channel_select">Selecionar canal</string>
<string name="channel_default">Padrão</string>
<string name="channel_first">Primeiro</string>
<string name="channel_second">Segundo</string>
<string name="channel_summation">Somatório</string>
<string name="channel_analytic">Analítico</string>
<string name="audio_source">Fonte de áudio</string>
<string name="source_default">Padrão</string>
<string name="source_microphone">Microfone</string>
<string name="source_camcorder">Filmadora</string>
<string name="source_voice_recognition">Reconhecimento de voz</string>
<string name="source_unprocessed">Não processado</string>
<string name="audio_format">Formato de Áudio</string>
<string name="fixed_point">Ponto Fixo</string>
<string name="floating_point">Ponto Flutuante</string>
<string name="audio_init_failed">Falha ao iniciar o áudio</string>
<string name="audio_setup_failed">Falha na configuração de áudio</string>
<string name="audio_permission_denied">Permissão de áudio negada</string>
<string name="audio_recording_error">Erro de gravação de áudio</string>
<string name="creating_picture_directory_failed">Falha ao criar o diretório de imagem</string>
<string name="creating_picture_file_failed">Falha ao criar imagem</string>
<string name="storing_picture_failed">Falha ao salvar imagem</string>
<string name="scope_description">Imagem SSTV decodificada</string>
<string name="peak_meter_description">Nível de sinal de áudio máximo</string>
<string name="waterfall_plot">Gráfico de cascata</string>
<string name="frequency_plot">Gráfico de frequência</string>
<string name="spectrogram">Espectrograma</string>
<string name="auto_save">Salvamento automático</string>
<string name="night_mode">Modo noturno</string>
<string name="enable">Habilitar</string>
<string name="disable">Desabilitar</string>
<string name="close">Fechar</string>
<string name="privacy_policy">Política de privacidade</string>
<string name="privacy_policy_text"><![CDATA[
<p><h1>Política de privacidade</h1></p>
<p><h5>Acesso ao microfone</h5>
Este aplicativo requer acesso ao microfone do seu dispositivo para decodificar sinais de televisão de varredura lenta (SSTV).
O microfone captura o áudio contendo a transmissão SSTV.
</p>
<p><h5>Manipulação de dados</h5>
O aplicativo utiliza um pequeno buffer temporário na memória para processar os dados de áudio em tempo real.
Este buffer é constantemente sobrescrito com novos dados à medida que a decodificação avança.
O aplicativo não armazena o áudio bruto capturado pelo microfone.
Apenas as imagens decodificadas resultantes do processo SSTV são salvas no armazenamento do seu dispositivo.
</p>
]]></string>
<string name="about">Sobre Robot36</string>
<string name="about_text"><![CDATA[
<p><h1>Robot36 %1$s</h1>Copyright 2024 Ahmet Inan</p>
<p>Por favor, leia o AVISO no final desta página.</p>
<p><h5>Descrição</h5>Decodifica imagens de televisão de varredura lenta a partir de áudio</p>
<p><h5>Código fonte</h5><a href="https://github.com/xdsopl/robot36">Robot36 no GitHub</a><br />BSD Zero Clause License</p>
<p><h5>Especificação técnica</h5><a href="http://www.barberdsp.com/downloads/Dayton%%20Paper.pdf">Dayton Paper</a><br />por JL Barber - 2000</p>
<p><h5>AVISO</h5>%2$s</p>
]]></string>
</resources>

View file

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="language">Язык</string>
<string name="share">Поделиться</string>
<string name="more_options">Ещё</string>
<string name="store_scope">Сохранить экран</string>
<string name="toggle_mode">Зафиксировать режим</string>
<string name="auto_mode">Автоматический</string>
<string name="lock_mode">Выбор режима</string>
<string name="raw_mode">Без обработки</string>
<string name="listening">Слушаю</string>
<string name="audio_settings">Настройки аудио</string>
<string name="sample_rate">Частота дискретизации</string>
<string name="channel_select">Выбор канала</string>
<string name="channel_default">Стандартный</string>
<string name="channel_first">Первый</string>
<string name="channel_second">Второй</string>
<string name="channel_summation">Общий</string>
<string name="channel_analytic">Анализ</string>
<string name="audio_source">Источник аудио</string>
<string name="source_default">Стандартный</string>
<string name="source_microphone">Микрофон</string>
<string name="source_camcorder">Камера</string>
<string name="source_voice_recognition">Распознавание голоса</string>
<string name="source_unprocessed">Необработанный</string>
<string name="audio_format">Аудио формат</string>
<string name="fixed_point">Фиксированная точка</string>
<string name="floating_point">Плавающая точка</string>
<string name="audio_init_failed">Ошибка инициализации аудио</string>
<string name="audio_setup_failed">Ошибка настройки аудио</string>
<string name="audio_permission_denied">Аудио - отказано в доступе</string>
<string name="audio_recording_error">Ошибка записи аудио</string>
<string name="creating_picture_directory_failed">Ошибка создания каталога для изображений</string>
<string name="creating_picture_file_failed">Ошибка создания файла изображения</string>
<string name="storing_picture_failed">Ошибка сохранения изображения</string>
<string name="scope_description">Декодированное изображение SSTV</string>
<string name="peak_meter_description">Пиковый уровень аудиосигнала</string>
<string name="waterfall_plot">Водопадный график</string>
<string name="frequency_plot">График частот</string>
<string name="spectrogram">Спектрограмма</string>
<string name="auto_save">Автосохранение</string>
<string name="night_mode">Ночной режим</string>
<string name="enable">Включить</string>
<string name="disable">Выключить</string>
<string name="close">Закрыть</string>
<string name="privacy_policy">Политика приватности</string>
<string name="privacy_policy_text"><![CDATA[
<p><h1>Политика приватности</h1></p>
<p><h5>Доступ к микрофону</h5>
Это приложение требует доступ к микрофону вашего устройства для декодирования сигналов телевидения с медленной развёрткой (SSTV).
Микрофон записывает аудио, содержащее передачу SSTV.
</p>
<p><h5>Обработка данных</h5>
Приложение использует небольшой временный буфер в памяти для обработки аудиоданных в реальном времени.
Этот буфер постоянно перезаписывается новыми данными по мере продвижения декодирования.
Приложение не сохраняет необработанное аудио, записанное с микрофона.
На устройстве сохраняются только изображения, полученные в результате декодирования сигнала SSTV.
</p>
]]></string>
<string name="about">О приложении Robot36</string>
<string name="about_text"><![CDATA[
<p><h1>Robot36 %1$s</h1>Авторские права 2024 Ahmet Inan</p>
<p>Пожалуйста, ознакомьтесь с ОТКАЗОМ ОТ ОТВЕТСТВЕННОСТИ внизу этой страницы</p>
<p><h5>Описание</h5>Декодирует изображения телевидения с медленной развёрткой из аудиосигнала</p>
<p><h5>Реализация</h5><a href="https://github.com/xdsopl/robot36">Robot36 на GitHub</a><br />Лицензия BSD Zero Clause</p>
<p><h5>Спецификации режимов</h5><a href="http://www.barberdsp.com/downloads/Dayton%%20Paper.pdf">Dayton Paper</a><br />автор JL Barber - 2000</p>
<p><h5>ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ</h5>%2$s</p>
]]></string>
</resources>

View file

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="language">Мова</string>
<string name="share">Поділитись</string>
<string name="more_options">Більше</string>
<string name="store_scope">Зберегти екран</string>
<string name="toggle_mode">Зафіксувати режим</string>
<string name="auto_mode">Автоматичний</string>
<string name="lock_mode">Вибір режиму</string>
<string name="raw_mode">Без обробок</string>
<string name="listening">Слухаю</string>
<string name="audio_settings">Параметри аудіо</string>
<string name="sample_rate">Частота дискретизації</string>
<string name="channel_select">Вибір каналу</string>
<string name="channel_default">Стандартний</string>
<string name="channel_first">Перший</string>
<string name="channel_second">Другий</string>
<string name="channel_summation">Спільний</string>
<string name="channel_analytic">Аналіз</string>
<string name="audio_source">Джерело аудіо</string>
<string name="source_default">Стандартний</string>
<string name="source_microphone">Мікрофон</string>
<string name="source_camcorder">Камера</string>
<string name="source_voice_recognition">Розпізнавання голосу</string>
<string name="source_unprocessed">Необроблений</string>
<string name="audio_format">Формат аудіо</string>
<string name="fixed_point">Фіксована точка</string>
<string name="floating_point">Плаваюча точка</string>
<string name="audio_init_failed">Помилка ініціалізації аудіо</string>
<string name="audio_setup_failed">Помилка налаштування аудіо</string>
<string name="audio_permission_denied">Аудіо - відмовлено у доступі</string>
<string name="audio_recording_error">Помилка запису аудіо</string>
<string name="creating_picture_directory_failed">Помилка створення каталогу для зображень</string>
<string name="creating_picture_file_failed">Помилка створення файлу зображення</string>
<string name="storing_picture_failed">Помилка збереження зображення</string>
<string name="scope_description">Декодоване зображення SSTV</string>
<string name="peak_meter_description">Піковий рівень аудіосигналу</string>
<string name="waterfall_plot">Графік водоспаду</string>
<string name="frequency_plot">Графік частот</string>
<string name="spectrogram">Спектрограма</string>
<string name="auto_save">Автозбереження</string>
<string name="night_mode">Нічний режим</string>
<string name="enable">Увімкнути</string>
<string name="disable">Вимкнути</string>
<string name="close">Закрити</string>
<string name="privacy_policy">Політика приватності</string>
<string name="privacy_policy_text"><![CDATA[
<p><h1>Політика приватності</h1></p>
<p><h5>Доступ до мікрофону</h5>
Ця програма вимагає доступу до мікрофону вашого пристрою для декодування сигналів телебачення з повільною розгорткою (SSTV).
Мікрофон записує аудіо, що містить передачу SSTV.
</p>
<p><h5>Обробка даних</h5>
Додаток використовує невеликий тимчасовий буфер у пам\'яті для обробки аудіоданих у реальному часі.
Цей буфер постійно перезаписується новими даними у міру просування декодування.
Програма не зберігає необроблене аудіо, записане з мікрофона.
На пристрої зберігаються лише зображення, отримані внаслідок декодування сигналу SSTV.
</p>
]]></string>
<string name="about">Про застосунок Robot36</string>
<string name="about_text"><![CDATA[
<p><h1>Robot36 %1$s</h1>Авторські права 2024 Ahmet Inan</p>
<p>Будь ласка, ознайомтеся з ВІДМОВОЮ ВІД ВІДПОВІДАЛЬНОСТІ внизу цієї сторінки</p>
<p><h5>Опис</h5>Декодує зображення телебачення з повільною розгорткою з аудіосигналу</p>
<p><h5>Реалізація</h5><a href="https://github.com/xdsopl/robot36">Robot36 на Ґітхаб</a><br />Ліцензія BSD Zero Clause</p>
<p><h5>Специфікації режимів</h5><a href="http://www.barberdsp.com/downloads/Dayton%%20Paper.pdf">Dayton Paper</a><br />автор JL Barber - 2000</p>
<p><h5>ВІДМОВА ВІД ВІДПОВІДАЛЬНОСТІ</h5>%2$s</p>
]]></string>
</resources>

View file

@ -1,70 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="language">语言</string>
<string name="share">分享</string>
<string name="more_options">更多选项</string>
<string name="store_scope">保存图像</string>
<string name="toggle_mode">切换模式</string>
<string name="auto_mode">自动解码</string>
<string name="lock_mode">锁定解码模式</string>
<string name="raw_mode">原始模式</string>
<string name="listening">监听中</string>
<string name="audio_settings">音频设置</string>
<string name="sample_rate">采样率</string>
<string name="channel_select">通道选择</string>
<string name="channel_default">默认</string>
<string name="channel_first">第一</string>
<string name="channel_second">第二</string>
<string name="channel_summation">总和</string>
<string name="channel_analytic">分析</string>
<string name="audio_source">音源</string>
<string name="source_default">默认</string>
<string name="source_microphone">麦克风</string>
<string name="source_camcorder">摄录机</string>
<string name="source_voice_recognition">音频识别</string>
<string name="source_unprocessed">不处理</string>
<string name="audio_format">音频格式</string>
<string name="fixed_point">定点数</string>
<string name="floating_point">浮点数</string>
<string name="audio_init_failed">初始化音频失败</string>
<string name="audio_setup_failed">音频设定失败</string>
<string name="audio_permission_denied">音频权限被拒绝</string>
<string name="audio_recording_error">音频录制出错</string>
<string name="creating_picture_directory_failed">创建图片文件夹出错</string>
<string name="creating_picture_file_failed">创建图片文件出错</string>
<string name="storing_picture_failed">写入图像数据失败</string>
<string name="scope_description">解码的SSTV图像</string>
<string name="peak_meter_description">音频峰值信号水平</string>
<string name="waterfall_plot">瀑布图</string>
<string name="frequency_plot">频率图</string>
<string name="spectrogram">频谱图</string>
<string name="auto_save">自动保存</string>
<string name="night_mode">夜间模式</string>
<string name="enable">开启</string>
<string name="disable">禁用</string>
<string name="close">关闭</string>
<string name="privacy_policy">隐私策略</string>
<string name="privacy_policy_text"><![CDATA[
<p><h1>隐私政策</h1></p>
<p><h5>麦克风访问</h5>
该应用程序需要访问您设备的麦克风以解码慢扫描电视SSTV信号。
麦克风捕获包含SSTV传输的音频。
</p>
<p><h5>数据处理</h5>
该应用程序使用内存中的小型临时缓冲区来实时处理音频数据。
随着解码的进行,此缓冲区将不断被新数据覆盖。
该应用不会存储从麦克风捕获的原始音频。
仅会保存通过SSTV过程产生的解码图像在您设备的存储空间中。
</p>
]]></string>
<string name="about">关于Robot36</string>
<string name="about_text"><![CDATA[
<p><h1>Robot36 %1$s</h1>版权所有 © 2024 Ahmet Inan</p>
<p>请阅读本页面底部的免责声明</p>
<p><h5>描述</h5>从音频解码慢扫描电视图像</p>
<p><h5>实现</h5><a href="https://github.com/xdsopl/robot36">Robot36 GitHub</a><br />BSD Zero Clause 许可证</p>
<p><h5>免责声明</h5>请注意,本应用程序仅用于个人用途。任何商业用途需谨慎考虑。</p>
<p><h5>模式规范</h5><a href="http://www.barberdsp.com/downloads/Dayton%%20Paper.pdf">Dayton Paper</a><br />作者JL Barber - 2000年</p>
<p><h5>免责声明</h5>%2$s</p>
]]></string>
</resources>

View file

@ -1,54 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">Robot36</string>
<string name="robot_modes" translatable="false">Robot</string>
<string name="pd_modes" translatable="false">PD</string>
<string name="martin_modes" translatable="false">Martin</string>
<string name="scottie_modes" translatable="false">Scottie</string>
<string name="wraase_modes" translatable="false">Wraase</string>
<string name="contributed_modes" translatable="false">Contrib</string>
<string name="robot36_color" translatable="false">Robot 36 Color</string>
<string name="robot72_color" translatable="false">Robot 72 Color</string>
<string name="pd50" translatable="false">PD 50</string>
<string name="pd90" translatable="false">PD 90</string>
<string name="pd120" translatable="false">PD 120</string>
<string name="pd160" translatable="false">PD 160</string>
<string name="pd180" translatable="false">PD 180</string>
<string name="pd240" translatable="false">PD 240</string>
<string name="pd290" translatable="false">PD 290</string>
<string name="martin1" translatable="false">Martin 1</string>
<string name="martin2" translatable="false">Martin 2</string>
<string name="scottie1" translatable="false">Scottie 1</string>
<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>
<string name="rate_44100" translatable="false">44.1 kHz</string>
<string name="rate_48000" translatable="false">48 kHz</string>
<string name="english" translatable="false">English</string>
<string name="simplified_chinese" translatable="false">简体中文</string>
<string name="russian" translatable="false">Русский</string>
<string name="german" translatable="false">Deutsch</string>
<string name="brazilian_portuguese" translatable="false">Português brasileiro</string>
<string name="polish" translatable="false">Polski</string>
<string name="ukrainian" translatable="false">Українська</string>
<string name="latin_american_spanish" translatable="false">Español de América Latina</string>
<string name="french" translatable="false">Français</string>
<string name="disclaimer" translatable="false">THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.</string>
<string name="language">Language</string>
<string name="app_name">Robot36</string>
<string name="share">Share</string>
<string name="more_options">More Options</string>
<string name="store_scope">Store Scope</string>
<string name="toggle_mode">Toggle Mode</string>
<string name="auto_mode">Auto Mode</string>
<string name="lock_mode">Lock Mode</string>
<string name="force_mode">Force Mode</string>
<string name="robot_modes">Robot Modes</string>
<string name="pd_modes">PD Modes</string>
<string name="martin_modes">Martin Modes</string>
<string name="scottie_modes">Scottie Modes</string>
<string name="wraase_modes">Wraase Modes</string>
<string name="raw_mode">Raw Mode</string>
<string name="robot36_color">Robot 36 Color</string>
<string name="robot72_color">Robot 72 Color</string>
<string name="pd50">PD 50</string>
<string name="pd90">PD 90</string>
<string name="pd120">PD 120</string>
<string name="pd160">PD 160</string>
<string name="pd180">PD 180</string>
<string name="pd240">PD 240</string>
<string name="martin1">Martin 1</string>
<string name="martin2">Martin 2</string>
<string name="scottie1">Scottie 1</string>
<string name="scottie2">Scottie 2</string>
<string name="scottie_dx">Scottie DX</string>
<string name="wraase_sc2_180">Wraase SC2180</string>
<string name="listening">Listening</string>
<string name="audio_settings">Audio Settings</string>
<string name="sample_rate">Sample Rate</string>
<string name="rate_8000">8 kHz</string>
<string name="rate_16000">16 kHz</string>
<string name="rate_32000">32 kHz</string>
<string name="rate_44100">44.1 kHz</string>
<string name="rate_48000">48 kHz</string>
<string name="channel_select">Channel Select</string>
<string name="channel_default">Default</string>
<string name="channel_first">First</string>
@ -61,9 +44,6 @@
<string name="source_camcorder">Camcorder</string>
<string name="source_voice_recognition">Voice Recognition</string>
<string name="source_unprocessed">Unprocessed</string>
<string name="audio_format">Audio Format</string>
<string name="fixed_point">Fixed Point</string>
<string name="floating_point">Floating Point</string>
<string name="audio_init_failed">Audio init failed</string>
<string name="audio_setup_failed">Audio setup failed</string>
<string name="audio_permission_denied">Audio permission denied</string>
@ -72,11 +52,8 @@
<string name="creating_picture_file_failed">Creating picture file failed</string>
<string name="storing_picture_failed">Storing picture failed</string>
<string name="scope_description">Decoded SSTV picture</string>
<string name="freq_plot_description">Frequency plot</string>
<string name="peak_meter_description">Peak audio signal level</string>
<string name="waterfall_plot">Waterfall plot</string>
<string name="frequency_plot">Frequency plot</string>
<string name="spectrogram">Spectrogram</string>
<string name="auto_save">Auto Save</string>
<string name="night_mode">Night Mode</string>
<string name="enable">Enable</string>
<string name="disable">Disable</string>
@ -91,8 +68,8 @@ The microphone captures the audio containing the SSTV transmission.
<p><h5>Data Handling</h5>
The app uses a small temporary buffer in memory to process the audio data in real-time.
This buffer is constantly overwritten with new data as the decoding progresses.
The app does not store the raw audio captured from the microphone.
Only the decoded images resulting from the SSTV process are saved on your device\'s storage.
The app <b>does not</b> store the raw audio captured from the microphone.
Only the <b>decoded images</b> resulting from the SSTV process are saved on your device\'s storage.
</p>
]]></string>
<string name="about">About Robot36</string>
@ -101,7 +78,7 @@ Only the decoded images resulting from the SSTV process are saved on your device
<p>Please read DISCLAIMER at the bottom of this page</p>
<p><h5>Description</h5>Decodes Slow Scan Television images from audio</p>
<p><h5>Implementation</h5><a href="https://github.com/xdsopl/robot36">Robot36 on GitHub</a><br />BSD Zero Clause License</p>
<p><h5>Mode specifications</h5><a href="http://www.barberdsp.com/downloads/Dayton%%20Paper.pdf">Dayton Paper</a><br />by JL Barber - 2000</p>
<p><h5>DISCLAIMER</h5>%2$s</p>
<p><h5>Mode specifications</h5><a href="http://www.barberdsp.com/downloads/Dayton%%20Paper.pdf">Dayton Paper</a><br />by JL Barber - 2000<br /></p>
<p><h5>DISCLAIMER</h5>THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.</p>
]]></string>
</resources>

View file

@ -1,7 +1,7 @@
Following SSTV modes are supported:
<ul><li>Robot Modes: 36 & 72</li>
<li>PD Modes: 50, 90, 120, 160, 180, 240 & 290</li>
<li>PD Modes: 50, 90, 120, 160, 180 & 240</li>
<li>Martin Modes: 1 & 2</li>
<li>Scottie Modes: 1, 2 & DX</li>
<li>Wraase Mode: SC2-180</li></ul>

View file

@ -1,12 +1,12 @@
[versions]
agp = "8.13.1"
agp = "8.3.2"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
appcompat = "1.7.1"
material = "1.13.0"
activity = "1.11.0"
constraintlayout = "2.2.1"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
appcompat = "1.6.1"
material = "1.11.0"
activity = "1.8.0"
constraintlayout = "2.1.4"
[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }

View file

@ -1,6 +1,6 @@
#Fri Apr 12 11:35:07 CEST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists