content/hifi-content/Experiences/LoadTest/LoudApp/V4/loudApp.js
2022-02-13 23:16:46 +01:00

865 lines
27 KiB
JavaScript

// userStore = {
// UUID: {
// audioLevel,
// audioAccumulated,
// audioAvg,
// overlayID
// velocity:
// previousPosition
// filter
// avgDistance
// avgVelocity
// displayName: isNewEntry ? data.displayName : previousInfo.displayName
// username,
// overlayID: overlayID,
// uuid:
// audioLoudness: 0.21,
// previousGain: null,
// previousPosition:,
// isToggled
// }
// }
(function () {
// velocity constants
var samplingLength = 100;
var userStore = {};
var interval = null;
var isAppActive = false,
isTabletUIOpen = false;
var RADIUS = 10;
var muteList = [];
var isRadiusOnly = true;
var selectedUserUUID;
// status of our app
var isListening = false,
activeTargetUUID = null;
var DEBUG = true;
var COLOR_IN_LIST = { red: 255, blue: 255, green: 255 };
var COLOR_SELECTED = { red: 255, blue: 0, green: 255 };
var UPDATE_INTERVAL_TIME = 150;
var GAIN_TIMEOUT = 50;
var GAIN_TIMEOUT_MULTIPLIER = 1550;
var button;
var buttonName = "SEEK LOUD";
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"),
APP_URL = Script.resolvePath('./Tablet/Loud_Tablet.html?v2'), // TODO
ACTIVE_ICON_URL = Script.resolvePath('./icons/LoudIcon.svg'),
ICON_URL = Script.resolvePath('./icons/LoudIcon_White.svg'),
EVENT_BRIDGE_OPEN_MESSAGE = "eventBridgeOpen";
var LISTEN_TOGGLE = "listen_toggle",
SET_ACTIVE_MESSAGE = "setActive",
CLOSE_DIALOG_MESSAGE = "closeDialog",
SELECT_AVATAR = "selectAvatar",
BAN = "ban",
MUTE = "mute",
REFRESH = "refresh",
GOTO = "goto",
// not finished REMOVE = "remove",
UPDATE_UI = "update_ui",
MUTE_GAIN = -60,
LISTEN_GAIN = 0;
var settings = {
users: [],
ui: {}
};
// audio constants
var AVERAGING_RATIO = 0.05;
var LOUDNESS_FLOOR = 11.0;
var LOUDNESS_SCALE = 2.8 / 5.0;
var LOG2 = Math.log(2.0);
var AUDIO_PEAK_DECAY = 0.02;
var audio = {
update: function (uuid) {
var user = userStore[uuid];
if (!user) {
return;
}
// scale audio
function scaleAudio(val) {
var audioLevel = 0.0;
if (val <= LOUDNESS_FLOOR) {
audioLevel = val / LOUDNESS_FLOOR * LOUDNESS_SCALE;
} else {
audioLevel = (val - (LOUDNESS_FLOOR - 1)) * LOUDNESS_SCALE;
}
if (audioLevel > 1.0) {
audioLevel = 1;
}
return audioLevel;
}
// the VU meter should work similarly to the one in AvatarInputs: log scale, exponentially averaged
// But of course it gets the data at a different rate, so we tweak the averaging ratio and frequency
// of updating (the latter for efficiency too).
var audioLevel = 0.0;
var avgAudioLevel = 0.0;
// var user = userStore[uuid]; // userStore[uuid].accumulated level or live
if (user) {
// we will do exponential moving average by taking some the last loudness and averaging
user.accumulatedLevel = AVERAGING_RATIO * (user.accumulatedLevel || 0) + (1 - AVERAGING_RATIO) * (user.audioLoudness);
// add 1 to insure we don't go log() and hit -infinity. Math.log is
// natural log, so to get log base 2, just divide by ln(2).
audioLevel = scaleAudio(Math.log(user.accumulatedLevel + 1) / LOG2);
// decay avgAudioLevel
avgAudioLevel = Math.max((1 - AUDIO_PEAK_DECAY) * (user.avgAudioLevel || 0), audioLevel).toFixed(3);
}
userStore[uuid].audioLevel = audioLevel;
userStore[uuid].avgAudioLevel = avgAudioLevel;
},
mute: function (uuid) {
Script.setTimeout(function(){
Users.setAvatarGain(uuid, MUTE_GAIN);
// Users.setAvatarGain(oldTargetUUID, MUTE_GAIN);
}, Math.random() * GAIN_TIMEOUT_MULTIPLIER + GAIN_TIMEOUT);
},
unmute: function (uuid) {
Script.setTimeout(function(){
Users.setAvatarGain(uuid, LISTEN_GAIN);
// Users.setAvatarGain(oldTargetUUID, MUTE_GAIN);
}, Math.random() * GAIN_TIMEOUT_MULTIPLIER + GAIN_TIMEOUT);
},
listenToAvatar: function (targetUUID) {
if (isListening) {
// mute old target
var oldTargetUUID = activeTargetUUID;
var oldTarget = userStore[oldTargetUUID];
oldTarget.isToggled = false;
this.mute(oldTargetUUID);
// unmute new target
activeTargetUUID = targetUUID;
var newTarget = userStore[activeTargetUUID];
newTarget.isToggled = true;
var muteListIndex = muteList.indexOf(oldTargetUUID);
this.unmute(activeTargetUUID);
return;
}
isListening = true;
activeTargetUUID = targetUUID;
settings.ui.isListening = true;
var newRadiusList = lists.getAvatarsInRadius(RADIUS); // Object.keys(userStore);
for (var i = 0; i < newRadiusList.length; i++) {
var uuid = newRadiusList[i];
var muteListIndex = muteList.indexOf(uuid);
if (muteListIndex === -1) {
muteList.push(uuid);
}
}
for (var i = 0; i < settings.users.length; i++) {
var uuid = settings.users[i].uuid;
var muteListIndex = muteList.indexOf(uuid);
if (muteListIndex === -1) {
muteList.push(uuid);
}
}
// console.log("LENGTH: ", muteList.length);
// var avatarsInRadius = ** ROBIN
for (var i = 0; i < muteList.length; i++) {
var uuid = muteList[i];
var user = userStore[uuid];
var isTarget = targetUUID === uuid;
if (isTarget) {
user.isToggled = true;
// user.previousGain = Users.getAvatarGain(uuid);
} else {
// not target avatar
// mute
// user.previousGain = Users.getAvatarGain(uuid);
user.isToggled = false;
this.mute(uuid);
}
}
},
resetListenToAvatar: function () {
isListening = false;
activeTargetUUID = null;
// *** var allAvatars = Object.keys(userStore);
for (var i = 0; i < muteList.length; i++) {
userStore[muteList[i]].isToggled = false;
this.unmute(muteList[i]);
}
muteList = [];
}
};
var lists = {
// currently not used
getAvatarsInRadius: function (radius) {
return AvatarList.getAvatarsInRange(MyAvatar.position, radius).filter(function (uuid) {
return uuid !== MyAvatar.sessionUUID;
});
},
// returns an array of avatarPaldata
// Example of returned: [{"audioLoudness":0,"isReplicated":false,"palOrbOffset":0.2948298454284668,"position":{"x":0.5748982429504395,"y":-10.898207664489746,"z":2.4195659160614014},"sessionDisplayName":"Robin","sessionUUID":""}]
allAvatars: function () {
return AvatarList.getPalData().data;
},
getIndexOfSettingsUser: function (uuid) {
if (settings.users.length) {
var index = settings.users.map(function (item) {
// print(item.uuid)
return item.uuid;
}).indexOf(uuid);
return index;
}
}
};
var app = {
setup: function () {
button = tablet.addButton({
text: buttonName,
icon: ICON_URL,
activeIcon: ACTIVE_ICON_URL,
isActive: isAppActive
});
if (button) {
button.clicked.connect(this.onTabletButtonClicked);
} else {
console.error("ERROR: Tablet button not created! App not started.");
tablet = null;
return;
}
tablet.gotoHomeScreen();
tablet.screenChanged.connect(this.onTabletScreenChanged);
AvatarList.avatarAddedEvent.connect(userUtils.addUser);
AvatarList.avatarRemovedEvent.connect(userUtils.removeUser);
Users.usernameFromIDReply.connect(userUtils.setUserName);
updateInterval.start();
// button.clicked.connect(this.toggleOnOff);
},
onTabletButtonClicked: function () {
// Application tablet/toolbar button clicked.
if (isTabletUIOpen) {
tablet.gotoHomeScreen();
} else {
// Initial button active state is communicated via URL parameter so that active state is set immediately without
// waiting for the event bridge to be established.
tablet.gotoWebScreen(APP_URL + "?active=" + isAppActive);
}
},
doUIUpdate: function (update) {
tablet.emitScriptEvent(JSON.stringify({
type: UPDATE_UI,
value: settings,
update: update || {}
}));
},
setAppActive: function (active) {
// print("SETUP APP ACTIVE");
// Start/stop application activity.
if (active) {
// print("Start app");
// TODO: Start app activity.
} else {
// print("Stop app");
// TODO: Stop app activity.
// TODO PAUSE?????
// updateInterval.stop();
// go through taking all data from live and setting into userStore
}
// isAppActive = active;
},
onTabletScreenChanged: function (type, url) {
// Tablet screen changed / desktop dialog changed.
var wasTabletUIOpen = isTabletUIOpen;
isTabletUIOpen = url.substring(0, APP_URL.length) === APP_URL; // Ignore URL parameter.
if (isTabletUIOpen === wasTabletUIOpen) {
return;
}
if (isTabletUIOpen) {
button.editProperties({ isActive: true });
tablet.webEventReceived.connect(webEvent.recieved);
} else {
// setUIUpdating(false);
button.editProperties({ isActive: false });
tablet.webEventReceived.disconnect(webEvent.recieved);
}
},
sortData: function () {
function sortNumber(a, b) {
return b.avgAudioLevel - a.avgAudioLevel;
}
// remove previous overlays
for (var i = 0; i < settings.users.length; i++) {
var user = settings.users[i];
var uuid = user.uuid;
overlays.deleteOverlay(uuid);
}
var avatarsInRadius = lists.getAvatarsInRadius(RADIUS);
settings.users = avatarsInRadius.map(function (uuid) { return userStore[uuid]; });
settings.users = settings.users.sort(sortNumber).slice(0, 10);
// add new overlays
for (var i = 0; i < settings.users.length; i++) {
var user = settings.users[i];
var uuid = user.uuid;
overlays.addOverlayToUser(uuid);
}
},
unload: function () {
if (isAppActive) {
this.setAppActive(false);
}
if (isTabletUIOpen) {
tablet.webEventReceived.disconnect(webEvent.recieved);
}
if (button) {
// print("UNLOAD");
button.clicked.connect(this.onTabletButtonClicked);
tablet.removeButton(button);
button = null;
}
if (settings.users) {
settings.users.forEach(function (user) {
if (user.overlayID) {
overlays.deleteOverlay(user.overlayID)
}
});
}
audio.resetListenToAvatar();
Users.usernameFromIDReply.disconnect(userUtils.setUserName);
AvatarList.avatarAddedEvent.disconnect(userUtils.addUser);
AvatarList.avatarRemovedEvent.disconnect(userUtils.removeUser);
tablet = null;
}
};
var overlays = {
addOverlayToUser: function (uuid) {
var user = userStore[uuid];
var overlayProperties = {
position: user.currentPosition, // assigned on creation
dimensions: { x: 0.3, y: 0.3, z: 0.3 },
solid: true,
parentID: uuid, // assigned on creation
color: COLOR_IN_LIST,
drawInFront: true
};
var overlayID = Overlays.addOverlay("sphere", overlayProperties);
user.overlayID = overlayID;
},
deleteOverlay: function (uuid) {
var user = userStore[uuid];
Overlays.deleteOverlay(user.overlayID);
user.overlayID = null;
},
selectUser: function (uuid) {
var user = userStore[uuid];
if(selectedUserUUID) {
this.deselectUser(selectedUserUUID);
}
Overlays.editOverlay(user.overlayID, { color: COLOR_SELECTED });
user.isSelected = true;
selectedUserUUID = user.uuid;
},
deselectUser: function (uuid) {
var user = userStore[uuid];
user.isSelected = false;
Overlays.editOverlay(user.overlayID, { color: COLOR_IN_LIST });
selectedUserUUID = null;
}
}
var velocity = {
update: function (uuid) {
var user = userStore[uuid];
if (!user.previousPosition) {
user.previousPosition = user.currentPosition;
return;
}
var distance = Vec3.distance(user.previousPosition, user.currentPosition);
user.avgDistance = +user.distanceFilter.process(distance).toFixed(3);
user.previousPosition = user.currentPosition;
}
}
var updateInterval = {
start: function () {
interval = Script.setInterval(this.handleUpdate, UPDATE_INTERVAL_TIME);
},
stop: function () {
if (interval) {
Script.clearInterval(interval);
}
},
handleUpdate: function () {
// var palList;
// if (isRadiusOnly) {
// var list = lists.getAvatarsInRadius(RADIUS);
// palList = [];
// for(var i = 0; i < list.length; i++) {
// if (MyAvatar.sessionUUID !== list[i]){
// var avatarInfo = AvatarList.getAvatar(list[i]);
// }
// palList.push(avatarInfo);
// }
// } else {
// palList = lists.allAvatars();
// }
var palList = lists.allAvatars();
if (isListening) {
// refresh mute list with avatars in range
var list = lists.getAvatarsInRadius(RADIUS);
for (var i = 0; i < list.length; i++) {
var uuid = list[i];
if (muteList.indexOf(uuid) === -1) {
muteList.push(uuid);
audio.mute(uuid);
}
}
}
// Add users to userStore
for (var a = 0; a < palList.length; a++) {
var user = palList[a];
var uuid = palList[a].sessionUUID;
var hasUUID = uuid;
var isInUserStore = userStore[uuid] !== undefined;
if (hasUUID && !isInUserStore) {
//print("ADDED USER TO USERSTORE");
userUtils.addUser(uuid);
} else if (hasUUID) {
//print("UPDATE AUDIO", uuid, JSON.stringify(userStore[uuid]));
userStore[uuid].audioLoudness = user.audioLoudness;
userStore[uuid].currentPosition = user.position;
audio.update(uuid);
// VELOCITY
// velocity.update(uuid);
// if (userStore[uuid].avgDistance > 1) { // 1 moving over a meter a ~second
// overlays.addOverlayToUser(uuid);
// userStore[uuid].hasMovedFast = true;
// var index = lists.getIndexOfSettingsUser(uuid);
// if (index === -1) {
// settings.users.push(userStore[uuid]);
// }
// }
}
}
// Remove users from userStore
for (var uuid in userStore) {
// if user crashes, leaving domain signal will not be called
// handle this case
var hasUUID = uuid;
var isInNewList = palList.map(function (item) {
return item.sessionUUID;
}).indexOf(uuid) !== -1;
if (hasUUID && !isInNewList) {
delete userStore[uuid];
removeUserFromSettingsUser(uuid);
}
}
app.doUIUpdate();
}
};
function removeUserFromSettingsUser(uuid) {
// print("REMOVE USER FROM SETTINGS");
var settingsUsersListIndex = lists.getIndexOfSettingsUser(uuid);
var muteListIndex = muteList.indexOf(uuid);
if (settingsUsersListIndex !== -1) {
if (settings.users[settingsUsersListIndex].overlayID) {
overlays.deleteOverlay(uuid);
// userStore[uuid].hasMovedFast = true;
}
settings.users.splice(settingsUsersListIndex, 1);
app.doUIUpdate();
}
if (muteListIndex !== -1) {
muteList.splice(muteListIndex, 1);
}
}
var webEvent = {
recieved: function (data) {
// EventBridge message from HTML script.
var message;
try {
message = JSON.parse(data);
} catch (e) {
return;
}
switch (message.type) {
case EVENT_BRIDGE_OPEN_MESSAGE:
// print("OPEN EVENTBRIDGE");
app.sortData();
app.doUIUpdate();
break;
case SET_ACTIVE_MESSAGE:
// print("Event recieved: ", SET_ACTIVE_MESSAGE);
if (isAppActive !== message.value) {
// button.editProperties({
// isActive: message.value
// });
app.setAppActive(message.value);
}
// tablet.gotoHomeScreen(); // Automatically close app.
break;
case LISTEN_TOGGLE:
// print("Event recieved: ", LISTEN_TOGGLE);
handleEvent.listenToggle(message.value);
app.doUIUpdate();
break;
case SELECT_AVATAR:
// print("Event recieved: ", BAN);
handleEvent.selectAvatar(message.value);
app.doUIUpdate();
break;
case REFRESH:
// print("Event recieved: ", REFRESH);
handleEvent.refresh();
break;
case GOTO:
// print("Event recieved: ", GOTO);
handleEvent.goto(message.value);
break;
case BAN:
// print("Event recieved: ", BAN);
handleEvent.ban(message.value);
app.doUIUpdate();
break;
case MUTE:
// print("Event recieved: ", MUTE);
handleEvent.mute(message.value);
app.doUIUpdate();
break;
case CLOSE_DIALOG_MESSAGE:
if (settings.users) {
settings.users.forEach(function (user) {
if (user.overlayID) {
overlays.deleteOverlay(user.uuid)
}
});
}
// print("CLOSE_DIALOGUE");
tablet.gotoHomeScreen();
break;
default:
// print("DEFAULT Event recieved: ", SET_ACTIVE_MESSAGE);
break;
}
},
};
function AveragingFilter(length) {
// initialise the array of past values
this.pastValues = [];
for (var i = 0; i < length; i++) {
this.pastValues.push(0);
}
// single arg is the nextInputValue
this.process = function () {
if (this.pastValues.length === 0 && arguments[0]) {
return arguments[0];
} else if (arguments[0] !== null) {
// console.log(this.pastValues)
this.pastValues.push(arguments[0]);
// console.log(this.pastValues)
this.pastValues.shift();
// console.log(this.pastValues)
var nextOutputValue = 0;
for (var value in this.pastValues) nextOutputValue += this.pastValues[value];
return nextOutputValue / this.pastValues.length;
} else {
return 0;
}
};
};
var filter = (function () {
return {
createAveragingFilter: function (length) {
var newAveragingFilter = new AveragingFilter(length);
return newAveragingFilter;
}
};
})();
function User(uuid, displayName, initialGain) {
this.uuid = uuid;
this.displayName = displayName;
this.audioLevel = 0;
this.audioAccumulated = 0;
this.audioAvg = 0;
this.userName = null;
this.overlayID = null;
this.audioLoudness = 0;
this.previousGain = 0;
this.isToggled = false;
this.previousPosition = null;
this.currentPosition = null;
this.currentDistance = null;
this.distanceFilter = filter.createAveragingFilter(samplingLength);
this.avgDistance = 0;
this.hasMovedFast = false;
this.isSelected = false;
}
var userUtils = {
setUserName: function (uuid, userName) {
userStore[uuid].userName = userName;
if (lists.getIndexOfSettingsUser(uuid) !== -1) {
app.doUIUpdate();
}
},
addUser: function (sessionUUID) {
var avatarData = AvatarList.getAvatar(sessionUUID);
if (!userStore[sessionUUID]) {
// if (isListening) {
// initialGain = MUTE_GAIN;
// Script.setTimeout(function(){
// Users.setAvatarGain(sessionUUID, MUTE_GAIN);
// // Users.setAvatarGain(oldTargetUUID, MUTE_GAIN);
// }, Math.random() * GAIN_TIMEOUT_MULTIPLIER + GAIN_TIMEOUT)
// }
userStore[sessionUUID] = new User(sessionUUID, avatarData.displayName, LISTEN_GAIN);
Users.requestUsernameFromID(sessionUUID);
}
// this.prepSettingsUser();
},
removeUser: function (sessionUUID) {
if (userStore[sessionUUID]) {
delete userStore[sessionUUID];
}
removeUserFromSettingsUser(sessionUUID);
// this.prepSettingsUser();
}
}
var handleEvent = {
// removeUser: function (avatarInfo) {
// var uuid = avatarInfo.uuid;
// removeUserFromSettingsUser(uuid);
// },
selectAvatar: function (avatarInfo) {
var uuid = avatarInfo.uuid;
var userPosition = avatarInfo.currentPosition;
var orientationTowardsUser = Quat.cancelOutRollAndPitch(Quat.lookAtSimple(MyAvatar.position, userPosition));
MyAvatar.orientation = orientationTowardsUser;
if (selectedUserUUID === uuid) {
overlays.deselectUser(uuid);
} else {
overlays.selectUser(uuid);
}
},
goto: function (avatarInfo) {
var uuid = avatarInfo.uuid;
// MyAvatar.position = avatarInfo.currentPosition;
var userOrientation = AvatarList.getAvatar(uuid).orientation;
var offset = Vec3.multiplyQbyV(userOrientation, { x: 0, y: 0.2, z: 1.5 });
var newPosition = Vec3.sum(avatarInfo.currentPosition, offset);
MyAvatar.position = newPosition;
MyAvatar.orientation = userOrientation;
},
ban: function (avatarInfo) {
Users.kick(avatarInfo.uuid);
},
listenToggle: function (avatarInfo) {
// print("LISTEN TOGGLE ", avatarInfo.uuid !== activeTargetUUID, JSON.stringify(avatarInfo));
if (avatarInfo.uuid !== activeTargetUUID) {
audio.listenToAvatar(avatarInfo.uuid);
} else {
audio.resetListenToAvatar();
}
},
refresh: function() {
app.sortData();
app.doUIUpdate();
audio.resetListenToAvatar();
muteList = [];
},
mute: function (avatarInfo) {
// print("MUTED");
Users.mute(avatarInfo.uuid);
}
};
function scriptEnding() {
updateInterval.stop();
app.unload();
}
app.setup();
updateInterval.start();
Script.scriptEnding.connect(scriptEnding);
})();