forked from dolske/modem.js
-
Notifications
You must be signed in to change notification settings - Fork 0
/
afsk-encoder.js
340 lines (287 loc) · 11.8 KB
/
afsk-encoder.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
function AfskEncoder(data, sampleRate, baud) {
// Convert JavaScript's 16bit UCS2 characters to a UTF8 string, so we can
// treat data[x] as an 8-bit byte for transmission.
var utf8data = unescape(encodeURIComponent(data));
this.expandFlags(utf8data); // into this.symbolData + numResidueBits
// can convert back with decodeURIComponent(escape(utf8data))
this.sampleRate = sampleRate;
this.baud = baud;
// Unclear if the orig code uses this. But allows preamble and trailer to be
// specified by time, which then gets converted to how many bytes to send.
var preambleTime = 0.04;
var trailerTime = 0.04;
this.preambleBytes = Math.ceil(preambleTime / (8 / baud)); // 8 bits-per-byte
this.trailerBytes = Math.ceil(trailerTime / (8 / baud)); // 8 bits-per-byte
//this.preambleBytes = this.trailerBytes = 1;
console.log("Encoder: " + this.preambleBytes + " preamble bytes, " +
this.trailerBytes + " trailer Bytes");
// Explain this?
this.phaseIncrementFreqHi = 2 * Math.PI * this.freqHi / sampleRate;
this.phaseIncrementFreqLo = 2 * Math.PI * this.freqLo / sampleRate;
// Orig code uses tx_symbol_phase and phase_inc_symbol as such:
// phase_inc_symbol = 0;
// ...
// for each bit:
// while (tx_symbol_phase < (float) (2.0*Math.PI)) {
// tx_symbol_phase += phase_inc_symbol;
// ...
// (compute a sample's value)
// }
// tx_symbol_phase -= (float) (2.0*Math.PI);
//
// This is basically a samples-per-bit counter, with a slight residual to
// keep long-term timing more accurate. I'm going to clarify and code it
// like that (using a samples-pre-bit as a fractional float).
this.samplesPerBit = sampleRate / baud; // Not rounded! Floating point!
var numBits = 8 * this.preambleBytes + 8 * this.symbolData.length + 8 * this.trailerBytes;
this.numSamplesRequired = Math.ceil(numBits * this.samplesPerBit);
// Set initial state
this.state.current = this.state.PREAMBLE;
this.state.phase = 0;
this.state.currentByte = this.PREAMBLE_BYTE;
this.state.unprocessedBytes = this.preambleBytes - 1;
this.state.unprocessedBits = 8;
this.state.bitBuffer = new Float32Array(Math.ceil(this.samplesPerBit));
this.state.bitBufferBegin = 0;
this.state.bitBufferEnd = 0;
}
AfskEncoder.prototype = {
symbolData: null,
numResidueBits: 0,
sampleRate: 0,
baud: 0,
freqHi: 2200,
freqLo: 1200,
phaseIncrementFreqHi: 0,
phaseIncrementFreqLo: 0,
samplesPerBit: 0.0,
PREAMBLE_BYTE: 0x7E,
TRAILER_BYTE: 0x7E,
state : {
current: 1,
IDLE : 0,
PREAMBLE : 1,
DATA : 2,
TRAILER : 3,
phase: 0.0, // loops 0 --> 2*PI
samplesPerBitResidual: 0.0,
unprocessedBits: 0,
unprocessedBytes: 0,
currentByte: 0,
nrziToggle: false,
bitBuffer: null,
bitBufferBegin: 0,
bitBufferEnd: 0,
},
/*
* Implement HDLC/AX.25 bitstuffing. For any run of 5 sequential 1-bits,
* a 0-bit is inserted (stuffed) into the stream. This serves two purposes:
* One is to help with timing (clock-skew) on the receiving end, as it
* ensures that the maximum length of a constant frequency is limited to 5
* bit intervals (the frequency is toggled upon a 0 bit). It also allows the
* sequence 0x7E (01111110) to serve as a special flag symbol that will not
* be encountered in the data stream (i.e., it's escaped/stuffed). On the
* reveiving end, a 0-bit after 5 contigious 1-bits will be discarded to
* restore the original unstuffed data.
*
* XXX http://www.interfacebus.com/HDLC_Protocol_Description.html says
* the preamble is 0x7E, with bitstuffing for user data inserting a whole
* _byte_, * eg 0x7E --> 0x7D, 0x5E and 0x7D --> 0x7D, 0x5D
*/
expandFlags: function(utf8data) {
var maxLength = Math.ceil(utf8data.length * 6 / 5);
var buf = new Uint8Array(maxLength);
var i = 0;
var sequentialOnes = 0;
var residueBits = 0;
var numResidueBits = 0;
for (var c = 0; c < utf8data.length; c++) {
residueBits = residueBits | (utf8data.charCodeAt(c) << numResidueBits);
numResidueBits += 8;
//console.log("Loop input is: 0x" + residueBits.toString(16) + " (len = " + numResidueBits + " bits)");
// Worst case: we had 7 residue bits + 8 new bits (now 15). Could need to stuff
// 2 bits (if sequentialOnes = 4, first bit from new utf8data bytes
// causes a stuff, in remaining 7 new bits could have one more stuff)
// We don't start at index 0, since we've already processed these bits
// in the last loop.
for (var b = (numResidueBits - 8); b < numResidueBits; b++) {
if (residueBits & (1 << b))
sequentialOnes++;
else
sequentialOnes = 0;
//console.log("ch[" + c + "] bit[" + b + "], seqOnes=" + sequentialOnes);
if (sequentialOnes == 5) {
sequentialOnes = 0;
//console.log("-- stuffing! --");
// piece together an expanded residueBits
var hiMask = 0x1FFFF << (b + 1)
var hiBits = (residueBits & hiMask) << 1;
//console.log("hiMask = " + hiMask.toString(16));
//console.log("hiBits = " + hiBits.toString(16));
var loMask = ~hiMask & 0xFFFFF;
var loBits = residueBits & loMask;
//console.log("loMask = " + loMask.toString(16));
//console.log("loBits = " + loBits.toString(16));
residueBits = hiBits | loBits; // 0-bit in between
// console.log("resBits= " + residueBits.toString(16));
// Skip over the bit we just stuffed
b++;
numResidueBits++;
}
}
// Worst case: we had 15 residue bits, now have 17 due to stuffing.
// Always have at least 8 bits
buf[i++] = residueBits & 0xFF;
numResidueBits -= 8;
residueBits >>>= 8;
// If we have a whole byte's worth of residue, emit it now.
if (numResidueBits >= 8) {
buf[i++] = residueBits & 0xFF;
residueBits >>>= 8;
numResidueBits -= 8;
}
// If we _still_ have a whole byte's worth of residue, emit it now.
if (numResidueBits >= 8) {
buf[i++] = residueBits & 0xFF;
residueBits >>>= 8;
numResidueBits -= 8;
}
//console.log("Loop residue is: 0x" + residueBits.toString(16) + " (len = " + numResidueBits + ") @ " + i);
}
if (numResidueBits) {
buf[i++] = residueBits & 0xFF;
}
// Trim view to the space we used.
buf = buf.subarray(0, i);
//console.log("Output buffer: " + this.dumpBuffer(buf));
this.symbolData = buf;
this.numResidueBits = numResidueBits;
},
dumpBuffer: function(buf) {
var out = "";
for (var i = 0; i < buf.length; i++)
out += "0x" + buf[i].toString(16) + ",";
return out;
},
modulate: function(samples) {
var state = this.state;
// actual start index of complete buffer (not just chunk);
var actualOffset = samples.byteOffset / 4;
//console.log("-- modulate for " + samples.length + " samples @ " + actualOffset + "--");
if (state.current == state.IDLE)
return;
var i = 0;
do {
i += this.drainBitBuffer(samples, i);
if (this.bitBufferRemaining())
break;
if (!state.unprocessedBits) {
if (!state.unprocessedBytes) {
//
// No more data for current state, so transition to next state.
// Recursively call ourselves to process the next state.
//
if (state.current == state.PREAMBLE) {
state.current = state.DATA;
state.currentByte = this.symbolData[0];
state.unprocessedBytes = this.symbolData.length - 1;
state.unprocessedBits = 8;
// console.log("...recursing for state DATA...");
this.modulate(samples.subarray(i, samples.length));
} else if (state.current == state.DATA) {
state.current = state.TRAILER;
state.currentByte = this.TRAILER_BYTE;
state.unprocessedBytes = this.trailerBytes - 1;
state.unprocessedBits = 8;
//console.log("...recursing for state TRAILER...");
this.modulate(samples.subarray(i, samples.length));
} else if (state.current == state.TRAILER) {
state.current = state.IDLE;
} else {
throw "can't transition from unexpected state";
}
return;
}
if (state.current == state.PREAMBLE) {
state.currentByte = this.PREAMBLE_BYTE;
state.unprocessedBits = 8;
} else if (state.current == state.TRAILER) {
state.currentByte = this.TRAILER_BYTE;
state.unprocessedBits = 8;
} else if (state.current == state.DATA) {
var b = this.symbolData.length - state.unprocessedBytes;
state.currentByte = this.symbolData[b];
state.unprocessedBits = 8;
// If we're processing the last byte of data, it might be a partial
// byte due to bit flag expansion.
if (b == this.symbolData.length - 1 && this.numResidueBits) {
console.log("partial last data byte (" + this.numResidueBits + " bits)");
state.unprocessedBits = this.numResidueBits;
}
} else {
throw "unexpected next byte state";
}
state.unprocessedBytes--;
}
var bit = !!(state.currentByte & 1);
state.currentByte >>>= 1;
state.unprocessedBits--;
this.fillBitBuffer(bit);
} while (i < samples.length);
},
// given a bit (0x00 or 0x01), generate a baud's worth of waveform
// into the bitBuffer. Eventually this could probably just copy from
// a pre-rendered waveform (or more like drain could).
fillBitBuffer: function (bit) {
var state = this.state;
if (state.bitBufferBegin != 0 ||
state.bitBufferEnd != 0)
throw "Uhh, can't fill a bitBuffer with stuff in it.";
// The number of samples for a bit is usualy a non-integer, so for better
// long-term timing we carry over the residual. Thus the exact number of
// samples to encode a particular bit may vary by 1.
var fracSamples = state.samplesPerBitResidual + this.samplesPerBit;
var numSamples = Math.floor(fracSamples);
state.samplesPerBitResidual = fracSamples - numSamples;
if (numSamples > state.bitBuffer.length)
throw "Uhh, we want to make more samples than bitBuffer holds";
// Bell 202 uses a low frequency tone (1200Hz) for a "mark" symbol (bit),
// and a high frequency tone (2200Hz) for a "space" symbol. The encoding
// uses NRZI (non-return to zero inverted) encoding. For a terrible
// explanation, see http://en.wikipedia.org/wiki/Non-return-to-zero
//
// Basically NRZI just means that to encode a 0-bit we need to switch
// the frequency and encode a symbol, while to encode a 1-bit we
// remain on the same frequency. We are encoding transitions, instead of
// simply mapping bit values to frequncies.
var phaseInc;
if (!bit)
state.nrziToggle = !state.nrziToggle;
phaseInc = state.nrziToggle ? this.phaseIncrementFreqHi : this.phaseIncrementFreqLo;
while (numSamples--) {
state.bitBuffer[state.bitBufferEnd++] = Math.sin(state.phase);
state.phase += phaseInc;
if (state.phase > Math.PI * 2)
state.phase -= Math.PI * 2;
}
},
drainBitBuffer: function (samples, i) {
var toCopy = this.bitBufferRemaining();
var spaceLeft = samples.length - i;
if (spaceLeft < toCopy)
toCopy = spaceLeft;
var state = this.state;
for (var more = toCopy; more; more--, i++) {
samples[i] = state.bitBuffer[state.bitBufferBegin++];
}
// If we drained it, reset counters to the beginning.
if (!this.bitBufferRemaining()) {
state.bitBufferBegin = 0;
state.bitBufferEnd = 0;
}
return toCopy;
},
bitBufferRemaining : function() {
return this.state.bitBufferEnd - this.state.bitBufferBegin;
},
};