@e04/ft8ts 0.0.6 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -18
- package/dist/{ft8js.cjs → cli.js} +1856 -660
- package/dist/cli.js.map +1 -0
- package/dist/ft8ts.cjs +1467 -569
- package/dist/ft8ts.cjs.map +1 -1
- package/dist/ft8ts.d.ts +48 -12
- package/dist/ft8ts.mjs +1465 -569
- package/dist/ft8ts.mjs.map +1 -1
- package/example/browser/index.html +3 -2
- package/package.json +4 -1
- package/src/cli.ts +171 -0
- package/src/ft4/constants.ts +41 -0
- package/src/ft4/decode.ts +1018 -0
- package/src/ft4/encode.ts +40 -0
- package/src/ft4/scramble.ts +9 -0
- package/src/ft8/constants.ts +18 -0
- package/src/ft8/decode.ts +22 -31
- package/src/ft8/encode.ts +6 -5
- package/src/index.ts +6 -0
- package/src/util/constants.ts +4 -13
- package/src/util/hashcall.ts +2 -2
- package/src/util/waveform.ts +92 -20
- package/dist/ft8js.cjs.map +0 -1
- package/dist/ft8js.mjs +0 -2116
- package/dist/ft8js.mjs.map +0 -1
- package/example/decode-ft8-wav.ts +0 -78
- package/example/generate-ft8-wav.ts +0 -82
package/src/cli.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type DecodedMessage,
|
|
6
|
+
type DecodeOptions,
|
|
7
|
+
decodeFT4,
|
|
8
|
+
decodeFT8,
|
|
9
|
+
encodeFT8,
|
|
10
|
+
} from "./index.js";
|
|
11
|
+
import { parseWavBuffer, writeMono16WavFile } from "./util/wav.js";
|
|
12
|
+
|
|
13
|
+
const SAMPLE_RATE = 12_000;
|
|
14
|
+
const DEFAULT_OUTPUT = "output.wav";
|
|
15
|
+
const DEFAULT_DF_HZ = 1_000;
|
|
16
|
+
|
|
17
|
+
function printUsage(): void {
|
|
18
|
+
console.error(`ft8ts - FT8 encoder/decoder
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
ft8ts decode <file.wav> [options]
|
|
22
|
+
ft8ts encode "<message>" [options]
|
|
23
|
+
|
|
24
|
+
Decode options:
|
|
25
|
+
--mode <ft8|ft4> Mode: ft8 (default) or ft4
|
|
26
|
+
--low <hz> Lower frequency bound (default: 200)
|
|
27
|
+
--high <hz> Upper frequency bound (default: 3000)
|
|
28
|
+
--depth <1|2|3> Decoding depth (default: 2)
|
|
29
|
+
|
|
30
|
+
Encode options:
|
|
31
|
+
--out <file> Output WAV file (default: output.wav)
|
|
32
|
+
--df <hz> Base frequency in Hz (default: 1000)
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatMessage(d: DecodedMessage): string {
|
|
37
|
+
const freq = d.freq.toFixed(0).padStart(5);
|
|
38
|
+
const dt = (d.dt >= 0 ? "+" : "") + d.dt.toFixed(1);
|
|
39
|
+
const snr = (d.snr >= 0 ? "+" : "") + Math.round(d.snr).toString().padStart(3);
|
|
40
|
+
return `${dt.padStart(5)} ${snr} ${freq} ${d.msg}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function runDecode(argv: string[]): void {
|
|
44
|
+
if (argv.length === 0) {
|
|
45
|
+
console.error("Error: missing input file");
|
|
46
|
+
printUsage();
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const wavFile = argv[0]!;
|
|
51
|
+
const options: DecodeOptions = {};
|
|
52
|
+
let mode: "ft8" | "ft4" = "ft8";
|
|
53
|
+
|
|
54
|
+
for (let i = 1; i < argv.length; i++) {
|
|
55
|
+
const arg = argv[i]!;
|
|
56
|
+
if (arg === "--mode") {
|
|
57
|
+
const value = argv[++i];
|
|
58
|
+
if (value === "ft8" || value === "ft4") {
|
|
59
|
+
mode = value;
|
|
60
|
+
} else {
|
|
61
|
+
throw new Error(`Invalid --mode: ${value ?? "(missing)"}. Use ft8 or ft4`);
|
|
62
|
+
}
|
|
63
|
+
} else if (arg === "--low") {
|
|
64
|
+
options.freqLow = Number(argv[++i]);
|
|
65
|
+
} else if (arg === "--high") {
|
|
66
|
+
options.freqHigh = Number(argv[++i]);
|
|
67
|
+
} else if (arg === "--depth") {
|
|
68
|
+
options.depth = Number(argv[++i]);
|
|
69
|
+
} else {
|
|
70
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const filePath = resolve(process.cwd(), wavFile);
|
|
75
|
+
console.log(`Reading ${filePath}...`);
|
|
76
|
+
|
|
77
|
+
const { sampleRate, samples } = parseWavBuffer(readFileSync(filePath));
|
|
78
|
+
console.log(
|
|
79
|
+
`WAV: ${sampleRate} Hz, ${samples.length} samples, ${(samples.length / sampleRate).toFixed(1)}s`,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const startTime = performance.now();
|
|
83
|
+
const decoded =
|
|
84
|
+
mode === "ft4"
|
|
85
|
+
? decodeFT4(samples, { ...options, sampleRate })
|
|
86
|
+
: decodeFT8(samples, { ...options, sampleRate });
|
|
87
|
+
const elapsed = performance.now() - startTime;
|
|
88
|
+
|
|
89
|
+
console.log(`\nDecoded ${decoded.length} messages in ${(elapsed / 1000).toFixed(2)}s:\n`);
|
|
90
|
+
console.log(" dt snr freq message");
|
|
91
|
+
console.log(" --- --- ----- -------");
|
|
92
|
+
for (const d of decoded) {
|
|
93
|
+
console.log(formatMessage(d));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function runEncode(argv: string[]): void {
|
|
98
|
+
if (argv.length === 0) {
|
|
99
|
+
console.error("Error: missing message");
|
|
100
|
+
printUsage();
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const message = argv[0]!;
|
|
105
|
+
let outputFile = DEFAULT_OUTPUT;
|
|
106
|
+
let dfHz = DEFAULT_DF_HZ;
|
|
107
|
+
|
|
108
|
+
for (let i = 1; i < argv.length; i++) {
|
|
109
|
+
const arg = argv[i]!;
|
|
110
|
+
if (arg === "--out") {
|
|
111
|
+
i++;
|
|
112
|
+
const value = argv[i];
|
|
113
|
+
if (!value) throw new Error("Missing value for --out");
|
|
114
|
+
outputFile = value;
|
|
115
|
+
} else if (arg === "--df") {
|
|
116
|
+
i++;
|
|
117
|
+
const value = argv[i];
|
|
118
|
+
if (!value) throw new Error("Missing value for --df");
|
|
119
|
+
const parsed = Number(value);
|
|
120
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
121
|
+
throw new Error(`Invalid --df value: ${value}`);
|
|
122
|
+
}
|
|
123
|
+
dfHz = parsed;
|
|
124
|
+
} else {
|
|
125
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const waveform = encodeFT8(message, {
|
|
130
|
+
sampleRate: SAMPLE_RATE,
|
|
131
|
+
samplesPerSymbol: 1_920,
|
|
132
|
+
baseFrequency: dfHz,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const outPath = resolve(process.cwd(), outputFile);
|
|
136
|
+
writeMono16WavFile(outPath, waveform, SAMPLE_RATE);
|
|
137
|
+
|
|
138
|
+
console.log(
|
|
139
|
+
`Wrote ${outPath} (${waveform.length} samples, ${(waveform.length / SAMPLE_RATE).toFixed(3)} s)`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function main(): void {
|
|
144
|
+
const args = process.argv.slice(2);
|
|
145
|
+
const subcommand = args[0];
|
|
146
|
+
const subArgs = args.slice(1);
|
|
147
|
+
|
|
148
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
149
|
+
printUsage();
|
|
150
|
+
process.exit(0);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
if (subcommand === "decode") {
|
|
155
|
+
runDecode(subArgs);
|
|
156
|
+
} else if (subcommand === "encode") {
|
|
157
|
+
runEncode(subArgs);
|
|
158
|
+
} else {
|
|
159
|
+
console.error(`Error: unknown subcommand '${subcommand}'`);
|
|
160
|
+
printUsage();
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
165
|
+
console.error(`Error: ${msg}`);
|
|
166
|
+
printUsage();
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
main();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** FT4-specific constants (lib/ft4/ft4_params.f90). */
|
|
2
|
+
|
|
3
|
+
import { SAMPLE_RATE } from "../util/constants.js";
|
|
4
|
+
|
|
5
|
+
export const COSTAS_A = [0, 1, 3, 2] as const;
|
|
6
|
+
export const COSTAS_B = [1, 0, 2, 3] as const;
|
|
7
|
+
export const COSTAS_C = [2, 3, 1, 0] as const;
|
|
8
|
+
export const COSTAS_D = [3, 2, 0, 1] as const;
|
|
9
|
+
export const GRAYMAP = [0, 1, 3, 2] as const;
|
|
10
|
+
|
|
11
|
+
// Message scrambling vector (rvec) from WSJT-X.
|
|
12
|
+
export const RVEC = [
|
|
13
|
+
0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1,
|
|
14
|
+
0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0,
|
|
15
|
+
1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1,
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export const NSPS = 576;
|
|
19
|
+
export const NFFT1 = 4 * NSPS; // 2304
|
|
20
|
+
export const NH1 = NFFT1 / 2; // 1152
|
|
21
|
+
export const NSTEP = NSPS;
|
|
22
|
+
export const NMAX = 21 * 3456; // 72576
|
|
23
|
+
export const NHSYM = Math.floor((NMAX - NFFT1) / NSTEP); // 122
|
|
24
|
+
export const NDOWN = 18;
|
|
25
|
+
export const ND = 87;
|
|
26
|
+
export const NS = 16;
|
|
27
|
+
export const NN = NS + ND; // 103
|
|
28
|
+
|
|
29
|
+
export const NFFT2 = NMAX / NDOWN; // 4032
|
|
30
|
+
export const NSS = NSPS / NDOWN; // 32
|
|
31
|
+
export const FS2 = SAMPLE_RATE / NDOWN; // 666.67 Hz
|
|
32
|
+
export const MAX_FREQ = 4910;
|
|
33
|
+
export const SYNC_PASS_MIN = 1.2;
|
|
34
|
+
export const TWO_PI = 2 * Math.PI;
|
|
35
|
+
|
|
36
|
+
export const HARD_SYNC_PATTERNS = [
|
|
37
|
+
{ offset: 0, bits: [0, 0, 0, 1, 1, 0, 1, 1] as const },
|
|
38
|
+
{ offset: 66, bits: [0, 1, 0, 0, 1, 1, 1, 0] as const },
|
|
39
|
+
{ offset: 132, bits: [1, 1, 1, 0, 0, 1, 0, 0] as const },
|
|
40
|
+
{ offset: 198, bits: [1, 0, 1, 1, 0, 0, 0, 1] as const },
|
|
41
|
+
] as const;
|