├── .gitignore ├── shell.nix ├── README.md ├── babel.config.json ├── package.json ├── dist └── index.html └── src ├── index.js └── rtty.js /.gitignore: -------------------------------------------------------------------------------- 1 | output.js 2 | dist/main.js 3 | node_modules 4 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | with import {}; 2 | 3 | pkgs.mkShell { 4 | buildInputs = [ 5 | pkgs.nodejs 6 | ]; 7 | 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | To build for development, clone it somewhere that you can serve it by a webserver (PHP's built in webserver or Python's would work). 2 | 3 | ```bash 4 | npm install 5 | npm run watch 6 | ``` 7 | 8 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "edge": "17", 8 | "firefox": "60", 9 | "chrome": "67", 10 | "safari": "11.1" 11 | } 12 | } 13 | ] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webrtty", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "scripts": { 7 | "build": "npx webpack", 8 | "watch": "npx webpack watch" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@babel/cli": "^7.16.7", 14 | "@babel/core": "^7.16.7", 15 | "@babel/preset-env": "^7.16.7", 16 | "acorn": "^8.7.0", 17 | "webpack": "^5.65.0", 18 | "webpack-cli": "^4.9.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebRTTY test page 6 | 7 | 8 | 9 |

Test page

10 | 11 |
12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import RTTY from './rtty'; 2 | 3 | function go(window, document) { 4 | var context = new AudioContext(); 5 | var rtty = new RTTY(context); 6 | 7 | document.getElementById('rttyEncode').addEventListener('click', function (e) { 8 | e.preventDefault(); 9 | var elem = document.getElementById('rttyChars'); 10 | var buffer = rtty.encode(elem.value); 11 | 12 | var source = context.createBufferSource(); 13 | source.buffer = buffer; 14 | source.connect(context.destination); 15 | source.start(); 16 | }, true); 17 | 18 | document.getElementById('rttyDownload').addEventListener('click', function (e) { 19 | e.preventDefault(); 20 | var elem = document.getElementById('rttyChars'); 21 | var buffer = rtty.encode(elem.value); 22 | 23 | rtty.writeWav(buffer); 24 | }, true); 25 | 26 | } 27 | 28 | go(window, document); -------------------------------------------------------------------------------- /src/rtty.js: -------------------------------------------------------------------------------- 1 | export default class RTTY 2 | { 3 | constructor(context) { 4 | if (!context) { 5 | context = this.getContext(); 6 | } 7 | 8 | this.context = context; 9 | 10 | this.timePerBit = 1 / 45.45; 11 | this.spaceFreq = 2295; 12 | this.markFreq = 2125; 13 | 14 | this.letters = { 15 | "\0": '00000', 16 | ' ': '00100', 17 | 'Q': '10111', 18 | 'W': '10011', 19 | 'E': '00001', 20 | 'R': '01010', 21 | 'T': '10000', 22 | 'Y': '10101', 23 | 'U': '00111', 24 | 'I': '00110', 25 | 'O': '11000', 26 | 'P': '10110', 27 | 'A': '00011', 28 | 'S': '00101', 29 | 'D': '01001', 30 | 'F': '01101', 31 | 'G': '11010', 32 | 'H': '10100', 33 | 'J': '01011', 34 | 'K': '01111', 35 | 'L': '10010', 36 | 'Z': '10001', 37 | 'X': '11101', 38 | 'C': '01110', 39 | 'V': '11110', 40 | 'B': '11001', 41 | 'N': '01100', 42 | 'M': '11100', 43 | "\r": '01000', 44 | "\n": '00010' 45 | }; 46 | 47 | this.figures = { 48 | "\0": '00000', 49 | ' ': '00100', 50 | '1': '10111', 51 | '2': '10011', 52 | '3': '00001', 53 | '4': '01010', 54 | '5': '10000', 55 | '6': '10101', 56 | '7': '00111', 57 | '8': '00110', 58 | '9': '11000', 59 | '0': '10110', 60 | '–': '00011', 61 | 'Bell': '00101', 62 | '$': '01001', 63 | '!': '01101', 64 | '&': '11010', 65 | '#': '10100', 66 | '\'': '01011', 67 | '(': '01111', 68 | ')': '10010', 69 | '"': '10001', 70 | '/': '11101', 71 | ':': '01110', 72 | ';': '11110', 73 | '?': '11001', 74 | ',': '01100', 75 | '.': '11100', 76 | "\r": '01000', 77 | "\n": '00010' 78 | }; 79 | 80 | this.figureShift = '11011'; 81 | this.letterShift = '11111'; 82 | } 83 | 84 | getContext() { 85 | try { 86 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 87 | return new AudioContext(); 88 | } catch (e) { 89 | alert('Sorry, you have no web audio support.'); 90 | } 91 | } 92 | 93 | convertToBinary(chars) { 94 | var self = this; 95 | let charCodes = []; 96 | chars = chars.toUpperCase(); 97 | 98 | let inLetters = true; 99 | for (var i = 0; i < chars.length; i++) { 100 | let val = chars.charAt(i); 101 | if (this.letters[val]) { 102 | if (!inLetters) { 103 | charCodes.push(this.letterShift); 104 | inLetters = true; 105 | } 106 | 107 | charCodes.push(this.letters[val].split('').reverse().join("")); 108 | } else if (this.figures[val]) { 109 | if (inLetters) { 110 | charCodes.push(this.figureShift); 111 | inLetters = false; 112 | } 113 | 114 | charCodes.push(this.figures[val].split('').reverse().join("")); 115 | } 116 | } 117 | 118 | return charCodes; 119 | }; 120 | 121 | encode(chars) { 122 | let messageSamples = Math.floor(44100 / 45.45) * 8 * (chars.length + 4); 123 | 124 | let binary = this.convertToBinary(chars); 125 | let buffer = this.context.createBuffer(1, messageSamples, 44100); 126 | let data = buffer.getChannelData(0); 127 | let samplesPerSymbol = Math.floor(44100 / 45.45); 128 | 129 | let digits = '111111111111111' + binary.reduce(function(r, c) { 130 | return r + '0' + c + '11'; 131 | }, '') + '111111111111111'; 132 | 133 | let samplePosition = 0; 134 | let theta = 0.0; 135 | 136 | for (let char = 0; char < digits.length; char++) { 137 | let digit = digits[char]; 138 | let frequency = digit === '1' ? this.markFreq : this.spaceFreq; 139 | let omega = frequency * 2 * Math.PI / 44100; 140 | 141 | let j = 0; 142 | for (j = 0; j < samplesPerSymbol; j++) { 143 | data[samplePosition + j] = Math.sin(theta); 144 | theta += omega; 145 | } 146 | 147 | samplePosition += j; 148 | } 149 | 150 | return buffer; 151 | } 152 | 153 | writeUTFBytes(view, offset, string) { 154 | var lng = string.length; 155 | for (var i = 0; i < lng; i++){ 156 | view.setUint8(offset + i, string.charCodeAt(i)); 157 | } 158 | } 159 | 160 | writeWav(buffer) { 161 | var sampleRate = 44100; 162 | var output = new ArrayBuffer(144 + buffer.length * 2); 163 | var view = new DataView(output); 164 | var data = buffer.getChannelData(0); 165 | 166 | this.writeUTFBytes(view, 0, 'RIFF'); 167 | view.setUint32(4, 44 + buffer.length, true); 168 | this.writeUTFBytes(view, 8, 'WAVE'); 169 | // FMT sub-chunk 170 | this.writeUTFBytes(view, 12, 'fmt '); 171 | view.setUint32(16, 16, true); // Length of format chunk 172 | view.setUint16(20, 1, true); // PCM format 173 | 174 | view.setUint16(22, 1, true); // Mono (1 channel) 175 | view.setUint32(24, sampleRate, true); // Sample rate 176 | view.setUint32(28, sampleRate * 2, true); // (Sample Rate * BitsPerSample * Channels) / 8; Bytes per second 177 | 178 | // (BitsPerSample * Channels) / 8. 1 - 8 bit mono; 2 - 8 bit stereo/16 bit mono; 4 - 16 bit stereo 179 | view.setUint16(32, 2, true); // Bytes per sample: 180 | 181 | view.setUint16(34, 16, true); // Bits per sample 182 | // data sub-chunk 183 | this.writeUTFBytes(view, 36, 'data'); 184 | view.setUint32(40, buffer.length, true); // Size of data section 185 | 186 | // write the PCM samples 187 | var lng = buffer.length; 188 | var index = 44; 189 | var volume = 1; 190 | for (var i = 0; i < lng; i++){ 191 | view.setInt16(index, data[i] * 0x7FFF, true); 192 | index += 2; 193 | } 194 | 195 | // our final binary blob that we can hand off 196 | var blob = new Blob( [ view ], { type : 'audio/wav' } ); 197 | var url = (window.URL || window.webkitURL).createObjectURL(blob); 198 | var link = window.document.createElement('a'); 199 | link.href = url; 200 | link.download = 'output.wav'; 201 | link.click(); 202 | } 203 | } 204 | --------------------------------------------------------------------------------