Compare commits

..

79 commits
v2.7 ... v2

Author SHA1 Message Date
Ahmet Inan 75146a5342 v2.16 2025-11-11 11:10:38 +01:00
Ahmet Inan 716fc64892 updated libs and tools 2025-11-11 10:44:28 +01:00
Ahmet Inan a39c180c44 moved hffax to contributed modes 2025-11-11 10:44:11 +01:00
Помаранча 265223f43d
Update strings.xml (#42)
* Update strings.xml

Final translation update. Sorry for not fixing it earlier.

* Update strings.xml

As you requested - I fixed it.
2025-09-11 17:14:02 +02:00
Ahmet Inan c55ae1b2ca
Merge pull request #40 from marek-o/adding-hffax2
Adding HF Fax
2025-08-23 07:53:21 +02:00
Marek Ossowski 03dc577ae8 Added HF Fax mode 2025-08-22 22:27:08 +02:00
Marek Ossowski 5fee5e0672 Copied RawDecoder.java to HFFax.java 2025-08-22 22:19:38 +02:00
Marek Ossowski 5b20da27a0 Adding BaseMode and postProcessScopeImage() 2025-08-22 22:13:54 +02:00
Marek Ossowski 1a4ffac26e Renaming and refactoring 2025-08-22 22:09:18 +02:00
Ahmet Inan 7ad54873da added frequency markers 2025-08-20 12:47:05 +02:00
Ahmet Inan 1502f20af1 updated libs and tools 2025-08-20 10:38:30 +02:00
Ahmet Inan 1a722d5289 overlooked this one 2025-04-16 07:47:08 +02:00
Ahmet Inan b105a415f3 numbering starts from tip 2025-04-15 21:37:29 +02:00
Ahmet Inan cb607c9d4e added schematic 2025-04-15 21:11:56 +02:00
Ahmet Inan 8b12ac6153 added line to mic converter 2025-04-15 20:24:48 +02:00
Ahmet Inan 040655552f integrated French translation 2025-04-07 08:30:30 +02:00
Raphaël de Courville aaf31ac4a7 Added french translations
Initial French translation
2025-04-07 07:59:27 +02:00
Ahmet Inan 54381558d1 updated libs and tools 2025-03-12 19:41:50 +01:00
Ahmet Inan ef3b251a8e v2.15 2025-02-26 08:26:29 +01:00
Ahmet Inan d61be31811 water falls downwards 2025-02-26 08:03:00 +01:00
Ahmet Inan ec302f6800 use capital letter for language 2025-02-25 20:40:09 +01:00
Ahmet Inan 913168adc3 integrated new default spectrogram 2025-02-25 19:45:17 +01:00
Ahmet Inan 771ab0fe0b we can go lower still 2025-02-25 17:22:40 +01:00
Ahmet Inan 1bd3d05782 start spectrum at 140 Hz so we can see up to 2700 Hz 2025-02-25 14:25:27 +01:00
Ahmet Inan f081bbddfd experimenting with STFT 2025-02-25 13:46:05 +01:00
Ahmet Inan 7f503ecef3 updated libs and tools 2025-02-25 12:24:35 +01:00
Ahmet Inan 8afa86d660 added Latin American Spanish translation 2025-01-21 12:40:19 +01:00
Ahmet Inan ef7e20f3a0 PD290 is now supported 2024-12-08 11:22:27 +01:00
Ahmet Inan 728c9df890 v2.14 2024-11-25 09:36:01 +01:00
Ahmet Inan c93dff432e use fixed point by default
simply too many devices have a broken floating point implementation
2024-11-24 07:43:38 +01:00
Ahmet Inan 1fbec49583 oops 2024-11-23 14:24:19 +01:00
Ahmet Inan 28ebde8e93 support full resolution saving of PD290 images
the scope image of PD290 is still cropped to 640 pixels but aspect
ratio is now correct
2024-11-22 16:34:25 +01:00
Ahmet Inan 2c3c6b1293 added experimental PD290 support
full 616 lines are supported but width is 640 pixels instead of 800 to
avoid breaking other modes. that means the aspect ratio is wrong for
PD290 at the moment.
2024-11-22 10:45:24 +01:00
Ahmet Inan 2d74f33bb6 integrated Ukrainian translation 2024-11-19 18:02:41 +01:00
Помаранча 07e2f59389 Added Ukrainian language 2024-11-19 17:59:23 +01:00
Ahmet Inan 0d1e90d613 v2.13 2024-11-14 11:00:55 +01:00
Ahmet Inan 61799452ff updated libs 2024-11-14 10:59:26 +01:00
Ahmet Inan beb6647909 added option to disable auto save 2024-11-13 11:28:43 +01:00
Ahmet Inan 418b1f0f06 updated libs and tools 2024-10-22 10:39:15 +02:00
Ahmet Inan a15b5b8104 integrated Polish translation 2024-10-22 09:43:06 +02:00
SP1IMJ a0873b2590 Polish language translation 2024-10-22 09:33:46 +02:00
Ahmet Inan b69d064e5b v2.12 2024-08-14 16:41:11 +02:00
Ahmet Inan a98c945f37 added option to change audio format 2024-08-14 16:39:37 +02:00
Ahmet Inan 656693609d updated libs and tools 2024-08-14 14:19:22 +02:00
Ahmet Inan 55f2726768 release audio if initialization fails 2024-07-03 08:39:33 +02:00
Ahmet Inan 879b5244f2 v2.11 2024-05-25 10:49:30 +02:00
Ahmet Inan 57916c665e updated libs and tools 2024-05-24 10:04:16 +02:00
豪 樹 冬 木 59500bbf49 Address typos on PTBR 2024-05-23 16:47:42 +02:00
Ahmet Inan 0e863e6304 use system language as default 2024-05-22 14:26:03 +02:00
Ahmet Inan 6e8a524416 use shorter string for privacy policy 2024-05-22 14:06:27 +02:00
Ahmet Inan 72eeae7b29 v2.10 2024-05-22 09:43:30 +02:00
Ahmet Inan 477ff84df6 rearranged menu and made some icons appear 2024-05-22 09:34:27 +02:00
Ahmet Inan 871293b8d3 removed bold tags from privacy policy page
causes more problems than it's worth
2024-05-22 08:13:25 +02:00
Ahmet Inan 97448c9c05 added menu item for Brazilian Portuguese 2024-05-22 08:03:28 +02:00
Ahmet Inan 65b09103d2 made disclaimer text untranslatable 2024-05-22 07:48:52 +02:00
豪 樹 冬 木 25a442fcae Brazilian Portuguese!
* Initial Brazilian Portuguese translation

Signed-off-by: 豪 樹 冬 木 <fuyukihidekii@gmail.com>

* Minor update to pt-br

Signed-off-by: 豪 樹 冬 木 <fuyukihidekii@gmail.com>

* Update pt-br locale

Signed-off-by: 豪 樹 冬 木 <fuyukihidekii@gmail.com>

---------

Signed-off-by: 豪 樹 冬 木 <fuyukihidekii@gmail.com>
2024-05-22 07:36:37 +02:00
Ahmet Inan c56d30d33f added option to change language in-app 2024-05-21 07:56:17 +02:00
ropucyka 2150004849 Russian translation update 2 2024-05-20 06:23:33 +02:00
Ahmet Inan de2c9353ad v2.9 2024-05-19 21:16:05 +02:00
Ahmet Inan 5593732225 renamed Force Mode to Lock Mode 2024-05-19 07:22:56 +02:00
Ahmet Inan 707cfe740c minor cleanup of string files 2024-05-19 06:50:59 +02:00
Ahmet Inan c5c1c973d9 renamed mode toggle and added auto mode to force mode menu 2024-05-19 06:27:44 +02:00
ropucyka f80b4d35c8 Russian translation fix 1 2024-05-18 16:31:24 +02:00
Ahmet Inan fe3a34f639 capitalized Russian translation of raw mode 2024-05-18 09:08:34 +02:00
Ahmet Inan 0ef30af88e moved raw mode menu item down to avoid confusion 2024-05-18 09:06:53 +02:00
Ahmet Inan 8a44d0d05a v2.8 2024-05-18 07:41:28 +02:00
Ahmet Inan 0eec2c501b removed "Modes" word and translated "Raw Mode" 2024-05-18 07:34:33 +02:00
ropucyka d5b72b75f6 Update strings.xml 2024-05-17 22:34:47 +02:00
Ahmet Inan 857cd7eeb4 added Russian translations 2024-05-16 17:54:57 +02:00
Ahmet Inan 3554cbd5e0 this break does not belong here 2024-05-16 17:19:12 +02:00
Ahmet Inan 3bbd22a34e added German translations 2024-05-16 13:10:08 +02:00
Ahmet Inan 2f641f4b8d added Simplified Chinese translations for privacy policy and about page 2024-05-16 09:26:27 +02:00
Ahmet Inan d664ac9c8d make Robot36 appear in App languages list 2024-05-16 09:24:45 +02:00
WeiguangTWK eb62016c5c Tagged some strings as untranslatable 2024-05-16 09:24:45 +02:00
WeiguangTWK 901d9361a0 Add Chinese Simplified Translation 2024-05-16 09:24:34 +02:00
Ahmet Inan f7ac973153 reduce by action bar height estimate 2024-05-12 08:26:18 +02:00
Ahmet Inan 21ac7ea2f5 reduce black borders with split screens 2024-05-10 17:42:03 +02:00
Ahmet Inan 23471b4246 stretch frequency plot vertically on hires displays 2024-05-10 16:29:44 +02:00
Ahmet Inan bd40d8ec2b take width and height from config 2024-05-10 15:18:11 +02:00
42 changed files with 2013 additions and 362 deletions

View file

@ -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 1V, while electret microphones produce signals in the millivolt range, so attenuation is needed.
* Electret microphones are biased via the TRRS connector, allowing their internal amplifiers to function. This bias must be blocked to avoid distortion.
* To make the smartphone recognize the input as a microphone, a resistor must be placed between the second ring and sleeve.
To reduce power consumption on the line-out device, a higher impedance can be achieved by inserting a resistor in series with the primary winding of a 1:1 audio transformer. If the source cannot drive high impedance, the primary can be connected directly, and attenuation applied on the secondary side. This increases power consumption and may heat the transformer.
Because the electret mic input is high-impedance and AC-coupled, the transformers secondary can resonate if left unterminated. Adding a resistor across the secondary dampens this resonance and flattens the frequency response. A value equal to the transformers impedance is typical, but a lower value can be used to both improve damping and provide additional attenuation. The ratio of the series resistor on the primary to the parallel resistor on the secondary determines the overall attenuation.
### Example Values
* Transformer: 1:1 audio transformer, 600Ω impedance, 140Ω DC resistance
* Primary side: 2.2kΩ resistor in series (any value between 1kΩ and 10kΩ is fine; a 10kΩ potentiometer allows adjustment)
* Secondary side: 100Ω resistor across the winding for damping and attenuation
* DC blocking capacitor: 2.2µF film capacitor (anything between 1µF and 100µF works; avoid values below 1µF to keep low-frequency SSTV content intact)
* Microphone sensing resistor: 2.2kΩ between the second ring and sleeve (values near 2kΩ are fine)
### Schematic
```
[Line IN] O---[R1]---+||+---+---|C1|---+---O [Ring 2]
S||S | |
T1: S||S [R2] [R3]
S||S | |
[Line GND] O----------+||+---+----------+---O [Sleeve]
```
Explanation of Symbols:
* [R1]: 2.2kΩ series resistor (primary side attenuation)
* T1: 1:1 audio transformer
* [C1]: 2.2µF capacitor (DC blocking)
* [R2]: 100Ω damping resistor (across secondary)
* [R3]: 2.2kΩ MIC detect resistor (between MIC and GND)
* [Line IN], [Line GND]: input from radio/sound card
* [Ring 2], [Sleeve]: TRRS plug connections to smartphone

View file

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

View 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;
}
}

