├── .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 |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 |
24 |
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 |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 | 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 |  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 |51 |
52 | 53 || Frequency | Name | Mode |
|---|
62 | 63 | 64 |
65 || Frequency | Name | Mode |
|---|
| Frequency | Name | Mode |
|---|
| Frequency | Name | Mode |
|---|
98 | 99 | 100 |
101 |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 | 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 | 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 | 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 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 |

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 |You can change frequency in several ways:
59 |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 |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 |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 |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 |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 |To be able to use Free Tuning, open the settings window and click on the checkbox to enable Free Tuning mode.
87 | 88 |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 |
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 |You can change a lot of settings by clicking on the “wrench” icon (12). There you can change several settings:
108 | 109 |
| Key | Function |
|---|---|
| Space Bar | Turn radio on/off |
| Up Arrow | Volume Up |
| Down Arrow | Volume Down |
| Left Arrow | Decrease Frequency |
| Right Arrow | Increase Frequency |
| < | Scan for stations by decreasing frequency |
| > | Scan for stations by increasing frequency |
| f | Edit frequency |
| b | Change band |
| s | Toggle stereo off/on |
| p | Go to preset selector |
| Shift + S | Save preset |
| Shift + R | Remove preset |
| w | Record from the radio |
| Shift + W | Stop recording from the radio |
| ? | Help (this page) |
| ! | Settings menu |
| Escape | Remove focus from all elements and re-enable shortcuts |