// 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); })();