├── icon-16.png ├── icon-48.png ├── icon-128.png ├── .gitmodules ├── common.css ├── manifest.json ├── audio.html ├── spectrum.html ├── main.js ├── README.md ├── js ├── Detector.js ├── stats.min.js └── TrackballControls.js ├── Lut.js ├── audio.js ├── spectrum.js ├── rtl2832u.js ├── radiocontroller.js ├── r820t.js └── rtlcom.js /icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttrftech/threejs-spectrum/HEAD/icon-16.png -------------------------------------------------------------------------------- /icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttrftech/threejs-spectrum/HEAD/icon-48.png -------------------------------------------------------------------------------- /icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttrftech/threejs-spectrum/HEAD/icon-128.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "jsfft"] 2 | path = jsfft 3 | url = https://github.com/dntj/jsfft.git 4 | -------------------------------------------------------------------------------- /common.css: -------------------------------------------------------------------------------- 1 | canvas { 2 | margin:10px auto; 3 | display:block; 4 | border:1px solid black; 5 | } 6 | 7 | body { 8 | color: #cccccc; 9 | font-family:Monospace; 10 | font-size:13px; 11 | text-align:center; 12 | 13 | background-color: #050505; 14 | margin: 0px; 15 | overflow: hidden; 16 | } 17 | 18 | #info { 19 | position: absolute; 20 | top: 0px; width: 100%; 21 | padding: 5px; 22 | } 23 | 24 | a { 25 | color: #0080ff; 26 | } 27 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Radio Spectrum", 3 | "description": "Display radio spectrum on your browser or ChromeBook using an RTL2832U-based USB digital TV tuner.", 4 | "version": "0.0", 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 | "usb" 17 | ], 18 | "optional_permissions": [ 19 | { 20 | "usbDevices": [ 21 | { 22 | "vendorId": 3034, 23 | "productId": 10290 24 | }, 25 | { 26 | "vendorId": 3034, 27 | "productId": 10296 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /audio.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | WebAudio and three.js spectrum flow 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /spectrum.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Radio Spectrum Display 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 |
28 |
29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /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 | var usedConnections = []; 16 | 17 | chrome.app.runtime.onLaunched.addListener(function() { 18 | chrome.app.window.create('spectrum.html', { 19 | 'id': 'radioSpectrum', 20 | 'bounds': { 21 | 'width': 800, 22 | 'height': 800 23 | }, 24 | 'resizable': true 25 | }); 26 | }); 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Spectrum Display using three.js 2 | =============================== 3 | 4 | This project contains real time 3D Spectrum display demo using three.js. 5 | 6 | Two options of signal source, audio and radio. 7 | 8 | ## Audio Spectrum Display 9 | 10 | ### Requirement 11 | 12 | - Browser supporting WebGL (tested with Chrome) 13 | 14 | ### Run 15 | 16 | Just open following link [https://ttrftech.github.io/threejs-spectrum/audio.html](https://ttrftech.github.io/threejs-spectrum/audio.html). If permission to access microphone required, click allow button. 17 | 18 | === 19 | 20 | ## Radio Spectrum Display 21 | 22 | ### Requirement 23 | 24 | - RTL2832U USB Dongle 25 | - Chrome Web Browser 26 | 27 | ### Setup 28 | 29 | $ git clone https://github.com/ttrftech/threejs-spectrum.git 30 | $ cd threejs-spectrum 31 | $ git submodule update --init 32 | 33 | ### Run 34 | 35 | On Chrome Web Browser, 36 | 37 | 1. Open Window>Extensions menu, 38 | 2. Check developer mode, 39 | 3. Click "Load unpacked extension..." button, 40 | 4. Select threejs-spectrum folder on file dialog, 41 | 5. Click "Launch" link in Radio Spectrum extension item. 42 | 6. Extension window will appear, then click Start button. 43 | 7. To change frequency, hit Tab key twice to move focus and enter frequency in MHz. 44 | 45 | === 46 | 47 | ## Acknowledgment 48 | 49 | - Chrome Radio Receiver Extension [https://github.com/google/radioreceiver](https://github.com/google/radioreceiver) 50 | - three.js [http://threejs.org/](http://threejs.org/) [https://github.com/mrdoob/three.js/](https://github.com/mrdoob/three.js/) 51 | - jsfft [https://github.com/dntj/jsfft](https://github.com/dntj/jsfft) 52 | -------------------------------------------------------------------------------- /js/Detector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | * @author mr.doob / http://mrdoob.com/ 4 | */ 5 | 6 | var Detector = { 7 | 8 | canvas: !! window.CanvasRenderingContext2D, 9 | webgl: ( function () { try { return !! window.WebGLRenderingContext && !! document.createElement( 'canvas' ).getContext( 'experimental-webgl' ); } catch( e ) { return false; } } )(), 10 | workers: !! window.Worker, 11 | fileapi: window.File && window.FileReader && window.FileList && window.Blob, 12 | 13 | getWebGLErrorMessage: function () { 14 | 15 | var element = document.createElement( 'div' ); 16 | element.id = 'webgl-error-message'; 17 | element.style.fontFamily = 'monospace'; 18 | element.style.fontSize = '13px'; 19 | element.style.fontWeight = 'normal'; 20 | element.style.textAlign = 'center'; 21 | element.style.background = '#fff'; 22 | element.style.color = '#000'; 23 | element.style.padding = '1.5em'; 24 | element.style.width = '400px'; 25 | element.style.margin = '5em auto 0'; 26 | 27 | if ( ! this.webgl ) { 28 | 29 | element.innerHTML = window.WebGLRenderingContext ? [ 30 | 'Your graphics card does not seem to support WebGL.
', 31 | 'Find out how to get it here.' 32 | ].join( '\n' ) : [ 33 | 'Your browser does not seem to support WebGL.
', 34 | 'Find out how to get it here.' 35 | ].join( '\n' ); 36 | 37 | } 38 | 39 | return element; 40 | 41 | }, 42 | 43 | addGetWebGLMessage: function ( parameters ) { 44 | 45 | var parent, id, element; 46 | 47 | parameters = parameters || {}; 48 | 49 | parent = parameters.parent !== undefined ? parameters.parent : document.body; 50 | id = parameters.id !== undefined ? parameters.id : 'oldie'; 51 | 52 | element = Detector.getWebGLErrorMessage(); 53 | element.id = id; 54 | 55 | parent.appendChild( element ); 56 | 57 | } 58 | 59 | }; 60 | -------------------------------------------------------------------------------- /js/stats.min.js: -------------------------------------------------------------------------------- 1 | // stats.js - http://github.com/mrdoob/stats.js 2 | var Stats=function(){var l=Date.now(),m=l,g=0,n=Infinity,o=0,h=0,p=Infinity,q=0,r=0,s=0,f=document.createElement("div");f.id="stats";f.addEventListener("mousedown",function(b){b.preventDefault();t(++s%2)},!1);f.style.cssText="width:80px;opacity:0.9;cursor:pointer";var a=document.createElement("div");a.id="fps";a.style.cssText="padding:0 0 3px 3px;text-align:left;background-color:#002";f.appendChild(a);var i=document.createElement("div");i.id="fpsText";i.style.cssText="color:#0ff;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px"; 3 | i.innerHTML="FPS";a.appendChild(i);var c=document.createElement("div");c.id="fpsGraph";c.style.cssText="position:relative;width:74px;height:30px;background-color:#0ff";for(a.appendChild(c);74>c.children.length;){var j=document.createElement("span");j.style.cssText="width:1px;height:30px;float:left;background-color:#113";c.appendChild(j)}var d=document.createElement("div");d.id="ms";d.style.cssText="padding:0 0 3px 3px;text-align:left;background-color:#020;display:none";f.appendChild(d);var k=document.createElement("div"); 4 | k.id="msText";k.style.cssText="color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px";k.innerHTML="MS";d.appendChild(k);var e=document.createElement("div");e.id="msGraph";e.style.cssText="position:relative;width:74px;height:30px;background-color:#0f0";for(d.appendChild(e);74>e.children.length;)j=document.createElement("span"),j.style.cssText="width:1px;height:30px;float:left;background-color:#131",e.appendChild(j);var t=function(b){s=b;switch(s){case 0:a.style.display= 5 | "block";d.style.display="none";break;case 1:a.style.display="none",d.style.display="block"}};return{REVISION:11,domElement:f,setMode:t,begin:function(){l=Date.now()},end:function(){var b=Date.now();g=b-l;n=Math.min(n,g);o=Math.max(o,g);k.textContent=g+" MS ("+n+"-"+o+")";var a=Math.min(30,30-30*(g/200));e.appendChild(e.firstChild).style.height=a+"px";r++;b>m+1E3&&(h=Math.round(1E3*r/(b-m)),p=Math.min(p,h),q=Math.max(q,h),i.textContent=h+" FPS ("+p+"-"+q+")",a=Math.min(30,30-30*(h/100)),c.appendChild(c.firstChild).style.height= 6 | a+"px",m=b,r=0);return b},update:function(){l=this.end()}}}; 7 | -------------------------------------------------------------------------------- /Lut.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author daron1337 / http://daron1337.github.io/ 3 | */ 4 | 5 | THREE.Lut = function ( colormap, numberofcolors ) { 6 | 7 | this.lut = new Array(); 8 | this.map = THREE.ColorMapKeywords[ colormap ]; 9 | this.n = numberofcolors; 10 | 11 | var step = 1. / this.n; 12 | 13 | for ( var i = 0; i <= 1; i+=step ) { 14 | 15 | for ( var j = 0; j < this.map.length - 1; j++ ) { 16 | 17 | if ( i >= this.map[ j ][ 0 ] && i < this.map[ j+1 ][ 0 ] ) { 18 | 19 | var min = this.map[ j ][ 0 ]; 20 | var max = this.map[ j+1 ][ 0 ]; 21 | var color = new THREE.Color( 0xffffff ); 22 | var minColor = new THREE.Color( 0xffffff ).setHex( this.map[ j ][ 1 ] ); 23 | var maxColor = new THREE.Color( 0xffffff ).setHex( this.map[ j+1 ][ 1 ] ); 24 | 25 | color = minColor.lerp( maxColor, ( i - min ) / ( max - min ) ); 26 | 27 | this.lut.push(color); 28 | 29 | } 30 | 31 | } 32 | 33 | } 34 | 35 | return this.set( this ); 36 | 37 | }; 38 | 39 | THREE.Lut.prototype = { 40 | 41 | constructor: THREE.Lut, 42 | 43 | lut: [], map: [], mapname: 'rainbow' , n: 256, minV: 0, maxV: 1, 44 | 45 | set: function ( value ) { 46 | 47 | if ( value instanceof THREE.Lut ) { 48 | 49 | this.copy( value ); 50 | 51 | } 52 | 53 | return this; 54 | 55 | }, 56 | 57 | setMin: function ( min ) { 58 | 59 | this.minV = min; 60 | 61 | return this; 62 | 63 | }, 64 | 65 | setMax: function ( max ) { 66 | 67 | this.maxV = max; 68 | 69 | return this; 70 | 71 | }, 72 | 73 | changeNumberOfColors: function ( numberofcolors ) { 74 | 75 | this.n = numberofcolors; 76 | 77 | return new THREE.Lut( this.mapname, this.n ); 78 | 79 | }, 80 | 81 | changeColorMap: function ( colormap ) { 82 | 83 | this.mapname = colormap; 84 | 85 | return new THREE.Lut( this.mapname, this.n ); 86 | 87 | }, 88 | 89 | copy: function ( lut ) { 90 | 91 | this.lut = lut.lut; 92 | this.mapname = lut.mapname; 93 | this.map = lut.map; 94 | this.n = lut.n; 95 | this.minV = lut.minV; 96 | this.maxV = lut.maxV; 97 | 98 | return this; 99 | 100 | }, 101 | 102 | getColor: function ( alpha ) { 103 | 104 | 105 | if ( alpha <= this.minV ) { 106 | 107 | alpha = this.minV; 108 | 109 | } 110 | 111 | else if ( alpha >= this.maxV ) { 112 | 113 | alpha = this.maxV; 114 | 115 | } 116 | 117 | 118 | alpha = ( alpha - this.minV ) / ( this.maxV - this.minV ); 119 | 120 | var colorPosition = Math.round ( alpha * this.n ); 121 | colorPosition == this.n ? colorPosition -= 1 : colorPosition; 122 | 123 | return this.lut[ colorPosition ]; 124 | 125 | }, 126 | 127 | addColorMap: function ( colormapName, arrayOfColors ) { 128 | 129 | THREE.ColorMapKeywords[ colormapName ] = arrayOfColors; 130 | 131 | }, 132 | 133 | }; 134 | 135 | THREE.ColorMapKeywords = { 136 | 137 | "rainbow": [ [ 0.0, '0x0000FF' ], [ 0.2, '0x00FFFF' ], [ 0.5, '0x00FF00' ], [ 0.8, '0xFFFF00'], [1.0, '0xFF0000' ] ], 138 | "cooltowarm": [ [ 0.0, '0x3C4EC2' ], [ 0.2, '0x9BBCFF' ], [ 0.5, '0xDCDCDC' ], [ 0.8, '0xF6A385'], [1.0, '0xB40426' ] ], 139 | "blackbody" : [ [ 0.0, '0x000000' ], [ 0.2, '0x780000' ], [ 0.5, '0xE63200' ], [ 0.8, '0xFFFF00'], [1.0, '0xFFFFFF' ] ], 140 | "picm" : [ [ 0.0, '0x000000' ], [ 0.10, '0xff00ff' ], [ 0.2, '0x0000ff' ], [ 0.4, '0x00FFFF'], [0.65, '0x00FF00' ], [0.8, '0xFF0000' ], [0.95, '0xFFFF00' ], [1.0, '0xFFFFFF' ]] 141 | 142 | } 143 | -------------------------------------------------------------------------------- /audio.js: -------------------------------------------------------------------------------- 1 | navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; 2 | window.URL = window.URL || window.webkitURL || window.mozURL || window.msURL; 3 | window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext; 4 | 5 | if ( ! Detector.webgl ) Detector.addGetWebGLMessage(); 6 | 7 | function initialize() { 8 | var container, stats; 9 | var camera, scene, renderer, controls; 10 | var mesh; 11 | var particleSystem; 12 | 13 | var audioElement = document.getElementById("audio"); 14 | var container = document.getElementById('container'); 15 | 16 | var lut = new THREE.Lut( "picm", 512 ); 17 | //var lut = new THREE.Lut( "blackbody", 512 ); 18 | //var lut = new THREE.Lut( "cooltowarm", 512 ); 19 | //var lut = new THREE.Lut( "rainbow", 512 ); 20 | lut.setMax(100); 21 | 22 | function render() { 23 | //var time = Date.now() * 0.001; 24 | //particleSystem.rotation.x = time * 0.25; 25 | //particleSystem.rotation.y = time * 0.5; 26 | renderer.render( scene, camera ); 27 | stats.update(); 28 | } 29 | 30 | function onWindowResize() { 31 | camera.aspect = window.innerWidth / window.innerHeight; 32 | camera.updateProjectionMatrix(); 33 | renderer.setSize( window.innerWidth, window.innerHeight ); 34 | controls.handleResize(); 35 | render(); 36 | } 37 | 38 | function createPlot(frequencyData) { 39 | var z = 500.0; 40 | var l = frequencyData.length; 41 | 42 | var positions = new THREE.Float32Attribute(l, 3); 43 | var colors = new THREE.Float32Attribute(l, 3); 44 | for (var i = 0; i < l; i++) { 45 | var x = i - l/2; 46 | var y = frequencyData[i]; 47 | positions.setXYZ(i, x, y, z); 48 | var color = lut.getColor(y); 49 | colors.setXYZ(i, color.r, color.g, color.b); 50 | } 51 | var geometry = new THREE.BufferGeometry(); 52 | var material = new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, opacity: 0.7 }); 53 | geometry.addAttribute('position', positions); 54 | geometry.addAttribute('color', colors); 55 | geometry.computeBoundingSphere(); 56 | return new THREE.Line(geometry, material); 57 | } 58 | 59 | camera = new THREE.PerspectiveCamera( 27, window.innerWidth / window.innerHeight, 5, 100000 ); 60 | camera.position.z = 2000; 61 | camera.position.y = 500; 62 | 63 | controls = new THREE.TrackballControls( camera ); 64 | controls.staticMoving = true; 65 | controls.dynamicDampingFactor = 0.3; 66 | controls.keys = [ 65, 83, 68 ]; 67 | controls.addEventListener( 'change', render ); 68 | 69 | renderer = new THREE.WebGLRenderer( { antialias: false, clearColor: 0x333333, clearAlpha: 1, alpha: false } ); 70 | renderer.setSize( window.innerWidth, window.innerHeight ); 71 | 72 | container.appendChild( renderer.domElement ); 73 | 74 | stats = new Stats(); 75 | stats.domElement.style.position = 'absolute'; 76 | stats.domElement.style.top = '0px'; 77 | container.appendChild( stats.domElement ); 78 | 79 | window.addEventListener( 'resize', onWindowResize, false ); 80 | 81 | navigator.getUserMedia( 82 | {audio : true}, 83 | function(stream) { 84 | var url = URL.createObjectURL(stream); 85 | audioElement.src = url; 86 | var audioContext = new AudioContext(); 87 | var mediastreamsource = audioContext.createMediaStreamSource(stream); 88 | var analyser = audioContext.createAnalyser(); 89 | var frequencyData = new Uint8Array(analyser.frequencyBinCount); 90 | //var timeDomainData = new Uint8Array(analyser.frequencyBinCount); 91 | mediastreamsource.connect(analyser); 92 | 93 | var animation = function(){ 94 | analyser.getByteFrequencyData(frequencyData); 95 | //analyser.getByteTimeDomainData(timeDomainData); 96 | 97 | //scene = new THREE.Scene(); 98 | var i; 99 | for (i = 0; i < scene.children.length - 200; i++) { 100 | scene.remove(scene.children[i]); 101 | } 102 | for (; i < scene.children.length; i++) { 103 | scene.children[i].translateZ(-5); 104 | } 105 | particleSystem = createPlot(frequencyData); 106 | scene.add( particleSystem ); 107 | render(); 108 | controls.update(); 109 | 110 | requestAnimationFrame(animation); 111 | }; 112 | 113 | scene = new THREE.Scene(); 114 | //scene.fog = new THREE.Fog( 0x000000, 3000, 5000); 115 | scene.translateZ(-500); 116 | animation(); 117 | }, 118 | function(e) { 119 | console.log(e); 120 | } 121 | ); 122 | } 123 | 124 | window.addEventListener("load", initialize, false); 125 | -------------------------------------------------------------------------------- /spectrum.js: -------------------------------------------------------------------------------- 1 | 2 | if ( ! Detector.webgl ) Detector.addGetWebGLMessage(); 3 | 4 | function initialize() { 5 | var container, stats; 6 | var camera, scene, renderer, controls; 7 | var mesh; 8 | var particleSystem; 9 | 10 | var audioElement = document.getElementById("audio"); 11 | var container = document.getElementById('container'); 12 | var radio = new RadioController(); 13 | 14 | var lut = new THREE.Lut( "picm", 512 ); 15 | //var lut = new THREE.Lut( "blackbody", 512 ); 16 | //var lut = new THREE.Lut( "cooltowarm", 512 ); 17 | //var lut = new THREE.Lut( "rainbow", 512 ); 18 | lut.setMax(100); 19 | 20 | function render() { 21 | //var time = Date.now() * 0.001; 22 | //particleSystem.rotation.x = time * 0.25; 23 | //particleSystem.rotation.y = time * 0.5; 24 | renderer.render( scene, camera ); 25 | stats.update(); 26 | } 27 | 28 | function onWindowResize() { 29 | camera.aspect = window.innerWidth / window.innerHeight; 30 | camera.updateProjectionMatrix(); 31 | renderer.setSize( window.innerWidth, window.innerHeight ); 32 | controls.handleResize(); 33 | render(); 34 | } 35 | 36 | function createParticleSystem(data) { 37 | var z = 500.0; 38 | var pts = []; 39 | var l = data.length; 40 | if (l > 4096) 41 | l = 4096; 42 | 43 | 44 | 45 | var positions = new THREE.Float32Attribute(l, 3); 46 | var colors = new THREE.Float32Attribute(l, 3); 47 | for (var i = 0; i < l/2; i++) { 48 | var x = i - l/2; 49 | var y = data[i + l/2]; 50 | positions.setXYZ(i, x, y, z); 51 | var color = lut.getColor(y); 52 | colors.setXYZ(i, color.r, color.g, color.b); 53 | } 54 | for ( ; i < l; i++) { 55 | var x = i - l/2; 56 | var y = data[i - l/2]; 57 | positions.setXYZ(i, x, y, z); 58 | var color = lut.getColor(y); 59 | colors.setXYZ(i, color.r, color.g, color.b); 60 | } 61 | var geometry = new THREE.BufferGeometry(); 62 | var material = new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, opacity: 0.7 }); 63 | geometry.addAttribute('position', positions); 64 | geometry.addAttribute('color', colors); 65 | geometry.computeBoundingSphere(); 66 | return new THREE.Line(geometry, material); 67 | } 68 | 69 | function fft(samples) { 70 | var data = new complex_array.ComplexArray(4096); 71 | data.map(function(value, i, n) { 72 | value.real = samples[i * 2 + 1] - 128; 73 | value.imag = samples[i * 2] - 128; 74 | //console.log(samples[i * 2]); 75 | }); 76 | data.FFT(); 77 | return data.magnitude(); 78 | } 79 | 80 | camera = new THREE.PerspectiveCamera( 27, window.innerWidth / window.innerHeight, 5, 100000 ); 81 | camera.position.z = 2000; 82 | camera.position.y = 500; 83 | 84 | controls = new THREE.TrackballControls( camera ); 85 | controls.staticMoving = true; 86 | controls.dynamicDampingFactor = 0.3; 87 | controls.keys = [ 65, 83, 68 ]; 88 | controls.addEventListener( 'change', render ); 89 | 90 | renderer = new THREE.WebGLRenderer( { antialias: false, clearColor: 0x333333, clearAlpha: 1, alpha: false } ); 91 | renderer.setSize( window.innerWidth, window.innerHeight ); 92 | 93 | container.appendChild( renderer.domElement ); 94 | 95 | stats = new Stats(); 96 | stats.domElement.style.position = 'absolute'; 97 | stats.domElement.style.top = '0px'; 98 | container.appendChild( stats.domElement ); 99 | 100 | window.addEventListener( 'resize', onWindowResize, false ); 101 | 102 | var animation = function() { 103 | samples = radio.getSamples(); 104 | if (radio.isPlaying() && samples) { 105 | //console.log(samples.length); 106 | var i; 107 | for (i = 0; i < scene.children.length - 400; i++) { 108 | scene.remove(scene.children[i]); 109 | } 110 | for (; i < scene.children.length; i++) { 111 | scene.children[i].translateZ(-5); 112 | } 113 | 114 | data = fft(new Uint8Array(samples)); 115 | //console.log(data); 116 | particleSystem = createParticleSystem(data); 117 | scene.add( particleSystem ); 118 | } 119 | render(); 120 | controls.update(); 121 | 122 | requestAnimationFrame(animation); 123 | }; 124 | 125 | scene = new THREE.Scene(); 126 | //scene.fog = new THREE.Fog( 0x000000, 3000, 5000); 127 | scene.translateZ(-500); 128 | 129 | function start() { 130 | radio.start(function() { 131 | animation(); 132 | }); 133 | } 134 | function stop() { 135 | radio.stop(); 136 | } 137 | function changeFrequency() { 138 | radio.setFrequency(parseFloat(frequencyField.value) * 1e6); 139 | } 140 | function updateFrequency() { 141 | frequencyField.value = radio.getFrequency() / 1e6; 142 | } 143 | 144 | startButton.addEventListener('click', start); 145 | stopButton.addEventListener('click', stop); 146 | frequencyField.addEventListener('focus', updateFrequency); 147 | frequencyField.addEventListener('change', changeFrequency); 148 | } 149 | 150 | window.addEventListener("load", initialize, false); 151 | -------------------------------------------------------------------------------- /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. 186 | */ 187 | function setCenterFrequency(freq, kont) { 188 | com.i2c.open(function() { 189 | tuner.setFrequency(freq + IF_FREQ, function() { 190 | com.i2c.close(kont); 191 | })}); 192 | } 193 | 194 | /** 195 | * Resets the sample buffer. Call this before starting to read samples. 196 | * @param {Function} kont The continuation for this function. 197 | */ 198 | function resetBuffer(kont) { 199 | com.writeEach([ 200 | [CMD.REG, BLOCK.USB, REG.EPA_CTL, 0x0210, 2], 201 | [CMD.REG, BLOCK.USB, REG.EPA_CTL, 0x0000, 2] 202 | ], kont); 203 | } 204 | 205 | /** 206 | * Reads a block of samples off the device. 207 | * @param {number} length The number of samples to read. 208 | * @param {Function} kont The continuation for this function. It will receive 209 | * as its argument an ArrayBuffer containing the read samples, which you 210 | * can interpret as pairs of unsigned 8-bit integers; the first one is 211 | * the sample's I value, and the second one is its Q value. 212 | */ 213 | function readSamples(length, kont) { 214 | com.bulk.readBuffer(length * BYTES_PER_SAMPLE, kont); 215 | } 216 | 217 | /** 218 | * Stops the demodulator. 219 | * @param {Function} kont The continuation for this function. 220 | */ 221 | function close(kont) { 222 | com.i2c.open(function() { 223 | tuner.close(function() { 224 | com.i2c.close(function() { 225 | com.iface.release(kont); 226 | })})}); 227 | } 228 | 229 | /** 230 | * Handles an error. 231 | * @param {string} msg The error message. 232 | */ 233 | function throwError(msg) { 234 | if (errorHandler) { 235 | errorHandler(msg); 236 | } else { 237 | throw msg; 238 | } 239 | } 240 | 241 | /** 242 | * Sets a function to call if there's an error communicating with the 243 | * demodulator. 244 | * @param {Function} func The function to call on error. 245 | */ 246 | function setOnError(func) { 247 | errorHandler = func; 248 | com.setOnError(throwError); 249 | } 250 | 251 | return { 252 | open: open, 253 | setSampleRate: setSampleRate, 254 | setCenterFrequency: setCenterFrequency, 255 | resetBuffer: resetBuffer, 256 | readSamples: readSamples, 257 | close: close, 258 | setOnError: setOnError 259 | }; 260 | } 261 | 262 | -------------------------------------------------------------------------------- /radiocontroller.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 | * High-level radio control functions. 17 | * @constructor 18 | */ 19 | function RadioController() { 20 | 21 | var TUNERS = [{'vendorId': 0x0bda, 'productId': 0x2832}, 22 | {'vendorId': 0x0bda, 'productId': 0x2838}]; 23 | //var SAMPLE_RATE = 1024000; // Must be a multiple of 512 * BUFS_PER_SEC 24 | var SAMPLE_RATE = 2048000; 25 | var BUFS_PER_SEC = 100; 26 | var SAMPLES_PER_BUF = Math.floor(SAMPLE_RATE / BUFS_PER_SEC); 27 | var NULL_FUNC = function(){}; 28 | var STATE = { 29 | OFF: 0, 30 | STARTING: 1, 31 | PLAYING: 2, 32 | STOPPING: 3, 33 | CHG_FREQ: 4 34 | }; 35 | var SUBSTATE = { 36 | USB: 1, 37 | TUNER: 2, 38 | ALL_ON: 3, 39 | TUNING: 4 40 | }; 41 | 42 | var state = new State(STATE.OFF); 43 | var requestingBlocks = 0; 44 | var playingBlocks = 0; 45 | var mode = {}; 46 | //var frequency = 832500000; 47 | //var frequency = 837500000; 48 | //var frequency = 80400000; 49 | var frequency = 82500000; 50 | var ppm = 0; 51 | var actualPpm = 0; 52 | var offsetCount = -1; 53 | var offsetSum = 0; 54 | var autoGain = true; 55 | var gain = 0; 56 | var errorHandler; 57 | var tuner; 58 | var connection; 59 | var ui; 60 | var samples; 61 | 62 | /** 63 | * Starts playing the radio. 64 | * @param {Function=} opt_callback A function to call when the radio 65 | * starts playing. 66 | */ 67 | function start(opt_callback) { 68 | if (state.state == STATE.OFF) { 69 | state = new State(STATE.STARTING, SUBSTATE.USB, opt_callback); 70 | chrome.permissions.request( 71 | {'permissions': [{'usbDevices': TUNERS}]}, 72 | function(res) { 73 | if (!res) { 74 | state = new State(STATE.OFF); 75 | throwError('This app has no permission to access the USB ports.'); 76 | } else { 77 | processState(); 78 | } 79 | }); 80 | } else if (state.state == STATE.STOPPING || state.state == STATE.STARTING) { 81 | state = new State(STATE.STARTING, state.substate, opt_callback); 82 | } 83 | } 84 | 85 | /** 86 | * Stops playing the radio. 87 | * @param {Function=} opt_callback A function to call after the radio 88 | * stops playing. 89 | */ 90 | function stop(opt_callback) { 91 | if (state.state == STATE.OFF) { 92 | opt_callback && opt_callback(); 93 | } else if (state.state == STATE.STARTING || state.state == STATE.STOPPING) { 94 | state = new State(STATE.STOPPING, state.substate, opt_callback); 95 | } else if (state.state != STATE.STOPPING) { 96 | state = new State(STATE.STOPPING, SUBSTATE.ALL_ON, opt_callback); 97 | } 98 | } 99 | 100 | /** 101 | * Tunes to another frequency. 102 | * @param {number} freq The new frequency in Hz. 103 | */ 104 | function setFrequency(freq) { 105 | if (state.state == STATE.PLAYING || state.state == STATE.CHG_FREQ) { 106 | state = new State(STATE.CHG_FREQ, null, freq); 107 | } else { 108 | frequency = freq; 109 | ui && ui.update(); 110 | } 111 | } 112 | 113 | /** 114 | * Returns the currently tuned frequency. 115 | * @return {number} The current frequency in Hz. 116 | */ 117 | function getFrequency() { 118 | return frequency; 119 | } 120 | 121 | /** 122 | * Returns whether the radio is currently playing. 123 | * @param {boolean} Whether the radio is currently playing. 124 | */ 125 | function isPlaying() { 126 | return state.state != STATE.OFF && state.state != STATE.STOPPING; 127 | } 128 | 129 | /** 130 | * Returns whether the radio is currently stopping. 131 | * @param {boolean} Whether the radio is currently stopping. 132 | */ 133 | function isStopping() { 134 | return state.state == STATE.STOPPING; 135 | } 136 | 137 | /** 138 | * Sets automatic tuner gain. 139 | */ 140 | function setAutoGain() { 141 | autoGain = true; 142 | } 143 | 144 | /** 145 | * Sets a particular tuner gain. 146 | * @param {number} gain The tuner gain in dB. 147 | */ 148 | function setManualGain(newGain) { 149 | autoGain = false; 150 | if (newGain < 0) { 151 | gain = 0; 152 | } else if (newGain > 47.4) { 153 | gain = 47.4; 154 | } else { 155 | gain = newGain; 156 | } 157 | } 158 | 159 | /** 160 | * Returns whether automatic gain is currently set. 161 | */ 162 | function isAutoGain() { 163 | return autoGain; 164 | } 165 | 166 | /** 167 | * Returns the currently-set manual gain in dB. 168 | */ 169 | function getManualGain() { 170 | return gain; 171 | } 172 | 173 | /** 174 | * Saves a reference to the current user interface controller. 175 | * @param {Object} iface The controller. Must have an update() method. 176 | */ 177 | function setInterface(iface) { 178 | ui = iface; 179 | } 180 | 181 | function getSamples() { 182 | return samples; 183 | } 184 | 185 | /** 186 | * Sets a function to be called when there is an error. 187 | * @param {Function} handler The function to call. Its only parameter 188 | * is the error message. 189 | */ 190 | function setOnError(handler) { 191 | errorHandler = handler; 192 | } 193 | 194 | /** 195 | * Handles an error. 196 | * @param {string} msg The error message. 197 | */ 198 | function throwError(msg) { 199 | if (errorHandler) { 200 | errorHandler(msg); 201 | } else { 202 | throw msg; 203 | } 204 | } 205 | 206 | /** 207 | * Starts the decoding pipeline. 208 | */ 209 | function startPipeline() { 210 | // In this way we read one block while we decode and play another. 211 | if (state.state == STATE.PLAYING) { 212 | processState(); 213 | } 214 | processState(); 215 | } 216 | 217 | /** 218 | * Performs the appropriate action according to the current state. 219 | */ 220 | function processState() { 221 | switch (state.state) { 222 | case STATE.STARTING: 223 | return stateStarting(); 224 | case STATE.PLAYING: 225 | return statePlaying(); 226 | case STATE.CHG_FREQ: 227 | return stateChangeFrequency(); 228 | case STATE.STOPPING: 229 | return stateStopping(); 230 | } 231 | } 232 | 233 | /** 234 | * STARTING state. Initializes the tuner and starts the decoding pipeline. 235 | * 236 | * This state has several substates: USB (when it needs to acquire and 237 | * initialize the USB device), TUNER (needs to set the sample rate and 238 | * tuned frequency), and ALL_ON (needs to start the decoding pipeline). 239 | * 240 | * At the last substate it transitions into the PLAYING state. 241 | */ 242 | function stateStarting() { 243 | if (state.substate == SUBSTATE.USB) { 244 | state = new State(STATE.STARTING, SUBSTATE.TUNER, state.param); 245 | doFindDevices(0); 246 | } else if (state.substate == SUBSTATE.TUNER) { 247 | state = new State(STATE.STARTING, SUBSTATE.ALL_ON, state.param); 248 | actualPpm = ppm; 249 | tuner = new RTL2832U(connection, actualPpm, autoGain ? null : gain); 250 | tuner.setOnError(throwError); 251 | tuner.open(function() { 252 | tuner.setSampleRate(SAMPLE_RATE, function(rate) { 253 | offsetSum = 0; 254 | offsetCount = -1; 255 | tuner.setCenterFrequency(frequency, function() { 256 | processState(); 257 | })})}); 258 | } else if (state.substate == SUBSTATE.ALL_ON) { 259 | var cb = state.param; 260 | state = new State(STATE.PLAYING); 261 | tuner.resetBuffer(function() { 262 | cb && cb(); 263 | ui && ui.update(); 264 | startPipeline(); 265 | }); 266 | } 267 | } 268 | 269 | /** 270 | * Finds the first matching tuner USB device in the tuner device definition 271 | * list and transitions to the next substate. 272 | * @param {number} index The first element in the list to find. 273 | */ 274 | function doFindDevices(index) { 275 | if (index == TUNERS.length) { 276 | state = new State(STATE.OFF); 277 | throwError('USB tuner device not found. The Radio Receiver ' + 278 | 'app needs an RTL2832U-based DVB-T dongle ' + 279 | '(with an R820T tuner chip) to work.'); 280 | } else { 281 | chrome.usb.findDevices(TUNERS[index], 282 | function(conns) { 283 | if (conns.length == 0) { 284 | doFindDevices(index + 1); 285 | } else { 286 | connection = conns[0]; 287 | processState(); 288 | } 289 | }); 290 | } 291 | } 292 | 293 | /** 294 | * PLAYING state. Reads a block of samples from the tuner and plays it. 295 | * 296 | * 2 blocks are in flight all at times, so while one block is being 297 | * demodulated and played, the next one is already being sampled. 298 | */ 299 | function statePlaying() { 300 | ++requestingBlocks; 301 | tuner.readSamples(SAMPLES_PER_BUF, function(data) { 302 | --requestingBlocks; 303 | if (state.state == STATE.PLAYING) { 304 | //if (playingBlocks <= 2) { 305 | //++playingBlocks; 306 | //decoder.postMessage([0, data, stereoEnabled], [data]); 307 | samples = data; 308 | //} 309 | } 310 | processState(); 311 | }); 312 | } 313 | 314 | /** 315 | * CHG_FREQ state. Changes tuned frequency. 316 | * 317 | * First it waits until all in-flight blocks have been dealt with. When 318 | * there are no more in-flight blocks it sets the new frequency, resets 319 | * the buffer and transitions into the PLAYING state. 320 | */ 321 | function stateChangeFrequency() { 322 | if (requestingBlocks > 0) { 323 | return; 324 | } 325 | frequency = state.param; 326 | ui && ui.update(); 327 | offsetSum = 0; 328 | offsetCount = -1; 329 | tuner.setCenterFrequency(frequency, function() { 330 | tuner.resetBuffer(function() { 331 | state = new State(STATE.PLAYING); 332 | startPipeline(); 333 | })}); 334 | } 335 | 336 | /** 337 | * STOPPING state. Stops playing and shuts the tuner down. 338 | * 339 | * This state has several substates: ALL_ON (when it needs to wait until 340 | * all in-flight blocks have been vacated and close the tuner), TUNER (when 341 | * it has closed the tuner and needs to close the USB device), and USB (when 342 | * it has closed the USB device). After the USB substate it will transition 343 | * to the OFF state. 344 | */ 345 | function stateStopping() { 346 | if (state.substate == SUBSTATE.ALL_ON) { 347 | if (requestingBlocks > 0) { 348 | return; 349 | } 350 | state = new State(STATE.STOPPING, SUBSTATE.TUNER, state.param); 351 | ui && ui.update(); 352 | tuner.close(function() { 353 | processState(); 354 | }); 355 | } else if (state.substate == SUBSTATE.TUNER) { 356 | state = new State(STATE.STOPPING, SUBSTATE.USB, state.param); 357 | chrome.usb.closeDevice(connection, function() { 358 | processState(); 359 | }); 360 | } else if (state.substate == SUBSTATE.USB) { 361 | var cb = state.param; 362 | state = new State(STATE.OFF); 363 | cb && cb(); 364 | samples = null; 365 | ui && ui.update(); 366 | } 367 | } 368 | 369 | /** 370 | * Constructs a state object. 371 | * @param {number} state The state. 372 | * @param {number=} opt_substate The sub-state. 373 | * @param {*=} opt_param The state's parameter. 374 | */ 375 | function State(state, opt_substate, opt_param) { 376 | return { 377 | state: state, 378 | substate: opt_substate, 379 | param: opt_param 380 | }; 381 | } 382 | 383 | return { 384 | start: start, 385 | stop: stop, 386 | setFrequency: setFrequency, 387 | getFrequency: getFrequency, 388 | isPlaying: isPlaying, 389 | isStopping: isStopping, 390 | setAutoGain: setAutoGain, 391 | setManualGain: setManualGain, 392 | isAutoGain: isAutoGain, 393 | getManualGain: getManualGain, 394 | setInterface: setInterface, 395 | getSamples: getSamples, 396 | setOnError: setOnError 397 | }; 398 | } 399 | -------------------------------------------------------------------------------- /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. 83 | */ 84 | function setFrequency(freq, kont) { 85 | setMux(freq, function() { 86 | setPll(freq, kont); 87 | }); 88 | } 89 | 90 | /** 91 | * Stops the tuner. 92 | * @param {Function} kont The continuation for this function. 93 | */ 94 | function close(kont) { 95 | writeEach([ 96 | [0x06, 0xb1, 0xff], 97 | [0x05, 0xb3, 0xff], 98 | [0x07, 0x3a, 0xff], 99 | [0x08, 0x40, 0xff], 100 | [0x09, 0xc0, 0xff], 101 | [0x0a, 0x36, 0xff], 102 | [0x0c, 0x35, 0xff], 103 | [0x0f, 0x68, 0xff], 104 | [0x11, 0x03, 0xff], 105 | [0x17, 0xf4, 0xff], 106 | [0x19, 0x0c, 0xff] 107 | ], kont); 108 | } 109 | 110 | /** 111 | * Initializes all the components of the tuner. 112 | * @param {Function} kont The continuation for this function. 113 | */ 114 | function initElectronics(kont) { 115 | writeEach([ 116 | [0x0c, 0x00, 0x0f], 117 | [0x13, 49, 0x3f], 118 | [0x1d, 0x00, 0x38] 119 | ], function() { 120 | calibrateFilter(true, function(filterCap) { 121 | writeEach([ 122 | [0x0a, 0x10 | filterCap, 0x1f], 123 | [0x0b, 0x6b, 0xef], 124 | [0x07, 0x00, 0x80], 125 | [0x06, 0x10, 0x30], 126 | [0x1e, 0x40, 0x60], 127 | [0x05, 0x00, 0x80], 128 | [0x1f, 0x00, 0x80], 129 | [0x0f, 0x00, 0x80], 130 | [0x19, 0x60, 0x60], 131 | [0x1d, 0xe5, 0xc7], 132 | [0x1c, 0x24, 0xf8], 133 | [0x0d, 0x53, 0xff], 134 | [0x0e, 0x75, 0xff], 135 | [0x05, 0x00, 0x60], 136 | [0x06, 0x00, 0x08], 137 | [0x11, 0x38, 0x08], 138 | [0x17, 0x30, 0x30], 139 | [0x0a, 0x40, 0x60], 140 | [0x1d, 0x00, 0x38], 141 | [0x1c, 0x00, 0x04], 142 | [0x06, 0x00, 0x40], 143 | [0x1a, 0x30, 0x30], 144 | [0x1d, 0x18, 0x38], 145 | [0x1c, 0x24, 0x04], 146 | [0x1e, 0x0d, 0x1f], 147 | [0x1a, 0x20, 0x30] 148 | ], kont); 149 | })}); 150 | } 151 | 152 | /** 153 | * Sets the tuner to automatic gain. 154 | * @param {Function} kont The continuation for this function. 155 | */ 156 | function setAutoGain(kont) { 157 | writeEach([ 158 | [0x05, 0x00, 0x10], 159 | [0x07, 0x10, 0x10], 160 | [0x0c, 0x0b, 0x9f] 161 | ], kont); 162 | } 163 | 164 | /** 165 | * Sets the tuner's manual gain. 166 | * @param {number} gain The tuner's gain, in dB. 167 | * @param {Function} kont The continuation for this function. 168 | */ 169 | function setManualGain(gain, kont) { 170 | var step = 0; 171 | if (gain <= 15) { 172 | step = Math.round(((0.00467 * gain - 0.127) * gain + 1.42) * gain - 0.05); 173 | } else if (gain <= 41.5) { 174 | step = Math.round(((0.00703 * gain - 0.0542) * gain + 1.82) * gain - 8.8); 175 | } else { 176 | step = Math.round(((0.116105 * gain - 15.5431) * gain + 693.409) * gain - 10282.3); 177 | } 178 | if (step < 0) { 179 | step = 0; 180 | } else if (step > 28) { 181 | step = 28; 182 | } 183 | var lnaValue = Math.floor((step + 1) / 2); 184 | var mixerValue = Math.floor(step / 2); 185 | writeEach([ 186 | [0x05, 0x10, 0x10], 187 | [0x07, 0x00, 0x10], 188 | [0x0c, 0x08, 0x9f], 189 | [0x05, lnaValue, 0x0f], 190 | [0x07, mixerValue, 0x0f] 191 | ], kont); 192 | } 193 | 194 | /** 195 | * Calibrates the filters. 196 | * @param {boolean} firstTry Whether this is the first try to calibrate. 197 | * @param {Function} kont The continuation for this function. 198 | */ 199 | function calibrateFilter(firstTry, kont) { 200 | writeEach([ 201 | [0x0b, 0x6b, 0x60], 202 | [0x0f, 0x04, 0x04], 203 | [0x10, 0x00, 0x03] 204 | ], function() { 205 | setPll(56000000, function() { 206 | if (!hasPllLock) { 207 | throwError("PLL not locked -- cannot tune to the selected frequency."); 208 | return; 209 | } 210 | writeEach([ 211 | [0x0b, 0x10, 0x10], 212 | [0x0b, 0x00, 0x10], 213 | [0x0f, 0x00, 0x04] 214 | ], function() { 215 | readRegBuffer(0x00, 5, function(data) { 216 | var arr = new Uint8Array(data); 217 | var filterCap = arr[4] & 0x0f; 218 | if (filterCap == 0x0f) { 219 | filterCap = 0; 220 | } 221 | if (filterCap != 0 && firstTry) { 222 | calibrateFilter(false, kont); 223 | } else { 224 | kont(filterCap); 225 | } 226 | })})})}); 227 | } 228 | 229 | /** 230 | * Sets the multiplexer's frequency. 231 | * @param {number} freq The frequency to set. 232 | * @param {Function} kont The continuation for this function. 233 | */ 234 | function setMux(freq, kont) { 235 | var freqMhz = freq / 1000000; 236 | for (var i = 0; i < MUX_CFGS.length - 1; ++i) { 237 | if (freqMhz < MUX_CFGS[i + 1][0]) { 238 | break; 239 | } 240 | } 241 | var cfg = MUX_CFGS[i]; 242 | writeEach([ 243 | [0x17, cfg[1], 0x08], 244 | [0x1a, cfg[2], 0xc3], 245 | [0x1b, cfg[3], 0xff], 246 | [0x10, 0x00, 0x0b], 247 | [0x08, 0x00, 0x3f], 248 | [0x09, 0x00, 0x3f] 249 | ], kont); 250 | } 251 | 252 | /** 253 | * Sets the PLL's frequency. 254 | * @param {number} freq The frequency to set. 255 | * @param {Function} kont The continuation for this function. 256 | */ 257 | function setPll(freq, kont) { 258 | var freqKhz = Math.round(freq / 1000); 259 | var pllRef = Math.floor(xtalFreq); 260 | var pllRefKhz = Math.round(pllRef / 1000); 261 | writeEach([ 262 | [0x10, 0x00, 0x10], 263 | [0x1a, 0x00, 0x0c], 264 | [0x12, 0x80, 0xe0] 265 | ], function() { 266 | var divNum = Math.min(6, Math.floor(Math.log(1770000 / freqKhz) / Math.LN2)); 267 | var mixDiv = 1 << (divNum + 1); 268 | readRegBuffer(0x00, 5, function(data) { 269 | var arr = new Uint8Array(data); 270 | var vcoFineTune = (arr[4] & 0x30) >> 4; 271 | if (vcoFineTune > 2) { 272 | --divNum; 273 | } else if (vcoFineTune < 2) { 274 | ++divNum; 275 | } 276 | writeRegMask(0x10, divNum << 5, 0xe0, function() { 277 | var vcoFreq = freq * mixDiv; 278 | var nint = Math.floor(vcoFreq / (2 * pllRef)); 279 | var vcoFra = Math.floor((vcoFreq - 2 * pllRef * nint) / 1000); 280 | if (nint > 63) { 281 | throwError("No valid PLL values for " + freq + " Hz"); 282 | return; 283 | } 284 | var ni = Math.floor((nint - 13) / 4); 285 | var si = nint - 4 * ni - 13; 286 | writeEach([ 287 | [0x14, ni + (si << 6), 0xff], 288 | [0x12, vcoFra == 0 ? 0x08 : 0x00, 0x08] 289 | ], function() { 290 | var sdm = Math.min(65535, Math.floor(32768 * vcoFra / pllRefKhz)); 291 | writeEach([ 292 | [0x16, sdm >> 8, 0xff], 293 | [0x15, sdm & 0xff, 0xff] 294 | ], function() { 295 | getPllLock(true, function() { 296 | writeRegMask(0x1a, 0x08, 0x08, kont); 297 | })})})})})}); 298 | } 299 | 300 | /** 301 | * Checks whether the PLL has achieved lock. 302 | * @param {boolean} firstTry Whether this is the first try to achieve lock. 303 | * @param {Function} kont The continuation for this function. 304 | */ 305 | function getPllLock(firstTry, kont) { 306 | readRegBuffer(0x00, 3, function(data) { 307 | var arr = new Uint8Array(data); 308 | if (arr[2] & 0x40) { 309 | hasPllLock = true; 310 | return kont(); 311 | } 312 | if (firstTry) { 313 | writeRegMask(0x12, 0x60, 0xe0, function() { 314 | return getPllLock(false, kont); 315 | }); 316 | } else { 317 | throwError("PLL not locked -- cannot tune to the selected frequency."); 318 | return; 319 | } 320 | }); 321 | } 322 | 323 | /** 324 | * Sets the initial values of the 0x05-0x1f registers. 325 | * @param {Array.} regs The values for the registers. 326 | * @param {Function} kont The continuation for this function. 327 | */ 328 | function initRegisters(regs, kont) { 329 | shadowRegs = new Uint8Array(regs); 330 | var cmds = []; 331 | for (var i = 0; i < regs.length; ++i) { 332 | cmds.push([CMD.I2CREG, 0x34, i + 5, regs[i]]); 333 | } 334 | com.writeEach(cmds, kont); 335 | } 336 | 337 | /** 338 | * Reads a series of registers into a buffer. 339 | * @param {number} addr The first register's address to read. 340 | * @param {number} length The number of registers to read. 341 | * @param {Function} kont The continuation for this function. It receives 342 | * an ArrayBuffer with the data. 343 | */ 344 | function readRegBuffer(addr, length, kont) { 345 | com.i2c.readRegBuffer(0x34, addr, length, function(data) { 346 | var buf = new Uint8Array(data); 347 | for (var i = 0; i < buf.length; ++i) { 348 | var b = buf[i]; 349 | buf[i] = (BIT_REVS[b & 0xf] << 4) | BIT_REVS[b >> 4]; 350 | } 351 | kont(buf.buffer); 352 | }); 353 | } 354 | 355 | /** 356 | * Writes a masked value into a register. 357 | * @param {number} addr The address of the register to write into. 358 | * @param {number} value The value to write. 359 | * @param {number} mask A mask that specifies which bits to write. 360 | * @param {Function} kont The continuation for this function. 361 | */ 362 | function writeRegMask(addr, value, mask, kont) { 363 | var rc = shadowRegs[addr - 5]; 364 | var val = (rc & ~mask) | (value & mask); 365 | shadowRegs[addr - 5] = val; 366 | com.i2c.writeRegister(0x34, addr, val, kont); 367 | } 368 | 369 | /** 370 | * Perform the write operations given in the array. 371 | * @param {Array.>} array The operations. 372 | * @param {Function} kont The continuation for this function. 373 | */ 374 | function writeEach(array, kont) { 375 | var index = 0; 376 | function iterate() { 377 | if (index >= array.length) { 378 | kont(); 379 | } else { 380 | var line = array[index++]; 381 | writeRegMask(line[0], line[1], line[2], iterate); 382 | } 383 | } 384 | iterate(); 385 | } 386 | 387 | return { 388 | init: init, 389 | setFrequency: setFrequency, 390 | setAutoGain: setAutoGain, 391 | setManualGain: setManualGain, 392 | close: close 393 | }; 394 | } 395 | 396 | /** 397 | * Checks if the R820T tuner is present. 398 | * @param {RtlCom} com The RTL communications object. 399 | * @param {Function} kont The continuation for this function. It receives 400 | * a boolean that tells whether the tuner is present. 401 | */ 402 | R820T.check = function (com, kont) { 403 | com.i2c.readRegister(0x34, 0, function(data) { 404 | kont(data == 0x69); 405 | }); 406 | }; 407 | 408 | -------------------------------------------------------------------------------- /js/TrackballControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Eberhard Graether / http://egraether.com/ 3 | */ 4 | 5 | THREE.TrackballControls = function ( object, domElement ) { 6 | 7 | var _this = this; 8 | var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM: 4, TOUCH_PAN: 5 }; 9 | 10 | this.object = object; 11 | this.domElement = ( domElement !== undefined ) ? domElement : document; 12 | 13 | // API 14 | 15 | this.enabled = true; 16 | 17 | this.screen = { width: 0, height: 0, offsetLeft: 0, offsetTop: 0 }; 18 | this.radius = ( this.screen.width + this.screen.height ) / 4; 19 | 20 | this.rotateSpeed = 1.0; 21 | this.zoomSpeed = 1.2; 22 | this.panSpeed = 0.3; 23 | 24 | this.noRotate = false; 25 | this.noZoom = false; 26 | this.noPan = false; 27 | 28 | this.staticMoving = false; 29 | this.dynamicDampingFactor = 0.2; 30 | 31 | this.minDistance = 0; 32 | this.maxDistance = Infinity; 33 | 34 | this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ]; 35 | 36 | // internals 37 | 38 | this.target = new THREE.Vector3(); 39 | 40 | var lastPosition = new THREE.Vector3(); 41 | 42 | var _state = STATE.NONE, 43 | _prevState = STATE.NONE, 44 | 45 | _eye = new THREE.Vector3(), 46 | 47 | _rotateStart = new THREE.Vector3(), 48 | _rotateEnd = new THREE.Vector3(), 49 | 50 | _zoomStart = new THREE.Vector2(), 51 | _zoomEnd = new THREE.Vector2(), 52 | 53 | _touchZoomDistanceStart = 0, 54 | _touchZoomDistanceEnd = 0, 55 | 56 | _panStart = new THREE.Vector2(), 57 | _panEnd = new THREE.Vector2(); 58 | 59 | // for reset 60 | 61 | this.target0 = this.target.clone(); 62 | this.position0 = this.object.position.clone(); 63 | this.up0 = this.object.up.clone(); 64 | 65 | // events 66 | 67 | var changeEvent = { type: 'change' }; 68 | 69 | 70 | // methods 71 | 72 | this.handleResize = function () { 73 | 74 | this.screen.width = window.innerWidth; 75 | this.screen.height = window.innerHeight; 76 | 77 | this.screen.offsetLeft = 0; 78 | this.screen.offsetTop = 0; 79 | 80 | this.radius = ( this.screen.width + this.screen.height ) / 4; 81 | 82 | }; 83 | 84 | this.handleEvent = function ( event ) { 85 | 86 | if ( typeof this[ event.type ] == 'function' ) { 87 | 88 | this[ event.type ]( event ); 89 | 90 | } 91 | 92 | }; 93 | 94 | this.getMouseOnScreen = function ( clientX, clientY ) { 95 | 96 | return new THREE.Vector2( 97 | ( clientX - _this.screen.offsetLeft ) / _this.radius * 0.5, 98 | ( clientY - _this.screen.offsetTop ) / _this.radius * 0.5 99 | ); 100 | 101 | }; 102 | 103 | this.getMouseProjectionOnBall = function ( clientX, clientY ) { 104 | 105 | var mouseOnBall = new THREE.Vector3( 106 | ( clientX - _this.screen.width * 0.5 - _this.screen.offsetLeft ) / _this.radius, 107 | ( _this.screen.height * 0.5 + _this.screen.offsetTop - clientY ) / _this.radius, 108 | 0.0 109 | ); 110 | 111 | var length = mouseOnBall.length(); 112 | 113 | if ( length > 1.0 ) { 114 | 115 | mouseOnBall.normalize(); 116 | 117 | } else { 118 | 119 | mouseOnBall.z = Math.sqrt( 1.0 - length * length ); 120 | 121 | } 122 | 123 | _eye.copy( _this.object.position ).sub( _this.target ); 124 | 125 | var projection = _this.object.up.clone().setLength( mouseOnBall.y ); 126 | projection.add( _this.object.up.clone().cross( _eye ).setLength( mouseOnBall.x ) ); 127 | projection.add( _eye.setLength( mouseOnBall.z ) ); 128 | 129 | return projection; 130 | 131 | }; 132 | 133 | this.rotateCamera = function () { 134 | 135 | var angle = Math.acos( _rotateStart.dot( _rotateEnd ) / _rotateStart.length() / _rotateEnd.length() ); 136 | 137 | if ( angle ) { 138 | 139 | var axis = ( new THREE.Vector3() ).crossVectors( _rotateStart, _rotateEnd ).normalize(), 140 | quaternion = new THREE.Quaternion(); 141 | 142 | angle *= _this.rotateSpeed; 143 | 144 | quaternion.setFromAxisAngle( axis, -angle ); 145 | 146 | _eye.applyQuaternion( quaternion ); 147 | _this.object.up.applyQuaternion( quaternion ); 148 | 149 | _rotateEnd.applyQuaternion( quaternion ); 150 | 151 | if ( _this.staticMoving ) { 152 | 153 | _rotateStart.copy( _rotateEnd ); 154 | 155 | } else { 156 | 157 | quaternion.setFromAxisAngle( axis, angle * ( _this.dynamicDampingFactor - 1.0 ) ); 158 | _rotateStart.applyQuaternion( quaternion ); 159 | 160 | } 161 | 162 | } 163 | 164 | }; 165 | 166 | this.zoomCamera = function () { 167 | 168 | if ( _state === STATE.TOUCH_ZOOM ) { 169 | 170 | var factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; 171 | _touchZoomDistanceStart = _touchZoomDistanceEnd; 172 | _eye.multiplyScalar( factor ); 173 | 174 | } else { 175 | 176 | var factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed; 177 | 178 | if ( factor !== 1.0 && factor > 0.0 ) { 179 | 180 | _eye.multiplyScalar( factor ); 181 | 182 | if ( _this.staticMoving ) { 183 | 184 | _zoomStart.copy( _zoomEnd ); 185 | 186 | } else { 187 | 188 | _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor; 189 | 190 | } 191 | 192 | } 193 | 194 | } 195 | 196 | }; 197 | 198 | this.panCamera = function () { 199 | 200 | var mouseChange = _panEnd.clone().sub( _panStart ); 201 | 202 | if ( mouseChange.lengthSq() ) { 203 | 204 | mouseChange.multiplyScalar( _eye.length() * _this.panSpeed ); 205 | 206 | var pan = _eye.clone().cross( _this.object.up ).setLength( mouseChange.x ); 207 | pan.add( _this.object.up.clone().setLength( mouseChange.y ) ); 208 | 209 | _this.object.position.add( pan ); 210 | _this.target.add( pan ); 211 | 212 | if ( _this.staticMoving ) { 213 | 214 | _panStart = _panEnd; 215 | 216 | } else { 217 | 218 | _panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) ); 219 | 220 | } 221 | 222 | } 223 | 224 | }; 225 | 226 | this.checkDistances = function () { 227 | 228 | if ( !_this.noZoom || !_this.noPan ) { 229 | 230 | if ( _this.object.position.lengthSq() > _this.maxDistance * _this.maxDistance ) { 231 | 232 | _this.object.position.setLength( _this.maxDistance ); 233 | 234 | } 235 | 236 | if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) { 237 | 238 | _this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) ); 239 | 240 | } 241 | 242 | } 243 | 244 | }; 245 | 246 | this.update = function () { 247 | 248 | _eye.subVectors( _this.object.position, _this.target ); 249 | 250 | if ( !_this.noRotate ) { 251 | 252 | _this.rotateCamera(); 253 | 254 | } 255 | 256 | if ( !_this.noZoom ) { 257 | 258 | _this.zoomCamera(); 259 | 260 | } 261 | 262 | if ( !_this.noPan ) { 263 | 264 | _this.panCamera(); 265 | 266 | } 267 | 268 | _this.object.position.addVectors( _this.target, _eye ); 269 | 270 | _this.checkDistances(); 271 | 272 | _this.object.lookAt( _this.target ); 273 | 274 | if ( lastPosition.distanceToSquared( _this.object.position ) > 0 ) { 275 | 276 | _this.dispatchEvent( changeEvent ); 277 | 278 | lastPosition.copy( _this.object.position ); 279 | 280 | } 281 | 282 | }; 283 | 284 | this.reset = function () { 285 | 286 | _state = STATE.NONE; 287 | _prevState = STATE.NONE; 288 | 289 | _this.target.copy( _this.target0 ); 290 | _this.object.position.copy( _this.position0 ); 291 | _this.object.up.copy( _this.up0 ); 292 | 293 | _eye.subVectors( _this.object.position, _this.target ); 294 | 295 | _this.object.lookAt( _this.target ); 296 | 297 | _this.dispatchEvent( changeEvent ); 298 | 299 | lastPosition.copy( _this.object.position ); 300 | 301 | }; 302 | 303 | // listeners 304 | 305 | function keydown( event ) { 306 | 307 | if ( _this.enabled === false ) return; 308 | 309 | window.removeEventListener( 'keydown', keydown ); 310 | 311 | _prevState = _state; 312 | 313 | if ( _state !== STATE.NONE ) { 314 | 315 | return; 316 | 317 | } else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && !_this.noRotate ) { 318 | 319 | _state = STATE.ROTATE; 320 | 321 | } else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && !_this.noZoom ) { 322 | 323 | _state = STATE.ZOOM; 324 | 325 | } else if ( event.keyCode === _this.keys[ STATE.PAN ] && !_this.noPan ) { 326 | 327 | _state = STATE.PAN; 328 | 329 | } 330 | 331 | } 332 | 333 | function keyup( event ) { 334 | 335 | if ( _this.enabled === false ) return; 336 | 337 | _state = _prevState; 338 | 339 | window.addEventListener( 'keydown', keydown, false ); 340 | 341 | } 342 | 343 | function mousedown( event ) { 344 | 345 | if ( _this.enabled === false ) return; 346 | 347 | event.preventDefault(); 348 | event.stopPropagation(); 349 | 350 | if ( _state === STATE.NONE ) { 351 | 352 | _state = event.button; 353 | 354 | } 355 | 356 | if ( _state === STATE.ROTATE && !_this.noRotate ) { 357 | 358 | _rotateStart = _rotateEnd = _this.getMouseProjectionOnBall( event.clientX, event.clientY ); 359 | 360 | } else if ( _state === STATE.ZOOM && !_this.noZoom ) { 361 | 362 | _zoomStart = _zoomEnd = _this.getMouseOnScreen( event.clientX, event.clientY ); 363 | 364 | } else if ( _state === STATE.PAN && !_this.noPan ) { 365 | 366 | _panStart = _panEnd = _this.getMouseOnScreen( event.clientX, event.clientY ); 367 | 368 | } 369 | 370 | document.addEventListener( 'mousemove', mousemove, false ); 371 | document.addEventListener( 'mouseup', mouseup, false ); 372 | 373 | } 374 | 375 | function mousemove( event ) { 376 | 377 | if ( _this.enabled === false ) return; 378 | 379 | event.preventDefault(); 380 | event.stopPropagation(); 381 | 382 | if ( _state === STATE.ROTATE && !_this.noRotate ) { 383 | 384 | _rotateEnd = _this.getMouseProjectionOnBall( event.clientX, event.clientY ); 385 | 386 | } else if ( _state === STATE.ZOOM && !_this.noZoom ) { 387 | 388 | _zoomEnd = _this.getMouseOnScreen( event.clientX, event.clientY ); 389 | 390 | } else if ( _state === STATE.PAN && !_this.noPan ) { 391 | 392 | _panEnd = _this.getMouseOnScreen( event.clientX, event.clientY ); 393 | 394 | } 395 | 396 | } 397 | 398 | function mouseup( event ) { 399 | 400 | if ( _this.enabled === false ) return; 401 | 402 | event.preventDefault(); 403 | event.stopPropagation(); 404 | 405 | _state = STATE.NONE; 406 | 407 | document.removeEventListener( 'mousemove', mousemove ); 408 | document.removeEventListener( 'mouseup', mouseup ); 409 | 410 | } 411 | 412 | function mousewheel( event ) { 413 | 414 | if ( _this.enabled === false ) return; 415 | 416 | event.preventDefault(); 417 | event.stopPropagation(); 418 | 419 | var delta = 0; 420 | 421 | if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9 422 | 423 | delta = event.wheelDelta / 40; 424 | 425 | } else if ( event.detail ) { // Firefox 426 | 427 | delta = - event.detail / 3; 428 | 429 | } 430 | 431 | _zoomStart.y += delta * 0.01; 432 | 433 | } 434 | 435 | function touchstart( event ) { 436 | 437 | if ( _this.enabled === false ) return; 438 | 439 | switch ( event.touches.length ) { 440 | 441 | case 1: 442 | _state = STATE.TOUCH_ROTATE; 443 | _rotateStart = _rotateEnd = _this.getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 444 | break; 445 | 446 | case 2: 447 | _state = STATE.TOUCH_ZOOM; 448 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 449 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 450 | _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy ); 451 | break; 452 | 453 | case 3: 454 | _state = STATE.TOUCH_PAN; 455 | _panStart = _panEnd = _this.getMouseOnScreen( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 456 | break; 457 | 458 | default: 459 | _state = STATE.NONE; 460 | 461 | } 462 | 463 | } 464 | 465 | function touchmove( event ) { 466 | 467 | if ( _this.enabled === false ) return; 468 | 469 | event.preventDefault(); 470 | event.stopPropagation(); 471 | 472 | switch ( event.touches.length ) { 473 | 474 | case 1: 475 | _rotateEnd = _this.getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 476 | break; 477 | 478 | case 2: 479 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 480 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 481 | _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy ) 482 | break; 483 | 484 | case 3: 485 | _panEnd = _this.getMouseOnScreen( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 486 | break; 487 | 488 | default: 489 | _state = STATE.NONE; 490 | 491 | } 492 | 493 | } 494 | 495 | function touchend( event ) { 496 | 497 | if ( _this.enabled === false ) return; 498 | 499 | switch ( event.touches.length ) { 500 | 501 | case 1: 502 | _rotateStart = _rotateEnd = _this.getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 503 | break; 504 | 505 | case 2: 506 | _touchZoomDistanceStart = _touchZoomDistanceEnd = 0; 507 | break; 508 | 509 | case 3: 510 | _panStart = _panEnd = _this.getMouseOnScreen( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 511 | break; 512 | 513 | } 514 | 515 | _state = STATE.NONE; 516 | 517 | } 518 | 519 | this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); 520 | 521 | this.domElement.addEventListener( 'mousedown', mousedown, false ); 522 | 523 | this.domElement.addEventListener( 'mousewheel', mousewheel, false ); 524 | this.domElement.addEventListener( 'DOMMouseScroll', mousewheel, false ); // firefox 525 | 526 | this.domElement.addEventListener( 'touchstart', touchstart, false ); 527 | this.domElement.addEventListener( 'touchend', touchend, false ); 528 | this.domElement.addEventListener( 'touchmove', touchmove, false ); 529 | 530 | window.addEventListener( 'keydown', keydown, false ); 531 | window.addEventListener( 'keyup', keyup, false ); 532 | 533 | this.handleResize(); 534 | 535 | }; 536 | 537 | THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 538 | -------------------------------------------------------------------------------- /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 |= mask; 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 | --------------------------------------------------------------------------------