mirror of
https://github.com/xdsopl/robot36.git
synced 2025-12-06 07:12:07 +01:00
Compare commits
58 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 | ||
|
|
b69d064e5b | ||
|
|
a98c945f37 | ||
|
|
656693609d | ||
|
|
55f2726768 | ||
|
|
879b5244f2 | ||
|
|
57916c665e | ||
|
|
59500bbf49 | ||
|
|
0e863e6304 | ||
|
|
6e8a524416 | ||
|
|
72eeae7b29 | ||
|
|
477ff84df6 | ||
|
|
871293b8d3 | ||
|
|
97448c9c05 | ||
|
|
65b09103d2 | ||
|
|
25a442fcae | ||
|
|
c56d30d33f | ||
|
|
2150004849 |
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 {
|
||||
namespace 'xdsopl.robot36'
|
||||
compileSdk 34
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId "xdsopl.robot36"
|
||||
minSdk 24
|
||||
targetSdk 34
|
||||
versionCode 59
|
||||
versionName "2.9"
|
||||
targetSdk 36
|
||||
versionCode 66
|
||||
versionName "2.16"
|
||||
|
||||
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 visCodeSamples;
|
||||
private final Mode rawMode;
|
||||
private final Mode hfFaxMode;
|
||||
private final ArrayList<Mode> syncPulse5msModes;
|
||||
private final ArrayList<Mode> syncPulse9msModes;
|
||||
private final ArrayList<Mode> syncPulse20msModes;
|
||||
|
|
@ -54,7 +55,7 @@ public class Decoder {
|
|||
this.scopeBuffer = scopeBuffer;
|
||||
this.imageBuffer = imageBuffer;
|
||||
imageBuffer.line = -1;
|
||||
pixelBuffer = new PixelBuffer(scopeBuffer.width, 2);
|
||||
pixelBuffer = new PixelBuffer(800, 2);
|
||||
demodulator = new Demodulator(sampleRate);
|
||||
double pulseFilterSeconds = 0.0025;
|
||||
int pulseFilterSamples = (int) Math.round(pulseFilterSeconds * sampleRate) | 1;
|
||||
|
|
@ -95,6 +96,7 @@ public class Decoder {
|
|||
double scanLineToleranceSeconds = 0.001;
|
||||
scanLineToleranceSamples = (int) Math.round(scanLineToleranceSeconds * sampleRate);
|
||||
rawMode = new RawDecoder(rawName, sampleRate);
|
||||
hfFaxMode = new HFFax(sampleRate);
|
||||
Mode robot36 = new Robot_36_Color(sampleRate);
|
||||
currentMode = robot36;
|
||||
currentScanLineSamples = robot36.getScanLineSamples();
|
||||
|
|
@ -115,6 +117,7 @@ public class Decoder {
|
|||
syncPulse20msModes.add(new PaulDon("160", 98, 512, 400, 0.195584, sampleRate));
|
||||
syncPulse20msModes.add(new PaulDon("180", 96, 640, 496, 0.18304, sampleRate));
|
||||
syncPulse20msModes.add(new PaulDon("240", 97, 640, 496, 0.24448, sampleRate));
|
||||
syncPulse20msModes.add(new PaulDon("290", 94, 800, 616, 0.2288, sampleRate));
|
||||
}
|
||||
|
||||
private double scanLineMean(int[] lines) {
|
||||
|
|
@ -156,7 +159,7 @@ public class Decoder {
|
|||
|
||||
private Mode findMode(ArrayList<Mode> modes, int code) {
|
||||
for (Mode mode : modes)
|
||||
if (mode.getCode() == code)
|
||||
if (mode.getVISCode() == code)
|
||||
return mode;
|
||||
return null;
|
||||
}
|
||||
|
|
@ -169,10 +172,11 @@ public class Decoder {
|
|||
}
|
||||
|
||||
private void copyUnscaled() {
|
||||
int width = Math.min(scopeBuffer.width, pixelBuffer.width);
|
||||
for (int row = 0; row < pixelBuffer.height; ++row) {
|
||||
int line = scopeBuffer.width * scopeBuffer.line;
|
||||
System.arraycopy(pixelBuffer.pixels, row * pixelBuffer.width, scopeBuffer.pixels, line, pixelBuffer.width);
|
||||
Arrays.fill(scopeBuffer.pixels, line + pixelBuffer.width, line + scopeBuffer.width, 0);
|
||||
System.arraycopy(pixelBuffer.pixels, row * pixelBuffer.width, scopeBuffer.pixels, line, width);
|
||||
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);
|
||||
scopeBuffer.line = (scopeBuffer.line + 1) % (scopeBuffer.height / 2);
|
||||
}
|
||||
|
|
@ -206,7 +210,7 @@ public class Decoder {
|
|||
finish = imageBuffer.line == imageBuffer.height;
|
||||
}
|
||||
int scale = scopeBuffer.width / pixelBuffer.width;
|
||||
if (scale == 1)
|
||||
if (scale <= 1)
|
||||
copyUnscaled();
|
||||
else
|
||||
copyScaled(scale);
|
||||
|
|
@ -330,7 +334,7 @@ public class Decoder {
|
|||
}
|
||||
if (lockMode && mode != currentMode)
|
||||
return false;
|
||||
mode.reset();
|
||||
mode.resetState();
|
||||
imageBuffer.width = mode.getWidth();
|
||||
imageBuffer.height = mode.getHeight();
|
||||
imageBuffer.line = 0;
|
||||
|
|
@ -344,29 +348,29 @@ public class Decoder {
|
|||
for (int i = 0; i < pulses.length; ++i)
|
||||
pulses[i] = oldestSyncPulseIndex + i * currentScanLineSamples;
|
||||
Arrays.fill(lines, currentScanLineSamples);
|
||||
shiftSamples(lastSyncPulseIndex + mode.getBegin());
|
||||
shiftSamples(lastSyncPulseIndex + mode.getFirstPixelSampleIndex());
|
||||
drawLines(0xff00ff00, 8);
|
||||
drawLines(0xff000000, 10);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean processSyncPulse(ArrayList<Mode> modes, float[] freqOffs, int[] pulses, int[] lines, int index) {
|
||||
for (int i = 1; i < pulses.length; ++i)
|
||||
pulses[i - 1] = pulses[i];
|
||||
pulses[pulses.length - 1] = index;
|
||||
for (int i = 1; i < lines.length; ++i)
|
||||
lines[i - 1] = lines[i];
|
||||
lines[lines.length - 1] = pulses[pulses.length - 1] - pulses[pulses.length - 2];
|
||||
private boolean processSyncPulse(ArrayList<Mode> modes, float[] freqOffs, int[] syncIndexes, int[] lineLengths, int latestSyncIndex) {
|
||||
for (int i = 1; i < syncIndexes.length; ++i)
|
||||
syncIndexes[i - 1] = syncIndexes[i];
|
||||
syncIndexes[syncIndexes.length - 1] = latestSyncIndex;
|
||||
for (int i = 1; i < lineLengths.length; ++i)
|
||||
lineLengths[i - 1] = lineLengths[i];
|
||||
lineLengths[lineLengths.length - 1] = syncIndexes[syncIndexes.length - 1] - syncIndexes[syncIndexes.length - 2];
|
||||
for (int i = 1; i < freqOffs.length; ++i)
|
||||
freqOffs[i - 1] = freqOffs[i];
|
||||
freqOffs[pulses.length - 1] = demodulator.frequencyOffset;
|
||||
if (lines[0] == 0)
|
||||
freqOffs[syncIndexes.length - 1] = demodulator.frequencyOffset;
|
||||
if (lineLengths[0] == 0)
|
||||
return false;
|
||||
double mean = scanLineMean(lines);
|
||||
double mean = scanLineMean(lineLengths);
|
||||
int scanLineSamples = (int) Math.round(mean);
|
||||
if (scanLineSamples < scanLineMinSamples || scanLineSamples > scratchBuffer.length)
|
||||
return false;
|
||||
if (scanLineStdDev(lines, mean) > scanLineToleranceSamples)
|
||||
if (scanLineStdDev(lineLengths, mean) > scanLineToleranceSamples)
|
||||
return false;
|
||||
boolean pictureChanged = false;
|
||||
if (lockMode || imageBuffer.line >= 0 && imageBuffer.line < imageBuffer.height) {
|
||||
|
|
@ -377,7 +381,7 @@ public class Decoder {
|
|||
currentMode = detectMode(modes, scanLineSamples);
|
||||
pictureChanged = currentMode != prevMode
|
||||
|| Math.abs(currentScanLineSamples - scanLineSamples) > scanLineToleranceSamples
|
||||
|| Math.abs(lastSyncPulseIndex + scanLineSamples - pulses[pulses.length - 1]) > syncPulseToleranceSamples;
|
||||
|| Math.abs(lastSyncPulseIndex + scanLineSamples - syncIndexes[syncIndexes.length - 1]) > syncPulseToleranceSamples;
|
||||
}
|
||||
if (pictureChanged) {
|
||||
drawLines(0xff000000, 10);
|
||||
|
|
@ -385,23 +389,24 @@ public class Decoder {
|
|||
drawLines(0xff000000, 10);
|
||||
}
|
||||
float frequencyOffset = (float) frequencyOffsetMean(freqOffs);
|
||||
if (pulses[0] >= scanLineSamples && pictureChanged) {
|
||||
int endPulse = pulses[0];
|
||||
if (syncIndexes[0] >= scanLineSamples && pictureChanged) {
|
||||
int endPulse = syncIndexes[0];
|
||||
int extrapolate = endPulse / scanLineSamples;
|
||||
int firstPulse = endPulse - extrapolate * scanLineSamples;
|
||||
for (int pulseIndex = firstPulse; pulseIndex < endPulse; pulseIndex += scanLineSamples)
|
||||
copyLines(currentMode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, pulseIndex, scanLineSamples, frequencyOffset));
|
||||
}
|
||||
for (int i = pictureChanged ? 0 : lines.length - 1; i < lines.length; ++i)
|
||||
copyLines(currentMode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, pulses[i], lines[i], frequencyOffset));
|
||||
lastSyncPulseIndex = pulses[pulses.length - 1];
|
||||
for (int i = pictureChanged ? 0 : lineLengths.length - 1; i < lineLengths.length; ++i)
|
||||
copyLines(currentMode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, syncIndexes[i], lineLengths[i], frequencyOffset));
|
||||
lastSyncPulseIndex = syncIndexes[syncIndexes.length - 1];
|
||||
currentScanLineSamples = scanLineSamples;
|
||||
lastFrequencyOffset = frequencyOffset;
|
||||
shiftSamples(lastSyncPulseIndex + currentMode.getBegin());
|
||||
shiftSamples(lastSyncPulseIndex + currentMode.getFirstPixelSampleIndex());
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean process(float[] recordBuffer, int channelSelect) {
|
||||
boolean newLinesPresent = false;
|
||||
boolean syncPulseDetected = demodulator.process(recordBuffer, channelSelect);
|
||||
int syncPulseIndex = currentSample + demodulator.syncPulseOffset;
|
||||
int channels = channelSelect > 0 ? 2 : 1;
|
||||
|
|
@ -415,25 +420,28 @@ public class Decoder {
|
|||
if (syncPulseDetected) {
|
||||
switch (demodulator.syncPulseWidth) {
|
||||
case FiveMilliSeconds:
|
||||
return processSyncPulse(syncPulse5msModes, last5msFrequencyOffsets, last5msSyncPulses, last5msScanLines, syncPulseIndex);
|
||||
newLinesPresent = processSyncPulse(syncPulse5msModes, last5msFrequencyOffsets, last5msSyncPulses, last5msScanLines, syncPulseIndex);
|
||||
break;
|
||||
case NineMilliSeconds:
|
||||
leaderBreakIndex = syncPulseIndex;
|
||||
return processSyncPulse(syncPulse9msModes, last9msFrequencyOffsets, last9msSyncPulses, last9msScanLines, syncPulseIndex);
|
||||
newLinesPresent = processSyncPulse(syncPulse9msModes, last9msFrequencyOffsets, last9msSyncPulses, last9msScanLines, syncPulseIndex);
|
||||
break;
|
||||
case TwentyMilliSeconds:
|
||||
leaderBreakIndex = syncPulseIndex;
|
||||
return processSyncPulse(syncPulse20msModes, last20msFrequencyOffsets, last20msSyncPulses, last20msScanLines, syncPulseIndex);
|
||||
newLinesPresent = processSyncPulse(syncPulse20msModes, last20msFrequencyOffsets, last20msSyncPulses, last20msScanLines, syncPulseIndex);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (handleHeader())
|
||||
return true;
|
||||
if (currentSample > lastSyncPulseIndex + (currentScanLineSamples * 5) / 4) {
|
||||
} else if (handleHeader()) {
|
||||
newLinesPresent = true;
|
||||
} else if (currentSample > lastSyncPulseIndex + (currentScanLineSamples * 5) / 4) {
|
||||
copyLines(currentMode.decodeScanLine(pixelBuffer, scratchBuffer, scanLineBuffer, scopeBuffer.width, lastSyncPulseIndex, currentScanLineSamples, lastFrequencyOffset));
|
||||
lastSyncPulseIndex += currentScanLineSamples;
|
||||
return true;
|
||||
newLinesPresent = true;
|
||||
}
|
||||
return false;
|
||||
|
||||
return newLinesPresent;
|
||||
}
|
||||
|
||||
public void setMode(String name) {
|
||||
|
|
@ -448,6 +456,8 @@ public class Decoder {
|
|||
mode = findMode(syncPulse9msModes, name);
|
||||
if (mode == null)
|
||||
mode = findMode(syncPulse20msModes, name);
|
||||
if (mode == null && hfFaxMode.getName().equals(name))
|
||||
mode = hfFaxMode;
|
||||
if (mode == currentMode) {
|
||||
lockMode = true;
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ public class Demodulator {
|
|||
private final SchmittTrigger syncPulseTrigger;
|
||||
private final Phasor baseBandOscillator;
|
||||
private final Delay syncPulseValueDelay;
|
||||
private final double scanLineBandwidth;
|
||||
private final double centerFrequency;
|
||||
private final float syncPulseFrequencyValue;
|
||||
private final float syncPulseFrequencyTolerance;
|
||||
private final int syncPulse5msMinSamples;
|
||||
|
|
@ -33,10 +35,12 @@ public class Demodulator {
|
|||
public int syncPulseOffset;
|
||||
public float frequencyOffset;
|
||||
|
||||
public static final double syncPulseFrequency = 1200;
|
||||
public static final double blackFrequency = 1500;
|
||||
public static final double whiteFrequency = 2300;
|
||||
|
||||
Demodulator(int sampleRate) {
|
||||
double blackFrequency = 1500;
|
||||
double whiteFrequency = 2300;
|
||||
double scanLineBandwidth = whiteFrequency - blackFrequency;
|
||||
scanLineBandwidth = whiteFrequency - blackFrequency;
|
||||
frequencyModulation = new FrequencyModulation(scanLineBandwidth, sampleRate);
|
||||
double syncPulse5msSeconds = 0.005;
|
||||
double syncPulse9msSeconds = 0.009;
|
||||
|
|
@ -63,20 +67,23 @@ public class Demodulator {
|
|||
Kaiser kaiser = new Kaiser();
|
||||
for (int i = 0; i < baseBandLowPass.length; ++i)
|
||||
baseBandLowPass.taps[i] = (float) (kaiser.window(2.0, i, baseBandLowPass.length) * Filter.lowPass(cutoffFrequency, sampleRate, i, baseBandLowPass.length));
|
||||
double centerFrequency = (lowestFrequency + highestFrequency) / 2;
|
||||
centerFrequency = (lowestFrequency + highestFrequency) / 2;
|
||||
baseBandOscillator = new Phasor(-centerFrequency, sampleRate);
|
||||
double syncPulseFrequency = 1200;
|
||||
syncPulseFrequencyValue = (float) ((syncPulseFrequency - centerFrequency) * 2 / scanLineBandwidth);
|
||||
syncPulseFrequencyValue = (float) normalizeFrequency(syncPulseFrequency);
|
||||
syncPulseFrequencyTolerance = (float) (50 * 2 / scanLineBandwidth);
|
||||
double syncPorchFrequency = 1500;
|
||||
double syncHighFrequency = (syncPulseFrequency + syncPorchFrequency) / 2;
|
||||
double syncLowFrequency = (syncPulseFrequency + syncHighFrequency) / 2;
|
||||
double syncLowValue = (syncLowFrequency - centerFrequency) * 2 / scanLineBandwidth;
|
||||
double syncHighValue = (syncHighFrequency - centerFrequency) * 2 / scanLineBandwidth;
|
||||
double syncLowValue = normalizeFrequency(syncLowFrequency);
|
||||
double syncHighValue = normalizeFrequency(syncHighFrequency);
|
||||
syncPulseTrigger = new SchmittTrigger((float) syncLowValue, (float) syncHighValue);
|
||||
baseBand = new Complex();
|
||||
}
|
||||
|
||||
private double normalizeFrequency(double frequency) {
|
||||
return (frequency - centerFrequency) * 2 / scanLineBandwidth;
|
||||
}
|
||||
|
||||
public boolean process(float[] buffer, int channelSelect) {
|
||||
boolean syncPulseDetected = false;
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@ import androidx.appcompat.widget.ShareActionProvider;
|
|||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.os.LocaleListCompat;
|
||||
import androidx.core.view.MenuItemCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
|
@ -63,24 +64,33 @@ public class MainActivity extends AppCompatActivity {
|
|||
private Bitmap scopeBitmap;
|
||||
private PixelBuffer scopeBuffer;
|
||||
private ImageView scopeView;
|
||||
private Bitmap freqPlotBitmap;
|
||||
private PixelBuffer freqPlotBuffer;
|
||||
private ImageView freqPlotView;
|
||||
private Bitmap waterfallPlotBitmap;
|
||||
private PixelBuffer waterfallPlotBuffer;
|
||||
private ImageView waterfallPlotView;
|
||||
private Bitmap peakMeterBitmap;
|
||||
private PixelBuffer peakMeterBuffer;
|
||||
private ImageView peakMeterView;
|
||||
private PixelBuffer imageBuffer;
|
||||
private ShortTimeFourierTransform stft;
|
||||
private short[] shortBuffer;
|
||||
private float[] recordBuffer;
|
||||
private AudioRecord audioRecord;
|
||||
private Decoder decoder;
|
||||
private Menu menu;
|
||||
private String currentMode;
|
||||
private String language;
|
||||
private Complex input;
|
||||
private int recordRate;
|
||||
private int recordChannel;
|
||||
private int audioSource;
|
||||
private int audioFormat;
|
||||
private int fgColor;
|
||||
private int thinColor;
|
||||
private int tintColor;
|
||||
private boolean autoSave;
|
||||
private boolean showSpectrogram;
|
||||
private final int binWidthHz = 10;
|
||||
private final int[] freqMarkers = { 1100, 1300, 1500, 2300 };
|
||||
|
||||
private void setStatus(int id) {
|
||||
setTitle(id);
|
||||
|
|
@ -124,9 +134,18 @@ public class MainActivity extends AppCompatActivity {
|
|||
|
||||
@Override
|
||||
public void onPeriodicNotification(AudioRecord audioRecord) {
|
||||
if (shortBuffer == null) {
|
||||
audioRecord.read(recordBuffer, 0, recordBuffer.length, AudioRecord.READ_BLOCKING);
|
||||
} else {
|
||||
audioRecord.read(shortBuffer, 0, shortBuffer.length, AudioRecord.READ_BLOCKING);
|
||||
for (int i = 0; i < shortBuffer.length; ++i)
|
||||
recordBuffer[i] = .000030517578125f * shortBuffer[i];
|
||||
}
|
||||
processPeakMeter();
|
||||
if (showSpectrogram)
|
||||
processSpectrogram();
|
||||
boolean newLines = decoder.process(recordBuffer, recordChannel);
|
||||
if (!showSpectrogram)
|
||||
processFreqPlot();
|
||||
if (newLines) {
|
||||
processScope();
|
||||
|
|
@ -150,29 +169,104 @@ public class MainActivity extends AppCompatActivity {
|
|||
peakMeterView.invalidate();
|
||||
}
|
||||
|
||||
private double clamp(double x) {
|
||||
return Math.min(Math.max(x, 0), 1);
|
||||
}
|
||||
|
||||
private int argb(double a, double r, double g, double b) {
|
||||
a = clamp(a);
|
||||
r = clamp(r);
|
||||
g = clamp(g);
|
||||
b = clamp(b);
|
||||
r *= a;
|
||||
g *= a;
|
||||
b *= a;
|
||||
r = Math.sqrt(r);
|
||||
g = Math.sqrt(g);
|
||||
b = Math.sqrt(b);
|
||||
int A = (int) Math.rint(255 * a);
|
||||
int R = (int) Math.rint(255 * r);
|
||||
int G = (int) Math.rint(255 * g);
|
||||
int B = (int) Math.rint(255 * b);
|
||||
return (A << 24) | (R << 16) | (G << 8) | B;
|
||||
}
|
||||
|
||||
private int rainbow(double v) {
|
||||
v = clamp(v);
|
||||
double t = 4 * v - 2;
|
||||
return argb(4 * v, t, 1 - Math.abs(t), -t);
|
||||
}
|
||||
|
||||
private void processSpectrogram() {
|
||||
boolean process = false;
|
||||
int channels = recordChannel > 0 ? 2 : 1;
|
||||
for (int j = 0; j < recordBuffer.length / channels; ++j) {
|
||||
switch (recordChannel) {
|
||||
case 1:
|
||||
input.set(recordBuffer[2 * j]);
|
||||
break;
|
||||
case 2:
|
||||
input.set(recordBuffer[2 * j + 1]);
|
||||
break;
|
||||
case 3:
|
||||
input.set(recordBuffer[2 * j] + recordBuffer[2 * j + 1]);
|
||||
break;
|
||||
case 4:
|
||||
input.set(recordBuffer[2 * j], recordBuffer[2 * j + 1]);
|
||||
break;
|
||||
default:
|
||||
input.set(recordBuffer[j]);
|
||||
}
|
||||
if (stft.push(input)) {
|
||||
process = true;
|
||||
int stride = waterfallPlotBuffer.width;
|
||||
waterfallPlotBuffer.line = (waterfallPlotBuffer.line + waterfallPlotBuffer.height / 2 - 1) % (waterfallPlotBuffer.height / 2);
|
||||
int line = stride * waterfallPlotBuffer.line;
|
||||
double lowest = Math.log(1e-9);
|
||||
double highest = Math.log(1);
|
||||
double range = highest - lowest;
|
||||
int minFreq = 140;
|
||||
int minBin = minFreq / binWidthHz;
|
||||
for (int i = 0; i < stride; ++i)
|
||||
waterfallPlotBuffer.pixels[line + i] = rainbow((Math.log(stft.power[i + minBin]) - lowest) / range);
|
||||
for (int freq : freqMarkers)
|
||||
waterfallPlotBuffer.pixels[line + (freq - minFreq) / binWidthHz] = fgColor;
|
||||
System.arraycopy(waterfallPlotBuffer.pixels, line, waterfallPlotBuffer.pixels, line + stride * (waterfallPlotBuffer.height / 2), stride);
|
||||
}
|
||||
}
|
||||
if (process) {
|
||||
int width = waterfallPlotBitmap.getWidth();
|
||||
int height = waterfallPlotBitmap.getHeight();
|
||||
int stride = waterfallPlotBuffer.width;
|
||||
int offset = stride * waterfallPlotBuffer.line;
|
||||
waterfallPlotBitmap.setPixels(waterfallPlotBuffer.pixels, offset, stride, 0, 0, width, height);
|
||||
waterfallPlotView.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private void processFreqPlot() {
|
||||
int width = freqPlotBitmap.getWidth();
|
||||
int height = freqPlotBitmap.getHeight();
|
||||
int stride = freqPlotBuffer.width;
|
||||
int line = stride * freqPlotBuffer.line;
|
||||
int width = waterfallPlotBitmap.getWidth();
|
||||
int height = waterfallPlotBitmap.getHeight();
|
||||
int stride = waterfallPlotBuffer.width;
|
||||
waterfallPlotBuffer.line = (waterfallPlotBuffer.line + waterfallPlotBuffer.height / 2 - 1) % (waterfallPlotBuffer.height / 2);
|
||||
int line = stride * waterfallPlotBuffer.line;
|
||||
int channels = recordChannel > 0 ? 2 : 1;
|
||||
int samples = recordBuffer.length / channels;
|
||||
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) {
|
||||
int x = Math.round((recordBuffer[i] + 2.5f) * 0.25f * stride);
|
||||
if (x >= spread && x < stride - spread)
|
||||
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;
|
||||
for (int i = 0; i < stride; ++i)
|
||||
freqPlotBuffer.pixels[line + i] = 0x00FFFFFF & fgColor | Math.min(factor * freqPlotBuffer.pixels[line + i], 255) << 24;
|
||||
System.arraycopy(freqPlotBuffer.pixels, line, freqPlotBuffer.pixels, line + stride * (freqPlotBuffer.height / 2), stride);
|
||||
freqPlotBuffer.line = (freqPlotBuffer.line + 1) % (freqPlotBuffer.height / 2);
|
||||
int offset = stride * (freqPlotBuffer.line + freqPlotBuffer.height / 2 - height);
|
||||
freqPlotBitmap.setPixels(freqPlotBuffer.pixels, offset, stride, 0, 0, width, height);
|
||||
freqPlotView.invalidate();
|
||||
waterfallPlotBuffer.pixels[line + i] = 0x00FFFFFF & fgColor | Math.min(factor * waterfallPlotBuffer.pixels[line + i], 255) << 24;
|
||||
System.arraycopy(waterfallPlotBuffer.pixels, line, waterfallPlotBuffer.pixels, line + stride * (waterfallPlotBuffer.height / 2), stride);
|
||||
int offset = stride * waterfallPlotBuffer.line;
|
||||
waterfallPlotBitmap.setPixels(waterfallPlotBuffer.pixels, offset, stride, 0, 0, width, height);
|
||||
waterfallPlotView.invalidate();
|
||||
}
|
||||
|
||||
private void processScope() {
|
||||
|
|
@ -188,6 +282,7 @@ public class MainActivity extends AppCompatActivity {
|
|||
if (imageBuffer.line < imageBuffer.height)
|
||||
return;
|
||||
imageBuffer.line = -1;
|
||||
if (autoSave)
|
||||
storeBitmap(Bitmap.createBitmap(imageBuffer.pixels, imageBuffer.width, imageBuffer.height, Bitmap.Config.ARGB_8888));
|
||||
}
|
||||
|
||||
|
|
@ -197,7 +292,8 @@ public class MainActivity extends AppCompatActivity {
|
|||
rateChanged = audioRecord.getSampleRate() != recordRate;
|
||||
boolean channelChanged = audioRecord.getChannelCount() != (recordChannel == 0 ? 1 : 2);
|
||||
boolean sourceChanged = audioRecord.getAudioSource() != audioSource;
|
||||
if (!rateChanged && !channelChanged && !sourceChanged)
|
||||
boolean formatChanged = audioRecord.getAudioFormat() != audioFormat;
|
||||
if (!rateChanged && !channelChanged && !sourceChanged && !formatChanged)
|
||||
return;
|
||||
stopListening();
|
||||
audioRecord.release();
|
||||
|
|
@ -209,13 +305,14 @@ public class MainActivity extends AppCompatActivity {
|
|||
channelCount = 2;
|
||||
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
|
||||
}
|
||||
int sampleSize = 4;
|
||||
int sampleSize = audioFormat == AudioFormat.ENCODING_PCM_FLOAT ? 4 : 2;
|
||||
int frameSize = sampleSize * channelCount;
|
||||
int audioFormat = AudioFormat.ENCODING_PCM_FLOAT;
|
||||
int readsPerSecond = 50;
|
||||
int bufferSize = Integer.highestOneBit(recordRate) * frameSize;
|
||||
int frameCount = recordRate / readsPerSecond;
|
||||
recordBuffer = new float[frameCount * channelCount];
|
||||
int bufferCount = frameCount * channelCount;
|
||||
recordBuffer = new float[bufferCount];
|
||||
shortBuffer = audioFormat == AudioFormat.ENCODING_PCM_FLOAT ? null : new short[bufferCount];
|
||||
try {
|
||||
audioRecord = new AudioRecord(audioSource, recordRate, channelConfig, audioFormat, bufferSize);
|
||||
if (audioRecord.getState() == AudioRecord.STATE_INITIALIZED) {
|
||||
|
|
@ -224,9 +321,12 @@ public class MainActivity extends AppCompatActivity {
|
|||
if (rateChanged) {
|
||||
decoder = new Decoder(scopeBuffer, imageBuffer, getString(R.string.raw_mode), recordRate);
|
||||
decoder.setMode(currentMode);
|
||||
stft = new ShortTimeFourierTransform(recordRate / binWidthHz, 3);
|
||||
}
|
||||
startListening();
|
||||
} else {
|
||||
audioRecord.release();
|
||||
audioRecord = null;
|
||||
setStatus(R.string.audio_init_failed);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
|
|
@ -240,7 +340,10 @@ public class MainActivity extends AppCompatActivity {
|
|||
if (audioRecord != null) {
|
||||
audioRecord.startRecording();
|
||||
if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
|
||||
if (shortBuffer == null)
|
||||
audioRecord.read(recordBuffer, 0, recordBuffer.length, AudioRecord.READ_BLOCKING);
|
||||
else
|
||||
audioRecord.read(shortBuffer, 0, recordBuffer.length, AudioRecord.READ_BLOCKING);
|
||||
setStatus(R.string.listening);
|
||||
} else {
|
||||
setStatus(R.string.audio_recording_error);
|
||||
|
|
@ -277,6 +380,42 @@ public class MainActivity extends AppCompatActivity {
|
|||
initAudioRecord();
|
||||
}
|
||||
|
||||
private void setAudioFormat(int newAudioFormat) {
|
||||
if (audioFormat == newAudioFormat)
|
||||
return;
|
||||
audioFormat = newAudioFormat;
|
||||
updateAudioFormatMenu();
|
||||
initAudioRecord();
|
||||
}
|
||||
|
||||
private void setShowSpectrogram(boolean newShowSpectrogram) {
|
||||
if (showSpectrogram == newShowSpectrogram)
|
||||
return;
|
||||
showSpectrogram = newShowSpectrogram;
|
||||
updateWaterfallPlotMenu();
|
||||
}
|
||||
|
||||
private void updateWaterfallPlotMenu() {
|
||||
if (showSpectrogram)
|
||||
menu.findItem(R.id.action_show_spectrogram).setChecked(true);
|
||||
else
|
||||
menu.findItem(R.id.action_show_frequency_plot).setChecked(true);
|
||||
}
|
||||
|
||||
private void setAutoSave(boolean newAutoSave) {
|
||||
if (autoSave == newAutoSave)
|
||||
return;
|
||||
autoSave = newAutoSave;
|
||||
updateAutoSaveMenu();
|
||||
}
|
||||
|
||||
private void updateAutoSaveMenu() {
|
||||
if (autoSave)
|
||||
menu.findItem(R.id.action_enable_auto_save).setChecked(true);
|
||||
else
|
||||
menu.findItem(R.id.action_disable_auto_save).setChecked(true);
|
||||
}
|
||||
|
||||
private void updateRecordRateMenu() {
|
||||
switch (recordRate) {
|
||||
case 8000:
|
||||
|
|
@ -337,6 +476,10 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
|
||||
private void updateAudioFormatMenu() {
|
||||
menu.findItem(audioFormat == AudioFormat.ENCODING_PCM_FLOAT ? R.id.action_set_floating_point : R.id.action_set_fixed_point).setChecked(true);
|
||||
}
|
||||
|
||||
private final int permissionID = 1;
|
||||
|
||||
@Override
|
||||
|
|
@ -355,6 +498,10 @@ public class MainActivity extends AppCompatActivity {
|
|||
state.putInt("recordRate", recordRate);
|
||||
state.putInt("recordChannel", recordChannel);
|
||||
state.putInt("audioSource", audioSource);
|
||||
state.putInt("audioFormat", audioFormat);
|
||||
state.putBoolean("autoSave", autoSave);
|
||||
state.putBoolean("showSpectrogram", showSpectrogram);
|
||||
state.putString("language", language);
|
||||
super.onSaveInstanceState(state);
|
||||
}
|
||||
|
||||
|
|
@ -365,6 +512,10 @@ public class MainActivity extends AppCompatActivity {
|
|||
edit.putInt("recordRate", recordRate);
|
||||
edit.putInt("recordChannel", recordChannel);
|
||||
edit.putInt("audioSource", audioSource);
|
||||
edit.putInt("audioFormat", audioFormat);
|
||||
edit.putBoolean("autoSave", autoSave);
|
||||
edit.putBoolean("showSpectrogram", showSpectrogram);
|
||||
edit.putString("language", language);
|
||||
edit.apply();
|
||||
}
|
||||
|
||||
|
|
@ -373,19 +524,32 @@ public class MainActivity extends AppCompatActivity {
|
|||
final int defaultSampleRate = 44100;
|
||||
final int defaultChannelSelect = 0;
|
||||
final int defaultAudioSource = MediaRecorder.AudioSource.MIC;
|
||||
final int defaultAudioFormat = AudioFormat.ENCODING_PCM_16BIT;
|
||||
final boolean defaultAutoSave = true;
|
||||
final boolean defaultShowSpectrogram = true;
|
||||
final String defaultLanguage = "system";
|
||||
if (state == null) {
|
||||
SharedPreferences pref = getPreferences(Context.MODE_PRIVATE);
|
||||
AppCompatDelegate.setDefaultNightMode(pref.getInt("nightMode", AppCompatDelegate.getDefaultNightMode()));
|
||||
recordRate = pref.getInt("recordRate", defaultSampleRate);
|
||||
recordChannel = pref.getInt("recordChannel", defaultChannelSelect);
|
||||
audioSource = pref.getInt("audioSource", defaultAudioSource);
|
||||
audioFormat = pref.getInt("audioFormat", defaultAudioFormat);
|
||||
autoSave = pref.getBoolean("autoSave", defaultAutoSave);
|
||||
showSpectrogram = pref.getBoolean("showSpectrogram", defaultShowSpectrogram);
|
||||
language = pref.getString("language", defaultLanguage);
|
||||
} else {
|
||||
AppCompatDelegate.setDefaultNightMode(state.getInt("nightMode", AppCompatDelegate.getDefaultNightMode()));
|
||||
recordRate = state.getInt("recordRate", defaultSampleRate);
|
||||
recordChannel = state.getInt("recordChannel", defaultChannelSelect);
|
||||
audioSource = state.getInt("audioSource", defaultAudioSource);
|
||||
audioFormat = state.getInt("audioFormat", defaultAudioFormat);
|
||||
autoSave = state.getBoolean("autoSave", defaultAutoSave);
|
||||
showSpectrogram = state.getBoolean("showSpectrogram", defaultShowSpectrogram);
|
||||
language = state.getString("language", defaultLanguage);
|
||||
}
|
||||
super.onCreate(state);
|
||||
setLanguage(language);
|
||||
Configuration config = getResources().getConfiguration();
|
||||
EdgeToEdge.enable(this);
|
||||
setContentView(config.orientation == Configuration.ORIENTATION_LANDSCAPE ? R.layout.activity_main_land : R.layout.activity_main);
|
||||
|
|
@ -394,11 +558,12 @@ public class MainActivity extends AppCompatActivity {
|
|||
thinColor = getColor(R.color.thin);
|
||||
tintColor = getColor(R.color.tint);
|
||||
scopeBuffer = new PixelBuffer(640, 2 * 1280);
|
||||
freqPlotBuffer = new PixelBuffer(256, 2 * 256);
|
||||
waterfallPlotBuffer = new PixelBuffer(256, 2 * 256);
|
||||
peakMeterBuffer = new PixelBuffer(1, 16);
|
||||
imageBuffer = new PixelBuffer(640, 496);
|
||||
imageBuffer = new PixelBuffer(800, 616);
|
||||
input = new Complex();
|
||||
createScope(config);
|
||||
createFreqPlot(config);
|
||||
createWaterfallPlot(config);
|
||||
createPeakMeter();
|
||||
List<String> permissions = new ArrayList<>();
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||
|
|
@ -428,6 +593,9 @@ public class MainActivity extends AppCompatActivity {
|
|||
updateRecordRateMenu();
|
||||
updateRecordChannelMenu();
|
||||
updateAudioSourceMenu();
|
||||
updateAudioFormatMenu();
|
||||
updateWaterfallPlotMenu();
|
||||
updateAutoSaveMenu();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -450,6 +618,10 @@ public class MainActivity extends AppCompatActivity {
|
|||
setMode(R.string.raw_mode);
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_force_hffax_mode) {
|
||||
setMode(R.string.hf_fax);
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_force_robot36_color) {
|
||||
setMode(R.string.robot36_color);
|
||||
return true;
|
||||
|
|
@ -482,6 +654,10 @@ public class MainActivity extends AppCompatActivity {
|
|||
setMode(R.string.pd240);
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_force_pd290) {
|
||||
setMode(R.string.pd290);
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_force_martin1) {
|
||||
setMode(R.string.martin1);
|
||||
return true;
|
||||
|
|
@ -566,6 +742,30 @@ public class MainActivity extends AppCompatActivity {
|
|||
setAudioSource(MediaRecorder.AudioSource.UNPROCESSED);
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_set_floating_point) {
|
||||
setAudioFormat(AudioFormat.ENCODING_PCM_FLOAT);
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_set_fixed_point) {
|
||||
setAudioFormat(AudioFormat.ENCODING_PCM_16BIT);
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_show_spectrogram) {
|
||||
setShowSpectrogram(true);
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_show_frequency_plot) {
|
||||
setShowSpectrogram(false);
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_enable_auto_save) {
|
||||
setAutoSave(true);
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_disable_auto_save) {
|
||||
setAutoSave(false);
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_enable_night_mode) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
return true;
|
||||
|
|
@ -579,28 +779,77 @@ public class MainActivity extends AppCompatActivity {
|
|||
return true;
|
||||
}
|
||||
if (id == R.id.action_about) {
|
||||
showTextPage(getString(R.string.about_text, BuildConfig.VERSION_NAME));
|
||||
showTextPage(getString(R.string.about_text, BuildConfig.VERSION_NAME, getString(R.string.disclaimer)));
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_english) {
|
||||
setLanguage("en-US");
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_simplified_chinese) {
|
||||
setLanguage("zh-CN");
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_russian) {
|
||||
setLanguage("ru");
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_german) {
|
||||
setLanguage("de");
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_brazilian_portuguese) {
|
||||
setLanguage("pt-BR");
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_polish) {
|
||||
setLanguage("pl");
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_ukrainian) {
|
||||
setLanguage("uk");
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_latin_american_spanish) {
|
||||
setLanguage("es-r419");
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.action_french) {
|
||||
setLanguage("fr");
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void setLanguage(String language) {
|
||||
this.language = language;
|
||||
if (!language.equals("system"))
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(language));
|
||||
}
|
||||
|
||||
private void storeScope() {
|
||||
int width = scopeBuffer.width;
|
||||
int height = scopeBuffer.height / 2;
|
||||
int stride = scopeBuffer.width;
|
||||
int offset = stride * scopeBuffer.line;
|
||||
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) {
|
||||
int screenWidthDp = config.screenWidthDp;
|
||||
int screenHeightDp = config.screenHeightDp;
|
||||
int freqPlotHeightDp = 64;
|
||||
int waterfallPlotHeightDp = 64;
|
||||
if (config.orientation == Configuration.ORIENTATION_LANDSCAPE)
|
||||
screenWidthDp /= 2;
|
||||
else
|
||||
screenHeightDp -= freqPlotHeightDp;
|
||||
screenHeightDp -= waterfallPlotHeightDp;
|
||||
int actionBarHeightDp = 64;
|
||||
screenHeightDp -= actionBarHeightDp;
|
||||
int width = scopeBuffer.width;
|
||||
|
|
@ -614,18 +863,18 @@ public class MainActivity extends AppCompatActivity {
|
|||
scopeView.setImageBitmap(scopeBitmap);
|
||||
}
|
||||
|
||||
private void createFreqPlot(Configuration config) {
|
||||
int width = freqPlotBuffer.width;
|
||||
int height = freqPlotBuffer.height / 2;
|
||||
private void createWaterfallPlot(Configuration config) {
|
||||
int width = waterfallPlotBuffer.width;
|
||||
int height = waterfallPlotBuffer.height / 2;
|
||||
if (config.orientation != Configuration.ORIENTATION_LANDSCAPE)
|
||||
height /= 4;
|
||||
freqPlotBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
int stride = freqPlotBuffer.width;
|
||||
int offset = stride * (freqPlotBuffer.line + freqPlotBuffer.height / 2 - height);
|
||||
freqPlotBitmap.setPixels(freqPlotBuffer.pixels, offset, stride, 0, 0, width, height);
|
||||
freqPlotView = findViewById(R.id.freq_plot);
|
||||
freqPlotView.setScaleType(ImageView.ScaleType.FIT_XY);
|
||||
freqPlotView.setImageBitmap(freqPlotBitmap);
|
||||
waterfallPlotBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
int stride = waterfallPlotBuffer.width;
|
||||
int offset = stride * waterfallPlotBuffer.line;
|
||||
waterfallPlotBitmap.setPixels(waterfallPlotBuffer.pixels, offset, stride, 0, 0, width, height);
|
||||
waterfallPlotView = findViewById(R.id.waterfall_plot);
|
||||
waterfallPlotView.setScaleType(ImageView.ScaleType.FIT_XY);
|
||||
waterfallPlotView.setImageBitmap(waterfallPlotBitmap);
|
||||
}
|
||||
|
||||
private void createPeakMeter() {
|
||||
|
|
@ -642,7 +891,7 @@ public class MainActivity extends AppCompatActivity {
|
|||
setContentView(config.orientation == Configuration.ORIENTATION_LANDSCAPE ? R.layout.activity_main_land : R.layout.activity_main);
|
||||
handleInsets();
|
||||
createScope(config);
|
||||
createFreqPlot(config);
|
||||
createWaterfallPlot(config);
|
||||
createPeakMeter();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,22 +6,30 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
|
|||
|
||||
package xdsopl.robot36;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
public interface Mode {
|
||||
String getName();
|
||||
|
||||
int getCode();
|
||||
int getVISCode();
|
||||
|
||||
int getWidth();
|
||||
|
||||
int getHeight();
|
||||
|
||||
int getBegin();
|
||||
int getFirstPixelSampleIndex();
|
||||
|
||||
int getFirstSyncPulseIndex();
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
|
|||
|
||||
package xdsopl.robot36;
|
||||
|
||||
public class PaulDon implements Mode {
|
||||
public class PaulDon extends BaseMode {
|
||||
private final ExponentialMovingAverage lowPassFilter;
|
||||
private final int horizontalPixels;
|
||||
private final int verticalPixels;
|
||||
|
|
@ -56,7 +56,7 @@ public class PaulDon implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int getCode() {
|
||||
public int getVISCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ public class PaulDon implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int getBegin() {
|
||||
public int getFirstPixelSampleIndex() {
|
||||
return beginSamples;
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ public class PaulDon implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
public void resetState() {
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
|
|||
|
||||
package xdsopl.robot36;
|
||||
|
||||
public class RGBDecoder implements Mode {
|
||||
public class RGBDecoder extends BaseMode {
|
||||
private final ExponentialMovingAverage lowPassFilter;
|
||||
private final int horizontalPixels;
|
||||
private final int verticalPixels;
|
||||
|
|
@ -51,7 +51,7 @@ public class RGBDecoder implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int getCode() {
|
||||
public int getVISCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ public class RGBDecoder implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int getBegin() {
|
||||
public int getFirstPixelSampleIndex() {
|
||||
return beginSamples;
|
||||
}
|
||||
|
||||
|
|
@ -81,7 +81,7 @@ public class RGBDecoder implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
public void resetState() {
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
|
|||
|
||||
package xdsopl.robot36;
|
||||
|
||||
public class RawDecoder implements Mode {
|
||||
public class RawDecoder extends BaseMode {
|
||||
private final ExponentialMovingAverage lowPassFilter;
|
||||
private final int smallPictureMaxSamples;
|
||||
private final int mediumPictureMaxSamples;
|
||||
|
|
@ -29,7 +29,7 @@ public class RawDecoder implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int getCode() {
|
||||
public int getVISCode() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ public class RawDecoder implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int getBegin() {
|
||||
public int getFirstPixelSampleIndex() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ public class RawDecoder implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
public void resetState() {
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
|
|||
|
||||
package xdsopl.robot36;
|
||||
|
||||
public class Robot_36_Color implements Mode {
|
||||
public class Robot_36_Color extends BaseMode {
|
||||
private final ExponentialMovingAverage lowPassFilter;
|
||||
private final int horizontalPixels;
|
||||
private final int verticalPixels;
|
||||
|
|
@ -59,7 +59,7 @@ public class Robot_36_Color implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int getCode() {
|
||||
public int getVISCode() {
|
||||
return 8;
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ public class Robot_36_Color implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int getBegin() {
|
||||
public int getFirstPixelSampleIndex() {
|
||||
return beginSamples;
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ public class Robot_36_Color implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
public void resetState() {
|
||||
lastEven = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
|
|||
|
||||
package xdsopl.robot36;
|
||||
|
||||
public class Robot_72_Color implements Mode {
|
||||
public class Robot_72_Color extends BaseMode {
|
||||
private final ExponentialMovingAverage lowPassFilter;
|
||||
private final int horizontalPixels;
|
||||
private final int verticalPixels;
|
||||
|
|
@ -57,7 +57,7 @@ public class Robot_72_Color implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int getCode() {
|
||||
public int getVISCode() {
|
||||
return 12;
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ public class Robot_72_Color implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int getBegin() {
|
||||
public int getFirstPixelSampleIndex() {
|
||||
return beginSamples;
|
||||
}
|
||||
|
||||
|
|
@ -87,7 +87,7 @@ public class Robot_72_Color implements Mode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
public void resetState() {
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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.9,21.9l-8.49,-8.49l0,0L3.59,3.59l0,0L2.1,2.1L0.69,3.51L3,5.83V19c0,1.1 0.9,2 2,2h13.17l2.31,2.31L21.9,21.9zM5,18l3.5,-4.5l2.5,3.01L12.17,15l3,3H5zM21,18.17L5.83,3H19c1.1,0 2,0.9 2,2V18.17z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/baseline_info_24.xml
Normal file
5
app/src/main/res/drawable/baseline_info_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="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/baseline_language_24.xml
Normal file
5
app/src/main/res/drawable/baseline_language_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="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM18.92,8h-2.95c-0.32,-1.25 -0.78,-2.45 -1.38,-3.56 1.84,0.63 3.37,1.91 4.33,3.56zM12,4.04c0.83,1.2 1.48,2.53 1.91,3.96h-3.82c0.43,-1.43 1.08,-2.76 1.91,-3.96zM4.26,14C4.1,13.36 4,12.69 4,12s0.1,-1.36 0.26,-2h3.38c-0.08,0.66 -0.14,1.32 -0.14,2 0,0.68 0.06,1.34 0.14,2L4.26,14zM5.08,16h2.95c0.32,1.25 0.78,2.45 1.38,3.56 -1.84,-0.63 -3.37,-1.9 -4.33,-3.56zM8.03,8L5.08,8c0.96,-1.66 2.49,-2.93 4.33,-3.56C8.81,5.55 8.35,6.75 8.03,8zM12,19.96c-0.83,-1.2 -1.48,-2.53 -1.91,-3.96h3.82c-0.43,1.43 -1.08,2.76 -1.91,3.96zM14.34,14L9.66,14c-0.09,-0.66 -0.16,-1.32 -0.16,-2 0,-0.68 0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2 0,0.68 -0.07,1.34 -0.16,2zM14.59,19.56c0.6,-1.11 1.06,-2.31 1.38,-3.56h2.95c-0.96,1.65 -2.49,2.93 -4.33,3.56zM16.36,14c0.08,-0.66 0.14,-1.32 0.14,-2 0,-0.68 -0.06,-1.34 -0.14,-2h3.38c0.16,0.64 0.26,1.31 0.26,2s-0.1,1.36 -0.26,2h-3.38z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/baseline_menu_24.xml
Normal file
5
app/src/main/res/drawable/baseline_menu_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="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/baseline_mode_night_24.xml
Normal file
5
app/src/main/res/drawable/baseline_mode_night_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="M9.5,2c-1.82,0 -3.53,0.5 -5,1.35c2.99,1.73 5,4.95 5,8.65s-2.01,6.92 -5,8.65C5.97,21.5 7.68,22 9.5,22c5.52,0 10,-4.48 10,-10S15.02,2 9.5,2z"/>
|
||||
|
||||
</vector>
|
||||
7
app/src/main/res/drawable/baseline_policy_24.xml
Normal file
7
app/src/main/res/drawable/baseline_policy_24.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M21,5l-9,-4L3,5v6c0,5.55 3.84,10.74 9,12c2.3,-0.56 4.33,-1.9 5.88,-3.71l-3.12,-3.12c-1.94,1.29 -4.58,1.07 -6.29,-0.64c-1.95,-1.95 -1.95,-5.12 0,-7.07c1.95,-1.95 5.12,-1.95 7.07,0c1.71,1.71 1.92,4.35 0.64,6.29l2.9,2.9C20.29,15.69 21,13.38 21,11V5z"/>
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,12m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/baseline_settings_voice_24.xml
Normal file
5
app/src/main/res/drawable/baseline_settings_voice_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="M7,24h2v-2L7,22v2zM12,13c1.66,0 2.99,-1.34 2.99,-3L15,4c0,-1.66 -1.34,-3 -3,-3S9,2.34 9,4v6c0,1.66 1.34,3 3,3zM11,24h2v-2h-2v2zM15,24h2v-2h-2v2zM19,10h-1.7c0,3 -2.54,5.1 -5.3,5.1S6.7,13 6.7,10L5,10c0,3.41 2.72,6.23 6,6.72L11,20h2v-3.28c3.28,-0.49 6,-3.31 6,-6.72z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/baseline_sunny_24.xml
Normal file
5
app/src/main/res/drawable/baseline_sunny_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="M11,4V2c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v2c0,0.55 -0.45,1 -1,1S11,4.55 11,4zM18.36,7.05l1.41,-1.42c0.39,-0.39 0.39,-1.02 0,-1.41c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.41,1.42c-0.39,0.39 -0.39,1.02 0,1.41C17.34,7.44 17.97,7.44 18.36,7.05zM22,11h-2c-0.55,0 -1,0.45 -1,1s0.45,1 1,1h2c0.55,0 1,-0.45 1,-1S22.55,11 22,11zM12,19c-0.55,0 -1,0.45 -1,1v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-2C13,19.45 12.55,19 12,19zM5.64,7.05L4.22,5.64c-0.39,-0.39 -0.39,-1.03 0,-1.41s1.03,-0.39 1.41,0l1.41,1.41c0.39,0.39 0.39,1.03 0,1.41S6.02,7.44 5.64,7.05zM16.95,16.95c-0.39,0.39 -0.39,1.03 0,1.41l1.41,1.41c0.39,0.39 1.03,0.39 1.41,0c0.39,-0.39 0.39,-1.03 0,-1.41l-1.41,-1.41C17.98,16.56 17.34,16.56 16.95,16.95zM2,13h2c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1H2c-0.55,0 -1,0.45 -1,1S1.45,13 2,13zM5.64,19.78l1.41,-1.41c0.39,-0.39 0.39,-1.03 0,-1.41s-1.03,-0.39 -1.41,0l-1.41,1.41c-0.39,0.39 -0.39,1.03 0,1.41C4.61,20.17 5.25,20.17 5.64,19.78zM12,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6s6,-2.69 6,-6S15.31,6 12,6z"/>
|
||||
|
||||
</vector>
|
||||
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_height="0dp"
|
||||
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_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/freq_plot"
|
||||
android:id="@+id/waterfall_plot"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="64dp"
|
||||
android:contentDescription="@string/freq_plot_description"
|
||||
android:contentDescription="@string/waterfall_plot"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/peak_meter"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
@ -35,6 +35,6 @@
|
|||
android:contentDescription="@string/peak_meter_description"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/freq_plot"
|
||||
app:layout_constraintStart_toEndOf="@+id/waterfall_plot"
|
||||
app:layout_constraintTop_toBottomOf="@+id/scope" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -14,15 +14,15 @@
|
|||
android:layout_height="0dp"
|
||||
android:contentDescription="@string/scope_description"
|
||||
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_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/freq_plot"
|
||||
android:id="@+id/waterfall_plot"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:contentDescription="@string/freq_plot_description"
|
||||
android:contentDescription="@string/waterfall_plot"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/peak_meter"
|
||||
app:layout_constraintStart_toEndOf="@+id/scope"
|
||||
|
|
@ -35,6 +35,6 @@
|
|||
android:contentDescription="@string/peak_meter_description"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/freq_plot"
|
||||
app:layout_constraintStart_toEndOf="@+id/waterfall_plot"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -21,7 +21,16 @@
|
|||
android:title="@string/share"
|
||||
app:actionProviderClass="androidx.appcompat.widget.ShareActionProvider"
|
||||
app:showAsAction="always" />
|
||||
<item android:title="@string/lock_mode">
|
||||
<item
|
||||
android:icon="@drawable/baseline_menu_24"
|
||||
android:title="@string/more_options"
|
||||
app:iconTint="@color/tint"
|
||||
app:showAsAction="always">
|
||||
<menu>
|
||||
<item
|
||||
android:icon="@drawable/baseline_lock_24"
|
||||
android:title="@string/lock_mode"
|
||||
app:iconTint="@color/tint">
|
||||
<menu>
|
||||
<item android:title="@string/robot_modes">
|
||||
<menu>
|
||||
|
|
@ -53,6 +62,9 @@
|
|||
<item
|
||||
android:id="@+id/action_force_pd240"
|
||||
android:title="@string/pd240" />
|
||||
<item
|
||||
android:id="@+id/action_force_pd290"
|
||||
android:title="@string/pd290" />
|
||||
</menu>
|
||||
</item>
|
||||
<item android:title="@string/martin_modes">
|
||||
|
|
@ -85,15 +97,29 @@
|
|||
android:title="@string/wraase_sc2_180" />
|
||||
</menu>
|
||||
</item>
|
||||
<item android:title="@string/contributed_modes">
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/action_force_raw_mode"
|
||||
android:title="@string/raw_mode" />
|
||||
<item
|
||||
android:id="@+id/action_auto_mode"
|
||||
android:title="@string/auto_mode" />
|
||||
android:id="@+id/action_force_hffax_mode"
|
||||
android:title="@string/hf_fax" />
|
||||
</menu>
|
||||
</item>
|
||||
<item android:title="@string/audio_settings">
|
||||
<item
|
||||
android:id="@+id/action_force_raw_mode"
|
||||
android:icon="@drawable/baseline_image_not_supported_24"
|
||||
android:title="@string/raw_mode"
|
||||
app:iconTint="@color/tint" />
|
||||
<item
|
||||
android:id="@+id/action_auto_mode"
|
||||
android:icon="@drawable/baseline_auto_mode_24"
|
||||
android:title="@string/auto_mode"
|
||||
app:iconTint="@color/tint" />
|
||||
</menu>
|
||||
</item>
|
||||
<item
|
||||
android:icon="@drawable/baseline_settings_voice_24"
|
||||
android:title="@string/audio_settings"
|
||||
app:iconTint="@color/tint">
|
||||
<menu>
|
||||
<item android:title="@string/sample_rate">
|
||||
<menu>
|
||||
|
|
@ -158,22 +184,117 @@
|
|||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
<item android:title="@string/audio_format">
|
||||
<menu>
|
||||
<group android:checkableBehavior="single">
|
||||
<item
|
||||
android:id="@+id/action_set_floating_point"
|
||||
android:title="@string/floating_point" />
|
||||
<item
|
||||
android:id="@+id/action_set_fixed_point"
|
||||
android:title="@string/fixed_point" />
|
||||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
<item android:title="@string/night_mode">
|
||||
</menu>
|
||||
</item>
|
||||
<item
|
||||
android:icon="@drawable/baseline_mode_night_24"
|
||||
android:title="@string/night_mode"
|
||||
app:iconTint="@color/tint">
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/action_enable_night_mode"
|
||||
android:title="@string/enable" />
|
||||
android:icon="@drawable/baseline_mode_night_24"
|
||||
android:title="@string/enable"
|
||||
app:iconTint="@color/tint" />
|
||||
<item
|
||||
android:id="@+id/action_disable_night_mode"
|
||||
android:title="@string/disable" />
|
||||
android:icon="@drawable/baseline_sunny_24"
|
||||
android:title="@string/disable"
|
||||
app:iconTint="@color/tint" />
|
||||
</menu>
|
||||
</item>
|
||||
<item
|
||||
android:icon="@drawable/baseline_water_24"
|
||||
android:title="@string/waterfall_plot"
|
||||
app:iconTint="@color/tint">
|
||||
<menu>
|
||||
<group android:checkableBehavior="single">
|
||||
<item
|
||||
android:id="@+id/action_show_spectrogram"
|
||||
android:title="@string/spectrogram"
|
||||
app:iconTint="@color/tint" />
|
||||
<item
|
||||
android:id="@+id/action_show_frequency_plot"
|
||||
android:title="@string/frequency_plot"
|
||||
app:iconTint="@color/tint" />
|
||||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
<item
|
||||
android:icon="@drawable/baseline_auto_mode_24"
|
||||
android:title="@string/auto_save"
|
||||
app:iconTint="@color/tint">
|
||||
<menu>
|
||||
<group android:checkableBehavior="single">
|
||||
<item
|
||||
android:id="@+id/action_enable_auto_save"
|
||||
android:icon="@drawable/baseline_save_alt_24"
|
||||
android:title="@string/enable"
|
||||
app:iconTint="@color/tint" />
|
||||
<item
|
||||
android:id="@+id/action_disable_auto_save"
|
||||
android:icon="@drawable/outline_scan_delete_24"
|
||||
android:title="@string/disable"
|
||||
app:iconTint="@color/tint" />
|
||||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
<item
|
||||
android:icon="@drawable/baseline_language_24"
|
||||
android:title="@string/language"
|
||||
app:iconTint="@color/tint">
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/action_english"
|
||||
android:title="@string/english" />
|
||||
<item
|
||||
android:id="@+id/action_simplified_chinese"
|
||||
android:title="@string/simplified_chinese" />
|
||||
<item
|
||||
android:id="@+id/action_russian"
|
||||
android:title="@string/russian" />
|
||||
<item
|
||||
android:id="@+id/action_german"
|
||||
android:title="@string/german" />
|
||||
<item
|
||||
android:id="@+id/action_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>
|
||||
</item>
|
||||
<item
|
||||
android:id="@+id/action_privacy_policy"
|
||||
android:title="@string/privacy_policy" />
|
||||
android:icon="@drawable/baseline_policy_24"
|
||||
android:title="@string/privacy_policy"
|
||||
app:iconTint="@color/tint" />
|
||||
<item
|
||||
android:id="@+id/action_about"
|
||||
android:title="@string/about" />
|
||||
android:icon="@drawable/baseline_info_24"
|
||||
android:title="@string/about"
|
||||
app:iconTint="@color/tint" />
|
||||
</menu>
|
||||
</item>
|
||||
</menu>
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="language">Sprache</string>
|
||||
<string name="share">Teilen</string>
|
||||
<string name="more_options">Weitere Optionen</string>
|
||||
<string name="store_scope">Schirm Speichern</string>
|
||||
<string name="toggle_mode">Modus umschalten</string>
|
||||
<string name="auto_mode">Automatikmodus</string>
|
||||
|
|
@ -21,6 +23,9 @@
|
|||
<string name="source_camcorder">Videokamera</string>
|
||||
<string name="source_voice_recognition">Spracherkennung</string>
|
||||
<string name="source_unprocessed">Unbearbeitet</string>
|
||||
<string name="audio_format">Audioformat</string>
|
||||
<string name="fixed_point">Festkomma</string>
|
||||
<string name="floating_point">Gleitkomma</string>
|
||||
<string name="audio_init_failed">Audioinitialisierung fehlgeschlagen</string>
|
||||
<string name="audio_setup_failed">Audioeinrichtung fehlgeschlagen</string>
|
||||
<string name="audio_permission_denied">Audioberechtigung verweigert</string>
|
||||
|
|
@ -29,8 +34,11 @@
|
|||
<string name="creating_picture_file_failed">Erstellen der Bilddatei fehlgeschlagen</string>
|
||||
<string name="storing_picture_failed">Speichern des Bildes fehlgeschlagen</string>
|
||||
<string name="scope_description">Dekodiertes SSTV-Bild</string>
|
||||
<string name="freq_plot_description">Frequenzdiagramm</string>
|
||||
<string name="peak_meter_description">Spitzenpegel des Audiosignals</string>
|
||||
<string name="waterfall_plot">Wasserfalldiagramm</string>
|
||||
<string name="frequency_plot">Frequenzdiagramm</string>
|
||||
<string name="spectrogram">Spektrogramm</string>
|
||||
<string name="auto_save">Automatisches Speichern</string>
|
||||
<string name="night_mode">Nachtmodus</string>
|
||||
<string name="enable">Aktivieren</string>
|
||||
<string name="disable">Deaktivieren</string>
|
||||
|
|
@ -45,8 +53,8 @@ Das Mikrofon erfasst den Ton, der das SSTV-Signal enthält.
|
|||
<p><h5>Datenverarbeitung</h5>
|
||||
Die App verwendet einen kleinen temporären Puffer im Speicher, um die Audiodaten in Echtzeit zu verarbeiten.
|
||||
Dieser Puffer wird kontinuierlich mit neuen Daten überschrieben, während die Dekodierung fortschreitet.
|
||||
Die App speichert <b>nicht</b> den rohen Ton, der vom Mikrofon erfasst wird.
|
||||
Nur die <b>dekodierten Bilder</b>, die aus dem SSTV-Prozess resultieren, werden auf Ihrem Gerät gespeichert.
|
||||
Die App speichert nicht den rohen Ton, der vom Mikrofon erfasst wird.
|
||||
Nur die dekodierten Bilder, die aus dem SSTV-Prozess resultieren, werden auf Ihrem Gerät gespeichert.
|
||||
</p>
|
||||
]]></string>
|
||||
<string name="about">Über Robot36</string>
|
||||
|
|
@ -56,6 +64,6 @@ Nur die <b>dekodierten Bilder</b>, die aus dem SSTV-Prozess resultieren, werden
|
|||
<p><h5>Beschreibung</h5>Decodiert Slow-Scan-Fernsehbilder aus Audio</p>
|
||||
<p><h5>Implementierung</h5><a href="https://github.com/xdsopl/robot36">Robot36 auf GitHub</a><br />BSD Zero Clause Lizenz</p>
|
||||
<p><h5>Modusspezifikationen</h5><a href="http://www.barberdsp.com/downloads/Dayton%%20Paper.pdf">Dayton Paper</a><br />von JL Barber - 2000</p>
|
||||
<p><h5>Haftungsausschluss</h5>THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.</p>
|
||||
<p><h5>Haftungsausschluss</h5>%2$s</p>
|
||||
]]></string>
|
||||
</resources>
|
||||
|
|
|
|||
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>
|
||||
69
app/src/main/res/values-pt-rBR/strings.xml
Executable file
69
app/src/main/res/values-pt-rBR/strings.xml
Executable file
|
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="language">Idioma</string>
|
||||
<string name="share">Compartilhar</string>
|
||||
<string name="more_options">Mais opções</string>
|
||||
<string name="store_scope">Salvar imagem</string>
|
||||
<string name="toggle_mode">Alternar modo</string>
|
||||
<string name="auto_mode">Modo automático</string>
|
||||
<string name="lock_mode">Modo de bloqueio</string>
|
||||
<string name="raw_mode">Modo Raw</string>
|
||||
<string name="listening">Ouvindo</string>
|
||||
<string name="audio_settings">Configurações de áudio</string>
|
||||
<string name="sample_rate">Taxa de amostragem</string>
|
||||
<string name="channel_select">Selecionar canal</string>
|
||||
<string name="channel_default">Padrão</string>
|
||||
<string name="channel_first">Primeiro</string>
|
||||
<string name="channel_second">Segundo</string>
|
||||
<string name="channel_summation">Somatório</string>
|
||||
<string name="channel_analytic">Analítico</string>
|
||||
<string name="audio_source">Fonte de áudio</string>
|
||||
<string name="source_default">Padrão</string>
|
||||
<string name="source_microphone">Microfone</string>
|
||||
<string name="source_camcorder">Filmadora</string>
|
||||
<string name="source_voice_recognition">Reconhecimento de voz</string>
|
||||
<string name="source_unprocessed">Não processado</string>
|
||||
<string name="audio_format">Formato de Áudio</string>
|
||||
<string name="fixed_point">Ponto Fixo</string>
|
||||
<string name="floating_point">Ponto Flutuante</string>
|
||||
<string name="audio_init_failed">Falha ao iniciar o áudio</string>
|
||||
<string name="audio_setup_failed">Falha na configuração de áudio</string>
|
||||
<string name="audio_permission_denied">Permissão de áudio negada</string>
|
||||
<string name="audio_recording_error">Erro de gravação de áudio</string>
|
||||
<string name="creating_picture_directory_failed">Falha ao criar o diretório de imagem</string>
|
||||
<string name="creating_picture_file_failed">Falha ao criar imagem</string>
|
||||
<string name="storing_picture_failed">Falha ao salvar imagem</string>
|
||||
<string name="scope_description">Imagem SSTV decodificada</string>
|
||||
<string name="peak_meter_description">Nível de sinal de áudio máximo</string>
|
||||
<string name="waterfall_plot">Gráfico de cascata</string>
|
||||
<string name="frequency_plot">Gráfico de frequência</string>
|
||||
<string name="spectrogram">Espectrograma</string>
|
||||
<string name="auto_save">Salvamento automático</string>
|
||||
<string name="night_mode">Modo noturno</string>
|
||||
<string name="enable">Habilitar</string>
|
||||
<string name="disable">Desabilitar</string>
|
||||
<string name="close">Fechar</string>
|
||||
<string name="privacy_policy">Política de privacidade</string>
|
||||
<string name="privacy_policy_text"><![CDATA[
|
||||
<p><h1>Política de privacidade</h1></p>
|
||||
<p><h5>Acesso ao microfone</h5>
|
||||
Este aplicativo requer acesso ao microfone do seu dispositivo para decodificar sinais de televisão de varredura lenta (SSTV).
|
||||
O microfone captura o áudio contendo a transmissão SSTV.
|
||||
</p>
|
||||
<p><h5>Manipulação de dados</h5>
|
||||
O aplicativo utiliza um pequeno buffer temporário na memória para processar os dados de áudio em tempo real.
|
||||
Este buffer é constantemente sobrescrito com novos dados à medida que a decodificação avança.
|
||||
O aplicativo não armazena o áudio bruto capturado pelo microfone.
|
||||
Apenas as imagens decodificadas resultantes do processo SSTV são salvas no armazenamento do seu dispositivo.
|
||||
</p>
|
||||
]]></string>
|
||||
<string name="about">Sobre Robot36</string>
|
||||
<string name="about_text"><![CDATA[
|
||||
<p><h1>Robot36 %1$s</h1>Copyright 2024 Ahmet Inan</p>
|
||||
<p>Por favor, leia o AVISO no final desta página.</p>
|
||||
<p><h5>Descrição</h5>Decodifica imagens de televisão de varredura lenta a partir de áudio</p>
|
||||
<p><h5>Código fonte</h5><a href="https://github.com/xdsopl/robot36">Robot36 no GitHub</a><br />BSD Zero Clause License</p>
|
||||
<p><h5>Especificação técnica</h5><a href="http://www.barberdsp.com/downloads/Dayton%%20Paper.pdf">Dayton Paper</a><br />por JL Barber - 2000</p>
|
||||
<p><h5>AVISO</h5>%2$s</p>
|
||||
]]></string>
|
||||
</resources>
|
||||
|
|
@ -1,26 +1,31 @@
|
|||
<?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="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_default">Стандартный</string>
|
||||
<string name="channel_first">Первый</string>
|
||||
<string name="channel_second">Второй</string>
|
||||
<string name="channel_summation">Суммарный</string>
|
||||
<string name="channel_summation">Общий</string>
|
||||
<string name="channel_analytic">Анализ</string>
|
||||
<string name="audio_source">Источник аудио</string>
|
||||
<string name="source_default">По-умолчанию</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>
|
||||
|
|
@ -29,15 +34,18 @@
|
|||
<string name="creating_picture_file_failed">Ошибка создания файла изображения</string>
|
||||
<string name="storing_picture_failed">Ошибка сохранения изображения</string>
|
||||
<string name="scope_description">Декодированное изображение SSTV</string>
|
||||
<string name="freq_plot_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="enable">Включить</string>
|
||||
<string name="disable">Выключить</string>
|
||||
<string name="close">Закрыть</string>
|
||||
<string name="privacy_policy">Политика конфиденциальности</string>
|
||||
<string name="privacy_policy">Политика приватности</string>
|
||||
<string name="privacy_policy_text"><![CDATA[
|
||||
<p><h1>Политика конфиденциальности</h1></p>
|
||||
<p><h1>Политика приватности</h1></p>
|
||||
<p><h5>Доступ к микрофону</h5>
|
||||
Это приложение требует доступ к микрофону вашего устройства для декодирования сигналов телевидения с медленной развёрткой (SSTV).
|
||||
Микрофон записывает аудио, содержащее передачу SSTV.
|
||||
|
|
@ -45,8 +53,8 @@
|
|||
<p><h5>Обработка данных</h5>
|
||||
Приложение использует небольшой временный буфер в памяти для обработки аудиоданных в реальном времени.
|
||||
Этот буфер постоянно перезаписывается новыми данными по мере продвижения декодирования.
|
||||
Приложение <b>не сохраняет</b> необработанное аудио, записанное с микрофона.
|
||||
На устройстве сохраняются <b>только изображения</b>, полученные в результате декодирования сигнала SSTV.
|
||||
Приложение не сохраняет необработанное аудио, записанное с микрофона.
|
||||
На устройстве сохраняются только изображения, полученные в результате декодирования сигнала SSTV.
|
||||
</p>
|
||||
]]></string>
|
||||
<string name="about">О приложении Robot36</string>
|
||||
|
|
@ -56,6 +64,6 @@
|
|||
<p><h5>Описание</h5>Декодирует изображения телевидения с медленной развёрткой из аудиосигнала</p>
|
||||
<p><h5>Реализация</h5><a href="https://github.com/xdsopl/robot36">Robot36 на GitHub</a><br />Лицензия BSD Zero Clause</p>
|
||||
<p><h5>Спецификации режимов</h5><a href="http://www.barberdsp.com/downloads/Dayton%%20Paper.pdf">Dayton Paper</a><br />автор JL Barber - 2000</p>
|
||||
<p><h5>ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ</h5>THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.</p>
|
||||
<p><h5>ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ</h5>%2$s</p>
|
||||
]]></string>
|
||||
</resources>
|
||||
|
|
|
|||
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>
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
<?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>
|
||||
|
|
@ -21,6 +23,9 @@
|
|||
<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>
|
||||
|
|
@ -29,8 +34,11 @@
|
|||
<string name="creating_picture_file_failed">创建图片文件出错</string>
|
||||
<string name="storing_picture_failed">写入图像数据失败</string>
|
||||
<string name="scope_description">解码的SSTV图像</string>
|
||||
<string name="freq_plot_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="enable">开启</string>
|
||||
<string name="disable">禁用</string>
|
||||
|
|
@ -57,6 +65,6 @@
|
|||
<p><h5>实现</h5><a href="https://github.com/xdsopl/robot36">Robot36 GitHub</a><br />BSD Zero Clause 许可证</p>
|
||||
<p><h5>免责声明</h5>请注意,本应用程序仅用于个人用途。任何商业用途需谨慎考虑。</p>
|
||||
<p><h5>模式规范</h5><a href="http://www.barberdsp.com/downloads/Dayton%%20Paper.pdf">Dayton Paper</a><br />作者:JL Barber - 2000年</p>
|
||||
<p><h5>免责声明</h5>THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.</p>
|
||||
<p><h5>免责声明</h5>%2$s</p>
|
||||
]]></string>
|
||||
</resources>
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
<string name="martin_modes" translatable="false">Martin</string>
|
||||
<string name="scottie_modes" translatable="false">Scottie</string>
|
||||
<string name="wraase_modes" translatable="false">Wraase</string>
|
||||
<string name="contributed_modes" translatable="false">Contrib</string>
|
||||
<string name="robot36_color" translatable="false">Robot 36 Color</string>
|
||||
<string name="robot72_color" translatable="false">Robot 72 Color</string>
|
||||
<string name="pd50" translatable="false">PD 50</string>
|
||||
|
|
@ -14,18 +15,32 @@
|
|||
<string name="pd160" translatable="false">PD 160</string>
|
||||
<string name="pd180" translatable="false">PD 180</string>
|
||||
<string name="pd240" translatable="false">PD 240</string>
|
||||
<string name="pd290" translatable="false">PD 290</string>
|
||||
<string name="martin1" translatable="false">Martin 1</string>
|
||||
<string name="martin2" translatable="false">Martin 2</string>
|
||||
<string name="scottie1" translatable="false">Scottie 1</string>
|
||||
<string name="scottie2" translatable="false">Scottie 2</string>
|
||||
<string name="scottie_dx" translatable="false">Scottie DX</string>
|
||||
<string name="wraase_sc2_180" translatable="false">Wraase SC2–180</string>
|
||||
<string name="hf_fax" translatable="false">HF Fax</string>
|
||||
<string name="rate_8000" translatable="false">8 kHz</string>
|
||||
<string name="rate_16000" translatable="false">16 kHz</string>
|
||||
<string name="rate_32000" translatable="false">32 kHz</string>
|
||||
<string name="rate_44100" translatable="false">44.1 kHz</string>
|
||||
<string name="rate_48000" translatable="false">48 kHz</string>
|
||||
<string name="english" translatable="false">English</string>
|
||||
<string name="simplified_chinese" translatable="false">简体中文</string>
|
||||
<string name="russian" translatable="false">Русский</string>
|
||||
<string name="german" translatable="false">Deutsch</string>
|
||||
<string name="brazilian_portuguese" translatable="false">Português brasileiro</string>
|
||||
<string name="polish" translatable="false">Polski</string>
|
||||
<string name="ukrainian" translatable="false">Українська</string>
|
||||
<string name="latin_american_spanish" translatable="false">Español de América Latina</string>
|
||||
<string name="french" translatable="false">Français</string>
|
||||
<string name="disclaimer" translatable="false">THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.</string>
|
||||
<string name="language">Language</string>
|
||||
<string name="share">Share</string>
|
||||
<string name="more_options">More Options</string>
|
||||
<string name="store_scope">Store Scope</string>
|
||||
<string name="toggle_mode">Toggle Mode</string>
|
||||
<string name="auto_mode">Auto Mode</string>
|
||||
|
|
@ -46,6 +61,9 @@
|
|||
<string name="source_camcorder">Camcorder</string>
|
||||
<string name="source_voice_recognition">Voice Recognition</string>
|
||||
<string name="source_unprocessed">Unprocessed</string>
|
||||
<string name="audio_format">Audio Format</string>
|
||||
<string name="fixed_point">Fixed Point</string>
|
||||
<string name="floating_point">Floating Point</string>
|
||||
<string name="audio_init_failed">Audio init failed</string>
|
||||
<string name="audio_setup_failed">Audio setup failed</string>
|
||||
<string name="audio_permission_denied">Audio permission denied</string>
|
||||
|
|
@ -54,8 +72,11 @@
|
|||
<string name="creating_picture_file_failed">Creating picture file failed</string>
|
||||
<string name="storing_picture_failed">Storing picture failed</string>
|
||||
<string name="scope_description">Decoded SSTV picture</string>
|
||||
<string name="freq_plot_description">Frequency plot</string>
|
||||
<string name="peak_meter_description">Peak audio signal level</string>
|
||||
<string name="waterfall_plot">Waterfall plot</string>
|
||||
<string name="frequency_plot">Frequency plot</string>
|
||||
<string name="spectrogram">Spectrogram</string>
|
||||
<string name="auto_save">Auto Save</string>
|
||||
<string name="night_mode">Night Mode</string>
|
||||
<string name="enable">Enable</string>
|
||||
<string name="disable">Disable</string>
|
||||
|
|
@ -70,8 +91,8 @@ The microphone captures the audio containing the SSTV transmission.
|
|||
<p><h5>Data Handling</h5>
|
||||
The app uses a small temporary buffer in memory to process the audio data in real-time.
|
||||
This buffer is constantly overwritten with new data as the decoding progresses.
|
||||
The app <b>does not</b> store the raw audio captured from the microphone.
|
||||
Only the <b>decoded images</b> resulting from the SSTV process are saved on your device\'s storage.
|
||||
The app does not store the raw audio captured from the microphone.
|
||||
Only the decoded images resulting from the SSTV process are saved on your device\'s storage.
|
||||
</p>
|
||||
]]></string>
|
||||
<string name="about">About Robot36</string>
|
||||
|
|
@ -81,6 +102,6 @@ Only the <b>decoded images</b> resulting from the SSTV process are saved on your
|
|||
<p><h5>Description</h5>Decodes Slow Scan Television images from audio</p>
|
||||
<p><h5>Implementation</h5><a href="https://github.com/xdsopl/robot36">Robot36 on GitHub</a><br />BSD Zero Clause License</p>
|
||||
<p><h5>Mode specifications</h5><a href="http://www.barberdsp.com/downloads/Dayton%%20Paper.pdf">Dayton Paper</a><br />by JL Barber - 2000</p>
|
||||
<p><h5>DISCLAIMER</h5>THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.</p>
|
||||
<p><h5>DISCLAIMER</h5>%2$s</p>
|
||||
]]></string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
Following SSTV modes are supported:
|
||||
|
||||
<ul><li>Robot Modes: 36 & 72</li>
|
||||
<li>PD Modes: 50, 90, 120, 160, 180 & 240</li>
|
||||
<li>PD Modes: 50, 90, 120, 160, 180, 240 & 290</li>
|
||||
<li>Martin Modes: 1 & 2</li>
|
||||
<li>Scottie Modes: 1, 2 & DX</li>
|
||||
<li>Wraase Mode: SC2-180</li></ul>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
[versions]
|
||||
agp = "8.3.2"
|
||||
agp = "8.13.1"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.1.5"
|
||||
espressoCore = "3.5.1"
|
||||
appcompat = "1.6.1"
|
||||
material = "1.11.0"
|
||||
activity = "1.8.0"
|
||||
constraintlayout = "2.1.4"
|
||||
junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
appcompat = "1.7.1"
|
||||
material = "1.13.0"
|
||||
activity = "1.11.0"
|
||||
constraintlayout = "2.2.1"
|
||||
|
||||
[libraries]
|
||||
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
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
|||
Loading…
Reference in a new issue