View file

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

View file

@ -13,6 +13,8 @@ public class Demodulator {
private final SchmittTrigger syncPulseTrigger; private final SchmittTrigger syncPulseTrigger;
private final Phasor baseBandOscillator; private final Phasor baseBandOscillator;
private final Delay syncPulseValueDelay; private final Delay syncPulseValueDelay;
private final double scanLineBandwidth;
private final double centerFrequency;
private final float syncPulseFrequencyValue; private final float syncPulseFrequencyValue;
private final float syncPulseFrequencyTolerance; private final float syncPulseFrequencyTolerance;
private final int syncPulse5msMinSamples; private final int syncPulse5msMinSamples;
@ -33,10 +35,12 @@ public class Demodulator {
public int syncPulseOffset; public int syncPulseOffset;
public float frequencyOffset; public float frequencyOffset;
public static final double syncPulseFrequency = 1200;
public static final double blackFrequency = 1500;
public static final double whiteFrequency = 2300;
Demodulator(int sampleRate) { Demodulator(int sampleRate) {
double blackFrequency = 1500; scanLineBandwidth = whiteFrequency - blackFrequency;
double whiteFrequency = 2300;
double scanLineBandwidth = whiteFrequency - blackFrequency;
frequencyModulation = new FrequencyModulation(scanLineBandwidth, sampleRate); frequencyModulation = new FrequencyModulation(scanLineBandwidth, sampleRate);
double syncPulse5msSeconds = 0.005; double syncPulse5msSeconds = 0.005;
double syncPulse9msSeconds = 0.009; double syncPulse9msSeconds = 0.009;
@ -63,20 +67,23 @@ public class Demodulator {
Kaiser kaiser = new Kaiser(); Kaiser kaiser = new Kaiser();
for (int i = 0; i < baseBandLowPass.length; ++i) for (int i = 0; i < baseBandLowPass.length; ++i)
baseBandLowPass.taps[i] = (float) (kaiser.window(2.0, i, baseBandLowPass.length) * Filter.lowPass(cutoffFrequency, sampleRate, i, baseBandLowPass.length)); baseBandLowPass.taps[i] = (float) (kaiser.window(2.0, i, baseBandLowPass.length) * Filter.lowPass(cutoffFrequency, sampleRate, i, baseBandLowPass.length));
double centerFrequency = (lowestFrequency + highestFrequency) / 2; centerFrequency = (lowestFrequency + highestFrequency) / 2;
baseBandOscillator = new Phasor(-centerFrequency, sampleRate); baseBandOscillator = new Phasor(-centerFrequency, sampleRate);
double syncPulseFrequency = 1200; syncPulseFrequencyValue = (float) normalizeFrequency(syncPulseFrequency);
syncPulseFrequencyValue = (float) ((syncPulseFrequency - centerFrequency) * 2 / scanLineBandwidth);
syncPulseFrequencyTolerance = (float) (50 * 2 / scanLineBandwidth); syncPulseFrequencyTolerance = (float) (50 * 2 / scanLineBandwidth);
double syncPorchFrequency = 1500; double syncPorchFrequency = 1500;
double syncHighFrequency = (syncPulseFrequency + syncPorchFrequency) / 2; double syncHighFrequency = (syncPulseFrequency + syncPorchFrequency) / 2;
double syncLowFrequency = (syncPulseFrequency + syncHighFrequency) / 2; double syncLowFrequency = (syncPulseFrequency + syncHighFrequency) / 2;
double syncLowValue = (syncLowFrequency - centerFrequency) * 2 / scanLineBandwidth; double syncLowValue = normalizeFrequency(syncLowFrequency);
double syncHighValue = (syncHighFrequency - centerFrequency) * 2 / scanLineBandwidth; double syncHighValue = normalizeFrequency(syncHighFrequency);
syncPulseTrigger = new SchmittTrigger((float) syncLowValue, (float) syncHighValue); syncPulseTrigger = new SchmittTrigger((float) syncLowValue, (float) syncHighValue);
baseBand = new Complex(); baseBand = new Complex();
} }
private double normalizeFrequency(double frequency) {
return (frequency - centerFrequency) * 2 / scanLineBandwidth;
}
public boolean process(float[] buffer, int channelSelect) { public boolean process(float[] buffer, int channelSelect) {
boolean syncPulseDetected = false; boolean syncPulseDetected = false;
int channels = channelSelect > 0 ? 2 : 1; int channels = channelSelect > 0 ? 2 : 1;

View 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);
}
}

View 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;
}
}

