Browse Source

first commit

Keelan Lightfoot 8 years ago
commit
a9892cd531
3 changed files with 448 additions and 0 deletions
  1. 3
    0
      README.md
  2. 110
    0
      rtty.html
  3. 335
    0
      rtty.js

+ 3
- 0
README.md View File

@@ -0,0 +1,3 @@
1
+# rtty.js
2
+
3
+A FSK modulator implemented purely in Javascript. Requires a browser with reasonbly good support for Web Audio (Chrome, Safari).

+ 110
- 0
rtty.html View File

@@ -0,0 +1,110 @@
1
+<html>
2
+<head>
3
+<meta charset="utf-8">
4
+<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
5
+<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
6
+<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
7
+<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
8
+<script src="rtty.js"></script>
9
+<style>
10
+button, input, textarea {
11
+	margin-bottom: 10px;
12
+}
13
+
14
+</style>
15
+</head>
16
+<body>
17
+	<div class="container">
18
+		<div class="row">
19
+			<div class="col-md-12">
20
+				<h1>rtty.js</h1>
21
+			</div>
22
+		</div>
23
+		
24
+		<div class="row">
25
+			<div class="col-md-4">
26
+				<div class="panel panel-default">
27
+					<div class="panel-heading"><h3 class="panel-title">Output Control</h3></div>
28
+					<div class="panel-body">
29
+						<b>Baud Rate:</b> <input class="form-control" id="baudrate" type="number" value="45.45">
30
+						<b>Mark Frequency:</b> <input class="form-control" id="markfrequency" type="number" value="2125">
31
+						<b>Space Frequency:</b> <input class="form-control" id="spacefrequency" type="number" value="2295">
32
+						<button class="btn btn-default" id="start">Start Audio</button>
33
+						<button class="btn btn-default" id="stop">Stop Audio</button>
34
+						<button class="btn btn-default" id="invert">Invert</button>
35
+					</div>
36
+				</div>
37
+				<div class="panel panel-default">
38
+					<div class="panel-heading"><h3 class="panel-title">Modulator Control</h3></div>
39
+					<div class="panel-body">
40
+						<button class="btn btn-default" id="pause">Pause</button>
41
+						<button class="btn btn-default" id="resume">Resume</button>
42
+						<button class="btn btn-default" id="flush">Flush Buffer</button>
43
+					</div>
44
+				</div>
45
+			</div>
46
+			<div class="col-md-8">
47
+				<div class="panel panel-default">
48
+					<div class="panel-heading"><h3 class="panel-title">Input</h3></div>
49
+					<div class="panel-body">
50
+<textarea class="form-control" id="data" rows="14">
51
+Is it a fact—or have I dreamt it—that, by means of electricity, the world of matter has become a great nerve, vibrating thousands of miles in a breathless point of time?
52
+</textarea>
53
+						<button class="btn btn-default" id="encode">Encode</button>
54
+					</div>
55
+				</div>
56
+			</div>	
57
+		</div>
58
+	</div>
59
+</body>
60
+<script>
61
+
62
+	var modulator = new RTTY({
63
+		baudrate: parseFloat(document.querySelector('#baudrate').value),
64
+		mark: parseFloat(document.querySelector('#markfrequency').value),
65
+		space: parseFloat(document.querySelector('#spacefrequency').value)
66
+	});
67
+	
68
+	modulator.bufferEmptyCallback = function() {
69
+	};
70
+
71
+	document.querySelector('#start').onclick = function() {
72
+		document.querySelector('#baudrate').disabled=true;
73
+		document.querySelector('#markfrequency').disabled=true;
74
+		document.querySelector('#spacefrequency').disabled=true;
75
+		modulator.baudrate = parseFloat(document.querySelector('#baudrate').value);
76
+		modulator.mark = parseFloat(document.querySelector('#markfrequency').value);
77
+		modulator.space = parseFloat(document.querySelector('#spacefrequency').value);
78
+		modulator.startSound();
79
+	};
80
+
81
+	document.querySelector('#stop').onclick = function() {
82
+		modulator.stopSound();
83
+		document.querySelector('#baudrate').disabled=false;
84
+		document.querySelector('#markfrequency').disabled=false;
85
+		document.querySelector('#spacefrequency').disabled=false;
86
+	};
87
+
88
+	document.querySelector('#invert').onclick = function() {
89
+		modulator.invert();
90
+	};
91
+
92
+	document.querySelector('#pause').onclick = function() {
93
+		modulator.pauseModulation();
94
+	};
95
+
96
+	document.querySelector('#resume').onclick = function() {
97
+		modulator.resumeModulation();
98
+	};
99
+	
100
+	document.querySelector('#encode').onclick = function() {
101
+		modulator.modulate(document.querySelector('#data').value);
102
+		
103
+	};
104
+	
105
+	document.querySelector('#flush').onclick = function() {
106
+		modulator.flushBuffer();
107
+	};
108
+
109
+</script>
110
+</html>

