├── README.md ├── decoder.js ├── encoder.js ├── example.js ├── index.js ├── package.json └── test └── decode.js /README.md: -------------------------------------------------------------------------------- 1 | # binary-fsk 2 | 3 | > streaming encoder/decoder for binary frequency-shift keying 4 | 5 | https://en.wikipedia.org/wiki/Frequency-shift_keying 6 | 7 | ## STATUS 8 | 9 | Work-in-progress. Works great if you pipe the encoder to the decoder, but still 10 | working on getting it working well in noisy / real-world environments! 11 | 12 | 13 | ## Usage 14 | 15 | Let's encode the string 'Hello!' into sound waves and then back again to a 16 | string: 17 | 18 | ```js 19 | var bfsk = require('binary-fsk') 20 | var speaker = require('audio-speaker') 21 | var microphone = require('mic-stream') 22 | 23 | var opts = { 24 | mark: 884, 25 | space: 324, 26 | baud: 5, 27 | sampleRate: 8000, 28 | samplesPerFrame: 320 29 | } 30 | 31 | var encode = fsk.createEncodeStream(opts) 32 | var decode = fsk.createDecodeStream(opts) 33 | 34 | // pipe to your speaker 35 | encode 36 | .pipe(speaker()) 37 | 38 | // receive sound from your speaker and decode it back to text to print out 39 | microphone() 40 | .pipe(decode) 41 | .pipe(process.stdout) 42 | 43 | // write a message! 44 | e.end('heya!') 45 | ``` 46 | 47 | This will make your computer scream garbage at you (make sure your speakers and 48 | microphone are on!) briefly, but it should then output 49 | 50 | ``` 51 | hello warld! 52 | ``` 53 | 54 | ## API 55 | 56 | ```js 57 | var bfsk = require('binary-fsk') 58 | ``` 59 | 60 | ### bfsk.createEncodeStream(opts) 61 | 62 | Returns a Transform stream. Pipe text or other interesting data into this, and 63 | it will output audio data. You can pipe this to your speakers. 64 | 65 | Valid `opts` include: 66 | 67 | - `mark` (required) - the "mark" frequench, in hertz. This frequency is used to 68 | signal a '1' bit. 69 | - `space` (required) - the "space" frequench, in hertz. This frequency is used 70 | to signal a '0' bit. 71 | - `baud` (required) - the number of bits to transmit/expect, per second. 72 | - `sampleRate` (optional) - the number of samples per second. Defaults to 8000. 73 | 74 | 75 | ### bfsk.createDecodeStream(opts) 76 | 77 | Returns a Transform stream. Pipe audio data into this, and it will output the 78 | original data that was passed to the encoder. 79 | 80 | This takes the same `opts` object as the encoder. For proper results, use the 81 | same options as your encoder. 82 | 83 | 84 | ## Install 85 | 86 | With [npm](https://npmjs.org/) installed, run 87 | 88 | ``` 89 | $ npm install binary-fsk 90 | ``` 91 | 92 | ## See Also 93 | 94 | - [goertzel](https://github.com/noffle/goertzel) 95 | - [mic-stream](https://github.com/noffle/mic-stream) 96 | - [audio-speaker](https://github.com/audio-lab/audio-speaker) 97 | 98 | ## License 99 | 100 | ISC 101 | 102 | -------------------------------------------------------------------------------- /decoder.js: -------------------------------------------------------------------------------- 1 | var util = require('util') 2 | var goertzel = require('goertzel') 3 | var Transform = require('stream').Transform 4 | 5 | util.inherits(Decoder, Transform) 6 | 7 | function Decoder (opts) { 8 | if (!(this instanceof Decoder)) return new Decoder(opts) 9 | 10 | opts = opts || {} 11 | 12 | if (!opts.baud) throw new Error('must specify opts.baud') 13 | if (!opts.space) throw new Error('must specify opts.space') 14 | if (!opts.mark) throw new Error('must specify opts.mark') 15 | opts.sampleRate = opts.sampleRate || 8000 16 | opts.samplesPerFrame = opts.samplesPerFrame || getMinSamplesPerFrame(opts.sampleRate, opts.baud) 17 | 18 | function getMinSamplesPerFrame (sampleRate, baud) { 19 | return Math.floor(sampleRate / baud / 5) 20 | } 21 | 22 | Transform.call(this, { objectMode: true }) 23 | 24 | var hasSpace = goertzel({ 25 | targetFrequency: opts.space, 26 | sampleRate: opts.sampleRate, 27 | samplesPerFrame: opts.samplesPerFrame, 28 | threshold: 0.5 29 | }) 30 | 31 | var hasMark = goertzel({ 32 | targetFrequency: opts.mark, 33 | sampleRate: opts.sampleRate, 34 | samplesPerFrame: opts.samplesPerFrame, 35 | threshold: 0.5 36 | }) 37 | 38 | var symbolDuration = 1 / opts.baud 39 | var frameDuration = opts.samplesPerFrame / opts.sampleRate 40 | var state = 'preamble:space' 41 | var clock = 0 42 | var totalTime = 0 43 | var marksSeen = 0 44 | var spacesSeen = 0 45 | 46 | var bytePos = 0 47 | var byteAccum = 0 48 | 49 | this._transform = function (chunk, enc, cb) { 50 | this.handleFrame(chunk) 51 | cb(null) 52 | } 53 | 54 | this._flush = function (done) { 55 | decideOnSymbol() 56 | done() 57 | } 58 | 59 | this.handleFrame = function (frame) { 60 | var s = hasSpace(frame) 61 | var m = hasMark(frame) 62 | 63 | var bit 64 | if (s && !m) bit = 0 65 | else if (!s && m) bit = 1 66 | // else console.error('no match: space', s, ' mark', m) 67 | 68 | // console.error('bit', bit, ' clock', clock) 69 | 70 | if (state === 'preamble:space') { 71 | if (bit === 1) { 72 | // console.error('preamble:space done @', totalTime) 73 | // console.error('starting mark clock') 74 | clock = 0 75 | state = 'preamble:mark' 76 | } 77 | } 78 | 79 | else if (state === 'preamble:mark') { 80 | // if (bit !== 1) { 81 | // throw new Error('got non-mark while in preamble:mark') 82 | // } 83 | if (clock >= symbolDuration) { 84 | // console.error('preamble:mark done @', totalTime) 85 | // console.error('starting decode') 86 | clock = 0 87 | state = 'decode' 88 | return 89 | } 90 | } 91 | 92 | else if (state === 'decode') { 93 | if (bit === 0) spacesSeen++ 94 | else marksSeen++ 95 | 96 | if (clock >= symbolDuration) { 97 | decideOnSymbol() 98 | } 99 | } 100 | 101 | clock += frameDuration 102 | totalTime += frameDuration 103 | } 104 | 105 | decideOnSymbol = function () { 106 | // console.error('saw ', spacesSeen, 'spaces and', marksSeen, 'marks') 107 | var bit 108 | var error 109 | 110 | if (marksSeen > spacesSeen) { 111 | error = spacesSeen 112 | bit = 1 113 | } else { 114 | error = marksSeen 115 | bit = 0 116 | } 117 | spacesSeen = marksSeen = 0 118 | // console.error('SYMBOL:', bit, ' (err =', error + ')') 119 | 120 | // apply bit to the high end of the byte accumulator 121 | byteAccum >>= 1 122 | byteAccum |= (bit << 7) 123 | bytePos++ 124 | 125 | // emit byte if finished 126 | if (bytePos === 8) { 127 | var buf = new Buffer(1) 128 | buf[0] = byteAccum 129 | 130 | this.push(buf) 131 | 132 | byteAccum = 0 133 | bytePos = 0 134 | } else if (bytePos > 8) { 135 | throw new Error('somehow accumulated more than 8 bits!') 136 | } 137 | 138 | // push clock ahead a frame, since we've already trodden into the next 139 | // symbol 140 | clock = frameDuration * error 141 | }.bind(this) 142 | } 143 | 144 | module.exports = Decoder 145 | -------------------------------------------------------------------------------- /encoder.js: -------------------------------------------------------------------------------- 1 | var util = require('util') 2 | var Transform = require('stream').Transform 3 | 4 | util.inherits(Encoder, Transform) 5 | 6 | function Encoder (opts) { 7 | if (!(this instanceof Encoder)) return new Encoder(opts) 8 | 9 | opts = opts || {} 10 | 11 | if (!opts.baud) throw new Error('must specify opts.baud') 12 | if (!opts.space) throw new Error('must specify opts.space') 13 | if (!opts.mark) throw new Error('must specify opts.mark') 14 | opts.sampleRate = opts.sampleRate || 8000 15 | opts.samplesPerFrame = opts.samplesPerFrame || getMinSamplesPerFrame(opts.sampleRate, opts.baud) 16 | 17 | function getMinSamplesPerFrame (sampleRate, baud) { 18 | return Math.floor(sampleRate / baud / 5) 19 | } 20 | 21 | Transform.call(this, { objectMode: true }) 22 | 23 | var symbolDuration = 1 / opts.baud 24 | var frameDuration = opts.samplesPerFrame / opts.sampleRate 25 | var state = 'preamble:space' 26 | var clock = 0 27 | var totalTime = 0 28 | 29 | function sin (hz, t) { 30 | return Math.sin(Math.PI * 2 * t * hz) 31 | } 32 | 33 | function sinSamples (hz, samples, data) { 34 | for (var i = 0; i < samples; i++) { 35 | var v = sin(hz, i / opts.sampleRate) 36 | data.push(v) 37 | } 38 | } 39 | 40 | function writeByte (b, data) { 41 | for (var i=0; i < 8; i++) { 42 | var bit = b & 0x1 43 | b >>= 1 44 | sinSamples(bit === 0 ? opts.space : opts.mark, 45 | opts.sampleRate / opts.baud, data) 46 | } 47 | } 48 | 49 | function writePreamble (opts, data) { 50 | // preamble space (>= 1 baud (1 sampleRate)) 51 | sinSamples(opts.space, opts.sampleRate / opts.baud, data) 52 | 53 | // begin space symbol (== 1 baud (1 sampleRate)) 54 | sinSamples(opts.mark, opts.sampleRate / opts.baud, data) 55 | } 56 | 57 | // internal buffer 58 | var data = [] 59 | var frame = new Float32Array(opts.samplesPerFrame) 60 | 61 | var firstWrite = true 62 | this._transform = function (chunk, enc, next) { 63 | if (typeof(chunk) === 'string') { 64 | chunk = new Buffer(chunk) 65 | } 66 | 67 | if (firstWrite) { 68 | writePreamble(opts, data) 69 | firstWrite = false 70 | } 71 | 72 | for (var i=0; i < chunk.length; i++) { 73 | writeByte(chunk.readUInt8(i), data) 74 | } 75 | 76 | // Split into chunks 77 | var frames = Math.floor(data.length / opts.samplesPerFrame) 78 | // var start = new Date().getTime() 79 | for (var i=0; i < frames; i++) { 80 | var idx = i * opts.samplesPerFrame 81 | var frame = data.slice(idx, idx + opts.samplesPerFrame) 82 | this.push(frame) 83 | } 84 | // var end = new Date().getTime() 85 | // console.log('wrote frames in', (end-start), 'ms') 86 | 87 | next() 88 | } 89 | 90 | this._flush = function (done) { 91 | this.push(data) 92 | done() 93 | } 94 | } 95 | 96 | module.exports = Encoder 97 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var fsk = require('./index') 2 | 3 | var opts = { 4 | mark: 884, 5 | space: 324, 6 | baud: 20, 7 | sampleRate: 8000, 8 | samplesPerFrame: 100 9 | } 10 | 11 | var e = fsk.createEncodeStream(opts) 12 | var d = fsk.createDecodeStream(opts) 13 | 14 | e 15 | .pipe(d) 16 | .pipe(process.stdout) 17 | 18 | e.end('woah, pretty neat!') 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | createDecodeStream: require('./decoder'), 3 | createEncodeStream: require('./encoder'), 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "binary-fsk", 3 | "description": "decoder for binary frequency-shift keying", 4 | "version": "0.0.0", 5 | "repository": { 6 | "url": "git://github.com/noffle/binary-fsk.git" 7 | }, 8 | "main": "index.js", 9 | "scripts": { 10 | "test": "tape test/*.js" 11 | }, 12 | "dependencies": { 13 | "readable-stream": "^2.1.2" 14 | }, 15 | "devDependencies": { 16 | "concat-stream": "^1.5.1", 17 | "tape": "*" 18 | }, 19 | "license": "ISC" 20 | } 21 | -------------------------------------------------------------------------------- /test/decode.js: -------------------------------------------------------------------------------- 1 | var fsk = require('../index') 2 | var test = require('tape') 3 | var concat = require('concat-stream') 4 | 5 | function sin (hz, t) { 6 | return Math.sin(Math.PI * 2 * t * hz) 7 | } 8 | 9 | function sinSamples (hz, samples, sampleRate, data) { 10 | for (var i = 0; i < samples; i++) { 11 | var v = sin(hz, i / sampleRate) 12 | data.push(v) 13 | } 14 | } 15 | 16 | function writeEncodedMessage (msg, opts, data) { 17 | msg.forEach(function (ch) { 18 | ch = ch.charCodeAt(0) 19 | for (var i=0; i < 8; i++) { 20 | var bit = ch & 0x1 21 | ch >>= 1 22 | sinSamples(bit === 0 ? opts.space : opts.mark, 23 | opts.sampleRate / opts.baud, opts.sampleRate, data) 24 | } 25 | }) 26 | } 27 | 28 | function writePreamble (opts, data) { 29 | // preamble space (>= 1 baud (1 sampleRate)) 30 | sinSamples(opts.space, opts.sampleRate / opts.baud, opts.sampleRate, data) 31 | 32 | // begin space symbol (== 1 baud (1 sampleRate)) 33 | sinSamples(opts.mark, opts.sampleRate / opts.baud, opts.sampleRate, data) 34 | } 35 | 36 | test('basic', function (t) { 37 | var opts = { 38 | sampleRate: 8000, 39 | samplesPerFrame: 100, 40 | space: 324, 41 | mark: 884, 42 | baud: 1 43 | } 44 | 45 | var encoder = fsk.createEncodeStream(opts) 46 | var decoder = fsk.createDecodeStream(opts) 47 | 48 | encoder.pipe(decoder) 49 | 50 | encoder.end('hello world!') 51 | 52 | decoder.pipe(concat(function (data) { 53 | t.equal(data.toString(), 'hello world!') 54 | t.end() 55 | })) 56 | }) 57 | 58 | test('high baud', function (t) { 59 | var opts = { 60 | space: 324, 61 | mark: 884, 62 | baud: 100, 63 | sampleRate: 8000 64 | } 65 | 66 | var e = fsk.createEncodeStream(opts) 67 | var d = fsk.createDecodeStream(opts) 68 | 69 | e 70 | .pipe(d) 71 | .pipe(concat(function (data) { 72 | t.equal(data.toString(), 'woah, pretty neat!') 73 | t.end() 74 | })) 75 | e.end('woah, pretty neat!') 76 | }) 77 | 78 | test('extra long preamble', function (t) { 79 | var opts = { 80 | sampleRate: 8000, 81 | samplesPerFrame: 100, 82 | space: 324, 83 | mark: 884, 84 | baud: 1 85 | } 86 | 87 | var msg = 'hello world!'.split('') 88 | var data = [] 89 | 90 | // make pre-amble longer 91 | sinSamples(opts.space, 30, opts.sampleRate, data) 92 | 93 | writePreamble(opts, data) 94 | 95 | writeEncodedMessage(msg, opts, data) 96 | 97 | var decoder = fsk.createDecodeStream({ 98 | mark: opts.mark, 99 | space: opts.space, 100 | baud: opts.baud, 101 | sampleRate: opts.sampleRate, 102 | samplesPerFrame: opts.samplesPerFrame 103 | }) 104 | 105 | decoder.pipe(concat(function (data) { 106 | t.equal(data.toString(), 'hello world!') 107 | t.end() 108 | })) 109 | 110 | var i = 0 111 | while (i < data.length) { 112 | var frame = data.slice(i, i + opts.samplesPerFrame) 113 | i += opts.samplesPerFrame 114 | decoder.write(frame) 115 | } 116 | decoder.end() 117 | }) 118 | 119 | test('high frequency', function (t) { 120 | var opts = { 121 | sampleRate: 11025, 122 | samplesPerFrame: 500, 123 | space: 324, 124 | mark: 884, 125 | baud: 1 126 | } 127 | 128 | var msg = 'hey thar'.split('') 129 | var data = [] 130 | 131 | writePreamble(opts, data) 132 | 133 | writeEncodedMessage(msg, opts, data) 134 | 135 | var decoder = fsk.createDecodeStream({ 136 | mark: opts.mark, 137 | space: opts.space, 138 | baud: opts.baud, 139 | sampleRate: opts.sampleRate, 140 | samplesPerFrame: opts.samplesPerFrame 141 | }) 142 | 143 | decoder.pipe(concat(function (data) { 144 | t.equal(data.toString(), 'hey thar') 145 | t.end() 146 | })) 147 | 148 | var i = 0 149 | while (i < data.length) { 150 | var frame = data.slice(i, i + opts.samplesPerFrame) 151 | i += opts.samplesPerFrame 152 | decoder.write(frame) 153 | } 154 | decoder.end() 155 | }) 156 | 157 | test('non-standard opts', function (t) { 158 | var opts = { 159 | sampleRate: 8000, 160 | samplesPerFrame: 200, 161 | space: 481, 162 | mark: 1390, 163 | baud: 10 164 | } 165 | 166 | var msg = 'hello world!'.split('') 167 | var data = [] 168 | 169 | writePreamble(opts, data) 170 | 171 | writeEncodedMessage(msg, opts, data) 172 | 173 | var decoder = fsk.createDecodeStream({ 174 | mark: opts.mark, 175 | space: opts.space, 176 | baud: opts.baud, 177 | sampleRate: opts.sampleRate, 178 | samplesPerFrame: opts.samplesPerFrame 179 | }) 180 | 181 | decoder.pipe(concat(function (data) { 182 | t.equal(data.toString(), 'hello world!') 183 | t.end() 184 | })) 185 | 186 | var i = 0 187 | while (i < data.length) { 188 | var frame = data.slice(i, i + opts.samplesPerFrame) 189 | i += opts.samplesPerFrame 190 | decoder.write(frame) 191 | } 192 | decoder.end() 193 | }) 194 | --------------------------------------------------------------------------------