Advertisement
wadsonlucas

Untitled

Jun 25th, 2018
158
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.91 KB | None | 0 0
  1. var clamp = function(val, min, max){ return Math.min(Math.max(min, val), max); }
  2.  
  3. var is_playing = function(media)
  4. {
  5. return media.currentTime > 0 && !media.paused && !media.ended && media.readyState > 2;
  6. }
  7.  
  8. function AudioEngine()
  9. {
  10. this.c = new AudioContext();
  11. //choose processor buffer size (2^(8-14))
  12. this.processor_buffer_size = Math.pow(2, clamp(Math.floor(Math.log(this.c.sampleRate*0.1)/Math.log(2)), 8, 14));
  13.  
  14. this.sources = {};
  15. this.listener = this.c.listener;
  16. this.listener.upX.value = 0;
  17. this.listener.upY.value = 0;
  18. this.listener.upZ.value = 1;
  19.  
  20. this.last_check = new Date().getTime();
  21.  
  22. //VoIP
  23. this.voice_indicator_div = document.createElement("div");
  24. this.voice_indicator_div.id = "voice_indicator";
  25. document.body.appendChild(this.voice_indicator_div);
  26.  
  27. this.voice_channels = {};
  28.  
  29. var _this = this;
  30.  
  31. libopus.onload = function(){
  32. //encoder
  33. _this.mic_enc = new libopus.Encoder(1,48000,24000,true);
  34. }
  35. if(libopus.loaded) //force loading if already loaded
  36. libopus.onload();
  37.  
  38. //processor
  39. //prepare process function
  40. var processOut = function(peers, samples){
  41. //convert to Int16 pcm
  42. var isamples = new Int16Array(samples.length);
  43. for(var i = 0; i < samples.length; i++){
  44. var s = samples[i];
  45. s *= 32768 ;
  46. if(s > 32767)
  47. s = 32767;
  48. else if(s < -32768)
  49. s = -32768;
  50.  
  51. isamples[i] = s;
  52. }
  53.  
  54. //encode
  55. _this.mic_enc.input(isamples);
  56. var data;
  57. while(data = _this.mic_enc.output()){ //generate packets
  58. var buffer = data.slice().buffer;
  59.  
  60. //send packet to active/connected peers
  61. for(var i = 0; i < peers.length; i++){
  62. try{
  63. peers[i].data_channel.send(buffer);
  64. }catch(e){
  65. console.log("vRP-VoIP send error to player "+peers[i].player);
  66. }
  67. }
  68. }
  69. }
  70.  
  71.  
  72. this.mic_processor = this.c.createScriptProcessor(this.processor_buffer_size,1,1);
  73. this.mic_processor.onaudioprocess = function(e){
  74. var buffer = e.inputBuffer;
  75.  
  76. var peers = [];
  77. //prepare list of active/connected peers
  78. for(var nchannel in _this.voice_channels){
  79. var channel = _this.voice_channels[nchannel];
  80. for(var player in channel){
  81. if(player != "_config"){
  82. var peer = channel[player];
  83. if(peer.connected && peer.active)
  84. peers.push(peer);
  85. }
  86. }
  87. }
  88.  
  89. if(peers.length > 0){
  90. //resample to 48kHz if necessary
  91. if(buffer.sampleRate != 48000){
  92. var ratio = 48000/buffer.sampleRate;
  93. var oac = new OfflineAudioContext(1,Math.floor(ratio*buffer.length),48000);
  94. var sbuff = oac.createBufferSource();
  95. sbuff.buffer = buffer;
  96. sbuff.connect(oac.destination);
  97. sbuff.start();
  98.  
  99. oac.startRendering().then(function(out_buffer){
  100. processOut(peers, out_buffer.getChannelData(0));
  101. });
  102. }
  103. else
  104. processOut(peers, buffer.getChannelData(0));
  105. }
  106.  
  107. //silent output
  108. var out = e.outputBuffer.getChannelData(0);
  109. for(var k = 0; k < out.length; k++)
  110. out[k] = 0;
  111. }
  112.  
  113. this.mic_processor.connect(this.c.destination); //make the processor running
  114.  
  115. //mic stream
  116. navigator.mediaDevices.getUserMedia({
  117. audio: {
  118. autoGainControl: false,
  119. echoCancellation: false,
  120. noiseSuppression: false,
  121. latency: 0
  122. }
  123. }).then(function(stream){
  124. _this.mic_node = _this.c.createMediaStreamSource(stream);
  125. _this.mic_comp = _this.c.createDynamicsCompressor();
  126. _this.mic_node.connect(_this.mic_comp);
  127. _this.mic_comp.connect(_this.mic_processor);
  128. //_this.mic_comp.connect(_this.c.destination);
  129. });
  130.  
  131. this.player_positions = {};
  132. }
  133.  
  134. AudioEngine.prototype.setListenerData = function(data)
  135. {
  136. var l = this.listener;
  137. l.positionX.value = data.x;
  138. l.positionY.value = data.y;
  139. l.positionZ.value = data.z;
  140. l.forwardX.value = data.fx;
  141. l.forwardY.value = data.fy;
  142. l.forwardZ.value = data.fz;
  143.  
  144. var time = new Date().getTime();
  145. if(time-this.last_check >= 2000){ // every 2s
  146. this.last_check = time;
  147.  
  148. // pause too far away sources and unpause nearest sources paused
  149. for(var name in this.sources){
  150. var source = this.sources[name];
  151.  
  152. if(source[3]){ //spatialized
  153. var dx = data.x-source[2].positionX.value;
  154. var dy = data.y-source[2].positionY.value;
  155. var dz = data.z-source[2].positionZ.value;
  156. var dist = Math.sqrt(dx*dx+dy*dy+dz*dz);
  157. var active_dist = source[2].maxDistance*2;
  158.  
  159. if(!is_playing(source[0]) && dist <= active_dist)
  160. source[0].play();
  161. else if(is_playing(source[0]) && dist > active_dist)
  162. source[0].pause();
  163. }
  164. }
  165. }
  166. }
  167.  
  168. // return [audio, node, panner]
  169. AudioEngine.prototype.setupAudioSource = function(data)
  170. {
  171. var audio = new Audio();
  172. audio.src = data.url;
  173. audio.volume = data.volume;
  174.  
  175. var spatialized = (data.x != null && data.y != null && data.z != null && data.max_dist != null);
  176. var node = null;
  177. var panner = null;
  178.  
  179. if(spatialized){
  180. node = this.c.createMediaElementSource(audio);
  181.  
  182. panner = this.c.createPanner();
  183. // panner.panningModel = "HRTF";
  184. panner.distanceModel = "inverse";
  185. panner.refDistance = 1;
  186. panner.maxDistance = data.max_dist;
  187. panner.rolloffFactor = 1;
  188. panner.coneInnerAngle = 360;
  189. panner.coneOuterAngle = 0;
  190. panner.coneOuterGain = 0;
  191. panner.positionX.value = data.x;
  192. panner.positionY.value = data.y;
  193. panner.positionZ.value = data.z;
  194.  
  195. node.connect(panner);
  196. panner.connect(this.c.destination);
  197. }
  198.  
  199. return [audio, node, panner, spatialized];
  200. }
  201.  
  202. AudioEngine.prototype.playAudioSource = function(data)
  203. {
  204. var _this = this;
  205.  
  206. var spatialized = (data.x != null && data.y != null && data.z != null && data.max_dist != null);
  207. var dist = 10;
  208. var active_dist = 0;
  209.  
  210. if(spatialized){
  211. var dx = this.listener.positionX.value-data.x;
  212. var dy = this.listener.positionY.value-data.y;
  213. var dz = this.listener.positionZ.value-data.z;
  214. dist = Math.sqrt(dx*dx+dy*dy+dz*dz);
  215. active_dist = data.max_dist*2;
  216. }
  217.  
  218. if(!spatialized || dist <= active_dist){
  219. var source = this.setupAudioSource(data);
  220.  
  221. // bind deleter
  222. if(spatialized){
  223. source[0].onended = function(){
  224. source[2].disconnect(_this.c.destination);
  225. }
  226. }
  227.  
  228. // play
  229. source[0].play();
  230. }
  231. }
  232.  
  233. AudioEngine.prototype.setAudioSource = function(data)
  234. {
  235. this.removeAudioSource(data);
  236.  
  237. var source = this.setupAudioSource(data);
  238. source[0].loop = true;
  239. this.sources[data.name] = source;
  240.  
  241. // play
  242. var dist = 10;
  243. var active_dist = 0;
  244. if(source[3]){ // spatialized
  245. var dx = this.listener.positionX.value-source[2].positionX.value;
  246. var dy = this.listener.positionY.value-source[2].positionY.value;
  247. var dz = this.listener.positionZ.value-source[2].positionZ.value;
  248. dist = Math.sqrt(dx*dx+dy*dy+dz*dz);
  249. active_dist = source[2].maxDistance*2;
  250. }
  251.  
  252. if(!source[3] || dist <= active_dist)
  253. source[0].play();
  254. }
  255.  
  256. AudioEngine.prototype.removeAudioSource = function(data)
  257. {
  258. var source = this.sources[data.name];
  259. if(source){
  260. delete this.sources[data.name];
  261. source[0].src = "";
  262. source[0].loop = false;
  263. if(is_playing(source[0]))
  264. source[0].pause();
  265. if(source[3]) //spatialized
  266. source[2].disconnect(this.c.destination);
  267. }
  268. }
  269.  
  270. //VoIP
  271.  
  272. AudioEngine.prototype.setPeerConfiguration = function(data)
  273. {
  274. this.peer_config = data.config;
  275. }
  276.  
  277. AudioEngine.prototype.setPlayerPositions = function(data)
  278. {
  279. this.player_positions = data.positions;
  280.  
  281. //update panners (spatialization effect)
  282. for(var nchannel in this.voice_channels){
  283. var channel = this.voice_channels[nchannel];
  284. for(var player in channel){
  285. if(player != "_config"){
  286. var peer = channel[player];
  287. if(peer.panner){
  288. var pos = data.positions[player];
  289. if(pos){
  290. peer.panner.positionX.value = pos[0];
  291. peer.panner.positionY.value = pos[1];
  292. peer.panner.positionZ.value = pos[2];
  293. }
  294. }
  295. }
  296. }
  297. }
  298. }
  299.  
  300. AudioEngine.prototype.setupPeer = function(peer)
  301. {
  302. var _this = this;
  303.  
  304. //decoder
  305. peer.dec = new libopus.Decoder(1,48000);
  306. peer.psamples = []; //packets samples
  307. peer.processor = this.c.createScriptProcessor(this.processor_buffer_size,0,1);
  308. peer.processor.onaudioprocess = function(e){
  309. var out = e.outputBuffer.getChannelData(0);
  310.  
  311. //feed samples to output
  312. var nsamples = 0;
  313. var i = 0;
  314. while(nsamples < out.length && i < peer.psamples.length){
  315. var p = peer.psamples[i];
  316. var take = Math.min(p.length, out.length-nsamples);
  317.  
  318. //write packet samples to output
  319. for(var k = 0; k < take; k++){
  320. out[nsamples+k] = p[k];
  321. }
  322.  
  323. //advance
  324. nsamples += take;
  325.  
  326. if(take < p.length){ //partial samples
  327. //add rest packet
  328. peer.psamples.splice(i+1,0,p.subarray(take));
  329. }
  330.  
  331. i++;
  332. }
  333.  
  334. //remove processed packets
  335. peer.psamples.splice(0,i);
  336.  
  337. //silent last samples
  338. for(var k = nsamples; k < out.length; k++)
  339. out[k] = 0;
  340. }
  341.  
  342.  
  343. //add peer effects
  344. var node = peer.processor;
  345. var config = this.getChannel(peer.channel)._config || {};
  346. var effects = config.effects || {};
  347.  
  348. if(effects.spatialization){ //spatialization
  349. var panner = this.c.createPanner();
  350. panner.distanceModel = effects.spatialization.dist_model || "inverse";
  351. panner.refDistance = 1;
  352. panner.maxDistance = effects.spatialization.max_dist;
  353. panner.rolloffFactor = effects.spatialization.rolloff || 1;
  354. panner.coneInnerAngle = 360;
  355. panner.coneOuterAngle = 0;
  356. panner.coneOuterGain = 0;
  357.  
  358. var pos = this.player_positions[peer.player];
  359. if(pos){
  360. panner.positionX.value = pos[0];
  361. panner.positionY.value = pos[1];
  362. panner.positionZ.value = pos[2];
  363. }
  364.  
  365. peer.panner = panner;
  366.  
  367. node.connect(panner);
  368. node = panner;
  369. }
  370.  
  371. //connect final node
  372. peer.final_node = node;
  373. node.connect(config.in_node || this.c.destination); //connect to channel node or destination
  374.  
  375. //setup data channel (UDP-like)
  376. peer.data_channel = peer.conn.createDataChannel(peer.channel, {
  377. ordered: false,
  378. negotiated: true,
  379. maxRetransmits: 0,
  380. id: 0
  381. });
  382. peer.data_channel.binaryType = "arraybuffer";
  383.  
  384. peer.data_channel.onopen = function(){
  385. $.post("http://vrp/audio",JSON.stringify({act: "voice_connected", player: peer.player, channel: peer.channel, origin: peer.origin}));
  386. peer.connected = true;
  387. }
  388.  
  389. peer.data_channel.onclose = function(){
  390. $.post("http://vrp/audio",JSON.stringify({act: "voice_disconnected", player: peer.player, channel: peer.channel}));
  391. _this.disconnectVoice({channel: peer.channel, player: peer.player});
  392. }
  393.  
  394. peer.data_channel.onmessage = function(e){
  395. if(peer.dec){
  396. //receive opus packet
  397. peer.dec.input(new Uint8Array(e.data));
  398. var data;
  399. while(data = peer.dec.output()){
  400. //create buffer from samples
  401. var buffer = _this.c.createBuffer(1, data.length, 48000);
  402. var samples = buffer.getChannelData(0);
  403.  
  404. for(var k = 0; k < data.length; k++){
  405. //convert from int16 to float
  406. var s = data[k];
  407. s /= 32768 ;
  408. if(s > 1)
  409. s = 1;
  410. else if(s < -1)
  411. s = -1;
  412.  
  413. samples[k] = s;
  414. }
  415.  
  416. //resample to AudioContext samplerate if necessary
  417. if(_this.c.sampleRate != 48000){
  418. var ratio = _this.c.sampleRate/48000;
  419. var oac = new OfflineAudioContext(1,Math.floor(ratio*buffer.length),_this.c.sampleRate);
  420. var sbuff = oac.createBufferSource();
  421. sbuff.buffer = buffer;
  422. sbuff.connect(oac.destination);
  423. sbuff.start();
  424.  
  425. oac.startRendering().then(function(out_buffer){
  426. peer.psamples.push(out_buffer.getChannelData(0));
  427. });
  428. }
  429. else
  430. peer.psamples.push(samples);
  431. }
  432. }
  433. }
  434.  
  435. //ice
  436. peer.conn.onicecandidate = function(e){
  437. $.post("http://vrp/audio",JSON.stringify({act: "voice_peer_signal", player: peer.player, data: {channel: peer.channel, candidate: e.candidate}}));
  438. }
  439. }
  440.  
  441. AudioEngine.prototype.getChannel = function(channel)
  442. {
  443. var r = this.voice_channels[channel];
  444. if(!r){
  445. r = {};
  446. this.voice_channels[channel] = r;
  447. }
  448.  
  449. return r;
  450. }
  451.  
  452. AudioEngine.prototype.connectVoice = function(data)
  453. {
  454. //close previous peer
  455. this.disconnectVoice(data);
  456.  
  457. var channel = this.getChannel(data.channel);
  458.  
  459. //setup new peer
  460. var peer = {
  461. conn: new RTCPeerConnection(this.peer_config),
  462. channel: data.channel,
  463. player: data.player,
  464. origin: true,
  465. candidate_queue: []
  466. }
  467. channel[data.player] = peer;
  468.  
  469. //create data channel
  470. this.setupPeer(peer);
  471.  
  472. //SDP
  473. peer.conn.createOffer().then(function(sdp){
  474. $.post("http://vrp/audio",JSON.stringify({act: "voice_peer_signal", player: data.player, data: {channel: data.channel, sdp_offer: sdp}}));
  475. peer.conn.setLocalDescription(sdp);
  476. });
  477. }
  478.  
  479. AudioEngine.prototype.disconnectVoice = function(data)
  480. {
  481. var channel = this.getChannel(data.channel);
  482. var config = channel._config || {};
  483.  
  484. var players = [];
  485. if(data.player != null)
  486. players.push(data.player);
  487. else{ //add all players
  488. for(var player in channel){
  489. if(player != "_config")
  490. players.push(player);
  491. }
  492. }
  493.  
  494. //close peers
  495. for(var i = 0; i < players.length; i++){
  496. var player = players[i];
  497. var peer = channel[player];
  498. if(peer){
  499. if(peer.data_channel)
  500. peer.data_channel.close();
  501. if(peer.conn.connectionState != "closed")
  502. peer.conn.close();
  503. if(peer.final_node) //disconnect from channel node or destination
  504. peer.final_node.disconnect(config.in_node || this.c.destination);
  505. if(peer.dec){
  506. peer.dec.destroy();
  507. delete peer.dec;
  508. }
  509. }
  510.  
  511. delete channel[player];
  512. }
  513.  
  514. //update indicator
  515. this.updateVoiceIndicator();
  516. }
  517.  
  518. AudioEngine.prototype.voicePeerSignal = function(data)
  519. {
  520. var channel = this.getChannel(data.data.channel);
  521. if(data.data.candidate){ //candidate
  522. var peer = channel[data.player];
  523. if(peer){
  524. if(peer.initialized) //valid remote description
  525. peer.conn.addIceCandidate(new RTCIceCandidate(data.data.candidate));
  526. else if(peer.candidate_queue)
  527. peer.candidate_queue.push(new RTCIceCandidate(data.data.candidate));
  528. }
  529. }
  530. else if(data.data.sdp_offer){ //offer
  531. //disconnect peer
  532. this.disconnectVoice({channel: data.data.channel, player: data.player});
  533.  
  534. //setup answer peer
  535. var peer = {
  536. conn: new RTCPeerConnection(this.peer_config),
  537. channel: data.data.channel,
  538. player: data.player
  539. }
  540.  
  541. channel[data.player] = peer;
  542. this.setupPeer(peer);
  543.  
  544. //SDP
  545. peer.conn.setRemoteDescription(data.data.sdp_offer);
  546. peer.initialized = true;
  547. peer.conn.createAnswer().then(function(sdp){
  548. $.post("http://vrp/audio",JSON.stringify({act: "voice_peer_signal", player: data.player, data: {channel: data.data.channel, sdp_answer: sdp}}));
  549. peer.conn.setLocalDescription(sdp);
  550. });
  551. }
  552. else if(data.data.sdp_answer){ //answer
  553. var peer = channel[data.player];
  554. if(peer){
  555. peer.conn.setRemoteDescription(data.data.sdp_answer);
  556. peer.initialized = true;
  557. //add candidates
  558. for(var i = 0; i < peer.candidate_queue.length; i++)
  559. peer.conn.addIceCandidate(peer.candidate_queue[i]);
  560. peer.candidate_queue = [];
  561. }
  562. }
  563. }
  564.  
  565. AudioEngine.prototype.setVoiceState = function(data)
  566. {
  567. var channel = this.getChannel(data.channel);
  568. if(data.player != null){ //specific player
  569. var peer = channel[data.player];
  570. if(peer)
  571. peer.active = data.active;
  572. }
  573. else{ //entire channel
  574. for(var player in channel){
  575. if(player != "_config")
  576. channel[player].active = data.active;
  577. }
  578. }
  579.  
  580. //update indicator
  581. this.updateVoiceIndicator();
  582. }
  583.  
  584. AudioEngine.prototype.configureVoice = function(data)
  585. {
  586. var channel = this.getChannel(data.channel);
  587. if(!channel._config)
  588. channel._config = data.config; //bind config
  589.  
  590. var config = data.config;
  591. var effects = config.effects || {};
  592.  
  593.  
  594. var node = null;
  595.  
  596. //build channel effects
  597. if(effects.biquad){ //biquad filter
  598. var biquad = this.c.createBiquadFilter();
  599. if(effects.biquad.frequency != null)
  600. biquad.frequency.value = effects.biquad.frequency;
  601. if(effects.biquad.Q != null)
  602. biquad.Q.value = effects.biquad.Q;
  603. if(effects.biquad.detune != null)
  604. biquad.detune.value = effects.biquad.detune;
  605. if(effects.biquad.gain != null)
  606. biquad.gain.value = effects.biquad.gain;
  607.  
  608. if(effects.biquad.type != null)
  609. biquad.type = effects.biquad.type;
  610.  
  611. if(node)
  612. node.connect(biquad);
  613. node = biquad;
  614. if(!config.in_node)
  615. config.in_node = node;
  616. }
  617.  
  618. if(effects.gain){ //gain
  619. var gain = this.c.createGain();
  620. if(effects.gain.gain != null)
  621. gain.gain.value = effects.gain.gain;
  622.  
  623. if(node)
  624. node.connect(gain);
  625. node = gain;
  626. if(!config.in_node)
  627. config.in_node = node;
  628. }
  629.  
  630. //connect final node to output
  631. if(node)
  632. node.connect(this.c.destination);
  633. }
  634.  
  635. AudioEngine.prototype.isVoiceActive = function()
  636. {
  637. for(var name in this.voice_channels){
  638. var channel = this.voice_channels[name];
  639. for(var player in channel){
  640. if(player != "_config"){
  641. if(channel[player].active)
  642. return true;
  643. }
  644. }
  645. }
  646.  
  647. return false;
  648. }
  649.  
  650. AudioEngine.prototype.updateVoiceIndicator = function()
  651. {
  652. if(this.isVoiceActive())
  653. this.voice_indicator_div.classList.add("active");
  654. else
  655. this.voice_indicator_div.classList.remove("active");
  656. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement