// userStore = { // UUID: { // audioLevel, // audioAccumulated, // audioAvg, // displayName: isNewEntry ? data.displayName : previousInfo.displayName // username, // overlayID: overlayID, // uuid: // audioLoudness: 0.21, // previousGain: null, // isToggled // } // } (function () { var userStore = {}; var interval = null; var isAppActive = false, isTabletUIOpen = false; // status of our app var isListening = false, activeTargetUUID = null; var DEBUG = true; var UPDATE_INTERVAL_TIME = 120; 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", BAN = "ban", MUTE = "mute", UPDATE_UI = "update_ui", MUTE_GAIN = -60; var settings = { users: dummyData, ui: {} }, dummyData = [ { userName: 'robin', uuid: "{3f10b637-03df-45ca-9918-8e215f51b4ed}", currentLoudness: 1.5, currentGain: 0, isToggled: false }, { userName: 'milad', uuid: "{8d713c85-baf9-4f08-abdf-afc7346940e3}", currentLoudness: 0.21, currentGain: 20, isToggled: false }, { userName: '2343', uuid: "fart{8d713c85-baf9-4f08-abdf-afc7346940e3}", currentLoudness: 0.21, currentGain: 20, isToggled: false } ]; // 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; }, listenToAvatar: function (targetUUID) { if (isListening) { // mute old target var oldTargetUUID = activeTargetUUID; var oldTarget = userStore[oldTargetUUID]; oldTarget.isToggled = false; Users.setAvatarGain(oldTargetUUID, MUTE_GAIN); // unmute new target activeTargetUUID = targetUUID; var newTarget = userStore[activeTargetUUID]; newTarget.isToggled = true; Users.setAvatarGain(targetUUID, newTarget.previousGain); return; } isListening = true; activeTargetUUID = targetUUID; settings.ui.isListening = true; var allAvatars = Object.keys(userStore); for (var i = 0; i < allAvatars.length; i++) { var uuid = allAvatars[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; Users.setAvatarGain(uuid, MUTE_GAIN); } } }, resetListenToAvatar: function () { isListening = false; activeTargetUUID = null; var allAvatars = Object.keys(userStore); for (var i = 0; i < allAvatars.length; i++) { var uuid = allAvatars[i]; var user = userStore[uuid]; user.isToggled = false; Users.setAvatarGain(uuid, user.previousGain); } } }; var lists = { // currently not used getAvatarsInRadius: function (radius) { return AvatarList.getAvatarsInRange(MyAvatar.position, radius); }, // 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; }, }; 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; button.editProperties({ isActive: 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) { tablet.webEventReceived.connect(webEvent.recieved); } else { // setUIUpdating(false); tablet.webEventReceived.disconnect(webEvent.recieved); } }, sortData: function () { function sortNumber(a, b) { return b.avgAudioLevel - a.avgAudioLevel; } settings.users = Object.keys(userStore).reduce(function (prev, cur) { var obj = userStore[cur]; prev.push(obj); return prev; }, []); print("LIST1:", JSON.stringify(settings.users)); settings.users = settings.users.sort(sortNumber).slice(0,10); print("LIST2:", JSON.stringify(settings.users)); }, 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; } Users.usernameFromIDReply.disconnect(userUtils.setUserName); AvatarList.avatarAddedEvent.disconnect(userUtils.addUser); AvatarList.avatarRemovedEvent.disconnect(userUtils.removeUser); tablet = null; } }; var updateInterval = { start: function () { interval = Script.setInterval(this.handleUpdate, UPDATE_INTERVAL_TIME); }, stop: function () { if (interval) { Script.clearInterval(interval); } }, handleUpdate: function () { // *** PERFORMANCE OPTIMIZATION HERE var palList = lists.allAvatars(); // 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; if (!isListening) { userStore[uuid].previousGain = Users.getAvatarGain(uuid); } audio.update(uuid); } } // Remove users from userStore for (var uuid in userStore) { var hasUUID = uuid; var isInNewList = palList.map(function (item) { return item.sessionUUID; }).indexOf(uuid) !== -1; if (hasUUID && !isInNewList) { delete userStore[uuid]; // *** Halp checkTopTenLeft(uuid); } } // 10 LOUDEST // settings.users = Object.keys(userStore).reduce(function (prev, cur) { // var obj = userStore[cur]; // prev.push(obj); // return prev; // }, []); app.doUIUpdate(); // go through the userStore // run audio.update // Cull the 10 loudest // Set or Reset Overlay < --- when we get the 10 loudest, we are looking at them on the tablet // Create the settings.user for the tablet // doUiUpdate } }; function checkTopTenLeft (uuid) { print(JSON.stringify(settings.users)) // remove from settings.users var settingsUsersListIndex = settings.users.map(function (item) { print(item.uuid) return item.uuid; }).indexOf(uuid); print("INDEX:", settingsUsersListIndex); if (settingsUsersListIndex !== -1) { print("remove user"); settings.users.splice(settingsUsersListIndex, 1); app.doUIUpdate(); } } 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 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: print("CLOSE_DIALOGUE"); tablet.gotoHomeScreen(); break; default: print("DEFAULT Event recieved: ", SET_ACTIVE_MESSAGE); break; } }, }; 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; } var userUtils = { setUserName: function (uuid, userName) { userStore[uuid].userName = userName; }, addUser: function (sessionUUID) { var avatarData = AvatarList.getAvatar(sessionUUID); if (!userStore[sessionUUID]) { var initialGain = 0; if (isListening) { initialGain = MUTE_GAIN; Users.setAvatarGain(sessionUUID, MUTE_GAIN); } userStore[sessionUUID] = new User(sessionUUID, avatarData.displayName, initialGain); Users.requestUsernameFromID(sessionUUID); } // this.prepSettingsUser(); }, removeUser: function (sessionUUID) { if (userStore[sessionUUID]) { delete userStore[sessionUUID]; } checkTopTenLeft(sessionUUID); // this.prepSettingsUser(); }, } var handleEvent = { 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); print("1"); } else { audio.resetListenToAvatar(); print("2"); } }, mute: function (avatarInfo) { print("MUTED"); Users.mute(avatarInfo.uuid); } }; function scriptEnding() { updateInterval.stop(); app.unload(); } app.setup(); updateInterval.start(); Script.scriptEnding.connect(scriptEnding); })();