@e04/ft8ts 0.0.9 → 0.0.10
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 +27 -26
- package/dist/cli.js +806 -610
- package/dist/cli.js.map +1 -1
- package/dist/ft8ts.cjs +813 -617
- package/dist/ft8ts.cjs.map +1 -1
- package/dist/ft8ts.d.ts +1 -0
- package/dist/ft8ts.mjs +813 -617
- package/dist/ft8ts.mjs.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +3 -0
- package/src/ft4/constants.ts +0 -38
- package/src/ft4/decode.ts +297 -338
- package/src/ft4/encode.ts +6 -1
- package/src/ft4/scramble.ts +6 -1
- package/src/ft8/constants.ts +0 -11
- package/src/ft8/decode.ts +530 -270
- package/src/util/constants.ts +0 -4
- package/src/util/decode174_91.ts +61 -55
- package/src/util/fft.ts +97 -37
- package/src/util/waveform.ts +8 -4
package/src/util/constants.ts
CHANGED
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
export const SAMPLE_RATE = 12_000;
|
|
4
4
|
|
|
5
5
|
/** LDPC(174,91) code (shared by FT8 and FT4). */
|
|
6
|
-
export const KK = 91;
|
|
7
6
|
export const N_LDPC = 174;
|
|
8
|
-
export const M_LDPC = N_LDPC - KK; // 83
|
|
9
7
|
|
|
10
8
|
export const gHex = [
|
|
11
9
|
"8329ce11bf31eaf509f27fc",
|
|
@@ -93,8 +91,6 @@ export const gHex = [
|
|
|
93
91
|
"608cc857594bfbb55d69600",
|
|
94
92
|
];
|
|
95
93
|
|
|
96
|
-
export const CRC_POLY = 0x2757;
|
|
97
|
-
|
|
98
94
|
export const FTALPH = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./?";
|
|
99
95
|
|
|
100
96
|
export const A1 = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
package/src/util/decode174_91.ts
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
* Port of bpdecode174_91.f90 and decode174_91.f90.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { gHex,
|
|
6
|
+
import { gHex, N_LDPC } from "./constants.js";
|
|
7
7
|
import { checkCRC14 } from "./crc.js";
|
|
8
8
|
import { Mn, Nm, ncw, nrw } from "./ldpc_tables.js";
|
|
9
9
|
|
|
10
|
+
const KK = 91;
|
|
11
|
+
const M_LDPC = N_LDPC - KK; // 83
|
|
12
|
+
|
|
10
13
|
export interface DecodeResult {
|
|
11
14
|
message91: number[];
|
|
12
15
|
cw: number[];
|
|
@@ -183,39 +186,48 @@ function osdDecode174_91(
|
|
|
183
186
|
const K = KK;
|
|
184
187
|
|
|
185
188
|
const gen = getGenerator();
|
|
189
|
+
const absllr = new Float64Array(N);
|
|
190
|
+
for (let i = 0; i < N; i++) absllr[i] = Math.abs(llr[i]!);
|
|
186
191
|
|
|
187
192
|
// Sort by reliability (descending)
|
|
188
|
-
const indices = Array
|
|
189
|
-
|
|
193
|
+
const indices = new Array<number>(N);
|
|
194
|
+
for (let i = 0; i < N; i++) indices[i] = i;
|
|
195
|
+
indices.sort((a, b) => absllr[b]! - absllr[a]!);
|
|
190
196
|
|
|
191
197
|
// Reorder generator matrix columns
|
|
192
198
|
const genmrb = new Uint8Array(K * N);
|
|
193
|
-
for (let
|
|
194
|
-
|
|
195
|
-
|
|
199
|
+
for (let k = 0; k < K; k++) {
|
|
200
|
+
const row = k * N;
|
|
201
|
+
for (let i = 0; i < N; i++) {
|
|
202
|
+
genmrb[row + i] = gen[row + indices[i]!]!;
|
|
196
203
|
}
|
|
197
204
|
}
|
|
198
205
|
|
|
199
206
|
// Gaussian elimination to get systematic form on the K most-reliable bits
|
|
207
|
+
const maxPivotCol = Math.min(K + 20, N);
|
|
200
208
|
for (let id = 0; id < K; id++) {
|
|
201
209
|
let found = false;
|
|
202
|
-
|
|
203
|
-
|
|
210
|
+
const idRow = id * N;
|
|
211
|
+
for (let icol = id; icol < maxPivotCol; icol++) {
|
|
212
|
+
if (genmrb[idRow + icol] === 1) {
|
|
204
213
|
if (icol !== id) {
|
|
205
214
|
// Swap columns
|
|
206
215
|
for (let k = 0; k < K; k++) {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
genmrb[
|
|
216
|
+
const row = k * N;
|
|
217
|
+
const tmp = genmrb[row + id]!;
|
|
218
|
+
genmrb[row + id] = genmrb[row + icol]!;
|
|
219
|
+
genmrb[row + icol] = tmp;
|
|
210
220
|
}
|
|
211
221
|
const tmp = indices[id]!;
|
|
212
222
|
indices[id] = indices[icol]!;
|
|
213
223
|
indices[icol] = tmp;
|
|
214
224
|
}
|
|
215
225
|
for (let ii = 0; ii < K; ii++) {
|
|
216
|
-
if (ii
|
|
226
|
+
if (ii === id) continue;
|
|
227
|
+
const iiRow = ii * N;
|
|
228
|
+
if (genmrb[iiRow + id] === 1) {
|
|
217
229
|
for (let c = 0; c < N; c++) {
|
|
218
|
-
genmrb[
|
|
230
|
+
genmrb[iiRow + c]! ^= genmrb[idRow + c]!;
|
|
219
231
|
}
|
|
220
232
|
}
|
|
221
233
|
}
|
|
@@ -229,87 +241,82 @@ function osdDecode174_91(
|
|
|
229
241
|
// Hard decisions on reordered received word
|
|
230
242
|
const hdec = new Int8Array(N);
|
|
231
243
|
for (let i = 0; i < N; i++) {
|
|
232
|
-
|
|
244
|
+
const idx = indices[i]!;
|
|
245
|
+
hdec[i] = llr[idx]! >= 0 ? 1 : 0;
|
|
233
246
|
}
|
|
234
247
|
const absrx = new Float64Array(N);
|
|
235
248
|
for (let i = 0; i < N; i++) {
|
|
236
|
-
absrx[i] =
|
|
249
|
+
absrx[i] = absllr[indices[i]!]!;
|
|
237
250
|
}
|
|
238
251
|
|
|
239
|
-
//
|
|
240
|
-
const
|
|
252
|
+
// Encode hard decision on MRB (c0): xor selected rows of genmrb.
|
|
253
|
+
const c0 = new Int8Array(N);
|
|
241
254
|
for (let i = 0; i < K; i++) {
|
|
255
|
+
if (hdec[i] !== 1) continue;
|
|
256
|
+
const row = i * N;
|
|
242
257
|
for (let j = 0; j < N; j++) {
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function mrbencode(me: Int8Array): Int8Array {
|
|
248
|
-
const codeword = new Int8Array(N);
|
|
249
|
-
for (let i = 0; i < K; i++) {
|
|
250
|
-
if (me[i] === 1) {
|
|
251
|
-
for (let j = 0; j < N; j++) {
|
|
252
|
-
codeword[j]! ^= g2[j * K + i]!;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
258
|
+
c0[j]! ^= genmrb[row + j]!;
|
|
255
259
|
}
|
|
256
|
-
return codeword;
|
|
257
260
|
}
|
|
258
261
|
|
|
259
|
-
const m0 = hdec.slice(0, K);
|
|
260
|
-
const c0 = mrbencode(m0);
|
|
261
|
-
const bestCw = new Int8Array(c0);
|
|
262
262
|
let dmin = 0;
|
|
263
263
|
for (let i = 0; i < N; i++) {
|
|
264
264
|
const x = c0[i]! ^ hdec[i]!;
|
|
265
265
|
dmin += x * absrx[i]!;
|
|
266
266
|
}
|
|
267
|
+
let bestFlip1 = -1;
|
|
268
|
+
let bestFlip2 = -1;
|
|
267
269
|
|
|
268
270
|
// Order-1: flip single bits in the info portion
|
|
269
271
|
for (let i1 = K - 1; i1 >= 0; i1--) {
|
|
270
272
|
if (apmask[indices[i1]!] === 1) continue;
|
|
271
|
-
const
|
|
272
|
-
me[i1]! ^= 1;
|
|
273
|
-
const ce = mrbencode(me);
|
|
274
|
-
let _nh = 0;
|
|
273
|
+
const row1 = i1 * N;
|
|
275
274
|
let dd = 0;
|
|
276
275
|
for (let j = 0; j < N; j++) {
|
|
277
|
-
const x =
|
|
278
|
-
_nh += x;
|
|
276
|
+
const x = c0[j]! ^ genmrb[row1 + j]! ^ hdec[j]!;
|
|
279
277
|
dd += x * absrx[j]!;
|
|
280
278
|
}
|
|
281
279
|
if (dd < dmin) {
|
|
282
280
|
dmin = dd;
|
|
283
|
-
|
|
281
|
+
bestFlip1 = i1;
|
|
282
|
+
bestFlip2 = -1;
|
|
284
283
|
}
|
|
285
284
|
}
|
|
286
285
|
|
|
287
286
|
// Order-2: flip pairs of least-reliable info bits (limited search)
|
|
288
287
|
if (norder >= 2) {
|
|
289
|
-
const ntry = Math.min(
|
|
290
|
-
|
|
288
|
+
const ntry = Math.min(64, K);
|
|
289
|
+
const iMin = Math.max(0, K - ntry);
|
|
290
|
+
for (let i1 = K - 1; i1 >= iMin; i1--) {
|
|
291
291
|
if (apmask[indices[i1]!] === 1) continue;
|
|
292
|
-
|
|
292
|
+
const row1 = i1 * N;
|
|
293
|
+
for (let i2 = i1 - 1; i2 >= iMin; i2--) {
|
|
293
294
|
if (apmask[indices[i2]!] === 1) continue;
|
|
294
|
-
const
|
|
295
|
-
me[i1]! ^= 1;
|
|
296
|
-
me[i2]! ^= 1;
|
|
297
|
-
const ce = mrbencode(me);
|
|
298
|
-
let _nh = 0;
|
|
295
|
+
const row2 = i2 * N;
|
|
299
296
|
let dd = 0;
|
|
300
297
|
for (let j = 0; j < N; j++) {
|
|
301
|
-
const x =
|
|
302
|
-
_nh += x;
|
|
298
|
+
const x = c0[j]! ^ genmrb[row1 + j]! ^ genmrb[row2 + j]! ^ hdec[j]!;
|
|
303
299
|
dd += x * absrx[j]!;
|
|
304
300
|
}
|
|
305
301
|
if (dd < dmin) {
|
|
306
302
|
dmin = dd;
|
|
307
|
-
|
|
303
|
+
bestFlip1 = i1;
|
|
304
|
+
bestFlip2 = i2;
|
|
308
305
|
}
|
|
309
306
|
}
|
|
310
307
|
}
|
|
311
308
|
}
|
|
312
309
|
|
|
310
|
+
const bestCw = new Int8Array(c0);
|
|
311
|
+
if (bestFlip1 >= 0) {
|
|
312
|
+
const row1 = bestFlip1 * N;
|
|
313
|
+
for (let j = 0; j < N; j++) bestCw[j]! ^= genmrb[row1 + j]!;
|
|
314
|
+
if (bestFlip2 >= 0) {
|
|
315
|
+
const row2 = bestFlip2 * N;
|
|
316
|
+
for (let j = 0; j < N; j++) bestCw[j]! ^= genmrb[row2 + j]!;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
313
320
|
// Reorder codeword back to original order
|
|
314
321
|
const finalCw = new Int8Array(N);
|
|
315
322
|
for (let i = 0; i < N; i++) {
|
|
@@ -321,13 +328,12 @@ function osdDecode174_91(
|
|
|
321
328
|
|
|
322
329
|
// Compute dmin in original order
|
|
323
330
|
let dminOrig = 0;
|
|
324
|
-
const hdecOrig = new Int8Array(N);
|
|
325
|
-
for (let i = 0; i < N; i++) hdecOrig[i] = llr[i]! >= 0 ? 1 : 0;
|
|
326
331
|
let nhe = 0;
|
|
327
332
|
for (let i = 0; i < N; i++) {
|
|
328
|
-
const
|
|
333
|
+
const hard = llr[i]! >= 0 ? 1 : 0;
|
|
334
|
+
const x = finalCw[i]! ^ hard;
|
|
329
335
|
nhe += x;
|
|
330
|
-
dminOrig += x *
|
|
336
|
+
dminOrig += x * absllr[i]!;
|
|
331
337
|
}
|
|
332
338
|
|
|
333
339
|
return {
|
package/src/util/fft.ts
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
* Supports real-to-complex, complex-to-complex, and inverse transforms.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
interface Radix2Plan {
|
|
7
|
+
bitReversed: Uint32Array;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface BluesteinPlan {
|
|
11
|
+
m: number;
|
|
12
|
+
chirpRe: Float64Array;
|
|
13
|
+
chirpIm: Float64Array;
|
|
14
|
+
bFftRe: Float64Array;
|
|
15
|
+
bFftIm: Float64Array;
|
|
16
|
+
aRe: Float64Array;
|
|
17
|
+
aIm: Float64Array;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const RADIX2_PLAN_CACHE = new Map<number, Radix2Plan>();
|
|
21
|
+
const BLUESTEIN_PLAN_CACHE = new Map<string, BluesteinPlan>();
|
|
22
|
+
|
|
6
23
|
export function fftComplex(re: Float64Array, im: Float64Array, inverse: boolean): void {
|
|
7
24
|
const n = re.length;
|
|
8
25
|
if (n <= 1) return;
|
|
@@ -12,9 +29,11 @@ export function fftComplex(re: Float64Array, im: Float64Array, inverse: boolean)
|
|
|
12
29
|
return;
|
|
13
30
|
}
|
|
14
31
|
|
|
32
|
+
const { bitReversed } = getRadix2Plan(n);
|
|
33
|
+
|
|
15
34
|
// Bit-reversal permutation
|
|
16
|
-
let j = 0;
|
|
17
35
|
for (let i = 0; i < n; i++) {
|
|
36
|
+
const j = bitReversed[i]!;
|
|
18
37
|
if (j > i) {
|
|
19
38
|
let tmp = re[i]!;
|
|
20
39
|
re[i] = re[j]!;
|
|
@@ -23,12 +42,6 @@ export function fftComplex(re: Float64Array, im: Float64Array, inverse: boolean)
|
|
|
23
42
|
im[i] = im[j]!;
|
|
24
43
|
im[j] = tmp;
|
|
25
44
|
}
|
|
26
|
-
let m = n >> 1;
|
|
27
|
-
while (m >= 1 && j >= m) {
|
|
28
|
-
j -= m;
|
|
29
|
-
m >>= 1;
|
|
30
|
-
}
|
|
31
|
-
j += m;
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
const sign = inverse ? 1 : -1;
|
|
@@ -59,56 +72,46 @@ export function fftComplex(re: Float64Array, im: Float64Array, inverse: boolean)
|
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
if (inverse) {
|
|
75
|
+
const scale = 1 / n;
|
|
62
76
|
for (let i = 0; i < n; i++) {
|
|
63
|
-
re[i]!
|
|
64
|
-
im[i]!
|
|
77
|
+
re[i] = re[i]! * scale;
|
|
78
|
+
im[i] = im[i]! * scale;
|
|
65
79
|
}
|
|
66
80
|
}
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
function bluestein(re: Float64Array, im: Float64Array, inverse: boolean): void {
|
|
70
84
|
const n = re.length;
|
|
71
|
-
const m =
|
|
72
|
-
const s = inverse ? 1 : -1;
|
|
73
|
-
|
|
74
|
-
const aRe = new Float64Array(m);
|
|
75
|
-
const aIm = new Float64Array(m);
|
|
76
|
-
const bRe = new Float64Array(m);
|
|
77
|
-
const bIm = new Float64Array(m);
|
|
85
|
+
const { m, chirpRe, chirpIm, bFftRe, bFftIm, aRe, aIm } = getBluesteinPlan(n, inverse);
|
|
78
86
|
|
|
87
|
+
aRe.fill(0);
|
|
88
|
+
aIm.fill(0);
|
|
79
89
|
for (let i = 0; i < n; i++) {
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
aRe[i] =
|
|
85
|
-
aIm[i] =
|
|
86
|
-
|
|
87
|
-
bRe[i] = cosA;
|
|
88
|
-
bIm[i] = -sinA;
|
|
89
|
-
}
|
|
90
|
-
for (let i = 1; i < n; i++) {
|
|
91
|
-
bRe[m - i] = bRe[i]!;
|
|
92
|
-
bIm[m - i] = bIm[i]!;
|
|
90
|
+
const cosA = chirpRe[i]!;
|
|
91
|
+
const sinA = chirpIm[i]!;
|
|
92
|
+
const inRe = re[i]!;
|
|
93
|
+
const inIm = im[i]!;
|
|
94
|
+
aRe[i] = inRe * cosA - inIm * sinA;
|
|
95
|
+
aIm[i] = inRe * sinA + inIm * cosA;
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
fftComplex(aRe, aIm, false);
|
|
96
|
-
fftComplex(bRe, bIm, false);
|
|
97
99
|
|
|
98
100
|
for (let i = 0; i < m; i++) {
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
const ar = aRe[i]!;
|
|
102
|
+
const ai = aIm[i]!;
|
|
103
|
+
const br = bFftRe[i]!;
|
|
104
|
+
const bi = bFftIm[i]!;
|
|
105
|
+
aRe[i] = ar * br - ai * bi;
|
|
106
|
+
aIm[i] = ar * bi + ai * br;
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
fftComplex(aRe, aIm, true);
|
|
106
110
|
|
|
107
111
|
const scale = inverse ? 1 / n : 1;
|
|
108
112
|
for (let i = 0; i < n; i++) {
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const sinA = Math.sin(angle);
|
|
113
|
+
const cosA = chirpRe[i]!;
|
|
114
|
+
const sinA = chirpIm[i]!;
|
|
112
115
|
|
|
113
116
|
const r = aRe[i]! * cosA - aIm[i]! * sinA;
|
|
114
117
|
const iIm = aRe[i]! * sinA + aIm[i]! * cosA;
|
|
@@ -117,6 +120,63 @@ function bluestein(re: Float64Array, im: Float64Array, inverse: boolean): void {
|
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
122
|
|
|
123
|
+
function getRadix2Plan(n: number): Radix2Plan {
|
|
124
|
+
let plan = RADIX2_PLAN_CACHE.get(n);
|
|
125
|
+
if (plan) return plan;
|
|
126
|
+
|
|
127
|
+
const bits = 31 - Math.clz32(n);
|
|
128
|
+
const bitReversed = new Uint32Array(n);
|
|
129
|
+
for (let i = 1; i < n; i++) {
|
|
130
|
+
bitReversed[i] = (bitReversed[i >> 1]! >> 1) | ((i & 1) << (bits - 1));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
plan = { bitReversed };
|
|
134
|
+
RADIX2_PLAN_CACHE.set(n, plan);
|
|
135
|
+
return plan;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getBluesteinPlan(n: number, inverse: boolean): BluesteinPlan {
|
|
139
|
+
const key = `${n}:${inverse ? 1 : 0}`;
|
|
140
|
+
const cached = BLUESTEIN_PLAN_CACHE.get(key);
|
|
141
|
+
if (cached) return cached;
|
|
142
|
+
|
|
143
|
+
const m = nextPow2(n * 2 - 1);
|
|
144
|
+
const s = inverse ? 1 : -1;
|
|
145
|
+
const chirpRe = new Float64Array(n);
|
|
146
|
+
const chirpIm = new Float64Array(n);
|
|
147
|
+
for (let i = 0; i < n; i++) {
|
|
148
|
+
const angle = (s * Math.PI * ((i * i) % (2 * n))) / n;
|
|
149
|
+
chirpRe[i] = Math.cos(angle);
|
|
150
|
+
chirpIm[i] = Math.sin(angle);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const bFftRe = new Float64Array(m);
|
|
154
|
+
const bFftIm = new Float64Array(m);
|
|
155
|
+
for (let i = 0; i < n; i++) {
|
|
156
|
+
const cosA = chirpRe[i]!;
|
|
157
|
+
const sinA = chirpIm[i]!;
|
|
158
|
+
bFftRe[i] = cosA;
|
|
159
|
+
bFftIm[i] = -sinA;
|
|
160
|
+
}
|
|
161
|
+
for (let i = 1; i < n; i++) {
|
|
162
|
+
bFftRe[m - i] = bFftRe[i]!;
|
|
163
|
+
bFftIm[m - i] = bFftIm[i]!;
|
|
164
|
+
}
|
|
165
|
+
fftComplex(bFftRe, bFftIm, false);
|
|
166
|
+
|
|
167
|
+
const plan: BluesteinPlan = {
|
|
168
|
+
m,
|
|
169
|
+
chirpRe,
|
|
170
|
+
chirpIm,
|
|
171
|
+
bFftRe,
|
|
172
|
+
bFftIm,
|
|
173
|
+
aRe: new Float64Array(m),
|
|
174
|
+
aIm: new Float64Array(m),
|
|
175
|
+
};
|
|
176
|
+
BLUESTEIN_PLAN_CACHE.set(key, plan);
|
|
177
|
+
return plan;
|
|
178
|
+
}
|
|
179
|
+
|
|
120
180
|
/**
|
|
121
181
|
* Real-to-complex FFT. Input: n real values. Output: n/2+1 complex values
|
|
122
182
|
* stored in re[0..n/2] and im[0..n/2].
|
package/src/util/waveform.ts
CHANGED
|
@@ -3,10 +3,8 @@ const FT8_DEFAULT_SAMPLE_RATE = 12_000;
|
|
|
3
3
|
const FT8_DEFAULT_SAMPLES_PER_SYMBOL = 1_920;
|
|
4
4
|
const FT8_DEFAULT_BT = 2.0;
|
|
5
5
|
|
|
6
|
-
import { NSPS as FT4_NSPS } from "../ft4/constants.js";
|
|
7
|
-
|
|
8
6
|
const FT4_DEFAULT_SAMPLE_RATE = 12_000;
|
|
9
|
-
const FT4_DEFAULT_SAMPLES_PER_SYMBOL =
|
|
7
|
+
const FT4_DEFAULT_SAMPLES_PER_SYMBOL = 576;
|
|
10
8
|
const FT4_DEFAULT_BT = 1.0;
|
|
11
9
|
const MODULATION_INDEX = 1.0;
|
|
12
10
|
|
|
@@ -15,6 +13,7 @@ export interface WaveformOptions {
|
|
|
15
13
|
samplesPerSymbol?: number;
|
|
16
14
|
bt?: number;
|
|
17
15
|
baseFrequency?: number;
|
|
16
|
+
initialPhase?: number;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
interface WaveformDefaults {
|
|
@@ -68,6 +67,7 @@ function generateGfskWaveform(
|
|
|
68
67
|
const nsps = options.samplesPerSymbol ?? defaults.samplesPerSymbol;
|
|
69
68
|
const bt = options.bt ?? defaults.bt;
|
|
70
69
|
const f0 = options.baseFrequency ?? 0;
|
|
70
|
+
const initialPhase = options.initialPhase ?? 0;
|
|
71
71
|
|
|
72
72
|
assertPositiveFinite(sampleRate, "sampleRate");
|
|
73
73
|
assertPositiveFinite(nsps, "samplesPerSymbol");
|
|
@@ -75,6 +75,9 @@ function generateGfskWaveform(
|
|
|
75
75
|
if (!Number.isFinite(f0)) {
|
|
76
76
|
throw new Error("baseFrequency must be finite");
|
|
77
77
|
}
|
|
78
|
+
if (!Number.isFinite(initialPhase)) {
|
|
79
|
+
throw new Error("initialPhase must be finite");
|
|
80
|
+
}
|
|
78
81
|
if (!Number.isInteger(nsps)) {
|
|
79
82
|
throw new Error("samplesPerSymbol must be an integer");
|
|
80
83
|
}
|
|
@@ -111,7 +114,8 @@ function generateGfskWaveform(
|
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
const wave = new Float32Array(nwave);
|
|
114
|
-
let phi =
|
|
117
|
+
let phi = initialPhase % TWO_PI;
|
|
118
|
+
if (phi < 0) phi += TWO_PI;
|
|
115
119
|
const phaseStart = shape.includeRampSymbols ? 0 : nsps;
|
|
116
120
|
for (let k = 0; k < nwave; k++) {
|
|
117
121
|
const j = phaseStart + k;
|