├── .gitignore ├── extension ├── icon-128.png ├── icon-16.png ├── icon-48.png ├── help-iface.png ├── help-settings.png ├── help-freetuning.png ├── manifest.json ├── help.js ├── error.html ├── main.js ├── error.js ├── savedialog.html ├── savedialog.js ├── estimateppm.js ├── estimateppm.html ├── demodulator-am.js ├── demodulator-nbfm.js ├── demodulator-ssb.js ├── settings.html ├── settings.js ├── presetmanager.html ├── demodulator-wbfm.js ├── decode-worker.js ├── audio.js ├── wavsaver.js ├── auxwindows.js ├── frequencies.js ├── presets.js ├── interface.css ├── appconfig.js ├── rtl2832u.js ├── help.html ├── interface.html ├── presetmanager.js ├── r820t.js ├── rtlcom.js └── dsp.js ├── image-src ├── interface.png ├── interface-freetuning.png ├── stereo.svg ├── icon.svg ├── promo-440.svg ├── help-freetuning.svg └── help.svg ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | *.crx 3 | *.zip 4 | nacl-src/pnacl 5 | -------------------------------------------------------------------------------- /extension/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozlima/radioreceiver/master/extension/icon-128.png -------------------------------------------------------------------------------- /extension/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozlima/radioreceiver/master/extension/icon-16.png -------------------------------------------------------------------------------- /extension/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozlima/radioreceiver/master/extension/icon-48.png -------------------------------------------------------------------------------- /extension/help-iface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozlima/radioreceiver/master/extension/help-iface.png -------------------------------------------------------------------------------- /image-src/interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozlima/radioreceiver/master/image-src/interface.png -------------------------------------------------------------------------------- /extension/help-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozlima/radioreceiver/master/extension/help-settings.png -------------------------------------------------------------------------------- /extension/help-freetuning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozlima/radioreceiver/master/extension/help-freetuning.png -------------------------------------------------------------------------------- /image-src/interface-freetuning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozlima/radioreceiver/master/image-src/interface-freetuning.png -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Radio Receiver", 3 | "description": "Receive and listen to FM and AM radio broadcasts on your browser or ChromeBook using an RTL2832U-based USB digital TV tuner.", 4 | "version": "1.1.5", 5 | "icons": { 6 | "16": "icon-16.png", 7 | "48": "icon-48.png", 8 | "128": "icon-128.png" 9 | }, 10 | "app": { 11 | "background": { 12 | "scripts": ["main.js"] 13 | } 14 | }, 15 | "permissions": [ 16 | "storage", 17 | "usb", 18 | {"fileSystem": ["write"]} 19 | ], 20 | "optional_permissions": [ 21 | { 22 | "usbDevices": [ 23 | { 24 | "vendorId": 3034, 25 | "productId": 10290 26 | }, 27 | { 28 | "vendorId": 3034, 29 | "productId": 10296 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /extension/help.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | function exit() { 16 | AuxWindows.closeCurrent(); 17 | } 18 | 19 | exitButton.addEventListener('click', exit); 20 | 21 | exitButton.focus(); 22 | document.body.firstElementChild.scrollIntoView(); 23 | 24 | -------------------------------------------------------------------------------- /extension/error.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | Error 19 | 20 | 21 | 22 |

There was a problem:

23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /extension/main.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | chrome.app.runtime.onLaunched.addListener(function() { 16 | chrome.app.window.create('interface.html', { 17 | 'id': 'radioTuner', 18 | 'bounds': { 19 | 'width': 500, 20 | 'height': 225 21 | }, 22 | 'resizable': false, 23 | 'frame': 'none' 24 | }); 25 | }); 26 | 27 | -------------------------------------------------------------------------------- /extension/error.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | errorText.textContent = window['errorMsg']; 16 | 17 | function exit() { 18 | var msg = { 19 | 'type': 'exit' 20 | }; 21 | window['opener'].postMessage(msg, '*'); 22 | AuxWindows.closeAll(); 23 | } 24 | 25 | exitButton.addEventListener('click', exit); 26 | 27 | AuxWindows.resizeCurrentTo(500, 0); 28 | -------------------------------------------------------------------------------- /extension/savedialog.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | Name this station 19 | 20 | 21 | 22 | 23 |


24 |

25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /extension/savedialog.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | var station = window['station']; 16 | 17 | frequencyLabel.textContent = station['display']; 18 | if (station['name']) { 19 | stationName.value = station['name']; 20 | } 21 | 22 | function save() { 23 | station['name'] = stationName.value; 24 | var msg = { 25 | 'type': 'savepreset', 26 | 'data': station 27 | }; 28 | window['opener'].postMessage(msg, '*'); 29 | exit(); 30 | } 31 | 32 | function exit() { 33 | AuxWindows.closeCurrent(); 34 | } 35 | 36 | cancel.addEventListener('click', exit); 37 | ok.addEventListener('click', save); 38 | 39 | AuxWindows.resizeCurrentTo(300, 0); 40 | -------------------------------------------------------------------------------- /extension/estimateppm.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | var opener = window['opener']; 16 | var mainWindow = window['mainWindow']; 17 | 18 | function exit() { 19 | AuxWindows.closeCurrent(); 20 | } 21 | 22 | function startEstimating() { 23 | mainWindow.radio.estimatePpm(true); 24 | showPpmEstimate(); 25 | } 26 | 27 | function showPpmEstimate() { 28 | ppm.value = mainWindow.window['radio'].getPpmEstimate(); 29 | if (mainWindow.window['radio'].isEstimatingPpm()) { 30 | setTimeout(showPpmEstimate, 200); 31 | } 32 | } 33 | 34 | closeButton.addEventListener('click', exit); 35 | estimate.addEventListener('click', startEstimating); 36 | 37 | AuxWindows.resizeCurrentTo(350, 0); 38 | 39 | -------------------------------------------------------------------------------- /extension/estimateppm.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | Frequency correction factor estimation 19 | 20 | 21 | 22 |

Please turn the radio on, tune it to the loudest and clearest FM station, 23 | and press the "Estimate PPM" button.

24 | 25 |

Suggested frequency correction factor: PPM.

26 |

After you set the new correction factor and turn the radio off and on again, 27 | you may need to repeat this operation a couple of times to refine the estimate.

28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /extension/demodulator-am.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * @fileoverview A demodulator for amplitude modulated signals. 17 | */ 18 | 19 | /** 20 | * A class to implement an AM demodulator. 21 | * @param {number} inRate The sample rate of the input samples. 22 | * @param {number} outRate The sample rate of the output audio. 23 | * @param {number} bandwidth The bandwidth of the input signal. 24 | * @constructor 25 | */ 26 | function Demodulator_AM(inRate, outRate, bandwidth) { 27 | var INTER_RATE = 48000; 28 | var filterF = bandwidth / 2; 29 | 30 | var demodulator = new AMDemodulator(inRate, INTER_RATE, filterF, 351); 31 | var filterCoefs = getLowPassFIRCoeffs(INTER_RATE, 10000, 41); 32 | var downSampler = new Downsampler(INTER_RATE, outRate, filterCoefs); 33 | 34 | /** 35 | * Demodulates the signal. 36 | * @param {Float32Array} samplesI The I components of the samples. 37 | * @param {Float32Array} samplesQ The Q components of the samples. 38 | * @return {{left:ArrayBuffer,right:ArrayBuffer,stereo:boolean,carrier:boolean}} 39 | * The demodulated audio signal. 40 | */ 41 | function demodulate(samplesI, samplesQ) { 42 | var demodulated = demodulator.demodulateTuned(samplesI, samplesQ); 43 | var audio = downSampler.downsample(demodulated); 44 | return {left: audio.buffer, 45 | right: new Float32Array(audio).buffer, 46 | stereo: false, 47 | signalLevel: Math.pow(demodulator.getRelSignalPower(), 0.17)}; 48 | } 49 | 50 | return { 51 | demodulate: demodulate 52 | }; 53 | } 54 | 55 | -------------------------------------------------------------------------------- /extension/demodulator-nbfm.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * @fileoverview A demodulator for narrowband FM signals. 17 | */ 18 | 19 | /** 20 | * A class to implement a Narrowband FM demodulator. 21 | * @param {number} inRate The sample rate of the input samples. 22 | * @param {number} outRate The sample rate of the output audio. 23 | * @param {number} maxF The frequency shift for maximum amplitude. 24 | * @constructor 25 | */ 26 | function Demodulator_NBFM(inRate, outRate, maxF) { 27 | var multiple = 1 + Math.floor((maxF - 1) * 7 / 75000); 28 | var interRate = 48000 * multiple; 29 | var filterF = maxF * 0.8; 30 | 31 | var demodulator = new FMDemodulator(inRate, interRate, maxF, filterF, Math.floor(50 * 7 / multiple)); 32 | var filterCoefs = getLowPassFIRCoeffs(interRate, 8000, 41); 33 | var downSampler = new Downsampler(interRate, outRate, filterCoefs); 34 | 35 | /** 36 | * Demodulates the signal. 37 | * @param {Float32Array} samplesI The I components of the samples. 38 | * @param {Float32Array} samplesQ The Q components of the samples. 39 | * @return {{left:ArrayBuffer,right:ArrayBuffer,stereo:boolean,carrier:boolean}} 40 | * The demodulated audio signal. 41 | */ 42 | function demodulate(samplesI, samplesQ) { 43 | var demodulated = demodulator.demodulateTuned(samplesI, samplesQ); 44 | var audio = downSampler.downsample(demodulated); 45 | return {left: audio.buffer, 46 | right: new Float32Array(audio).buffer, 47 | stereo: false, 48 | signalLevel: demodulator.getRelSignalPower()}; 49 | } 50 | 51 | return { 52 | demodulate: demodulate 53 | }; 54 | } 55 | 56 | -------------------------------------------------------------------------------- /extension/demodulator-ssb.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * @fileoverview A demodulator for single-sideband modulated signals. 17 | */ 18 | 19 | /** 20 | * A class to implement a SSB demodulator. 21 | * @param {number} inRate The sample rate of the input samples. 22 | * @param {number} outRate The sample rate of the output audio. 23 | * @param {number} bandwidth The bandwidth of the input signal. 24 | * @param {boolean} upper Whether to demodulate the upper sideband 25 | * (lower otherwise). 26 | * @constructor 27 | */ 28 | function Demodulator_SSB(inRate, outRate, bandwidth, upper) { 29 | var INTER_RATE = 48000; 30 | 31 | var demodulator = new SSBDemodulator(inRate, INTER_RATE, bandwidth, upper, 151); 32 | var filterCoefs = getLowPassFIRCoeffs(INTER_RATE, 10000, 41); 33 | var downSampler = new Downsampler(INTER_RATE, outRate, filterCoefs); 34 | 35 | /** 36 | * Demodulates the signal. 37 | * @param {Float32Array} samplesI The I components of the samples. 38 | * @param {Float32Array} samplesQ The Q components of the samples. 39 | * @return {{left:ArrayBuffer,right:ArrayBuffer,stereo:boolean,carrier:boolean}} 40 | * The demodulated audio signal. 41 | */ 42 | function demodulate(samplesI, samplesQ) { 43 | var demodulated = demodulator.demodulateTuned(samplesI, samplesQ); 44 | var audio = downSampler.downsample(demodulated); 45 | return {left: audio.buffer, 46 | right: new Float32Array(audio).buffer, 47 | stereo: false, 48 | signalLevel: Math.pow(demodulator.getRelSignalPower(), 0.17) }; 49 | } 50 | 51 | return { 52 | demodulate: demodulate 53 | }; 54 | } 55 | 56 | -------------------------------------------------------------------------------- /extension/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | Radio Receiver Settings 19 | 20 | 28 | 29 | 30 |

37 |

38 |

PPM. Suggest a value.

39 |

/ dB. 40 |

41 |

/ Hz.

42 |

43 |

You may need to turn the radio off and on before those settings take effect.

44 |

Manage your presets.

45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /extension/settings.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | var settings = window['settings']; 16 | 17 | if (settings && settings['region']) { 18 | for (var i = 0; i < region.options.length; ++i) { 19 | if (region.options[i].value == settings['region']) { 20 | region.selectedIndex = i; 21 | } 22 | } 23 | } 24 | ppm.value = (settings && settings['ppm']) || 0; 25 | 26 | autoGain.checked = settings && settings['autoGain']; 27 | gain.value = (settings && settings['gain']) || 0; 28 | gain.disabled = autoGain.checked; 29 | useUpconverter.checked = settings && settings['useUpconverter']; 30 | upconverterFreq.value = (settings && settings['upconverterFreq']) || 125000000; 31 | upconverterFreqInput.className = useUpconverter.checked ? '' : 'invisible'; 32 | upconverterFreq.disabled = !useUpconverter.checked; 33 | enableFreeTuning.checked = settings && settings['enableFreeTuning']; 34 | 35 | function save() { 36 | var msg = { 37 | 'type': 'setsettings', 38 | 'data': { 39 | 'region': region.options[region.selectedIndex].value || 'WW', 40 | 'ppm': ppm.value || 0, 41 | 'autoGain': autoGain.checked, 42 | 'gain': gain.value, 43 | 'useUpconverter': useUpconverter.checked, 44 | 'upconverterFreq': upconverterFreq.value, 45 | 'enableFreeTuning': enableFreeTuning.checked 46 | } 47 | }; 48 | window['opener'].postMessage(msg, '*'); 49 | exit(); 50 | } 51 | 52 | function exit() { 53 | AuxWindows.closeCurrent(); 54 | } 55 | 56 | cancel.addEventListener('click', exit); 57 | ok.addEventListener('click', save); 58 | estimatePpmLink.addEventListener('click', function() { 59 | AuxWindows.estimatePpm(opener); 60 | }); 61 | autoGain.addEventListener('change', function() { 62 | gain.disabled = autoGain.checked; 63 | }); 64 | useUpconverter.addEventListener('change', function() { 65 | upconverterFreq.disabled = !useUpconverter.checked; 66 | upconverterFreqInput.className = useUpconverter.checked ? '' : 'invisible'; 67 | }); 68 | managePresetsLink.addEventListener('click', function() { 69 | AuxWindows.managePresets(opener); 70 | }); 71 | 72 | AuxWindows.resizeCurrentTo(350, 0); 73 | 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Radio Receiver 2 | 3 | An application to listen to broadcast FM and AM radio from your Chrome browser or your ChromeBook computer using a $15 USB digital TV tuner. 4 | 5 | ![Radio Receiver screenshot](image-src/interface.png) 6 | 7 | ## What is this 8 | 9 | Radio Receiver is a Chrome application that uses an USB digital TV receiver to capture radio signals, does FM and AM demodulation in the browser, and plays them through your computer's speakers or headphones. This is called SDR (Software-Defined Radio), because all the radio signal processing is done by software running in the computer. 10 | 11 | Radio Receiver is 100% written in JavaScript, but is nevertheless fast enough that it can run on a 2012 Samsung ChromeBook laptop at full quality. 12 | 13 | ## Features 14 | 15 | * Stereo FM. 16 | * Scan for stations. 17 | * Record what you hear on the radio. 18 | * Built-in bands: 19 | * International and Japanese FM bands. 20 | * Weather band (US and Canada). 21 | * Medium Wave AM (requires an upconverter). 22 | * Free-tuning mode to use the program as a multi-band radio and listen to anything: short wave, air band, marine band, etc. 23 | * Supported modes: Wideband FM, Narrowband FM, AM, SSB. 24 | 25 | ## Compatible hardware and software 26 | 27 | Radio Receiver was written to work with an RTL-2832U-based DVB-T (European digital TV) USB receiver, with a R820T tuner chip. You can easily buy one for $15 or less by searching for [RTL2832U R820T] on your favorite online store or web search engine. 28 | 29 | You can use this application on a ChromeBook, or on any other computer running a Chrome browser. Just [install it using the Chrome Web Store](https://chrome.google.com/webstore/detail/radio-receiver/miieomcelenidlleokajkghmifldohpo) or any other mechanism, plug in your USB dongle, and click on the icon to start the Radio Receiver application. 30 | 31 | To listen to Medium Wave and Short Wave radio, you need an upconverter connected between your antenna and the USB dongle. This upconverter shifts the signals up in frequency so that they can be tuned by your dongle. You can find upconverters for sale by searching for [SDR upconverter] on your favorite online store or web search engine. 32 | 33 | ## Support 34 | 35 | If you'd like to talk about Radio Receiver, or have any bug reports or suggestions, please post a message in [the radioreceiver Google Group](https://groups.google.com/forum/#!forum/radioreceiver). 36 | 37 | Note: This is not an official Google product (experimental or otherwise), it is just code that happens to be owned by Google. 38 | 39 | ## Acknowledgements 40 | 41 | Kudos and thanks to the [RTL-SDR project](http://sdr.osmocom.org/trac/wiki/rtl-sdr) for figuring out the magic numbers needed to drive the USB tuner. 42 | 43 | If you want to experiment further with Software-Defined Radio and listen to more things using your $15 tuner, you can try [the various programs listed on rtl-sdr.com](http://www.rtl-sdr.com/big-list-rtl-sdr-supported-software/). 44 | -------------------------------------------------------------------------------- /extension/presetmanager.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | Preset Manager 19 | 20 | 21 | 22 | 47 | 48 | 49 | 50 | 52 | 53 |
54 |

Available presets

55 |
56 | 57 | 58 |
FrequencyNameMode
59 |
60 | 61 |

62 | 63 | 64 |

65 |
66 | 67 | 102 | 103 |
104 |

105 | 106 |