+ 335
- 0
rtty.js View File

@@ -0,0 +1,335 @@
1
+var RTTY = RTTY || {};
2
+
3
+var RTTY = function(props) {
4
+    
5
+    this.mark = props.mark;
6
+	this.space = props.space;
7
+	this.baudrate = props.baudrate;
8
+	
9
+	this.bufferEmptyCallback = null;
10
+	this.characterDoneCallback = null;
11
+	
12
+	this.fadeDoneCallback = null;
13
+	
14
+    this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
15
+	
16
+	this.osc = 0;
17
+	this.freq = this.mark;
18
+
19
+	this.buffer = [];
20
+	this.bufferOffset = 0;
21
+	
22
+	this.idleBits = 0;
23
+	
24
+	this.bit = 8;
25
+	this.ct = {};
26
+	this.figsmask = 1<<5;
27
+	this.currentchar = 0x00;
28
+	this.pause = false;
29
+	this.suspend = false;
30
+	this.source = null;
31
+	this.figures = true;
32
+	this.unshift = false;
33
+	this.idle = true;
34
+	this.bufferEmptyCalled = false;
35
+	this.flush = null;
36
+	this.amplitude = 1;
37
+	this.fade = 0;
38
+	
39
+	this.initTables();
40
+	
41
+	this.scriptNode = this.audioCtx.createScriptProcessor(4096, 1, 1);
42
+
43
+	this.scriptNode.onaudioprocess = (function(audioProcessingEvent) {
44
+		var inputBuffer = audioProcessingEvent.inputBuffer;
45
+		var outputBuffer = audioProcessingEvent.outputBuffer;
46
+		var inputData = inputBuffer.getChannelData(0);
47
+		var outputData = outputBuffer.getChannelData(0);
48
+		for (var sample = 0; sample < inputBuffer.length; sample++) {
49
+			if (!this.suspend && inputData[sample]==1) { // bit marker
50
+				if (this.idleBits > 0) {
51
+					this.idleBits--;
52
+					if (this.idleBits==0) {
53
+						if (this.characterDoneCallback != null && typeof this.characterDoneCallback == "function") {
54
+							this.characterDoneCallback(bufferLen);
55
+						}
56
+						this.idle = true;
57
+					}
58
+				} else if (this.pause && this.idle) {
59
+					this.suspend = true;
60
+					this.pause = false;
61
+				} else if (this.idle) {
62
+					if (this.flush != null) {
63
+						this.buffer = [];
64
+						this.bufferOffset = 0;
65
+						if (typeof this.flush == "function") {
66
+							this.flush();
67
+						}
68
+						this.flush = null;
69
+					}
70
+					var bufferLen = this.buffer.length - this.bufferOffset;
71
+					if (bufferLen > 0)  {
72
+						this.idle = false;
73
+						if (this.bufferEmptyCalled) {
74
+							this.bufferEmptyCalled = false;
75
+						}
76
+						var next = this.buffer[this.bufferOffset];
77
+						if (++this.bufferOffset * 2 >= this.buffer.length){
78
+							this.buffer  = this.buffer.slice(this.bufferOffset);
79
+							this.bufferOffset = 0;
80
+						}
81
+						switch (next.type) {
82
+						case "char":
83
+							this.currentchar = next.val;
84
+							break;
85
+						case "pause":
86
+							this.idleBits = next.val;
87
+							break;	
88
+						case "callback":
89
+							next.val();
90
+							this.idle = true;
91
+							break;	
92
+						}
93
+					} else if (!this.bufferEmptyCalled) {
94
+						if (this.bufferEmptyCallback != null && typeof this.bufferEmptyCallback == "function") {
95
+							this.bufferEmptyCallback();
96
+							this.bufferEmptyCalled = true;
97
+						} 
98
+					}
99
+				} else {
100
+					if (this.bit==0) { // start
101
+						this.freq = this.space;
102
+					}
103
+					if (this.bit >= 1 && this.bit <= 5) { // data bits
104
+						this.freq = (this.currentchar&1)?this.mark:this.space;
105
+						this.currentchar=this.currentchar>>1;
106
+					}
107
+					if (this.bit==6||this.bit==7) { // stop bits
108
+						this.freq = this.mark;
109
+					}
110
+					if (this.bit==8) {
111
+						this.bit=0;
112
+						this.idle=true;
113
+						if (this.characterDoneCallback != null && typeof this.characterDoneCallback == "function") {
114
+							this.characterDoneCallback(bufferLen);
115
+						}
116
+					} else {
117
+						this.bit++;
118
+					}
119
+				}
120
+			}
121
+			
122
+			outputData[sample] =  this.waveTable[this.osc]*this.amplitude;
123
+			this.osc = (this.osc+this.freq)%this.audioCtx.sampleRate;
124
+			
125
+			if (this.fade > 0 && (this.amplitude + this.fade) > 1) {
126
+				this.fade = 0;
127
+				this.amplitude = 1;
128
+				if (this.fadeDoneCallback != null && typeof this.fadeDoneCallback == "function") {
129
+					this.fadeDoneCallback();
130
+					this.fadeDoneCallback = null;
131
+				}
132
+			} else if (this.fade < 0 && (this.amplitude + this.fade) < 0) {
133
+				this.fade = 0;
134
+				this.amplitude = 0;
135
+				if (this.fadeDoneCallback != null && typeof this.fadeDoneCallback == "function") {
136
+					this.fadeDoneCallback();
137
+					this.fadeDoneCallback = null;
138
+				}
139
+			}
140
+			this.amplitude += this.fade;
141
+		}
142
+	}).bind(this); 
143
+	
144
+}
145
+
146
+RTTY.prototype.initTables = function() {
147
+	this.waveTable = new Float32Array(this.audioCtx.sampleRate);
148
+	for (var i=0;i<this.audioCtx.sampleRate;i++) {
149
+		this.waveTable[i] = Math.sin((i/this.audioCtx.sampleRate)*2*Math.PI);
150
+	}
151
+	this.ct = {
152
+		"0": 0x36, "1": 0x37, "2": 0x33, "3": 0x21, "4": 0x2a, "5": 0x30, "6": 0x35, 
153
+		"7": 0x25, "8": 0x26, "9": 0x38, "A": 0x03, "B": 0x19, "C": 0x0e, "D": 0x09, 
154
+		"E": 0x01, "F": 0x0d, "G": 0x1a, "H": 0x14, "I": 0x06, "J": 0x0b, "K": 0x0f, 
155
+		"L": 0x12, "M": 0x1c, "N": 0x0c, "O": 0x18, "P": 0x16, "Q": 0x17, "R": 0x0a, 
156
+		"S": 0x05, "T": 0x10, "U": 0x07, "V": 0x1e, "W": 0x13, "X": 0x1d, "Y": 0x15, 
157
+		"Z": 0x11, "\r": 0x02, "\n": 0x08, "'": 0x2b, "-": 0x23, "_": 0x23, ",": 0x2c, 
158
+		"!": 0x2d, ":": 0x2e, "(": 0x2f, "+": 0x31, ")": 0x32, "#": 0x34, "?": 0x39, 
159
+		"&": 0x3a, ".": 0x3c, "/": 0x3d, ";": 0x3e, "$": 0x29
160
+	}
161
+};
162
+
163
+
164
+RTTY.prototype.setFade = function(fade, callback) {
165
+	this.fade = fade;
166
+	this.fadeDoneCallback = callback;
167
+}
168
+
169
+RTTY.prototype.pauseModulation = function() {
170
+	this.pause = true;
171
+}
172
+
173
+RTTY.prototype.resumeModulation = function() {
174
+	this.suspend = false;
175
+}
176
+
177
+
178
+
179
+RTTY.prototype.startSound = function() {
180
+	if (this.source == null) {
181
+		this.idle = true;
182
+		this.amplitude = 0;
183
+		var samplesPerSymbol = Math.floor(this.audioCtx.sampleRate/this.baudrate);
184
+		var myArrayBuffer = this.audioCtx.createBuffer(1, samplesPerSymbol, this.audioCtx.sampleRate);
185
+		this.source = this.audioCtx.createBufferSource();
186
+		this.source.buffer = myArrayBuffer;
187
+		this.source.loop = true;
188
+		
189
+		var tickTrack = myArrayBuffer.getChannelData(0);
190
+		tickTrack[0] = 1;
191
+		
192
+		this.freq = this.mark;
193
+		
194
+		this.source.connect(this.scriptNode);
195
+		this.scriptNode.connect(this.audioCtx.destination);
196
+		this.source.start();	
197
+		
198
+		this.setFade(.01, null);	
199
+	} 
200
+}
201
+
202
+RTTY.prototype.stopSound = function() {
203
+	if (this.source != null) {
204
+		var me = this
205
+		this.flushBuffer(function(){
206
+			me.setFade(-.01, function(){
207
+				setTimeout(function(){
208
+					me.source.stop();
209
+	 				me.scriptNode.disconnect();
210
+	 				me.source = null;
211
+				}, 500);
212
+			});
213
+		});
214
+	}
215
+}
216
+
217
+RTTY.prototype.flushBuffer = function(callback) {
218
+	if (callback == null) {
219
+		callback=function(){};
220
+	}
221
+	this.flush = callback;
222
+}
223
+
224
+RTTY.prototype.invert = function() {
225
+	var x = this.mark;
226
+	if (this.freq==this.mark) {
227
+		this.freq=this.space;
228
+	} else {
229
+		this.freq=this.mark;
230
+	}
231
+	this.mark = this.space;
232
+	this.space = x;
233
+}
234
+
235
+
236
+RTTY.prototype.modulate = function(str) {
237
+	for (var i = 0, len = str.length; i < len; i++) {
238
+		this.modulateChar(str[i]);
239
+	}
240
+}
241
+
242
+RTTY.prototype.modulateChar = function(c) {
243
+	var baudot
244
+	
245
+	var c = c.toUpperCase();
246
+	
247
+	switch (c) {
248
+		case 0x00: // null
249
+			this.pushChar(0x00); // null
250
+			break;
251
+		case 0x0a: // lf
252
+			this.pushChar(0x02); // cr
253
+			this.pushChar(0x08); // lf
254
+			break;
255
+		case ' ': // space
256
+			// if we print a space in figures mode, terminals set to
257
+			// unshift on space will unshift. Remember that, so that if a
258
+			// figures character needs to be printed, we can turn figures
259
+			// mode back on before printing it.
260
+			if (this.figures) {
261
+				this.unshift = true;
262
+			}
263
+			this.pushChar(0x04);
264
+			break;
265
+		case '@': // Change @ to (at)
266
+			this.figs();
267
+			this.pushChar(this.ct['(']);
268
+			this.ltrs();
269
+			this.pushChar(this.ct['A']);
270
+			this.pushChar(this.ct['T']);
271
+			this.figs();
272
+			this.pushChar(this.ct[')']);
273
+			break;
274
+		case '"': // use two single quotes as a double quote
275
+			this.figs();
276
+			this.pushChar(this.ct["'"]);
277
+			this.pushChar(this.ct["'"]);
278
+			break;
279
+		default:
280
+			baudot=this.ct[c];
281
+			if (baudot!=undefined) {
282
+				// correction for unshift on space
283
+				if ((baudot&this.figsmask)&&this.figures&&this.unshift) {
284
+					this.figs();
285
+				}
286
+				if ((baudot&this.figsmask)&&!this.figures) {
287
+					this.figs();
288
+				}
289
+				if (!(baudot&this.figsmask)&&this.figures) {
290
+					this.ltrs();
291
+				}
292
+				this.pushChar(baudot);
293
+			}
294
+			break;
295
+	}
296
+
297
+}
298
+
299
+RTTY.prototype.pushChar = function(c) {
300
+	this.buffer.push({
301
+		type: "char",
302
+		val: c
303
+	});
304
+
305
+}
306
+
307
+RTTY.prototype.pushPause = function(p) {
308
+	this.buffer.push({
309
+		type: "pause",
310
+		val: p
311
+	});
312
+}
313
+
314
+RTTY.prototype.pushCallback = function(callback) {
315
+	this.buffer.push({
316
+		type: "callback",
317
+		val: callback
318
+	});
319
+}
320
+
321
+
322
+RTTY.prototype.figs = function() {
323
+	this.pushChar(0x1b); // figs
324
+	this.figures=true;
325
+	this.unshift=false;
326
+}
327
+
328
+RTTY.prototype.ltrs = function() {
329
+	this.pushChar(0x1f); // ltrs
330
+	this.figures=false;
331
+	this.unshift=false;
332
+}
333
+
334
+
335
+