mirror of
https://github.com/xdsopl/robot36.git
synced 2025-12-06 07:12:07 +01:00
Compare commits
41 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75146a5342 | ||
|
|
716fc64892 | ||
|
|
a39c180c44 | ||
|
|
265223f43d | ||
|
|
c55ae1b2ca | ||
|
|
03dc577ae8 | ||
|
|
5fee5e0672 | ||
|
|
5b20da27a0 | ||
|
|
1a4ffac26e | ||
|
|
7ad54873da | ||
|
|
1502f20af1 | ||
|
|
1a722d5289 | ||
|
|
b105a415f3 | ||
|
|
cb607c9d4e | ||
|
|
8b12ac6153 | ||
|
|
040655552f | ||
|
|
aaf31ac4a7 | ||
|
|
54381558d1 | ||
|
|
ef3b251a8e | ||
|
|
d61be31811 | ||
|
|
ec302f6800 | ||
|
|
913168adc3 | ||
|
|
771ab0fe0b | ||
|
|
1bd3d05782 | ||
|
|
f081bbddfd | ||
|
|
7f503ecef3 | ||
|
|
8afa86d660 | ||
|
|
ef7e20f3a0 | ||
|
|
728c9df890 | ||
|
|
c93dff432e | ||
|
|
1fbec49583 | ||
|
|
28ebde8e93 | ||
|
|
2c3c6b1293 | ||
|
|
2d74f33bb6 | ||
|
|
07e2f59389 | ||
|
|
0d1e90d613 | ||
|
|
61799452ff | ||
|
|
beb6647909 | ||
|
|
418b1f0f06 | ||
|
|
a15b5b8104 | ||
|
|
a0873b2590 |
45
README.md
45
README.md
|
|
@ -1,9 +1,46 @@
|
||||||
|
|
||||||
### Robot36: The Java Cut
|
# Robot36 - SSTV Image Decoder
|
||||||
|
|
||||||
This is not a drill!
|
## 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.
|
||||||
|
|
||||||
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!
|
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.
|
||||||
|
|
||||||
Stay tuned for further transmissions...
|
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.
|
||||||
|
|
||||||
|
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 1 V, 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 transformer’s 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 transformer’s 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.2 kΩ resistor in series (any value between 1 kΩ and 10 kΩ is fine; a 10 kΩ 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.2 kΩ between the second ring and sleeve (values near 2 kΩ 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.2 kΩ 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.2 kΩ MIC detect resistor (between MIC and GND)
|
||||||
|
* [Line IN], [Line GND]: input from radio/sound card
|
||||||
|
* [Ring 2], [Sleeve]: TRRS plug connections to smartphone
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'xdsopl.robot36'
|
namespace 'xdsopl.robot36'
|
||||||
compileSdk 34
|
compileSdk = 36
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "xdsopl.robot36"
|
applicationId "xdsopl.robot36"
|
||||||
minSdk 24
|
minSdk 24
|
||||||
targetSdk 34
|
targetSdk 36
|
||||||
versionCode 62
|
versionCode 66
|
||||||
versionName "2.12"
|
versionName "2.16"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
app/src/main/java/xdsopl/robot36/BaseMode.java
Normal file
16
app/src/main/java/xdsopl/robot36/BaseMode.java
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,6 +38,7 @@ public class Decoder {
|
||||||
private final int visCodeBitSamples;
|
private final int visCodeBitSamples;
|
||||||
private final int visCodeSamples;
|
private final int visCodeSamples;
|
||||||
private final Mode rawMode;
|
private final Mode rawMode;
|
||||||
|
private final Mode hfFaxMode;
|
||||||
private final ArrayList<Mode> syncPulse5msModes;
|
private final ArrayList<Mode> syncPulse5msModes;
|
||||||
private final ArrayList<Mode> syncPulse9msModes;
|
private final ArrayList<Mode> syncPulse9msModes;
|
||||||
private final ArrayList<Mode> syncPulse20msModes;
|
private final ArrayList<Mode> syncPulse20msModes;
|
||||||
|
|
@ -54,7 +55,7 @@ public class Decoder {
|
||||||
this.scopeBuffer = scopeBuffer;
|
this.scopeBuffer = scopeBuffer;
|
||||||
this.imageBuffer = imageBuffer;
|
this.imageBuffer = imageBuffer;
|
||||||
imageBuffer.line = -1;
|
imageBuffer.line = -1;
|
||||||
pixelBuffer = new PixelBuffer(scopeBuffer.width, 2);
|
pixelBuffer = new PixelBuffer(800, 2);
|
||||||
demodulator = new Demodulator(sampleRate);
|
demodulator = new Demodulator(sampleRate);
|
||||||
double pulseFilterSeconds = 0.0025;
|
double pulseFilterSeconds = 0.0025;
|
||||||
int pulseFilterSamples = (int) Math.round(pulseFilterSeconds * sampleRate) | 1;
|
int pulseFilterSamples = (int) Math.round(pulseFilterSeconds * sampleRate) | 1;
|
||||||
|
|
@ -95,6 +96,7 @@ public class Decoder {
|
||||||
double scanLineToleranceSeconds = 0.001;
|
double scanLineToleranceSeconds = 0.001;
|
||||||
scanLineToleranceSamples = (int) Math.round(scanLineToleranceSeconds * sampleRate);
|
scanLineToleranceSamples = (int) Math.round(scanLineToleranceSeconds * sampleRate);
|
||||||
rawMode = new RawDecoder(rawName, sampleRate);
|
rawMode = new RawDecoder(rawName, sampleRate);
|
||||||
|
hfFaxMode = new HFFax(sampleRate);
|
||||||
Mode robot36 = new Robot_36_Color(sampleRate);
|
Mode robot36 = new Robot_36_Color(sampleRate);
|
||||||
currentMode = robot36;
|
currentMode = robot36;
|
||||||
currentScanLineSamples = robot36.getScanLineSamples();
|
currentScanLineSamples = robot36.getScanLineSamples();
|
||||||
|
|
@ -115,6 +117,7 @@ public class Decoder {
|
||||||
syncPulse20msModes.add(new PaulDon("160", 98, 512, 400, 0.195584, sampleRate));
|
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("180", 96, 640, 496, 0.18304, sampleRate));
|
||||||
syncPulse20msModes.add(new PaulDon("240", 97, 640, 496, 0.24448, 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) {
|
private double scanLineMean(int[] lines) {
|
||||||
|
|
@ -156,7 +159,7 @@ public class Decoder {
|
||||||
|
|
||||||
private Mode findMode(ArrayList<Mode> modes, int code) {
|
private Mode findMode(ArrayList<Mode> modes, int code) {
|
||||||
for (Mode mode : modes)
|
for (Mode mode : modes)
|
||||||
if (mode.getCode() == code)
|
if (mode.getVISCode() == code)
|
||||||
return mode;
|
return mode;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -169,10 +172,11 @@ public class Decoder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void copyUnscaled() {
|
private void copyUnscaled() {
|
||||||
|
int width = Math.min(scopeBuffer.width, pixelBuffer.width);
|
||||||
for (int row = 0; row < pixelBuffer.height; ++row) {
|
for (int row = 0; row < pixelBuffer.height; ++row) {
|
||||||
int line = scopeBuffer.width * scopeBuffer.line;
|
int line = scopeBuffer.width * scopeBuffer.line;
|
||||||
System.arraycopy(pixelBuffer.pixels, row * pixelBuffer.width, scopeBuffer.pixels, line, pixelBuffer.width);
|
System.arraycopy(pixelBuffer.pixels, row * pixelBuffer.width, scopeBuffer.pixels, line, width);
|
||||||
Arrays.fill(scopeBuffer.pixels, line + pixelBuffer.width, line + scopeBuffer.width, 0);
|
Arrays.fill(scopeBuffer.pixels, line + width, line + scopeBuffer.width, 0);
|
||||||
System.arraycopy(scopeBuffer.pixels, line, scopeBuffer.pixels, scopeBuffer.width * (scopeBuffer.line + scopeBuffer.height / 2), scopeBuffer.width);
|
System.arraycopy(scopeBuffer.pixels, line, scopeBuffer.pixels, scopeBuffer.width * (scopeBuffer.line + scopeBuffer.height / 2), scopeBuffer.width);
|
||||||
scopeBuffer.line = (scopeBuffer.line + 1) % (scopeBuffer.height / 2);
|
scopeBuffer.line = (scopeBuffer.line + 1) % (scopeBuffer.height / 2);
|
||||||
}
|
}
|
||||||
|
|
@ -206,7 +210,7 @@ public class Decoder {
|
||||||
finish = imageBuffer.line == imageBuffer.height;
|
finish = imageBuffer.line == imageBuffer.height;
|
||||||
}
|
}
|
||||||
int scale = scopeBuffer.width / pixelBuffer.width;
|
int scale = scopeBuffer.width / pixelBuffer.width;
|
||||||
if (scale == 1)
|
if (scale <= 1)
|
||||||
copyUnscaled();
|
copyUnscaled();
|
||||||
else
|
else
|
||||||
copyScaled(scale);
|
copyScaled(scale);
|
||||||
|
|
@ -330,7 +334,7 @@ public class Decoder {
|
||||||
}
|
}
|
||||||
if (lockMode && mode != currentMode)
|
if (lockMode && mode != currentMode)
|
||||||
return false;
|
return false;
|
||||||
mode.reset();
|
mode.resetState();
|
||||||
imageBuffer.width = mode.getWidth();
|
imageBuffer.width = mode.getWidth();
|
||||||
imageBuffer.height = mode.getHeight();
|
imageBuffer.height = mode.getHeight();
|
||||||
imageBuffer.line = 0;
|
imageBuffer.line = 0;
|
||||||
|
|
@ -344,29 +348,29 @@ public class Decoder {
|
||||||
for (int i = 0; i < pulses.length; ++i)
|
for (int i = 0; i < pulses.length; ++i)
|
||||||
pulses[i] = oldestSyncPulseIndex + i * currentScanLineSamples;
|
pulses[i] = oldestSyncPulseIndex + i * currentScanLineSamples;
|
||||||
Arrays.fill(lines, currentScanLineSamples);
|
Arrays.fill(lines, currentScanLineSamples);
|
||||||
shiftSamples(lastSyncPulseIndex + mode.getBegin());
|
shiftSamples(lastSyncPulseIndex + mode.getFirstPixelSampleIndex());
|
||||||
drawLines(0xff00ff00, 8);
|
drawLines(0xff00ff00, 8);
|
||||||
drawLines(0xff000000, 10);
|
drawLines(0xff000000, 10);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean processSyncPulse(ArrayList<Mode> modes, float[] freqOffs, int[] pulses, int[] lines, int index) {
|
private boolean processSyncPulse(ArrayList<Mode> modes, float[] freqOffs, int[] syncIndexes, int[] lineLengths, int latestSyncIndex) {
|
||||||
for (int i = 1; i < pulses.length; ++i)
|
for (int i = 1; i < syncIndexes.length; ++i)
|
||||||
pulses[i - 1] = pulses[i];
|
syncIndexes[i - 1] = syncIndexes[i];
|
||||||
pulses[pulses.length - 1] = index;
|
syncIndexes[syncIndexes.length - 1] = latestSyncIndex;
|
||||||
for (int i = 1; i < lines.length; ++i)
|
for (int i = 1; i < lineLengths.length; ++i)
|
||||||
lines[i - 1] = lines[i];
|
lineLengths[i - 1] = lineLengths[i];
|
||||||
lines[lines.length - 1] = pulses[pulses.length - 1] - pulses[pulses.length - 2];
|
lineLengths[lineLengths.length - 1] = syncIndexes[syncIndexes.length - 1] - syncIndexes[syncIndexes.length - 2];
|
||||||
for (int i = 1; i < freqOffs.length; ++i)
|
for (int i = 1; i < freqOffs.length; ++i)
|
||||||
freqOffs[i - 1] = freqOffs[i];
|
freqOffs[i - 1] = freqOffs[i];
|
||||||
freqOffs[pulses.length - 1] = demodulator.frequencyOffset;
|
freqOffs[syncIndexes.length - 1] = demodulator.frequencyOffset;
|
||||||
if (lines[0] == 0)
|
if (lineLengths[0] == 0)
|
||||||
return false;
|
return false;
|
||||||
double mean = scanLineMean(lines);
|
double mean = scanLineMean(lineLengths);
|
||||||
int scanLineSamples = (int) Math.round(mean);
|
int scanLineSamples = (int) Math.round(mean);
|
||||||
if (scanLineSamples < scanLineMinSamples || scanLineSamples > scratchBuffer.length)
|
if (scanLineSamples < scanLineMinSamples || scanLineSamples > scratchBuffer.length)
|
||||||
return false;
|
return false;
|
||||||
if (scanLineStdDev(lines, mean) > scanLineToleranceSamples)
|
if (scanLineStdDev(lineLengths, mean) > scanLineToleranceSamples)
|
||||||
return false;
|
return false;
|
||||||
boolean pictureChanged = false;
|
boolean pictureChanged = false;
|
||||||
if (lockMode || imageBuffer.line >= 0 && imageBuffer.line < imageBuffer.height) {
|
if (lockMode || imageBuffer.line >= 0 && imageBuffer.line < imageBuffer.height) {
|
||||||
|
|
@ -377,7 +381,7 @@ public class Decoder {
|
||||||
currentMode = detectMode(modes, scanLineSamples);
|
currentMode = detectMode(modes, scanLineSamples);
|
||||||
pictureChanged = currentMode != prevMode
|
pictureChanged = currentMode != prevMode
|
||||||
|| Math.abs(currentScanLineSamples - scanLineSamples) > scanLineToleranceSamples
|
|| Math.abs(currentScanLineSamples - scanLineSamples) > scanLineToleranceSamples
|
||||||
|| Math.abs(lastSyncPulseIndex + scanLineSamples - pulses[pulses.length - 1]) > syncPulseToleranceSamples;
|
|| Math.abs(lastSyncPulseIndex + scanLineSamples - syncIndexes[syncIndexes.length - 1]) > syncPulseToleranceSamples;
|
||||||
}
|
}
|
||||||
if (pictureChanged) {
|
if (pictureChanged) {
|
||||||
drawLines(0xff000000, 10);
|
drawLines(0xff000000, 10);
|
||||||
|
|
@ -385,23 +389,24 @@ public class Decoder {
|
||||||
drawLines(0xff000000, 10);
|
drawLines(0xff000000, 10);
|
||||||
}
|
}
|
||||||
float frequencyOffset = (float) frequencyOffsetMean(freqOffs);
|
float frequencyOffset = (float) frequencyOffsetMean(freqOffs);
|
||||||
if (pulses[0] >= scanLineSamples && pictureChanged) {
|
if (syncIndexes[0] >= scanLineSamples && pictureChanged) {
|
||||||
int endPulse = pulses[0];
|
int endPulse = syncIndexes[0];
|
||||||
int extrapolate = endPulse / scanLineSamples;
|
int extrapolate = endPulse / scanLineSamples;
|
||||||
int firstPulse = endPulse - extrapolate * scanLineSamples;
|
int firstPulse = endPulse - extrapolate * scanLineSamples;
|
||||||
for (int pulseIndex = firstPulse; pulseIndex < endPulse; pulseIndex += scanLineSamples)
|
for (int pulseIndex = firstPulse; pulseIndex < endPulse; pulseIndex += scanLineSamples)
|
||||||
copyLines(currentMode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, pulseIndex, scanLineSamples, frequencyOffset));
|
copyLines(currentMode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, pulseIndex, scanLineSamples, frequencyOffset));
|
||||||
}
|
}
|
||||||
for (int i = pictureChanged ? 0 : lines.length - 1; i < lines.length; ++i)
|
for (int i = pictureChanged ? 0 : lineLengths.length - 1; i < lineLengths.length; ++i)
|
||||||
copyLines(currentMode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, pulses[i], lines[i], frequencyOffset));
|
copyLines(currentMode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, syncIndexes[i], lineLengths[i], frequencyOffset));
|
||||||
lastSyncPulseIndex = pulses[pulses.length - 1];
|
lastSyncPulseIndex = syncIndexes[syncIndexes.length - 1];
|
||||||
currentScanLineSamples = scanLineSamples;
|
currentScanLineSamples = scanLineSamples;
|
||||||
lastFrequencyOffset = frequencyOffset;
|
lastFrequencyOffset = frequencyOffset;
|
||||||
shiftSamples(lastSyncPulseIndex + currentMode.getBegin());
|
shiftSamples(lastSyncPulseIndex + currentMode.getFirstPixelSampleIndex());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean process(float[] recordBuffer, int channelSelect) {
|
public boolean process(float[] recordBuffer, int channelSelect) {
|
||||||
|
boolean newLinesPresent = false;
|
||||||
boolean syncPulseDetected = demodulator.process(recordBuffer, channelSelect);
|
boolean syncPulseDetected = demodulator.process(recordBuffer, channelSelect);
|
||||||
int syncPulseIndex = currentSample + demodulator.syncPulseOffset;
|
int syncPulseIndex = currentSample + demodulator.syncPulseOffset;
|
||||||
int channels = channelSelect > 0 ? 2 : 1;
|
int channels = channelSelect > 0 ? 2 : 1;
|
||||||
|
|
@ -415,25 +420,28 @@ public class Decoder {
|
||||||
if (syncPulseDetected) {
|
if (syncPulseDetected) {
|
||||||
switch (demodulator.syncPulseWidth) {
|
switch (demodulator.syncPulseWidth) {
|
||||||
case FiveMilliSeconds:
|
case FiveMilliSeconds:
|
||||||
return processSyncPulse(syncPulse5msModes, last5msFrequencyOffsets, last5msSyncPulses, last5msScanLines, syncPulseIndex);
|
newLinesPresent = processSyncPulse(syncPulse5msModes, last5msFrequencyOffsets, last5msSyncPulses, last5msScanLines, syncPulseIndex);
|
||||||
|
break;
|
||||||
case NineMilliSeconds:
|
case NineMilliSeconds:
|
||||||
leaderBreakIndex = syncPulseIndex;
|
leaderBreakIndex = syncPulseIndex;
|
||||||
return processSyncPulse(syncPulse9msModes, last9msFrequencyOffsets, last9msSyncPulses, last9msScanLines, syncPulseIndex);
|
newLinesPresent = processSyncPulse(syncPulse9msModes, last9msFrequencyOffsets, last9msSyncPulses, last9msScanLines, syncPulseIndex);
|
||||||
|
break;
|
||||||
case TwentyMilliSeconds:
|
case TwentyMilliSeconds:
|
||||||
leaderBreakIndex = syncPulseIndex;
|
leaderBreakIndex = syncPulseIndex;
|
||||||
return processSyncPulse(syncPulse20msModes, last20msFrequencyOffsets, last20msSyncPulses, last20msScanLines, syncPulseIndex);
|
newLinesPresent = processSyncPulse(syncPulse20msModes, last20msFrequencyOffsets, last20msSyncPulses, last20msScanLines, syncPulseIndex);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return false;
|
break;
|
||||||
}
|
}
|
||||||
}
|
} else if (handleHeader()) {
|
||||||
if (handleHeader())
|
newLinesPresent = true;
|
||||||
return true;
|
} else if (currentSample > lastSyncPulseIndex + (currentScanLineSamples * 5) / 4) {
|
||||||
if (currentSample > lastSyncPulseIndex + (currentScanLineSamples * 5) / 4) {
|
|
||||||
copyLines(currentMode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, lastSyncPulseIndex, currentScanLineSamples, lastFrequencyOffset));
|
copyLines(currentMode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, lastSyncPulseIndex, currentScanLineSamples, lastFrequencyOffset));
|
||||||
lastSyncPulseIndex += currentScanLineSamples;
|
lastSyncPulseIndex += currentScanLineSamples;
|
||||||
return true;
|
newLinesPresent = true;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
return newLinesPresent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMode(String name) {
|
public void setMode(String name) {
|
||||||
|
|
@ -448,6 +456,8 @@ public class Decoder {
|
||||||
mode = findMode(syncPulse9msModes, name);
|
mode = findMode(syncPulse9msModes, name);
|
||||||
if (mode == null)
|
if (mode == null)
|
||||||
mode = findMode(syncPulse20msModes, name);
|
mode = findMode(syncPulse20msModes, name);
|
||||||
|
if (mode == null && hfFaxMode.getName().equals(name))
|
||||||
|
mode = hfFaxMode;
|
||||||
if (mode == currentMode) {
|
if (mode == currentMode) {
|
||||||
lockMode = true;
|
lockMode = true;
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ public class Demodulator {
|
||||||
private final SchmittTrigger syncPulseTrigger;
|
private final SchmittTrigger syncPulseTrigger;
|
||||||
private final Phasor baseBandOscillator;
|
private final Phasor baseBandOscillator;
|
||||||
private final Delay syncPulseValueDelay;
|
private final Delay syncPulseValueDelay;
|
||||||
|
private final double scanLineBandwidth;
|
||||||
|
private final double centerFrequency;
|
||||||
private final float syncPulseFrequencyValue;
|
private final float syncPulseFrequencyValue;
|
||||||
private final float syncPulseFrequencyTolerance;
|
private final float syncPulseFrequencyTolerance;
|
||||||
private final int syncPulse5msMinSamples;
|
private final int syncPulse5msMinSamples;
|
||||||
|
|
@ -33,10 +35,12 @@ public class Demodulator {
|
||||||
public int syncPulseOffset;
|
public int syncPulseOffset;
|
||||||
public float frequencyOffset;
|
public float frequencyOffset;
|
||||||
|
|
||||||
|
public static final double syncPulseFrequency = 1200;
|
||||||
|
public static final double blackFrequency = 1500;
|
||||||
|
public static final double whiteFrequency = 2300;
|
||||||
|
|
||||||
Demodulator(int sampleRate) {
|
Demodulator(int sampleRate) {
|
||||||
double blackFrequency = 1500;
|
scanLineBandwidth = whiteFrequency - blackFrequency;
|
||||||
double whiteFrequency = 2300;
|
|
||||||
double scanLineBandwidth = whiteFrequency - blackFrequency;
|
|
||||||
frequencyModulation = new FrequencyModulation(scanLineBandwidth, sampleRate);
|
frequencyModulation = new FrequencyModulation(scanLineBandwidth, sampleRate);
|
||||||
double syncPulse5msSeconds = 0.005;
|
double syncPulse5msSeconds = 0.005;
|
||||||
double syncPulse9msSeconds = 0.009;
|
double syncPulse9msSeconds = 0.009;
|
||||||
|
|
@ -63,20 +67,23 @@ public class Demodulator {
|
||||||
Kaiser kaiser = new Kaiser();
|
Kaiser kaiser = new Kaiser();
|
||||||
for (int i = 0; i < baseBandLowPass.length; ++i)
|
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));
|
baseBandLowPass.taps[i] = (float) (kaiser.window(2.0, i, baseBandLowPass.length) * Filter.lowPass(cutoffFrequency, sampleRate, i, baseBandLowPass.length));
|
||||||
double centerFrequency = (lowestFrequency + highestFrequency) / 2;
|
centerFrequency = (lowestFrequency + highestFrequency) / 2;
|
||||||
baseBandOscillator = new Phasor(-centerFrequency, sampleRate);
|
baseBandOscillator = new Phasor(-centerFrequency, sampleRate);
|
||||||
double syncPulseFrequency = 1200;
|
syncPulseFrequencyValue = (float) normalizeFrequency(syncPulseFrequency);
|
||||||
syncPulseFrequencyValue = (float) ((syncPulseFrequency - centerFrequency) * 2 / scanLineBandwidth);
|
|
||||||
syncPulseFrequencyTolerance = (float) (50 * 2 / scanLineBandwidth);
|
syncPulseFrequencyTolerance = (float) (50 * 2 / scanLineBandwidth);
|
||||||
double syncPorchFrequency = 1500;
|
double syncPorchFrequency = 1500;
|
||||||
double syncHighFrequency = (syncPulseFrequency + syncPorchFrequency) / 2;
|
double syncHighFrequency = (syncPulseFrequency + syncPorchFrequency) / 2;
|
||||||
double syncLowFrequency = (syncPulseFrequency + syncHighFrequency) / 2;
|
double syncLowFrequency = (syncPulseFrequency + syncHighFrequency) / 2;
|
||||||
double syncLowValue = (syncLowFrequency - centerFrequency) * 2 / scanLineBandwidth;
|
double syncLowValue = normalizeFrequency(syncLowFrequency);
|
||||||
double syncHighValue = (syncHighFrequency - centerFrequency) * 2 / scanLineBandwidth;
|
double syncHighValue = normalizeFrequency(syncHighFrequency);
|
||||||
syncPulseTrigger = new SchmittTrigger((float) syncLowValue, (float) syncHighValue);
|
syncPulseTrigger = new SchmittTrigger((float) syncLowValue, (float) syncHighValue);
|
||||||
baseBand = new Complex();
|
baseBand = new Complex();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private double normalizeFrequency(double frequency) {
|
||||||
|
return (frequency - centerFrequency) * 2 / scanLineBandwidth;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean process(float[] buffer, int channelSelect) {
|
public boolean process(float[] buffer, int channelSelect) {
|
||||||
boolean syncPulseDetected = false;
|
boolean syncPulseDetected = false;
|
||||||
int channels = channelSelect > 0 ? 2 : 1;
|
int channels = channelSelect > 0 ? 2 : 1;
|
||||||
|
|
|
||||||
353
app/src/main/java/xdsopl/robot36/FastFourierTransform.java
Normal file
353
app/src/main/java/xdsopl/robot36/FastFourierTransform.java
Normal file
|
|
@ -0,0 +1,353 @@
|
||||||
|
/*
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
137
app/src/main/java/xdsopl/robot36/HFFax.java
Normal file
137
app/src/main/java/xdsopl/robot36/HFFax.java
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
/*
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
app/src/main/java/xdsopl/robot36/Hann.java
Normal file
13
app/src/main/java/xdsopl/robot36/Hann.java
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -64,13 +64,14 @@ public class MainActivity extends AppCompatActivity {
|
||||||
private Bitmap scopeBitmap;
|
private Bitmap scopeBitmap;
|
||||||
private PixelBuffer scopeBuffer;
|
private PixelBuffer scopeBuffer;
|
||||||
private ImageView scopeView;
|
private ImageView scopeView;
|
||||||
private Bitmap freqPlotBitmap;
|
private Bitmap waterfallPlotBitmap;
|
||||||
private PixelBuffer freqPlotBuffer;
|
private PixelBuffer waterfallPlotBuffer;
|
||||||
private ImageView freqPlotView;
|
private ImageView waterfallPlotView;
|
||||||
private Bitmap peakMeterBitmap;
|
private Bitmap peakMeterBitmap;
|
||||||
private PixelBuffer peakMeterBuffer;
|
private PixelBuffer peakMeterBuffer;
|
||||||
private ImageView peakMeterView;
|
private ImageView peakMeterView;
|
||||||
private PixelBuffer imageBuffer;
|
private PixelBuffer imageBuffer;
|
||||||
|
private ShortTimeFourierTransform stft;
|
||||||
private short[] shortBuffer;
|
private short[] shortBuffer;
|
||||||
private float[] recordBuffer;
|
private float[] recordBuffer;
|
||||||
private AudioRecord audioRecord;
|
private AudioRecord audioRecord;
|
||||||
|
|
@ -78,6 +79,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
private Menu menu;
|
private Menu menu;
|
||||||
private String currentMode;
|
private String currentMode;
|
||||||
private String language;
|
private String language;
|
||||||
|
private Complex input;
|
||||||
private int recordRate;
|
private int recordRate;
|
||||||
private int recordChannel;
|
private int recordChannel;
|
||||||
private int audioSource;
|
private int audioSource;
|
||||||
|
|
@ -85,6 +87,10 @@ public class MainActivity extends AppCompatActivity {
|
||||||
private int fgColor;
|
private int fgColor;
|
||||||
private int thinColor;
|
private int thinColor;
|
||||||
private int tintColor;
|
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) {
|
private void setStatus(int id) {
|
||||||
setTitle(id);
|
setTitle(id);
|
||||||
|
|
@ -136,7 +142,10 @@ public class MainActivity extends AppCompatActivity {
|
||||||
recordBuffer[i] = .000030517578125f * shortBuffer[i];
|
recordBuffer[i] = .000030517578125f * shortBuffer[i];
|
||||||
}
|
}
|
||||||
processPeakMeter();
|
processPeakMeter();
|
||||||
|
if (showSpectrogram)
|
||||||
|
processSpectrogram();
|
||||||
boolean newLines = decoder.process(recordBuffer, recordChannel);
|
boolean newLines = decoder.process(recordBuffer, recordChannel);
|
||||||
|
if (!showSpectrogram)
|
||||||
processFreqPlot();
|
processFreqPlot();
|
||||||
if (newLines) {
|
if (newLines) {
|
||||||
processScope();
|
processScope();
|
||||||
|
|
@ -160,29 +169,104 @@ public class MainActivity extends AppCompatActivity {
|
||||||
peakMeterView.invalidate();
|
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() {
|
private void processFreqPlot() {
|
||||||
int width = freqPlotBitmap.getWidth();
|
int width = waterfallPlotBitmap.getWidth();
|
||||||
int height = freqPlotBitmap.getHeight();
|
int height = waterfallPlotBitmap.getHeight();
|
||||||
int stride = freqPlotBuffer.width;
|
int stride = waterfallPlotBuffer.width;
|
||||||
int line = stride * freqPlotBuffer.line;
|
waterfallPlotBuffer.line = (waterfallPlotBuffer.line + waterfallPlotBuffer.height / 2 - 1) % (waterfallPlotBuffer.height / 2);
|
||||||
|
int line = stride * waterfallPlotBuffer.line;
|
||||||
int channels = recordChannel > 0 ? 2 : 1;
|
int channels = recordChannel > 0 ? 2 : 1;
|
||||||
int samples = recordBuffer.length / channels;
|
int samples = recordBuffer.length / channels;
|
||||||
int spread = 2;
|
int spread = 2;
|
||||||
Arrays.fill(freqPlotBuffer.pixels, line, line + stride, 0);
|
Arrays.fill(waterfallPlotBuffer.pixels, line, line + stride, 0);
|
||||||
for (int i = 0; i < samples; ++i) {
|
for (int i = 0; i < samples; ++i) {
|
||||||
int x = Math.round((recordBuffer[i] + 2.5f) * 0.25f * stride);
|
int x = Math.round((recordBuffer[i] + 2.5f) * 0.25f * stride);
|
||||||
if (x >= spread && x < stride - spread)
|
if (x >= spread && x < stride - spread)
|
||||||
for (int j = -spread; j <= spread; ++j)
|
for (int j = -spread; j <= spread; ++j)
|
||||||
freqPlotBuffer.pixels[line + x + j] += 1 + spread * spread - j * j;
|
waterfallPlotBuffer.pixels[line + x + j] += 1 + spread * spread - j * j;
|
||||||
}
|
}
|
||||||
int factor = 960 / samples;
|
int factor = 960 / samples;
|
||||||
for (int i = 0; i < stride; ++i)
|
for (int i = 0; i < stride; ++i)
|
||||||
freqPlotBuffer.pixels[line + i] = 0x00FFFFFF & fgColor | Math.min(factor * freqPlotBuffer.pixels[line + i], 255) << 24;
|
waterfallPlotBuffer.pixels[line + i] = 0x00FFFFFF & fgColor | Math.min(factor * waterfallPlotBuffer.pixels[line + i], 255) << 24;
|
||||||
System.arraycopy(freqPlotBuffer.pixels, line, freqPlotBuffer.pixels, line + stride * (freqPlotBuffer.height / 2), stride);
|
System.arraycopy(waterfallPlotBuffer.pixels, line, waterfallPlotBuffer.pixels, line + stride * (waterfallPlotBuffer.height / 2), stride);
|
||||||
freqPlotBuffer.line = (freqPlotBuffer.line + 1) % (freqPlotBuffer.height / 2);
|
int offset = stride * waterfallPlotBuffer.line;
|
||||||
int offset = stride * (freqPlotBuffer.line + freqPlotBuffer.height / 2 - height);
|
waterfallPlotBitmap.setPixels(waterfallPlotBuffer.pixels, offset, stride, 0, 0, width, height);
|
||||||
freqPlotBitmap.setPixels(freqPlotBuffer.pixels, offset, stride, 0, 0, width, height);
|
waterfallPlotView.invalidate();
|
||||||
freqPlotView.invalidate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processScope() {
|
private void processScope() {
|
||||||
|
|
@ -198,6 +282,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
if (imageBuffer.line < imageBuffer.height)
|
if (imageBuffer.line < imageBuffer.height)
|
||||||
return;
|
return;
|
||||||
imageBuffer.line = -1;
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -236,6 +321,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
if (rateChanged) {
|
if (rateChanged) {
|
||||||
decoder = new Decoder(scopeBuffer, imageBuffer, getString(R.string.raw_mode), recordRate);
|
decoder = new Decoder(scopeBuffer, imageBuffer, getString(R.string.raw_mode), recordRate);
|
||||||
decoder.setMode(currentMode);
|
decoder.setMode(currentMode);
|
||||||
|
stft = new ShortTimeFourierTransform(recordRate / binWidthHz, 3);
|
||||||
}
|
}
|
||||||
startListening();
|
startListening();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -302,6 +388,34 @@ public class MainActivity extends AppCompatActivity {
|
||||||
initAudioRecord();
|
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() {
|
private void updateRecordRateMenu() {
|
||||||
switch (recordRate) {
|
switch (recordRate) {
|
||||||
case 8000:
|
case 8000:
|
||||||
|
|
@ -385,6 +499,8 @@ public class MainActivity extends AppCompatActivity {
|
||||||
state.putInt("recordChannel", recordChannel);
|
state.putInt("recordChannel", recordChannel);
|
||||||
state.putInt("audioSource", audioSource);
|
state.putInt("audioSource", audioSource);
|
||||||
state.putInt("audioFormat", audioFormat);
|
state.putInt("audioFormat", audioFormat);
|
||||||
|
state.putBoolean("autoSave", autoSave);
|
||||||
|
state.putBoolean("showSpectrogram", showSpectrogram);
|
||||||
state.putString("language", language);
|
state.putString("language", language);
|
||||||
super.onSaveInstanceState(state);
|
super.onSaveInstanceState(state);
|
||||||
}
|
}
|
||||||
|
|
@ -397,6 +513,8 @@ public class MainActivity extends AppCompatActivity {
|
||||||
edit.putInt("recordChannel", recordChannel);
|
edit.putInt("recordChannel", recordChannel);
|
||||||
edit.putInt("audioSource", audioSource);
|
edit.putInt("audioSource", audioSource);
|
||||||
edit.putInt("audioFormat", audioFormat);
|
edit.putInt("audioFormat", audioFormat);
|
||||||
|
edit.putBoolean("autoSave", autoSave);
|
||||||
|
edit.putBoolean("showSpectrogram", showSpectrogram);
|
||||||
edit.putString("language", language);
|
edit.putString("language", language);
|
||||||
edit.apply();
|
edit.apply();
|
||||||
}
|
}
|
||||||
|
|
@ -406,7 +524,9 @@ public class MainActivity extends AppCompatActivity {
|
||||||
final int defaultSampleRate = 44100;
|
final int defaultSampleRate = 44100;
|
||||||
final int defaultChannelSelect = 0;
|
final int defaultChannelSelect = 0;
|
||||||
final int defaultAudioSource = MediaRecorder.AudioSource.MIC;
|
final int defaultAudioSource = MediaRecorder.AudioSource.MIC;
|
||||||
final int defaultAudioFormat = AudioFormat.ENCODING_PCM_FLOAT;
|
final int defaultAudioFormat = AudioFormat.ENCODING_PCM_16BIT;
|
||||||
|
final boolean defaultAutoSave = true;
|
||||||
|
final boolean defaultShowSpectrogram = true;
|
||||||
final String defaultLanguage = "system";
|
final String defaultLanguage = "system";
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
SharedPreferences pref = getPreferences(Context.MODE_PRIVATE);
|
SharedPreferences pref = getPreferences(Context.MODE_PRIVATE);
|
||||||
|
|
@ -415,6 +535,8 @@ public class MainActivity extends AppCompatActivity {
|
||||||
recordChannel = pref.getInt("recordChannel", defaultChannelSelect);
|
recordChannel = pref.getInt("recordChannel", defaultChannelSelect);
|
||||||
audioSource = pref.getInt("audioSource", defaultAudioSource);
|
audioSource = pref.getInt("audioSource", defaultAudioSource);
|
||||||
audioFormat = pref.getInt("audioFormat", defaultAudioFormat);
|
audioFormat = pref.getInt("audioFormat", defaultAudioFormat);
|
||||||
|
autoSave = pref.getBoolean("autoSave", defaultAutoSave);
|
||||||
|
showSpectrogram = pref.getBoolean("showSpectrogram", defaultShowSpectrogram);
|
||||||
language = pref.getString("language", defaultLanguage);
|
language = pref.getString("language", defaultLanguage);
|
||||||
} else {
|
} else {
|
||||||
AppCompatDelegate.setDefaultNightMode(state.getInt("nightMode", AppCompatDelegate.getDefaultNightMode()));
|
AppCompatDelegate.setDefaultNightMode(state.getInt("nightMode", AppCompatDelegate.getDefaultNightMode()));
|
||||||
|
|
@ -422,6 +544,8 @@ public class MainActivity extends AppCompatActivity {
|
||||||
recordChannel = state.getInt("recordChannel", defaultChannelSelect);
|
recordChannel = state.getInt("recordChannel", defaultChannelSelect);
|
||||||
audioSource = state.getInt("audioSource", defaultAudioSource);
|
audioSource = state.getInt("audioSource", defaultAudioSource);
|
||||||
audioFormat = state.getInt("audioFormat", defaultAudioFormat);
|
audioFormat = state.getInt("audioFormat", defaultAudioFormat);
|
||||||
|
autoSave = state.getBoolean("autoSave", defaultAutoSave);
|
||||||
|
showSpectrogram = state.getBoolean("showSpectrogram", defaultShowSpectrogram);
|
||||||
language = state.getString("language", defaultLanguage);
|
language = state.getString("language", defaultLanguage);
|
||||||
}
|
}
|
||||||
super.onCreate(state);
|
super.onCreate(state);
|
||||||
|
|
@ -434,11 +558,12 @@ public class MainActivity extends AppCompatActivity {
|
||||||
thinColor = getColor(R.color.thin);
|
thinColor = getColor(R.color.thin);
|
||||||
tintColor = getColor(R.color.tint);
|
tintColor = getColor(R.color.tint);
|
||||||
scopeBuffer = new PixelBuffer(640, 2 * 1280);
|
scopeBuffer = new PixelBuffer(640, 2 * 1280);
|
||||||
freqPlotBuffer = new PixelBuffer(256, 2 * 256);
|
waterfallPlotBuffer = new PixelBuffer(256, 2 * 256);
|
||||||
peakMeterBuffer = new PixelBuffer(1, 16);
|
peakMeterBuffer = new PixelBuffer(1, 16);
|
||||||
imageBuffer = new PixelBuffer(640, 496);
|
imageBuffer = new PixelBuffer(800, 616);
|
||||||
|
input = new Complex();
|
||||||
createScope(config);
|
createScope(config);
|
||||||
createFreqPlot(config);
|
createWaterfallPlot(config);
|
||||||
createPeakMeter();
|
createPeakMeter();
|
||||||
List<String> permissions = new ArrayList<>();
|
List<String> permissions = new ArrayList<>();
|
||||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
|
@ -469,6 +594,8 @@ public class MainActivity extends AppCompatActivity {
|
||||||
updateRecordChannelMenu();
|
updateRecordChannelMenu();
|
||||||
updateAudioSourceMenu();
|
updateAudioSourceMenu();
|
||||||
updateAudioFormatMenu();
|
updateAudioFormatMenu();
|
||||||
|
updateWaterfallPlotMenu();
|
||||||
|
updateAutoSaveMenu();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -491,6 +618,10 @@ public class MainActivity extends AppCompatActivity {
|
||||||
setMode(R.string.raw_mode);
|
setMode(R.string.raw_mode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (id == R.id.action_force_hffax_mode) {
|
||||||
|
setMode(R.string.hf_fax);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (id == R.id.action_force_robot36_color) {
|
if (id == R.id.action_force_robot36_color) {
|
||||||
setMode(R.string.robot36_color);
|
setMode(R.string.robot36_color);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -523,6 +654,10 @@ public class MainActivity extends AppCompatActivity {
|
||||||
setMode(R.string.pd240);
|
setMode(R.string.pd240);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (id == R.id.action_force_pd290) {
|
||||||
|
setMode(R.string.pd290);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (id == R.id.action_force_martin1) {
|
if (id == R.id.action_force_martin1) {
|
||||||
setMode(R.string.martin1);
|
setMode(R.string.martin1);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -615,6 +750,22 @@ public class MainActivity extends AppCompatActivity {
|
||||||
setAudioFormat(AudioFormat.ENCODING_PCM_16BIT);
|
setAudioFormat(AudioFormat.ENCODING_PCM_16BIT);
|
||||||
return true;
|
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) {
|
if (id == R.id.action_enable_night_mode) {
|
||||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -651,6 +802,22 @@ public class MainActivity extends AppCompatActivity {
|
||||||
setLanguage("pt-BR");
|
setLanguage("pt-BR");
|
||||||
return true;
|
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");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -665,17 +832,24 @@ public class MainActivity extends AppCompatActivity {
|
||||||
int height = scopeBuffer.height / 2;
|
int height = scopeBuffer.height / 2;
|
||||||
int stride = scopeBuffer.width;
|
int stride = scopeBuffer.width;
|
||||||
int offset = stride * scopeBuffer.line;
|
int offset = stride * scopeBuffer.line;
|
||||||
storeBitmap(Bitmap.createBitmap(scopeBuffer.pixels, offset, stride, width, height, Bitmap.Config.ARGB_8888));
|
Bitmap bmp = Bitmap.createBitmap(scopeBuffer.pixels, offset, stride, width, height, Bitmap.Config.ARGB_8888);
|
||||||
|
|
||||||
|
if (decoder != null)
|
||||||
|
{
|
||||||
|
bmp = decoder.currentMode.postProcessScopeImage(bmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
storeBitmap(bmp);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createScope(Configuration config) {
|
private void createScope(Configuration config) {
|
||||||
int screenWidthDp = config.screenWidthDp;
|
int screenWidthDp = config.screenWidthDp;
|
||||||
int screenHeightDp = config.screenHeightDp;
|
int screenHeightDp = config.screenHeightDp;
|
||||||
int freqPlotHeightDp = 64;
|
int waterfallPlotHeightDp = 64;
|
||||||
if (config.orientation == Configuration.ORIENTATION_LANDSCAPE)
|
if (config.orientation == Configuration.ORIENTATION_LANDSCAPE)
|
||||||
screenWidthDp /= 2;
|
screenWidthDp /= 2;
|
||||||
else
|
else
|
||||||
screenHeightDp -= freqPlotHeightDp;
|
screenHeightDp -= waterfallPlotHeightDp;
|
||||||
int actionBarHeightDp = 64;
|
int actionBarHeightDp = 64;
|
||||||
screenHeightDp -= actionBarHeightDp;
|
screenHeightDp -= actionBarHeightDp;
|
||||||
int width = scopeBuffer.width;
|
int width = scopeBuffer.width;
|
||||||
|
|
@ -689,18 +863,18 @@ public class MainActivity extends AppCompatActivity {
|
||||||
scopeView.setImageBitmap(scopeBitmap);
|
scopeView.setImageBitmap(scopeBitmap);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createFreqPlot(Configuration config) {
|
private void createWaterfallPlot(Configuration config) {
|
||||||
int width = freqPlotBuffer.width;
|
int width = waterfallPlotBuffer.width;
|
||||||
int height = freqPlotBuffer.height / 2;
|
int height = waterfallPlotBuffer.height / 2;
|
||||||
if (config.orientation != Configuration.ORIENTATION_LANDSCAPE)
|
if (config.orientation != Configuration.ORIENTATION_LANDSCAPE)
|
||||||
height /= 4;
|
height /= 4;
|
||||||
freqPlotBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
waterfallPlotBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||||
int stride = freqPlotBuffer.width;
|
int stride = waterfallPlotBuffer.width;
|
||||||
int offset = stride * (freqPlotBuffer.line + freqPlotBuffer.height / 2 - height);
|
int offset = stride * waterfallPlotBuffer.line;
|
||||||
freqPlotBitmap.setPixels(freqPlotBuffer.pixels, offset, stride, 0, 0, width, height);
|
waterfallPlotBitmap.setPixels(waterfallPlotBuffer.pixels, offset, stride, 0, 0, width, height);
|
||||||
freqPlotView = findViewById(R.id.freq_plot);
|
waterfallPlotView = findViewById(R.id.waterfall_plot);
|
||||||
freqPlotView.setScaleType(ImageView.ScaleType.FIT_XY);
|
waterfallPlotView.setScaleType(ImageView.ScaleType.FIT_XY);
|
||||||
freqPlotView.setImageBitmap(freqPlotBitmap);
|
waterfallPlotView.setImageBitmap(waterfallPlotBitmap);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createPeakMeter() {
|
private void createPeakMeter() {
|
||||||
|
|
@ -717,7 +891,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
setContentView(config.orientation == Configuration.ORIENTATION_LANDSCAPE ? R.layout.activity_main_land : R.layout.activity_main);
|
setContentView(config.orientation == Configuration.ORIENTATION_LANDSCAPE ? R.layout.activity_main_land : R.layout.activity_main);
|
||||||
handleInsets();
|
handleInsets();
|
||||||
createScope(config);
|
createScope(config);
|
||||||
createFreqPlot(config);
|
createWaterfallPlot(config);
|
||||||
createPeakMeter();
|
createPeakMeter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,22 +6,30 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
|
||||||
|
|
||||||
package xdsopl.robot36;
|
package xdsopl.robot36;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
|
||||||
public interface Mode {
|
public interface Mode {
|
||||||
String getName();
|
String getName();
|
||||||
|
|
||||||
int getCode();
|
int getVISCode();
|
||||||
|
|
||||||
int getWidth();
|
int getWidth();
|
||||||
|
|
||||||
int getHeight();
|
int getHeight();
|
||||||
|
|
||||||
int getBegin();
|
int getFirstPixelSampleIndex();
|
||||||
|
|
||||||
int getFirstSyncPulseIndex();
|
int getFirstSyncPulseIndex();
|
||||||
|
|
||||||
int getScanLineSamples();
|
int getScanLineSamples();
|
||||||
|
|
||||||
void reset();
|
Bitmap postProcessScopeImage(Bitmap bmp);
|
||||||
|
|
||||||
|
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);
|
boolean decodeScanLine(PixelBuffer pixelBuffer, float[] scratchBuffer, float[] scanLineBuffer, int scopeBufferWidth, int syncPulseIndex, int scanLineSamples, float frequencyOffset);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
|
||||||
|
|
||||||
package xdsopl.robot36;
|
package xdsopl.robot36;
|
||||||
|
|
||||||
public class PaulDon implements Mode {
|
public class PaulDon extends BaseMode {
|
||||||
private final ExponentialMovingAverage lowPassFilter;
|
private final ExponentialMovingAverage lowPassFilter;
|
||||||
private final int horizontalPixels;
|
private final int horizontalPixels;
|
||||||
private final int verticalPixels;
|
private final int verticalPixels;
|
||||||
|
|
@ -56,7 +56,7 @@ public class PaulDon implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCode() {
|
public int getVISCode() {
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,7 +71,7 @@ public class PaulDon implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getBegin() {
|
public int getFirstPixelSampleIndex() {
|
||||||
return beginSamples;
|
return beginSamples;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,7 +86,7 @@ public class PaulDon implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reset() {
|
public void resetState() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
|
||||||
|
|
||||||
package xdsopl.robot36;
|
package xdsopl.robot36;
|
||||||
|
|
||||||
public class RGBDecoder implements Mode {
|
public class RGBDecoder extends BaseMode {
|
||||||
private final ExponentialMovingAverage lowPassFilter;
|
private final ExponentialMovingAverage lowPassFilter;
|
||||||
private final int horizontalPixels;
|
private final int horizontalPixels;
|
||||||
private final int verticalPixels;
|
private final int verticalPixels;
|
||||||
|
|
@ -51,7 +51,7 @@ public class RGBDecoder implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCode() {
|
public int getVISCode() {
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,7 +66,7 @@ public class RGBDecoder implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getBegin() {
|
public int getFirstPixelSampleIndex() {
|
||||||
return beginSamples;
|
return beginSamples;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,7 +81,7 @@ public class RGBDecoder implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reset() {
|
public void resetState() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
|
||||||
|
|
||||||
package xdsopl.robot36;
|
package xdsopl.robot36;
|
||||||
|
|
||||||
public class RawDecoder implements Mode {
|
public class RawDecoder extends BaseMode {
|
||||||
private final ExponentialMovingAverage lowPassFilter;
|
private final ExponentialMovingAverage lowPassFilter;
|
||||||
private final int smallPictureMaxSamples;
|
private final int smallPictureMaxSamples;
|
||||||
private final int mediumPictureMaxSamples;
|
private final int mediumPictureMaxSamples;
|
||||||
|
|
@ -29,7 +29,7 @@ public class RawDecoder implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCode() {
|
public int getVISCode() {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,7 +44,7 @@ public class RawDecoder implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getBegin() {
|
public int getFirstPixelSampleIndex() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,7 +59,7 @@ public class RawDecoder implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reset() {
|
public void resetState() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
|
||||||
|
|
||||||
package xdsopl.robot36;
|
package xdsopl.robot36;
|
||||||
|
|
||||||
public class Robot_36_Color implements Mode {
|
public class Robot_36_Color extends BaseMode {
|
||||||
private final ExponentialMovingAverage lowPassFilter;
|
private final ExponentialMovingAverage lowPassFilter;
|
||||||
private final int horizontalPixels;
|
private final int horizontalPixels;
|
||||||
private final int verticalPixels;
|
private final int verticalPixels;
|
||||||
|
|
@ -59,7 +59,7 @@ public class Robot_36_Color implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCode() {
|
public int getVISCode() {
|
||||||
return 8;
|
return 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,7 +74,7 @@ public class Robot_36_Color implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getBegin() {
|
public int getFirstPixelSampleIndex() {
|
||||||
return beginSamples;
|
return beginSamples;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,7 +89,7 @@ public class Robot_36_Color implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reset() {
|
public void resetState() {
|
||||||
lastEven = false;
|
lastEven = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
|
||||||
|
|
||||||
package xdsopl.robot36;
|
package xdsopl.robot36;
|
||||||
|
|
||||||
public class Robot_72_Color implements Mode {
|
public class Robot_72_Color extends BaseMode {
|
||||||
private final ExponentialMovingAverage lowPassFilter;
|
private final ExponentialMovingAverage lowPassFilter;
|
||||||
private final int horizontalPixels;
|
private final int horizontalPixels;
|
||||||
private final int verticalPixels;
|
private final int verticalPixels;
|
||||||
|
|
@ -57,7 +57,7 @@ public class Robot_72_Color implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCode() {
|
public int getVISCode() {
|
||||||
return 12;
|
return 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ public class Robot_72_Color implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getBegin() {
|
public int getFirstPixelSampleIndex() {
|
||||||
return beginSamples;
|
return beginSamples;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,7 +87,7 @@ public class Robot_72_Color implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reset() {
|
public void resetState() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
app/src/main/res/drawable/baseline_water_24.xml
Normal file
5
app/src/main/res/drawable/baseline_water_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<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>
|
||||||
5
app/src/main/res/drawable/outline_scan_delete_24.xml
Normal file
5
app/src/main/res/drawable/outline_scan_delete_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<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>
|
||||||
|
|
@ -13,16 +13,16 @@
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:contentDescription="@string/scope_description"
|
android:contentDescription="@string/scope_description"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/freq_plot"
|
app:layout_constraintBottom_toTopOf="@+id/waterfall_plot"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/freq_plot"
|
android:id="@+id/waterfall_plot"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="64dp"
|
android:layout_height="64dp"
|
||||||
android:contentDescription="@string/freq_plot_description"
|
android:contentDescription="@string/waterfall_plot"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/peak_meter"
|
app:layout_constraintEnd_toStartOf="@+id/peak_meter"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
|
@ -35,6 +35,6 @@
|
||||||
android:contentDescription="@string/peak_meter_description"
|
android:contentDescription="@string/peak_meter_description"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@+id/freq_plot"
|
app:layout_constraintStart_toEndOf="@+id/waterfall_plot"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/scope" />
|
app:layout_constraintTop_toBottomOf="@+id/scope" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
@ -14,15 +14,15 @@
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:contentDescription="@string/scope_description"
|
android:contentDescription="@string/scope_description"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/freq_plot"
|
app:layout_constraintEnd_toStartOf="@+id/waterfall_plot"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/freq_plot"
|
android:id="@+id/waterfall_plot"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:contentDescription="@string/freq_plot_description"
|
android:contentDescription="@string/waterfall_plot"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/peak_meter"
|
app:layout_constraintEnd_toStartOf="@+id/peak_meter"
|
||||||
app:layout_constraintStart_toEndOf="@+id/scope"
|
app:layout_constraintStart_toEndOf="@+id/scope"
|
||||||
|
|
@ -35,6 +35,6 @@
|
||||||
android:contentDescription="@string/peak_meter_description"
|
android:contentDescription="@string/peak_meter_description"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@+id/freq_plot"
|
app:layout_constraintStart_toEndOf="@+id/waterfall_plot"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
@ -62,6 +62,9 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_force_pd240"
|
android:id="@+id/action_force_pd240"
|
||||||
android:title="@string/pd240" />
|
android:title="@string/pd240" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_force_pd290"
|
||||||
|
android:title="@string/pd290" />
|
||||||
</menu>
|
</menu>
|
||||||
</item>
|
</item>
|
||||||
<item android:title="@string/martin_modes">
|
<item android:title="@string/martin_modes">
|
||||||
|
|
@ -94,6 +97,13 @@
|
||||||
android:title="@string/wraase_sc2_180" />
|
android:title="@string/wraase_sc2_180" />
|
||||||
</menu>
|
</menu>
|
||||||
</item>
|
</item>
|
||||||
|
<item android:title="@string/contributed_modes">
|
||||||
|
<menu>
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_force_hffax_mode"
|
||||||
|
android:title="@string/hf_fax" />
|
||||||
|
</menu>
|
||||||
|
</item>
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_force_raw_mode"
|
android:id="@+id/action_force_raw_mode"
|
||||||
android:icon="@drawable/baseline_image_not_supported_24"
|
android:icon="@drawable/baseline_image_not_supported_24"
|
||||||
|
|
@ -205,6 +215,42 @@
|
||||||
app:iconTint="@color/tint" />
|
app:iconTint="@color/tint" />
|
||||||
</menu>
|
</menu>
|
||||||
</item>
|
</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
|
<item
|
||||||
android:icon="@drawable/baseline_language_24"
|
android:icon="@drawable/baseline_language_24"
|
||||||
android:title="@string/language"
|
android:title="@string/language"
|
||||||
|
|
@ -225,6 +271,18 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_brazilian_portuguese"
|
android:id="@+id/action_brazilian_portuguese"
|
||||||
android:title="@string/brazilian_portuguese" />
|
android:title="@string/brazilian_portuguese" />
|
||||||
|
<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" />
|
||||||
</menu>
|
</menu>
|
||||||
</item>
|
</item>
|
||||||
<item
|
<item
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,11 @@
|
||||||
<string name="creating_picture_file_failed">Erstellen der Bilddatei 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="storing_picture_failed">Speichern des Bildes fehlgeschlagen</string>
|
||||||
<string name="scope_description">Dekodiertes SSTV-Bild</string>
|
<string name="scope_description">Dekodiertes SSTV-Bild</string>
|
||||||
<string name="freq_plot_description">Frequenzdiagramm</string>
|
|
||||||
<string name="peak_meter_description">Spitzenpegel des Audiosignals</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="night_mode">Nachtmodus</string>
|
||||||
<string name="enable">Aktivieren</string>
|
<string name="enable">Aktivieren</string>
|
||||||
<string name="disable">Deaktivieren</string>
|
<string name="disable">Deaktivieren</string>
|
||||||
|
|
|
||||||
68
app/src/main/res/values-es-rUS/strings.xml
Normal file
68
app/src/main/res/values-es-rUS/strings.xml
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?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>
|
||||||
69
app/src/main/res/values-fr/strings.xml
Normal file
69
app/src/main/res/values-fr/strings.xml
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?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>
|
||||||
69
app/src/main/res/values-pl/strings.xml
Normal file
69
app/src/main/res/values-pl/strings.xml
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -34,8 +34,11 @@
|
||||||
<string name="creating_picture_file_failed">Falha ao criar 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="storing_picture_failed">Falha ao salvar imagem</string>
|
||||||
<string name="scope_description">Imagem SSTV decodificada</string>
|
<string name="scope_description">Imagem SSTV decodificada</string>
|
||||||
<string name="freq_plot_description">Gráfico de frequência</string>
|
|
||||||
<string name="peak_meter_description">Nível de sinal de áudio máximo</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="night_mode">Modo noturno</string>
|
||||||
<string name="enable">Habilitar</string>
|
<string name="enable">Habilitar</string>
|
||||||
<string name="disable">Desabilitar</string>
|
<string name="disable">Desabilitar</string>
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,11 @@
|
||||||
<string name="creating_picture_file_failed">Ошибка создания файла изображения</string>
|
<string name="creating_picture_file_failed">Ошибка создания файла изображения</string>
|
||||||
<string name="storing_picture_failed">Ошибка сохранения изображения</string>
|
<string name="storing_picture_failed">Ошибка сохранения изображения</string>
|
||||||
<string name="scope_description">Декодированное изображение SSTV</string>
|
<string name="scope_description">Декодированное изображение SSTV</string>
|
||||||
<string name="freq_plot_description">График частот</string>
|
|
||||||
<string name="peak_meter_description">Пиковый уровень аудиосигнала</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="night_mode">Ночной режим</string>
|
||||||
<string name="enable">Включить</string>
|
<string name="enable">Включить</string>
|
||||||
<string name="disable">Выключить</string>
|
<string name="disable">Выключить</string>
|
||||||
|
|
|
||||||
69
app/src/main/res/values-uk/strings.xml
Normal file
69
app/src/main/res/values-uk/strings.xml
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -34,8 +34,11 @@
|
||||||
<string name="creating_picture_file_failed">创建图片文件出错</string>
|
<string name="creating_picture_file_failed">创建图片文件出错</string>
|
||||||
<string name="storing_picture_failed">写入图像数据失败</string>
|
<string name="storing_picture_failed">写入图像数据失败</string>
|
||||||
<string name="scope_description">解码的SSTV图像</string>
|
<string name="scope_description">解码的SSTV图像</string>
|
||||||
<string name="freq_plot_description">频率图</string>
|
|
||||||
<string name="peak_meter_description">音频峰值信号水平</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="night_mode">夜间模式</string>
|
||||||
<string name="enable">开启</string>
|
<string name="enable">开启</string>
|
||||||
<string name="disable">禁用</string>
|
<string name="disable">禁用</string>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
<string name="martin_modes" translatable="false">Martin</string>
|
<string name="martin_modes" translatable="false">Martin</string>
|
||||||
<string name="scottie_modes" translatable="false">Scottie</string>
|
<string name="scottie_modes" translatable="false">Scottie</string>
|
||||||
<string name="wraase_modes" translatable="false">Wraase</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="robot36_color" translatable="false">Robot 36 Color</string>
|
||||||
<string name="robot72_color" translatable="false">Robot 72 Color</string>
|
<string name="robot72_color" translatable="false">Robot 72 Color</string>
|
||||||
<string name="pd50" translatable="false">PD 50</string>
|
<string name="pd50" translatable="false">PD 50</string>
|
||||||
|
|
@ -14,12 +15,14 @@
|
||||||
<string name="pd160" translatable="false">PD 160</string>
|
<string name="pd160" translatable="false">PD 160</string>
|
||||||
<string name="pd180" translatable="false">PD 180</string>
|
<string name="pd180" translatable="false">PD 180</string>
|
||||||
<string name="pd240" translatable="false">PD 240</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="martin1" translatable="false">Martin 1</string>
|
||||||
<string name="martin2" translatable="false">Martin 2</string>
|
<string name="martin2" translatable="false">Martin 2</string>
|
||||||
<string name="scottie1" translatable="false">Scottie 1</string>
|
<string name="scottie1" translatable="false">Scottie 1</string>
|
||||||
<string name="scottie2" translatable="false">Scottie 2</string>
|
<string name="scottie2" translatable="false">Scottie 2</string>
|
||||||
<string name="scottie_dx" translatable="false">Scottie DX</string>
|
<string name="scottie_dx" translatable="false">Scottie DX</string>
|
||||||
<string name="wraase_sc2_180" translatable="false">Wraase SC2–180</string>
|
<string name="wraase_sc2_180" translatable="false">Wraase SC2–180</string>
|
||||||
|
<string name="hf_fax" translatable="false">HF Fax</string>
|
||||||
<string name="rate_8000" translatable="false">8 kHz</string>
|
<string name="rate_8000" translatable="false">8 kHz</string>
|
||||||
<string name="rate_16000" translatable="false">16 kHz</string>
|
<string name="rate_16000" translatable="false">16 kHz</string>
|
||||||
<string name="rate_32000" translatable="false">32 kHz</string>
|
<string name="rate_32000" translatable="false">32 kHz</string>
|
||||||
|
|
@ -30,6 +33,10 @@
|
||||||
<string name="russian" translatable="false">Русский</string>
|
<string name="russian" translatable="false">Русский</string>
|
||||||
<string name="german" translatable="false">Deutsch</string>
|
<string name="german" translatable="false">Deutsch</string>
|
||||||
<string name="brazilian_portuguese" translatable="false">Português brasileiro</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="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="language">Language</string>
|
||||||
<string name="share">Share</string>
|
<string name="share">Share</string>
|
||||||
|
|
@ -65,8 +72,11 @@
|
||||||
<string name="creating_picture_file_failed">Creating picture file failed</string>
|
<string name="creating_picture_file_failed">Creating picture file failed</string>
|
||||||
<string name="storing_picture_failed">Storing picture failed</string>
|
<string name="storing_picture_failed">Storing picture failed</string>
|
||||||
<string name="scope_description">Decoded SSTV picture</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="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="night_mode">Night Mode</string>
|
||||||
<string name="enable">Enable</string>
|
<string name="enable">Enable</string>
|
||||||
<string name="disable">Disable</string>
|
<string name="disable">Disable</string>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
Following SSTV modes are supported:
|
Following SSTV modes are supported:
|
||||||
|
|
||||||
<ul><li>Robot Modes: 36 & 72</li>
|
<ul><li>Robot Modes: 36 & 72</li>
|
||||||
<li>PD Modes: 50, 90, 120, 160, 180 & 240</li>
|
<li>PD Modes: 50, 90, 120, 160, 180, 240 & 290</li>
|
||||||
<li>Martin Modes: 1 & 2</li>
|
<li>Martin Modes: 1 & 2</li>
|
||||||
<li>Scottie Modes: 1, 2 & DX</li>
|
<li>Scottie Modes: 1, 2 & DX</li>
|
||||||
<li>Wraase Mode: SC2-180</li></ul>
|
<li>Wraase Mode: SC2-180</li></ul>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
[versions]
|
[versions]
|
||||||
agp = "8.5.2"
|
agp = "8.13.1"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.2.1"
|
junitVersion = "1.3.0"
|
||||||
espressoCore = "3.6.1"
|
espressoCore = "3.7.0"
|
||||||
appcompat = "1.7.0"
|
appcompat = "1.7.1"
|
||||||
material = "1.12.0"
|
material = "1.13.0"
|
||||||
activity = "1.9.1"
|
activity = "1.11.0"
|
||||||
constraintlayout = "2.1.4"
|
constraintlayout = "2.2.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
|
|
|
||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
#Fri Apr 12 11:35:07 CEST 2024
|
#Fri Apr 12 11:35:07 CEST 2024
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue