forked from luiscuenca/Spatial-Audio-API-Examples
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.html
602 lines (587 loc) · 34.8 KB
/
index.html
1
2
3
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>HiFi Audio API Example: Tracks</title>
<link rel="shortcut icon" type="image/x-icon" href="images/favicon.ico"/>
</head>
<body style="width: 100%; height: 100%; margin: 0; padding: 0;">
<div class="console" style="z-index: 1; background-color: #efefef; padding: 10px; position: fixed; left: 0; top: 60px; width: 260px; font-family: monospace; font-size: 12px;">
<div class="console-log" style="margin:2px; border-radius: 5px; padding: 5px; border: 2px solid transparent; display: block; width: fit-content;"></div>
</div>
<canvas class="thecanvas" style="position:fixed; right: 0; top: 0;"></canvas>
<p class="example-description" style="position: fixed; left: 0; width: 100%; text-align: center;">Click the connect button to setup the audio nodes.<br>Move the nodes around to perceive spatialization</p>
<button class="triggerButton" style="cursor: pointer; font-size: 18px; background-color: #000000; color: white; width: 280px; top: 0; left: 0; height: 60px; margin: 0; position: fixed;"></button>
<script src="https://hifi-spatial-audio-api.s3-us-west-2.amazonaws.com/releases/latest/HighFidelityAudio-latest.js"></script>
<script src="https://hifi-spatial-audio-api.s3-us-west-2.amazonaws.com/releases/latest/HighFidelityControls-latest.js"></script>
<script>
// Conversion constant since we use radians to draw on the canvas but we are setting degrees on the API
const RADIANS_TO_DEGREES = 57.2958;
// Connect to the High Fidelity Audio Spatial API Server by supplying your own JWT here.
// Follow this guide to get a JWT: https://www.highfidelity.com/api/guides/misc/getAJWT
// If you don't need a guide, obtain JWT credentials after signing up for a developer account at https://account.highfidelity.com/dev/account
const HIFI_AUDIO_JWT = "";
const SoundNodeType = {
NODE: 0, // Basic node
EMITTER: 1, // Sends a stream from a audio file
RECEIVER: 2 // Receives the mix and play locally
}
// Some helper functions to simplify drawing calls and changes in coordenates
class CanvasHelper {
static finishPath(ctx, color, width) {
ctx[(width !== undefined) ? "strokeStyle" : "fillStyle"] = color;
ctx[(width !== undefined) ? "lineWidth" : ""] = width;
ctx[(width !== undefined) ? "stroke" : "fill"].call(ctx);
}
// Compact function to render circles filled or stroked
static renderCircle(ctx, x, y, radius, from, to, color, width) {
ctx.beginPath();
ctx.arc(x, y, radius, from, to, false);
CanvasHelper.finishPath(ctx, color, width);
}
// Compact function to render polygons filled or stroked
static renderPolygon(ctx, points, color, width) {
ctx.beginPath();
points.forEach((point, i) => {
ctx[i === 0 ? "moveTo" : "lineTo"].call(ctx, point[0], point[1]);
});
CanvasHelper.finishPath(ctx, color, width);
}
// Render text
static renderText(ctx, x, y, text, color, size, family) {
family = family ? family : "console";
ctx.font = `${size}px ${family}`;
ctx.fillStyle = color;
ctx.fillText(text, x, y);
}
// World coordenates to canvas conversion
static worldToCanvasCoords(canvasWidth, canvasHeight, coords, offset, zoom) {
let relPos = { x: coords.x - offset.x, y: coords.y - offset.y };
let canvasPos = { x: 0.5 * canvasWidth - relPos.x / zoom, y: 0.5 * canvasHeight - relPos.y / zoom };
return canvasPos;
}
// Canvas coordenates to world conversion
static canvasToWorldCoords(canvasWidth, canvasHeight, coords, offset, zoom) {
let newLocPos = { x: (coords.x - 0.5 * canvasWidth) * zoom, y: (coords.y - 0.5 * canvasHeight) * zoom, z: 0.0 };
let npos = { x: offset.x - newLocPos.x, y: offset.y - newLocPos.y, z: offset.z - newLocPos.z };
return npos;
}
}
// Root class with the node's physical attributes needed to display it
class Renderable2D {
constructor(config) {
this.position = Object.assign({}, config.position);
this.orientation = config.orientation; // radians
this.radius = config.radius;
this.name = config.name;
this.color = config.color;
this.selected = false;
this.hover = false;
}
// Simple point-circle collision detection for node selection.
isPointInside(worldPoint) {
let distVec = { x: worldPoint.x - this.position.x, y: worldPoint.y - this.position.y };
let distance = Math.sqrt(distVec.x * distVec.x + distVec.y * distVec.y);
return distance < this.radius;
}
// Get a point located one unit in front of node.
getLookingAtPoint() {
let angle = this.orientation - 0.5 * Math.PI;
return { x: this.position.x - Math.cos(angle), y: this.position.y - Math.sin(angle) };
}
// Get the absolute lookat angle for a point in the plane
getLookAtAngle(targetPos) {
let dir = { x: targetPos.x - this.position.x, y: targetPos.y - this.position.y };
return Math.atan2(dir.y, dir.x) - 0.5 * Math.PI;
}
// Render loop. Child classes can implement their own method
render(canvas, ctx, offset, zoom) {
let canvasPos = CanvasHelper.worldToCanvasCoords(canvas.scrollWidth, canvas.scrollHeight, this.position, offset, zoom);
// Apply transform
ctx.translate(canvasPos.x, canvasPos.y);
ctx.rotate(this.orientation)
if (this.hover || this.selected) { // Render the selection/hover effect
this.renderSelect(ctx, canvasPos, zoom);
}
this.renderNode(ctx, canvasPos, zoom); // Call the renderNode function
// Undo transform
ctx.rotate(-this.orientation);
ctx.translate(-canvasPos.x, -canvasPos.y);
}
// Default method to render basic nodes
renderNode(ctx, position, zoom) {
CanvasHelper.renderCircle(ctx, 0, 0, this.radius / zoom, 0, 2.0 * Math.PI, this.color );
}
// Default method to render selection/hover effect
renderSelect(ctx, position, zoom) {
CanvasHelper.renderCircle(ctx, 0, 0, (this.radius / zoom) + 4, 0, 2.0 * Math.PI, this.selected ? '#FF0000' : '#CCCCCC', 4);
}
}
// Simple class to handle the node's connection, position and orientation
class SoundNode extends Renderable2D {
constructor(config) {
super(config);
// Create the API's position and orientation objects that will be sent to the mixer
this.mixerPosition = new HighFidelityAudio.Point3D({ x: -this.position.x, y: this.position.z, z: -this.position.y });
this.mixerOrientation = new HighFidelityAudio.OrientationEuler3D({ "pitchDegrees": 0, "yawDegrees": -RADIANS_TO_DEGREES * this.orientation, "rollDegrees": 0 });
this.hifiCommunicator = null; // HighFidelityAudio.HiFiCommunicator
this.stream = null; // Input or output stream
this.type = SoundNodeType.NODE;
this.volume = null; // Value with the volume from mixer in decibels.
this.connectResponse = null;
}
// If the node is connected, its id will be the visit id hash provided by the server
getId() {
return this.connectResponse && this.connectResponse.success ? this.connectResponse.audionetInitResponse.visit_id_hash : null;
}
// This function receives position ({x, y, z}) and orientation (radians), updates the renderable2D and soundNode values and send it.
updateData() {
// We need to convert the position sent to the mixer
this.mixerPosition.x = -this.position.x;
this.mixerPosition.y = this.position.z;
this.mixerPosition.z = -this.position.y;
this.mixerOrientation.yawDegrees = -RADIANS_TO_DEGREES * this.orientation;
this.sendUpdatedData();
}
// Send the converted position and orientation to the mixer
sendUpdatedData(name) {
if (this.hifiCommunicator) {
let response = this.hifiCommunicator.updateUserDataAndTransmit({
position: this.mixerPosition,
orientationEuler: this.mixerOrientation
});
}
}
// Volume data can be used to render a sound bubble effect on the node
updateReceivedData(data) {
this.volume = data.volumeDecibels !== null ? data.volumeDecibels : this.volume;
}
// Notify connection changes for debugging
onConnectionStateChanged(newConnectionState) {
console.log(`New High Fidelity connection for: ${this.name} state: ${newConnectionState}`);
}
// Connect to the server using a valid space token
async connect() {
console.log(`Connecting Receiver: ` + this.name + ` to High Fidelity Audio API Servers...`);
// Setup the communicator
this.hifiCommunicator = new HighFidelityAudio.HiFiCommunicator({
initialHiFiAudioAPIData: new HighFidelityAudio.HiFiAudioAPIData({
position: this.mixerPosition,
orientationEuler: this.mixerOrientation
}),
onConnectionStateChanged: this.onConnectionStateChanged.bind(this), // Subscribe to connection changes
});
if (this.stream) { // The stream can be valid at this point if it has been set previously by children
await this.hifiCommunicator.setInputAudioMediaStream(this.stream, false);
}
try {
let stackURLOverride = new URLSearchParams(location.search).get("stack");
this.connectResponse = await this.hifiCommunicator.connectToHiFiAudioAPIServer(HIFI_AUDIO_JWT, stackURLOverride);
console.log(`Call to \`connectToHiFiAudioAPIServer()\` for: ${this.name} succeeded! Response:\n${JSON.stringify(this.connectResponse)}`);
return this.connectResponse.success;
} catch (e) {
console.error(`Call to \`connectToHiFiAudioAPIServer()\` for: ${this.name} failed! Error:\n${e}`);
this.connectResponse = null;
return false;
}
}
// Disconnect from the server
async disconnect() {
console.log(`Disconnecting Emitter: ${this.name} from High Fidelity Audio API Servers...`);
let disconnectStatus = await this.hifiCommunicator.disconnectFromHiFiAudioAPIServer();
this.connectResponse = null;
console.log(`Disconnected status for ${this.name} : ${disconnectStatus}`);
}
}
// SoundNode with output stream only. Establish a connection just for listening
class SoundReceiver extends SoundNode {
constructor(config, onDataReceived) {
super(config);
this.type = SoundNodeType.RECEIVER;
this.onDataReceived = onDataReceived ? onDataReceived : () => {};
}
// Custom render method.
renderNode(ctx, position, zoom) {
// Create a sound cone pointing forward
CanvasHelper.renderPolygon(ctx, [[0, 0], [1000, -1000], [-1000, -1000]], "#FAFAFFAA");
// Render first 10 circles around the receiver every meter. Undo rotation first.
ctx.rotate(-this.orientation);
for (let i = 1; i < 11; i++) {
let radius = i / zoom;
CanvasHelper.renderCircle(ctx, 0, 0, radius, 0, 2.0 * Math.PI, this.color + "AA", 1);
CanvasHelper.renderText(ctx, radius, 0, `${i}m`, this.color, 15);
}
ctx.rotate(this.orientation);
super.renderNode(ctx, position, zoom);
}
// Custom connection method. After successfully connected we setup the output audio to play the server mix locally.
async connect() {
if (await super.connect()) {
let outputAudioElem = document.createElement('audio');
outputAudioElem.srcObject = this.hifiCommunicator.getOutputAudioMediaStream();
// We must call `play()` here because certain browsers won't autoplay this stream as we expect.
outputAudioElem.play();
// This will get only volume updates for all Users (including ourselves).
let userDataSubscription = new HighFidelityAudio.UserDataSubscription({
"components": [ HighFidelityAudio.AvailableUserDataSubscriptionComponents.VolumeDecibels ],
"callback": (data) => { this.onDataReceived(data); }
});
this.hifiCommunicator.addUserDataSubscription(userDataSubscription);
return true;
}
return false;
}
}
// Helper class to handle the creation of a stream object from an audio file.
class FileAudioStream {
constructor(gain) {
this.stream = null; // Stream object
this.mediaElement = null; // Media element used to control the stream
this.fileName = null; // File id to avoid unnecessary reloads
this.gain = gain; // Volume of the input stream
}
async createStreamFromAudioFile(filename, onFinishPlaying) {
if (this.fileName === filename) {
resolve(this.mediaElement);
}
let loadAudioFile = new Promise(resolve => {
let audioElem = document.createElement('audio');
audioElem.setAttribute('src', filename);
audioElem.addEventListener("loadeddata", (e) => {
resolve(e.target);
});
audioElem.addEventListener('ended',function(){
if (onFinishPlaying) {
onFinishPlaying();
}
}.bind(this), false);
});
// Wait until the audio file is loaded
this.mediaElement = await loadAudioFile;
// Create a media element from audio context
var ac = new AudioContext();
let audioElementSource = ac.createMediaElementSource(this.mediaElement);
var dest = ac.createMediaStreamDestination();
// Setup a gain control
var volume = ac.createGain();
volume.connect(dest);
volume.gain.value = this.gain;
audioElementSource.connect(volume);
// Return the input stream
this.stream = dest.stream;
return this.stream;
}
}
// SoundNode with input stream from an audio file. Used to just send the audio file data from it's position and orientation
class SoundEmitter extends SoundNode {
constructor(config, gain) {
super(config); // config : {position, orientation, name, radius, color}
this.type = SoundNodeType.EMITTER;
this.fileStream = new FileAudioStream(gain);
this.emitting = false;
}
// Set the input audio stream once the file is loaded
async prepareAudioFile(fileName, onFinishedEmitting) {
this.stream = await this.fileStream.createStreamFromAudioFile(fileName, () => { // on audio 'ended' event fired
this.emitting = false;
if (onFinishedEmitting) {
onFinishedEmitting(); // Send the event to child classes
}
});
await this.hifiCommunicator.setInputAudioMediaStream(this.stream, false);
}
// Set the input audio stream and play it once it loaded
async prepareAndPlayAudioFile(fileName, onFinishedEmitting) {
await this.prepareAudioFile(fileName, onFinishedEmitting);
this.emit();
}
// Play the audio file
emit() {
this.fileStream.mediaElement.play();
this.emitting = true;
}
}
// This class is used to customize the SoundEmitter render method
class Speaker extends SoundEmitter {
constructor(config, gain) {
super(config, gain);
}
// Override renderNode function to for custom rendering for speakers
renderNode(ctx, position, zoom) {
let volumeRatio = ((this.volume ? this.volume : -120.0) + 120.0) / 120.0; // Normalize volume
let radius = (this.radius / zoom); // Get radius on pixels
let wRadius = radius * 0.3; // Emitter surface width
let hRadius = radius * 0.6; // Emitter surface height
let xRadius = (1.0 + 0.3 * volumeRatio) * hRadius; // White surface width with offset
let vRadius = (1.0 + volumeRatio) * 1.2 * hRadius; // Volume ratio offset
// Render a circular area for the volume bubble affect
CanvasHelper.renderCircle(ctx, 0, 0, volumeRatio * 1.2 * radius, 0, 2.0 * Math.PI, this.color + "22");
// Render a circular area as a base
CanvasHelper.renderCircle(ctx, 0, 0, 0.6 * radius, 0, 2.0 * Math.PI, this.color + "EE");
// White surface slightly offset. Area based on the volumeRatio
CanvasHelper.renderPolygon(ctx, [[-wRadius, hRadius],[-xRadius, -vRadius],[xRadius, -vRadius], [wRadius, hRadius]], "#FFFFFF");
// Colored surface. Area based on the volumeRatio
CanvasHelper.renderPolygon(ctx, [[-wRadius, hRadius],[-hRadius, -vRadius],[hRadius, -vRadius], [wRadius, hRadius]], this.color + "55");
}
}
// Class to initiate all necessary objects and manage UI and control events
class App {
constructor(config) {
this.config = config;
this.soundNodes = {};
this.receiverId = null;
this.zoomAmount = 0.01; // Define the scale for how the world is displayed on the canvas
this.selectedNodeId = null;
this.hoveredNodeId = null;
this.canvas = null;
this.triggerButton = null;
this.consoleElement = null;
// To add/remove event listener propertly and with the right context we need to bind them first into variables
this.connectNodesBinded = this.connectNodes.bind(this);
this.disconnectNodesBinded = this.disconnectNodes.bind(this);
}
// Connect the UI elements that trigger the example and and display information
setupUI(canvas, triggerButton, consoleElement) {
this.canvas = canvas;
this.triggerButton = triggerButton;
this.consoleElement = consoleElement;
this.triggerButton.addEventListener("click", this.connectNodesBinded, false);
this.triggerButton.innerHTML = `Click to Connect`;
window.addEventListener('resize', this.onResizeCanvas.bind(this), false);
window.addEventListener('load', this.onResizeCanvas.bind(this), false);
// Setup the canvas to receive control events
let hifiControls = new HighFidelityControls.HiFiControls({ mainAppElement: this.canvas });
hifiControls.onLeftDrag = this.onLeftDrag.bind(this);
hifiControls.onRightDrag = this.onRightDrag.bind(this);
hifiControls.onCanvasMove = this.onCanvasMove.bind(this);
hifiControls.onCanvasDown = this.onCanvasDown.bind(this);
// Start the rendering loop
this.redraw()
}
// To avoid desync on replay when we use audioElement.loop = true we need to make
// sure that all tracks start playing at the same time.
checkAllFinishedAndReplay() {
let allFinished = true;
Object.keys(this.soundNodes).forEach(id => {
let node = this.soundNodes[id];
if (node.type === SoundNodeType.EMITTER) {
allFinished = !node.emitting && allFinished;
}
});
if (allFinished) { // If the emmitters are done playing we replay all at the same time
Object.keys(this.soundNodes).forEach(id => {
let node = this.soundNodes[id];
if (node.type === SoundNodeType.EMITTER) {
node.emit();
}
});
}
}
async connectNodes() {
// Clear console
this.displayLog("");
// Configuration data for creating all nodes. The name of the emmiters are used to build the path to the audio file
let nodeData = [
{name: "Listener", position: this.config.SPAWN_POINT, color: "#AFAAFF", orientation: 0, radius: this.config.RECEIVER_RADIUS, type: SoundNodeType.RECEIVER},
{name: "accompaniment", color: "#AFAFAF", orientation: 0, radius: this.config.EMITTER_RADIUS, type: SoundNodeType.EMITTER},
{name: "Vox1", color: "#FF00FF", orientation: 0, radius: this.config.EMITTER_RADIUS, type: SoundNodeType.EMITTER},
{name: "arp1", color: "#FFA0FF", orientation: 0, radius: this.config.EMITTER_RADIUS, type: SoundNodeType.EMITTER},
{name: "Vox2", color: "#00FFFF", orientation: 0, radius: this.config.EMITTER_RADIUS, type: SoundNodeType.EMITTER},
{name: "arp2", color: "#03AFFF", orientation: 0, radius: this.config.EMITTER_RADIUS, type: SoundNodeType.EMITTER},
{name: "Vox3", color: "#0000FF", orientation: 0, radius: this.config.EMITTER_RADIUS, type: SoundNodeType.EMITTER},
{name: "arp3", color: "#00A0AF", orientation: 0, radius: this.config.EMITTER_RADIUS, type: SoundNodeType.EMITTER},
{name: "Vox4", color: "#0F0FAF", orientation: 0, radius: this.config.EMITTER_RADIUS, type: SoundNodeType.EMITTER},
{name: "arp4", color: "#0AAFAF", orientation: 0, radius: this.config.EMITTER_RADIUS, type: SoundNodeType.EMITTER}
];
// Set triggerButton on waiting mode
this.triggerButton.disabled = true;
this.triggerButton.innerHTML = `wait...`;
// Create and connect nodes based on previous configuration
for (let i = 0; i < nodeData.length; i++) {
let node = null;
switch (nodeData[i].type) {
case SoundNodeType.EMITTER:
node = new Speaker(nodeData[i], this.config.TRACKS_GAIN); // Create Speaker
// Prepare the audio file
node.prepareAudioFile(`./audio/${nodeData[i].name}.mp3`, () => {
this.checkAllFinishedAndReplay(); // When 'ended' event is fired we check that all emitters are done before replay
});
// Set the emmiter position around the listener
let angle = 2.0 * Math.PI * ((i - 1) / (nodeData.length - 1));
node.position = {x: this.config.SPAWN_POINT.x + Math.cos(angle), y: this.config.SPAWN_POINT.y + Math.sin(angle), z: this.config.SPAWN_POINT.z};
node.orientation = node.getLookAtAngle(this.config.SPAWN_POINT); // Look at Listener
break;
case SoundNodeType.RECEIVER:
// When the receiver gets data from the server we update all nodes. Currently used to get volumen data
let onDataReceived = (dataArray) => {
dataArray.forEach(data => {
if (this.soundNodes[data.hashedVisitID]) {
this.soundNodes[data.hashedVisitID].updateReceivedData(data);
}
});
}
node = new SoundReceiver(nodeData[i], onDataReceived); // Create Listener
break;
default:
break;
}
if (node) {
if (await node.connect()) { // If connected succesfully we add the node to the array and collect receiver
let nodeId = node.getId();
this.displayLog(`Node "${nodeData[i].name}" connected.`, `${nodeId}`, (e) => {
this.selectNode(e.target.dataset.log);
});
this.soundNodes[nodeId] = node;
if (node.type === SoundNodeType.RECEIVER) {
this.receiverId = nodeId;
}
} else {
this.displayLog(`Node "${nodeData[i].name}" error connecting.`);
}
}
}
// Start the emitters for the first time
this.checkAllFinishedAndReplay();
// Configure triggerButton for disconnection
this.triggerButton.disabled = false;
this.triggerButton.innerHTML = `Disconnect`;
this.triggerButton.removeEventListener('click', this.connectNodesBinded, false);
this.triggerButton.addEventListener('click', this.disconnectNodesBinded, false);
this.displayLog(`Emitters playing!`);
}
// Finish the application by disconnecting all nodes
async disconnectNodes() {
// Clear console
this.displayLog("");
// Set triggerButton on waiting mode
this.triggerButton.disabled = true;
this.triggerButton.innerHTML = `wait...`;
let ids = Object.keys(this.soundNodes);
for (let i = 0; i < ids.length; i++) {
let id = ids[i];
await this.soundNodes[id].disconnect();
this.displayLog(`Node "${this.soundNodes[id].name}" disconnected.`);
delete this.soundNodes[id];
};
// Reconfigure triggerButton for connection
this.triggerButton.disabled = false;
this.triggerButton.innerHTML = `Connect`;
this.triggerButton.removeEventListener('click', this.disconnectNodesBinded, false);
this.triggerButton.addEventListener('click', this.connectNodesBinded, false);
}
// Add log entries to the console log UI element
displayLog(text, data, onclick) {
let newLog = document.querySelector(".console-log").cloneNode(); // Make a copy of one of the logs
newLog.onclick = onclick;
if (text.length === 0) { // Clear the console log
this.consoleElement.innerHTML = "";
}
// Set the text on the copied element and add the new log entry
newLog.innerHTML = text;
if (data) {
newLog.style.cursor = "pointer";
newLog.dataset["log"] = data;
}
this.consoleElement.appendChild(newLog);
}
// Render loop
redraw() {
let ctx = this.canvas.getContext('2d');
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Call render method for all nodes
Object.keys(this.soundNodes).forEach(id => {
this.soundNodes[id].updateData();
this.soundNodes[id].render(this.canvas, ctx, this.config.SPAWN_POINT, this.zoomAmount);
});
window.requestAnimationFrame(() => { this.redraw() });
}
// Hover node using id
hoverNode(id) {
if (this.hoveredNodeId !== id) {
this.hoveredNodeId = id;
Object.keys(this.soundNodes).forEach(nodeId => {
this.soundNodes[nodeId].hover = nodeId === id;
});
this.canvas.style.cursor = this.hoveredNodeId ? "pointer" : "default";
}
}
// Select node using id
selectNode(id) {
if (this.selectedNodeId !== id) {
this.selectedNodeId = id;
Object.keys(this.soundNodes).forEach(nodeId => {
this.soundNodes[nodeId].selected = nodeId === id;
let logEntry = this.consoleElement.querySelector(`[data-log="${nodeId}"]`);
logEntry.style.borderColor = nodeId === id ? "#FF0000" : "transparent";
});
}
}
// Find the first node that intersects with a point in the canvas
findNodeOnCanvas(point, isSelection) {
// Compute world position based on the canvas point and search for a node
let worldPoint = CanvasHelper.canvasToWorldCoords(this.canvas.scrollWidth, this.canvas.scrollHeight, point, this.config.SPAWN_POINT, this.zoomAmount);
for (let [id, node] of Object.entries(this.soundNodes)) {
if (node.isPointInside(worldPoint)) {
return id;
}
}
}
// Move the selected node with a left drag
onLeftDrag(e) {
// Compute the world position based on the control event
let worldPos = CanvasHelper.canvasToWorldCoords(this.canvas.scrollWidth, this.canvas.scrollHeight, {x: e.clientX, y: e.clientY}, this.config.SPAWN_POINT, this.zoomAmount);
if (this.selectedNodeId && this.soundNodes[this.selectedNodeId]) {
let selectedNode = this.soundNodes[this.selectedNodeId];
// Update node base on the computed position
selectedNode.position = worldPos;
}
}
//Rotate the selected node with a right drag
onRightDrag(e) {
// Compute the world position based on the control event
let worldPos = CanvasHelper.canvasToWorldCoords(this.canvas.scrollWidth, this.canvas.scrollHeight, {x: e.clientX, y: e.clientY}, this.config.SPAWN_POINT, this.zoomAmount);
if (this.selectedNodeId) {
this.soundNodes[this.selectedNodeId].orientation = this.soundNodes[this.selectedNodeId].getLookAtAngle(worldPos);
}
}
// Compute hover on all nodes
onCanvasMove(e) {
let nodeId = this.findNodeOnCanvas({x: e.clientX, y: e.clientY});
this.hoverNode(nodeId);
}
// Select a node with a left click or rotate the listener with a right click
onCanvasDown(e) {
const MOUSE_RIGHT_BUTTON = 2;
if (e.button != MOUSE_RIGHT_BUTTON) {
let nodeId = this.findNodeOnCanvas({x: e.clientX, y: e.clientY});
this.selectNode(nodeId);
} else {
if (this.selectedNodeId && this.soundNodes[this.selectedNodeId] && this.soundNodes[this.selectedNodeId].type === SoundNodeType.RECEIVER) {
let targetPos = CanvasHelper.canvasToWorldCoords(this.canvas.scrollWidth, this.canvas.scrollHeight, {x: e.clientX, y: e.clientY}, this.config.SPAWN_POINT, this.zoomAmount);
this.soundNodes[this.selectedNodeId].orientation = this.soundNodes[this.selectedNodeId].getLookAtAngle(targetPos);
}
}
}
// Set the zoom according to the window size
onResizeCanvas() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
this.zoomAmount = 0.0025 * 1280 / (Math.min(this.canvas.height, this.canvas.width));
}
}
// Set the log level
HighFidelityAudio.HiFiLogger.setHiFiLogLevel(HighFidelityAudio.HiFiLogLevel.Debug);
let APP_CONFIG = {
SPAWN_POINT : {x: 0, y: 0, z: 0}, // Initial position for the receiver
TRACKS_GAIN : 0.4, // The volume sent for all tracks
EMITTER_RADIUS: 0.185, // Radius of the speakers
RECEIVER_RADIUS: 0.15 // Radius of the listener
}
let app = new App(APP_CONFIG); // Create app
let canvasElement = document.querySelector('.thecanvas');
let triggerButtonElement = document.querySelector(`.triggerButton`);
let consoleElement = document.querySelector(".console");
app.setupUI(canvasElement, triggerButtonElement, consoleElement); // Setup UI and run
</script>
</body>
</html>