View 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)));
}
}

View file

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

View file

@ -6,22 +6,30 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
package xdsopl.robot36; package xdsopl.robot36;
import android.graphics.Bitmap;
public interface Mode { public interface Mode {
String getName(); String getName();
int getCode(); int getVISCode();
int getWidth(); int getWidth();
int getHeight(); int getHeight();
int getBegin(); int getFirstPixelSampleIndex();
int getFirstSyncPulseIndex(); int getFirstSyncPulseIndex();
int getScanLineSamples(); int getScanLineSamples();
void reset(); Bitmap postProcessScopeImage(Bitmap bmp);
void resetState();
/**
* @param frequencyOffset normalized correction of frequency (expected vs actual)
* @return true if scanline was decoded
*/
boolean decodeScanLine(PixelBuffer pixelBuffer, float[] scratchBuffer, float[] scanLineBuffer, int scopeBufferWidth, int syncPulseIndex, int scanLineSamples, float frequencyOffset); boolean decodeScanLine(PixelBuffer pixelBuffer, float[] scratchBuffer, float[] scanLineBuffer, int scopeBufferWidth, int syncPulseIndex, int scanLineSamples, float frequencyOffset);
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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.9,21.9l-8.49,-8.49l0,0L3.59,3.59l0,0L2.1,2.1L0.69,3.51L3,5.83V19c0,1.1 0.9,2 2,2h13.17l2.31,2.31L21.9,21.9zM5,18l3.5,-4.5l2.5,3.01L12.17,15l3,3H5zM21,18.17L5.83,3H19c1.1,0 2,0.9 2,2V18.17z"/>
</vector>

View file

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

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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>

View 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>

View 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>

View 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 на GitHub</a><br />Лицензия BSD Zero Clause</p>
<p><h5>Спецификации режимов</h5><a href="http://www.barberdsp.com/downloads/Dayton%%20Paper.pdf">Dayton Paper</a><br />автор JL Barber - 2000</p>
<p><h5>ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ</h5>%2$s</p>
]]></string>
</resources>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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