107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /extension/demodulator-wbfm.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * @fileoverview A demodulator for wideband FM signals. 17 | */ 18 | 19 | /** 20 | * A class to implement a Wideband FM demodulator. 21 | * @param {number} inRate The sample rate of the input samples. 22 | * @param {number} outRate The sample rate of the output audio. 23 | * @constructor 24 | */ 25 | function Demodulator_WBFM(inRate, outRate) { 26 | var INTER_RATE = 336000; 27 | var MAX_F = 75000; 28 | var FILTER = MAX_F * 0.8; 29 | var PILOT_FREQ = 19000; 30 | var DEEMPH_TC = 50; 31 | 32 | var demodulator = new FMDemodulator(inRate, INTER_RATE, MAX_F, FILTER, 51); 33 | var filterCoefs = getLowPassFIRCoeffs(INTER_RATE, 10000, 41); 34 | var monoSampler = new Downsampler(INTER_RATE, outRate, filterCoefs); 35 | var stereoSampler = new Downsampler(INTER_RATE, outRate, filterCoefs); 36 | var stereoSeparator = new StereoSeparator(INTER_RATE, PILOT_FREQ); 37 | var leftDeemph = new Deemphasizer(outRate, DEEMPH_TC); 38 | var rightDeemph = new Deemphasizer(outRate, DEEMPH_TC); 39 | 40 | /** 41 | * Demodulates the signal. 42 | * @param {Float32Array} samplesI The I components of the samples. 43 | * @param {Float32Array} samplesQ The Q components of the samples. 44 | * @param {boolean} inStereo Whether to try decoding the stereo signal. 45 | * @return {{left:ArrayBuffer,right:ArrayBuffer,stereo:boolean,carrier:boolean}} 46 | * The demodulated audio signal. 47 | */ 48 | function demodulate(samplesI, samplesQ, inStereo) { 49 | var demodulated = demodulator.demodulateTuned(samplesI, samplesQ); 50 | var leftAudio = monoSampler.downsample(demodulated); 51 | var rightAudio = new Float32Array(leftAudio); 52 | var stereoOut = false; 53 | 54 | if (inStereo) { 55 | var stereo = stereoSeparator.separate(demodulated); 56 | if (stereo.found) { 57 | stereoOut = true; 58 | var diffAudio = stereoSampler.downsample(stereo.diff); 59 | for (var i = 0; i < diffAudio.length; ++i) { 60 | rightAudio[i] -= diffAudio[i]; 61 | leftAudio[i] += diffAudio[i]; 62 | } 63 | } 64 | } 65 | 66 | leftDeemph.inPlace(leftAudio); 67 | rightDeemph.inPlace(rightAudio); 68 | return {left: leftAudio.buffer, 69 | right: rightAudio.buffer, 70 | stereo: stereoOut, 71 | signalLevel: demodulator.getRelSignalPower() }; 72 | } 73 | 74 | return { 75 | demodulate: demodulate 76 | }; 77 | } 78 | 79 | -------------------------------------------------------------------------------- /extension/decode-worker.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * @fileoverview A worker that receives samples captured by the tuner, 17 | * demodulates them, extracts the audio signals, and sends them back. 18 | */ 19 | 20 | importScripts('dsp.js'); 21 | importScripts('demodulator-am.js'); 22 | importScripts('demodulator-ssb.js'); 23 | importScripts('demodulator-nbfm.js'); 24 | importScripts('demodulator-wbfm.js'); 25 | 26 | var IN_RATE = 1024000; 27 | var OUT_RATE = 48000; 28 | 29 | /** 30 | * A class to implement a worker that demodulates an FM broadcast station. 31 | * @constructor 32 | */ 33 | function Decoder() { 34 | var demodulator = new Demodulator_WBFM(IN_RATE, OUT_RATE); 35 | var cosine = 1; 36 | var sine = 0; 37 | 38 | /** 39 | * Demodulates the tuner's output, producing mono or stereo sound, and 40 | * sends the demodulated audio back to the caller. 41 | * @param {ArrayBuffer} buffer A buffer containing the tuner's output. 42 | * @param {boolean} inStereo Whether to try decoding the stereo signal. 43 | * @param {number} freqOffset The frequency to shift the samples by. 44 | * @param {Object=} opt_data Additional data to echo back to the caller. 45 | */ 46 | function process(buffer, inStereo, freqOffset, opt_data) { 47 | var data = opt_data || {}; 48 | var IQ = iqSamplesFromUint8(buffer, IN_RATE); 49 | IQ = shiftFrequency(IQ, freqOffset, IN_RATE, cosine, sine); 50 | cosine = IQ[2]; 51 | sine = IQ[3]; 52 | var out = demodulator.demodulate(IQ[0], IQ[1], inStereo); 53 | data['stereo'] = out['stereo']; 54 | data['signalLevel'] = out['signalLevel']; 55 | postMessage([out.left, out.right, data], [out.left, out.right]); 56 | } 57 | 58 | /** 59 | * Changes the modulation scheme. 60 | * @param {Object} mode The new mode. 61 | */ 62 | function setMode(mode) { 63 | switch (mode.modulation) { 64 | case 'AM': 65 | demodulator = new Demodulator_AM(IN_RATE, OUT_RATE, mode.bandwidth); 66 | break; 67 | case 'USB': 68 | demodulator = new Demodulator_SSB(IN_RATE, OUT_RATE, mode.bandwidth, true); 69 | break; 70 | case 'LSB': 71 | demodulator = new Demodulator_SSB(IN_RATE, OUT_RATE, mode.bandwidth, false); 72 | break; 73 | case 'NBFM': 74 | demodulator = new Demodulator_NBFM(IN_RATE, OUT_RATE, mode.maxF); 75 | break; 76 | default: 77 | demodulator = new Demodulator_WBFM(IN_RATE, OUT_RATE); 78 | break; 79 | } 80 | } 81 | 82 | return { 83 | process: process, 84 | setMode: setMode 85 | }; 86 | } 87 | 88 | var decoder = new Decoder(); 89 | 90 | onmessage = function(event) { 91 | switch (event.data[0]) { 92 | case 1: 93 | decoder.setMode(event.data[1]); 94 | break; 95 | default: 96 | decoder.process(event.data[1], event.data[2], event.data[3], event.data[4]); 97 | break; 98 | } 99 | }; 100 | 101 | -------------------------------------------------------------------------------- /extension/audio.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * A class to play a series of sample buffers at a constant rate. 17 | * @constructor 18 | */ 19 | function Player() { 20 | var OUT_RATE = 48000; 21 | var TIME_BUFFER = 0.05; 22 | var SQUELCH_TAIL = 0.3; 23 | 24 | var lastPlayedAt = -1; 25 | var squelchTime = -2; 26 | var frameno = 0; 27 | 28 | var wavSaver = null; 29 | 30 | var ac = new (window.AudioContext || window.webkitAudioContext)(); 31 | var gainNode = ac.createGain ? ac.createGain() : ac.createGainNode(); 32 | gainNode.connect(ac.destination); 33 | 34 | /** 35 | * Queues the given samples for playing at the appropriate time. 36 | * @param {Float32Array} leftSamples The samples for the left speaker. 37 | * @param {Float32Array} rightSamples The samples for the right speaker. 38 | * @param {number} level The radio signal's level. 39 | * @param {number} squelch The current squelch level. 40 | */ 41 | function play(leftSamples, rightSamples, level, squelch) { 42 | var buffer = ac.createBuffer(2, leftSamples.length, OUT_RATE); 43 | if (level >= squelch) { 44 | squelchTime = null; 45 | } else if (squelchTime === null) { 46 | squelchTime = lastPlayedAt; 47 | } 48 | if (squelchTime === null || lastPlayedAt - squelchTime < SQUELCH_TAIL) { 49 | buffer.getChannelData(0).set(leftSamples); 50 | buffer.getChannelData(1).set(rightSamples); 51 | if (wavSaver != null) { 52 | wavSaver.writeSamples(leftSamples, rightSamples); 53 | } 54 | } 55 | var source = ac.createBufferSource(); 56 | source.buffer = buffer; 57 | source.connect(gainNode); 58 | lastPlayedAt = Math.max( 59 | lastPlayedAt + leftSamples.length / OUT_RATE, 60 | ac.currentTime + TIME_BUFFER); 61 | source.start(lastPlayedAt); 62 | } 63 | 64 | /** 65 | * Starts recording a WAV file into the given entry. 66 | * @param {FileEntry} entry A file entry for the new WAV file. 67 | */ 68 | function startWriting(writer) { 69 | if (wavSaver) { 70 | wavSaver.finish(); 71 | } 72 | wavSaver = new WavSaver(writer); 73 | } 74 | 75 | /** 76 | * Stops recording a WAV file. 77 | */ 78 | function stopWriting() { 79 | if (wavSaver) { 80 | wavSaver.finish(); 81 | wavSaver = null; 82 | } 83 | } 84 | 85 | /** 86 | * Tells whether we're recording a WAV file. 87 | * @return {boolean} Whether a WAV file is being recorded. 88 | */ 89 | function isWriting() { 90 | if (wavSaver && wavSaver.hasFinished()) { 91 | wavSaver = null; 92 | } 93 | return wavSaver != null; 94 | } 95 | 96 | /** 97 | * Sets the volume for playing samples. 98 | * @param {number} volume The volume to set, between 0 and 1. 99 | */ 100 | function setVolume(volume) { 101 | gainNode.gain.value = volume; 102 | } 103 | 104 | return { 105 | play: play, 106 | setVolume: setVolume, 107 | startWriting: startWriting, 108 | stopWriting: stopWriting, 109 | isWriting: isWriting 110 | }; 111 | } 112 | 113 | -------------------------------------------------------------------------------- /image-src/stereo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 38 | 45 | 46 | 48 | 49 | 51 | image/svg+xml 52 | 54 | 55 | 56 | 57 | 61 | 66 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /extension/wavsaver.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * A class to save a WAV file (48k, 16-bit, stereo). 17 | * 18 | * The FileSystem API doesn't buffer writes, so this class implements that 19 | * buffer. The processor writes out the contents of the queue until it's 20 | * empty, and then polls it once a second for new items to write. 21 | * @param {FileEntry} fileEntry An entry for the WAV file. 22 | * @constructor 23 | */ 24 | function WavSaver(fileEntry) { 25 | 26 | var fileWriter; 27 | var queue = []; 28 | var writing = true; 29 | 30 | writeHeader(); 31 | fileEntry.createWriter(function(writer) { 32 | fileWriter = writer; 33 | writer.onwriteend = processQueue; 34 | writer.onerror = processError; 35 | processQueue(); 36 | }); 37 | 38 | /** 39 | * Writes the contents of the queue and schedules the next execution of 40 | * this function, if the queue is empty. After finish() was called and 41 | * the queue goes empty, fixes up the chunk sizes in the headers and 42 | * stops rescheduling. 43 | */ 44 | function processQueue() { 45 | if (queue == null) { 46 | return; 47 | } 48 | if (queue.length == 0) { 49 | if (writing) { 50 | setTimeout(processQueue, 1000); 51 | } else { 52 | fileWriter.seek(0); 53 | fileWriter.write(new Blob([createHeader(fileWriter.length).buffer])); 54 | } 55 | return; 56 | } 57 | var blob = new Blob(queue); 58 | queue = []; 59 | fileWriter.write(blob); 60 | } 61 | 62 | /** 63 | * Empties the queue and stops writing. 64 | */ 65 | function processError() { 66 | writing = false; 67 | queue = null; 68 | } 69 | 70 | /** 71 | * Puts the contents of the given array in the queue. 72 | */ 73 | function writeArray(arr) { 74 | if (writing) { 75 | queue.push(arr.buffer); 76 | } 77 | } 78 | 79 | /** 80 | * Creates a WAV header's data. 81 | * @param {number} size The total file size. 82 | */ 83 | function createHeader(size) { 84 | return new Int32Array([ 85 | 0x46464952, // "RIFF" 86 | size - 8, // chunk size 87 | 0x45564157, // "WAVE" 88 | 0x20746d66, // "fmt " 89 | 0x10, // chunk size 90 | 0x00020001, // PCM, 2 channels 91 | 48000, // sample rate 92 | 192000, // data rate 93 | 0x00100004, // 4 bytes/block, 16 bits/sample 94 | 0x61746164, // "data" 95 | size - 44 // chunk size (0 for now) 96 | ]); 97 | } 98 | 99 | /** 100 | * Puts the WAV headers in the queue. 101 | */ 102 | function writeHeader() { 103 | writeArray(createHeader(44)); 104 | } 105 | 106 | /** 107 | * Writes a block of samples. 108 | * @param {Float32Array} leftSamples The samples for the left speaker. 109 | * @param {Float32Array} rightSamples The samples for the right speaker. 110 | */ 111 | function writeSamples(leftSamples, rightSamples) { 112 | var out = new Int16Array(leftSamples.length * 2); 113 | for (var i = 0; i < leftSamples.length; ++i) { 114 | out[i * 2] = 115 | Math.floor(Math.max(-1, Math.min(1, leftSamples[i])) * 32767); 116 | out[i * 2 + 1] = 117 | Math.floor(Math.max(-1, Math.min(1, rightSamples[i])) * 32767); 118 | } 119 | writeArray(out); 120 | } 121 | 122 | /** 123 | * Finishes writing to the WAV file. 124 | */ 125 | function finish() { 126 | writing = false; 127 | } 128 | 129 | /** 130 | * Tells whether the class has finished writing to the WAV file. 131 | */ 132 | function hasFinished() { 133 | return !writing; 134 | } 135 | 136 | return { 137 | writeSamples: writeSamples, 138 | finish: finish, 139 | hasFinished: hasFinished 140 | }; 141 | } 142 | 143 | -------------------------------------------------------------------------------- /image-src/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 41 | 48 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | 58 | 59 | 60 | 61 | 66 | 73 | 78 | 83 | 88 | 93 | 103 | 113 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /extension/auxwindows.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * @fileoverview Functions for managing auxiliary windows. 17 | */ 18 | var AuxWindows = (function() { 19 | 20 | /** 21 | * Shows a window to save a preset. 22 | * @param {number} frequency The current frequency. 23 | * @param {number} display The frequency's display name. 24 | * @param {number} name The current name for the station. 25 | * @param {string} band The name of the station's band. 26 | * @param {string} mode The station's mode. 27 | */ 28 | function savePreset(frequency, name, band) { 29 | chrome.app.window.create('savedialog.html', { 30 | 'bounds': { 31 | 'width': 300, 32 | 'height': 1 33 | }, 34 | 'resizable': false 35 | }, function(win) { 36 | win.contentWindow['opener'] = window; 37 | var modeData = copyObject(band.getMode()); 38 | modeData['step'] = band.getStep(); 39 | var stationData = { 40 | 'frequency': frequency, 41 | 'display': band.toDisplayName(frequency, true), 42 | 'band': band.getName(), 43 | 'mode': modeData, 44 | 'name': name 45 | }; 46 | win.contentWindow['station'] = stationData; 47 | }); 48 | } 49 | 50 | /** 51 | * Makes a copy of an object as a map. 52 | */ 53 | function copyObject(obj) { 54 | var dest = {}; 55 | for (var key in obj) { 56 | if (obj.hasOwnProperty(key)) { 57 | dest[key] = obj[key]; 58 | } 59 | } 60 | return dest; 61 | } 62 | 63 | /** 64 | * Shows a window to change the application settings. 65 | * @param {Object} settings The current settings. 66 | */ 67 | function settings(settings) { 68 | chrome.app.window.create('settings.html', { 69 | 'bounds': { 70 | 'width': 350, 71 | 'height': 1 72 | }, 73 | 'resizable': false 74 | }, function(win) { 75 | win.contentWindow['opener'] = window; 76 | win.contentWindow['settings'] = settings; 77 | }); 78 | } 79 | 80 | /** 81 | * Shows a window for the frequency correction factor estimator. 82 | * @param {AppWindow} mainWindow The app's main window. 83 | */ 84 | function estimatePpm(mainWindow) { 85 | chrome.app.window.create('estimateppm.html', { 86 | 'bounds': { 87 | 'width': 350, 88 | 'height': 1 89 | }, 90 | 'resizable': false 91 | }, function(win) { 92 | win.contentWindow['opener'] = window; 93 | win.contentWindow['mainWindow'] = mainWindow; 94 | }); 95 | } 96 | 97 | /** 98 | * Shows a window to manage the presets. 99 | * @param {AppWindow} mainWindow The app's main window. 100 | */ 101 | function managePresets(mainWindow) { 102 | chrome.app.window.create('presetmanager.html', { 103 | 'bounds': { 104 | 'width': 700, 105 | 'height': 1 106 | }, 107 | 'resizable': false 108 | }, function(win) { 109 | win.contentWindow['opener'] = window; 110 | win.contentWindow['mainWindow'] = mainWindow; 111 | }); 112 | } 113 | 114 | /** 115 | * Shows an error window. 116 | * @param {string} msg The error message to show. 117 | */ 118 | function error(msg) { 119 | chrome.app.window.create('error.html', { 120 | 'bounds': { 121 | 'width': 500, 122 | 'height': 1 123 | }, 124 | 'resizable': false 125 | }, function(win) { 126 | win.contentWindow['opener'] = window; 127 | win.contentWindow['errorMsg'] = msg; 128 | }); 129 | } 130 | 131 | /** 132 | * Shows the help window. 133 | * @param {string} anchor An optional anchor to jump to. 134 | */ 135 | function help(anchor) { 136 | chrome.app.window.create('help.html' + (anchor ? '#' + anchor : ''), { 137 | 'bounds': { 138 | 'width': 700, 139 | 'height': 600 140 | }, 141 | 'state': 'maximized', 142 | 'resizable': true 143 | }); 144 | } 145 | 146 | /** 147 | * Resizes the current window to the given dimensions, compensating for zoom. 148 | * @param {number} width The desired width. 149 | * @param {number} height The desired height. 0 to set it automagically. 150 | */ 151 | function resizeCurrentTo(width, height) { 152 | // If the user has set a custom zoom level, resize the window to fit 153 | var bounds = chrome.app.window.current().innerBounds; 154 | var zoom = (bounds.width / window.innerWidth) || 1; 155 | bounds.width = Math.round(width * zoom); 156 | if (height) { 157 | bounds.height = Math.round(height * zoom); 158 | } else { 159 | bounds.height = Math.round(document.body.scrollHeight * zoom); 160 | } 161 | } 162 | 163 | /** 164 | * Closes the current window. 165 | */ 166 | function closeCurrent() { 167 | chrome.app.window.current().close(); 168 | } 169 | 170 | /** 171 | * Closes all windows. 172 | */ 173 | function closeAll() { 174 | var current = chrome.app.window.current(); 175 | var all = chrome.app.window.getAll(); 176 | for (var i = 0; i < all.length; ++i) { 177 | if (all[i] !== current) { 178 | all[i].close(); 179 | } 180 | } 181 | current.close(); 182 | } 183 | 184 | return { 185 | savePreset: savePreset, 186 | settings: settings, 187 | estimatePpm: estimatePpm, 188 | managePresets: managePresets, 189 | error: error, 190 | help: help, 191 | resizeCurrentTo: resizeCurrentTo, 192 | closeCurrent: closeCurrent, 193 | closeAll: closeAll 194 | }; 195 | 196 | })(); 197 | -------------------------------------------------------------------------------- /extension/frequencies.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * @fileoverview Functions and objects to manipulate single frequencies and 17 | * bands of frequencies. 18 | */ 19 | 20 | /** 21 | * Functions to convert frequencies to human-readable strings. 22 | */ 23 | var Frequencies = (function() { 24 | 25 | /** 26 | * Converts a frequency to a human-readable format. 27 | * @param {number} frequency The frequency to convert. 28 | * @param {boolean} showUnits Whether to show the units (Hz, kHz, MHz, etc.) 29 | * @param {number=} opt_digits If specified, use a fixed number of digits. 30 | * @return {string} The converted frequency. 31 | */ 32 | function humanReadable(frequency, showUnits, opt_digits) { 33 | var units; 34 | var suffix; 35 | if (frequency < 2e3) { 36 | units = 1; 37 | suffix = ''; 38 | } else if (frequency < 2e6) { 39 | units = 1e3; 40 | suffix = 'k'; 41 | } else if (frequency < 2e9) { 42 | units = 1e6; 43 | suffix = 'M'; 44 | } else { 45 | units = 1e9; 46 | suffix = 'G'; 47 | } 48 | if (opt_digits) { 49 | var number = (frequency / units).toFixed(opt_digits); 50 | } else { 51 | var number = String(frequency / units); 52 | } 53 | if (showUnits) { 54 | return number + ' ' + suffix + 'Hz'; 55 | } else { 56 | return number; 57 | } 58 | } 59 | 60 | /** 61 | * Converts a frequency in a human-readable format to a number. 62 | * @param {string} frequency The frequency to convert. 63 | * @return {number} The converted frequency. 64 | */ 65 | function parseReadableInput(frequency) { 66 | var mul = 1; 67 | frequency = frequency.toLowerCase().trim(); 68 | if (frequency.substr(-2) == "hz") { 69 | frequency = frequency.substr(0, frequency.length - 2).trim(); 70 | } 71 | var suffix = frequency.substr(-1); 72 | if (suffix == "k") { 73 | mul = 1e3; 74 | } else if (suffix == "m") { 75 | mul = 1e6; 76 | } else if (suffix == "g") { 77 | mul = 1e9; 78 | } 79 | if (mul != 1) { 80 | frequency = frequency.substr(0, frequency.length - 1).trim(); 81 | } 82 | return Math.floor(mul * Number(frequency)); 83 | } 84 | 85 | return { 86 | humanReadable: humanReadable, 87 | parseReadableInput: parseReadableInput 88 | }; 89 | 90 | })(); 91 | 92 | /** 93 | * Default modes. 94 | */ 95 | var DefaultModes = { 96 | 'AM': { 97 | modulation: 'AM', 98 | bandwidth: 10000 99 | }, 100 | 'LSB': { 101 | modulation: 'LSB', 102 | bandwidth: 2900 103 | }, 104 | 'USB': { 105 | modulation: 'USB', 106 | bandwidth: 2900 107 | }, 108 | 'NBFM': { 109 | modulation: 'NBFM', 110 | maxF: 10000 111 | }, 112 | 'WBFM': { 113 | modulation: 'WBFM' 114 | } 115 | }; 116 | 117 | /** 118 | * Known frequency bands. 119 | */ 120 | var Bands = (function() { 121 | var WBFM = {modulation: 'WBFM'}; 122 | 123 | function fmDisplay(freq, opt_full) { 124 | return Frequencies.humanReadable(freq, false, 2) + (opt_full ? ' FM' : ''); 125 | } 126 | 127 | function fmInput(input) { 128 | return input * 1e6; 129 | } 130 | 131 | var WXFM = { 132 | modulation: 'NBFM', 133 | maxF: 10000 134 | }; 135 | 136 | var WX_NAMES = [2, 4, 5, 3, 6, 7, 1]; 137 | var WX_INDEX = [6, 0, 3, 1, 2, 4, 5]; 138 | 139 | function wxDisplay(freq, opt_full) { 140 | return (opt_full ? 'WX ' : '') + WX_NAMES[Math.floor((freq - 162400000) / 25000)]; 141 | } 142 | 143 | function wxInput(input) { 144 | return Math.floor(WX_INDEX[input - 1] * 25000) + 162400000; 145 | } 146 | 147 | var AM = { 148 | modulation: 'AM', 149 | bandwidth: 10000, 150 | upconvert: true 151 | }; 152 | 153 | function amDisplay(freq, opt_full) { 154 | return Frequencies.humanReadable(freq, false, 0) + (opt_full ? ' AM' : ''); 155 | } 156 | 157 | function amInput(input) { 158 | return input * 1e3; 159 | } 160 | 161 | return { 162 | 'WW': { 163 | 'FM': new Band('FM', 87500000, 108000000, 100000, WBFM, fmDisplay, fmInput), 164 | 'AM': new Band('AM', 531000, 1611000, 9000, AM, amDisplay, amInput) 165 | }, 166 | 'NA': { 167 | 'FM': new Band('FM', 87500000, 108000000, 100000, WBFM, fmDisplay, fmInput), 168 | 'WX': new Band('WX', 162400000, 162550000, 25000, WXFM, wxDisplay, wxInput), 169 | 'AM': new Band('AM', 540000, 1710000, 10000, AM, amDisplay, amInput) 170 | }, 171 | 'AM': { 172 | 'FM': new Band('FM', 87500000, 108000000, 100000, WBFM, fmDisplay, fmInput), 173 | 'AM': new Band('AM', 540000, 1710000, 10000, AM, amDisplay, amInput) 174 | }, 175 | 'JP': { 176 | 'FM': new Band('FM', 76000000, 95000000, 100000, WBFM, fmDisplay, fmInput), 177 | 'AM': new Band('AM', 531000, 1611000, 9000, AM, amDisplay, amInput) 178 | }, 179 | 'IT': { 180 | 'FM': new Band('FM', 87500000, 108000000, 50000, WBFM, fmDisplay, fmInput), 181 | 'AM': new Band('AM', 531000, 1611000, 9000, AM, amDisplay, amInput) 182 | } 183 | }; 184 | })(); 185 | 186 | /** 187 | * A particular frequency band. 188 | * @param {string} bandName The band's name 189 | * @param {number} minF The minimum frequency in the band. 190 | * @param {number} maxF The maximum frequency in the band. 191 | * @param {number} stepF The step between channels in the band. 192 | * @param {Object} mode The band's modulation parameters. 193 | * @param {(function(number, boolean=):string)=} opt_displayFn A function that 194 | * takes a frequency and returns its presentation for display. 195 | * @param {(function(string):number)=} opt_inputFn A function that takes a 196 | * display representation and returns the corresponding frequency. 197 | * @constructor 198 | */ 199 | function Band(bandName, minF, maxF, stepF, mode, opt_displayFn, opt_inputFn) { 200 | var name = bandName; 201 | var min = minF; 202 | var max = maxF; 203 | var step = stepF; 204 | var mode = mode; 205 | 206 | function freeDisplayFn(freq, opt_full) { 207 | return opt_full ? Frequencies.humanReadable(freq, true) : freq; 208 | } 209 | 210 | function freeInputFn(input) { 211 | return Frequencies.parseReadableInput(input); 212 | } 213 | 214 | return { 215 | getName: function() { return name; }, 216 | getMin: function() { return min; }, 217 | getMax: function() { return max; }, 218 | getStep: function() { return step; }, 219 | getMode: function() { return mode; }, 220 | toDisplayName: opt_displayFn || freeDisplayFn, 221 | fromDisplayName: opt_inputFn || freeInputFn 222 | }; 223 | } 224 | 225 | -------------------------------------------------------------------------------- /image-src/promo-440.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 23 | 27 | 31 | 32 | 43 | 47 | 52 | 58 | 63 | 68 | 74 | 75 | 76 | 94 | 96 | 97 | 99 | image/svg+xml 100 | 102 | 103 | 104 | 105 | 106 | 111 | 118 | 121 | 123 | 129 | 134 | 139 | 144 | 149 | 159 | 169 | 170 | 175 | 176 | Radio Receiver 187 | 188 | 189 | -------------------------------------------------------------------------------- /extension/presets.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * Presets and saved stations. 17 | * @param {Object} opt_presets The optional initial set of presets. 18 | * @constructor 19 | */ 20 | function Presets(opt_presets) { 21 | 22 | /** 23 | * The station presets. Maps from frequency to a saved station's settings. 24 | */ 25 | var presets = opt_presets || {}; 26 | 27 | /** 28 | * The functions that listen for changes in the presets. 29 | */ 30 | var listeners = []; 31 | 32 | /** 33 | * Loads the presets from the cloud. 34 | * @param {function} callback A function to call after loading the presets. 35 | */ 36 | function load(callback) { 37 | chrome.storage.local.get('presets', function(cfg) { 38 | if (cfg['presets']) { 39 | importPresets(cfg['presets']); 40 | chrome.storage.onChanged.addListener(reload); 41 | callback && callback(); 42 | } else { 43 | chrome.storage.sync.get('presets', function(cfg) { 44 | var info = cfg['presets'] || {}; 45 | importPresets(info); 46 | save(function() { 47 | chrome.storage.onChanged.addListener(reload); 48 | callback && callback(); 49 | }); 50 | }); 51 | } 52 | }); 53 | } 54 | 55 | /** 56 | * Saves the presets in the cloud. 57 | * @param {function} callback A function to call after saving the presets. 58 | */ 59 | function save(callback) { 60 | chrome.storage.local.set(exportPresets(), callback); 61 | } 62 | 63 | /** 64 | * Reloads the presets when someone has modified them. 65 | * @param {Object} changes The changes made in the storage area. 66 | * @param {string} areaName The area the changes were made in. 67 | */ 68 | function reload(changes, areaName) { 69 | if (areaName != 'local') { 70 | return; 71 | } 72 | var presetChange = changes['presets']; 73 | if (!presetChange) { 74 | return; 75 | } 76 | importPresets(presetChange['newValue']); 77 | for (var i = 0; i < listeners.length; ++i) { 78 | listeners[i](); 79 | } 80 | } 81 | 82 | /** 83 | * Adds a function to listen for changes in the presets. 84 | * @param {Function} fun The function to call when there's a change. 85 | */ 86 | function addListener(fun) { 87 | listeners.push(fun); 88 | } 89 | 90 | /** 91 | * Imports the presets from the given object. 92 | * @param {Object} obj The object to import the presets from. 93 | */ 94 | function importPresets(obj) { 95 | if (obj['version'] != 1) { 96 | var band = Bands['WW']['FM']; 97 | presets = {}; 98 | for (var key in obj) { 99 | var freq = String(key * 1e6); 100 | presets[freq] = { 101 | name: obj[key], 102 | display: band.toDisplayName(freq, true), 103 | band: band.getName(), 104 | mode: band.getMode() 105 | }; 106 | } 107 | } else { 108 | presets = obj['stations']; 109 | } 110 | } 111 | 112 | /** 113 | * Exports the presets to the given object. 114 | * @return {Object} The exported presets. 115 | */ 116 | function exportPresets(obj) { 117 | return { 118 | presets: { 119 | version: 1, 120 | stations: presets 121 | } 122 | }; 123 | } 124 | 125 | /** 126 | * Gets a preset, or a list of all presets. 127 | * @param {number=} opt_frequency The frequency to get, or undefined to 128 | * get all presets. 129 | * @return {Object} The presets. 130 | */ 131 | function get(opt_frequency) { 132 | if (opt_frequency != null) { 133 | return presets[opt_frequency]; 134 | } else { 135 | return presets; 136 | } 137 | } 138 | 139 | /** 140 | * Sets the value of a preset. 141 | * @param {number} frequency The preset's frequency. 142 | * @param {string} display The preset frequency's display name. 143 | * @param {string} name The station's name. 144 | * @param {string} band The name of the band. 145 | * @param {Object} mode A description of the modulation scheme. 146 | */ 147 | function set(frequency, display, name, band, mode) { 148 | presets[frequency] = { 149 | name: name, 150 | display: display, 151 | band: band, 152 | mode: mode 153 | }; 154 | } 155 | 156 | /** 157 | * Removes a preset. 158 | * @param {number} frequency The preset's frequency. 159 | */ 160 | function remove(frequency) { 161 | delete presets[frequency]; 162 | } 163 | 164 | /** 165 | * Calculates the difference between the current presets and a different 166 | * set of presets. 167 | * @param {Presets} other The presets object to get the difference. 168 | * @return {add:Presets,del:Presets,changeAdd:Presets,changeDel:Presets} 169 | * An object containing four sets of presets: 'add' with the presets that 170 | * appear in "other" but not in the current set, 'del' with the presets 171 | * that appear in the current set but not in "other", and finally 172 | * 'changeAdd' and 'changeDel' with the presets that changed from one 173 | * set to the other. 174 | */ 175 | function diff(other) { 176 | var these = get(); 177 | var those = other.get(); 178 | var add = {}; 179 | var del = {}; 180 | var changeAdd = {}; 181 | var changeDel = {}; 182 | for (var freq in these) { 183 | var myPreset = these[freq]; 184 | var theirPreset = those[freq]; 185 | if (theirPreset) { 186 | if (!areEqual(myPreset, theirPreset)) { 187 | changeDel[freq] = myPreset; 188 | changeAdd[freq] = theirPreset; 189 | } 190 | } else { 191 | del[freq] = myPreset; 192 | } 193 | } 194 | for (var freq in those) { 195 | var myPreset = these[freq]; 196 | var theirPreset = those[freq]; 197 | if (!myPreset) { 198 | add[freq] = theirPreset; 199 | } 200 | } 201 | return { 202 | add: new Presets(add), 203 | del: new Presets(del), 204 | changeAdd: new Presets(changeAdd), 205 | changeDel: new Presets(changeDel) 206 | }; 207 | } 208 | 209 | /** 210 | * Checks if two objects have the same contents. 211 | */ 212 | function areEqual(a, b) { 213 | if (typeof a !== typeof b) { 214 | return false; 215 | } 216 | if ("string" === typeof a || "number" === typeof a 217 | || "undefined" === typeof a || a === null) { 218 | return a === b; 219 | } 220 | if ("function" === typeof a) { 221 | return false; 222 | } 223 | for (var k in a) { 224 | if (!areEqual(a[k], b[k])) { 225 | return false; 226 | } 227 | } 228 | for (var k in b) { 229 | if (!areEqual(a[k], b[k])) { 230 | return false; 231 | } 232 | } 233 | return true; 234 | } 235 | 236 | /** 237 | * Bulk-adds and removes presets from the current set. 238 | * @param {Presets} del The presets to remove. 239 | * @param {Presets} add The presets to add. 240 | */ 241 | function change(del, add) { 242 | var these = get(); 243 | var toDelete = del.get(); 244 | var toAdd = add.get(); 245 | for (var freq in toDelete) { 246 | remove(freq); 247 | } 248 | for (var freq in toAdd) { 249 | var preset = toAdd[freq]; 250 | set(freq, preset.display, preset.name, preset.band, preset.mode); 251 | } 252 | } 253 | 254 | return { 255 | load: load, 256 | save: save, 257 | addListener: addListener, 258 | get: get, 259 | set: set, 260 | remove: remove, 261 | diff: diff, 262 | change: change, 263 | importPresets: importPresets, 264 | exportPresets: exportPresets 265 | }; 266 | } 267 | 268 | -------------------------------------------------------------------------------- /extension/interface.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 Google Inc. All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | body { 18 | background-color: #ddd; 19 | } 20 | 21 | option { 22 | color: black; 23 | } 24 | 25 | .invisible { 26 | visibility: hidden; 27 | } 28 | 29 | .invisibleButton { 30 | cursor: pointer; 31 | margin: 0; 32 | padding: 0; 33 | border: 0; 34 | background: transparent; 35 | color: inherit; 36 | font: inherit; 37 | align-items: inherit; 38 | display: inherit; 39 | box-sizing: inherit; 40 | -webkit-appearance: inherit; 41 | } 42 | 43 | .titleBar { 44 | position: absolute; 45 | top: 0; 46 | left: 0; 47 | right: 0; 48 | height: 25px; 49 | background-color: #aaa; 50 | border-bottom: 1px #ddd groove; 51 | -webkit-app-region: drag; 52 | } 53 | 54 | .titleText { 55 | position: absolute; 56 | bottom: 2px; 57 | left: 10px; 58 | right: 50px; 59 | font-family: sans-serif; 60 | font-size: 15px; 61 | font-weight: bold; 62 | } 63 | 64 | .closeButton, .settingsButton, .helpButton { 65 | position: absolute; 66 | top: 0; 67 | bottom: 0; 68 | border-left: 1px #ddd groove; 69 | text-align: center; 70 | font-size: 17px; 71 | font-weight: bold; 72 | -webkit-app-region: no-drag; 73 | } 74 | 75 | .settingsButton:focus, .helpButton:focus { 76 | outline: 0; 77 | } 78 | 79 | .closeButton:hover, .prefsButton:hover { 80 | background-color: #bbb; 81 | } 82 | 83 | .settingsButton { 84 | right: 80px; 85 | width: 30px; 86 | } 87 | 88 | .helpButton { 89 | right: 50px; 90 | width: 30px; 91 | } 92 | 93 | .closeButton { 94 | right: 0; 95 | width: 50px; 96 | } 97 | 98 | .closeIcon { 99 | margin-top: 9px; 100 | } 101 | 102 | .top { 103 | position: absolute; 104 | top: 30px; 105 | left: 5px; 106 | width: 490px; 107 | height: 135px; 108 | } 109 | 110 | .bottom { 111 | position: absolute; 112 | width: 490px; 113 | left: 5px; 114 | top: 165px; 115 | height: 55px; 116 | } 117 | 118 | .display { 119 | position: absolute; 120 | border: 1px inset; 121 | background-color: #343; 122 | color: #afd; 123 | left: 55px; 124 | top: 0; 125 | height: 125px; 126 | width: 380px; 127 | font-family: sans-serif; 128 | } 129 | 130 | .frequencyDisplay, .frequencyInput { 131 | position: absolute; 132 | left: 5px; 133 | width: 310px; 134 | font-size: 100px; 135 | text-align: right; 136 | } 137 | 138 | .frequencyDisplay { 139 | top: 5px; 140 | cursor: text; 141 | } 142 | 143 | .frequencyInput { 144 | top: 4px; 145 | background: transparent; 146 | border: none; 147 | font-family: sans-serif; 148 | color: #ffa; 149 | } 150 | 151 | .frequencyDisplay.freeTuning, .frequencyInput.freeTuning { 152 | font-size: 50px; 153 | } 154 | 155 | .volumeBox, .volumeOccluder { 156 | position: absolute; 157 | right: 5px; 158 | top: 50px; 159 | width: 50px; 160 | height: 19px; 161 | text-align: center; 162 | font-size: 16px; 163 | } 164 | 165 | .volumeOccluder { 166 | z-index: 1; 167 | } 168 | 169 | .volumeIcon { 170 | vertical-align: bottom; 171 | fill: #afd; 172 | } 173 | 174 | .volumeSliderBox { 175 | position: absolute; 176 | right: -50px; 177 | top: 20px; 178 | width: 160px; 179 | height: 30px; 180 | background-color: rgba(0, 0, 0, 0.85); 181 | box-shadow: 0px 0px 5px black; 182 | border-radius: 15px; 183 | z-index: 2; 184 | } 185 | 186 | .volumeSlider { 187 | position: absolute; 188 | top: 10px; 189 | left: 10px; 190 | height: 6px; 191 | width: 140px; 192 | -webkit-appearance: none; 193 | border-radius: 2px; 194 | background-color: #7fbfa5; 195 | } 196 | 197 | .volumeSlider:focus { 198 | outline: 0; 199 | } 200 | 201 | .volumeMuted { 202 | color: #c54; 203 | } 204 | 205 | .bandBox { 206 | position: absolute; 207 | right: 5px; 208 | top: 75px; 209 | width: 50px; 210 | text-align: center; 211 | font-size: 25px; 212 | } 213 | 214 | .bandBox.freeTuning { 215 | font-style: italic; 216 | } 217 | 218 | .scanning { 219 | -webkit-animation: blink 0.3s step-end infinite alternate; 220 | } 221 | 222 | @-webkit-keyframes blink { 223 | 0% { 224 | opacity: 1; 225 | } 226 | 70% { 227 | opacity: 0; 228 | } 229 | } 230 | 231 | .stereoIndicatorBox { 232 | position: absolute; 233 | right: 10px; 234 | top: 25px; 235 | width: 40px; 236 | height: 22px; 237 | fill: #afd; 238 | } 239 | 240 | .stereoIndicator { 241 | fill: #afd; 242 | } 243 | 244 | .stereoDisabled { 245 | fill: #275; 246 | } 247 | 248 | .stereoUnavailable { 249 | fill: transparent; 250 | } 251 | 252 | .freeTuningStuff { 253 | visibility: hidden; 254 | font-size: 16px; 255 | } 256 | 257 | .freeTuningStuff.freeTuning { 258 | visibility: visible; 259 | } 260 | 261 | .freeTuningBox { 262 | position: absolute; 263 | width: 165px; 264 | } 265 | 266 | .freeTuningLabel { 267 | position: absolute; 268 | left: 0; 269 | bottom: 0; 270 | width: 80px; 271 | text-align: right; 272 | color: #7a9; 273 | } 274 | 275 | .freeTuningDisplay, .freeTuningInput { 276 | position: absolute; 277 | right: 25px; 278 | bottom: 0; 279 | text-align: right; 280 | } 281 | 282 | .freeTuningDisplay { 283 | cursor: text; 284 | } 285 | 286 | .freeTuningInput { 287 | bottom: -1px; 288 | background: transparent; 289 | border: none; 290 | font-family: sans-serif; 291 | font-size: 16px; 292 | color: #ffa; 293 | } 294 | 295 | .freeTuningButton { 296 | position: absolute; 297 | right: 0; 298 | bottom: 0; 299 | width: 60px; 300 | text-align: right; 301 | } 302 | 303 | .freeTuningHz { 304 | position: absolute; 305 | right: 0; 306 | bottom: 0; 307 | width: 20px; 308 | color: #7a9; 309 | } 310 | 311 | .modulationBox { 312 | bottom: 50px; 313 | left: 10px; 314 | width: 140px; 315 | } 316 | 317 | .modulationDisplay { 318 | width: 60px; 319 | bottom: -1px; 320 | } 321 | 322 | .freqStepBox { 323 | bottom: 50px; 324 | right: 60px; 325 | } 326 | 327 | .freqStepDisplay, .freqStepInput { 328 | width: 55px; 329 | } 330 | 331 | .bandwidthBox { 332 | bottom: 30px; 333 | left: 10px; 334 | } 335 | 336 | .bandwidthDisplay, .bandwidthInput { 337 | width: 45px; 338 | } 339 | 340 | .maxfBox { 341 | bottom: 30px; 342 | left: 10px; 343 | } 344 | 345 | .maxfDisplay, .maxfInput { 346 | width: 45px; 347 | } 348 | 349 | .upconverterBox { 350 | bottom: 30px; 351 | right: 60px; 352 | width: 122px; 353 | } 354 | 355 | .upconverterDisplay { 356 | width: 25px; 357 | text-align: right; 358 | } 359 | 360 | .squelchBox { 361 | bottom: 10px; 362 | right: 35px; 363 | width: 135px; 364 | } 365 | 366 | .squelchDisplay, .squelchInput { 367 | width: 30px; 368 | } 369 | 370 | .freqMinusButton, .freqPlusButton { 371 | position: absolute; 372 | top: 0; 373 | width: 50px; 374 | height: 50px; 375 | } 376 | 377 | .scanDownButton, .scanUpButton { 378 | position: absolute; 379 | top: 55px; 380 | width: 50px; 381 | height: 70px; 382 | } 383 | 384 | .freqMinusButton, .scanDownButton { 385 | left: 0; 386 | } 387 | 388 | .freqPlusButton, .scanUpButton { 389 | right: 0; 390 | } 391 | 392 | .stereoButton { 393 | border: 2px solid black; 394 | } 395 | 396 | .powerButton { 397 | position: absolute; 398 | left: 0; 399 | top: 0; 400 | bottom: 0; 401 | width: 90px; 402 | } 403 | 404 | .recordingButton { 405 | position: absolute; 406 | left: 100px; 407 | top: 0; 408 | bottom: 0; 409 | width: 60px; 410 | } 411 | 412 | .presets { 413 | position: absolute; 414 | left: 170px; 415 | right: 0; 416 | top: 0; 417 | bottom: 0; 418 | } 419 | 420 | .presetsBox { 421 | position: absolute; 422 | left: 0px; 423 | top: 0; 424 | bottom: 0; 425 | width: 250px; 426 | -webkit-appearance: menulist-button; 427 | border-color: #a9a9a9; 428 | background: white; 429 | } 430 | 431 | .savePresetButton { 432 | position: absolute; 433 | right: 0; 434 | top: 0; 435 | bottom: 25px; 436 | width: 70px; 437 | } 438 | 439 | .removePresetButton { 440 | position: absolute; 441 | right: 0; 442 | top: 25px; 443 | bottom: 0; 444 | width: 70px; 445 | } 446 | 447 | -------------------------------------------------------------------------------- /extension/appconfig.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * Application configuration. 17 | * @constructor 18 | */ 19 | function AppConfig() { 20 | 21 | var config = { 22 | version: 1, 23 | /** Application settings. */ 24 | settings: { 25 | /** World region. */ 26 | region: 'WW', 27 | /** Frequency correction factor, parts per million. */ 28 | ppm: 0, 29 | /** Tuner gain. */ 30 | gain: { 31 | auto: true, 32 | value: 0 33 | }, 34 | /** Upconverter. */ 35 | upconverter: { 36 | enable: false, 37 | frequency: 125000000 38 | }, 39 | /** Whether free tuning is enabled. */ 40 | freeTuning: false 41 | }, 42 | /** Current state. */ 43 | state: { 44 | /** Audio volume, from 0 to 1. */ 45 | volume: 1, 46 | /** Currently active band name. */ 47 | bandName: 'FM', 48 | /** Map from band name to selected frequency. */ 49 | bandFrequencies: {}, 50 | /** Current mode in free tuning. */ 51 | modeName: 'WBFM', 52 | /** Map from mode name to settings. */ 53 | modeConfigs: {} 54 | } 55 | }; 56 | 57 | function getRegion() { 58 | return config.settings.region; 59 | } 60 | 61 | function getRegionBands() { 62 | return Bands[getRegion()] || Bands['WW']; 63 | } 64 | 65 | function selectRegion(region) { 66 | config.settings.region = region; 67 | } 68 | 69 | function getPpm() { 70 | return config.settings.ppm; 71 | } 72 | 73 | function setPpm(ppm) { 74 | config.settings.ppm = Number(ppm) || 0; 75 | } 76 | 77 | function isAutoGain() { 78 | return config.settings.gain.auto; 79 | } 80 | 81 | function setAutoGain(auto) { 82 | config.settings.gain.auto = !!auto; 83 | } 84 | 85 | function getGain() { 86 | return config.settings.gain.value; 87 | } 88 | 89 | function isUpconverterEnabled() { 90 | return config.settings.upconverter.enable; 91 | } 92 | 93 | function enableUpconverter(enabled) { 94 | config.settings.upconverter.enable = !!enabled; 95 | } 96 | 97 | function getUpconverterFrequency() { 98 | return config.settings.upconverter.frequency; 99 | } 100 | 101 | function setUpconverterFrequency(frequency) { 102 | config.settings.upconverter.frequency = Number(frequency) || 0; 103 | } 104 | 105 | function isFreeTuningEnabled() { 106 | return config.settings.freeTuning; 107 | } 108 | 109 | function enableFreeTuning(enabled) { 110 | config.settings.freeTuning = !!enabled; 111 | } 112 | 113 | function setGain(gain) { 114 | config.settings.gain.value = Math.max(0, gain) || 0; 115 | } 116 | 117 | function getBandName() { 118 | if (isAllowedBand(config.state.bandName)) { 119 | return config.state.bandName; 120 | } else { 121 | return 'FM'; 122 | } 123 | } 124 | 125 | function getCurrentBand() { 126 | var bandName = getBandName(); 127 | if (bandName) { 128 | return getRegionBands()[bandName] || getRegionBands()['FM']; 129 | } else { 130 | var modeConfig = getCurrentMode(); 131 | return Band('', 0, 9999999999, modeConfig.step, modeConfig.params); 132 | } 133 | } 134 | 135 | function isAllowedBand(bandName) { 136 | var bands = getRegionBands(); 137 | if (!bandName) { 138 | return isFreeTuningEnabled(); 139 | } 140 | if (!bands[bandName]) { 141 | return false; 142 | } 143 | return !bands[bandName].getMode().upconvert || isUpconverterEnabled(); 144 | } 145 | 146 | function selectBand(bandName) { 147 | config.state.bandName = bandName; 148 | } 149 | 150 | function setFrequency(frequency) { 151 | config.state.bandFrequencies[getBandName()] = (1 * frequency) || 0; 152 | } 153 | 154 | function getFrequency() { 155 | return config.state.bandFrequencies[getBandName()] || 0; 156 | } 157 | 158 | function getCurrentMode() { 159 | return config.state.modeConfigs[config.state.modeName] || { 160 | step: 10000, 161 | params: DefaultModes[config.state.modeName] 162 | }; 163 | } 164 | 165 | function selectMode(modeName) { 166 | config.state.modeName = modeName; 167 | } 168 | 169 | function updateCurrentMode(settings) { 170 | config.state.modeConfigs[config.state.modeName] = settings; 171 | } 172 | 173 | function getVolume() { 174 | return config.state.volume; 175 | } 176 | 177 | function setVolume(volume) { 178 | config.state.volume = Math.min(1, Math.max(0, volume)) || 0; 179 | } 180 | 181 | function load(callback) { 182 | chrome.storage.local.get('AppConfig', function(cfg) { 183 | if (cfg['AppConfig']) { 184 | var newCfg = cfg['AppConfig']; 185 | if (newCfg.version >= 1) { 186 | config.settings.region = newCfg.settings.region; 187 | config.settings.ppm = newCfg.settings.ppm; 188 | config.settings.gain.auto = newCfg.settings.gain.auto; 189 | config.settings.gain.value = newCfg.settings.gain.value; 190 | config.settings.upconverter.enable = 191 | newCfg.settings.upconverter.enable; 192 | config.settings.upconverter.frequency = 193 | newCfg.settings.upconverter.frequency; 194 | config.settings.freeTuning = newCfg.settings.freeTuning; 195 | config.state.volume = newCfg.state.volume; 196 | config.state.bandName = newCfg.state.bandName; 197 | config.state.bandFrequencies = newCfg.state.bandFrequencies; 198 | config.state.modeName = newCfg.state.modeName; 199 | config.state.modeConfigs = newCfg.state.modeConfigs; 200 | } 201 | callback(); 202 | } else { 203 | loadLegacy(callback); 204 | } 205 | }); 206 | } 207 | 208 | function save() { 209 | chrome.storage.local.set({'AppConfig': config}); 210 | } 211 | 212 | function loadLegacy(callback) { 213 | chrome.storage.local.get('currentStation', function(cfg) { 214 | var newCfg = cfg['currentStation']; 215 | if (newCfg) { 216 | if ('number' === typeof newCfg) { 217 | config.state.bandName = 'FM'; 218 | config.state.bandFrequencies = {'FM': newCfg}; 219 | } else { 220 | config.state.bandName = newCfg['currentBand']; 221 | config.state.bandFrequencies = newCfg['bands']; 222 | } 223 | chrome.storage.local.remove('currentStation'); 224 | } 225 | 226 | chrome.storage.local.get('volume', function(cfg) { 227 | var newCfg = cfg['volume']; 228 | if (newCfg != null) { 229 | config.state.volume = newCfg; 230 | chrome.storage.local.remove('volume'); 231 | } 232 | 233 | chrome.storage.local.get('settings', function(cfg) { 234 | var newCfg = cfg['settings']; 235 | if (newCfg) { 236 | config.settings.region = newCfg['region']; 237 | config.settings.ppm = newCfg['ppm']; 238 | config.settings.gain.auto = newCfg['autoGain']; 239 | config.settings.gain.value = newCfg['gain']; 240 | config.settings.upconverter.enable = newCfg['useUpconverter']; 241 | config.settings.upconverter.frequency = newCfg['upconverterFreq']; 242 | config.settings.freeTuning = newCfg['enableFreeTuning']; 243 | chrome.storage.local.remove('settings'); 244 | } 245 | save(); 246 | callback(); 247 | })})}); 248 | } 249 | 250 | return { 251 | load: load, 252 | save: save, 253 | settings: { 254 | region: { 255 | select: selectRegion, 256 | get: getRegion, 257 | getAvailableBands: getRegionBands 258 | }, 259 | ppm: { 260 | set: setPpm, 261 | get: getPpm 262 | }, 263 | gain: { 264 | setAuto: setAutoGain, 265 | isAuto: isAutoGain, 266 | set: setGain, 267 | get: getGain 268 | }, 269 | upconverter: { 270 | enable: enableUpconverter, 271 | isEnabled: isUpconverterEnabled, 272 | set: setUpconverterFrequency, 273 | get: getUpconverterFrequency 274 | }, 275 | freeTuning: { 276 | enable: enableFreeTuning, 277 | isEnabled: isFreeTuningEnabled 278 | } 279 | }, 280 | state: { 281 | band: { 282 | isAllowed: isAllowedBand, 283 | select: selectBand, 284 | get: getCurrentBand 285 | }, 286 | frequency: { 287 | set: setFrequency, 288 | get: getFrequency 289 | }, 290 | mode: { 291 | select: selectMode, 292 | update: updateCurrentMode, 293 | get: getCurrentMode 294 | }, 295 | volume: { 296 | set: setVolume, 297 | get: getVolume 298 | } 299 | } 300 | }; 301 | } 302 | 303 | -------------------------------------------------------------------------------- /extension/rtl2832u.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * Operations on the RTL2832U demodulator. 17 | * @param {ConnectionHandle} conn The USB connection handle. 18 | * @param {number} ppm The frequency correction factor, in parts per million. 19 | * @param {number=} opt_gain The optional gain in dB. If unspecified or null, sets auto gain. 20 | * @constructor 21 | */ 22 | function RTL2832U(conn, ppm, opt_gain) { 23 | 24 | /** 25 | * Frequency of the oscillator crystal. 26 | */ 27 | var XTAL_FREQ = 28800000; 28 | 29 | /** 30 | * Tuner intermediate frequency. 31 | */ 32 | var IF_FREQ = 3570000; 33 | 34 | /** 35 | * The number of bytes for each sample. 36 | */ 37 | var BYTES_PER_SAMPLE = 2; 38 | 39 | /** 40 | * Communications with the demodulator via USB. 41 | */ 42 | var com = new RtlCom(conn); 43 | 44 | /** 45 | * A handler for errors. It's a function that receives the error message. 46 | */ 47 | var errorHandler; 48 | 49 | /** 50 | * The tuner used by the dongle. 51 | */ 52 | var tuner; 53 | 54 | /** 55 | * Initialize the demodulator. 56 | * @param {Function} kont The continuation for this function. 57 | */ 58 | function open(kont) { 59 | com.writeEach([ 60 | [CMD.REG, BLOCK.USB, REG.SYSCTL, 0x09, 1], 61 | [CMD.REG, BLOCK.USB, REG.EPA_MAXPKT, 0x0200, 2], 62 | [CMD.REG, BLOCK.USB, REG.EPA_CTL, 0x0210, 2] 63 | ], function() { 64 | com.iface.claim(function() { 65 | com.writeEach([ 66 | [CMD.REG, BLOCK.SYS, REG.DEMOD_CTL_1, 0x22, 1], 67 | [CMD.REG, BLOCK.SYS, REG.DEMOD_CTL, 0xe8, 1], 68 | [CMD.DEMODREG, 1, 0x01, 0x14, 1], 69 | [CMD.DEMODREG, 1, 0x01, 0x10, 1], 70 | [CMD.DEMODREG, 1, 0x15, 0x00, 1], 71 | [CMD.DEMODREG, 1, 0x16, 0x0000, 2], 72 | [CMD.DEMODREG, 1, 0x16, 0x00, 1], 73 | [CMD.DEMODREG, 1, 0x17, 0x00, 1], 74 | [CMD.DEMODREG, 1, 0x18, 0x00, 1], 75 | [CMD.DEMODREG, 1, 0x19, 0x00, 1], 76 | [CMD.DEMODREG, 1, 0x1a, 0x00, 1], 77 | [CMD.DEMODREG, 1, 0x1b, 0x00, 1], 78 | [CMD.DEMODREG, 1, 0x1c, 0xca, 1], 79 | [CMD.DEMODREG, 1, 0x1d, 0xdc, 1], 80 | [CMD.DEMODREG, 1, 0x1e, 0xd7, 1], 81 | [CMD.DEMODREG, 1, 0x1f, 0xd8, 1], 82 | [CMD.DEMODREG, 1, 0x20, 0xe0, 1], 83 | [CMD.DEMODREG, 1, 0x21, 0xf2, 1], 84 | [CMD.DEMODREG, 1, 0x22, 0x0e, 1], 85 | [CMD.DEMODREG, 1, 0x23, 0x35, 1], 86 | [CMD.DEMODREG, 1, 0x24, 0x06, 1], 87 | [CMD.DEMODREG, 1, 0x25, 0x50, 1], 88 | [CMD.DEMODREG, 1, 0x26, 0x9c, 1], 89 | [CMD.DEMODREG, 1, 0x27, 0x0d, 1], 90 | [CMD.DEMODREG, 1, 0x28, 0x71, 1], 91 | [CMD.DEMODREG, 1, 0x29, 0x11, 1], 92 | [CMD.DEMODREG, 1, 0x2a, 0x14, 1], 93 | [CMD.DEMODREG, 1, 0x2b, 0x71, 1], 94 | [CMD.DEMODREG, 1, 0x2c, 0x74, 1], 95 | [CMD.DEMODREG, 1, 0x2d, 0x19, 1], 96 | [CMD.DEMODREG, 1, 0x2e, 0x41, 1], 97 | [CMD.DEMODREG, 1, 0x2f, 0xa5, 1], 98 | [CMD.DEMODREG, 0, 0x19, 0x05, 1], 99 | [CMD.DEMODREG, 1, 0x93, 0xf0, 1], 100 | [CMD.DEMODREG, 1, 0x94, 0x0f, 1], 101 | [CMD.DEMODREG, 1, 0x11, 0x00, 1], 102 | [CMD.DEMODREG, 1, 0x04, 0x00, 1], 103 | [CMD.DEMODREG, 0, 0x61, 0x60, 1], 104 | [CMD.DEMODREG, 0, 0x06, 0x80, 1], 105 | [CMD.DEMODREG, 1, 0xb1, 0x1b, 1], 106 | [CMD.DEMODREG, 0, 0x0d, 0x83, 1] 107 | ], function() { 108 | 109 | var xtalFreq = Math.floor(XTAL_FREQ * (1 + ppm / 1000000)); 110 | com.i2c.open(function() { 111 | R820T.check(com, function(found) { 112 | if (found) { 113 | tuner = new R820T(com, xtalFreq, throwError); 114 | } 115 | if (!tuner) { 116 | throwError('Sorry, your USB dongle has an unsupported tuner chip. ' + 117 | 'Only the R820T chip is supported.'); 118 | return; 119 | } 120 | var multiplier = -1 * Math.floor(IF_FREQ * (1<<22) / xtalFreq); 121 | com.writeEach([ 122 | [CMD.DEMODREG, 1, 0xb1, 0x1a, 1], 123 | [CMD.DEMODREG, 0, 0x08, 0x4d, 1], 124 | [CMD.DEMODREG, 1, 0x19, (multiplier >> 16) & 0x3f, 1], 125 | [CMD.DEMODREG, 1, 0x1a, (multiplier >> 8) & 0xff, 1], 126 | [CMD.DEMODREG, 1, 0x1b, multiplier & 0xff, 1], 127 | [CMD.DEMODREG, 1, 0x15, 0x01, 1] 128 | ], function() { 129 | tuner.init(function() { 130 | setGain(opt_gain, function() { 131 | com.i2c.close(kont); 132 | })})})})})})})}); 133 | } 134 | 135 | /** 136 | * Sets the requested gain. 137 | * @param {number|null|undefined} gain The gain in dB, or null/undefined 138 | * for automatic gain. 139 | * @param {Function} kont The continuation for this function. 140 | */ 141 | function setGain(gain, kont) { 142 | if (gain == null) { 143 | tuner.setAutoGain(kont); 144 | } else { 145 | tuner.setManualGain(gain, kont); 146 | } 147 | } 148 | 149 | /** 150 | * Set the sample rate. 151 | * @param {number} rate The sample rate, in samples/sec. 152 | * @param {Function} kont The continuation for this function. Receives the 153 | * sample rate that was actually set as its first parameter. 154 | */ 155 | function setSampleRate(rate, kont) { 156 | var ratio = Math.floor(XTAL_FREQ * (1 << 22) / rate); 157 | ratio &= 0x0ffffffc; 158 | var realRate = Math.floor(XTAL_FREQ * (1 << 22) / ratio); 159 | var ppmOffset = -1 * Math.floor(ppm * (1 << 24) / 1000000); 160 | com.writeEach([ 161 | [CMD.DEMODREG, 1, 0x9f, (ratio >> 16) & 0xffff, 2], 162 | [CMD.DEMODREG, 1, 0xa1, ratio & 0xffff, 2], 163 | [CMD.DEMODREG, 1, 0x3e, (ppmOffset >> 8) & 0x3f, 1], 164 | [CMD.DEMODREG, 1, 0x3f, ppmOffset & 0xff, 1] 165 | ], function() { 166 | resetDemodulator(function() { 167 | kont(realRate); 168 | })}); 169 | } 170 | 171 | /** 172 | * Resets the demodulator. 173 | * @param {Function} kont The continuation for this function. 174 | */ 175 | function resetDemodulator(kont) { 176 | com.writeEach([ 177 | [CMD.DEMODREG, 1, 0x01, 0x14, 1], 178 | [CMD.DEMODREG, 1, 0x01, 0x10, 1] 179 | ], kont); 180 | } 181 | 182 | /** 183 | * Tunes the device to the given frequency. 184 | * @param {number} freq The frequency to tune to, in Hertz. 185 | * @param {Function} kont The continuation for this function, which receives 186 | * the actual tuned frequency. 187 | */ 188 | function setCenterFrequency(freq, kont) { 189 | com.i2c.open(function() { 190 | tuner.setFrequency(freq + IF_FREQ, function(actualFreq) { 191 | com.i2c.close(function() { 192 | kont(actualFreq - IF_FREQ); 193 | })})}); 194 | } 195 | 196 | /** 197 | * Resets the sample buffer. Call this before starting to read samples. 198 | * @param {Function} kont The continuation for this function. 199 | */ 200 | function resetBuffer(kont) { 201 | com.writeEach([ 202 | [CMD.REG, BLOCK.USB, REG.EPA_CTL, 0x0210, 2], 203 | [CMD.REG, BLOCK.USB, REG.EPA_CTL, 0x0000, 2] 204 | ], kont); 205 | } 206 | 207 | /** 208 | * Reads a block of samples off the device. 209 | * @param {number} length The number of samples to read. 210 | * @param {Function} kont The continuation for this function. It will receive 211 | * as its argument an ArrayBuffer containing the read samples, which you 212 | * can interpret as pairs of unsigned 8-bit integers; the first one is 213 | * the sample's I value, and the second one is its Q value. 214 | */ 215 | function readSamples(length, kont) { 216 | com.bulk.readBuffer(length * BYTES_PER_SAMPLE, kont); 217 | } 218 | 219 | /** 220 | * Stops the demodulator. 221 | * @param {Function} kont The continuation for this function. 222 | */ 223 | function close(kont) { 224 | com.i2c.open(function() { 225 | tuner.close(function() { 226 | com.i2c.close(function() { 227 | com.iface.release(kont); 228 | })})}); 229 | } 230 | 231 | /** 232 | * Handles an error. 233 | * @param {string} msg The error message. 234 | */ 235 | function throwError(msg) { 236 | if (errorHandler) { 237 | errorHandler(msg); 238 | } else { 239 | throw msg; 240 | } 241 | } 242 | 243 | /** 244 | * Sets a function to call if there's an error communicating with the 245 | * demodulator. 246 | * @param {Function} func The function to call on error. 247 | */ 248 | function setOnError(func) { 249 | errorHandler = func; 250 | com.setOnError(throwError); 251 | } 252 | 253 | return { 254 | open: open, 255 | setSampleRate: setSampleRate, 256 | setCenterFrequency: setCenterFrequency, 257 | resetBuffer: resetBuffer, 258 | readSamples: readSamples, 259 | close: close, 260 | setOnError: setOnError 261 | }; 262 | } 263 | 264 | -------------------------------------------------------------------------------- /extension/help.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | Radio Receiver Help 19 | 20 | 40 | 41 | 42 |

Radio Receiver help

43 | 44 |

Radio Receiver is a Chrome application to listen to radio signals on your computer using a USB digital TV tuner. You can use it as a regular AM/FM radio, or you can use it in Free Tuning mode to listen to all kinds of radio signals, like amateur radio, marine radio, aviation radio, public safety radio, or short wave radio (using an upconverter).

45 | 46 |

You can use the settings window to set Radio Receiver up for your country's radio bands, configure an upconverter, set custom tuner gain, adjust your tuner's oscillators, etc.

47 | 48 |

Radio Receiver also has a lot of keyboard shortcuts so you don't even need to use a mouse! 49 | 50 |

Using Radio Receiver

51 | 52 |
53 | 54 |

Start and stop

55 |

Press the “Power On” button (1) to start playing the radio. You'll see that the label changes to “Power Off”; if you press it again, the radio will stop.

56 | 57 |

Changing frequency and band

58 |

You can change frequency in several ways:

59 | 65 |

To change the band you are listening to, click on the band indicator (5). Note that, before you can listen to AM radio, you'll need to set up an upconverter in the settings window.

66 | 67 |

Using presets

68 |

Click the “Save” button (6) to save the station you are listening to as a preset. Enter a name for the preset and press “Save".

69 |

To switch to a preset, click the preset selection box (7) and choose your new preset. The radio will switch to it immediately.

70 |

To delete a preset, switch to it and press the “Remove” button (8).

71 |

To change a preset's name, switch to it and click the “Save” button (6). Type the new name and press “Save”.

72 | 73 |

Changing volume and stereo

74 |

To change the volume, click on the “loudspeaker” icon (9) and move the slider left and right to decrease or increase the volume. You can also use your mouse wheel on the icon to change volume directly.

75 |

To disable stereo click on the stereo indicator (10).

76 | 77 |

Recording from the radio

78 |

You can record what you hear on the radio into a WAV file. Just click the “Record” button (11) to start, and type a name for the file. You'll see that the button's label changes to “Stop”; if you press it again, the radio will stop recording.

79 | 80 |

Tuning into other radio signals

81 | 82 |

With Free Tuning you can use Radio Receiver to listen to all kinds of radio signals in all frequency bands your tuner can receive, such as amateur radio, marine radio, aviation radio, short wave radio (with an upconverter), etc.

83 | 84 |

Enabling Free Tuning

85 | 86 |

To be able to use Free Tuning, open the settings window and click on the checkbox to enable Free Tuning mode.

87 | 88 |

Using Free Tuning

89 | 90 |

Free Tuning appears like an extra band in Radio Receiver. To enter Free Tuning, click the band indicator (5 in the picture above) until the display changes to look like this:

91 | 92 |
93 | 94 |

Every button in Radio Receiver works in Free Tuning mode like it does elsewhere; however, you have some extra functions available.

95 | 96 |

The frequency display (1) shows the current frequency in Hertz. You can change frequency by clicking on it and typing the new frequency, using the scroll wheel, and using the “Freq-” and “Freq+” buttons. You can adjust the amount by which the frequency is adjusted by changing the value of the “Step” field (2).

97 | 98 |

To change modulation scheme, click on the “Mode” field (3). A drop-down list will appear that lets you choose among the available schemes. Currently these are Wideband FM (WBFM), Narrowband FM (NBFM), AM, Lower Sideband (LSB), and Upper Sideband (USB).

99 | 100 |

Some modulation schemes have parameters that affect how they work; for example, NBFM has a maximum frequency deviation (Max fdev), AM has a bandwidth, etc. You can set the value of that parameter in the corresponding field pointed to by 4.

101 | 102 |

If you have configured an upconverter in the settings window you can enable and disable it here by clicking the “Upconverter” field (5).

103 | 104 |

The “Squelch” setting (6) lets you monitor a frequency without having to hear static when nobody is transmitting. The squelch level is the minimum strength level the signal must reach before you can hear it; if it's set to 0, you'll hear everything.

105 | 106 |

Settings

107 |

You can change a lot of settings by clicking on the “wrench” icon (12). There you can change several settings:

108 | 109 |
110 | 111 | 120 | 121 |

Keyboard shortcuts

122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 |
KeyFunction
Space BarTurn radio on/off
Up ArrowVolume Up
Down ArrowVolume Down
Left ArrowDecrease Frequency
Right ArrowIncrease Frequency
<Scan for stations by decreasing frequency
>Scan for stations by increasing frequency
fEdit frequency
bChange band
sToggle stereo off/on
pGo to preset selector
Shift + SSave preset
Shift + RRemove preset
wRecord from the radio
Shift + WStop recording from the radio
?Help (this page)
!Settings menu
EscapeRemove focus from all elements and re-enable shortcuts
143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /extension/interface.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | Radio Receiver 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 |
Radio Receiver
36 | 37 | 38 |
39 |
40 |
41 | 42 | 43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 |
53 |
54 | 61 |
62 |
63 |
64 |
65 | 66 |
Hz
67 |
68 |
69 |
70 |
71 | 72 |
Hz
73 |
74 |
75 |
76 |
77 | 78 |
Hz
79 |
80 |
81 |
Upconverter:
82 | 83 |
84 |
85 |
Squelch:
86 |
100
87 | 88 |
89 |
90 |
91 | 92 | 93 |
94 |
95 | 96 | 97 | 98 | 99 |
100 | 102 | 103 | 104 |
105 |
106 | 107 | 108 | -------------------------------------------------------------------------------- /extension/presetmanager.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | var opener = window['opener']; 16 | var mainWindow = window['mainWindow']; 17 | 18 | closeButton.addEventListener('click', exit); 19 | exportButton.addEventListener('click', exportSelectedPresets); 20 | importButton.addEventListener('click', importPresetsFile); 21 | cancelImportButton.addEventListener('click', showCurrentEntries); 22 | 23 | var presets = new Presets(); 24 | presets.load(showCurrentEntries); 25 | 26 | /** 27 | * Adjusts the window size to its contents. 28 | */ 29 | function adjustWindow() { 30 | AuxWindows.resizeCurrentTo(700, 0); 31 | } 32 | 33 | /** 34 | * Closes the current window. 35 | */ 36 | function exit() { 37 | AuxWindows.closeCurrent(); 38 | } 39 | 40 | /** 41 | * Removes duplicates in place from a sorted list. 42 | */ 43 | function uniq(freqs) { 44 | var prev = null; 45 | for (var i = 0; i < freqs.length; ) { 46 | if (freqs[i] == prev) { 47 | freqs.splice(i, 1); 48 | } else { 49 | prev = freqs[i]; 50 | ++i; 51 | } 52 | } 53 | } 54 | 55 | /** 56 | * Shows the current list of presets. 57 | */ 58 | function showPresets(table, presets, delPresets, precheck) { 59 | var lines = table.getElementsByClassName('preset'); 60 | while (lines.length > 0) { 61 | lines[0].parentElement.removeChild(lines[0]); 62 | } 63 | var freqs = []; 64 | if (presets) { 65 | for (var freq in presets.get()) { 66 | freqs.push(freq); 67 | } 68 | } 69 | if (delPresets) { 70 | for (var freq in delPresets.get()) { 71 | freqs.push(freq); 72 | } 73 | } 74 | freqs.sort(function(a,b) { return Number(a) - Number(b) }); 75 | uniq(freqs); 76 | for (var i = 0; i < freqs.length; ++i) { 77 | var value = freqs[i]; 78 | var preset = presets && presets.get(value); 79 | var delPreset = delPresets && delPresets.get(value); 80 | var isDouble = preset && delPreset; 81 | var line; 82 | if (delPreset) { 83 | line = makePresetLine(value, delPreset, i, true, isDouble, precheck); 84 | table.firstElementChild.appendChild(line); 85 | } 86 | if (preset) { 87 | line = makePresetLine(value, preset, i, false, isDouble, precheck); 88 | table.firstElementChild.appendChild(line); 89 | } 90 | } 91 | return freqs.length; 92 | } 93 | 94 | /** 95 | * Builds a line in a preset list. 96 | */ 97 | function makePresetLine(value, preset, index, isDelete, isDouble, precheck) { 98 | var isEven = 0 == (index % 2); 99 | var line = document.createElement('tr'); 100 | if (isEven) { 101 | line.classList.add('evenRow'); 102 | } 103 | if (isDelete) { 104 | line.classList.add('deleteRow'); 105 | } 106 | line.classList.add('preset'); 107 | if (isDelete || !isDouble) { 108 | line.appendChild(makeCheckboxCell(value, precheck, isDouble)); 109 | } 110 | line.appendChild(makeFreqCell(preset)); 111 | line.appendChild(makeNameCell(preset)); 112 | line.appendChild(makeModeCell(preset)); 113 | return line; 114 | } 115 | 116 | /** 117 | * Makes a cell containing a preset selection checkbox. 118 | */ 119 | function makeCheckboxCell(value, precheck, isDouble) { 120 | var chk = document.createElement('input'); 121 | chk.className = 'presetBox'; 122 | chk.type = 'checkbox'; 123 | chk.id = 'freq-' + value; 124 | chk.checked = precheck; 125 | var td = document.createElement('td'); 126 | if (isDouble) { 127 | td.rowSpan = 2; 128 | } 129 | td.appendChild(chk); 130 | return td; 131 | } 132 | 133 | /** 134 | * Makes a cell containing a preset's frequency. 135 | */ 136 | function makeFreqCell(preset) { 137 | var td = document.createElement('td'); 138 | td.innerText = preset['display'].replace(' ', '\u00a0'); 139 | return td; 140 | } 141 | 142 | /** 143 | * Makes a cell containing a preset's name. 144 | */ 145 | function makeNameCell(preset) { 146 | var td = document.createElement('td'); 147 | td.innerText = preset['name'].replace(' ', '\u00a0'); 148 | td.style['font-weight'] = 700; 149 | return td; 150 | } 151 | 152 | /** 153 | * Makes a cell containing a preset's band or mode. 154 | */ 155 | function makeModeCell(preset) { 156 | var td = document.createElement('td'); 157 | var band = preset['band']; 158 | var mode = preset['mode']; 159 | var text = ''; 160 | if (band) { 161 | text = 'Band=' + band; 162 | } else { 163 | text = mode['modulation']; 164 | if (mode['bandwidth']) { 165 | text += '; bw=' + mode['bandwidth'] + 'Hz'; 166 | } 167 | if (mode['maxF']) { 168 | text += '; maxF=' + mode['maxF'] + 'Hz'; 169 | } 170 | if (mode['step']) { 171 | text += '; step=' + mode['step'] + 'Hz'; 172 | } 173 | if (mode['upconvert']) { 174 | text += '; upconvert=on'; 175 | } 176 | } 177 | td.appendChild(document.createTextNode(text)); 178 | return td; 179 | } 180 | 181 | /** 182 | * Returns a list of currently selected frequencies. 183 | */ 184 | function getSelectedFreqs(container) { 185 | var freqBoxes = container.getElementsByClassName('presetBox'); 186 | var selected = []; 187 | for (var i = 0; i < freqBoxes.length; ++i) { 188 | if (freqBoxes[i].checked) { 189 | selected.push(Number(freqBoxes[i].id.substring(5))); 190 | } 191 | } 192 | return selected; 193 | } 194 | 195 | /** 196 | * Returns a map of currently selected presets. 197 | */ 198 | function getSelectedEntries(container) { 199 | var selected = {}; 200 | var freqs = getSelectedFreqs(container); 201 | for (var i = 0; i < freqs.length; ++i) { 202 | selected[freqs[i]] = presets.get(freqs[i]); 203 | } 204 | return selected; 205 | } 206 | 207 | /** 208 | * Exports the selected presets. 209 | */ 210 | function exportSelectedPresets() { 211 | showError(''); 212 | var opt = { 213 | type: 'saveFile', 214 | suggestedName: 'presets.json' 215 | }; 216 | chrome.fileSystem.chooseEntry(opt, function(fileEntry) { 217 | var entries = getSelectedEntries(showPresetPane); 218 | var exported = new Presets(entries).exportPresets(); 219 | fileEntry.createWriter(function(writer) { 220 | writer.onwriteend = function() { 221 | if (this.position == 0) { 222 | writer.write(new Blob([JSON.stringify(exported, null, 4)])); 223 | } 224 | }; 225 | writer.truncate(0); 226 | }); 227 | }); 228 | } 229 | 230 | /** 231 | * Imports a presets file. 232 | */ 233 | function importPresetsFile() { 234 | showError(''); 235 | var opt = { 236 | type: 'openFile', 237 | accepts: [{ 238 | description: 'JSON Files', 239 | extensions: ['json']}] 240 | }; 241 | chrome.fileSystem.chooseEntry(opt, function(fileEntry) { 242 | fileEntry.file(function(file) { 243 | var reader = new FileReader(); 244 | reader.onloadend = function() { 245 | try { 246 | var newPresets = JSON.parse(this.result); 247 | if (newPresets['presets']) { 248 | showImportedEntries(newPresets['presets']); 249 | } else { 250 | showError('Invalid presets file'); 251 | showCurrentEntries(); 252 | } 253 | } catch (e) { 254 | showError('Error reading the presets file: ' + e); 255 | showCurrentEntries(); 256 | } 257 | }; 258 | reader.readAsText(file); 259 | }); 260 | }); 261 | } 262 | 263 | /** 264 | * Shows an error message. 265 | */ 266 | function showError(msg) { 267 | errorPane.innerText = msg; 268 | if (msg) { 269 | errorPane.classList.remove('invisible'); 270 | } else { 271 | errorPane.classList.add('invisible'); 272 | } 273 | } 274 | 275 | /** 276 | * Shows the current presets. 277 | */ 278 | function showCurrentEntries() { 279 | showPresets(presetList, presets, null, true); 280 | showPresetPane.classList.remove('invisible'); 281 | showDiffPane.classList.add('invisible'); 282 | adjustWindow(); 283 | } 284 | 285 | /** 286 | * Shows the presets to be imported. 287 | */ 288 | function showImportedEntries(entries) { 289 | var newPresets = new Presets(); 290 | newPresets.importPresets(entries); 291 | var diff = presets.diff(newPresets); 292 | if (showPresets(addPresetList, diff.add, null, true)) { 293 | addPresetPanel.classList.remove('invisible'); 294 | } else { 295 | addPresetPanel.classList.add('invisible'); 296 | } 297 | if (showPresets(changePresetList, diff.changeAdd, diff.changeDel, false)) { 298 | changePresetPanel.classList.remove('invisible'); 299 | } else { 300 | changePresetPanel.classList.add('invisible'); 301 | } 302 | if (showPresets(deletePresetList, null, diff.del, false)) { 303 | delPresetPanel.classList.remove('invisible'); 304 | } else { 305 | delPresetPanel.classList.add('invisible'); 306 | } 307 | showPresetPane.classList.add('invisible'); 308 | showDiffPane.classList.remove('invisible'); 309 | doImportButton.addEventListener('click', performImport(diff)); 310 | adjustWindow(); 311 | } 312 | 313 | /** 314 | * Imports the selected items. 315 | */ 316 | function performImport(diff) { 317 | return function() { 318 | var selectedFreqs = getSelectedFreqs(showDiffPane); 319 | var add = {}; 320 | var del = {}; 321 | for (var i = 0; i < selectedFreqs.length; ++i) { 322 | var freq = selectedFreqs[i]; 323 | var toDel = diff.del.get(freq); 324 | var toAdd = diff.add.get(freq); 325 | if (toDel) { 326 | del[freq] = toDel; 327 | } else if (toAdd) { 328 | add[freq] = toAdd; 329 | } else { 330 | del[freq] = diff.changeDel.get(freq); 331 | add[freq] = diff.changeAdd.get(freq); 332 | } 333 | } 334 | presets.change(new Presets(del), new Presets(add)); 335 | presets.save(exit); 336 | } 337 | } 338 | 339 | -------------------------------------------------------------------------------- /image-src/help-freetuning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 31 | 36 | 37 | 44 | 49 | 50 | 57 | 63 | 64 | 65 | 83 | 85 | 86 | 88 | image/svg+xml 89 | 91 | 92 | 93 | 94 | 95 | 100 | 110 | 119 | 128 | 3 142 | 151 | 2 165 | 5 179 | 188 | 197 | 4 211 | 1 225 | 6 239 | 248 | 249 | 250 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | -------------------------------------------------------------------------------- /extension/r820t.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * Operations on the R820T tuner chip. 17 | * @param {RtlCom} com The RTL communications object. 18 | * @param {number} xtalFreq The frequency of the oscillator crystal. 19 | * @param {Function} throwError A function to handle errors. 20 | * @constructor 21 | */ 22 | function R820T(com, xtalFreq, throwError) { 23 | 24 | /** 25 | * Initial values for registers 0x05-0x1f. 26 | */ 27 | var REGISTERS = [0x83, 0x32, 0x75, 0xc0, 0x40, 0xd6, 0x6c, 0xf5, 0x63, 0x75, 28 | 0x68, 0x6c, 0x83, 0x80, 0x00, 0x0f, 0x00, 0xc0, 0x30, 0x48, 29 | 0xcc, 0x60, 0x00, 0x54, 0xae, 0x4a, 0xc0]; 30 | 31 | /** 32 | * Configurations for the multiplexer in different frequency bands. 33 | */ 34 | var MUX_CFGS = [ 35 | [0, 0x08, 0x02, 0xdf], 36 | [50, 0x08, 0x02, 0xbe], 37 | [55, 0x08, 0x02, 0x8b], 38 | [60, 0x08, 0x02, 0x7b], 39 | [65, 0x08, 0x02, 0x69], 40 | [70, 0x08, 0x02, 0x58], 41 | [75, 0x00, 0x02, 0x44], 42 | [90, 0x00, 0x02, 0x34], 43 | [110, 0x00, 0x02, 0x24], 44 | [140, 0x00, 0x02, 0x14], 45 | [180, 0x00, 0x02, 0x13], 46 | [250, 0x00, 0x02, 0x11], 47 | [280, 0x00, 0x02, 0x00], 48 | [310, 0x00, 0x41, 0x00], 49 | [588, 0x00, 0x40, 0x00] 50 | ]; 51 | 52 | /** 53 | * A bit mask to reverse the bits in a byte. 54 | */ 55 | var BIT_REVS = [0x0, 0x8, 0x4, 0xc, 0x2, 0xa, 0x6, 0xe, 56 | 0x1, 0x9, 0x5, 0xd, 0x3, 0xb, 0x7, 0xf]; 57 | 58 | /** 59 | * Whether the PLL in the tuner is locked. 60 | */ 61 | var hasPllLock = false; 62 | 63 | /** 64 | * Shadow registers 0x05-0x1f, for setting values using masks. 65 | */ 66 | var shadowRegs; 67 | 68 | 69 | /** 70 | * Initializes the tuner. 71 | * @param {Function} kont The continuation for this function. 72 | */ 73 | function init(kont) { 74 | initRegisters(REGISTERS, function() { 75 | initElectronics(kont); 76 | }); 77 | } 78 | 79 | /** 80 | * Sets the tuner's frequency. 81 | * @param {number} freq The frequency to tune to. 82 | * @param {Function} kont The continuation for this function, which receives 83 | * the actual tuned frequency. 84 | */ 85 | function setFrequency(freq, kont) { 86 | setMux(freq, function() { 87 | setPll(freq, kont); 88 | }); 89 | } 90 | 91 | /** 92 | * Stops the tuner. 93 | * @param {Function} kont The continuation for this function. 94 | */ 95 | function close(kont) { 96 | writeEach([ 97 | [0x06, 0xb1, 0xff], 98 | [0x05, 0xb3, 0xff], 99 | [0x07, 0x3a, 0xff], 100 | [0x08, 0x40, 0xff], 101 | [0x09, 0xc0, 0xff], 102 | [0x0a, 0x36, 0xff], 103 | [0x0c, 0x35, 0xff], 104 | [0x0f, 0x68, 0xff], 105 | [0x11, 0x03, 0xff], 106 | [0x17, 0xf4, 0xff], 107 | [0x19, 0x0c, 0xff] 108 | ], kont); 109 | } 110 | 111 | /** 112 | * Initializes all the components of the tuner. 113 | * @param {Function} kont The continuation for this function. 114 | */ 115 | function initElectronics(kont) { 116 | writeEach([ 117 | [0x0c, 0x00, 0x0f], 118 | [0x13, 49, 0x3f], 119 | [0x1d, 0x00, 0x38] 120 | ], function() { 121 | calibrateFilter(true, function(filterCap) { 122 | writeEach([ 123 | [0x0a, 0x10 | filterCap, 0x1f], 124 | [0x0b, 0x6b, 0xef], 125 | [0x07, 0x00, 0x80], 126 | [0x06, 0x10, 0x30], 127 | [0x1e, 0x40, 0x60], 128 | [0x05, 0x00, 0x80], 129 | [0x1f, 0x00, 0x80], 130 | [0x0f, 0x00, 0x80], 131 | [0x19, 0x60, 0x60], 132 | [0x1d, 0xe5, 0xc7], 133 | [0x1c, 0x24, 0xf8], 134 | [0x0d, 0x53, 0xff], 135 | [0x0e, 0x75, 0xff], 136 | [0x05, 0x00, 0x60], 137 | [0x06, 0x00, 0x08], 138 | [0x11, 0x38, 0x08], 139 | [0x17, 0x30, 0x30], 140 | [0x0a, 0x40, 0x60], 141 | [0x1d, 0x00, 0x38], 142 | [0x1c, 0x00, 0x04], 143 | [0x06, 0x00, 0x40], 144 | [0x1a, 0x30, 0x30], 145 | [0x1d, 0x18, 0x38], 146 | [0x1c, 0x24, 0x04], 147 | [0x1e, 0x0d, 0x1f], 148 | [0x1a, 0x20, 0x30] 149 | ], kont); 150 | })}); 151 | } 152 | 153 | /** 154 | * Sets the tuner to automatic gain. 155 | * @param {Function} kont The continuation for this function. 156 | */ 157 | function setAutoGain(kont) { 158 | writeEach([ 159 | [0x05, 0x00, 0x10], 160 | [0x07, 0x10, 0x10], 161 | [0x0c, 0x0b, 0x9f] 162 | ], kont); 163 | } 164 | 165 | /** 166 | * Sets the tuner's manual gain. 167 | * @param {number} gain The tuner's gain, in dB. 168 | * @param {Function} kont The continuation for this function. 169 | */ 170 | function setManualGain(gain, kont) { 171 | var step = 0; 172 | if (gain <= 15) { 173 | step = Math.round(1.36 + gain * (1.1118 + gain * (-0.0786 + gain * 0.0027))); 174 | } else { 175 | step = Math.round(1.2068 + gain * (0.6875 + gain * (-0.01011 + gain * 0.0001587))); 176 | } 177 | if (step < 0) { 178 | step = 0; 179 | } else if (step > 30) { 180 | step = 30; 181 | } 182 | var lnaValue = Math.floor(step / 2); 183 | var mixerValue = Math.floor((step - 1) / 2); 184 | writeEach([ 185 | [0x05, 0x10, 0x10], 186 | [0x07, 0x00, 0x10], 187 | [0x0c, 0x08, 0x9f], 188 | [0x05, lnaValue, 0x0f], 189 | [0x07, mixerValue, 0x0f] 190 | ], kont); 191 | } 192 | 193 | /** 194 | * Calibrates the filters. 195 | * @param {boolean} firstTry Whether this is the first try to calibrate. 196 | * @param {Function} kont The continuation for this function. 197 | */ 198 | function calibrateFilter(firstTry, kont) { 199 | writeEach([ 200 | [0x0b, 0x6b, 0x60], 201 | [0x0f, 0x04, 0x04], 202 | [0x10, 0x00, 0x03] 203 | ], function() { 204 | setPll(56000000, function() { 205 | if (!hasPllLock) { 206 | throwError("PLL not locked -- cannot tune to the selected frequency."); 207 | return; 208 | } 209 | writeEach([ 210 | [0x0b, 0x10, 0x10], 211 | [0x0b, 0x00, 0x10], 212 | [0x0f, 0x00, 0x04] 213 | ], function() { 214 | readRegBuffer(0x00, 5, function(data) { 215 | var arr = new Uint8Array(data); 216 | var filterCap = arr[4] & 0x0f; 217 | if (filterCap == 0x0f) { 218 | filterCap = 0; 219 | } 220 | if (filterCap != 0 && firstTry) { 221 | calibrateFilter(false, kont); 222 | } else { 223 | kont(filterCap); 224 | } 225 | })})})}); 226 | } 227 | 228 | /** 229 | * Sets the multiplexer's frequency. 230 | * @param {number} freq The frequency to set. 231 | * @param {Function} kont The continuation for this function. 232 | */ 233 | function setMux(freq, kont) { 234 | var freqMhz = freq / 1000000; 235 | for (var i = 0; i < MUX_CFGS.length - 1; ++i) { 236 | if (freqMhz < MUX_CFGS[i + 1][0]) { 237 | break; 238 | } 239 | } 240 | var cfg = MUX_CFGS[i]; 241 | writeEach([ 242 | [0x17, cfg[1], 0x08], 243 | [0x1a, cfg[2], 0xc3], 244 | [0x1b, cfg[3], 0xff], 245 | [0x10, 0x00, 0x0b], 246 | [0x08, 0x00, 0x3f], 247 | [0x09, 0x00, 0x3f] 248 | ], kont); 249 | } 250 | 251 | /** 252 | * Sets the PLL's frequency. 253 | * @param {number} freq The frequency to set. 254 | * @param {Function} kont The continuation for this function. 255 | */ 256 | function setPll(freq, kont) { 257 | var pllRef = Math.floor(xtalFreq); 258 | writeEach([ 259 | [0x10, 0x00, 0x10], 260 | [0x1a, 0x00, 0x0c], 261 | [0x12, 0x80, 0xe0] 262 | ], function() { 263 | var divNum = Math.min(6, Math.floor(Math.log(1770000000 / freq) / Math.LN2)); 264 | var mixDiv = 1 << (divNum + 1); 265 | readRegBuffer(0x00, 5, function(data) { 266 | var arr = new Uint8Array(data); 267 | var vcoFineTune = (arr[4] & 0x30) >> 4; 268 | if (vcoFineTune > 2) { 269 | --divNum; 270 | } else if (vcoFineTune < 2) { 271 | ++divNum; 272 | } 273 | writeRegMask(0x10, divNum << 5, 0xe0, function() { 274 | var vcoFreq = freq * mixDiv; 275 | var nint = Math.floor(vcoFreq / (2 * pllRef)); 276 | var vcoFra = vcoFreq % (2 * pllRef); 277 | if (nint > 63) { 278 | hasPllLock = false; 279 | return kont(); 280 | } 281 | var ni = Math.floor((nint - 13) / 4); 282 | var si = (nint - 13) % 4; 283 | writeEach([ 284 | [0x14, ni + (si << 6), 0xff], 285 | [0x12, vcoFra == 0 ? 0x08 : 0x00, 0x08] 286 | ], function() { 287 | var sdm = Math.min(65535, Math.floor(32768 * vcoFra / pllRef)); 288 | writeEach([ 289 | [0x16, sdm >> 8, 0xff], 290 | [0x15, sdm & 0xff, 0xff] 291 | ], function() { 292 | getPllLock(true, function() { 293 | writeRegMask(0x1a, 0x08, 0x08, function() { 294 | var actualFreq = 2 * pllRef * (nint + sdm / 65536) / mixDiv; 295 | kont(actualFreq); 296 | })})})})})})}); 297 | } 298 | 299 | /** 300 | * Checks whether the PLL has achieved lock. 301 | * @param {boolean} firstTry Whether this is the first try to achieve lock. 302 | * @param {Function} kont The continuation for this function. 303 | */ 304 | function getPllLock(firstTry, kont) { 305 | readRegBuffer(0x00, 3, function(data) { 306 | var arr = new Uint8Array(data); 307 | if (arr[2] & 0x40) { 308 | hasPllLock = true; 309 | return kont(); 310 | } 311 | if (firstTry) { 312 | writeRegMask(0x12, 0x60, 0xe0, function() { 313 | return getPllLock(false, kont); 314 | }); 315 | } else { 316 | hasPllLock = false; 317 | kont(); 318 | } 319 | }); 320 | } 321 | 322 | /** 323 | * Sets the initial values of the 0x05-0x1f registers. 324 | * @param {Array.} regs The values for the registers. 325 | * @param {Function} kont The continuation for this function. 326 | */ 327 | function initRegisters(regs, kont) { 328 | shadowRegs = new Uint8Array(regs); 329 | var cmds = []; 330 | for (var i = 0; i < regs.length; ++i) { 331 | cmds.push([CMD.I2CREG, 0x34, i + 5, regs[i]]); 332 | } 333 | com.writeEach(cmds, kont); 334 | } 335 | 336 | /** 337 | * Reads a series of registers into a buffer. 338 | * @param {number} addr The first register's address to read. 339 | * @param {number} length The number of registers to read. 340 | * @param {Function} kont The continuation for this function. It receives 341 | * an ArrayBuffer with the data. 342 | */ 343 | function readRegBuffer(addr, length, kont) { 344 | com.i2c.readRegBuffer(0x34, addr, length, function(data) { 345 | var buf = new Uint8Array(data); 346 | for (var i = 0; i < buf.length; ++i) { 347 | var b = buf[i]; 348 | buf[i] = (BIT_REVS[b & 0xf] << 4) | BIT_REVS[b >> 4]; 349 | } 350 | kont(buf.buffer); 351 | }); 352 | } 353 | 354 | /** 355 | * Writes a masked value into a register. 356 | * @param {number} addr The address of the register to write into. 357 | * @param {number} value The value to write. 358 | * @param {number} mask A mask that specifies which bits to write. 359 | * @param {Function} kont The continuation for this function. 360 | */ 361 | function writeRegMask(addr, value, mask, kont) { 362 | var rc = shadowRegs[addr - 5]; 363 | var val = (rc & ~mask) | (value & mask); 364 | shadowRegs[addr - 5] = val; 365 | com.i2c.writeRegister(0x34, addr, val, kont); 366 | } 367 | 368 | /** 369 | * Perform the write operations given in the array. 370 | * @param {Array.>} array The operations. 371 | * @param {Function} kont The continuation for this function. 372 | */ 373 | function writeEach(array, kont) { 374 | var index = 0; 375 | function iterate() { 376 | if (index >= array.length) { 377 | kont(); 378 | } else { 379 | var line = array[index++]; 380 | writeRegMask(line[0], line[1], line[2], iterate); 381 | } 382 | } 383 | iterate(); 384 | } 385 | 386 | return { 387 | init: init, 388 | setFrequency: setFrequency, 389 | setAutoGain: setAutoGain, 390 | setManualGain: setManualGain, 391 | close: close 392 | }; 393 | } 394 | 395 | /** 396 | * Checks if the R820T tuner is present. 397 | * @param {RtlCom} com The RTL communications object. 398 | * @param {Function} kont The continuation for this function. It receives 399 | * a boolean that tells whether the tuner is present. 400 | */ 401 | R820T.check = function (com, kont) { 402 | com.i2c.readRegister(0x34, 0, function(data) { 403 | kont(data == 0x69); 404 | }); 405 | }; 406 | 407 | -------------------------------------------------------------------------------- /image-src/help.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 28 | 33 | 34 | 41 | 46 | 47 | 54 | 60 | 61 | 62 | 80 | 82 | 83 | 85 | image/svg+xml 86 | 88 | 89 | 90 | 91 | 92 | 97 | 104 | 110 | 1 121 | 127 | 2 138 | 144 | 150 | 3 161 | 3 172 | 178 | 184 | 4 195 | 4 206 | 212 | 218 | 224 | 230 | 6 241 | 7 252 | 8 263 | 9 274 | 280 | 5 291 | 297 | 12 309 | 315 | 10 327 | 333 | 11 345 | 346 | 347 | -------------------------------------------------------------------------------- /extension/rtlcom.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * Low-level communications with the RTL2832U-based dongle. 17 | * @param {ConnectionHandle} conn The USB connection handle. 18 | * @constructor 19 | */ 20 | function RtlCom(conn) { 21 | 22 | /** 23 | * Whether to log all USB transfers. 24 | */ 25 | var VERBOSE = false; 26 | 27 | /** 28 | * Set in the control messages' index field for write operations. 29 | */ 30 | var WRITE_FLAG = 0x10; 31 | 32 | /** 33 | * Function to call if there was an error in USB transfers. 34 | */ 35 | var onError; 36 | 37 | /** 38 | * Writes a buffer into a dongle's register. 39 | * @param {number} block The register's block number. 40 | * @param {number} reg The register number. 41 | * @param {ArrayBuffer} buffer The buffer to write. 42 | * @param {Function} kont The continuation for this function. 43 | */ 44 | function writeRegBuffer(block, reg, buffer, kont) { 45 | writeCtrlMsg(reg, block | WRITE_FLAG, buffer, kont); 46 | } 47 | 48 | /** 49 | * Reads a buffer from a dongle's register. 50 | * @param {number} block The register's block number. 51 | * @param {number} reg The register number. 52 | * @param {number} length The length in bytes of the buffer to read. 53 | * @param {Function} kont The continuation for this function. 54 | * It receives the read buffer. 55 | */ 56 | function readRegBuffer(block, reg, length, kont) { 57 | readCtrlMsg(reg, block, length, kont); 58 | } 59 | 60 | /** 61 | * Writes a value into a dongle's register. 62 | * @param {number} block The register's block number. 63 | * @param {number} reg The register number. 64 | * @param {number} value The value to write. 65 | * @param {number} length The width in bytes of this value. 66 | * @param {Function} kont The continuation for this function. 67 | */ 68 | function writeReg(block, reg, value, length, kont) { 69 | writeCtrlMsg(reg, block | WRITE_FLAG, numberToBuffer(value, length), kont); 70 | } 71 | 72 | /** 73 | * Reads a value from a dongle's register. 74 | * @param {number} block The register's block number. 75 | * @param {number} reg The register number. 76 | * @param {number} length The width in bytes of the value to read. 77 | * @param {Function} kont The continuation for this function. 78 | * It receives the decoded value. 79 | */ 80 | function readReg(block, reg, length, kont) { 81 | readCtrlMsg(reg, block, length, function(data) { 82 | kont(bufferToNumber(data)); 83 | }); 84 | } 85 | 86 | /** 87 | * Writes a masked value into a dongle's register. 88 | * @param {number} block The register's block number. 89 | * @param {number} reg The register number. 90 | * @param {number} value The value to write. 91 | * @param {number} mask The mask for the value to write. 92 | * @param {Function} kont The continuation for this function. 93 | */ 94 | function writeRegMask(block, reg, value, mask, kont) { 95 | if (mask == 0xff) { 96 | writeReg(block, reg, value, 1, kont); 97 | } else { 98 | readReg(block, reg, 1, function(old) { 99 | value &= mask; 100 | old &= ~mask; 101 | value |= old; 102 | writeReg(block, reg, value, 1, kont); 103 | }); 104 | } 105 | } 106 | 107 | /** 108 | * Reads a value from a demodulator register. 109 | * @param {number} page The register page number. 110 | * @param {number} addr The register's address. 111 | * @param {Function} kont The continuation for this function. 112 | * It receives the decoded value. 113 | */ 114 | function readDemodReg(page, addr, kont) { 115 | readReg(page, (addr << 8) | 0x20, 1, kont); 116 | } 117 | 118 | /** 119 | * Writes a value into a demodulator register. 120 | * @param {number} page The register page number. 121 | * @param {number} addr The register's address. 122 | * @param {number} value The value to write. 123 | * @param {number} len The width in bytes of this value. 124 | * @param {Function} kont The continuation for this function. 125 | */ 126 | function writeDemodReg(page, addr, value, len, kont) { 127 | writeRegBuffer(page, (addr << 8) | 0x20, numberToBuffer(value, len, true), function() { 128 | readDemodReg(0x0a, 0x01, kont); 129 | }); 130 | } 131 | 132 | /** 133 | * Opens the I2C repeater. 134 | * @param {Function} kont The continuation for this function. 135 | */ 136 | function openI2C(kont) { 137 | writeDemodReg(1, 1, 0x18, 1, kont); 138 | } 139 | 140 | /** 141 | * Closes the I2C repeater. 142 | * @param {Function} kont The continuation for this function. 143 | */ 144 | function closeI2C(kont) { 145 | writeDemodReg(1, 1, 0x10, 1, kont); 146 | } 147 | 148 | /** 149 | * Reads a value from an I2C register. 150 | * @param {number} addr The device's address. 151 | * @param {number} reg The register number. 152 | * @param {Function} kont The continuation for this function. 153 | */ 154 | function readI2CReg(addr, reg, kont) { 155 | writeRegBuffer(BLOCK.I2C, addr, new Uint8Array([reg]).buffer, function() { 156 | readReg(BLOCK.I2C, addr, 1, kont); 157 | }); 158 | } 159 | 160 | /** 161 | * Writes a value to an I2C register. 162 | * @param {number} addr The device's address. 163 | * @param {number} reg The register number. 164 | * @param {number} value The value to write. 165 | * @param {number} len The width in bytes of this value. 166 | * @param {Function} kont The continuation for this function. 167 | */ 168 | function writeI2CReg(addr, reg, value, kont) { 169 | writeRegBuffer(BLOCK.I2C, addr, new Uint8Array([reg, value]).buffer, kont); 170 | } 171 | 172 | /** 173 | * Reads a buffer from an I2C register. 174 | * @param {number} addr The device's address. 175 | * @param {number} reg The register number. 176 | * @param {number} len The number of bytes to read. 177 | * @param {Function} kont The continuation for this function. 178 | */ 179 | function readI2CRegBuffer(addr, reg, len, kont) { 180 | writeRegBuffer(BLOCK.I2C, addr, new Uint8Array([reg]).buffer, function() { 181 | readRegBuffer(BLOCK.I2C, addr, len, kont); 182 | }); 183 | } 184 | 185 | /** 186 | * Writes a buffer to an I2C register. 187 | * @param {number} addr The device's address. 188 | * @param {number} reg The register number. 189 | * @param {ArrayBuffer} buffer The buffer to write. 190 | * @param {Function} kont The continuation for this function. 191 | */ 192 | function writeI2CRegBuffer(addr, reg, buffer, kont) { 193 | var data = new Uint8Array(buffer.byteLength + 1); 194 | data[0] = reg; 195 | data.set(new Uint8Array(buffer), 1); 196 | writeRegBuffer(BLOCK.I2C, addr, data.buffer, kont); 197 | } 198 | 199 | /** 200 | * Decodes a buffer as a little-endian number. 201 | * @param {ArrayBuffer} buffer The buffer to decode. 202 | * @return {number} The decoded number. 203 | */ 204 | function bufferToNumber(buffer) { 205 | var len = buffer.byteLength; 206 | var dv = new DataView(buffer); 207 | if (len == 0) { 208 | return null; 209 | } else if (len == 1) { 210 | return dv.getUint8(0); 211 | } else if (len == 2) { 212 | return dv.getUint16(0, true); 213 | } else if (len == 4) { 214 | return dv.getUint32(0, true); 215 | } 216 | throw 'Cannot parse ' + len + '-byte number'; 217 | } 218 | 219 | /** 220 | * Encodes a number into a buffer. 221 | * @param {number} value The number to encode. 222 | * @param {number} len The number of bytes to encode into. 223 | * @param {boolean=} opt_bigEndian Whether to use a big-endian encoding. 224 | */ 225 | function numberToBuffer(value, len, opt_bigEndian) { 226 | var buffer = new ArrayBuffer(len); 227 | var dv = new DataView(buffer); 228 | if (len == 1) { 229 | dv.setUint8(0, value); 230 | } else if (len == 2) { 231 | dv.setUint16(0, value, !opt_bigEndian); 232 | } else if (len == 4) { 233 | dv.setUint32(0, value, !opt_bigEndian); 234 | } else { 235 | throw 'Cannot write ' + len + '-byte number'; 236 | } 237 | return buffer; 238 | } 239 | 240 | /** 241 | * Sends a USB control message to read from the device. 242 | * @param {number} value The value field of the control message. 243 | * @param {number} index The index field of the control message. 244 | * @param {number} length The number of bytes to read. 245 | * @param {Function} kont The continuation for this function. 246 | */ 247 | function readCtrlMsg(value, index, length, kont) { 248 | var ti = { 249 | 'requestType': 'vendor', 250 | 'recipient': 'device', 251 | 'direction': 'in', 252 | 'request': 0, 253 | 'value': value, 254 | 'index': index, 255 | 'length': Math.max(8, length) 256 | }; 257 | chrome.usb.controlTransfer(conn, ti, function(event) { 258 | var data = event.data.slice(0, length); 259 | if (VERBOSE) { 260 | console.log('IN value 0x' + value.toString(16) + ' index 0x' + 261 | index.toString(16)); 262 | console.log(' read -> ' + dumpBuffer(data)); 263 | } 264 | var rc = event.resultCode; 265 | if (rc != 0) { 266 | var msg = 'USB read failed (value 0x' + value.toString(16) + 267 | ' index 0x' + index.toString(16) + '), rc=' + rc + 268 | ', lastErrorMessage="' + chrome.runtime.lastError.message + '"'; 269 | if (onError) { 270 | console.error(msg); 271 | return onError(msg); 272 | } else { 273 | throw msg; 274 | } 275 | } 276 | kont(data); 277 | }); 278 | } 279 | 280 | /** 281 | * Sends a USB control message to write to the device. 282 | * @param {number} value The value field of the control message. 283 | * @param {number} index The index field of the control message. 284 | * @param {ArrayBuffer} buffer The buffer to write to the device. 285 | * @param {Function} kont The continuation for this function. 286 | */ 287 | function writeCtrlMsg(value, index, buffer, kont) { 288 | var ti = { 289 | 'requestType': 'vendor', 290 | 'recipient': 'device', 291 | 'direction': 'out', 292 | 'request': 0, 293 | 'value': value, 294 | 'index': index, 295 | 'data': buffer 296 | }; 297 | chrome.usb.controlTransfer(conn, ti, function(event) { 298 | if (VERBOSE) { 299 | console.log('OUT value 0x' + value.toString(16) + ' index 0x' + 300 | index.toString(16) + ' data ' + dumpBuffer(buffer)); 301 | } 302 | var rc = event.resultCode; 303 | if (rc != 0) { 304 | var msg = 'USB write failed (value 0x' + value.toString(16) + 305 | ' index 0x' + index.toString(16) + ' data ' + dumpBuffer(buffer) + 306 | '), rc=' + rc + ', lastErrorMessage="' + 307 | chrome.runtime.lastError.message + '"'; 308 | if (onError) { 309 | console.error(msg); 310 | return onError(msg); 311 | } else { 312 | throw msg; 313 | } 314 | } 315 | kont(); 316 | }); 317 | } 318 | 319 | /** 320 | * Does a bulk transfer from the device. 321 | * @param {number} length The number of bytes to read. 322 | * @param {Function} kont The continuation for this function. It receives the 323 | * received buffer. 324 | */ 325 | function readBulk(length, kont) { 326 | var ti = { 327 | 'direction': 'in', 328 | 'endpoint': 1, 329 | 'length': length 330 | }; 331 | chrome.usb.bulkTransfer(conn, ti, function(event) { 332 | if (VERBOSE) { 333 | console.log('IN BULK requested ' + length + ' received ' + event.data.byteLength); 334 | } 335 | var rc = event.resultCode; 336 | if (rc != 0) { 337 | var msg = 'USB bulk read failed (length 0x' + length.toString(16) + 338 | '), rc=' + rc + ', lastErrorMessage="' + 339 | chrome.runtime.lastError.message + '"'; 340 | if (onError) { 341 | console.error(msg); 342 | return onError(msg); 343 | } else { 344 | throw msg; 345 | } 346 | } 347 | kont(event.data); 348 | }); 349 | } 350 | 351 | /** 352 | * Claims the USB interface. 353 | * @param {Function} kont The continuation for this function. 354 | */ 355 | function claimInterface(kont) { 356 | chrome.usb.claimInterface(conn, 0, kont); 357 | } 358 | 359 | /** 360 | * Releases the USB interface. 361 | * @param {Function} kont The continuation for this function. 362 | */ 363 | function releaseInterface(kont) { 364 | chrome.usb.releaseInterface(conn, 0, kont); 365 | } 366 | 367 | /** 368 | * Performs several write operations as specified in an array. 369 | * @param {Array.>} array The operations to perform. 370 | * @param {Function} kont The continuation for this function. 371 | */ 372 | function writeEach(array, kont) { 373 | var index = 0; 374 | function iterate() { 375 | if (index >= array.length) { 376 | kont(); 377 | } else { 378 | var line = array[index++]; 379 | if (line[0] == CMD.REG) { 380 | writeReg(line[1], line[2], line[3], line[4], iterate); 381 | } else if (line[0] == CMD.REGMASK) { 382 | writeRegMask(line[1], line[2], line[3], line[4], iterate); 383 | } else if (line[0] == CMD.DEMODREG) { 384 | writeDemodReg(line[1], line[2], line[3], line[4], iterate); 385 | } else if (line[0] == CMD.I2CREG) { 386 | writeI2CReg(line[1], line[2], line[3], iterate); 387 | } else { 388 | throw 'Unsupported operation [' + line + ']'; 389 | } 390 | } 391 | } 392 | iterate(); 393 | } 394 | 395 | /** 396 | * Sets a function to call in case of error. 397 | * @param {Function} func The function to call. 398 | */ 399 | function setOnError(func) { 400 | onError = func; 401 | } 402 | 403 | /** 404 | * Returns a string representation of a buffer. 405 | * @param {ArrayBuffer} buffer The buffer to display. 406 | * @return {string} The string representation of the buffer. 407 | */ 408 | function dumpBuffer(buffer) { 409 | var bytes = []; 410 | var arr = new Uint8Array(buffer); 411 | for (var i = 0; i < arr.length; ++i) { 412 | bytes.push('0x' + arr[i].toString(16)); 413 | } 414 | return '[' + bytes + ']'; 415 | } 416 | 417 | 418 | return { 419 | writeRegister: writeReg, 420 | readRegister: readReg, 421 | writeRegMask: writeRegMask, 422 | demod: { 423 | readRegister: readDemodReg, 424 | writeRegister: writeDemodReg 425 | }, 426 | i2c: { 427 | open: openI2C, 428 | close: closeI2C, 429 | readRegister: readI2CReg, 430 | writeRegister: writeI2CReg, 431 | readRegBuffer: readI2CRegBuffer 432 | }, 433 | bulk: { 434 | readBuffer: readBulk 435 | }, 436 | iface: { 437 | claim: claimInterface, 438 | release: releaseInterface 439 | }, 440 | writeEach: writeEach, 441 | setOnError: setOnError 442 | }; 443 | } 444 | 445 | /** 446 | * Commands for writeEach. 447 | */ 448 | CMD = { 449 | REG: 1, 450 | REGMASK: 2, 451 | DEMODREG: 3, 452 | I2CREG: 4 453 | }; 454 | 455 | /** 456 | * Register blocks. 457 | */ 458 | BLOCK = { 459 | DEMOD: 0x000, 460 | USB: 0x100, 461 | SYS: 0x200, 462 | I2C: 0x600 463 | }; 464 | 465 | /** 466 | * Device registers. 467 | */ 468 | REG = { 469 | SYSCTL: 0x2000, 470 | EPA_CTL: 0x2148, 471 | EPA_MAXPKT: 0x2158, 472 | DEMOD_CTL: 0x3000, 473 | DEMOD_CTL_1: 0x300b 474 | }; 475 | 476 | -------------------------------------------------------------------------------- /extension/dsp.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * @fileoverview DSP functions and operations. 17 | */ 18 | 19 | /** 20 | * Generates coefficients for a FIR low-pass filter with the given 21 | * half-amplitude frequency and kernel length at the given sample rate. 22 | * @param {number} sampleRate The signal's sample rate. 23 | * @param {number} halfAmplFreq The half-amplitude frequency in Hz. 24 | * @param {number} length The filter kernel's length. Should be an odd number. 25 | * @return {Float32Array} The FIR coefficients for the filter. 26 | */ 27 | function getLowPassFIRCoeffs(sampleRate, halfAmplFreq, length) { 28 | length += (length + 1) % 2; 29 | var freq = halfAmplFreq / sampleRate; 30 | var coefs = new Float32Array(length); 31 | var center = Math.floor(length / 2); 32 | var sum = 0; 33 | for (var i = 0; i < length; ++i) { 34 | var val; 35 | if (i == center) { 36 | val = 2 * Math.PI * freq; 37 | } else { 38 | val = Math.sin(2 * Math.PI * freq * (i - center)) / (i - center); 39 | val *= 0.54 - 0.46 * Math.cos(2 * Math.PI * i / (length - 1)) 40 | } 41 | sum += val; 42 | coefs[i] = val; 43 | } 44 | for (var i = 0; i < length; ++i) { 45 | coefs[i] /= sum; 46 | } 47 | return coefs; 48 | } 49 | 50 | /** 51 | * Multiplies an array that represents a signal by a sinusoidal. 52 | * @param {Float32Array} samples The array to multiply. 53 | * @param {number} sampleRate The signal's sample rate. 54 | * @param {number} freq The frequency to multiply by. 55 | * @param {boolean} cosine Whether to use cosine (sine otherwise). 56 | * @return {Float32Array} The multiplied array. 57 | */ 58 | function multiplyArray(samples, sampleRate, freq, cosine) { 59 | var out = new Float32Array(samples.length); 60 | var angFreq = 2 * Math.PI * freq / sampleRate; 61 | var center = Math.floor(out.length / 2); 62 | for (var i = 0; i < out.length; ++i) { 63 | var angle = angFreq * (center - i); 64 | out[i] = samples[i] * (cosine ? Math.cos(angle) : Math.sin(angle)); 65 | } 66 | return out; 67 | } 68 | 69 | /** 70 | * Returns coefficients for a Hilbert transform. 71 | * @param {number} length The length of the kernel. 72 | * @param {bool} upper Whether to calculate the coefficients for USB. 73 | * @return {Float32Array} The kernel coefficients. 74 | */ 75 | function getHilbertCoeffs(length, upper) { 76 | length += (length + 1) % 2; 77 | var center = Math.floor(length / 2); 78 | var out = new Float32Array(length); 79 | for (var i = 0; i < out.length; ++i) { 80 | if ((i % 2) == 0) { 81 | out[i] = 2 / (Math.PI * (center - i)); 82 | } 83 | } 84 | return out; 85 | } 86 | 87 | /** 88 | * An object to apply a FIR filter to a sequence of samples. 89 | * @param {Float32Array} coefficients The coefficients of the filter to apply. 90 | * @constructor 91 | */ 92 | function FIRFilter(coefficients) { 93 | var coefs = coefficients; 94 | var offset = coefs.length - 1; 95 | var center = Math.floor(coefs.length / 2); 96 | var curSamples = new Float32Array(offset); 97 | 98 | /** 99 | * Loads a new block of samples to filter. 100 | * @param {Float32Array} samples The samples to load. 101 | */ 102 | function loadSamples(samples) { 103 | var newSamples = new Float32Array(samples.length + offset); 104 | newSamples.set(curSamples.subarray(curSamples.length - offset)); 105 | newSamples.set(samples, offset); 106 | curSamples = newSamples; 107 | } 108 | 109 | /** 110 | * Returns a filtered sample. 111 | * Be very careful when you modify this function. About 85% of the total execution 112 | * time is spent here, so performance is critical. 113 | * @param {number} index The index of the sample to return, corresponding 114 | * to the same index in the latest sample block loaded via loadSamples(). 115 | */ 116 | function get(index) { 117 | var out = 0; 118 | for (var i = 0; i < coefs.length; ++i) { 119 | out += coefs[i] * curSamples[index + i]; 120 | } 121 | return out; 122 | } 123 | 124 | /** 125 | * Returns a delayed sample. 126 | * @param {number} index The index of the relative sample to return. 127 | */ 128 | function getDelayed(index) { 129 | return curSamples[index + center]; 130 | } 131 | 132 | return { 133 | get: get, 134 | loadSamples: loadSamples, 135 | getDelayed: getDelayed 136 | }; 137 | } 138 | 139 | /** 140 | * Applies a low-pass filter and resamples to a lower sample rate. 141 | * @param {number} inRate The input signal's sample rate. 142 | * @param {number} outRate The output signal's sample rate. 143 | * @param {Float32Array} coefficients The coefficients for the FIR filter to 144 | * apply to the original signal before downsampling it. 145 | * @constructor 146 | */ 147 | function Downsampler(inRate, outRate, coefficients) { 148 | var filter = new FIRFilter(coefficients); 149 | var rateMul = inRate / outRate; 150 | 151 | /** 152 | * Returns a downsampled version of the given samples. 153 | * @param {Float32Array} samples The sample block to downsample. 154 | * @return {Float32Array} The downsampled block. 155 | */ 156 | function downsample(samples) { 157 | filter.loadSamples(samples); 158 | var outArr = new Float32Array(Math.floor(samples.length / rateMul)); 159 | for (var i = 0, readFrom = 0; i < outArr.length; ++i, readFrom += rateMul) { 160 | outArr[i] = filter.get(Math.floor(readFrom)); 161 | } 162 | return outArr; 163 | } 164 | 165 | return { 166 | downsample: downsample 167 | }; 168 | } 169 | 170 | /** 171 | * A class to demodulate IQ-interleaved samples into a raw audio signal. 172 | * @param {number} inRate The sample rate for the input signal. 173 | * @param {number} outRate The sample rate for the output audio. 174 | * @param {number} filterFreq The bandwidth of the sideband. 175 | * @param {number} upper Whether we are demodulating the upper sideband. 176 | * @param {number} kernelLen The length of the filter kernel. 177 | * @constructor 178 | */ 179 | function SSBDemodulator(inRate, outRate, filterFreq, upper, kernelLen) { 180 | var coefs = getLowPassFIRCoeffs(inRate, 10000, kernelLen); 181 | var downsamplerI = new Downsampler(inRate, outRate, coefs); 182 | var downsamplerQ = new Downsampler(inRate, outRate, coefs); 183 | var coefsHilbert = getHilbertCoeffs(kernelLen); 184 | var filterDelay = new FIRFilter(coefsHilbert); 185 | var filterHilbert = new FIRFilter(coefsHilbert, upper); 186 | var coefsSide = getLowPassFIRCoeffs(outRate, filterFreq, kernelLen); 187 | var filterSide = new FIRFilter(coefsSide); 188 | var hilbertMul = upper ? -1 : 1; 189 | var powerLongAvg = new ExpAverage(outRate * 5); 190 | var powerShortAvg = new ExpAverage(outRate * 0.5); 191 | var sigRatio = inRate / outRate; 192 | var relSignalPower = 0; 193 | 194 | /** 195 | * Demodulates the given I/Q samples. 196 | * @param {Float32Array} samplesI The I component of the samples 197 | * to demodulate. 198 | * @param {Float32Array} samplesQ The Q component of the samples 199 | * to demodulate. 200 | * @returns {Float32Array} The demodulated sound. 201 | */ 202 | function demodulateTuned(samplesI, samplesQ) { 203 | var I = downsamplerI.downsample(samplesI); 204 | var Q = downsamplerQ.downsample(samplesQ); 205 | 206 | var specSqrSum = 0; 207 | var sigSqrSum = 0; 208 | filterDelay.loadSamples(I); 209 | filterHilbert.loadSamples(Q); 210 | var prefilter = new Float32Array(I.length); 211 | for (var i = 0; i < prefilter.length; ++i) { 212 | prefilter[i] = filterDelay.getDelayed(i) + filterHilbert.get(i) * hilbertMul; 213 | } 214 | filterSide.loadSamples(prefilter); 215 | var out = new Float32Array(I.length); 216 | for (var i = 0; i < out.length; ++i) { 217 | var sig = filterSide.get(i); 218 | var power = sig * sig; 219 | sigSqrSum += power; 220 | var stPower = powerShortAvg.add(power); 221 | var ltPower = powerLongAvg.add(power); 222 | var multi = 0.9 * Math.max(1, Math.sqrt(2 / Math.min(1/128, Math.max(ltPower, stPower)))); 223 | out[i] = multi * filterSide.get(i); 224 | var origIndex = Math.floor(i * sigRatio); 225 | var origI = samplesI[origIndex]; 226 | var origQ = samplesQ[origIndex]; 227 | specSqrSum += origI * origI + origQ * origQ; 228 | } 229 | 230 | relSignalPower = sigSqrSum / specSqrSum; 231 | return out; 232 | } 233 | 234 | function getRelSignalPower() { 235 | return relSignalPower; 236 | } 237 | 238 | return { 239 | demodulateTuned: demodulateTuned, 240 | getRelSignalPower: getRelSignalPower 241 | } 242 | } 243 | 244 | /** 245 | * A class to demodulate IQ-interleaved samples into a raw audio signal. 246 | * @param {number} inRate The sample rate for the input signal. 247 | * @param {number} outRate The sample rate for the output audio. 248 | * @param {number} filterFreq The frequency of the low-pass filter. 249 | * @param {number} kernelLen The length of the filter kernel. 250 | 251 | * @constructor 252 | */ 253 | function AMDemodulator(inRate, outRate, filterFreq, kernelLen) { 254 | var coefs = getLowPassFIRCoeffs(inRate, filterFreq, kernelLen); 255 | var downsamplerI = new Downsampler(inRate, outRate, coefs); 256 | var downsamplerQ = new Downsampler(inRate, outRate, coefs); 257 | var sigRatio = inRate / outRate; 258 | var relSignalPower = 0; 259 | 260 | /** 261 | * Demodulates the given I/Q samples. 262 | * @param {Float32Array} samplesI The I component of the samples 263 | * to demodulate. 264 | * @param {Float32Array} samplesQ The Q component of the samples 265 | 266 | * to demodulate. 267 | * @returns {Float32Array} The demodulated sound. 268 | */ 269 | function demodulateTuned(samplesI, samplesQ) { 270 | var I = downsamplerI.downsample(samplesI); 271 | var Q = downsamplerQ.downsample(samplesQ); 272 | var iAvg = average(I); 273 | var qAvg = average(Q); 274 | var out = new Float32Array(I.length); 275 | 276 | var specSqrSum = 0; 277 | var sigSqrSum = 0; 278 | var sigSum = 0; 279 | for (var i = 0; i < out.length; ++i) { 280 | var iv = I[i] - iAvg; 281 | var qv = Q[i] - qAvg; 282 | var power = iv * iv + qv * qv; 283 | var ampl = Math.sqrt(power); 284 | out[i] = ampl; 285 | var origIndex = Math.floor(i * sigRatio); 286 | var origI = samplesI[origIndex]; 287 | var origQ = samplesQ[origIndex]; 288 | specSqrSum += origI * origI + origQ * origQ; 289 | sigSqrSum += power; 290 | sigSum += ampl; 291 | } 292 | var halfPoint = sigSum / out.length; 293 | for (var i = 0; i < out.length; ++i) { 294 | out[i] = (out[i] - halfPoint) / halfPoint; 295 | } 296 | relSignalPower = sigSqrSum / specSqrSum; 297 | return out; 298 | } 299 | 300 | function getRelSignalPower() { 301 | return relSignalPower; 302 | } 303 | 304 | return { 305 | demodulateTuned: demodulateTuned, 306 | getRelSignalPower: getRelSignalPower 307 | } 308 | } 309 | 310 | /** 311 | * A class to demodulate IQ-interleaved samples into a raw audio signal. 312 | * @param {number} inRate The sample rate for the input signal. 313 | * @param {number} outRate The sample rate for the output audio. 314 | * @param {number} maxF The maximum frequency deviation. 315 | * @param {number} filterFreq The frequency of the low-pass filter. 316 | * @param {number} kernelLen The length of the filter kernel. 317 | * @constructor 318 | */ 319 | function FMDemodulator(inRate, outRate, maxF, filterFreq, kernelLen) { 320 | var AMPL_CONV = outRate / (2 * Math.PI * maxF); 321 | 322 | var coefs = getLowPassFIRCoeffs(inRate, filterFreq, kernelLen); 323 | var downsamplerI = new Downsampler(inRate, outRate, coefs); 324 | var downsamplerQ = new Downsampler(inRate, outRate, coefs); 325 | var lI = 0; 326 | var lQ = 0; 327 | var relSignalPower = 0; 328 | 329 | /** 330 | * Demodulates the given I/Q samples. 331 | * @param {Float32Array} samplesI The I component of the samples 332 | * to demodulate. 333 | * @param {Float32Array} samplesQ The Q component of the samples 334 | * to demodulate. 335 | * @returns {Float32Array} The demodulated sound. 336 | */ 337 | function demodulateTuned(samplesI, samplesQ) { 338 | var I = downsamplerI.downsample(samplesI); 339 | var Q = downsamplerQ.downsample(samplesQ); 340 | var out = new Float32Array(I.length); 341 | 342 | var prev = 0; 343 | var difSqrSum = 0; 344 | for (var i = 0; i < out.length; ++i) { 345 | var real = lI * I[i] + lQ * Q[i]; 346 | var imag = lI * Q[i] - I[i] * lQ; 347 | var sgn = 1; 348 | var circ = 0; 349 | var ang = 0; 350 | var div = 1; 351 | if (real < 0) { 352 | sgn = -sgn; 353 | real = -real; 354 | circ = Math.PI; 355 | } 356 | if (imag < 0) { 357 | sgn = -sgn; 358 | imag = -imag; 359 | circ = -circ; 360 | } 361 | if (real > imag) { 362 | div = imag / real; 363 | } else if (real != imag) { 364 | ang = -Math.PI / 2; 365 | div = real / imag; 366 | sgn = -sgn; 367 | } 368 | out[i] = circ + sgn * 369 | (ang + div 370 | / (0.98419158358617365 371 | + div * (0.093485702629671305 372 | + div * 0.19556307900617517))) * AMPL_CONV; 373 | lI = I[i]; 374 | lQ = Q[i]; 375 | var dif = prev - out[i]; 376 | difSqrSum += dif * dif; 377 | prev = out[i]; 378 | } 379 | 380 | relSignalPower = 1 - Math.sqrt(difSqrSum / out.length); 381 | return out; 382 | } 383 | 384 | function getRelSignalPower() { 385 | return relSignalPower; 386 | } 387 | 388 | return { 389 | demodulateTuned: demodulateTuned, 390 | getRelSignalPower: getRelSignalPower 391 | } 392 | } 393 | 394 | /** 395 | * Demodulates the stereo signal in a demodulated FM signal. 396 | * @param {number} sampleRate The sample rate for the input signal. 397 | * @param {number} pilotFreq The frequency of the pilot tone. 398 | * @constructor 399 | */ 400 | function StereoSeparator(sampleRate, pilotFreq) { 401 | var AVG_COEF = 9999; 402 | var STD_THRES = 400; 403 | var SIN = new Float32Array(8001); 404 | var COS = new Float32Array(8001); 405 | 406 | var sin = 0 407 | var cos = 1; 408 | var iavg = new ExpAverage(9999); 409 | var qavg = new ExpAverage(9999); 410 | var cavg = new ExpAverage(49999, true); 411 | 412 | for (var i = 0; i < 8001; ++i) { 413 | var freq = (pilotFreq + i / 100 - 40) * 2 * Math.PI / sampleRate; 414 | SIN[i] = Math.sin(freq); 415 | COS[i] = Math.cos(freq); 416 | } 417 | 418 | /** 419 | * Locks on to the pilot tone and uses it to demodulate the stereo audio. 420 | * @param {Float32Array} samples The original audio stream. 421 | * @return {Object} An object with a key 'found' that tells whether a 422 | * consistent stereo pilot tone was detected and a key 'diff' 423 | * that contains the original stream demodulated with the 424 | * reconstructed stereo carrier. 425 | */ 426 | function separate(samples) { 427 | var out = new Float32Array(samples); 428 | for (var i = 0; i < out.length; ++i) { 429 | var hdev = iavg.add(out[i] * sin); 430 | var vdev = qavg.add(out[i] * cos); 431 | out[i] *= sin * cos * 2; 432 | var corr; 433 | if (hdev > 0) { 434 | corr = Math.max(-4, Math.min(4, vdev / hdev)); 435 | } else { 436 | corr = vdev == 0 ? 0 : (vdev > 0 ? 4 : -4); 437 | } 438 | var idx = Math.round((corr + 4) * 1000); 439 | var newSin = sin * COS[idx] + cos * SIN[idx]; 440 | cos = cos * COS[idx] - sin * SIN[idx]; 441 | sin = newSin; 442 | cavg.add(corr * 10); 443 | } 444 | return { 445 | found: cavg.getStd() < STD_THRES, 446 | diff: out 447 | }; 448 | } 449 | 450 | return { 451 | separate: separate 452 | }; 453 | } 454 | 455 | /** 456 | * A de-emphasis filter with the given time constant. 457 | * @param {number} sampleRate The signal's sample rate. 458 | * @param {number} timeConstant_uS The filter's time constant in microseconds. 459 | * @constructor 460 | */ 461 | function Deemphasizer(sampleRate, timeConstant_uS) { 462 | var alpha = 1 / (1 + sampleRate * timeConstant_uS / 1e6); 463 | var val = 0; 464 | 465 | /** 466 | * Deemphasizes the given samples in place. 467 | * @param {Float32Array} samples The samples to deemphasize. 468 | */ 469 | function inPlace(samples) { 470 | for (var i = 0; i < samples.length; ++i) { 471 | val = val + alpha * (samples[i] - val); 472 | samples[i] = val; 473 | } 474 | } 475 | 476 | return { 477 | inPlace: inPlace 478 | }; 479 | } 480 | 481 | /** 482 | * An exponential moving average accumulator. 483 | * @param {number} weight Weight of the previous average value. 484 | * @param {boolean=} opt_std Whether to calculate the standard deviation. 485 | * @constructor 486 | */ 487 | function ExpAverage(weight, opt_std) { 488 | var avg = 0; 489 | var std = 0; 490 | 491 | /** 492 | * Adds a value to the moving average. 493 | * @param {number} value The value to add. 494 | * @return {number} The moving average. 495 | */ 496 | function add(value) { 497 | avg = (weight * avg + value) / (weight + 1); 498 | if (opt_std) { 499 | std = (weight * std + (value - avg) * (value - avg)) / (weight + 1); 500 | } 501 | return avg; 502 | } 503 | 504 | /** 505 | * Returns the moving standard deviation. 506 | * @param {number} The moving standard deviation. 507 | */ 508 | function getStd() { 509 | return std; 510 | } 511 | 512 | return { 513 | add: add, 514 | getStd: getStd 515 | }; 516 | } 517 | 518 | /** 519 | * Calculates the average of an array. 520 | * @param {Float32Array} arr The array to calculate its average. 521 | * @return {number} The average value. 522 | */ 523 | function average(arr) { 524 | var sum = 0; 525 | for (var i = 0; i < arr.length; ++i) { 526 | sum += arr[i]; 527 | } 528 | return sum / arr.length; 529 | } 530 | 531 | /** 532 | * Converts the given buffer of unsigned 8-bit samples into a pair of 32-bit 533 | * floating-point sample streams. 534 | * @param {ArrayBuffer} buffer A buffer containing the unsigned 8-bit samples. 535 | * @param {number} rate The buffer's sample rate. 536 | * @return {Array.} An array that contains first the I stream 537 | * and next the Q stream. 538 | */ 539 | function iqSamplesFromUint8(buffer, rate) { 540 | var arr = new Uint8Array(buffer); 541 | var len = arr.length / 2; 542 | var outI = new Float32Array(len); 543 | var outQ = new Float32Array(len); 544 | for (var i = 0; i < len; ++i) { 545 | outI[i] = arr[2 * i] / 128 - 0.995; 546 | outQ[i] = arr[2 * i + 1] / 128 - 0.995; 547 | } 548 | return [outI, outQ]; 549 | } 550 | 551 | /** 552 | * Shifts a series of IQ samples by a given frequency. 553 | * @param {Array.} IQ An array containing the I and Q streams. 554 | * @param {number} freq The frequency to shift the samples by. 555 | * @param {number} sampleRate The sample rate. 556 | * @param {number} cosine The cosine of the initial phase. 557 | * @param {number} sine The sine of the initial phase. 558 | * @return {Array} An array containing the I stream, Q stream, 559 | * final cosine and final sine. 560 | */ 561 | function shiftFrequency(IQ, freq, sampleRate, cosine, sine) { 562 | var deltaCos = Math.cos(2 * Math.PI * freq / sampleRate); 563 | var deltaSin = Math.sin(2 * Math.PI * freq / sampleRate); 564 | var I = IQ[0]; 565 | var Q = IQ[1]; 566 | var oI = new Float32Array(I.length); 567 | var oQ = new Float32Array(Q.length); 568 | for (var i = 0; i < I.length; ++i) { 569 | oI[i] = I[i] * cosine - Q[i] * sine; 570 | oQ[i] = I[i] * sine + Q[i] * cosine; 571 | var newSine = cosine * deltaSin + sine * deltaCos; 572 | cosine = cosine * deltaCos - sine * deltaSin; 573 | sine = newSine; 574 | } 575 | return [oI, oQ, cosine, sine]; 576 | } 577 | 578 | --------------------------------------------------------------------------------